diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 859dd7673..5dc2891b8 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -13,7 +13,7 @@ env: jobs: autofix: - runs-on: blacksmith-32vcpu-ubuntu-2404 + runs-on: ${{ vars.CI_RUNNER_X64 || 'ubuntu-latest' }} steps: - name: Checkout Repository diff --git a/.github/workflows/crowdin-sync.yml b/.github/workflows/crowdin-sync.yml index 1962d8d23..7bc809728 100644 --- a/.github/workflows/crowdin-sync.yml +++ b/.github/workflows/crowdin-sync.yml @@ -10,7 +10,7 @@ concurrency: jobs: crowdin-sync: - runs-on: blacksmith-32vcpu-ubuntu-2404 + runs-on: ${{ vars.CI_RUNNER_X64 || 'ubuntu-latest' }} permissions: contents: write diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index c102ebe26..da9ef64c0 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -24,7 +24,7 @@ env: jobs: mode: - runs-on: blacksmith-32vcpu-ubuntu-2404 + runs-on: ${{ vars.CI_RUNNER_X64 || 'ubuntu-latest' }} outputs: nightly: ${{ steps.mode.outputs.nightly }} @@ -61,10 +61,10 @@ jobs: matrix: include: - platform: linux/amd64 - runner: blacksmith-32vcpu-ubuntu-2404 + runner: ${{ vars.CI_RUNNER_X64 || 'ubuntu-latest' }} arch: amd64 - platform: linux/arm64 - runner: blacksmith-32vcpu-ubuntu-2404-arm + runner: ${{ vars.CI_RUNNER_ARM64 || 'ubuntu-24.04-arm' }} arch: arm64 runs-on: ${{ matrix.runner }} @@ -80,12 +80,38 @@ jobs: - name: Checkout Repository uses: actions/checkout@v6 - - name: Setup Blacksmith Docker Builder - uses: useblacksmith/setup-docker-builder@v2 - with: - cache-key: Dockerfile-${{ matrix.arch }} + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v4 + + - ®istries + name: Determine registries + id: registries + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: | + set -euo pipefail + + dockerhub=false + ghcr_image="${GHCR_IMAGE,,}" + docker_image="${DOCKER_IMAGE,,}" + images="$ghcr_image" + if [[ -n "$DOCKER_USERNAME" && -n "$DOCKER_PASSWORD" ]]; then + dockerhub=true + images="${images}"$'\n'"$docker_image" + fi + + { + echo "ghcr_image=$ghcr_image" + echo "docker_image=$docker_image" + echo "images<> "$GITHUB_OUTPUT" - name: Login to Docker Hub + if: ${{ steps.registries.outputs.dockerhub == 'true' }} uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USERNAME }} @@ -102,15 +128,13 @@ jobs: id: meta uses: docker/metadata-action@v6 with: - images: | - ${{ env.GHCR_IMAGE }} - ${{ env.DOCKER_IMAGE }} + images: ${{ steps.registries.outputs.images }} tags: | type=sha,prefix=sha-,suffix=-${{ matrix.arch }} - name: Cache-only smoke build if: ${{ needs.mode.outputs.canary == 'true' }} - uses: useblacksmith/build-push-action@v2 + uses: docker/build-push-action@v7 with: context: . platforms: ${{ matrix.platform }} @@ -118,7 +142,7 @@ jobs: - name: Build and Push by Digest id: build - uses: useblacksmith/build-push-action@v2 + uses: docker/build-push-action@v7 with: context: . sbom: true @@ -148,7 +172,11 @@ jobs: - mode - build timeout-minutes: 30 - runs-on: blacksmith-32vcpu-ubuntu-2404 + runs-on: ${{ vars.CI_RUNNER_X64 || 'ubuntu-latest' }} + + env: + DEPLOY: ${{ secrets.SSH_KEY != '' && secrets.SSH_HOST != '' && secrets.SSH_USER != '' }} + PURGE_CLOUDFLARE: ${{ secrets.CLOUDFLARE_ZONE_ID != '' && secrets.CLOUDFLARE_API_TOKEN != '' }} permissions: contents: read @@ -177,7 +205,10 @@ jobs: - name: Setup Docker Buildx uses: docker/setup-buildx-action@v4 + - *registries + - name: Login to Docker Hub + if: ${{ steps.registries.outputs.dockerhub == 'true' }} uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USERNAME }} @@ -204,9 +235,7 @@ jobs: id: meta uses: docker/metadata-action@v6 with: - images: | - ${{ env.GHCR_IMAGE }} - ${{ env.DOCKER_IMAGE }} + images: ${{ steps.registries.outputs.images }} tags: | type=sha,prefix=sha- type=raw,value=canary-${{ github.run_id }}-${{ github.run_attempt }},enable=${{ needs.mode.outputs.canary == 'true' }} @@ -241,14 +270,17 @@ jobs: --annotation "index:org.opencontainers.image.documentation=https://docs.rxresu.me" \ --annotation "index:org.opencontainers.image.source=https://github.com/${{ github.repository }}" \ --annotation "index:org.opencontainers.image.version=${{ steps.version.outputs.version }}" \ - $(printf '${{ env.GHCR_IMAGE }}@sha256:%s ' *) + $(printf '${{ steps.registries.outputs.ghcr_image }}@sha256:%s ' *) # Get the digest of the multi-arch manifest - GHCR_DIGEST=$(docker buildx imagetools inspect ${{ env.GHCR_IMAGE }}:${FINAL_TAG} --format '{{json .Manifest.Digest}}' | tr -d '"') - DOCKER_DIGEST=$(docker buildx imagetools inspect ${{ env.DOCKER_IMAGE }}:${FINAL_TAG} --format '{{json .Manifest.Digest}}' | tr -d '"') + GHCR_DIGEST=$(docker buildx imagetools inspect ${{ steps.registries.outputs.ghcr_image }}:${FINAL_TAG} --format '{{json .Manifest.Digest}}' | tr -d '"') echo "final_tag=$FINAL_TAG" >> "$GITHUB_OUTPUT" echo "ghcr_digest=$GHCR_DIGEST" >> "$GITHUB_OUTPUT" - echo "docker_digest=$DOCKER_DIGEST" >> "$GITHUB_OUTPUT" + + if [[ "${{ steps.registries.outputs.dockerhub }}" == "true" ]]; then + DOCKER_DIGEST=$(docker buildx imagetools inspect ${{ steps.registries.outputs.docker_image }}:${FINAL_TAG} --format '{{json .Manifest.Digest}}' | tr -d '"') + echo "docker_digest=$DOCKER_DIGEST" >> "$GITHUB_OUTPUT" + fi - name: Install Cosign uses: sigstore/cosign-installer@v3 @@ -256,15 +288,19 @@ jobs: - name: Sign images with Cosign run: | # Sign GHCR image - cosign sign --yes ${{ env.GHCR_IMAGE }}@${{ steps.manifest.outputs.ghcr_digest }} + cosign sign --yes ${{ steps.registries.outputs.ghcr_image }}@${{ steps.manifest.outputs.ghcr_digest }} - # Sign Docker Hub image - cosign sign --yes ${{ env.DOCKER_IMAGE }}@${{ steps.manifest.outputs.docker_digest }} + if [[ "${{ steps.registries.outputs.dockerhub }}" == "true" ]]; then + # Sign Docker Hub image + cosign sign --yes ${{ steps.registries.outputs.docker_image }}@${{ steps.manifest.outputs.docker_digest }} + fi - name: Inspect image run: | - docker buildx imagetools inspect ${{ env.GHCR_IMAGE }}:${{ steps.manifest.outputs.final_tag }} - docker buildx imagetools inspect ${{ env.DOCKER_IMAGE }}:${{ steps.manifest.outputs.final_tag }} + docker buildx imagetools inspect ${{ steps.registries.outputs.ghcr_image }}:${{ steps.manifest.outputs.final_tag }} + if [[ "${{ steps.registries.outputs.dockerhub }}" == "true" ]]; then + docker buildx imagetools inspect ${{ steps.registries.outputs.docker_image }}:${{ steps.manifest.outputs.final_tag }} + fi - name: Verify anonymous pulls on both architectures run: | @@ -273,7 +309,11 @@ jobs: trap 'rm -rf "$registry_config"' EXIT # Prevent Docker from discovering a system credential helper. printf '%s\n' '{"auths":{"ghcr.io":{},"https://index.docker.io/v1/":{}}}' > "$registry_config/config.json" - for image in "$GHCR_IMAGE" "$DOCKER_IMAGE"; do + images=("${{ steps.registries.outputs.ghcr_image }}") + if [[ "${{ steps.registries.outputs.dockerhub }}" == "true" ]]; then + images+=("${{ steps.registries.outputs.docker_image }}") + fi + for image in "${images[@]}"; do for platform in linux/amd64 linux/arm64; do docker --config "$registry_config" pull --quiet --platform "$platform" \ "$image:${{ steps.manifest.outputs.final_tag }}" @@ -281,7 +321,7 @@ jobs: done - name: Redeploy Stack - if: ${{ needs.mode.outputs.release == 'true' }} + if: ${{ needs.mode.outputs.release == 'true' && env.DEPLOY == 'true' }} uses: appleboy/ssh-action@v1 with: key: ${{ secrets.SSH_KEY }} @@ -292,7 +332,7 @@ jobs: ./manage_stack.sh up reactive_resume - name: Purge Cloudflare cache - if: ${{ needs.mode.outputs.release == 'true' }} + if: ${{ needs.mode.outputs.release == 'true' && env.PURGE_CLOUDFLARE == 'true' }} env: CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 22bb5f75e..ac174a909 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -20,7 +20,7 @@ env: jobs: e2e: - runs-on: blacksmith-32vcpu-ubuntu-2404 + runs-on: ${{ vars.CI_RUNNER_X64 || 'ubuntu-latest' }} timeout-minutes: 30 services: @@ -58,12 +58,7 @@ jobs: - name: Install Playwright Browser timeout-minutes: 10 - run: | - # The runner's Ubuntu archive/security mirrors time out during dependency installation. - printf '%s\n' 'https://mirrors.edge.kernel.org/ubuntu/' | sudo tee /etc/apt/blacksmith-ubuntu-mirrors.txt > /dev/null - sudo find /etc/apt -type f \( -name '*.list' -o -name '*.sources' \) \ - -exec sed -i -E 's#https?://(archive|us\.archive|security)\.ubuntu\.com/ubuntu#https://mirrors.edge.kernel.org/ubuntu#g' {} + - pnpm exec playwright install --with-deps chromium + run: pnpm exec playwright install --with-deps chromium - name: Generate Test Secrets run: | diff --git a/.github/workflows/label-issues.yml b/.github/workflows/label-issues.yml index b14685a6f..f58ef2135 100644 --- a/.github/workflows/label-issues.yml +++ b/.github/workflows/label-issues.yml @@ -10,7 +10,7 @@ permissions: jobs: label: - runs-on: blacksmith-32vcpu-ubuntu-2404 + runs-on: ${{ vars.CI_RUNNER_X64 || 'ubuntu-latest' }} steps: - name: Checkout Repository diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml index 5ccff3679..600ace5ed 100644 --- a/.github/workflows/stale-issues.yml +++ b/.github/workflows/stale-issues.yml @@ -10,7 +10,7 @@ permissions: jobs: stale: - runs-on: blacksmith-32vcpu-ubuntu-2404 + runs-on: ${{ vars.CI_RUNNER_X64 || 'ubuntu-latest' }} steps: - name: Close Inactive Issues Awaiting Information diff --git a/docs/agents/container-publishing.md b/docs/agents/container-publishing.md index ceebeb0f7..68725c90c 100644 --- a/docs/agents/container-publishing.md +++ b/docs/agents/container-publishing.md @@ -6,31 +6,38 @@ The repository is `reactive-resume/reactive-resume`. Docker Hub remains ## Builds and release safety -`.github/workflows/docker-build.yml` builds AMD64 and ARM64 on matching native Blacksmith -32-vCPU runners. Blacksmith's persistent Docker builder caches layers and cache mounts; -the architecture-specific cache keys keep the two builders separate. +`.github/workflows/docker-build.yml` builds AMD64 and ARM64 on matching native runners. +Repository variables `CI_RUNNER_X64` and `CI_RUNNER_ARM64` select the runner labels; +they default to `ubuntu-latest` and `ubuntu-24.04-arm`, respectively. Other CI workflows +also use `CI_RUNNER_X64`. Docker Buildx shares its local cache between steps within a job; +no cache is persisted between workflow runs. | Trigger | Published aliases | Production deployment | | --- | --- | --- | | Push to `main` | `sha-*`, `nightly`, timestamped nightly | No | | Manual dispatch, default `release=false` | `sha-*`, `canary--` | No | -| Push of a `v*` tag or explicit `release=true` | `sha-*`, `latest`, version/major/minor | Yes: SSH redeploy and Cloudflare purge | +| Push of a `v*` tag or explicit `release=true` | `sha-*`, `latest`, version/major/minor | When configured: SSH redeploy and Cloudflare purge | -Manual `release=true` republishes the version already in `package.json` and redeploys -production. It does not create a Git tag, GitHub release, or version bump. Use this for -an approved current-version rebuild; it replaces the existing stable image aliases. +Manual `release=true` republishes the version already in `package.json` and runs configured +production integrations. SSH redeployment requires `SSH_KEY`, `SSH_HOST`, and `SSH_USER`; +Cloudflare purging requires `CLOUDFLARE_ZONE_ID` and `CLOUDFLARE_API_TOKEN`. Each integration +is skipped if any of its required secrets is missing. Dispatch does not create a Git tag, +GitHub release, or version bump. Use this for an approved current-version rebuild; +it replaces the existing stable image aliases. Manual canaries first run a cache-only build on each architecture, then publish, merge, -and sign both registry images. Run one with: +and sign images in the enabled registries. Run one with: ```bash gh workflow run docker-build.yml --repo reactive-resume/reactive-resume --ref main -f release=false ``` -Both registries retain SBOMs, maximum provenance, and Cosign signatures. Publishing uses -`DOCKER_USERNAME` / `DOCKER_PASSWORD` for Docker Hub and the destination repository's -`GITHUB_TOKEN` with `packages: write` for GHCR. New GHCR packages need public visibility, -repository linkage, and Actions access before consumers can pull anonymously. +Published images retain SBOMs, maximum provenance, and Cosign signatures. GHCR always uses +the destination repository's `GITHUB_TOKEN` with `packages: write`. Docker Hub publishing +is enabled only when both `DOCKER_USERNAME` and `DOCKER_PASSWORD` secrets are present; +otherwise login, publishing, signing, and verification target GHCR only. Image references +are normalized to lowercase. New GHCR packages need public visibility, repository linkage, +and Actions access before consumers can pull anonymously. ## Verification and historical images @@ -62,7 +69,7 @@ rm -r "$registry_config" On September 11, 2026, `latest`, `v5`, `v5.3`, and `v5.3.0` were copied to the then-current public GHCR package, `ghcr.io/reactive-resume/app`. Both architectures were pulled anonymously; the original Cosign signature, -SBOMs, provenance, and image digest were verified. The signed Blacksmith canary +SBOMs, provenance, and image digest were verified. The signed canary [`canary-34582818410-1`](https://github.com/reactive-resume/reactive-resume/actions/runs/34582818410) also passed on both registries without deploying production. @@ -108,6 +115,5 @@ OIDC trust policies; the repository URL alone does not describe the subject. Track availability and supported copied tags in [migration issue #3503](https://github.com/reactive-resume/reactive-resume/issues/3503). No database reset, volume deletion, or resume-data migration is required. -References: [Blacksmith Docker caching](https://docs.blacksmith.sh/blacksmith-caching/docker-builds), -[GitHub package permissions](https://docs.github.com/en/packages/learn-github-packages/about-permissions-for-github-packages), +References: [GitHub package permissions](https://docs.github.com/en/packages/learn-github-packages/about-permissions-for-github-packages), [Cosign verification](https://docs.sigstore.dev/cosign/verifying/verify/). diff --git a/tooling/playwright-mirrors.test.ts b/tooling/playwright-mirrors.test.ts deleted file mode 100644 index b12ccd8a4..000000000 --- a/tooling/playwright-mirrors.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { readFileSync } from "node:fs"; -import { expect, it } from "vitest"; - -it("rewrites Ubuntu APT sources while preserving unrelated repositories and signature settings", () => { - const workflow = readFileSync(new URL("../.github/workflows/e2e.yml", import.meta.url), "utf8"); - const expression = workflow.match(/-exec sed -i -E '([^']+)'/)?.[1]; - if (!expression) throw new Error("Ubuntu mirror rewrite is missing"); - const input = [ - "deb http://archive.ubuntu.com/ubuntu noble main", - "URIs: http://us.archive.ubuntu.com/ubuntu/", - "URIs: https://security.ubuntu.com/ubuntu/", - "Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg", - "URIs: mirror+file:/etc/apt/blacksmith-ubuntu-mirrors.txt", - "deb https://packages.microsoft.com/ubuntu/24.04/prod noble main", - ].join("\n"); - const output = execFileSync("sed", ["-E", expression], { input, encoding: "utf8" }); - expect(output.trimEnd()).toBe( - input - .replace("http://archive.ubuntu.com/ubuntu", "https://mirrors.edge.kernel.org/ubuntu") - .replace("http://us.archive.ubuntu.com/ubuntu", "https://mirrors.edge.kernel.org/ubuntu") - .replace("https://security.ubuntu.com/ubuntu", "https://mirrors.edge.kernel.org/ubuntu"), - ); -});