From 33f117a993a788c11d7e559a0c063868c973196d Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 16 Sep 2026 18:46:28 +0000 Subject: [PATCH 1/7] Install the Unicode font machinery in the base image 'Common.list' carried the fonts - 'lm', 'lm-math', 'noto', 'notomath', 'libertinus-otf' - but nothing that loads them. 'pytooling.sty' requires 'unicode-math' and 'newunicodechar' directly, 'unicode-math' needs 'fontspec', and LuaTeX's OpenType loader is 'luaotfload' with 'lualibs'. None of them was listed, so the image obtained them at run time through '[MPM]AutoInstall=1', which needs a package database the container never refreshed. 'luatex85' comes from the consumer side: Sphinx's LaTeX output requires it for LuaTeX >= 0.85, and 'pytooling/miktex:sphinx' has been failing there with "module 'luaotfload-main' not found" and "Sphinx requires luatex85.sty". Unicode font support belongs to the base image rather than to the Sphinx variant, so the six packages go into 'Common.list'. Co-Authored-By: Patrick Lehmann --- Common.list | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Common.list b/Common.list index f3ef291..a3c5969 100644 --- a/Common.list +++ b/Common.list @@ -38,6 +38,7 @@ eurosym float fmtcount # fontenc -> part of base installation +fontspec footmisc # footnote -> part of base installation fourier @@ -76,12 +77,16 @@ lm lm-math # longtable -> part of base installation ltxcmds +lualibs +luaotfload +luatex85 makecell marvosym mdwtools mptopdf # multicol -> part of base installation multirow +newunicodechar noto noto-emoji notomath @@ -114,6 +119,7 @@ tcolorbox tocbibind todonotes translations +unicode-math uniquecounter url # varioref -> part of base installation From 8b495d17f499f9fe769ea9dd07c89facca1c99e7 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 16 Sep 2026 19:14:35 +0000 Subject: [PATCH 2/7] Replace 'l3backend', which MiKTeX's repository no longer offers Every image build fails in 'miktex packages install' with "Sorry, but: The requested package is unknown.", and the '|| cat /var/log/miktex/mpmcli_admin.log' fallback prints "No such file or directory", so the log never names the package. Checking the repository manifest itself does: 'miktex-zzdb1-2.9.tar.lzma' holds 'mpm.ini' with 9099 package sections, and of the 107 entries in 'Common.list' exactly one is missing from it - 'l3backend'. 'l3kernel' and 'l3packages' are both present; 'l3kernel' takes its place, 'l3packages' stays as it was. Which mirror a run happens to visit decides whether it fails: runs at 18:46 and 18:59 UTC passed, and 19:08 (tug.ctan.org) and 19:20 (mirrors.rit.edu) failed with the same list. The mirrors that already carry the current database reject the package, so this gets worse as the rest catch up. Co-Authored-By: Patrick Lehmann --- Common.list | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Common.list b/Common.list index f3ef291..f40407b 100644 --- a/Common.list +++ b/Common.list @@ -60,7 +60,7 @@ koma-script kvdefinekeys kvoptions kvsetkeys -l3backend +l3kernel l3packages lastpage latexmk From 0710b46437282be0088c23228dddb4360a5ae5fa Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 16 Sep 2026 19:27:03 +0000 Subject: [PATCH 3/7] Fail the image build step when the build failed 'Docker.buildx.sh' counts nothing: it sets 'Counter_Error=0', recognizes build errors with 'Pattern_ERROR' and exits with 'exit $Counter_Error', but the branch matching that pattern only prints the line in red. The counter stays 0, so the filter always exits 0. Both build steps pipe 'docker buildx build' into it and carry no 'shell:' key, so they run under 'bash -e' without '-o pipefail' and the step's status is the filter's. A failed build therefore passed its own step, and the job died in the next one with 'No such image: ' and "numfmt: invalid number: ''" - an error naming the wrong step, which reads like a tagging problem. Run 35138670702 is an example: a package missing from 'Common.list' looked like a fault of the change under test. The error branch increments the counter again, and both build steps set '-o pipefail' so that 'docker' failing without printing 'ERROR:' is caught too. Checked against the filter directly: a clean log exits 0, a log with two 'ERROR:' lines exits 2, and the same log exits 0 on the current 'dev'. Co-Authored-By: Patrick Lehmann --- .github/workflows/Pipeline.yml | 6 ++++++ Docker.buildx.sh | 1 + 2 files changed, 7 insertions(+) diff --git a/.github/workflows/Pipeline.yml b/.github/workflows/Pipeline.yml index 8752edc..d57058c 100644 --- a/.github/workflows/Pipeline.yml +++ b/.github/workflows/Pipeline.yml @@ -89,6 +89,9 @@ jobs: - name: 🐋 Building MiKTeX image id: build run: | + # Without this the status of 'docker buildx build | Docker.buildx.sh' is the filter's alone. + set -o pipefail + ANSI_RED=$'\x1b[31m' ANSI_GREEN=$'\x1b[32m' ANSI_YELLOW=$'\x1b[33m' @@ -299,6 +302,9 @@ jobs: - name: 🐋 Building MiKTeX image for ${{ matrix.name }} id: build run: | + # Without this the status of 'docker buildx build | Docker.buildx.sh' is the filter's alone. + set -o pipefail + ANSI_RED=$'\x1b[31m' ANSI_GREEN=$'\x1b[32m' ANSI_YELLOW=$'\x1b[33m' diff --git a/Docker.buildx.sh b/Docker.buildx.sh index d25f25e..50f2e87 100755 --- a/Docker.buildx.sh +++ b/Docker.buildx.sh @@ -124,6 +124,7 @@ while IFS=$'\n' read -r line; do printf "%s\n" "${INDENT}${ANSI_GREEN}${line}${ANSI_NOCOLOR}" group=0 elif [[ "${line}" =~ $Pattern_ERROR ]]; then + Counter_Error=$((Counter_Error + 1)) printf "%s\n" "${INDENT}${ANSI_LIGHT_RED}${line}${ANSI_NOCOLOR}" elif [[ "${line}" =~ $Pattern_CANCELED ]]; then printf "%s\n" "${INDENT}${ANSI_LIGHT_RED}${line}${ANSI_NOCOLOR}" From c3651b7dd974678da4f14ffd5b8e03b480fcc910 Mon Sep 17 00:00:00 2001 From: Patrick Lehmann Date: Wed, 16 Sep 2026 21:33:47 +0200 Subject: [PATCH 4/7] Apply batched suggestions from code review Co-authored-by: Patrick Lehmann --- .github/workflows/Pipeline.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/Pipeline.yml b/.github/workflows/Pipeline.yml index d57058c..3c200e3 100644 --- a/.github/workflows/Pipeline.yml +++ b/.github/workflows/Pipeline.yml @@ -89,7 +89,6 @@ jobs: - name: 🐋 Building MiKTeX image id: build run: | - # Without this the status of 'docker buildx build | Docker.buildx.sh' is the filter's alone. set -o pipefail ANSI_RED=$'\x1b[31m' @@ -302,7 +301,6 @@ jobs: - name: 🐋 Building MiKTeX image for ${{ matrix.name }} id: build run: | - # Without this the status of 'docker buildx build | Docker.buildx.sh' is the filter's alone. set -o pipefail ANSI_RED=$'\x1b[31m' From 554218beb0cef4c64f929a586d65133e0863d41e Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 16 Sep 2026 19:07:41 +0000 Subject: [PATCH 5/7] Promote images to their official tags only after the checks pass Every run tagged its images 'dev', 'release' or 'latest' before a single check had run, and pushed them right after the build. A push to any branch therefore replaced 'miktex:dev' and 'miktex:sphinx-dev' for everyone, and an image that failed its checks stayed under an official tag until the next run overwrote it. The images are now built, pushed and tested as 'ci-' and 'ci--'. 'SanityCheck', 'Document' and 'SanityCheck-Sphinx' consume those, so the promoted image is the one that was tested, bit for bit. The new 'Publish' job moves the official tags onto it with 'docker buildx imagetools create', which copies the manifest inside the registry: no pull, and the tags share a digest. It needs every checking job, and runs only where an official tag is defined - a release tag ('latest'), a release commit ('release') and the 'dev' branch ('dev'). On every other branch 'publish_official' is 'false' and no official tag is written at all. 'TriggerTaggedRelease' and 'ReleasePage' now need 'Publish', so a release is neither tagged nor published when its images didn't reach the registry. Co-Authored-By: Patrick Lehmann --- .github/workflows/Pipeline.yml | 104 +++++++++++++++++++++++++++++---- 1 file changed, 94 insertions(+), 10 deletions(-) diff --git a/.github/workflows/Pipeline.yml b/.github/workflows/Pipeline.yml index 3c200e3..2fc16ae 100644 --- a/.github/workflows/Pipeline.yml +++ b/.github/workflows/Pipeline.yml @@ -40,6 +40,9 @@ jobs: miktex_image_tag: ${{ steps.variables.outputs.miktex_image_tag }} miktex_image: ${{ steps.variables.outputs.miktex_image }} specific_tag_extension: ${{ steps.variables.outputs.specific_tag_extension }} + short_sha: ${{ steps.variables.outputs.short_sha }} + publish_official: ${{ steps.variables.outputs.publish_official }} + miktex_official_tag: ${{ steps.variables.outputs.miktex_official_tag }} miktex_version: ${{ steps.test.outputs.miktex_version }} steps: @@ -55,15 +58,26 @@ jobs: miktex_source_repository="${base_image_os_codename,,}" image_name="miktex" + + # Built, pushed and tested as 'ci-'; the 'Publish' job moves the official tags onto it. + short_sha="${GITHUB_SHA::7}" + if [[ "${{ needs.Prepare.outputs.is_release_tag}}" == "true" ]]; then - image_tag="latest" + official_tag="latest" specific_tag_extension="" + publish_official="true" elif [[ "${{ needs.Prepare.outputs.is_release_commit}}" == "true" ]]; then - image_tag="release" + official_tag="release" specific_tag_extension="-release" - else - image_tag="dev" + publish_official="true" + elif [[ "${GITHUB_REF}" == "refs/heads/dev" ]]; then + official_tag="dev" specific_tag_extension="-dev" + publish_official="true" + else + official_tag="" + specific_tag_extension="" + publish_official="false" fi tee "${GITHUB_OUTPUT}" < %-16s " "${1}" "${2}" + output="$(docker buildx imagetools create --tag "${namespace}/${name}:${2}" "${namespace}/${name}:${1}" 2>&1)" + if [[ $? -eq 0 ]]; then + printf -- "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}\n" + else + printf -- "${ANSI_LIGHT_RED}[FAILED]${ANSI_NOCOLOR}\n" + printf -- "%s\n" "$(sed 's/^/ ↪ /g' <<< "${output}")" + printf -- "::error title=Publish::Promoting '%s' to '%s' failed.\n" "${1}" "${2}" + errorCount=$((errorCount + 1)) + fi + } + + printf -- "Promoting the images tested in this run:\n" + Promote "ci-${shortSHA}" "${{ needs.MiKTeX.outputs.miktex_official_tag }}" + for variant in "${variants[@]}"; do + Promote "ci-${variant}-${shortSHA}" "${variant}${extension}" + done + + if [[ ${errorCount} -ne 0 ]]; then + printf -- "${ANSI_LIGHT_RED}%s image(s) couldn't be promoted.${ANSI_NOCOLOR}\n" "${errorCount}" + exit 1 + fi + + printf -- "${ANSI_LIGHT_GREEN}Every image was promoted.${ANSI_NOCOLOR}\n" + TriggerTaggedRelease: uses: pyTooling/Actions/.github/workflows/TagReleaseCommit.yml@r7 needs: @@ -454,6 +536,7 @@ jobs: - SanityCheck - Document - SanityCheck-Sphinx + - Publish if: needs.Prepare.outputs.is_release_commit == 'true' && github.event_name != 'schedule' permissions: contents: write # required for create tag @@ -470,6 +553,7 @@ jobs: - SanityCheck - Document - SanityCheck-Sphinx + - Publish if: needs.Prepare.outputs.is_release_tag == 'true' permissions: contents: write From 2277363bf5033c449fc7a7b305b0b0724ca34c41 Mon Sep 17 00:00:00 2001 From: Patrick Lehmann Date: Wed, 16 Sep 2026 21:37:41 +0200 Subject: [PATCH 6/7] Minor updates. --- .github/workflows/Pipeline.yml | 2 +- example/sphinx/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Pipeline.yml b/.github/workflows/Pipeline.yml index 8752edc..db3a8f5 100644 --- a/.github/workflows/Pipeline.yml +++ b/.github/workflows/Pipeline.yml @@ -383,7 +383,7 @@ jobs: uses: actions/checkout@v7 - name: 🐍 Setup Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14" diff --git a/example/sphinx/requirements.txt b/example/sphinx/requirements.txt index 4ddfcf5..8bd4e57 100644 --- a/example/sphinx/requirements.txt +++ b/example/sphinx/requirements.txt @@ -9,6 +9,6 @@ docutils_stubs ~= 0.0.22 sphinx_rtd_theme ~= 3.1 # Sphinx Extenstions -sphinxcontrib-mermaid ~= 2.0 +sphinxcontrib-mermaid ~= 2.1 sphinx_design ~= 0.7.0 sphinx-copybutton >= 0.5.2 From 8ae7c5bb69689d2a13a37b22d07d9afa72d7bc97 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 16 Sep 2026 19:42:42 +0000 Subject: [PATCH 7/7] Move the reusable workflows to '@r8' 'PrepareJob.yml', 'TagReleaseCommit.yml' and 'PublishReleaseNotes.yml' were pinned to '@r7' while '@r8' is the current major line. 'TagReleaseCommit.yml' is identical in both. The other two differ in one pattern only: 'FATHER_SHA' became 'SECOND_PARENT_SHA', and with it '"{FATHER_SHA}" == ""' became '"${SECOND_PARENT_SHA}" == ""' - the '@r7' version compares a literal string, so the guard for an empty second parent never fires. That is the step which finds the release pull-request behind a merge commit, and its description is what becomes the release notes. No input, output or trigger differs between the two lines. Co-Authored-By: Patrick Lehmann --- .github/workflows/Pipeline.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Pipeline.yml b/.github/workflows/Pipeline.yml index db3a8f5..5f4d611 100644 --- a/.github/workflows/Pipeline.yml +++ b/.github/workflows/Pipeline.yml @@ -22,7 +22,7 @@ on: jobs: Prepare: - uses: pyTooling/Actions/.github/workflows/PrepareJob.yml@r7 + uses: pyTooling/Actions/.github/workflows/PrepareJob.yml@r8 MiKTeX: name: 🐋 Build MiKTeX base-image @@ -444,7 +444,7 @@ jobs: # retention-days: 1 TriggerTaggedRelease: - uses: pyTooling/Actions/.github/workflows/TagReleaseCommit.yml@r7 + uses: pyTooling/Actions/.github/workflows/TagReleaseCommit.yml@r8 needs: - Prepare - SanityCheck @@ -460,7 +460,7 @@ jobs: secrets: inherit ReleasePage: - uses: pyTooling/Actions/.github/workflows/PublishReleaseNotes.yml@r7 + uses: pyTooling/Actions/.github/workflows/PublishReleaseNotes.yml@r8 needs: - Prepare - SanityCheck