From 42aeed7bc082011863c7724f8a8b0d5b005bdf71 Mon Sep 17 00:00:00 2001 From: "Matthew (BlueT) Lien" Date: Fri, 8 May 2026 07:18:43 +0800 Subject: [PATCH 1/5] ci: add Docker Hub + GHCR publish workflow on release Wires up automatic container image publishing to both Docker Hub (`bluet/proxybroker2`) and GitHub Container Registry (`ghcr.io/bluet/proxybroker2`) on every GitHub release. Mirrors the trigger model of `python-publish.yml` so a single release event ships PyPI, Docker Hub, and GHCR artifacts together. Choices and rationale: - **Multi-arch (amd64 + arm64)**: Apple Silicon and AWS Graviton users were previously stuck with QEMU-emulated amd64 images. Native arm64 builds via `docker/setup-qemu-action` + `docker/setup-buildx-action`. - **Push to both registries simultaneously**: GHCR is free, has no rate limits for our scale, and uses the built-in GITHUB_TOKEN (no extra secret to manage). Docker Hub remains the primary endpoint per README. `docker/build-push-action` writes both in one pass - no double build cost. - **Tag strategy via `docker/metadata-action`**: - `:VERSION` always (e.g. `:2.0.0b2`). - `:major`, `:major.minor`, `:latest` ONLY on stable releases (`!github.event.release.prerelease`). A beta release won't silently advance the `:latest` tag - users pinning `:latest` stay on the last stable. - `workflow_dispatch` runs get a `:manual-` tag so they don't collide with release tags. - **GHA cache backend**: layer cache survives between runs so reruns on the same SHA finish in seconds. - **`workflow_dispatch` with ref input**: lets a maintainer re-run after a transient registry failure or token rotation without cutting a fresh release. Required repo secrets (one-time setup before first run): DOCKERHUB_USERNAME - Docker Hub account/org name DOCKERHUB_TOKEN - Hub access token, scoped to proxybroker2 GHCR needs no setup; uses the built-in GITHUB_TOKEN. Manual trigger for the just-published v2.0.0b2 (after secrets land): gh workflow run docker-publish.yml \ --repo bluet/proxybroker2 --ref master \ --field ref=v2.0.0b2 Co-Authored-By: Claude Opus 4.7 --- .github/workflows/docker-publish.yml | 93 ++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 .github/workflows/docker-publish.yml diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 00000000..a286aea3 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,93 @@ +name: Publish Docker Image + +# Triggers: +# - GitHub Release published: builds + pushes the image automatically. +# Same trigger as python-publish.yml, so one release ships both +# PyPI and Docker artifacts. +# - workflow_dispatch: lets a maintainer re-run after a transient +# registry failure or a token rotation, without cutting a new release. +on: + release: + types: [published] + workflow_dispatch: + inputs: + ref: + description: "Git ref (tag/branch/SHA) to build from" + required: true + default: "master" + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write # required to push to ghcr.io + steps: + - name: Check out source + uses: actions/checkout@v4 + with: + # On release events the default checkout is the tag. + # On workflow_dispatch we honor the user-supplied ref. + # All branches/tags/SHAs are repo-controlled, not arbitrary + # untrusted text - safe to pass to the checkout action. + ref: ${{ github.event.inputs.ref || github.ref }} + + # Multi-arch builds need QEMU for cross-compilation under buildx. + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + # Buildx is the modern docker builder; supports multi-platform output. + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Docker Hub login. Token must be a Hub access token (NOT a password) + # scoped to the proxybroker2 repository for least-privilege. + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # GHCR login uses the workflow's built-in GITHUB_TOKEN (no extra secret + # needed). The `packages: write` permission above is what authorises it. + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # docker/metadata-action computes the tag list from the git ref using + # semver rules. Strategy: + # - Version tag (e.g. `2.0.0b2`) always pushed. + # - `latest`, `2`, `2.0` only pushed for STABLE releases. The + # `enable=...!github.event.release.prerelease` guard means a + # beta release (which we mark as prerelease in the GH UI) won't + # advance these floating tags. + - name: Extract image metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: | + bluet/proxybroker2 + ghcr.io/bluet/proxybroker2 + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}},enable=${{ github.event_name == 'release' && !github.event.release.prerelease }} + type=semver,pattern={{major}},enable=${{ github.event_name == 'release' && !github.event.release.prerelease }} + type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }} + type=raw,value=manual-{{date 'YYYYMMDDHHmmss'}},enable=${{ github.event_name == 'workflow_dispatch' }} + + # Build once, push to both registries. cache-from/to use the GitHub + # Actions cache backend so a second run on the same code reuses + # layers and finishes in seconds instead of minutes. + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max From 2c131377d82f874bd47307dbdc208fe0ad21a270 Mon Sep 17 00:00:00 2001 From: "Matthew (BlueT) Lien" Date: Fri, 8 May 2026 07:40:02 +0800 Subject: [PATCH 2/5] ci: don't gate floating Docker tags on prerelease Original guard was over-conservative: `:latest`, `:major`, and `:major.minor` only fired on STABLE releases. Problem: this project has been in beta for over a year. Locking those tags to stable releases means users pulling `:latest` would be stuck on a year-old beta (`v2.0.0b1`, May 2025) until 2.0.0 ships - which defeats the purpose of having a floating `:latest`. Drop the prerelease guard on `:latest` / `:major` / `:major.minor`. A user pulling `:2` wants the most recent 2.x.y; a user pulling `:latest` wants the most recent published version. Both meanings include prereleases. `:latest` keeps a narrower guard (`event_name == 'release'`) so manual-trigger reruns don't accidentally move it; that guard is about source of truth, not stability. Acknowledged side-effect documented in the comment block: if we ever release a patch on an old version line (e.g. 1.5.1 after 2.0.0), `:latest` would move backward. Not maintaining old lines today; switch to `flavor: latest=auto` if that changes. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/docker-publish.yml | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index a286aea3..1daacc5a 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -59,11 +59,21 @@ jobs: # docker/metadata-action computes the tag list from the git ref using # semver rules. Strategy: - # - Version tag (e.g. `2.0.0b2`) always pushed. - # - `latest`, `2`, `2.0` only pushed for STABLE releases. The - # `enable=...!github.event.release.prerelease` guard means a - # beta release (which we mark as prerelease in the GH UI) won't - # advance these floating tags. + # - `:VERSION` (e.g. `:2.0.0b2`) always pushed - exact version pin. + # - `:major`, `:major.minor`, `:latest` ALSO pushed on every release, + # INCLUDING prereleases. Rationale: this project has been in beta + # for over a year. If `:latest` only moved on stable releases, + # users pulling `:latest` would be stuck on a year-old beta until + # 2.0.0 ships. A user who pulls `:2` wants the most recent 2.x.y, + # not the most recent stable 2.x.y - same for `:latest`. + # - `:latest` is gated on release events only (not workflow_dispatch) + # so a manual rerun from older code never accidentally advances + # the floating tag. + # - Side-effect: if a future patch is released for an OLDER version + # line (e.g. 1.5.1 after 2.0.0), it would advance `:latest` + # backward. Acceptable for now - this project doesn't maintain + # old version lines. Switch to `flavor: latest=auto` if that + # ever changes. - name: Extract image metadata (tags, labels) id: meta uses: docker/metadata-action@v5 @@ -73,9 +83,9 @@ jobs: ghcr.io/bluet/proxybroker2 tags: | type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}},enable=${{ github.event_name == 'release' && !github.event.release.prerelease }} - type=semver,pattern={{major}},enable=${{ github.event_name == 'release' && !github.event.release.prerelease }} - type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable=${{ github.event_name == 'release' }} type=raw,value=manual-{{date 'YYYYMMDDHHmmss'}},enable=${{ github.event_name == 'workflow_dispatch' }} # Build once, push to both registries. cache-from/to use the GitHub From 149645c8e5b67f5f9c87fdb329a72424398fe8ef Mon Sep 17 00:00:00 2001 From: "Matthew (BlueT) Lien" Date: Fri, 8 May 2026 08:04:42 +0800 Subject: [PATCH 3/5] ci: SHA-pin Docker workflow actions; add grouped Dependabot for monthly bumps SonarCloud Quality Gate failed on PR #204 with 6 hotspots from rule `githubactions:S7637` ("Use full commit SHA hash for this dependency") on the new docker-publish.yml. Tag-pinned actions (`@v3`, `@v5`) are mutable - the publisher can re-point them, and a compromised publisher account would let an attacker swap in a malicious commit. The reviewdog/action-setup compromise (March 2025) is a recent example of why this is a real concern, not just a theoretical one. The project's existing `python-publish.yml` already SHA-pins its one external action; bringing docker-publish.yml in line with that convention. Resolved each tag to its commit SHA via the GitHub API; kept the human-readable tag in a trailing comment so the version is still scannable in code review. Adding `.github/dependabot.yml` to keep the SHA-pinned actions from silently rotting: - Bumps grouped into ONE PR per month (avoids the per-action-per-release flood that put me off SHA-pinning in the past). - Security advisories fire IMMEDIATELY regardless of the monthly schedule - that path is internal to Dependabot and not affected by `interval`. So a CVE in one of these actions becomes a same-day PR, not a 30-day-old one. Co-Authored-By: Claude Opus 4.7 --- .github/dependabot.yml | 25 +++++++++++++++++++++++++ .github/workflows/docker-publish.yml | 14 +++++++------- 2 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..7b910f9b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,25 @@ +# Dependabot configuration. +# +# Goals: +# - Stay safe: SHA-pinned third-party Actions get bumped when they +# release new versions, so we don't fall behind for years. +# - Stay quiet: routine bumps land in ONE grouped PR per month, not +# one PR per action per release. (Past experience: ungrouped +# SHA-pinning floods the inbox; the grouping config below avoids it.) +# - Stay reactive on CVEs: security-advisory PRs ALWAYS fire +# immediately regardless of the schedule below. The `interval` only +# affects routine version updates, not the security-update path. +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + gha-deps: + patterns: + - "*" + # Use a single label so monthly grouped PRs are easy to filter. + labels: + - "dependencies" + - "github-actions" diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 1daacc5a..abad9755 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -24,7 +24,7 @@ jobs: packages: write # required to push to ghcr.io steps: - name: Check out source - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: # On release events the default checkout is the tag. # On workflow_dispatch we honor the user-supplied ref. @@ -34,16 +34,16 @@ jobs: # Multi-arch builds need QEMU for cross-compilation under buildx. - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 # Buildx is the modern docker builder; supports multi-platform output. - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 # Docker Hub login. Token must be a Hub access token (NOT a password) # scoped to the proxybroker2 repository for least-privilege. - name: Log in to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -51,7 +51,7 @@ jobs: # GHCR login uses the workflow's built-in GITHUB_TOKEN (no extra secret # needed). The `packages: write` permission above is what authorises it. - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: registry: ghcr.io username: ${{ github.actor }} @@ -76,7 +76,7 @@ jobs: # ever changes. - name: Extract image metadata (tags, labels) id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 with: images: | bluet/proxybroker2 @@ -92,7 +92,7 @@ jobs: # Actions cache backend so a second run on the same code reuses # layers and finishes in seconds instead of minutes. - name: Build and push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 with: context: . platforms: linux/amd64,linux/arm64 From 9e37ac28fe16048e65f96a9c57a06d60d2b3ea4d Mon Sep 17 00:00:00 2001 From: "Matthew (BlueT) Lien" Date: Fri, 8 May 2026 08:28:41 +0800 Subject: [PATCH 4/5] ci(docker): fix three real bugs flagged by Codex/CodeRabbit review PR #204 review surfaced three honest bugs in the workflow that all would have made the v2.0.0b2 publish silently fail or produce wrong tags. Fixing them per reviewer guidance. (a) PEP 440 vs SemVer: this project tags releases in PEP 440 form (`v2.0.0b2`), not SemVer (`v2.0.0-beta.2`). docker/metadata-action's `type=semver,pattern={{version}}` only parses SemVer and would silently SKIP `v2.0.0b2`. Switch to `type=pep440,pattern={{version}}` which is documented for Python prerelease forms (`b2`, `rc1`, etc.). (b) Floating aliases for prereleases: metadata-action's `{{major}}` and `{{major}}.{{minor}}` patterns deliberately hold back floating aliases for prerelease versions - they emit the full prerelease version instead of the major/minor truncation. We explicitly want these aliases to advance for prereleases (per the existing comment block: this project has been in beta for a year; locking floating tags to stable would freeze them on `v2.0.0b1` from May 2025). Use `type=match` with regex against the git tag name instead; `type=match` doesn't apply the prerelease guard, so floating aliases advance correctly. (c) workflow_dispatch + metadata-action ref mismatch: previously the workflow took an `inputs.ref` parameter and passed it to checkout. But docker/metadata-action reads GITHUB_REF (which stays at the workflow file's branch = master for workflow_dispatch), not the checkout's ref. So a manual rerun with `--field ref=v2.0.0b2` would only generate `:manual-`, not the version tags - silently mis-publish. Fix by removing the `inputs.ref` input entirely. workflow_dispatch now always builds from master HEAD with a `:manual-` tag - useful for testing the workflow itself. To publish a specific tagged release, re-fire the release event by deleting + recreating the GitHub release. Documented in the trigger comment block. Net effect: the v2.0.0b2 retroactive publish (delete + recreate GitHub release) will correctly produce: - bluet/proxybroker2:2.0.0b2 (exact pin, type=pep440) - bluet/proxybroker2:2.0 (major.minor, type=match) - bluet/proxybroker2:2 (major, type=match) - bluet/proxybroker2:latest (type=raw + release event guard) - same five on ghcr.io/bluet/proxybroker2 Refs: - CodeRabbit comment on docker-publish.yml:89 (PEP 440 vs SemVer) - chatgpt-codex on docker-publish.yml:33 (workflow_dispatch ref) - chatgpt-codex on docker-publish.yml:85 (PEP 440 parsing) - chatgpt-codex on docker-publish.yml:87 (floating aliases for prereleases) Co-Authored-By: Claude Opus 4.7 --- .github/workflows/docker-publish.yml | 77 ++++++++++++++++------------ 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index abad9755..8bbd9843 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -4,17 +4,18 @@ name: Publish Docker Image # - GitHub Release published: builds + pushes the image automatically. # Same trigger as python-publish.yml, so one release ships both # PyPI and Docker artifacts. -# - workflow_dispatch: lets a maintainer re-run after a transient -# registry failure or a token rotation, without cutting a new release. +# - workflow_dispatch: lets a maintainer kick a build of master HEAD, +# useful for testing the workflow itself. Does NOT publish version +# tags - to (re)publish a specific tagged release, re-fire the +# release event by deleting + recreating the GitHub release. +# Reason: docker/metadata-action reads GITHUB_REF, which stays at +# the workflow's branch (master) for workflow_dispatch even when +# checkout uses a different ref. So the version-tag generation +# wouldn't fire correctly from a manual ref input. on: release: types: [published] workflow_dispatch: - inputs: - ref: - description: "Git ref (tag/branch/SHA) to build from" - required: true - default: "master" jobs: publish: @@ -25,12 +26,8 @@ jobs: steps: - name: Check out source uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - # On release events the default checkout is the tag. - # On workflow_dispatch we honor the user-supplied ref. - # All branches/tags/SHAs are repo-controlled, not arbitrary - # untrusted text - safe to pass to the checkout action. - ref: ${{ github.event.inputs.ref || github.ref }} + # ref omitted: uses GITHUB_REF, which is the released tag for + # release events and master for workflow_dispatch. # Multi-arch builds need QEMU for cross-compilation under buildx. - name: Set up QEMU @@ -57,23 +54,37 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - # docker/metadata-action computes the tag list from the git ref using - # semver rules. Strategy: - # - `:VERSION` (e.g. `:2.0.0b2`) always pushed - exact version pin. - # - `:major`, `:major.minor`, `:latest` ALSO pushed on every release, - # INCLUDING prereleases. Rationale: this project has been in beta - # for over a year. If `:latest` only moved on stable releases, - # users pulling `:latest` would be stuck on a year-old beta until - # 2.0.0 ships. A user who pulls `:2` wants the most recent 2.x.y, - # not the most recent stable 2.x.y - same for `:latest`. - # - `:latest` is gated on release events only (not workflow_dispatch) - # so a manual rerun from older code never accidentally advances - # the floating tag. - # - Side-effect: if a future patch is released for an OLDER version - # line (e.g. 1.5.1 after 2.0.0), it would advance `:latest` - # backward. Acceptable for now - this project doesn't maintain - # old version lines. Switch to `flavor: latest=auto` if that - # ever changes. + # Tag strategy: + # + # `:VERSION` (e.g. `:2.0.0b2`) - exact version pin, always pushed. + # Uses type=pep440, NOT type=semver: this project versions in + # PEP 440 form (`2.0.0b2`), not SemVer (`2.0.0-beta.2`). + # `type=semver,pattern={{version}}` would silently fail to match + # PEP 440 prerelease tags and skip the build. + # + # `:major.minor` (e.g. `:2.0`) and `:major` (e.g. `:2`) - floating + # aliases, advanced on EVERY release including prereleases. + # We use `type=match` with a regex against the git tag name, + # NOT `type=pep440,pattern={{major}}.{{minor}}`, because + # metadata-action's PEP 440/SemVer parsers deliberately hold + # these floating aliases back for prereleases. We want them to + # advance for prereleases too: this project has been in beta + # for over a year, and locking floating tags to stable releases + # would leave them frozen on `v2.0.0b1` (May 2025) until 2.0.0 + # ships. A user pulling `:2` wants the latest 2.x.y, not the + # latest stable 2.x.y. + # + # `:latest` - same advancement policy as :major / :major.minor. + # Gated on `release` events only (not workflow_dispatch) so a + # manual master-HEAD build never accidentally moves :latest. + # + # `:manual-` - per-build tag for workflow_dispatch. + # Avoids collision with release-published tags. + # + # Acknowledged side-effect: if a future patch is released for an + # OLDER version line (e.g. 1.5.1 after 2.0.0), :latest / :major / + # :major.minor would advance backward. Not maintaining old version + # lines today; switch to `flavor: latest=auto` if that changes. - name: Extract image metadata (tags, labels) id: meta uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 @@ -82,9 +93,9 @@ jobs: bluet/proxybroker2 ghcr.io/bluet/proxybroker2 tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} + type=pep440,pattern={{version}} + type=match,pattern=v(\d+\.\d+),group=1,enable=${{ github.event_name == 'release' }} + type=match,pattern=v(\d+),group=1,enable=${{ github.event_name == 'release' }} type=raw,value=latest,enable=${{ github.event_name == 'release' }} type=raw,value=manual-{{date 'YYYYMMDDHHmmss'}},enable=${{ github.event_name == 'workflow_dispatch' }} From eb84c4b1f83cfd89b84e837224a1e5f6f2cac901 Mon Sep 17 00:00:00 2001 From: "Matthew (BlueT) Lien" Date: Fri, 8 May 2026 09:26:56 +0800 Subject: [PATCH 5/5] ci(docker): correct workflow_dispatch behavior; add flavor latest=false Codex caught a real correctness bug in my previous comment block. I claimed `workflow_dispatch` cannot publish version tags - that was wrong. GitHub documents `GITHUB_REF` for `workflow_dispatch` as the ref the workflow was dispatched with, so: - workflow_dispatch with `--ref v2.0.0b2` sets GITHUB_REF to `refs/tags/v2.0.0b2`, and `type=pep440,pattern={{version}}` DOES match - the version tag would be published. - For STABLE pep440 tags, metadata-action's default `flavor: latest=auto` would also auto-add `:latest`, contradicting my "manual runs never move :latest" claim. Two fixes: (a) Add `flavor: latest=false` to disable the auto-`:latest` behavior. We control `:latest` explicitly via the type=raw rule with `enable=${{ event_name == 'release' }}`, so only release events ever advance it - not manual rebuilds. (b) Rewrite the trigger comment block to honestly describe what workflow_dispatch DOES. The capability is actually useful: a maintainer can do a one-shot retroactive rebuild of a tagged release via `gh workflow run ... --ref v...` and get JUST the version tag pushed (no floating-alias advancement). That's the safer path for retroactive publishes than re-firing the release event (which would advance every floating tag). Net effect on the v2.0.0b2 retroactive publish: - Path A: delete + recreate v2.0.0b2 release Publishes :2.0.0b2 + :2.0 + :2 + :latest (full set). - Path B: gh workflow run docker-publish.yml --ref v2.0.0b2 Publishes :2.0.0b2 only. No floating-tag movement. Lower blast radius if you only need the version pin. Both are valid; user picks based on intent. Refs Codex review on docker-publish.yml:96. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/docker-publish.yml | 86 ++++++++++++++++------------ 1 file changed, 50 insertions(+), 36 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 8bbd9843..3cd964bd 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -1,17 +1,25 @@ name: Publish Docker Image # Triggers: -# - GitHub Release published: builds + pushes the image automatically. -# Same trigger as python-publish.yml, so one release ships both -# PyPI and Docker artifacts. -# - workflow_dispatch: lets a maintainer kick a build of master HEAD, -# useful for testing the workflow itself. Does NOT publish version -# tags - to (re)publish a specific tagged release, re-fire the -# release event by deleting + recreating the GitHub release. -# Reason: docker/metadata-action reads GITHUB_REF, which stays at -# the workflow's branch (master) for workflow_dispatch even when -# checkout uses a different ref. So the version-tag generation -# wouldn't fire correctly from a manual ref input. +# +# - release: published +# Full tag set (:VERSION, :major.minor, :major, :latest) - the +# canonical "ship a new version" path. Same trigger as +# python-publish.yml, so one release ships PyPI + Docker together. +# +# - workflow_dispatch +# Manual trigger. Behavior depends on what ref it's invoked with +# (`gh workflow run ... --ref `): +# +# - With `--ref ` (e.g. `--ref v2.0.0b2`): +# Publishes :VERSION only (no floating aliases). Useful for +# retroactive single-version rebuilds that must NOT move +# :latest / :major / :major.minor. The version tag fires +# because metadata-action's pep440 type matches the tag ref. +# +# - With `--ref master` (default): +# No tag ref to match - publishes :manual- only. +# Useful for smoke-testing the workflow itself. on: release: types: [published] @@ -26,8 +34,8 @@ jobs: steps: - name: Check out source uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - # ref omitted: uses GITHUB_REF, which is the released tag for - # release events and master for workflow_dispatch. + # ref omitted: uses GITHUB_REF, which for both `release` events + # and `workflow_dispatch` is the ref that triggered the workflow. # Multi-arch builds need QEMU for cross-compilation under buildx. - name: Set up QEMU @@ -56,35 +64,39 @@ jobs: # Tag strategy: # - # `:VERSION` (e.g. `:2.0.0b2`) - exact version pin, always pushed. - # Uses type=pep440, NOT type=semver: this project versions in - # PEP 440 form (`2.0.0b2`), not SemVer (`2.0.0-beta.2`). - # `type=semver,pattern={{version}}` would silently fail to match - # PEP 440 prerelease tags and skip the build. + # `:VERSION` (e.g. `:2.0.0b2`) - exact version pin. + # Uses type=pep440 (NOT type=semver) because this project + # versions in PEP 440 form (`2.0.0b2`), not SemVer + # (`2.0.0-beta.2`). PyPI mandates PEP 440 - we follow. + # Fires on any tag-shaped GITHUB_REF: release events AND + # workflow_dispatch invoked with `--ref v...`. # - # `:major.minor` (e.g. `:2.0`) and `:major` (e.g. `:2`) - floating - # aliases, advanced on EVERY release including prereleases. - # We use `type=match` with a regex against the git tag name, - # NOT `type=pep440,pattern={{major}}.{{minor}}`, because - # metadata-action's PEP 440/SemVer parsers deliberately hold - # these floating aliases back for prereleases. We want them to - # advance for prereleases too: this project has been in beta - # for over a year, and locking floating tags to stable releases - # would leave them frozen on `v2.0.0b1` (May 2025) until 2.0.0 - # ships. A user pulling `:2` wants the latest 2.x.y, not the - # latest stable 2.x.y. - # - # `:latest` - same advancement policy as :major / :major.minor. - # Gated on `release` events only (not workflow_dispatch) so a - # manual master-HEAD build never accidentally moves :latest. + # `:major.minor` / `:major` / `:latest` - floating aliases. + # Gated on `release` events only via explicit enable=. + # Reasons: + # - Manual workflow_dispatch rebuilds shouldn't accidentally + # move floating tags. + # - Type=match is used instead of {{major}} / {{major.minor}} + # templates because metadata-action's pep440/semver parsers + # deliberately hold floating aliases back for prereleases. + # We want them to advance for prereleases too: this project + # has been in beta for a year, and locking floating tags to + # stable releases would freeze them on `v2.0.0b1` (May 2025) + # until 2.0.0 ships. # # `:manual-` - per-build tag for workflow_dispatch. # Avoids collision with release-published tags. # + # `flavor: latest=false` disables metadata-action's automatic + # `:latest` setting for the highest stable version. We control + # `:latest` explicitly via the type=raw rule above so it ONLY + # fires on release events, not on manual workflow_dispatch runs + # of stable tags. + # # Acknowledged side-effect: if a future patch is released for an - # OLDER version line (e.g. 1.5.1 after 2.0.0), :latest / :major / - # :major.minor would advance backward. Not maintaining old version - # lines today; switch to `flavor: latest=auto` if that changes. + # OLDER version line (e.g. 1.5.1 after 2.0.0), the floating tags + # would advance backward. Not maintaining old version lines today; + # restore `flavor: latest=auto` if that ever changes. - name: Extract image metadata (tags, labels) id: meta uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 @@ -92,6 +104,8 @@ jobs: images: | bluet/proxybroker2 ghcr.io/bluet/proxybroker2 + flavor: | + latest=false tags: | type=pep440,pattern={{version}} type=match,pattern=v(\d+\.\d+),group=1,enable=${{ github.event_name == 'release' }}