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