ci(release): gate PRs on an authored release note (#1257)

* ci(release): gate PRs on an authored release note

Second slice of the release-notes pipeline (#1256 landed the format and
generator): make the .changes/ habit survive contact with reality.

- "Release note gate" job in ci.yml, PR-only: validates every .changes/*.md,
  then requires an added note (or the no-release-note label) when the PR
  touches runtime code under apps/ or libs/. Tests, e2e projects, the
  website, mock servers, shared testing helpers, snapshots and docs are
  auto-exempt.
- Policy lives in tools/release/check-release-note-gate.mjs as a pure
  function fed PR files+labels as JSON — unit-tested (10 cases) instead of
  encoded in workflow bash. The failure message lists the triggering files
  and names the exact fix.
- Labels are fetched live rather than from the stale event payload, so
  applying the label and re-running the check works without a new push.
- The job is dependency-free Node: no pnpm install, runs in seconds.
- release-notes and release-cut skills added under .claude/skills/ and
  mirrored to .codex/skills/; CLAUDE.md/AGENTS.md sections updated to point
  at the gate and the skills.

The no-release-note label itself was created in the repository.

Tests: 47 passing in release-tools (10 new gate cases); gate-step shell
verified with shellcheck at the CI severity; ci.yml YAML-parse checked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(agents): make the release skills discoverable by Claude Code too

`.codex/skills/**` was un-ignored so Codex picks up repository skills in any
clone, but `.claude` was ignored wholesale — and Claude Code only discovers
skills under `.claude/skills/`. The release-notes and release-cut skills
therefore existed only on whichever machine authored them.

Mirror both skills into `.claude/skills/` and opt them in by name rather than
un-ignoring the directory: contributors keep personal skills there
(i18n-fill, website, …) which must stay local and out of `git status`.

CLAUDE.md/AGENTS.md updated so the "skills live under .codex/skills/" claim
does not go stale, including the requirement to keep mirrored copies in sync.

The CI gate and the CLAUDE.md/AGENTS.md section remain the load-bearing
enforcement; skills only carry the detail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(ci): only a note this PR authored satisfies the release-note gate

Review follow-ups on #1257 (Codex P2 ×2, Greptile P1).

- Drop `renamed` from the accepted statuses. The PR files API compares
  base…head, so a note created and then renamed inside the same PR still
  reports as `added`; a `renamed` entry means the file already existed on the
  base branch. Accepting it let a runtime-code PR pass by moving another
  PR's unconsumed note, which documents nothing and gives the generator no
  adding commit to resolve a PR link from.
- Require a direct child of `.changes/`. `loadNotes()` reads only the
  immediate directory, so `.changes/sub/note.md` satisfied the old prefix
  check while never being validated or rendered into any release surface.

Tests: renamed and nested notes now assert a failing gate (12 gate cases).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5 authored and GitHub committed 2026-07-25 21:51:04 +02:00
1 parent 21e55e4bb4
commit 4e5132cbb5
11 files changed
+658 -5

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
---
name: release-cut
description: Cut an IPTVnator release — bump the version, generate release notes from .changes/, scaffold the website post, tag, and verify the draft. Use when asked to release, cut a version, prepare release notes, or publish a new version.
---
# Release Cut
The pipeline turns accumulated `.changes/*.md` notes into all three release
surfaces. Order matters: **the tag build extracts the CHANGELOG section into
the GitHub release body and fails if it is missing**, so the changelog step
is not optional.
## Sequence
1. **Pick the version** — deliberate choice, edit `version` in the root
`package.json`. Bare semver only: any suffix flips electron-updater into
prerelease mode and leaks into installer version fields.
2. **Review the notes** — read every file in `.changes/`. Fix wording (user
language, not reviewer language), then:
```bash
pnpm run release:notes:validate
```
3. **Generate the changelog section** (idempotent per version — rerunning
replaces the section, so regenerate freely until it reads well):
```bash
pnpm run release:notes:changelog
```
4. **Scaffold the website post**:
```bash
pnpm run release:notes:blog
```
Output is `apps/website/src/content/blog/v0-XX-release-notes.mdx` with
`draft: true`. The narrative intro, headlines, and `description` are
editorial — fill every `TODO` by hand. One post per **minor** version:
for a patch release, edit the existing post (the scaffold refuses to
overwrite without `--force`).
5. **Screenshots** — only from the release capture script against the mock
servers (`tools/release/`), never from a real playlist or account: real
streams, logos, and TMDB artwork are copyrighted, and credentials must
never reach a published image. Output goes to
`apps/website/public/blog/v0-XX/screenshots/`.
6. **Consume the notes** (the only destructive step):
```bash
node tools/release/build-release-notes.mjs --consume
```
7. **Commit, tag, push**:
```bash
git add CHANGELOG.md .changes apps/website package.json
git commit -m "chore(release): v0.XX.0"
git tag v0.XX.0 && git push && git push --tags
```
8. **Verify the draft release** once `build-and-make.yaml` finishes: authored
notes on top, GitHub's generated commit list below, all platform assets
present (`.dmg`/`.zip` + `latest-mac.yml`, `.exe`/`.msi` + `latest.yml`,
`.deb`/`.rpm`/`.AppImage`/`.snap`/`.flatpak` + `latest-linux*.yml`,
blockmaps). Publish manually; flip the blog post to `draft: false`.
## Failure modes
- **create-release fails with "CHANGELOG.md has no section for X"** — step 3
was skipped. Run it, commit, delete and re-push the tag.
- **Snap store publication** is a separate manual flow after the public
release exists (`publish-snap.yaml`).
- Post-release checklist candidates: i18n drift (`pnpm run i18n:check`),
update the website `v0-XX` blog assets, announce in Telegram.
+70
View File
@@ -0,0 +1,70 @@
---
name: release-notes
description: Write the .changes/ release note that every PR with a user-visible change must include. Use when creating or finishing a PR that changes behavior in apps/ or libs/, when the "Release note gate" CI check fails, or when deciding whether the no-release-note label applies.
---
# Release Notes (`.changes/`)
Every PR with a user-visible change adds **one** note file under `.changes/`.
At release time the notes become the GitHub release body, the `CHANGELOG.md`
section, and the website blog scaffold. CI enforces this: the **Release note
gate** check fails any PR that touches runtime code under `apps/` or `libs/`
without an added `.changes/*.md` file or the `no-release-note` label.
## File format
Name: `.changes/<area>-<short-slug>.md` — `area` matches the
conventional-commit scope of the PR.
```markdown
---
type: feature
area: playback
issues: [1187]
screenshot: up-next-rail
---
Series now show an "Up Next" rail beside the player on wide windows: the rest
of the current season, watch progress, and click-to-play inline.
```
| Field | Required | Value |
| ------------ | -------- | ---------------------------------------------------- |
| `type` | yes | `breaking` / `feature` / `fix` / `perf` / `internal` |
| `area` | yes | lowercase slug = conventional-commit scope |
| `issues` | no | `[1187]` or bare `1187` — issues this PR closes |
| `screenshot` | no | slug from the release screenshot manifest |
- **No version field.** The release version is chosen at release time.
- **Never write a PR number.** The generator resolves it from git.
- Unknown keys fail validation — this is what catches typos like `scopr:`.
## Writing the body
One to three sentences, present tense, max 400 characters, **written for a
user, not a reviewer**:
- ❌ "Refactor `WebVideoControlsAdapter` to hoist volume state"
- ✅ "The player now remembers volume between episodes"
- ❌ "Fix off-by-one in `resolveEnrichmentSeasonNumber`"
- ✅ "Series with a season marker in the title no longer show the wrong season"
`type: internal` is for changes worth recording but invisible to users
(dependency bumps with behavior risk, packaging moves). They are excluded
from the release body and blog, and collapsed in `CHANGELOG.md`.
## When to skip (`no-release-note` label)
Test-only changes, docs, CI/workflow plumbing, pure refactors with no
behavior change. The gate auto-exempts `*.spec.ts`, `*.e2e.ts`,
`__snapshots__/`, `apps/website/`, `apps/*-e2e/`, `apps/*-mock-server/`,
`libs/shared/testing/` and `*.md` — if only those changed, no label needed.
## Verify before finishing
```bash
pnpm run release:notes:validate
```
Full format reference: `.changes/README.md`. Gate policy:
`tools/release/check-release-note-gate.mjs`.