From 9fc1497e00aaff56e8204286213b5708f5804a97 Mon Sep 17 00:00:00 2001 From: MrChengLen Date: Wed, 30 Sep 2026 14:59:04 +0200 Subject: [PATCH 1/5] =?UTF-8?q?fix(docker):=20office=20image=20no=20longer?= =?UTF-8?q?=20gets=20:latest=20on=20a=20release=20=E2=80=94=20latest=3Dfal?= =?UTF-8?q?se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docker/metadata-action's default flavor, latest=auto, adds a `latest` tag of its own whenever a type=semver entry matches a non-prerelease tag, and a tag entry's suffix= does not reach that tag. On every v* release both matrix legs therefore pushed :latest, and whichever finished last kept it — usually the office image, the slower build. Anyone pulling :latest then got LibreOffice without choosing the office image, and with the default FILEMORPH_OFFICE_ENGINE=auto complex DOCX files went through it. The v1.1.0 run's logs have expired; its timestamps show the office build-and-push step ending 12 s after the slim one's, so on 2026-06-01 :latest was most likely the office image for about an hour, until the next main build. Main builds were never affected: the action adds `latest` only for tag refs. flavor: latest=false leaves the type=raw entry as the only source of :latest, set for the base leg; the office image keeps :office, and every other tag is unchanged. Rejected: keeping latest=auto for the base leg only (the raw entry already tags it on every build) and a per-leg `suffix=-office,onlatest=true` flavor (it would add a new :latest-office tag). Confirmed from the action's source at the pinned SHA (src/flavor.ts; src/meta.ts procSemver, setVersion, generateTags; identical in v6.1.0, which built v1.1.0) and by running its dist/index.cjs (blob 9edb5c8, hash-checked) locally on the workflow's inputs, against a localhost API stub and without credentials: before, tag v1.2.0 gave the office leg `latest`; after, only the slim leg gets it, on a release tag, a pre-release tag and main. Guard: test_only_the_slim_image_is_tagged_latest pins the flavor whole (the action skips only lines starting with `#` and unquotes CSV fields, so a looser match passed flavors it reads as latest=auto or latest=true), fails if another tag entry of the step names `latest` (an indented `# ...` line is an entry to the action, not a comment), and fails if the matrix loses the unsuffixed base leg the raw entry is meant for. Of 26 mutations run through both the guard and the real action, the guard fails all 15 that tag the wrong image, plus 3 more (an entry the action rejects, a `${{ vars.X }}` line, a swapped-out action); 4 controls pass; 4 equivalent spellings fail on purpose. Both reviews' suggestions are folded in. docker.yml pushes images, so it was not dispatched on this branch; the first main run shows the new flavor in its log, and the first real check of the release tags is the next release (main runs create no semver tags). Found by the security review of PR #180. Full suite 1489 green (72 skipped on Windows); ruff clean; gitleaks and the scope guard clean. No dependency, template or i18n change. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/docker.yml | 6 ++++ CHANGELOG.md | 31 +++++++++++++++++++ tests/test_supply_chain_hygiene.py | 48 +++++++++++++++++++++++++++++- 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 5005a79..c947166 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -84,6 +84,12 @@ jobs: 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 }} + # ``:latest`` comes from the raw entry above, and only from it. By + # default (``latest=auto``) the action also adds a ``latest`` tag + # to both variants on a release tag, without the ``-office`` + # suffix, so whichever image finished last would keep ``:latest`` — + # usually the office image, the slower build. + flavor: latest=false - name: Build and push Docker image (${{ matrix.target }}) id: build diff --git a/CHANGELOG.md b/CHANGELOG.md index 5346c0f..3321917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,37 @@ Versions follow [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Fixed — after a release, `:latest` is the slim image, not the office image + +`docker.yml` tags the slim image `:latest` and the office image `:office`, but +`docker/metadata-action` added a `latest` tag of its own on every release: with +its default flavor, `latest=auto`, a release tag (`vX.Y.Z` without a +pre-release part) that matches a `type=semver` entry gets `latest` as well, and +the office entries' `suffix=-office` does not apply to it. Both images were +pushed as `:latest`, and the one that finished last kept the tag — usually the +office image, the slower build. Whoever pulled `:latest` then got LibreOffice +without choosing the office image, and with the default +`FILEMORPH_OFFICE_ENGINE=auto` complex DOCX files were converted through it. +In the v1.1.0 run the office image's build and push finished 12 seconds after +the slim image's, so on 2026-06-01 `:latest` was most likely the office image +for about an hour, until the next build of `main`; if you pulled it then and +not since, pull it again. Builds of `main` were not affected. Found by the security review of +PR #180. + +- **Fix.** The metadata step sets `flavor: latest=false`, so `:latest` comes + only from the `type=raw` entry that names it for the slim image. The other + tags stay as they were: `:office`, and `X.Y.Z`, `X.Y` and `sha-…`, each with + `-office` on the office image. The docs already call `:latest` the slim + image; that now holds right after a release too. `docker.yml` pushes images + and cannot be tried before merge, so the pinned action (v6.2.0) was run + locally on the workflow's inputs: it now tags only the slim image `latest`, + on a release tag, a pre-release tag and `main`. The next release is the + first real test. +- **Guard.** `tests/test_supply_chain_hygiene.py` fails if the metadata step's + flavor is anything but `latest=false`, if another of its tag entries names + `latest`, or if the matrix loses the unsuffixed `base` leg the `type=raw` + entry is meant for. + ### Fixed — patch-policy's `cosign verify` names an image tag that exists `docs/patch-policy.md` told readers to verify the release image diff --git a/tests/test_supply_chain_hygiene.py b/tests/test_supply_chain_hygiene.py index 5bb5e50..bfc83b2 100644 --- a/tests/test_supply_chain_hygiene.py +++ b/tests/test_supply_chain_hygiene.py @@ -35,7 +35,10 @@ digest its build step reports: the SBOM comes from sbom.yml's steps in a job that can only read, and the job that signs the attestation checks nothing out, installs nothing and runs only GitHub's own actions; - docs/release-signing.md verifies it the way it is made. + docs/release-signing.md verifies it the way it is made; + * only the slim image is tagged ``:latest``: docker.yml switches off + metadata-action's automatic ``latest`` tag, which a release would + otherwise add to the office image too. This is a tripwire, not a substitute for the server-side Scorecard run / review: the per-job permissions check here is a heuristic (it asserts a @@ -1095,6 +1098,49 @@ def test_docs_verify_the_sbom_attestation_as_docker_yml_makes_it() -> None: ) +def test_only_the_slim_image_is_tagged_latest() -> None: + """docker.yml tags the slim image ``:latest``, and only the slim image. + + metadata-action adds a ``latest`` tag of its own when a ``type=semver`` + entry matches a release tag (``flavor: latest=auto``, its default), and + a tag entry's ``suffix=`` does not reach that tag: on a release both + variants pushed ``:latest``, and whichever finished last kept it — + usually the office image, the slower build. With ``latest=false`` the + ``type=raw`` entry that names ``latest`` for the unsuffixed base leg is + the only source of ``:latest``, so that entry and that leg have to stay. + The flavor is pinned whole: the action skips only lines that start with + ``#`` and unquotes CSV fields, so a looser match could pass a flavor it + reads differently. + """ + job = _workflow(_WORKFLOW_DIR / "docker.yml")["jobs"]["build-and-push"] + legs = {leg["target"]: leg.get("suffix") for leg in job["strategy"]["matrix"]["include"]} + assert legs.get("base") == "", ( + f"docker.yml: the `latest` entry is meant for the `base` leg, the slim image without " + f"a suffix; the matrix has {legs}" + ) + steps = [s for s in _steps(job) if str(s.get("uses", "")).startswith("docker/metadata-action@")] + assert steps, "docker.yml no longer uses docker/metadata-action — update this guard" + raw_latest = "type=raw,value=${{ matrix.target == 'base' && 'latest' || 'office' }}" + for step in steps: + inputs = step.get("with") or {} + assert str(inputs.get("flavor", "")).strip() == "latest=false", ( + f"docker.yml step {step.get('name')!r}: `flavor:` must be exactly `latest=false` " + f"(this guard pins it whole) — without it, a release tags the office image " + f"`:latest` as well" + ) + latest = [ + line.strip() + for line in str(inputs.get("tags", "")).splitlines() + # As in the action, only a line starting with "#" is a comment; an + # indented "# type=raw,value=latest" is a tag for both images. + if "latest" in line and not line.startswith("#") + ] + assert latest == [raw_latest], ( + f"docker.yml step {step.get('name')!r}: `:latest` comes from one `type=raw` entry, " + f"for the base leg only; found {latest}" + ) + + def test_verapdf_image_is_digest_pinned() -> None: """The veraPDF gate runs a validator image pinned by digest. From 4b4cf1a6c868a021cf2bbedb6a1ee6da07dbcda9 Mon Sep 17 00:00:00 2001 From: MrChengLen Date: Thu, 1 Oct 2026 12:22:26 +0200 Subject: [PATCH 2/5] =?UTF-8?q?chore(changelog):=20take=20the=20entry=20ou?= =?UTF-8?q?t=20to=20merge=20main=20in=20=E2=80=94=20back=20in=20two=20comm?= =?UTF-8?q?its?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The entry conflicts with main's newest one at the top of [Unreleased]. CHANGELOG.md goes back to the merge base here, GitHub merges main in, and the next commit puts the entry back on top. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3321917..5346c0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,37 +9,6 @@ Versions follow [Semantic Versioning](https://semver.org/). ## [Unreleased] -### Fixed — after a release, `:latest` is the slim image, not the office image - -`docker.yml` tags the slim image `:latest` and the office image `:office`, but -`docker/metadata-action` added a `latest` tag of its own on every release: with -its default flavor, `latest=auto`, a release tag (`vX.Y.Z` without a -pre-release part) that matches a `type=semver` entry gets `latest` as well, and -the office entries' `suffix=-office` does not apply to it. Both images were -pushed as `:latest`, and the one that finished last kept the tag — usually the -office image, the slower build. Whoever pulled `:latest` then got LibreOffice -without choosing the office image, and with the default -`FILEMORPH_OFFICE_ENGINE=auto` complex DOCX files were converted through it. -In the v1.1.0 run the office image's build and push finished 12 seconds after -the slim image's, so on 2026-06-01 `:latest` was most likely the office image -for about an hour, until the next build of `main`; if you pulled it then and -not since, pull it again. Builds of `main` were not affected. Found by the security review of -PR #180. - -- **Fix.** The metadata step sets `flavor: latest=false`, so `:latest` comes - only from the `type=raw` entry that names it for the slim image. The other - tags stay as they were: `:office`, and `X.Y.Z`, `X.Y` and `sha-…`, each with - `-office` on the office image. The docs already call `:latest` the slim - image; that now holds right after a release too. `docker.yml` pushes images - and cannot be tried before merge, so the pinned action (v6.2.0) was run - locally on the workflow's inputs: it now tags only the slim image `latest`, - on a release tag, a pre-release tag and `main`. The next release is the - first real test. -- **Guard.** `tests/test_supply_chain_hygiene.py` fails if the metadata step's - flavor is anything but `latest=false`, if another of its tag entries names - `latest`, or if the matrix loses the unsuffixed `base` leg the `type=raw` - entry is meant for. - ### Fixed — patch-policy's `cosign verify` names an image tag that exists `docs/patch-policy.md` told readers to verify the release image From 647c384eee305492af9e9424dd943429070d0930 Mon Sep 17 00:00:00 2001 From: MrChengLen Date: Thu, 1 Oct 2026 12:22:36 +0200 Subject: [PATCH 3/5] docs(changelog): entry back on top of the merged changelog Restores this PR's entry above the entries main gained meanwhile. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16668fa..512d9ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,37 @@ Versions follow [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Fixed — after a release, `:latest` is the slim image, not the office image + +`docker.yml` tags the slim image `:latest` and the office image `:office`, but +`docker/metadata-action` added a `latest` tag of its own on every release: with +its default flavor, `latest=auto`, a release tag (`vX.Y.Z` without a +pre-release part) that matches a `type=semver` entry gets `latest` as well, and +the office entries' `suffix=-office` does not apply to it. Both images were +pushed as `:latest`, and the one that finished last kept the tag — usually the +office image, the slower build. Whoever pulled `:latest` then got LibreOffice +without choosing the office image, and with the default +`FILEMORPH_OFFICE_ENGINE=auto` complex DOCX files were converted through it. +In the v1.1.0 run the office image's build and push finished 12 seconds after +the slim image's, so on 2026-06-01 `:latest` was most likely the office image +for about an hour, until the next build of `main`; if you pulled it then and +not since, pull it again. Builds of `main` were not affected. Found by the security review of +PR #180. + +- **Fix.** The metadata step sets `flavor: latest=false`, so `:latest` comes + only from the `type=raw` entry that names it for the slim image. The other + tags stay as they were: `:office`, and `X.Y.Z`, `X.Y` and `sha-…`, each with + `-office` on the office image. The docs already call `:latest` the slim + image; that now holds right after a release too. `docker.yml` pushes images + and cannot be tried before merge, so the pinned action (v6.2.0) was run + locally on the workflow's inputs: it now tags only the slim image `latest`, + on a release tag, a pre-release tag and `main`. The next release is the + first real test. +- **Guard.** `tests/test_supply_chain_hygiene.py` fails if the metadata step's + flavor is anything but `latest=false`, if another of its tag entries names + `latest`, or if the matrix loses the unsuffixed `base` leg the `type=raw` + entry is meant for. + ### Changed — a sign-in lasts 30 days from login - `POST /auth/refresh` returns a new access token together with the refresh From 80c7a36bd49e008f51a3da3e3975df54edde6c7d Mon Sep 17 00:00:00 2001 From: MrChengLen Date: Thu, 1 Oct 2026 12:47:19 +0200 Subject: [PATCH 4/5] =?UTF-8?q?chore(changelog):=20take=20the=20entry=20ou?= =?UTF-8?q?t=20to=20merge=20main=20in=20=E2=80=94=20back=20in=20two=20comm?= =?UTF-8?q?its?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The entry conflicts with main's newest one at the top of [Unreleased]. CHANGELOG.md goes back to the merge base here, GitHub merges main in, and the next commit puts the entry back on top. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 512d9ec..16668fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,37 +9,6 @@ Versions follow [Semantic Versioning](https://semver.org/). ## [Unreleased] -### Fixed — after a release, `:latest` is the slim image, not the office image - -`docker.yml` tags the slim image `:latest` and the office image `:office`, but -`docker/metadata-action` added a `latest` tag of its own on every release: with -its default flavor, `latest=auto`, a release tag (`vX.Y.Z` without a -pre-release part) that matches a `type=semver` entry gets `latest` as well, and -the office entries' `suffix=-office` does not apply to it. Both images were -pushed as `:latest`, and the one that finished last kept the tag — usually the -office image, the slower build. Whoever pulled `:latest` then got LibreOffice -without choosing the office image, and with the default -`FILEMORPH_OFFICE_ENGINE=auto` complex DOCX files were converted through it. -In the v1.1.0 run the office image's build and push finished 12 seconds after -the slim image's, so on 2026-06-01 `:latest` was most likely the office image -for about an hour, until the next build of `main`; if you pulled it then and -not since, pull it again. Builds of `main` were not affected. Found by the security review of -PR #180. - -- **Fix.** The metadata step sets `flavor: latest=false`, so `:latest` comes - only from the `type=raw` entry that names it for the slim image. The other - tags stay as they were: `:office`, and `X.Y.Z`, `X.Y` and `sha-…`, each with - `-office` on the office image. The docs already call `:latest` the slim - image; that now holds right after a release too. `docker.yml` pushes images - and cannot be tried before merge, so the pinned action (v6.2.0) was run - locally on the workflow's inputs: it now tags only the slim image `latest`, - on a release tag, a pre-release tag and `main`. The next release is the - first real test. -- **Guard.** `tests/test_supply_chain_hygiene.py` fails if the metadata step's - flavor is anything but `latest=false`, if another of its tag entries names - `latest`, or if the matrix loses the unsuffixed `base` leg the `type=raw` - entry is meant for. - ### Changed — a sign-in lasts 30 days from login - `POST /auth/refresh` returns a new access token together with the refresh From e6bced92bc13fa90a0d5cb377046d136b8dbc528 Mon Sep 17 00:00:00 2001 From: MrChengLen Date: Thu, 1 Oct 2026 12:47:29 +0200 Subject: [PATCH 5/5] docs(changelog): entry back on top of the merged changelog Restores this PR's entry above the entries main gained meanwhile. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2127d5f..0769474 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,37 @@ Versions follow [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Fixed — after a release, `:latest` is the slim image, not the office image + +`docker.yml` tags the slim image `:latest` and the office image `:office`, but +`docker/metadata-action` added a `latest` tag of its own on every release: with +its default flavor, `latest=auto`, a release tag (`vX.Y.Z` without a +pre-release part) that matches a `type=semver` entry gets `latest` as well, and +the office entries' `suffix=-office` does not apply to it. Both images were +pushed as `:latest`, and the one that finished last kept the tag — usually the +office image, the slower build. Whoever pulled `:latest` then got LibreOffice +without choosing the office image, and with the default +`FILEMORPH_OFFICE_ENGINE=auto` complex DOCX files were converted through it. +In the v1.1.0 run the office image's build and push finished 12 seconds after +the slim image's, so on 2026-06-01 `:latest` was most likely the office image +for about an hour, until the next build of `main`; if you pulled it then and +not since, pull it again. Builds of `main` were not affected. Found by the security review of +PR #180. + +- **Fix.** The metadata step sets `flavor: latest=false`, so `:latest` comes + only from the `type=raw` entry that names it for the slim image. The other + tags stay as they were: `:office`, and `X.Y.Z`, `X.Y` and `sha-…`, each with + `-office` on the office image. The docs already call `:latest` the slim + image; that now holds right after a release too. `docker.yml` pushes images + and cannot be tried before merge, so the pinned action (v6.2.0) was run + locally on the workflow's inputs: it now tags only the slim image `latest`, + on a release tag, a pre-release tag and `main`. The next release is the + first real test. +- **Guard.** `tests/test_supply_chain_hygiene.py` fails if the metadata step's + flavor is anything but `latest=false`, if another of its tag entries names + `latest`, or if the matrix loses the unsuffixed `base` leg the `type=raw` + entry is meant for. + ### Security — urllib3 2.8.0: three advisories published on 2026-09-30 On 2026-10-01 `pip-audit` flagged urllib3 2.7.0 in `requirements.lock`, and