---
name: swanlake-release
description: Cut a signed, scrubbed release for the Swanlake repo. Walks the operator through pre-publish audit, conventional-commit changelog, signed annotated tag, and gh release creation. Operator-invoked only.
disable-model-invocation: true
---

# swanlake-release

Operator workflow to cut a signed, scrubbed Swanlake release. The model never auto-invokes this — it runs only when the operator types the skill name.

## Hard rule before you start

**No AI attribution.** This applies to the tag annotation, the release notes, the CHANGELOG entries, and every commit included in the release. The operator's global CLAUDE.md forbids `Co-Authored-By: Claude`, the `🤖 Generated with` footer, "Generated by", "AI-assisted", "made with Opus", etc. anywhere in any artifact this skill produces. If a default template tries to inject any of that, strip it before writing.

If you find AI-attribution in any commit going into this release, stop and ask the operator how to proceed (rebase to scrub vs. abandon the release). Do not silently rewrite history without confirmation.

## Pre-flight checks

Run these in order. Stop on the first failure and surface a clear error.

1. **On `main`, clean tree, in sync with origin.**

   ```bash
   git rev-parse --abbrev-ref HEAD          # must equal: main
   git status --porcelain                   # must be empty
   git fetch origin
   git rev-list --count main..origin/main   # must equal: 0
   git rev-list --count origin/main..main   # must equal: 0
   ```

2. **Pre-publish audit returns clean.** Dispatch the `swanlake-pre-publish` subagent. Its output must be exactly the line `publication-clean`. Anything else: stop, surface the punch-list, do not proceed.

3. **Tests pass locally.** The CI workflow at `.github/workflows/test.yml` runs the same three; running them locally is fast and avoids a pointless tag.

   ```bash
   bash defense-beacon/reference/tests/canary_match_test.sh
   python3 defense-beacon/reference/tests/make_canaries_test.py
   bash trust-zones/reference/tests/apply_mcp_scopes_test.sh
   ```

## Compose the changelog

Generate the entry list from conventional commits since the last tag:

```bash
LAST_TAG="$(git describe --tags --abbrev=0 2>/dev/null || echo '')"
if [ -n "$LAST_TAG" ]; then
  RANGE="$LAST_TAG..HEAD"
else
  RANGE="HEAD"   # first release
fi
git log "$RANGE" --format='- %s'
```

Group by type prefix (`feat:`, `fix:`, `docs:`, `ci:`, `chore:`, etc.) into sections. Drop merge-commit subjects unless they carry meaning. Operator reviews and edits before tagging.

If the repo has a `CHANGELOG.md`, prepend a new `## vX.Y.Z — YYYY-MM-DD` section. If not, ask the operator whether to create one. Either way, this is content the operator owns — show the draft, do not auto-commit.

## Tag

Once the operator approves the changelog and a version number:

```bash
VERSION="vX.Y.Z"   # operator supplies
MSG="<one-line release summary>"

# Signed annotated tag. The operator's git config has gpg.format=ssh and a
# user.signingkey set, so -s is enough — no passphrase prompt expected.
git tag -s "$VERSION" -m "$MSG"
```

Reminder: the `MSG` must not contain AI attribution. Keep it short and factual.

If the changelog was committed in the previous step, push the commit first (`git push origin main` — but only if the operator added a CHANGELOG commit; tags annotating an unchanged main are also fine).

## Push and release

```bash
git push origin "$VERSION"
gh release create "$VERSION" \
  --title "$VERSION" \
  --notes-file <path-to-changelog-section>
```

Use `--notes-file` (not `--notes`) so the operator's reviewed changelog text is what ships verbatim. `gh release create` does not auto-inject AI attribution, but double-check the rendered release page before considering it done.

## Post-flight

- Confirm the tag is signed: `git tag -v "$VERSION"`. Output should show a valid SSH signature against the operator's key.
- Confirm the release is visible: `gh release view "$VERSION" --web` (or `--json`).
- Update `~/.claude/.last-swanlake-release` with the version + ISO timestamp so the operator's status line can surface staleness.

## Things this skill deliberately does not do

- Does not auto-merge any PR.
- Does not push `main` without operator confirmation.
- Does not create a CHANGELOG file unilaterally.
- Does not bump version in any package metadata file (none exists yet; if one is added later, update this skill).
