feat(release): add controlled stable tag creation - #317
Conversation
| export const CONTROLLED_TAG_SCHEMA = "clawsec.controlled-tag/v1"; | ||
| export const CONTROLLED_RELEASE_WORKFLOW = "controlled-skill-release.yml"; | ||
| export const REQUIRED_TAG_RULESET_PATTERN = "refs/tags/*-v*"; |
There was a problem hiding this comment.
Stale enforcement workflow reference
The wiki automation module still points enforcement at .github/workflows/skill-release.yml, but controlled release now dispatches controlled-skill-release.yml, so operators will look at the wrong workflow — should we update the docs to reference controlled-skill-release.yml?
Want Baz to fix this for you? Activate Fixer You can also update your AI coding guidelines based on this comment by apply pr to [branch name]
Other fix methods
Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
scripts/ci/controlled_tag_creation.mjs around lines 15-17, the code defines
CONTROLLED_RELEASE_WORKFLOW as "controlled-skill-release.yml" and dispatches that
workflow, but the wiki automation/docs still reference
".github/workflows/skill-release.yml". Update the wiki automation module (or any
documentation/scripts that hardcode the old workflow filename) to use
"controlled-skill-release.yml" instead, ensuring the referenced path and workflow name
match the constant. After updating, verify there are no remaining references to
"skill-release.yml" for this controlled-tag dispatch flow.
There was a problem hiding this comment.
Commit 2b0bf7f addressed this comment by updating workflow-related documentation/tests to reference controlled-skill-release.yml instead of the stale skill-release.yml path.
| function parseSkillMarkdownVersion(markdown, source) { | ||
| const normalized = markdown.replaceAll("\r\n", "\n"); | ||
| if (!normalized.startsWith("---\n")) { | ||
| fail(`${source} must start with YAML frontmatter`); | ||
| } |
There was a problem hiding this comment.
SKILL.md parsing drifts across tools
The new controller reimplements the SKILL.md frontmatter check already in scripts/ci/simulate_skill_tag_release.mjs:143-159 and scripts/ci/generate_skill_release_trust_packet.mjs:60-77, so delimiter, CRLF, or version-rule changes can make the release simulator, trust-packet generator, and controller disagree on valid release markdown — should we centralize this in a small readSkillMarkdownVersion() helper?
Want Baz to fix this for you? Activate Fixer
| const existing = await readTagRecord(client, repository, plan.tag, { allowMissing: true }); | ||
| if (existing) { | ||
| verifyTagRecord(existing, plan); | ||
| return { ...plan, ...existing, ...rulesetProof, operation: "resumed" }; | ||
| } |
There was a problem hiding this comment.
Tag tuple checks duplicated three times
readTagRecord() plus verifyTagRecord() is duplicated in verifyControlledReleaseTuple() and preflightControlledTag(), so any change to the annotated-tag contract or resume/error handling has to be kept in three places and the create/resume/preflight paths can drift — should we wrap that readback + assertion in a small helper like loadVerifiedTagRecord() and keep branch-specific behavior at each call site?
Want Baz to fix this for you? Activate Fixer
There was a problem hiding this comment.
Commit 2b0bf7f addressed this comment by introducing loadVerifiedTagRecord() and using it across create/resume, verification, and preflight paths, eliminating the duplicated read-and-verify logic.
| if ! jq --arg version "$VERSION" '.version = $version' \ | ||
| "$SKILL_PATH/skill.json" > "$TEMP_DIR/skill.json"; then | ||
| echo "Error: failed to update $SKILL_PATH/skill.json." >&2 | ||
| exit 1 | ||
| fi | ||
| mv "$TEMP_DIR/skill.json" "$SKILL_PATH/skill.json" | ||
| FILES_TO_STAGE+=("$SKILL_PATH/skill.json") | ||
|
|
||
| # Update any hardcoded version URLs in skill.json (openclaw.feed_url pattern) | ||
| if jq -e '.openclaw.feed_url' "$SKILL_PATH/skill.json" >/dev/null 2>&1; then | ||
| echo "Updating openclaw.feed_url to use tag $TAG..." | ||
| if ! jq --arg tag "$TAG" '.openclaw.feed_url = (.openclaw.feed_url | gsub("/[^/]+-v[0-9.]+(-[a-zA-Z0-9]+)?/"; "/\($tag)/"))' "$SKILL_PATH/skill.json" > "$TEMP_DIR/skill.json"; then | ||
| echo "Error: Failed to update feed_url in skill.json" | ||
| if ! jq --arg tag "$TAG" \ | ||
| '.openclaw.feed_url = (.openclaw.feed_url | gsub("/[^/]+-v[0-9.]+(-[a-zA-Z0-9.]+)?/"; "/\($tag)/"))' \ | ||
| "$SKILL_PATH/skill.json" > "$TEMP_DIR/skill.json"; then |
There was a problem hiding this comment.
Interrupted runs leave dirty repo
This helper moves skill.json, SKILL.md, and other markdown files into the working tree before staging/commit, so a failure after the first mv leaves partial version bumps on disk and the next run trips git status --porcelain=v1 --untracked-files=all, which means release prep can't be safely retried after an interrupted run. Should we keep the rewritten files in the temp dir until everything succeeds, or add a rollback trap?
Want Baz to fix this for you? Activate Fixer
Other fix methods
Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
scripts/release-skill.sh around lines 77-133 (the block that updates skill.json and
SKILL.md, and the subsequent loop that rewrites other *.md files), the script currently
applies changes to the working tree via mv back into $SKILL_PATH before all edits and
the commit succeed. Refactor so that rewritten files stay in $TEMP_DIR until every
transformation and verification step has completed successfully, then do a single
“apply” phase that replaces the originals atomically (or, alternatively, create
backups of every file you overwrite and add an EXIT/ERR rollback trap that restores
originals on failure). Ensure the error path restores all modified files so the script
can be safely retried after an interrupted run, without tripping the global clean-tree
check at lines 66-69.
There was a problem hiding this comment.
Commit 2b0bf7f addressed this comment by keeping rewritten files in the temporary directory until preparation completes, then applying them in one phase. It also added backups and an EXIT trap to restore modified files if staging or committing fails.
User description
Summary
contents: writefor tag-object/ref creation, then useGITHUB_TOKENseparately to dispatch the release workflowscripts/release-skill.shinto review-branch preparation only; it no longer creates, pushes, force-moves, deletes, or publishes tags/releasesWhy
The previous pipeline could enter release behavior from public tag pushes and mixed preparation with publication mutations. The lifecycle design requires RC and beta work to stay private in labs and permits public release only for a reviewed final version through one auditable controller.
Renaming the old workflow path also closes historical
workflow_dispatchaccess to that file. The new release workflow independently verifies the exact annotated-tag tuple before signing or publication jobs can start.Security properties
push.tagsrelease triggerclawsec-stable-releaseenvironment gate and a second enable-switch check after approvalupdated_atare bound into provenancereturn_run_detailsis absent from the current2026-03-10dispatch body, with an exact-body regression assertionOperator prerequisites before enabling
This PR does not configure or enable production release settings. An operator must separately configure the protected environment, dedicated GitHub App, private key secret, ruleset ID and exact
updated_at, verify the App is the sole ruleset bypass actor, and only then setCLAWSEC_CONTROLLED_RELEASE_ENABLED=true.Remote validation
Validated on
davida@20.14.133.241in/tmp/clawsec-controlled-tag.H2JlJT/sourceafter SHA-256 parity checks against every local changed file.scripts/test-skill-*.mjstestadvisory-feed-testscommand surface, including NanoClaw v2 and PicoClaw consumers, Pages checksum, and traffic archive testsnpm ci(422packages,0vulnerabilities)npx tsc --noEmitnpm run buildscripts/release-skill.shThe remote image lacks native
zip; release simulations used the previously verified compatibility wrapper with SHA-256472456191504d886c5871da3e900797512ab9266b8426d736c6a0f48248046b0.Two independent remote reviews approved the exact tested candidate. No tag, GitHub Release, skill-store publication, merge, or repository-setting change was performed.
Stack
This draft PR targets
davida-ps/stable-tag-policy-v1and should be rebased or retargeted only after the prerequisite stable-tag-policy PR lands.Generated description
Below is a concise technical summary of the changes proposed in this PR:
Add a controlled stable-release workflow that verifies protected-main provenance, tag-ruleset state, and final SemVer before creating an annotated release tag and dispatching publication from
controlled-skill-release.ymlandcontrolled_tag_creation.mjs. Refactorrelease-skill.shinto review-branch preparation only and update the release, installability, and advisory tests to match the new flow.release-skill.shinto a review-branch prep helper that only updates and commits version metadata, then refresh its installability and workflow coverage tests.Modified files (3)
Latest Contributors(2)
controlled_tag_creation.mjsto validate release provenance, mint the tag with a least-privilege App token, and dispatch publication only after re-verifying the exact tuple.Modified files (7)
Latest Contributors(1)