ci: guard against version drift between main and the published skill - #19
Conversation
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>
|
@codex review |
There was a problem hiding this comment.
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-syncCI job that enforces SKILL/package/README version agreement, and (onpushtomain) requires a matchingvX.Y.Zrelease to exist. - Add a new
release.ymlworkflow to cut a GitHub Release fromSKILL.md’s version and the correspondingCHANGELOG.mdsection.
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.
| 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||') |
| - 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; } | ||
|
|
There was a problem hiding this comment.
💡 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".
| env: | ||
| GH_TOKEN: ${{ github.token }} |
There was a problem hiding this comment.
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>
|
@codex review |
There was a problem hiding this comment.
💡 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".
| 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 |
There was a problem hiding this comment.
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 👍 / 👎.
| # This workflow does NOT publish to ClawHub — publish-clawhub.yml does, and it | ||
| # fires on `release: published`, so creating the release here triggers it. |
| 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>
|
@codex review |
There was a problem hiding this comment.
💡 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".
| 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 |
There was a problem hiding this comment.
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 👍 / 👎.
| gh workflow run publish-clawhub.yml \ | ||
| --repo "$GITHUB_REPOSITORY" --ref main -f dry_run=false \ | ||
| || rollback "could not dispatch the ClawHub publish" |
There was a problem hiding this comment.
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 👍 / 👎.
| gh release create "v$VERSION" \ | ||
| --repo "$GITHUB_REPOSITORY" \ | ||
| --title "v$VERSION" \ | ||
| --notes-file release-notes.md |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
| # 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. |
| 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>
|
@codex review |
There was a problem hiding this comment.
💡 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".
| 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 |
There was a problem hiding this comment.
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 👍 / 👎.
| 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 |
There was a problem hiding this comment.
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 👍 / 👎.
| gh release create "v$VERSION" \ | ||
| --repo "$GITHUB_REPOSITORY" \ | ||
| --target "$GITHUB_SHA" \ | ||
| --title "v$VERSION" \ | ||
| --notes-file release-notes.md |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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; }
| # 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 |
| 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>
|
@codex review |
There was a problem hiding this comment.
💡 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".
| # 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 |
There was a problem hiding this comment.
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 👍 / 👎.
| # 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' |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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: writeon all triggers (includingworkflow_dispatchdry-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 atcontents: read.
permissions:
# write is needed only by the rollback step below, which deletes a release whose
# publish failed.
contents: write
| 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)') |
|
@codex review |
There was a problem hiding this comment.
💡 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".
| echo "no real publish run ever targeted v$VERSION — sweeping leftover marker-less draft" | ||
| gh release delete "v$VERSION" --repo "$GITHUB_REPOSITORY" --yes |
There was a problem hiding this comment.
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>
|
@codex review |
There was a problem hiding this comment.
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), butworkflow_dispatchinputs 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:
| # ' (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)' || '' }}" |
There was a problem hiding this comment.
💡 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".
| 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" |
There was a problem hiding this comment.
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>
|
@codex review |
| 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 |
There was a problem hiding this comment.
💡 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 |
There was a problem hiding this comment.
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>
|
@codex review |
| 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" |
There was a problem hiding this comment.
💡 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".
| if ! runs2=$(gh run list --workflow=publish-clawhub.yml --repo "$GITHUB_REPOSITORY" \ | ||
| -L 50 --json databaseId,headBranch,event,displayTitle); then |
There was a problem hiding this comment.
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>
|
@codex review |
|
Codex Review: Didn't find any major issues. Chef's kiss. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
There was a problem hiding this comment.
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_BRANCHfor the actual check. If the repo default branch is ever renamed, this message becomes misleading; use$DEFAULT_BRANCHin 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 notmain.
# 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; }
Why
publish-clawhub.ymlonly fires onrelease: 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_TOKENreleases, 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 skipfailure()rollbacks.The proof is now a marker asset.
publish-clawhub.ymluploadsclawhub-published.txtonto thevX.Y.Zrelease — only after its publish command succeeded, and nothing else ever writes it.version-syncjob (ci.yml)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).vX.Y.Zrelease 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.ymlReal 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_TOKENevent 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 immediately —
SKILL.mdis 1.4.0 with no v1.4.0 release. That is the guard working. Clear it with:🤖 Generated with Claude Code