Skip to content

ci: guard against version drift between main and the published skill - #19

Merged
BenKalsky merged 27 commits into
mainfrom
ci/release-drift-guard
Jul 22, 2026
Merged

ci: guard against version drift between main and the published skill#19
BenKalsky merged 27 commits into
mainfrom
ci/release-drift-guard

Conversation

@BenKalsky

@BenKalsky BenKalsky commented Jul 21, 2026

Copy link
Copy Markdown
Member

Why

publish-clawhub.yml only fires on release: published. A version bump that merges to main without a release leaves ClawHub silently serving the old version — no warning, no red check.

Not hypothetical — the state when this PR opened: main at 1.4.0 (since #18), newest release v1.3.1, ClawHub serving 1.3.1.

Design (as converged through 8 Codex rounds)

The review loop killed the naive design. Its first version proved "ClawHub is in sync" from release existence, and seven rounds of findings showed that proof leaks everywhere: the publish event is suppressed for GITHUB_TOKEN releases, a failed publish leaves the release standing, a UI release goes green seconds before its publish run even starts, drafts satisfy lookups, bare tags sneak through the dispatch gate, cancellations skip failure() rollbacks.

The proof is now a marker asset. publish-clawhub.yml uploads clawhub-published.txt onto the vX.Y.Z release — only after its publish command succeeded, and nothing else ever writes it.

version-sync job (ci.yml)

  1. SKILL.md / package.json / README badge must agree (the drift Codex caught by hand in docs: reconcile catalog against the live MCP — no aliases, 64 phantom tools removed #15).
  2. On main only (PRs would deadlock — the tag must point at the merged commit): a published, non-draft vX.Y.Z release carrying the marker must exist, and the skill payload at HEAD must match the tag's tree — same version ⇒ same payload.

release.yml (dispatch-only)

Tag pinned to the inspected commit → draft release with the CHANGELOG section as notes → dispatches the publish on the tag (not main — main can advance) and watches it → flips the draft to published only on success. Failure rolls back the draft + tag; the flip step never rolls back (registry ahead of repo would wedge re-runs). Pre-flight sweeps leftovers, refuses re-releases, and auto-resumes a draft that already carries the marker (publish finished, flip lost).

publish-clawhub.yml

Real publishes only from: a UI release, or a release.yml dispatch on a version tag with its draft present. Freestanding manual real-publishes are blocked (they put a version on ClawHub with no release, wedging the flow on the registry's duplicate refusal). UI-path rollback covers failure() || cancelled().

Review loop

8 rounds, 12 findings (3 P1), all real — every one confirmed against the code and fixed; several were bugs in earlier rounds' fixes, which is the loop working. Key catches: the GITHUB_TOKEN event suppression (round 1), release-left-standing-after-failed-publish (2), main-advances races (3), payload drift / draft releases / cancellation windows (4–5), the third entry point (6), and the structural UI race that forced the marker redesign (7).

Expected behavior right after merge

Main goes red immediatelySKILL.md is 1.4.0 with no v1.4.0 release. That is the guard working. Clear it with:

gh workflow run release.yml --ref main

🤖 Generated with Claude Code

The publish workflow only fires on 'release: published', so a version bump
that merges to main without a release leaves ClawHub silently serving the
previous version. That already happened: main has been at 1.4.0 since #18
while the newest release — and therefore ClawHub — is v1.3.1.

Adds a version-sync job with two checks:

- All three version declarations (SKILL.md frontmatter, package.json,
  README badge) must agree. This is the drift Codex caught by hand during
  #15, where the badge was left at 1.3.0 after the other two were bumped.
- On main only, a matching vX.Y.Z release must exist. It is skipped on
  pull_request because a PR that bumps the version cannot have a release
  yet — the tag has to point at the merged commit — so gating PRs would
  deadlock.

The release check captures stderr rather than discarding it, and reports a
query failure differently from a genuine 'no such release', so an auth or
network error can't masquerade as a missing release.

Also adds release.yml: a dispatch-only workflow that reads the version from
SKILL.md, pulls the matching CHANGELOG section for the notes, and creates
the release — which in turn triggers the existing ClawHub publish. It
refuses to run off a non-default branch and refuses to re-release an
existing version. This is the one command the new guard tells you to run.

Verified locally: version parse agrees across all three files, the 1.4.0
CHANGELOG section extracts cleanly, and all three workflow files parse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 21, 2026 21:27
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds CI + a dispatch-only release workflow to prevent “version drift” where main bumps the skill version but no matching GitHub Release exists (so ClawHub keeps serving the old version).

Changes:

  • Add a version-sync CI job that enforces SKILL/package/README version agreement, and (on push to main) requires a matching vX.Y.Z release to exist.
  • Add a new release.yml workflow to cut a GitHub Release from SKILL.md’s version and the corresponding CHANGELOG.md section.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
.github/workflows/ci.yml Adds version-sync job to detect version declaration drift and missing releases on main.
.github/workflows/release.yml Adds a manual workflow to create a release from SKILL.md + CHANGELOG.md notes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +22 to +24
skill_v=$(grep -m1 '^version:' "$SKILL" | sed 's/^version:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
pkg_v=$(grep -m1 '"version"' package.json | sed 's/.*"version"[[:space:]]*:[[:space:]]*"//; s/".*//')
readme_v=$(grep -o 'badge/version-[0-9][^-]*-blue' README.md | sed 's|badge/version-||; s|-blue||')
Comment on lines +31 to +37
- name: Refuse to release off a non-default branch
run: |
# A release tag must point at a commit on main; tagging a feature branch
# would publish code that never landed.
[ "$GITHUB_REF_NAME" = "main" ] || {
echo "::error::dispatched against '$GITHUB_REF_NAME' — re-run with --ref main"; exit 1; }

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d0ffb1fc7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/release.yml Outdated
Comment on lines +40 to +41
env:
GH_TOKEN: ${{ github.token }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Trigger publishing without the repository GITHUB_TOKEN

When this workflow creates a release, gh authenticates with the repository's GITHUB_TOKEN; GitHub states that events triggered by this token “will not create a new workflow run” except for dispatch events (Automatic token authentication). Consequently, the resulting release: published event does not start .github/workflows/publish-clawhub.yml, while subsequent version-sync runs accept the release as evidence that publishing occurred, leaving ClawHub on the previous version. Use a PAT/GitHub App token for release creation or explicitly dispatch the publishing workflow with suitable permissions.

Useful? React with 👍 / 👎.

A release created by release.yml runs under GITHUB_TOKEN, and GitHub
suppresses the resulting 'release: published' event to prevent recursive
workflow runs. So the release would have been created and the publish
would never have fired — while the new version-sync guard saw a release
and went green. That is a false green, and worse than no guard: it is the
exact drift this PR exists to catch, now wearing a passing check.

workflow_dispatch is the documented exception to the event suppression,
so release.yml now triggers publish-clawhub.yml explicitly instead of
relying on the release event, and waits for that run with 'gh run watch
--exit-status'. If the publish fails, the release job fails with it, so
'a release exists' can never stand in for a publish that didn't happen.

The dispatched run is identified by requiring createdAt >= a timestamp
taken before the dispatch, so a stale earlier dispatch can't be mistaken
for this one. jq gets the --arg (gh's built-in --jq does not accept it).

Kept the 'release: published' trigger: it still fires for releases a human
creates in the UI, which use a real user token. Documented in
publish-clawhub.yml why both paths exist, so the explicit dispatch doesn't
later get 'simplified' away as redundant.

Needed permissions: actions: write on the release job.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 21, 2026 21:35
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7c8358e58c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +43 to +45
if out=$(gh release view "v$VERSION" --repo "$GITHUB_REPOSITORY" --json tagName 2>&1); then
echo "release v$VERSION exists — ClawHub publish fired for it"
exit 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the guard red when the publish fails

When the dispatched ClawHub job fails—for example because CLAWHUB_TOKEN is missing/revoked or the CLI upload errors—the release has already been created by release.yml before that job is started. This check then treats the release itself as proof that publishing succeeded, so subsequent pushes pass even though ClawHub still serves the previous version; rerunning release.yml also refuses the existing release. Verify a successful publish outcome rather than only release existence, or remove/roll back the release when publishing fails.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread .github/workflows/release.yml Outdated
Comment on lines +6 to +7
# This workflow does NOT publish to ClawHub — publish-clawhub.yml does, and it
# fires on `release: published`, so creating the release here triggers it.
Comment thread .github/workflows/ci.yml
Comment on lines +9 to +11
version-sync:
name: Version declarations agree, and a release exists
runs-on: ubuntu-latest
…-2 P2)

Round 1 made the release job fail when the publish failed, but the release
object had already been created by then. That left the worst state of all:

  - v1.4.0 release exists, ClawHub still on 1.3.1
  - version-sync goes GREEN on the next push, because a release exists
  - release.yml refuses to re-run, because the version is already released

So a failed publish produced a passing guard and a blocked recovery path.

release.yml now deletes the release and its tag if the publish does not
succeed — on a failed dispatch, on no run appearing, and on the run itself
failing. The invariant that the guard depends on is restored: a vX.Y.Z
release exists only if X.Y.Z was actually published. The guard stays red,
and re-running the workflow after fixing the cause just works.

Deviation from the suggested remedy: Codex offered 'verify a successful
publish outcome' as the alternative, but a dry-run of publish-clawhub.yml
also concludes 'success', and a workflow run does not expose the inputs it
was dispatched with — so scanning run history would count a dry run as
proof of a real publish. Rollback is deterministic and needs no such
inference.

Verified the control flow under 'bash -e' (the shell Actions uses): both
failure paths roll back and exit 1, the success path exits 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 21, 2026 21:40
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04595bd410

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +43 to +45
if out=$(gh release view "v$VERSION" --repo "$GITHUB_REPOSITORY" --json tagName 2>&1); then
echo "release v$VERSION exists — ClawHub publish fired for it"
exit 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify that the ClawHub publish actually succeeded

When a release is published through the supported UI/PAT path and publish-clawhub.yml fails—for example because CLAWHUB_TOKEN is missing or the upload errors—the GitHub release remains, so every subsequent main push passes this check even though ClawHub still serves the previous version. The rollback in release.yml protects only releases created by that workflow; this check requests only tagName, which, per gh release view, provides release metadata rather than the status of another workflow or registry. Verify a successful matching publish, or make the release workflow the sole supported release path, before treating release existence as proof of synchronization.

Useful? React with 👍 / 👎.

Comment thread .github/workflows/release.yml Outdated
Comment on lines +102 to +104
gh workflow run publish-clawhub.yml \
--repo "$GITHUB_REPOSITORY" --ref main -f dry_run=false \
|| rollback "could not dispatch the ClawHub publish"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pin the ClawHub dispatch to the released revision

If main advances after this job reads VERSION or creates the release but before this dispatch, --ref main runs publish-clawhub.yml from the newer main tip; that workflow then reads and publishes the newer tree's version while this job watches it successfully and reports that $VERSION was published. The gh workflow run documentation defines --ref as the branch or tag containing the workflow version, so dispatch the just-created v$VERSION tag rather than the moving main branch.

Useful? React with 👍 / 👎.

Comment on lines +73 to +76
gh release create "v$VERSION" \
--repo "$GITHUB_REPOSITORY" \
--title "v$VERSION" \
--notes-file release-notes.md

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Target the checked-out commit when creating the release

If another commit reaches main between checkout and this command, an absent tag is created at the then-current default-branch tip rather than necessarily at the commit whose SKILL.md and changelog were inspected. The gh release create documentation states that --target defaults to the repository's default branch; pass the checked-out $GITHUB_SHA so the release cannot silently include later code or a later version.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

.github/workflows/release.yml:10

  • Header comment says creating the release here triggers publish-clawhub.yml via release: published, but later in this same workflow you correctly note that releases created with GITHUB_TOKEN do not trigger other workflows and you dispatch publish-clawhub.yml explicitly. The top comment should be updated to match the actual behavior to avoid future “simplifications” that break publishing.
# This workflow does NOT publish to ClawHub — publish-clawhub.yml does, and it
# fires on `release: published`, so creating the release here triggers it.
#
# Run it after merging a version bump, which is exactly what CI's version-sync
# job tells you to do when main is ahead of the last release.

Comment thread .github/workflows/publish-clawhub.yml Outdated
Comment on lines +10 to +14
# Note on the release trigger: it only fires for releases created by a human
# (UI / a personal token). A release created by release.yml runs under
# GITHUB_TOKEN, and GitHub suppresses the resulting `release: published` event
# to prevent recursive runs — so release.yml dispatches this workflow explicitly
# rather than depending on the event. Do not "simplify" that away.
Comment thread .github/workflows/release.yml Outdated
Comment on lines +109 to +113
for _ in $(seq 1 12); do
sleep 5
run_id=$(gh run list --workflow=publish-clawhub.yml --repo "$GITHUB_REPOSITORY" \
-L 5 --json databaseId,createdAt,event \
| jq -r --arg s "$started" \
… the UI path too (Codex round-3 P1 x2 + P2)

Three findings, one theme: the release flow assumed main stands still and
assumed release.yml is the only door.

- gh release create now passes --target $GITHUB_SHA, so the tag points at
  the commit whose SKILL.md and CHANGELOG this job actually inspected —
  not at whatever main's tip happens to be by then.
- The publish dispatch now targets --ref v$VERSION (the tag it just
  created) instead of --ref main. publish-clawhub.yml reads the version
  from the tree it runs on, so a main dispatch could publish a NEWER
  version while this job watches it succeed and reports $VERSION. The
  run-identification filter also requires headBranch == the tag, so no
  other dispatch can be watched in its place.
- publish-clawhub.yml gains a rollback step for the UI entry point: on a
  release-triggered run whose publish fails, it deletes that release and
  its tag. release.yml's rollback only covered releases it created; a
  hand-made UI release with a failed publish would have left the guard
  green over a stale ClawHub. Requires contents: write (was read).

Also documents the invariant in ci.yml where the guard consumes it: release
existence proves ClawHub sync ONLY while both rollback paths exist — remove
either and the check must become a real publish-outcome check. And fixes
release.yml's header, which still described the suppressed-event design
from before round 1.

Verified: all three workflows parse; the tag-filtered run-selection jq
picks the fresh dispatch on the tag and skips both a main-ref run and a
stale tag run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 21, 2026 21:47
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0c749a7094

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +50 to +52
if out=$(gh release view "v$VERSION" --repo "$GITHUB_REPOSITORY" --json tagName 2>&1); then
echo "release v$VERSION exists — ClawHub publish fired for it"
exit 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Compare the released payload before passing version sync

After a version has been released, any subsequent push to main that changes .claude/skills/cloudways-mcp/ without bumping the three declarations still succeeds here because this checks only whether the old tag name exists. ClawHub then continues serving the payload from the tagged commit while CI reports that it is synchronized; fetch the tag and compare the skill payload, or enforce a version bump whenever that directory changes, before exiting successfully.

Useful? React with 👍 / 👎.

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +50 to +52
if out=$(gh release view "v$VERSION" --repo "$GITHUB_REPOSITORY" --json tagName 2>&1); then
echo "release v$VERSION exists — ClawHub publish fired for it"
exit 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject draft releases in the sync check

If a maintainer creates a draft v$VERSION release in the UI, the authenticated release lookup can find that draft, so this step passes even though no release: published event has fired and nothing has reached ClawHub. GitHub's release API exposes draft state, and gh release view --json supports isDraft; require the result to be non-draft before treating the release as proof of publication (GitHub release API).

Useful? React with 👍 / 👎.

Comment on lines +79 to +83
gh release create "v$VERSION" \
--repo "$GITHUB_REPOSITORY" \
--target "$GITHUB_SHA" \
--title "v$VERSION" \
--notes-file release-notes.md

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the release unpublished until ClawHub succeeds

If this workflow is canceled after gh release create but before or during the dispatch/watch step, the shell is terminated without invoking rollback, leaving a published release and tag even though the ClawHub run may never have succeeded; CI then passes and rerunning this workflow is blocked as already released. The gh release create manual documents --draft as saving the release as a draft instead of publishing it, so create a draft and publish it only after the nested run succeeds, or add cancellation-aware cleanup.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

.github/workflows/release.yml:42

  • This step says it refuses to run off a non-default branch, but it hard-codes main. If the repository default branch ever changes, this will start rejecting valid releases (and the error message will still instruct --ref main). Consider deriving the default branch from the GitHub context so the behavior matches the step name/comments.
          # A release tag must point at a commit on main; tagging a feature branch
          # would publish code that never landed.
          [ "$GITHUB_REF_NAME" = "main" ] || {
            echo "::error::dispatched against '$GITHUB_REF_NAME' — re-run with --ref main"; exit 1; }

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +48 to +63
# Capture stderr instead of discarding it: a network/auth failure must not
# be misread as "no release" (and vice versa).
if out=$(gh release view "v$VERSION" --repo "$GITHUB_REPOSITORY" --json tagName 2>&1); then
echo "release v$VERSION exists — ClawHub publish fired for it"
exit 0
fi
echo "$out"
case "$out" in
*"release not found"*|*"Not Found"*|*"HTTP 404"*)
echo "::error::SKILL.md is at $VERSION but there is no v$VERSION release, so ClawHub is still serving the previous version. Cut one with: gh workflow run release.yml"
;;
*)
echo "::error::could not query releases (this is NOT a verdict on whether v$VERSION exists): $out"
;;
esac
exit 1
Comment on lines 28 to +31
permissions:
contents: read
# write is needed only by the rollback step below, which deletes a release whose
# publish failed.
contents: write
… + P2 x2)

Three findings, same theme as round 3: 'a release exists' still leaked as
proof through three gaps.

- Payload drift (P1): a push that edits the skill directory without
  bumping the version passed the guard while ClawHub kept serving the
  payload frozen at the tag. The guard now fetches the vVERSION tag and
  diffs the payload directory against HEAD — same version must mean same
  payload, or it fails with the diffstat and tells you to bump.

- Draft releases (P2): gh release view finds drafts when authenticated,
  so a hand-made draft satisfied the old existence check even though no
  publish ever ran. The guard now requires isDraft == false.

- Cancellation window (P2): canceling release.yml between creating the
  release and finishing the publish left a published release with no
  ClawHub behind it — and blocked re-runs as 'already released'. The flow
  is now draft-first: create the tag explicitly at GITHUB_SHA, create the
  release as a DRAFT, dispatch and watch the publish, and only then flip
  the draft to published. A cancellation at any point leaves at most a
  tag + draft, which the guard already treats as unpublished, and which
  the new pre-flight step sweeps on the next run.

Consequences folded in:
- The tag must now be created explicitly (a draft release creates no tag,
  and the publish dispatch targets the tag ref); --target on release
  create is gone since the tag itself is pinned to GITHUB_SHA.
- rollback() deletes the draft and tag separately (--cleanup-tag does not
  apply to drafts).
- The flip step never rolls back: once ClawHub serves the version, the
  only correct terminal state is a published release — deleting it would
  wedge re-runs on a duplicate-version publish. It retries once, then
  hands off to a human with the exact command.
- One residual risk is documented in-line: canceling mid-upload can leave
  ClawHub ahead with the release still a draft; the comment says how to
  verify and finish by hand.

Verified: all three workflows parse; draft-detection jq; the tag-fetch +
payload-diff command against the real v1.3.1 tag (correctly reports the
1.4.0 tree differs); pre-flight sim covers sweep-draft / no-release /
refuse-published.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 21, 2026 21:56
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a53fc048c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/release.yml Outdated
# A DRAFT release or a bare tag is a leftover from a canceled/failed run
# (this workflow publishes the release only after ClawHub succeeds), so
# sweep it and continue.
if meta=$(gh release view "v$VERSION" --repo "$GITHUB_REPOSITORY" --json isDraft 2>/dev/null); then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Abort when the release lookup fails

If gh release view fails for a transient API or network error, this condition treats that exactly like a missing release and continues. When v$VERSION is already published, the following git ls-remote can still find its tag, delete it, and the next step recreates it at $GITHUB_SHA before gh release create fails because the release already exists; this can silently retarget an existing published release. Capture the error and continue only when the API definitively reports that no release exists.

Useful? React with 👍 / 👎.

Comment thread .github/workflows/publish-clawhub.yml Outdated
# this, a failed publish on the UI path leaves the release standing and CI's
# version-sync guard goes green over a ClawHub that is still a version behind.
- name: Roll back the release if a release-triggered publish failed
if: failure() && github.event_name == 'release'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Roll back canceled release-triggered publishes

When a UI-created release triggers this workflow and the run is canceled during installation, authentication, or publishing, the job status is canceled rather than failed, so failure() is false and this rollback never runs. The published release and tag then remain even though ClawHub may still serve the previous version, causing the new version-sync check to accept a false successful-publication signal. The cleanup condition needs to cover canceled release-triggered runs as well.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

.github/workflows/publish-clawhub.yml:31

  • This workflow now runs with contents: write on all triggers (including workflow_dispatch dry-runs) even though write access is only required for the rollback path. To follow least-privilege, consider splitting rollback into a separate job with elevated permissions, and keep the main publish job at contents: read.
permissions:
  # write is needed only by the rollback step below, which deletes a release whose
  # publish failed.
  contents: write

Comment thread .github/workflows/release.yml Outdated
Comment on lines +150 to +156
run_id=$(gh run list --workflow=publish-clawhub.yml --repo "$GITHUB_REPOSITORY" \
-L 10 --json databaseId,createdAt,event,headBranch \
| jq -r --arg s "$started" --arg tag "v$VERSION" \
'[.[] | select(.event == "workflow_dispatch")
| select(.headBranch == $tag)
| select(.createdAt >= $s)]
| (first.databaseId // empty)')
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4f959c93f6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +203 to +204
echo "no real publish run ever targeted v$VERSION — sweeping leftover marker-less draft"
gh release delete "v$VERSION" --repo "$GITHUB_REPOSITORY" --yes

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve drafts while an accepted dispatch may still be hidden

When the preceding release run reports a failed dispatch response or times out waiting for the child, the workflow explicitly preserves the draft because GitHub may have accepted a publish run that is not yet listable; however, an immediate retry reaches this branch when that child remains invisible and deletes the draft and tag. The delayed child can then run against the recreated draft and publish concurrently with the retry, causing a duplicate-version failure or leaving the registry version orphaned from its original release. Persist the outstanding-dispatch state or require a safe grace/reconciliation step before treating an empty run listing as proof that the draft can be swept.

Useful? React with 👍 / 👎.

…e (Codex P1)

A run that dispatches and dies in the not-yet-listable window (lost ack,
no-show timeout) preserves the draft — but a quick re-run's pre-flight
saw a clean marker-less draft, swept it, and could race the hidden child
when it surfaced against the recreated state.

The dispatching run now uploads a dispatch-pending.txt breadcrumb (epoch
+ correlation id) onto the draft BEFORE dispatching — durable exactly as
long as the draft it protects, deleted with it on any legitimate sweep or
rollback. Pre-flight's marker-less branch enforces a 10-minute grace on
it: within the window, run listings cannot prove the child's absence, so
nothing is swept; past it, any accepted child would long be listable and
the existing real-run aggregation decides as before. If the breadcrumb
cannot be written, the run refuses to dispatch at all rather than open
an invisible race window.

Verified: YAML parses; grace sim stops at 120s and proceeds at 900s.

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

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI review requested due to automatic review settings July 22, 2026 01:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

.github/workflows/publish-clawhub.yml:251

  • This if: compares the boolean input to a boolean literal (false), but workflow_dispatch inputs are strings. As written, a real dispatch from release.yml (dry_run=false) won’t satisfy this condition, so the marker upload will be skipped and CI’s version-sync guard will stay red.
      - name: Upload the publish-completion marker
        if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.dry_run == false)
        env:

Comment thread .github/workflows/publish-clawhub.yml Outdated
Comment on lines +49 to +52
# ' (dry-run)' is appended from the ACTUAL dry_run input, so a run's title is
# unforgeable evidence of its mode — recovery classifiers rely on it to reject
# correlated-but-dry dispatches.
run-name: "Publish to ClawHub${{ inputs.correlation && format(' [{0}]', inputs.correlation) || '' }}${{ inputs.dry_run && ' (dry-run)' || '' }}"

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f3006f3cf2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/release.yml Outdated
Comment on lines +136 to +140
if [ "$age" -lt 600 ]; then
echo "::error::a dispatch recorded ${age}s ago may still be surfacing (run listings cannot prove its absence yet) — not sweeping. Wait for the grace window (10 min) and re-run."
exit 1
fi
echo "dispatch-pending breadcrumb is ${age}s old — past the grace window; any accepted child would be listable by now"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not expire an unobservable dispatch after ten minutes

If GitHub accepts the dispatch but the run remains unlistable for more than ten minutes, this cutoff treats the breadcrumb as safe to ignore and allows the draft/tag to be deleted and recreated, despite the later dispatch code correctly noting that listability has no fixed guarantee. The delayed child can then start against the replacement draft; if main advanced in the meantime, it may publish its older payload and attach the marker to the newer release, causing the version-sync guard to report a false match. Preserve the draft until the correlated dispatch has a verifiable terminal outcome rather than inferring absence from elapsed time.

Useful? React with 👍 / 👎.

…ssions type-agnostic (Codex P2 + Copilot)

- Codex: the 10-minute grace inferred a hidden dispatch's absence from
  elapsed time, but listability has no bounded window — an accepted run
  surfacing later could publish an older payload against recreated state
  and attach its marker to the newer release. The breadcrumb already
  carries the dispatch's correlation id, so resolution is now fully
  deterministic: correlated run listed → the real-run aggregation
  classifies it; not listed → stop, and only a human may break the tie by
  deleting the breadcrumb asset after verifying no such run exists.
  Automation never deletes on inferred absence.

- Copilot (third repeat of the inputs-typing claim): rather than keep
  arguing typed-vs-string semantics, the run-name suffix and the marker
  step condition are now written type-agnostically (match boolean true OR
  string 'true'), correct under either runtime behavior. The stakes
  justify belt-and-braces: a mistyped suffix would flip the recovery
  classifiers' real/dry verdicts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 01:16
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread .github/workflows/release.yml Outdated
Comment on lines +133 to +156
if [ "$(jq -r '[.assets[].name] | index("dispatch-pending.txt") != null' <<<"$meta")" = "true" ]; then
gh release download "v$VERSION" --repo "$GITHUB_REPOSITORY" --pattern dispatch-pending.txt --output "$RUNNER_TEMP/pending.txt" || {
echo "::error::draft v$VERSION carries a dispatch-pending breadcrumb that could not be read — not sweeping. Retry when the API responds."
exit 1
}
pend_corr=$(cut -d' ' -f2 <"$RUNNER_TEMP/pending.txt")
if [ "$(jq -r --arg c "[$pend_corr]" '[.[] | select((.displayTitle // "") | contains($c))] | length' <<<"$runs2")" -eq 0 ]; then
echo "::error::draft v$VERSION records an outstanding dispatch ($pend_corr) whose run is not listed — it may still surface; elapsed time cannot prove otherwise. Not sweeping. Verify by hand (gh run list --workflow=publish-clawhub.yml, look for [$pend_corr]); if it truly never started, acknowledge by removing the breadcrumb (gh release delete-asset v$VERSION dispatch-pending.txt --yes) and re-run."
exit 1
fi
echo "the outstanding dispatch ($pend_corr) is listed — the aggregation below will classify it"
fi

# Interrogate the newest REAL child for this tag before deleting
# anything. Real = a release-event run, or any dispatch whose
# run-name lacks the ' (dry-run)' suffix — the suffix is computed
# from the actual input, so its absence proves a real publish,
# correlated or not (an uncorrelated manual real dispatch on the
# tag is allowed by the draft gate and must count here).
if ! runs2=$(gh run list --workflow=publish-clawhub.yml --repo "$GITHUB_REPOSITORY" \
-L 50 --json databaseId,headBranch,event,displayTitle); then
echo "::error::could not list publish runs — refusing to sweep the draft while the last publish outcome is unknown"
exit 1
fi

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 12488fda44

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

exit 1
}
pend_corr=$(cut -d' ' -f2 <"$RUNNER_TEMP/pending.txt")
if [ "$(jq -r --arg c "[$pend_corr]" '[.[] | select((.displayTitle // "") | contains($c))] | length' <<<"$runs2")" -eq 0 ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Query runs before testing the dispatch breadcrumb

When a previous release run leaves dispatch-pending.txt, runs2 has not been assigned yet—it is populated only below this block. On GitHub's Bash runner, jq therefore receives empty input and emits no value, making the numeric test error and evaluate false, so the workflow incorrectly treats even an unlisted correlated child as listed. If the dispatch was accepted but its child is still hidden, execution can then find no real runs and sweep the draft and tag while that child later publishes, orphaning the registry version and wedging retries.

Useful? React with 👍 / 👎.

… + Copilot)

The outstanding-dispatch breadcrumb check referenced $runs2 before the
fail-closed listing that assigns it — with set -e (and nounset-like
emptiness) the correlated-run lookup saw an empty list and the branch
misbehaved exactly where it must be most careful. Both reviewers caught
it. The listing now runs first; breadcrumb resolution and the real-run
aggregation consume the same snapshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 01:22
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread .github/workflows/ci.yml
Comment on lines +23 to +25
pkg_v=$(jq -r .version package.json)
readme_v=$(grep -o 'badge/version-[0-9][^-]*-blue' README.md | sed 's|badge/version-||; s|-blue||')
echo "SKILL.md=$skill_v package.json=$pkg_v README badge=$readme_v"

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c32329c7db

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +125 to +126
if ! runs2=$(gh run list --workflow=publish-clawhub.yml --repo "$GITHUB_REPOSITORY" \
-L 50 --json databaseId,headBranch,event,displayTitle); then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Query the complete publish history before recovery

When a marker-less draft is retried after more than 50 newer publish-clawhub.yml runs, this repository-wide -L 50 cap omits the correlated run recorded by dispatch-pending.txt. The workflow then treats the breadcrumb as an indefinitely unlisted dispatch and refuses every automated recovery attempt, even if that older run completed successfully and its Publish-step outcome could safely restore the marker. The gh run list manual defines --limit as the maximum number of runs fetched, so filter by the version tag/correlation through a paginated API query rather than truncating the history.

Useful? React with 👍 / 👎.

The README badge regex required the -blue suffix, so a cosmetic color
change would break the declaration-agreement check. Match the semver
itself instead; verified against the current README (1.4.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 01:28
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: 5f1756f46d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

.github/workflows/publish-clawhub.yml:172

  • This error message hard-codes "main" even though the workflow uses DEFAULT_BRANCH for the actual check. If the repo default branch is ever renamed, this message becomes misleading; use $DEFAULT_BRANCH in the text as well.
            if ! cmp_status=$(gh api "repos/$GITHUB_REPOSITORY/compare/$DEFAULT_BRANCH...$GITHUB_SHA" --jq .status 2>"$RUNNER_TEMP/gherr"); then
              cmp_status=$(cat "$RUNNER_TEMP/gherr")
              # Lookup failure is NOT a lineage verdict: the release may be
              # perfectly valid, so refuse to publish but also refuse to delete.
              echo "::error::could not verify that $RELEASE_TAG's commit is on main ($cmp_status) — refusing to publish blind; the release is preserved. Retry when the API responds."
              exit 1

.github/workflows/release.yml:53

  • This comment says the release tag must point at a commit on "main", but the workflow actually enforces the repository's default branch via github.event.repository.default_branch. Updating the comment avoids confusion if the default branch is not main.
          # A release tag must point at a commit on main; tagging a feature branch
          # would publish code that never landed.
          DEFAULT_BRANCH="${{ github.event.repository.default_branch }}"
          [ "$GITHUB_REF_NAME" = "$DEFAULT_BRANCH" ] || {
            echo "::error::dispatched against '$GITHUB_REF_NAME' — re-run with --ref $DEFAULT_BRANCH"; exit 1; }

@BenKalsky
BenKalsky merged commit 826f264 into main Jul 22, 2026
5 checks passed
@BenKalsky
BenKalsky deleted the ci/release-drift-guard branch July 22, 2026 09:41
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.

2 participants