From f49b5b7290de94c6b5ac71270138de2aed869c3a Mon Sep 17 00:00:00 2001 From: Karl Bauer Date: Sun, 11 Oct 2026 15:28:42 +0200 Subject: [PATCH 1/3] ci(release): gated the release on an image build of the tested commit The release run tagged a version before anything had built the image on main: the only pre-release gates were compose and shell validation, and the image was built for the first time after semantic-release had published. A build error then left a version tag and GitHub release without an image. The release was also not pinned, so a push landing during the run was tagged without being built or validated by it. * The PR-only image build now runs on every event without pushing (docker-build-validate) and the release job needs it and requires its success - a failed build skips the release and the image push * modules-semantic-release is called with ref: github.sha, so the run releases exactly the commit it validated and built; if main moved on, it releases nothing and the newer run releases both * The image push stays gated on release-created == 'true', so a PR run, a branch dispatch or a run without a release publishes nothing Prepares the workflow for being dispatched by the Dependabot auto-merge after a merge (workflow_dispatch already existed, no required inputs). --- .github/workflows/docker-release.yml | 69 +++++++++++++++++----------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 7bdd61f..915e28d 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -62,30 +62,68 @@ jobs: severity: 'warning' # ============================================ - # Release Job (only on main branch push) + # Image Build Gate (every event, never pushed) + # ============================================ + + # Builds the image from this commit on every event - the PR CI that + # docker-maintenance.yml waits for, and on a push or dispatch the gate + # of the release: a failed build skips the release and the image push. + docker-build-validate: + name: 🔨 Validate Docker Build + needs: [validate-compose, validate-scripts] + if: | + needs.validate-compose.result == 'success' && + needs.validate-scripts.result == 'success' + uses: bauer-group/automation-templates/.github/workflows/docker-build.yml@main + with: + publish-to: 'ghcr' + ghcr-image-name: 'bauer-group/CS-ClientPublicIP/clientpublicip' + + auto-tags: true + dockerfile-path: './src/Dockerfile' + docker-context: './src' + platforms: 'linux/amd64' + + push: false + + security-scan: false + security-fail-on: 'CRITICAL' + secrets: inherit + + # ============================================ + # Release Job (push to main or dispatch) # ============================================ release: name: 📦 Create Semantic Release - needs: [validate-compose, validate-scripts] + needs: [validate-compose, validate-scripts, docker-build-validate] if: | (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.validate-compose.result == 'success' && - needs.validate-scripts.result == 'success' + needs.validate-scripts.result == 'success' && + needs.docker-build-validate.result == 'success' uses: bauer-group/automation-templates/.github/workflows/modules-semantic-release.yml@main with: target-branch: 'main' + # Release the commit this run validated and built - the image job + # below builds it too. Unpinned, the release would take the tip of + # main: a push that landed meanwhile would be tagged untested and be + # missing from the image. Pinned, nothing is released once main has + # moved on, and the run for the newer main releases both. + ref: ${{ github.sha }} dry-run: false force-release: ${{ inputs.force-release || false }} secrets: inherit # ============================================ - # Docker Build Jobs + # Docker Publish Job (only after a release) # ============================================ docker-build-release: name: 🐳 Build & Push Docker Image needs: release + # Only a run that created a release publishes - never a PR, a branch + # dispatch or a run that released nothing. if: needs.release.outputs.release-created == 'true' uses: bauer-group/automation-templates/.github/workflows/docker-build.yml@main with: @@ -111,26 +149,3 @@ jobs: generate-sbom: true sync-dockerhub-readme: true secrets: inherit - - docker-build-pr: - name: 🔨 Validate Docker Build (PR) - needs: [validate-compose, validate-scripts] - if: | - github.event_name == 'pull_request' && - needs.validate-compose.result == 'success' && - needs.validate-scripts.result == 'success' - uses: bauer-group/automation-templates/.github/workflows/docker-build.yml@main - with: - publish-to: 'ghcr' - ghcr-image-name: 'bauer-group/CS-ClientPublicIP/clientpublicip' - - auto-tags: true - dockerfile-path: './src/Dockerfile' - docker-context: './src' - platforms: 'linux/amd64' - - push: false - - security-scan: false - security-fail-on: 'CRITICAL' - secrets: inherit From 89539bd02651c1cafbb0fb1162fbafffae3af70e Mon Sep 17 00:00:00 2001 From: Karl Bauer Date: Sun, 11 Oct 2026 15:28:43 +0200 Subject: [PATCH 2/3] ci(dependabot): dispatched the release after an auto-merge The auto-merge merges with the GITHUB_TOKEN, and a push made with it starts no workflow, so a merged Dependabot update was never released until the next human push. * release-workflow set to docker-release.yml: after a merge - and only then - the module dispatches it on main, waits for the run and fails the job if the release does not succeed * actions: write granted for the dispatch and reading the run; checks and statuses read added as documented by the module, so CI results stay readable should the repository ever become private * required-workflows and the unfiltered pull_request trigger unchanged --- .github/workflows/docker-maintenance.yml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-maintenance.yml b/.github/workflows/docker-maintenance.yml index 1f8272e..3a174d9 100644 --- a/.github/workflows/docker-maintenance.yml +++ b/.github/workflows/docker-maintenance.yml @@ -5,9 +5,13 @@ # merges only after docker-release.yml built the image on the PR (base # image and pip updates in src/); everything else stays open with an # annotation. -# After merge, the docker-release.yml workflow is triggered which: +# A merge made with the GITHUB_TOKEN starts no push workflow, so after the +# merge this workflow dispatches docker-release.yml on main and waits for +# it (release-workflow). That run builds the merged commit again and only +# then: # 1. Creates a semantic release # 2. Builds and pushes the new Docker image +# A release run that does not succeed turns this job red. # # Flow: # ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ @@ -33,8 +37,11 @@ on: # changes under .github/ always stay open for review. permissions: - contents: write - pull-requests: write + contents: write # merge + pull-requests: write # approve, read the PR + checks: read # check runs and suites of the PR head commit + statuses: read # commit statuses of the PR head commit + actions: write # dispatch the release; read workflow runs jobs: maintenance: @@ -48,4 +55,8 @@ jobs: # image in src/Dockerfile and the pip packages in src/requirements.txt # that the build installs; a PR outside them stays open (not tested). required-workflows: '.github/workflows/docker-release.yml' + # After a merge - and only then - dispatch the release on main and + # wait for it: it rebuilds the merged commit before it releases it, + # and a failed release run fails this job. + release-workflow: '.github/workflows/docker-release.yml' secrets: inherit From 193724676f1af2d834d7b35aa72fc0010301e2f1 Mon Sep 17 00:00:00 2001 From: Karl Bauer Date: Sun, 11 Oct 2026 15:28:45 +0200 Subject: [PATCH 3/3] ci(dependabot): released merged runtime updates via fix commit prefixes The commit prefix of a merged Dependabot update decides whether semantic-release cuts a release and the image is rebuilt. * docker: dropped include: "scope" - with a prefix that already has a scope Dependabot appends "(deps)" and produces the malformed "fix(docker)(deps): ..." subject * pip (/src): added an entry so security updates of the packages installed into the image use fix(deps) instead of the detected chore(deps), which released nothing (see the flask update in #1). Version updates stay off (open-pull-requests-limit: 0) as before; security updates are not subject to the limit * github-actions keeps chore(ci); labels and schedules unchanged --- .github/dependabot.yml | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 969fb7f..6a7760b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,6 +3,10 @@ # ============================================================================= # Monitors Docker base images for updates # Combined with docker-maintenance.yml workflow for auto-merge and release +# +# The commit prefix decides whether a merged update is released: fix(...) +# cuts a PATCH release and rebuilds the image, chore(...) releases nothing. +# Everything that ends up in the shipped image uses fix(...). # ============================================================================= version: 2 @@ -19,11 +23,33 @@ updates: - "dependencies" - "docker" - "dependabot" + # No include: "scope" - the prefix already has one, and Dependabot would + # append "(deps)" to it (fix(docker)(deps): ...). commit-message: prefix: "fix(docker)" - include: "scope" open-pull-requests-limit: 5 + # Python packages installed into the image (src/requirements.txt). + # Version updates are off (limit 0) as before; the entry sets the commit + # prefix of Dependabot's security updates, which are not subject to the + # limit - without it they arrive as chore(deps) and a merged security fix + # is never released. + - package-ecosystem: "pip" + directory: "/src" + schedule: + interval: "weekly" + day: "sunday" + time: "06:30" + timezone: "Etc/UTC" + labels: + - "dependencies" + - "python" + - "dependabot" + commit-message: + prefix: "fix(deps)" + prefix-development: "chore(deps-dev)" + open-pull-requests-limit: 0 + # GitHub Actions updates - package-ecosystem: "github-actions" directory: "/"