From c2b79443d267883bccfc6b403d7072226b10ea2e Mon Sep 17 00:00:00 2001 From: MrChengLen Date: Tue, 29 Sep 2026 08:55:47 +0200 Subject: [PATCH 1/7] =?UTF-8?q?docs(changelog):=20Docker=20PR=20build=20en?= =?UTF-8?q?try=20=E2=80=94=20CONTRIBUTING=20and=20PR=20template=20list=20t?= =?UTF-8?q?he=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #174 took its CHANGELOG entry out to merge main in, and auto-merge squash-merged it before the entry came back, so the entry lands here, with the timings of #174's two runs added: the slim image took 50 to 63 seconds and the office image 76 to 88, and both finished before lint-and-test. #175, merged in the same window, gave CONTRIBUTING.md a "CI gates" section and the pull request template a list of the checks that do not block a merge; both now name the Docker image smoke tests as well. Full suite 1449 green (72 skipped on Windows); ruff clean; docs only. Co-Authored-By: Claude Opus 5.5 --- .github/PULL_REQUEST_TEMPLATE.md | 3 ++- CHANGELOG.md | 43 ++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 8 +++--- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e629c51..531a4af 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,7 +2,8 @@ Thanks for contributing to FileMorph! This checklist mirrors the project's standards. It is a reminder, not a hard gate — but the three required checks (lint-and-test, secret-scan, scope-check) WILL block the merge until they - are green. lockfile-drift and the veraPDF run are reported but not required. + are green. lockfile-drift, the veraPDF run and the Docker image smoke tests + are reported but not required. See CONTRIBUTING.md § "CI gates" for what each check runs. --> diff --git a/CHANGELOG.md b/CHANGELOG.md index a9fcfe0..40356a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,49 @@ Versions follow [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Added — pull requests build and smoke-test both Docker images + +`docker.yml` builds the images only after merge, and `notify-ops.yml` deploys +every successful main build, so a broken `Dockerfile`, `.dockerignore`, +`entrypoint.sh` or `requirements.lock` used to show up first on its way to +production. On 2026-05-26 a digest pin that BuildKit rejected (`FROM requires +either one or three arguments`) failed three main builds in a row before the +fix was merged. The new `.github/workflows/docker-pr.yml` builds the slim and +the office image on every pull request, without pushing them, and smoke-tests +each one: + +- The image holds `scripts/first_run.py`, the compiled German message catalogue + and `alembic.ini`, and not `.env`, `data/api_keys.json`, `.git`, `.github` or + `tests/`. A `.env` and a `data/api_keys.json` are planted in the checkout + before the build, so the check sees what `.dockerignore` does with a + self-hoster's working folder, not only with a clean checkout. A third planted + file, which nothing ignores, has to reach the image, so the check cannot pass + on a build that did not use that folder. +- The container starts the way `docker-compose.yml` runs it (all capabilities + dropped, no privilege escalation), answers `/api/v1/health` on the published + port and prints a first-run API key. `curl` inside the container, which the + Compose healthcheck uses, reaches it too, and it does not run as root. +- `ffmpeg` and Ghostscript run, and WeasyPrint renders a page. Start-up alone + would not show a missing one: the converters call them only for a conversion. +- In the office image, LibreOffice converts a text file to PDF the way the DOCX + converter calls it. + +It runs on every pull request, not only on changes to the Docker files: an app +change can break the image alone, and a workflow that a `paths:` filter skips +never reports, so it could not become a required check. The builds read the +layer cache that the main builds write, and write none. In #174's two runs the +slim image took 50 to 63 seconds and the office image 76 to 88, and both +finished before `lint-and-test`. It is not a required check yet. + +Nothing in it can push, sign or deploy: its token is read-only and it uses no +secret. `notify-ops.yml` now also requires that the Docker run it follows was +started by a push or a manual dispatch in this repository, so a run for a pull +request never leads to a deploy. Tests pin both. A job in a workflow that +pull-request events trigger may hold neither a write token nor a secret; +`_privileged` in `tests/test_supply_chain_hygiene.py` now also recognises +`secrets['X']`, `toJSON(secrets)` and a reusable-workflow call's `secrets:`. +And `notify-ops.yml`'s condition and trigger are pinned word for word. + ### Fixed — API docs: keys are optional, video compression and converter registration described correctly The API reference marked every file route "Authentication: Required", and the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bce4ab5..831fdad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,10 +76,12 @@ your change touches before you push: `secret-scan` runs the gitleaks secret scanner; `scope-check` rejects operations files and internal documents, which do not belong in this public -repository. Two more checks run on pull requests without blocking the merge: +repository. More checks run on pull requests without blocking the merge: `lockfile-drift` (`requirements.lock` must match `requirements.txt` — see -[docs/development.md](docs/development.md)) and the veraPDF validation of the -PDF/A-2b output. +[docs/development.md](docs/development.md)), the veraPDF validation of the +PDF/A-2b output, and `smoke-test (base)` and `smoke-test (office)` +([`docker-pr.yml`](.github/workflows/docker-pr.yml)), which build both Docker +images without pushing them and smoke-test each one. --- From 90f7d6f1cf8babf940cac1c4ee7f4ae68c081f00 Mon Sep 17 00:00:00 2001 From: MrChengLen Date: Tue, 29 Sep 2026 09:53:03 +0200 Subject: [PATCH 2/7] =?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 Every PR adds its CHANGELOG entry at the same place, so each merge to main conflicts with every open PR. Taking this PR's entry out lets GitHub merge main in cleanly; the next commit puts it back on top. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 43 ------------------------------------------- 1 file changed, 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40356a3..a9fcfe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,49 +9,6 @@ Versions follow [Semantic Versioning](https://semver.org/). ## [Unreleased] -### Added — pull requests build and smoke-test both Docker images - -`docker.yml` builds the images only after merge, and `notify-ops.yml` deploys -every successful main build, so a broken `Dockerfile`, `.dockerignore`, -`entrypoint.sh` or `requirements.lock` used to show up first on its way to -production. On 2026-05-26 a digest pin that BuildKit rejected (`FROM requires -either one or three arguments`) failed three main builds in a row before the -fix was merged. The new `.github/workflows/docker-pr.yml` builds the slim and -the office image on every pull request, without pushing them, and smoke-tests -each one: - -- The image holds `scripts/first_run.py`, the compiled German message catalogue - and `alembic.ini`, and not `.env`, `data/api_keys.json`, `.git`, `.github` or - `tests/`. A `.env` and a `data/api_keys.json` are planted in the checkout - before the build, so the check sees what `.dockerignore` does with a - self-hoster's working folder, not only with a clean checkout. A third planted - file, which nothing ignores, has to reach the image, so the check cannot pass - on a build that did not use that folder. -- The container starts the way `docker-compose.yml` runs it (all capabilities - dropped, no privilege escalation), answers `/api/v1/health` on the published - port and prints a first-run API key. `curl` inside the container, which the - Compose healthcheck uses, reaches it too, and it does not run as root. -- `ffmpeg` and Ghostscript run, and WeasyPrint renders a page. Start-up alone - would not show a missing one: the converters call them only for a conversion. -- In the office image, LibreOffice converts a text file to PDF the way the DOCX - converter calls it. - -It runs on every pull request, not only on changes to the Docker files: an app -change can break the image alone, and a workflow that a `paths:` filter skips -never reports, so it could not become a required check. The builds read the -layer cache that the main builds write, and write none. In #174's two runs the -slim image took 50 to 63 seconds and the office image 76 to 88, and both -finished before `lint-and-test`. It is not a required check yet. - -Nothing in it can push, sign or deploy: its token is read-only and it uses no -secret. `notify-ops.yml` now also requires that the Docker run it follows was -started by a push or a manual dispatch in this repository, so a run for a pull -request never leads to a deploy. Tests pin both. A job in a workflow that -pull-request events trigger may hold neither a write token nor a secret; -`_privileged` in `tests/test_supply_chain_hygiene.py` now also recognises -`secrets['X']`, `toJSON(secrets)` and a reusable-workflow call's `secrets:`. -And `notify-ops.yml`'s condition and trigger are pinned word for word. - ### Fixed — API docs: keys are optional, video compression and converter registration described correctly The API reference marked every file route "Authentication: Required", and the From b380083ed308718b6c15078897f2ee4a03a39974 Mon Sep 17 00:00:00 2001 From: MrChengLen Date: Tue, 29 Sep 2026 09:54:12 +0200 Subject: [PATCH 3/7] docs(changelog): entry back on top of the merged changelog Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32dd320..f86fd7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,49 @@ Versions follow [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Added — pull requests build and smoke-test both Docker images + +`docker.yml` builds the images only after merge, and `notify-ops.yml` deploys +every successful main build, so a broken `Dockerfile`, `.dockerignore`, +`entrypoint.sh` or `requirements.lock` used to show up first on its way to +production. On 2026-05-26 a digest pin that BuildKit rejected (`FROM requires +either one or three arguments`) failed three main builds in a row before the +fix was merged. The new `.github/workflows/docker-pr.yml` builds the slim and +the office image on every pull request, without pushing them, and smoke-tests +each one: + +- The image holds `scripts/first_run.py`, the compiled German message catalogue + and `alembic.ini`, and not `.env`, `data/api_keys.json`, `.git`, `.github` or + `tests/`. A `.env` and a `data/api_keys.json` are planted in the checkout + before the build, so the check sees what `.dockerignore` does with a + self-hoster's working folder, not only with a clean checkout. A third planted + file, which nothing ignores, has to reach the image, so the check cannot pass + on a build that did not use that folder. +- The container starts the way `docker-compose.yml` runs it (all capabilities + dropped, no privilege escalation), answers `/api/v1/health` on the published + port and prints a first-run API key. `curl` inside the container, which the + Compose healthcheck uses, reaches it too, and it does not run as root. +- `ffmpeg` and Ghostscript run, and WeasyPrint renders a page. Start-up alone + would not show a missing one: the converters call them only for a conversion. +- In the office image, LibreOffice converts a text file to PDF the way the DOCX + converter calls it. + +It runs on every pull request, not only on changes to the Docker files: an app +change can break the image alone, and a workflow that a `paths:` filter skips +never reports, so it could not become a required check. The builds read the +layer cache that the main builds write, and write none. In #174's two runs the +slim image took 50 to 63 seconds and the office image 76 to 88, and both +finished before `lint-and-test`. It is not a required check yet. + +Nothing in it can push, sign or deploy: its token is read-only and it uses no +secret. `notify-ops.yml` now also requires that the Docker run it follows was +started by a push or a manual dispatch in this repository, so a run for a pull +request never leads to a deploy. Tests pin both. A job in a workflow that +pull-request events trigger may hold neither a write token nor a secret; +`_privileged` in `tests/test_supply_chain_hygiene.py` now also recognises +`secrets['X']`, `toJSON(secrets)` and a reusable-workflow call's `secrets:`. +And `notify-ops.yml`'s condition and trigger are pinned word for word. + ### Fixed — compliance templates describe what the code does The DPA template and its TOM annex, the records-of-processing template, the From def801acf43216465fb4e4cd5f3d695703396e31 Mon Sep 17 00:00:00 2001 From: MrChengLen Date: Tue, 29 Sep 2026 13:49:25 +0200 Subject: [PATCH 4/7] =?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 main moved on (#178, #179, #180 and #170) and adds entries at the top of CHANGELOG.md, so GitHub's update-branch refuses. A merge commit cannot be signed from this machine, so the entry leaves for the merge of main and comes back in the commit after it. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 43 ------------------------------------------- 1 file changed, 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f86fd7d..32dd320 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,49 +9,6 @@ Versions follow [Semantic Versioning](https://semver.org/). ## [Unreleased] -### Added — pull requests build and smoke-test both Docker images - -`docker.yml` builds the images only after merge, and `notify-ops.yml` deploys -every successful main build, so a broken `Dockerfile`, `.dockerignore`, -`entrypoint.sh` or `requirements.lock` used to show up first on its way to -production. On 2026-05-26 a digest pin that BuildKit rejected (`FROM requires -either one or three arguments`) failed three main builds in a row before the -fix was merged. The new `.github/workflows/docker-pr.yml` builds the slim and -the office image on every pull request, without pushing them, and smoke-tests -each one: - -- The image holds `scripts/first_run.py`, the compiled German message catalogue - and `alembic.ini`, and not `.env`, `data/api_keys.json`, `.git`, `.github` or - `tests/`. A `.env` and a `data/api_keys.json` are planted in the checkout - before the build, so the check sees what `.dockerignore` does with a - self-hoster's working folder, not only with a clean checkout. A third planted - file, which nothing ignores, has to reach the image, so the check cannot pass - on a build that did not use that folder. -- The container starts the way `docker-compose.yml` runs it (all capabilities - dropped, no privilege escalation), answers `/api/v1/health` on the published - port and prints a first-run API key. `curl` inside the container, which the - Compose healthcheck uses, reaches it too, and it does not run as root. -- `ffmpeg` and Ghostscript run, and WeasyPrint renders a page. Start-up alone - would not show a missing one: the converters call them only for a conversion. -- In the office image, LibreOffice converts a text file to PDF the way the DOCX - converter calls it. - -It runs on every pull request, not only on changes to the Docker files: an app -change can break the image alone, and a workflow that a `paths:` filter skips -never reports, so it could not become a required check. The builds read the -layer cache that the main builds write, and write none. In #174's two runs the -slim image took 50 to 63 seconds and the office image 76 to 88, and both -finished before `lint-and-test`. It is not a required check yet. - -Nothing in it can push, sign or deploy: its token is read-only and it uses no -secret. `notify-ops.yml` now also requires that the Docker run it follows was -started by a push or a manual dispatch in this repository, so a run for a pull -request never leads to a deploy. Tests pin both. A job in a workflow that -pull-request events trigger may hold neither a write token nor a secret; -`_privileged` in `tests/test_supply_chain_hygiene.py` now also recognises -`secrets['X']`, `toJSON(secrets)` and a reusable-workflow call's `secrets:`. -And `notify-ops.yml`'s condition and trigger are pinned word for word. - ### Fixed — compliance templates describe what the code does The DPA template and its TOM annex, the records-of-processing template, the From dba918e61d6f9e777ee2f3a11b80d904867674c9 Mon Sep 17 00:00:00 2001 From: MrChengLen Date: Tue, 29 Sep 2026 13:49:49 +0200 Subject: [PATCH 5/7] =?UTF-8?q?docs(changelog):=20entry=20back=20on=20top?= =?UTF-8?q?=20of=20the=20merged=20changelog=20=E2=80=94=20without=20what?= =?UTF-8?q?=20#170=20made=20stale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Puts back the entry taken out for the merge of main, on top of the merged [Unreleased] section, without two sentences #170 made stale: docker.yml now builds without a layer cache, so the PR builds no longer read one that main keeps writing, and the timings measured with that cache no longer describe a run. Full suite 1481 green (72 skipped on Windows) on the merged tree; docs only. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a53b5..f4d07e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,47 @@ Versions follow [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Added — pull requests build and smoke-test both Docker images + +`docker.yml` builds the images only after merge, and `notify-ops.yml` deploys +every successful main build, so a broken `Dockerfile`, `.dockerignore`, +`entrypoint.sh` or `requirements.lock` used to show up first on its way to +production. On 2026-05-26 a digest pin that BuildKit rejected (`FROM requires +either one or three arguments`) failed three main builds in a row before the +fix was merged. The new `.github/workflows/docker-pr.yml` builds the slim and +the office image on every pull request, without pushing them, and smoke-tests +each one: + +- The image holds `scripts/first_run.py`, the compiled German message catalogue + and `alembic.ini`, and not `.env`, `data/api_keys.json`, `.git`, `.github` or + `tests/`. A `.env` and a `data/api_keys.json` are planted in the checkout + before the build, so the check sees what `.dockerignore` does with a + self-hoster's working folder, not only with a clean checkout. A third planted + file, which nothing ignores, has to reach the image, so the check cannot pass + on a build that did not use that folder. +- The container starts the way `docker-compose.yml` runs it (all capabilities + dropped, no privilege escalation), answers `/api/v1/health` on the published + port and prints a first-run API key. `curl` inside the container, which the + Compose healthcheck uses, reaches it too, and it does not run as root. +- `ffmpeg` and Ghostscript run, and WeasyPrint renders a page. Start-up alone + would not show a missing one: the converters call them only for a conversion. +- In the office image, LibreOffice converts a text file to PDF the way the DOCX + converter calls it. + +It runs on every pull request, not only on changes to the Docker files: an app +change can break the image alone, and a workflow that a `paths:` filter skips +never reports, so it could not become a required check. It is not a required +check yet. + +Nothing in it can push, sign or deploy: its token is read-only and it uses no +secret. `notify-ops.yml` now also requires that the Docker run it follows was +started by a push or a manual dispatch in this repository, so a run for a pull +request never leads to a deploy. Tests pin both. A job in a workflow that +pull-request events trigger may hold neither a write token nor a secret; +`_privileged` in `tests/test_supply_chain_hygiene.py` now also recognises +`secrets['X']`, `toJSON(secrets)` and a reusable-workflow call's `secrets:`. +And `notify-ops.yml`'s condition and trigger are pinned word for word. + ### Added — the Docker images carry a signed SBOM attestation `docker.yml` now attests the CycloneDX SBOM to each image it pushes — slim and From d1245dd76c14f476a70d0afc57f1f7fdeb48915d Mon Sep 17 00:00:00 2001 From: MrChengLen Date: Tue, 29 Sep 2026 14:31:24 +0200 Subject: [PATCH 6/7] =?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 Every PR adds its CHANGELOG entry at the same place, so each merge to main conflicts with every open PR. Taking this PR's entry out lets GitHub merge main in cleanly; the next commit puts it back on top. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 41 ----------------------------------------- 1 file changed, 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4d07e9..d8a53b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,47 +9,6 @@ Versions follow [Semantic Versioning](https://semver.org/). ## [Unreleased] -### Added — pull requests build and smoke-test both Docker images - -`docker.yml` builds the images only after merge, and `notify-ops.yml` deploys -every successful main build, so a broken `Dockerfile`, `.dockerignore`, -`entrypoint.sh` or `requirements.lock` used to show up first on its way to -production. On 2026-05-26 a digest pin that BuildKit rejected (`FROM requires -either one or three arguments`) failed three main builds in a row before the -fix was merged. The new `.github/workflows/docker-pr.yml` builds the slim and -the office image on every pull request, without pushing them, and smoke-tests -each one: - -- The image holds `scripts/first_run.py`, the compiled German message catalogue - and `alembic.ini`, and not `.env`, `data/api_keys.json`, `.git`, `.github` or - `tests/`. A `.env` and a `data/api_keys.json` are planted in the checkout - before the build, so the check sees what `.dockerignore` does with a - self-hoster's working folder, not only with a clean checkout. A third planted - file, which nothing ignores, has to reach the image, so the check cannot pass - on a build that did not use that folder. -- The container starts the way `docker-compose.yml` runs it (all capabilities - dropped, no privilege escalation), answers `/api/v1/health` on the published - port and prints a first-run API key. `curl` inside the container, which the - Compose healthcheck uses, reaches it too, and it does not run as root. -- `ffmpeg` and Ghostscript run, and WeasyPrint renders a page. Start-up alone - would not show a missing one: the converters call them only for a conversion. -- In the office image, LibreOffice converts a text file to PDF the way the DOCX - converter calls it. - -It runs on every pull request, not only on changes to the Docker files: an app -change can break the image alone, and a workflow that a `paths:` filter skips -never reports, so it could not become a required check. It is not a required -check yet. - -Nothing in it can push, sign or deploy: its token is read-only and it uses no -secret. `notify-ops.yml` now also requires that the Docker run it follows was -started by a push or a manual dispatch in this repository, so a run for a pull -request never leads to a deploy. Tests pin both. A job in a workflow that -pull-request events trigger may hold neither a write token nor a secret; -`_privileged` in `tests/test_supply_chain_hygiene.py` now also recognises -`secrets['X']`, `toJSON(secrets)` and a reusable-workflow call's `secrets:`. -And `notify-ops.yml`'s condition and trigger are pinned word for word. - ### Added — the Docker images carry a signed SBOM attestation `docker.yml` now attests the CycloneDX SBOM to each image it pushes — slim and From 29fefbb7d3185acc9814ba252be948408885157b Mon Sep 17 00:00:00 2001 From: MrChengLen Date: Tue, 29 Sep 2026 14:31:35 +0200 Subject: [PATCH 7/7] docs(changelog): entry back on top of the merged changelog Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c787024..2251691 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,47 @@ Versions follow [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Added — pull requests build and smoke-test both Docker images + +`docker.yml` builds the images only after merge, and `notify-ops.yml` deploys +every successful main build, so a broken `Dockerfile`, `.dockerignore`, +`entrypoint.sh` or `requirements.lock` used to show up first on its way to +production. On 2026-05-26 a digest pin that BuildKit rejected (`FROM requires +either one or three arguments`) failed three main builds in a row before the +fix was merged. The new `.github/workflows/docker-pr.yml` builds the slim and +the office image on every pull request, without pushing them, and smoke-tests +each one: + +- The image holds `scripts/first_run.py`, the compiled German message catalogue + and `alembic.ini`, and not `.env`, `data/api_keys.json`, `.git`, `.github` or + `tests/`. A `.env` and a `data/api_keys.json` are planted in the checkout + before the build, so the check sees what `.dockerignore` does with a + self-hoster's working folder, not only with a clean checkout. A third planted + file, which nothing ignores, has to reach the image, so the check cannot pass + on a build that did not use that folder. +- The container starts the way `docker-compose.yml` runs it (all capabilities + dropped, no privilege escalation), answers `/api/v1/health` on the published + port and prints a first-run API key. `curl` inside the container, which the + Compose healthcheck uses, reaches it too, and it does not run as root. +- `ffmpeg` and Ghostscript run, and WeasyPrint renders a page. Start-up alone + would not show a missing one: the converters call them only for a conversion. +- In the office image, LibreOffice converts a text file to PDF the way the DOCX + converter calls it. + +It runs on every pull request, not only on changes to the Docker files: an app +change can break the image alone, and a workflow that a `paths:` filter skips +never reports, so it could not become a required check. It is not a required +check yet. + +Nothing in it can push, sign or deploy: its token is read-only and it uses no +secret. `notify-ops.yml` now also requires that the Docker run it follows was +started by a push or a manual dispatch in this repository, so a run for a pull +request never leads to a deploy. Tests pin both. A job in a workflow that +pull-request events trigger may hold neither a write token nor a secret; +`_privileged` in `tests/test_supply_chain_hygiene.py` now also recognises +`secrets['X']`, `toJSON(secrets)` and a reusable-workflow call's `secrets:`. +And `notify-ops.yml`'s condition and trigger are pinned word for word. + ### Fixed — self-hosting and security docs: systemd unit, licences and release facts The self-hosting, installation, development, security and licensing docs had