Skip to content

ci: auto-tag per merge + republish stack only when compose changes#7

Merged
OriNachum merged 3 commits into
mainfrom
ci/auto-tag-compose-gated-stack
Jun 21, 2026
Merged

ci: auto-tag per merge + republish stack only when compose changes#7
OriNachum merged 3 commits into
mainfrom
ci/auto-tag-compose-gated-stack

Conversation

@OriNachum

Copy link
Copy Markdown
Contributor

Release model: auto-tag every merge, republish the stack only when it changes

Implements the chosen release model (auto-tag per merge; GHCR stack image only on a real substrate change). Reworks publish-stack.yml from "publish on a v* tag" into a two-job release workflow that runs on push to main (a merged PR):

  • tag job — derive the version from pyproject.toml, create + push the v<version> git tag (so tags track the PyPI release 1:1), then gate: publish=true only when docker-compose.yml differs from the previous tag (or there's no previous tag). Idempotent — skips if the tag already exists.
  • publish-stack job — runs only when publish=true; the publish steps are unchanged (validate compose → GHCR login → docker compose publish :version + :latest → attach docker-compose.yml to the Release).

Why

Today every merge bumps the version (→ a PyPI release via publish.yml), but the stack image only existed when someone hand-pushed a v* tag. This gives:

  • a git tag per release (parity with PyPI), and
  • a stack image republished only when the substrate actually changes — most releases bump the CLI, not docker-compose.yml, so no more byte-identical OCI churn.

workflow_dispatch with a tag input still force-(re)publishes a specific version's stack, regardless of the gate.

Notes

  • Merge order: this is 0.5.2; the open Sonar-hygiene PR chore: per-version Sonar wiring + clear 4 stack.py smells #6 (0.5.1) should merge first. If they land out of order, the later one needs a trivial re-bump. CHANGELOG may need a one-line reorder on rebase — I'll handle it.
  • One-time gap: v0.5.1 won't get a tag (it merges before this workflow exists). Auto-tagging begins at the first push to main after this lands (which will tag v0.5.2; compose unchanged → tag only, no stack republish — exactly the intended behavior).
  • No publish.yml change — PyPI stays continuous per merge.
  • Touches CLAUDE.md/docs/stack-image.md for the description only (no culture.yaml/skills change → no sibling propagation).

Gates

YAML valid; embedded tag-job bash passes bash -n; markdownlint clean; pytest 135 passed (no Python changed); teken rubric 26/26. Version 0.5.0 → 0.5.2.

  • data-refinery-cli (Claude)

Reworks publish-stack.yml from "publish on a v* tag" to a two-job release
workflow on push to main (a merged PR):

- `tag` job: derive the version from pyproject.toml, create+push the
  `v<version>` git tag (so tags track the PyPI release 1:1), then gate —
  set publish=true only when docker-compose.yml differs from the previous
  tag (or there is no previous tag). Idempotent: skips if the tag exists.
- `publish-stack` job: runs only when publish=true; unchanged publish
  steps (validate compose, GHCR login, `docker compose publish` :version
  + :latest, attach docker-compose.yml to the Release).

Net effect: every merge gets a version tag (parity with the PyPI release
from publish.yml), but the stack OCI image is republished only when the
substrate actually changed — no more byte-identical churn on CLI-only
releases. `workflow_dispatch` with a tag input force-(re)publishes a
specific version.

Docs: docs/stack-image.md + CLAUDE.md updated. Version 0.5.0 -> 0.5.2
(0.5.1 is held by the open Sonar-hygiene PR #6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@OriNachum

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

qodo-code-review Bot commented Jun 21, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 15 rules

Grey Divider


Action required

1. Reruns skip stack publish ✓ Resolved 🐞 Bug ☼ Reliability
Description
In publish-stack.yml, when the computed version tag already exists the tag job sets publish=false
and exits, so the publish-stack job will never run on a rerun even if the previous run failed after
creating the tag (leaving no OCI artifact and/or no release asset). This can strand a release in an
incomplete state unless someone manually intervenes with workflow_dispatch.
Code

.github/workflows/publish-stack.yml[R70-78]

+            if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
+              echo "Tag ${TAG} already exists — no new release."
+              {
+                echo "tag=${TAG}"
+                echo "version=${VERSION}"
+                echo "publish=false"
+              } >> "$GITHUB_OUTPUT"
+              exit 0
+            fi
Evidence
The tag job explicitly forces publish=false and exits when the version tag already exists, and the
publish-stack job is strictly gated on that output, so reruns after partial failures cannot publish.

.github/workflows/publish-stack.yml[70-78]
.github/workflows/publish-stack.yml[106-109]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
On `push` to `main`, the workflow exits early when `refs/tags/${TAG}` already exists and forces `publish=false`, which prevents `publish-stack` from running on workflow reruns. If the previous run created/pushed the tag but failed during publish/release creation, rerunning the workflow cannot repair the missing artifacts.

### Issue Context
`publish-stack` is gated solely by `needs.tag.outputs.publish == 'true'`. The `tag` job currently treats “tag exists” as “no release work needed,” but that’s not equivalent to “stack published successfully.”

### Fix Focus Areas
- .github/workflows/publish-stack.yml[63-88]
- .github/workflows/publish-stack.yml[106-109]

### Suggested approach
- Remove the early-exit behavior for the `push` path when the tag exists.
- If the tag exists, compute the compose-change gate anyway (e.g., pick the previous tag excluding the current TAG) and set `publish` accordingly, or add an explicit recovery path (e.g., set `publish=true` when the GitHub Release asset is missing).
- Ensure the job remains idempotent (skip *tag creation* when the tag exists, but don’t automatically skip *publishing*).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Dispatch may publish wrong version 🐞 Bug ≡ Correctness
Description
For workflow_dispatch, the workflow uses the user-provided inputs.tag as the git tag and derives
VERSION from it, then (if missing) creates that tag pointing at the current checkout without
verifying that the repo state matches that version. This can publish docker-compose.yml and create
a Release under a tag that does not correspond to the commit/version intended by the operator,
breaking the stated “tags track PyPI 1:1” invariant.
Code

.github/workflows/publish-stack.yml[R56-61]

+          if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
+            # Manual: (re)publish exactly the requested tag, always.
+            TAG="$INPUT_TAG"
+            VERSION="${TAG#v}"
+            PUBLISH="true"
+            echo "Manual dispatch: will publish stack for ${TAG}"
Evidence
The dispatch path assigns TAG directly from user input, derives VERSION from it, and can create/push
the tag at the current checkout with no check that it matches the repo’s version; this conflicts
with the workflow’s stated goal of tags tracking pyproject.toml/PyPI releases 1:1.

.github/workflows/publish-stack.yml[3-6]
.github/workflows/publish-stack.yml[56-61]
.github/workflows/publish-stack.yml[90-97]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`workflow_dispatch` accepts an arbitrary `tag` input, strips a leading `v` to form `VERSION`, and may create/push that git tag at the current checkout. There’s no validation that the requested tag matches the `pyproject.toml` version or that the checkout corresponds to the intended release commit, so the workflow can publish the stack under a mismatched version/tag.

### Issue Context
The workflow documentation/comment states tags are derived from `pyproject.toml` to track PyPI releases 1:1, but the dispatch path bypasses that relationship.

### Fix Focus Areas
- .github/workflows/publish-stack.yml[56-61]
- .github/workflows/publish-stack.yml[90-97]
- .github/workflows/publish-stack.yml[3-6]

### Suggested approach
- Validate `INPUT_TAG` format (e.g., `^v\d+\.\d+\.\d+$`) and fail fast on invalid values.
- Prefer requiring the tag to already exist for dispatch republish (and do not create new tags), OR add a separate explicit input (e.g., `target_sha`) when creating a new tag.
- If creating a tag from the current checkout is intended, verify `pyproject.toml`’s version equals `${TAG#v}` before tagging/publishing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

CI: auto-tag merges and publish stack only when docker-compose changes
✨ Enhancement ⚙️ Configuration changes 📝 Documentation 🕐 20-40 Minutes

Grey Divider

Description

• Auto-create/push a v git tag on every merge to main from pyproject.toml.
• Publish the GHCR stack artifact only when docker-compose.yml changed since last tag.
• Update docs/changelog to reflect the new release and stack-image cadence.
Diagram

graph TD
  A(["Trigger: push main / dispatch"]) --> B["Job: tag"] --> C{{"Compose changed?"}}
  B --> T[("Git tag vX.Y.Z")]
  C -->|"yes"| D["Job: publish-stack"] --> E[["GHCR: stack artifact"]] --> F[["GitHub Release: compose asset"]]
  C -->|"no"| G["Skip publish"]

  subgraph Legend
    direction LR
    _t(["Trigger"]) ~~~ _j["Job"] ~~~ _d{{"Decision"}} ~~~ _x[["External"]]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use paths filtering / paths-filter for publish gating
  • ➕ Moves compose-change detection to a purpose-built action (less bespoke bash).
  • ➕ Can make gating logic more explicit/maintainable (e.g., dorny/paths-filter).
  • ➖ Still needs custom logic to compare against the previous tag, not just the current commit’s file changes.
  • ➖ Adds an action dependency and another moving piece in release automation.
2. Create tags/releases via GitHub API (github-script) instead of git push
  • ➕ Avoids relying on git tag pushing semantics/credentials; uses GitHub’s API as source of truth.
  • ➕ Can atomically create a Release when tagging (if desired in the future).
  • ➖ More YAML/JS complexity; harder to debug than simple git commands.
  • ➖ Still needs careful handling for idempotency (tag already exists) and race conditions.
3. Read version via Python (tomllib) instead of sed
  • ➕ More robust parsing of pyproject.toml; avoids sed quoting/format assumptions.
  • ➕ Keeps logic consistent with existing Python tooling used elsewhere in CI.
  • ➖ Requires ensuring Python is available in the runner step (usually true, but adds implicit dependency).
  • ➖ Slightly more verbose for a simple extraction.

Recommendation: The PR’s approach is solid for the chosen release model: it guarantees a tag per merge (parity with PyPI) while avoiding unnecessary GHCR churn by gating on compose diffs against the previous version tag. If maintainability becomes a concern, the most valuable incremental improvement would be switching the pyproject version extraction to a tiny Python/tomllib snippet; otherwise, keeping the current git-based plan/gate is reasonable and transparent.

Files changed (5) +140 / -39

Documentation (3) +24 / -6
CHANGELOG.mdAdd 0.5.2 entry describing auto-tagging and conditional stack publishing +6/-0

Add 0.5.2 entry describing auto-tagging and conditional stack publishing

• Documents the new release workflow behavior and the stack-image publishing cadence change (publish only on compose changes).

CHANGELOG.md

CLAUDE.mdUpdate project overview to reflect new publish-stack release model +3/-1

Update project overview to reflect new publish-stack release model

• Refreshes the Wave 1 description to note that publish-stack now auto-tags from pyproject.toml and only republishes the stack image when docker-compose.yml changes.

CLAUDE.md

stack-image.mdClarify stack image immutability and publish cadence under new workflow +15/-5

Clarify stack image immutability and publish cadence under new workflow

• Updates the stack image documentation to explain that republishing happens only when docker-compose.yml changes between releases, and that the workflow now runs on push to main with a tag-derivation job and compose-change gate.

docs/stack-image.md

Other (2) +116 / -33
publish-stack.ymlConvert stack publisher into auto-tag + compose-gated release workflow +115/-32

Convert stack publisher into auto-tag + compose-gated release workflow

• Reworks the workflow to run on pushes to main (and manual dispatch), adding a dedicated 'tag' job that derives the version from pyproject.toml, creates/pushes the v<version> tag, and computes a publish gate by diffing docker-compose.yml against the previous tag. The 'publish-stack' job now runs only when gated true (or always for workflow_dispatch), and checks out the computed tag ref to publish the OCI artifact and attach docker-compose.yml to the GitHub Release.

.github/workflows/publish-stack.yml

pyproject.tomlBump project version to 0.5.2 +1/-1

Bump project version to 0.5.2

• Updates the package version, aligning the derived git tag and the PyPI release version with the new auto-tagging workflow.

pyproject.toml

Comment thread .github/workflows/publish-stack.yml Outdated
The tag job early-exited with publish=false whenever the version tag
already existed, so a re-run could never repair a release that failed
*after* the tag was pushed but before the OCI artifact / release asset
were created — the artifacts stayed stranded until a manual dispatch.

Drop the early-exit. Compute the previous tag as the highest v* EXCLUDING
the current TAG, then run the compose-change gate normally. Tag creation
stays idempotent (skipped when the tag exists); only *publishing* is
gated. A re-run now re-evaluates and can re-publish to recover.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ated-stack

# Conflicts:
#	CHANGELOG.md
#	pyproject.toml
@sonarqubecloud

Copy link
Copy Markdown

@OriNachum OriNachum merged commit 40b5786 into main Jun 21, 2026
8 checks passed
@OriNachum OriNachum deleted the ci/auto-tag-compose-gated-stack branch June 21, 2026 09:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant