diff --git a/.devcontainer/base.Dockerfile b/.devcontainer/base.Dockerfile new file mode 100644 index 00000000..ee46d3bb --- /dev/null +++ b/.devcontainer/base.Dockerfile @@ -0,0 +1,43 @@ +# syntax=docker/dockerfile:1.5 +# HugoBlox devcontainer base image + +FROM mcr.microsoft.com/devcontainers/go:1.22-bookworm + +ARG NODE_VERSION=20 +ARG PNPM_VERSION=10.14.0 +ARG HUGO_VERSION=0.152.2 + +ENV DEBIAN_FRONTEND=noninteractive +ENV PNPM_HOME=/home/vscode/.local/share/pnpm +ENV PATH=${PNPM_HOME}:/usr/local/go/bin:${PATH} + +USER root + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends ca-certificates curl gnupg; \ + NODE_MAJOR="${NODE_VERSION%%.*}"; \ + curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash -; \ + apt-get install -y --no-install-recommends nodejs; \ + corepack enable; \ + corepack prepare "pnpm@${PNPM_VERSION}" --activate; \ + mkdir -p "${PNPM_HOME}"; \ + chown -R vscode:vscode "${PNPM_HOME}"; \ + ARCH="$(uname -m)"; \ + if [ "$ARCH" = "x86_64" ] || [ "$ARCH" = "amd64" ]; then \ + HUGO_ARCH="amd64"; \ + elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then \ + HUGO_ARCH="arm64"; \ + else \ + echo "Unsupported architecture: ${ARCH}"; \ + exit 1; \ + fi; \ + HUGO_PKG="hugo_extended_${HUGO_VERSION}_Linux-${HUGO_ARCH}.tar.gz"; \ + curl -fsSL -o "/tmp/${HUGO_PKG}" "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/${HUGO_PKG}"; \ + tar -xzf "/tmp/${HUGO_PKG}" -C /tmp hugo; \ + install -m 0755 /tmp/hugo /usr/local/bin/hugo; \ + rm -rf "/tmp/${HUGO_PKG}" /tmp/hugo; \ + apt-get clean; \ + rm -rf /var/lib/apt/lists/* + +USER vscode diff --git a/.github/workflows/devcontainer-image.yml b/.github/workflows/devcontainer-image.yml new file mode 100644 index 00000000..4a240bfa --- /dev/null +++ b/.github/workflows/devcontainer-image.yml @@ -0,0 +1,85 @@ +name: Build HugoBlox devcontainer image + +on: + push: + branches: [main] + paths: + - ".devcontainer/base.Dockerfile" + - ".github/workflows/devcontainer-image.yml" + workflow_dispatch: + inputs: + hugo_versions: + description: "Comma-separated Hugo versions to build (e.g. 0.152.2,0.154.0)" + required: false + +env: + IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/hugo-blox-dev + DEFAULT_HUGO_VERSION: 0.152.2 + NODE_VERSION: 20 + PNPM_VERSION: 10.14.0 + +jobs: + prepare-matrix: + runs-on: ubuntu-latest + outputs: + versions: ${{ steps.versions.outputs.matrix }} + steps: + - name: Select Hugo versions + id: versions + run: | + VERSIONS="${{ github.event.inputs.hugo_versions || env.DEFAULT_HUGO_VERSION }}" + MATRIX=$(python - <<'PY' + import os, json + versions = [v.strip() for v in os.environ["VERSIONS"].split(",") if v.strip()] + print(json.dumps(versions)) + PY + ) + echo "matrix=${MATRIX}" >> "$GITHUB_OUTPUT" + + build-and-push: + needs: prepare-matrix + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + hugo: ${{ fromJson(needs.prepare-matrix.outputs.versions) }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compute tags + id: tags + env: + HUGO_VERSION: ${{ matrix.hugo }} + run: | + TAGS="${IMAGE_NAME}:hugo${HUGO_VERSION}" + if [ "${HUGO_VERSION}" = "${DEFAULT_HUGO_VERSION}" ]; then + TAGS="${TAGS}\n${IMAGE_NAME}:latest" + fi + { + echo "list<<'EOF'" + printf "%b\n" "$TAGS" + echo "EOF" + } >> "$GITHUB_OUTPUT" + + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: .devcontainer/base.Dockerfile + push: true + build-args: | + HUGO_VERSION=${{ matrix.hugo }} + NODE_VERSION=${{ env.NODE_VERSION }} + PNPM_VERSION=${{ env.PNPM_VERSION }} + tags: ${{ steps.tags.outputs.list }} diff --git a/scripts/release_modules.py b/scripts/release_modules.py index 0f5e978f..062d9ed4 100644 --- a/scripts/release_modules.py +++ b/scripts/release_modules.py @@ -113,8 +113,23 @@ def update_go_mod_require_pseudo_version(go_mod_text: str, dep_module_path: str, return re.sub(pattern_line, replacement_line, go_mod_text) +def _bump_hugo_image_tag(image: str, latest_hugo: str) -> Optional[str]: + """ + Replace the hugo version segment in an image tag formatted like: + ghcr.io/org/hugo-blox-dev:hugo0.152.2[-suffix] + """ + pattern = re.compile(r"^(?P.+/hugo-blox-dev:hugo)(?P\d+\.\d+\.\d+)(?P.*)$") + match = pattern.match(image) + if not match: + return None + current = match.group("version") + if current == latest_hugo: + return None + return f"{match.group('prefix')}{latest_hugo}{match.group('rest')}" + + def update_devcontainer_hugo(starter_dir: Path, latest_hugo: Optional[str], updated_paths: List[Path]) -> None: - """Update devcontainer Hugo feature version if present.""" + """Update devcontainer Hugo version pinned via image tag (preferred) or legacy feature.""" if not latest_hugo: return devcontainer = starter_dir / ".devcontainer" / "devcontainer.json" @@ -126,21 +141,33 @@ def update_devcontainer_hugo(starter_dir: Path, latest_hugo: Optional[str], upda logging.warning("starter %s: failed to parse devcontainer.json (%s)", starter_dir.name, exc) return - features = data.get("features", {}) - hugo_feature = features.get("ghcr.io/devcontainers/features/hugo:1") - if not isinstance(hugo_feature, dict): - return + updated = False - current = hugo_feature.get("version") - if current == latest_hugo: - return + # Preferred: bump Hugo version embedded in the image tag. + image = data.get("image") + if isinstance(image, str): + new_image = _bump_hugo_image_tag(image, latest_hugo) + if new_image: + data["image"] = new_image + updated = True + logging.info("starter %s: devcontainer image -> %s", starter_dir.name, new_image) - hugo_feature["version"] = latest_hugo - features["ghcr.io/devcontainers/features/hugo:1"] = hugo_feature - data["features"] = features - devcontainer.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") - updated_paths.append(devcontainer) - logging.info("starter %s: devcontainer Hugo version -> %s", starter_dir.name, latest_hugo) + # Legacy fallback: devcontainer Hugo feature pin. + if not updated: + features = data.get("features", {}) + hugo_feature = features.get("ghcr.io/devcontainers/features/hugo:1") + if isinstance(hugo_feature, dict): + current = hugo_feature.get("version") + if current != latest_hugo: + hugo_feature["version"] = latest_hugo + features["ghcr.io/devcontainers/features/hugo:1"] = hugo_feature + data["features"] = features + updated = True + logging.info("starter %s: devcontainer Hugo feature -> %s", starter_dir.name, latest_hugo) + + if updated: + devcontainer.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + updated_paths.append(devcontainer) @dataclass diff --git a/templates/academic-cv/.devcontainer/devcontainer.json b/templates/academic-cv/.devcontainer/devcontainer.json index ef4eeba4..a02193cc 100644 --- a/templates/academic-cv/.devcontainer/devcontainer.json +++ b/templates/academic-cv/.devcontainer/devcontainer.json @@ -1,12 +1,8 @@ { - "image": "mcr.microsoft.com/devcontainers/go:1", - "features": { - "ghcr.io/devcontainers/features/hugo:1": { - "extended": true, - "version": "0.152.2" - }, - "ghcr.io/devcontainers/features/node:1": {} - }, + "name": "HugoBlox Codespace", + "image": "ghcr.io/HugoBlox/hugo-blox-dev:hugo0.152.2", + "updateContentCommand": "pnpm install --frozen-lockfile --prefer-offline", + "postCreateCommand": "pnpm --version && hugo version", "customizations": { "vscode": { "extensions": [ @@ -14,9 +10,16 @@ ] } }, - "postCreateCommand": "pnpm install --no-frozen-lockfile && hugo --logLevel debug && pnpm dlx pagefind --source 'public' --verbose", - "postAttachCommand": "(code --command workbench.view.extension.hugoblox-studio || true)", + "mounts": [ + "source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume" + ], + "remoteUser": "vscode", "forwardPorts": [ 1313 - ] -} \ No newline at end of file + ], + "portsAttributes": { + "1313": { + "label": "Hugo Server" + } + } +} diff --git a/templates/blog/.devcontainer/devcontainer.json b/templates/blog/.devcontainer/devcontainer.json index ef4eeba4..a02193cc 100644 --- a/templates/blog/.devcontainer/devcontainer.json +++ b/templates/blog/.devcontainer/devcontainer.json @@ -1,12 +1,8 @@ { - "image": "mcr.microsoft.com/devcontainers/go:1", - "features": { - "ghcr.io/devcontainers/features/hugo:1": { - "extended": true, - "version": "0.152.2" - }, - "ghcr.io/devcontainers/features/node:1": {} - }, + "name": "HugoBlox Codespace", + "image": "ghcr.io/HugoBlox/hugo-blox-dev:hugo0.152.2", + "updateContentCommand": "pnpm install --frozen-lockfile --prefer-offline", + "postCreateCommand": "pnpm --version && hugo version", "customizations": { "vscode": { "extensions": [ @@ -14,9 +10,16 @@ ] } }, - "postCreateCommand": "pnpm install --no-frozen-lockfile && hugo --logLevel debug && pnpm dlx pagefind --source 'public' --verbose", - "postAttachCommand": "(code --command workbench.view.extension.hugoblox-studio || true)", + "mounts": [ + "source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume" + ], + "remoteUser": "vscode", "forwardPorts": [ 1313 - ] -} \ No newline at end of file + ], + "portsAttributes": { + "1313": { + "label": "Hugo Server" + } + } +} diff --git a/templates/documentation/.devcontainer/devcontainer.json b/templates/documentation/.devcontainer/devcontainer.json index ef4eeba4..a02193cc 100644 --- a/templates/documentation/.devcontainer/devcontainer.json +++ b/templates/documentation/.devcontainer/devcontainer.json @@ -1,12 +1,8 @@ { - "image": "mcr.microsoft.com/devcontainers/go:1", - "features": { - "ghcr.io/devcontainers/features/hugo:1": { - "extended": true, - "version": "0.152.2" - }, - "ghcr.io/devcontainers/features/node:1": {} - }, + "name": "HugoBlox Codespace", + "image": "ghcr.io/HugoBlox/hugo-blox-dev:hugo0.152.2", + "updateContentCommand": "pnpm install --frozen-lockfile --prefer-offline", + "postCreateCommand": "pnpm --version && hugo version", "customizations": { "vscode": { "extensions": [ @@ -14,9 +10,16 @@ ] } }, - "postCreateCommand": "pnpm install --no-frozen-lockfile && hugo --logLevel debug && pnpm dlx pagefind --source 'public' --verbose", - "postAttachCommand": "(code --command workbench.view.extension.hugoblox-studio || true)", + "mounts": [ + "source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume" + ], + "remoteUser": "vscode", "forwardPorts": [ 1313 - ] -} \ No newline at end of file + ], + "portsAttributes": { + "1313": { + "label": "Hugo Server" + } + } +} diff --git a/templates/landing-page/.devcontainer/devcontainer.json b/templates/landing-page/.devcontainer/devcontainer.json index ef4eeba4..a02193cc 100644 --- a/templates/landing-page/.devcontainer/devcontainer.json +++ b/templates/landing-page/.devcontainer/devcontainer.json @@ -1,12 +1,8 @@ { - "image": "mcr.microsoft.com/devcontainers/go:1", - "features": { - "ghcr.io/devcontainers/features/hugo:1": { - "extended": true, - "version": "0.152.2" - }, - "ghcr.io/devcontainers/features/node:1": {} - }, + "name": "HugoBlox Codespace", + "image": "ghcr.io/HugoBlox/hugo-blox-dev:hugo0.152.2", + "updateContentCommand": "pnpm install --frozen-lockfile --prefer-offline", + "postCreateCommand": "pnpm --version && hugo version", "customizations": { "vscode": { "extensions": [ @@ -14,9 +10,16 @@ ] } }, - "postCreateCommand": "pnpm install --no-frozen-lockfile && hugo --logLevel debug && pnpm dlx pagefind --source 'public' --verbose", - "postAttachCommand": "(code --command workbench.view.extension.hugoblox-studio || true)", + "mounts": [ + "source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume" + ], + "remoteUser": "vscode", "forwardPorts": [ 1313 - ] -} \ No newline at end of file + ], + "portsAttributes": { + "1313": { + "label": "Hugo Server" + } + } +} diff --git a/templates/link-in-bio/.devcontainer/devcontainer.json b/templates/link-in-bio/.devcontainer/devcontainer.json index ef4eeba4..a02193cc 100644 --- a/templates/link-in-bio/.devcontainer/devcontainer.json +++ b/templates/link-in-bio/.devcontainer/devcontainer.json @@ -1,12 +1,8 @@ { - "image": "mcr.microsoft.com/devcontainers/go:1", - "features": { - "ghcr.io/devcontainers/features/hugo:1": { - "extended": true, - "version": "0.152.2" - }, - "ghcr.io/devcontainers/features/node:1": {} - }, + "name": "HugoBlox Codespace", + "image": "ghcr.io/HugoBlox/hugo-blox-dev:hugo0.152.2", + "updateContentCommand": "pnpm install --frozen-lockfile --prefer-offline", + "postCreateCommand": "pnpm --version && hugo version", "customizations": { "vscode": { "extensions": [ @@ -14,9 +10,16 @@ ] } }, - "postCreateCommand": "pnpm install --no-frozen-lockfile && hugo --logLevel debug && pnpm dlx pagefind --source 'public' --verbose", - "postAttachCommand": "(code --command workbench.view.extension.hugoblox-studio || true)", + "mounts": [ + "source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume" + ], + "remoteUser": "vscode", "forwardPorts": [ 1313 - ] -} \ No newline at end of file + ], + "portsAttributes": { + "1313": { + "label": "Hugo Server" + } + } +} diff --git a/templates/resume/.devcontainer/devcontainer.json b/templates/resume/.devcontainer/devcontainer.json index ef4eeba4..a02193cc 100644 --- a/templates/resume/.devcontainer/devcontainer.json +++ b/templates/resume/.devcontainer/devcontainer.json @@ -1,12 +1,8 @@ { - "image": "mcr.microsoft.com/devcontainers/go:1", - "features": { - "ghcr.io/devcontainers/features/hugo:1": { - "extended": true, - "version": "0.152.2" - }, - "ghcr.io/devcontainers/features/node:1": {} - }, + "name": "HugoBlox Codespace", + "image": "ghcr.io/HugoBlox/hugo-blox-dev:hugo0.152.2", + "updateContentCommand": "pnpm install --frozen-lockfile --prefer-offline", + "postCreateCommand": "pnpm --version && hugo version", "customizations": { "vscode": { "extensions": [ @@ -14,9 +10,16 @@ ] } }, - "postCreateCommand": "pnpm install --no-frozen-lockfile && hugo --logLevel debug && pnpm dlx pagefind --source 'public' --verbose", - "postAttachCommand": "(code --command workbench.view.extension.hugoblox-studio || true)", + "mounts": [ + "source=pnpm-store,target=/home/vscode/.local/share/pnpm,type=volume" + ], + "remoteUser": "vscode", "forwardPorts": [ 1313 - ] -} \ No newline at end of file + ], + "portsAttributes": { + "1313": { + "label": "Hugo Server" + } + } +}