Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 117 additions & 106 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -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/<owner>/<repo>@sha256:<digest> \
# --certificate-identity-regexp "^https://github\\.com/<owner>/<repo>/" \
# --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:<tag>` 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/<owner>/<repo>@sha256:<digest> \
# --certificate-identity-regexp "^https://github\\.com/<owner>/<repo>/" \
# --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:<tag>` works regardless of
# which alias the consumer pulls.
run: |
set -euo pipefail
for tag in $TAGS; do
cosign sign --yes "${tag}@${DIGEST}"
done