Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .skills/kfeatures-release-flow/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
name: kfeatures-release-flow
description: Cut a kfeatures release. Covers the staged CHANGELOG/commit/tag/push workflow with explicit-approval checkpoints, GH007 / noreply identity recovery, PR-label-driven categorized release notes, and cosign keyless verification. Use when the user says "cut a kfeatures release", "tag vX.Y.Z", "release notes are uncategorized", "regenerate release notes", "verify a kfeatures download". Triggers on "kfeatures release", "cut release", "tag vX", "labeler", "release.yml categories", "cosign verify-blob", "GH007".
---

# kfeatures release flow

For per-PR conventions read `AGENTS.md` and `CONTRIBUTING.md`. This skill covers the release cut.

## Hard rules

1. Never commit, push, tag, or merge without explicit user approval per step. A previous "yes" does not authorize the next operation. Ask again.
2. CHANGELOG `[Unreleased]` is the staging area. Every user-visible change lands there in the PR that introduces it. The release-cut PR only renames the section.
3. Tag pushes create a GitHub Release via GoReleaser (`push: tags: v*`). Confirm `gh release view vX.Y.Z` shows the right body before treating the release as done.
4. The release workflow reads the tag's tree, not HEAD. Do not push fixes to `main` after tagging and expect them in the release.

## Workflow

Read `references/release-cut.md` for the command sequence. Steps:

1. Cut commit: rename `[Unreleased]` to `[X.Y.Z] - YYYY-MM-DD`, update compare links at bottom of `CHANGELOG.md`. Subject: `docs(changelog): cut vX.Y.Z release`. Ask before committing.
2. Push the cut commit to `main`. Ask before pushing.
3. Wait for `main` CI green before tagging.
4. Annotated tag with noreply identity (see below). Ask before tagging.
5. Push the tag. Ask before pushing.
6. Verify `gh release view vX.Y.Z` shows the categorized body, all per-platform tarballs, `checksums.txt`, and a `.sigstore.json` sibling for each.

## GH007: noreply identity

The release commit and tag must be authored with the maintainer's GitHub-noreply email (`<id>+<username>@users.noreply.github.com`), not a personal email. The devcontainer's `postCreateCommand` sets `user.email` and `user.name` accordingly; the canonical values for this repo live in `.devcontainer/devcontainer.json`. Verify before tagging:

```bash
git config user.email # must end in @users.noreply.github.com
git config user.name
```

If the identity is wrong (e.g. running outside the devcontainer), use the inline form for each git operation:

```bash
git -c user.email=<noreply-email> -c user.name=<username> commit -m "..."
git -c user.email=<noreply-email> -c user.name=<username> tag -a vX.Y.Z -m "vX.Y.Z"
```

Symptom of a wrong identity on push: `remote: error: GH007: Your push would publish a private email address.` Recover by amending the commit or re-creating the tag with the noreply identity, then re-push.

## Categorized release notes

GitHub generates the release body from PR labels read at the tag, via `.github/release.yml`. Two files must stay in sync:

- `.github/workflows/labeler.yml`: auto-labels new PRs from their Conventional Commit prefix. The `prefixMap` and the `*(deps)` / `*(dependencies)` scope override govern what counts as `dependencies` vs `chore`.
- `.github/release.yml`: the category list (Breaking / Features / Bug Fixes / Documentation / Dependencies / Other) and the `exclude.labels` list (`no-releasenotes`, `chore`).

Add a new prefix or category in both files in the same PR.

### Re-categorizing an already-shipped release

The labeler is additive and only fires on `pull_request: [opened, edited]`. Old PRs need manual label fixes:

1. List PRs in the release range: search for `merged:>=YYYY-MM-DD base:main`.
2. For each, set the right label via `gh pr edit <n> --add-label <label> --remove-label <stale>`. The MCP `github_pull_request_*` tools work too and dodge anonymous rate limits.
3. Regenerate the body: GitHub UI > Release > Edit > "Generate release notes". It re-reads `release.yml` at the tag and the current PR labels. Do not re-tag.

## Verifying a release (cosign)

Released artifacts are signed via cosign keyless using GitHub's OIDC token. Each artifact has a sibling `<artifact>.sigstore.json` bundle. Verify with the commands in `README.md#verifying-releases`:

```bash
cosign verify-blob \
--bundle kfeatures_<version>_<os>_<arch>.tar.gz.sigstore.json \
--certificate-identity "https://github.com/leodido/kfeatures/.github/workflows/release.yaml@refs/tags/v<version>" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
kfeatures_<version>_<os>_<arch>.tar.gz
```

Requires cosign v2.0+ on the verifier side. The certificate identity pins the workflow path and the tag: a different tag's bundle fails verification against this one's identity.

## References

- `references/release-cut.md`: command sequence for steps 1-6, with the explicit-approval checkpoints called out.
100 changes: 100 additions & 0 deletions .skills/kfeatures-release-flow/references/release-cut.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Release cut: command sequence

Read this before cutting `vX.Y.Z`. Every numbered step is an explicit-approval checkpoint: ask before executing, even if a previous step in the same session was approved.

Assumes you are on `main`, working tree clean, and `[Unreleased]` in `CHANGELOG.md` reflects everything merged since the last tag. If not, stop and reconcile first.

## 0. Pre-flight (read-only, no approval needed)

```bash
git fetch --tags origin
git checkout main && git pull --ff-only
git status # must be clean
git log --oneline $(git describe --tags --abbrev=0)..HEAD
git config user.email # must end in @users.noreply.github.com
gh run list --branch main --limit 5 # main CI must be green
```

If `git config user.email` is wrong, prefix every `git commit`/`git tag` below with `-c user.email=<noreply-email> -c user.name=<username>`. Canonical values for this repo: see `.devcontainer/devcontainer.json`.

## 1. Cut commit: ASK BEFORE COMMITTING

Edit `CHANGELOG.md`:

- Rename `## [Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD` (today's UTC date).
- At the bottom of the file, update the compare links:
- Add a new `[Unreleased]: .../compare/vX.Y.Z...HEAD` line.
- Update the existing `[X.Y.Z]: .../compare/v<prev>...vX.Y.Z` line (or add it).

Then:

```bash
git add CHANGELOG.md
git diff --cached # show user; ASK
git commit -m "docs(changelog): cut vX.Y.Z release"
```

Do not add a `Co-authored-by` trailer to release-cut commits.

## 2. Push the cut commit: ASK BEFORE PUSHING

```bash
git push origin main
```

If `main` is protected and refuses direct pushes, open a PR titled `docs(changelog): cut vX.Y.Z release` and merge it. Do not tag until the cut commit is on `main`.

## 3. Wait for `main` CI green

```bash
gh run list --branch main --limit 3
gh run watch <run-id> # or poll until success
```

GoReleaser reads the tag's tree, not HEAD, but a red `main` usually means a broken release too. Do not skip.

## 4. Annotated tag: ASK BEFORE TAGGING

```bash
git tag -a vX.Y.Z -m "vX.Y.Z"
git tag -v vX.Y.Z # confirm the tagger identity
```

The tagger line must end in `@users.noreply.github.com`. If not, delete and re-create with the inline `-c user.email=...` form (see SKILL.md, GH007 section).

## 5. Push the tag: ASK BEFORE PUSHING

```bash
git push origin vX.Y.Z
```

This fires `.github/workflows/release.yaml`. Watch it:

```bash
gh run list --workflow release.yaml --limit 3
gh run watch <run-id>
```

The release workflow needs `id-token: write` for cosign keyless signing (already wired). If it fails at the `sign` step, check that `sigstore/cosign-installer` is pinned and that the workflow has the OIDC permission.

## 6. Verify the release

```bash
gh release view vX.Y.Z
gh release view vX.Y.Z --json assets --jq '.assets[].name' | sort
```

Checklist:

- Release body is categorized: Features / Bug Fixes / Documentation / Dependencies / Other sections appear (if there were PRs for each). If the body is one flat list, PR labels are missing. Fix labels (see SKILL.md, "Re-categorizing"), then UI > Edit > "Generate release notes". Do not re-tag.
- All per-platform tarballs are present (`kfeatures_X.Y.Z_<os>_<arch>.tar.gz` for each goreleaser target).
- `checksums.txt` is present.
- Each artifact has a sibling `<artifact>.sigstore.json` bundle.
- `cosign verify-blob` against one tarball succeeds (see SKILL.md, "Verifying a release").

## Recovery

- Wrong identity on the cut commit, already pushed: amend with the noreply identity and force-push the branch (or open a fix PR). Do not tag yet.
- Wrong identity on the tag, not yet pushed: `git tag -d vX.Y.Z`, re-create with the inline `-c user.email=...` form.
- Wrong identity on the tag, already pushed: `git push --delete origin vX.Y.Z`, recreate locally with noreply, push again. The release workflow will re-fire; delete the bad GitHub Release first if one was created.
- CHANGELOG entry missing or wrong, tag already pushed: add the correction under a fresh `[Unreleased]` and ship it in the next release. Do not retroactively edit a shipped tag's CHANGELOG section.
Loading