Free SKILL.md scraped from GitHub. Clone the repo or copy the file directly into your Claude Code skills directory.
npx versuz@latest install guardiatechnology-ahrena-cursor-skills-kata-release-publishgit clone https://github.com/guardiatechnology/ahrena.gitcp ahrena/SKILL.MD ~/.claude/skills/guardiatechnology-ahrena-cursor-skills-kata-release-publish/SKILL.md---
name: kata-release-publish
description: "Publish Release (Annotated Tag + GitHub Release). Phase 2 of the release cycle orchestrated by warrior-janus — annotated/signed tag creation, push to the remote, wait/edit the release via GitHub Action, and post-condition verification"
---
# Kata: Publish Release (Annotated Tag + GitHub Release)
> **Prefix:** `kata-` | **Type:** Repeatable Skill | **Scope:** Phase 2 of the release cycle orchestrated by `warrior-janus` — annotated/signed tag creation, push to the remote, wait/edit the release via GitHub Action, and post-condition verification
## Preconditions (blocking)
- [ ] `kata-release-prepare` ran and the human responded **"yes"** explicitly
- [ ] The version complies with `lex-semantic-version`
- [ ] GPG configured (`lex-signed-commits` / `lex-annotated-tags`)
- [ ] `.github/workflows/validate-tag.yml` exists in the target repository
- [ ] Trunk branch has green CI on the target commit (revalidate — the window may have opened between prepare and publish)
If any precondition fails: **abort**, record the reason, return control to the human.
## Workflow
```
Progress:
- [ ] 1. Revalidate preconditions
- [ ] 2. Detect release workflow in the target repository
- [ ] 3. Create annotated and signed tag (via kata-tag)
- [ ] 4. Push the tag to the remote
- [ ] 5. Wait for validate-tag.yml to complete successfully
- [ ] 6. Handle the GitHub Release cycle (workflow or fallback)
- [ ] 7. Verify post-conditions and report
```
### Step 1: Revalidate Preconditions
Re-run the checks listed under "Preconditions" (do not trust state from minutes ago). If any item fails, abort and return control to the human.
### Step 2: Detect Release Workflow
This step is **critical** — its absence caused a bug in v0.11.0 (race condition between `gh release create` and an automatic workflow).
Detection has two criteria; **both** must match for a file to count as release-creating:
1. **Tag push trigger** — any valid YAML spelling: `tags: ['v*']`, `tags: 'v*'`, or the `tags:\n - "v*"` block (YAML list).
2. **A step that creates a GitHub Release** — `softprops/action-gh-release`, `actions/create-release`, or an explicit `gh release create` call / `POST /releases` API call.
Without (2), a CI-only workflow triggered by tags would be misclassified as release-creating; without (1), a `workflow_dispatch` workflow that creates releases manually would be wrongly awaited.
```bash
RELEASE_WORKFLOW=""
for wf in .github/workflows/*.yml .github/workflows/*.yaml; do
[ -f "$wf" ] || continue
# (1) tag push trigger — covers bracket, string, and YAML-list spellings
has_tag_trigger=0
if grep -qE '^\s*tags:\s*\[' "$wf" \
|| grep -qE "^\s*tags:\s*['\"]?v" "$wf" \
|| awk '/^\s*tags:\s*$/,/^\s*[^[:space:]-]/' "$wf" | grep -qE '^\s*-\s*["'\'']?v'; then
has_tag_trigger=1
fi
[ $has_tag_trigger -eq 1 ] || continue
# (2) a step that creates a GitHub Release
if grep -qE 'softprops/action-gh-release|actions/create-release|gh release create|POST /repos/.+/releases' "$wf"; then
RELEASE_WORKFLOW="$wf"
break
fi
done
```
Record in the log:
- `RELEASE_WORKFLOW="<path>"` → "workflow-driven" path
- `RELEASE_WORKFLOW=""` → "fallback" path (`gh release create`)
**Known heuristic limits:**
- Workflows that delegate Release creation to another workflow via `workflow_call` require transitive inspection — out of scope for this heuristic. Document this in Janus as a case to handle manually.
- A custom Action (not the three listed) that creates a Release goes unnoticed; in that case, the repo maintainer SHOULD include `release` in the workflow filename and add a `# creates-github-release: true` comment (suggested convention for a future heuristic iteration).
### Step 3: Create Annotated and Signed Tag
Invoke `kata-tag` passing:
- Approved version (e.g., `v1.3.0`)
- Tag message: first line of the changelog (`# Release v1.3.0`) or the default `"Release v1.3.0"`
- Target commit (default HEAD; honor the human override if provided)
`kata-tag` returns the locally created tag. Validate with `git tag -v <version>` before proceeding.
### Step 4: Push the Tag to the Remote
```bash
git push origin "$NEXT_TAG"
```
Capture the exit code. If push fails (e.g., tag already exists on the remote), abort with a clear message — do not reuse the tag.
From this point onward, the tag is visible on GitHub and reactive workflows may trigger.
### Step 5: Wait for validate-tag.yml
The `validate-tag.yml` Action (introduced by `lex-annotated-tags`) verifies that the tag is annotated + signed + SemVer-valid. **Wait for its completion**.
> **Caution (race):** after `git push`, GitHub takes a few seconds to register the workflow run. Querying `gh run list` immediately may return `[]`. The Kata MUST poll until the `databaseId` appears, with a timeout.
```bash
TAG_SHA=$(git rev-parse "$NEXT_TAG")
RUN_ID=""
DEADLINE=$(($(date +%s) + 60))
while [ -z "$RUN_ID" ]; do
RUN_ID=$(gh run list \
--workflow validate-tag.yml \
--commit "$TAG_SHA" \
--limit 1 \
--json databaseId \
--jq '.[0].databaseId // empty')
if [ -n "$RUN_ID" ]; then break; fi
if [ $(date +%s) -ge $DEADLINE ]; then
echo "Timeout waiting for validate-tag.yml to register a run for $NEXT_TAG"
exit 1
fi
sleep 3
done
gh run watch "$RUN_ID" --exit-status
```
If `validate-tag.yml` fails: the remote tag is deleted by the Action itself; report to the human and exit with failure. Remediation is to recreate the local tag (likely a signature issue) and try to publish again.
### Step 6: Handle the GitHub Release Cycle
**Path A — workflow-driven (`RELEASE_WORKFLOW != ""`):**
1. Wait for the release workflow to complete (same polling as Step 5 to avoid the race condition):
```bash
REL_RUN_ID=""
DEADLINE=$(($(date +%s) + 60))
while [ -z "$REL_RUN_ID" ]; do
REL_RUN_ID=$(gh run list \
--workflow "$(basename "$RELEASE_WORKFLOW")" \
--commit "$TAG_SHA" \
--limit 1 \
--json databaseId \
--jq '.[0].databaseId // empty')
if [ -n "$REL_RUN_ID" ]; then break; fi
if [ $(date +%s) -ge $DEADLINE ]; then
echo "Timeout waiting for $(basename "$RELEASE_WORKFLOW") to register a run for $NEXT_TAG"
exit 1
fi
sleep 3
done
gh run watch "$REL_RUN_ID" --exit-status
```
2. Verify the Release exists: `gh release view "$NEXT_TAG"`.
3. Compare the auto-generated notes to the changelog from `kata-release-prepare`:
- If the draft is **substantially more informative** (grouping by type, closed issues, highlighted breaking changes): overwrite with `gh release edit`.
- Otherwise: **preserve the auto-generated Release** (default path).
4. Record in the Kata log which path was followed — auditable.
```bash
# Optional overwrite, only when the draft is more informative
gh release edit "$NEXT_TAG" --notes-file "$CHANGELOG_PATH"
```
**Path B — fallback (`RELEASE_WORKFLOW == ""`):**
```bash
gh release create "$NEXT_TAG" \
--title "Release $NEXT_TAG" \
--notes-file "$CHANGELOG_PATH"
```
### Step 7: Verify Post-conditions and Report
- [ ] Local tag exists and `git tag -v <version>` verifies the signature
- [ ] Remote tag exists (`gh api repos/$OWNER/$REPO/git/refs/tags/<version>`)
- [ ] `validate-tag.yml` completed successfully
- [ ] The GitHub Release exists and is reachable
- [ ] Changelog (draft) path moved to `.ahrena/workflow/release/changelog-<version>.published.md` (simple rename)
- [ ] Final report to the human: Release URL, path followed (workflow-driven / fallback), changelog size (auto vs custom)
## Outputs
| Output | Format | Destination |
|--------|--------|-------------|
| Published tag | Annotated + signed Git tag | Remote (`origin`) |
| GitHub Release | HTTPS URL | Presented to the human and recorded in the log |
| Published changelog | Markdown | `.ahrena/workflow/release/changelog-<version>.published.md` |
| Path followed | `workflow-driven` or `fallback` | Kata log (auditing) |
## Restrictions
- **NEVER invoke `gh release create`** when the target repository has a workflow of the form `on: push: tags: ['v*']` that already creates the Release — the race condition causes HTTP 422 (confirmed in v0.11.0, PR #68).
- **NEVER skip the `validate-tag.yml` wait** — without it, invalid releases can be visible to consumers for seconds before the Action deletes the tag.
- **NEVER overwrite silently** auto-generated notes — require the "substantially more informative draft" criterion and record the decision.
- **NEVER re-push** after a `validate-tag.yml` failure on the same SHA without fixing the root cause (likely an invalid signature).
- **NEVER invoke this Kata** without an explicit human approval recorded by `kata-release-prepare` — Janus is an orchestrator, not an autonomous executor.
## Anti-pattern (lesson learned — v0.11.0)
```bash
# ❌ INCORRECT — causes HTTP 422 when the workflow creates the Release first
git push origin v1.2.3
gh release create v1.2.3 --notes-file ./changelog.md
# → pushed tag triggers release.yml, which creates the Release
# → 5 seconds later, gh release create attempts to create again and fails with:
# "tag_name was used by an immutable release"
```
```bash
# ✅ CORRECT — detects workflow, waits, edits only when necessary
git push origin v1.2.3
gh run watch "$(gh run list --workflow release.yml --commit "$(git rev-parse v1.2.3)" \
--limit 1 --json databaseId --jq '.[0].databaseId')"
# Workflow completed; the Release was created automatically.
# Only edit the notes if the prepared changelog is substantially more informative.
gh release edit v1.2.3 --notes-file ./changelog.md
```