From f72dedc2a36800f800433df7119be1e8b64d747e Mon Sep 17 00:00:00 2001 From: MrChengLen Date: Mon, 14 Sep 2026 19:20:53 +0200 Subject: [PATCH] ci(docker): allow a manual rebuild via workflow_dispatch Docker only ran on a push to main or a version tag. main is protected by required status checks, so producing an image -- and reaching notify-ops and the deploy behind it -- always meant opening a pull request, even when nothing about the code needed to change. That gap cost three weeks in September 2026: the credential notify-ops uses had expired, it failed with HTTP 401 on every run, and no deploy reached production. Once the credential was replaced there was no way to prove it worked, or to redeploy the current main, without inventing a code change to push. An empty commit is rejected by the branch rules, which is correct and not something to work around. workflow_dispatch rebuilds from whatever main currently holds and runs the whole chain: build, push, sign, notify, deploy. That is also what is wanted during an incident, where the question is usually "redeploy what is on main" rather than "ship a change". Nothing else moves. The build, the matrix, the signing and the permissions are untouched, and the repo's supply-chain guards still pass. Co-Authored-By: Claude Opus 5 --- .github/workflows/docker.yml | 223 ++++++++++++++++++----------------- 1 file changed, 117 insertions(+), 106 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 957845e..b1facab 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,106 +1,117 @@ -name: Docker - -on: - push: - branches: ["main"] - tags: ["v*"] - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - strategy: - # Build slim + office in parallel; fail-fast off because the two - # variants are independent — a Pango regression in slim shouldn't - # mask an upstream LibreOffice break in office (or vice versa). - fail-fast: false - matrix: - include: - - target: base - suffix: "" - description: "Slim image (mammoth+WeasyPrint docx→pdf path)" - - target: office - suffix: "-office" - description: "Office image (adds LibreOffice for high-fidelity docx→pdf)" - permissions: - contents: read - packages: write - # NEU-B.4: cosign keyless signing uses the GitHub Actions OIDC - # token to prove identity to Sigstore's Fulcio CA — no long-lived - # signing key to manage or rotate. Verifiable later with - # cosign verify ghcr.io//@sha256: \ - # --certificate-identity-regexp "^https://github\\.com///" \ - # --certificate-oidc-issuer https://token.actions.githubusercontent.com - id-token: write - - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - # The default ``docker`` driver on GHA runners doesn't support - # ``cache-to: type=gha`` (GitHub Actions cache backend). Switching - # to buildx's ``docker-container`` driver enables the cache export - # so the matrix matrix-base / matrix-office leg can reuse the - # base-stage layers it already pulled. - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - # The office variant gets the same tag set as slim but with a - # ``-office`` suffix so a consumer can pick either by tag — - # filemorph:1.1.0 vs filemorph:1.1.0-office. The slim image - # keeps ``:latest`` (default for naive pulls); the office image - # gets ``:office`` for the matching shorthand. - tags: | - type=semver,pattern={{version}},suffix=${{ matrix.suffix }} - type=semver,pattern={{major}}.{{minor}},suffix=${{ matrix.suffix }} - type=raw,value=${{ matrix.target == 'base' && 'latest' || 'office' }} - type=sha,format=short,prefix=sha-,suffix=${{ matrix.suffix }} - - - name: Build and push Docker image (${{ matrix.target }}) - id: build - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 - with: - context: . - target: ${{ matrix.target }} - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - # GHA-cache lets the office stage reuse every layer the base - # stage already produced — without it the office build redoes - # ffmpeg, ghostscript, pip install. Scope is per-target so the - # two matrix legs don't trample each other's cache. - cache-from: type=gha,scope=${{ matrix.target }} - cache-to: type=gha,scope=${{ matrix.target }},mode=max - - - name: Install cosign - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - with: - cosign-release: "v2.4.1" - - - name: Sign published image (keyless / Sigstore) - env: - COSIGN_EXPERIMENTAL: "true" - DIGEST: ${{ steps.build.outputs.digest }} - TAGS: ${{ steps.meta.outputs.tags }} - # Sign every tag that got pushed at the SAME digest so a downstream - # `cosign verify ghcr.io/.../filemorph:` works regardless of - # which alias the consumer pulls. - run: | - set -euo pipefail - for tag in $TAGS; do - cosign sign --yes "${tag}@${DIGEST}" - done +name: Docker + +on: + push: + branches: ["main"] + tags: ["v*"] + # Manual rebuild. Without this there is no way to produce an image, or to + # reach `notify-ops` and the deploy that follows it, other than pushing to + # main — and main is protected, so that means opening a PR. + # + # That gap cost three weeks in September 2026: the credential `notify-ops` + # uses had expired, so it failed with HTTP 401 on every run and nothing + # reached production. Once it was replaced there was no way to prove the fix, + # or to redeploy, without inventing a code change. A dispatch here rebuilds + # from the current main and runs the whole chain, which is also what you want + # during an incident. + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + strategy: + # Build slim + office in parallel; fail-fast off because the two + # variants are independent — a Pango regression in slim shouldn't + # mask an upstream LibreOffice break in office (or vice versa). + fail-fast: false + matrix: + include: + - target: base + suffix: "" + description: "Slim image (mammoth+WeasyPrint docx→pdf path)" + - target: office + suffix: "-office" + description: "Office image (adds LibreOffice for high-fidelity docx→pdf)" + permissions: + contents: read + packages: write + # NEU-B.4: cosign keyless signing uses the GitHub Actions OIDC + # token to prove identity to Sigstore's Fulcio CA — no long-lived + # signing key to manage or rotate. Verifiable later with + # cosign verify ghcr.io//@sha256: \ + # --certificate-identity-regexp "^https://github\\.com///" \ + # --certificate-oidc-issuer https://token.actions.githubusercontent.com + id-token: write + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + # The default ``docker`` driver on GHA runners doesn't support + # ``cache-to: type=gha`` (GitHub Actions cache backend). Switching + # to buildx's ``docker-container`` driver enables the cache export + # so the matrix matrix-base / matrix-office leg can reuse the + # base-stage layers it already pulled. + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + # The office variant gets the same tag set as slim but with a + # ``-office`` suffix so a consumer can pick either by tag — + # filemorph:1.1.0 vs filemorph:1.1.0-office. The slim image + # keeps ``:latest`` (default for naive pulls); the office image + # gets ``:office`` for the matching shorthand. + tags: | + type=semver,pattern={{version}},suffix=${{ matrix.suffix }} + type=semver,pattern={{major}}.{{minor}},suffix=${{ matrix.suffix }} + type=raw,value=${{ matrix.target == 'base' && 'latest' || 'office' }} + type=sha,format=short,prefix=sha-,suffix=${{ matrix.suffix }} + + - name: Build and push Docker image (${{ matrix.target }}) + id: build + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: . + target: ${{ matrix.target }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # GHA-cache lets the office stage reuse every layer the base + # stage already produced — without it the office build redoes + # ffmpeg, ghostscript, pip install. Scope is per-target so the + # two matrix legs don't trample each other's cache. + cache-from: type=gha,scope=${{ matrix.target }} + cache-to: type=gha,scope=${{ matrix.target }},mode=max + + - name: Install cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + with: + cosign-release: "v2.4.1" + + - name: Sign published image (keyless / Sigstore) + env: + COSIGN_EXPERIMENTAL: "true" + DIGEST: ${{ steps.build.outputs.digest }} + TAGS: ${{ steps.meta.outputs.tags }} + # Sign every tag that got pushed at the SAME digest so a downstream + # `cosign verify ghcr.io/.../filemorph:` works regardless of + # which alias the consumer pulls. + run: | + set -euo pipefail + for tag in $TAGS; do + cosign sign --yes "${tag}@${DIGEST}" + done