fix(ci): restore Docker publishing with portable runner fallbacks (#3533)

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
Lihan YANG
2026-09-22 17:07:28 +02:00
committed by GitHub
co-authored by Amruth Pillai
parent 0ac320b0e9
commit f0bc26cb3d
8 changed files with 95 additions and 78 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+68 -28
View File
@@ -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
- &registries
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<<EOF"
echo "$images"
echo "EOF"
echo "dockerhub=$dockerhub"
} >> "$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 }}
+2 -7
View File
@@ -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: |
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+21 -15
View File
@@ -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-<run-id>-<attempt>` | 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/).
-24
View File
@@ -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"),
);
});