From ec2a8137f9bbf89b209a7e72f774f78260f2dae2 Mon Sep 17 00:00:00 2001 From: 4gray Date: Thu, 30 Jul 2026 08:10:33 +0200 Subject: [PATCH] docs(release): synchronize release workflow guidance --- .changes/README.md | 14 +-- .claude/skills/release-cut/SKILL.md | 118 +++++++++++--------------- .claude/skills/release-notes/SKILL.md | 73 ++++++---------- .codex/skills/release-cut/SKILL.md | 118 +++++++++++--------------- .codex/skills/release-notes/SKILL.md | 73 ++++++---------- .github/workflows/build-and-make.yaml | 15 ++-- AGENTS.md | 3 + CLAUDE.md | 3 + tools/release/project.json | 1 + tools/release/release-notes.test.mjs | 17 +++- 10 files changed, 192 insertions(+), 243 deletions(-) diff --git a/.changes/README.md b/.changes/README.md index e41282f03..b8be5c01e 100644 --- a/.changes/README.md +++ b/.changes/README.md @@ -49,14 +49,18 @@ The body is capped at 400 characters — depth belongs in the blog post. - ❌ "Fix off-by-one in `resolveEnrichmentSeasonNumber`" - ✅ "Series whose title carries a season marker no longer show the wrong season" -`type: internal` is for changes with no user-visible effect that are still worth -recording (dependency bumps with behaviour risk, packaging moves). They stay out -of the release body and blog post, and land collapsed in `CHANGELOG.md`. +`type: internal` records invisible maintenance. Internal notes stay collapsed in +`CHANGELOG.md`, are omitted from the blog scaffold, and are removed from the +authored public GitHub body by +`extract-changelog-section.mjs --public`. GitHub's generated commit list remains +separate. An internal-only release can therefore have an empty authored body. ## When a note is not needed -Skip the note — and apply the `no-release-note` label — for test-only changes, -docs, CI/workflow plumbing, and pure refactors with no behaviour change. +The gate auto-exempts website, E2E and mock-server apps, `*.spec.{js,ts}`, +`*.e2e.{js,ts}`, snapshots, any `/testing/` path, and Markdown. For other +test-only, documentation, CI/workflow, or pure-refactor changes under +`apps/`/`libs/`, apply `no-release-note` when no user-visible note is warranted. ## Commands diff --git a/.claude/skills/release-cut/SKILL.md b/.claude/skills/release-cut/SKILL.md index 62e07edd5..b62b77d96 100644 --- a/.claude/skills/release-cut/SKILL.md +++ b/.claude/skills/release-cut/SKILL.md @@ -1,88 +1,72 @@ --- 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. +description: Use when preparing, cutting, tagging, publishing, or verifying an IPTVnator release or its release assets. --- # 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. +The tag build takes authored public text from the new CHANGELOG section. Keep +the full changelog committed before tagging. -## Sequence +## Preflight -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. +Work from clean, current `master` with the intended remote named explicitly. +Confirm `package.json` contains bare semver, the exact `v` tag does not +exist locally or remotely, CI is green, and all notes validate. -2. **Review the notes** — read every file in `.changes/`. Fix wording (user - language, not reviewer language), then: +```bash +pnpm run release:notes:validate +pnpm run i18n:check +``` - ```bash - pnpm run release:notes:validate - ``` +## Generate -3. **Generate the changelog section** (idempotent per version — rerunning - replaces the section, so regenerate freely until it reads well): +1. Set `package.json.version`. +2. Run `pnpm run release:notes:changelog`. +3. Minor release: run `pnpm run release:notes:blog` and finish every editorial + field. Patch release: edit the existing `vX-Y` post; do not scaffold or + force-overwrite it. +4. Capture required manifest screenshots only against mock servers: + `pnpm nx run electron-backend:build-e2e`, then + `pnpm run release:screenshots`. +5. Consume notes only after reviewing all generated output: + `node tools/release/build-release-notes.mjs --consume`. - ```bash - pnpm run release:notes:changelog - ``` +The consume command is the destructive boundary: it deletes the direct note +files. Stage only release-owned files, including exact website post/assets and +`git add -A -- .changes`, then commit and create the exact tag. -4. **Scaffold the website post**: +```bash +git commit -m "chore(release): v0.24.0" +git tag v0.24.0 +``` - ```bash - pnpm run release:notes:blog - ``` +## Push and External Effects - 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`). +Push only the intended branch and tag; never use broad `git push --tags`. -5. **Screenshots** — only from the fail-closed capture script against the - mock servers, never from a real playlist or account: real streams, logos, - and TMDB artwork are copyrighted, and credentials must never reach a - published image. +```bash +git push --atomic origin \ + HEAD:refs/heads/master \ + refs/tags/v0.24.0 +``` - ```bash - pnpm nx run electron-backend:build-e2e # once - pnpm run release:screenshots # all manifest shots, dark+light - ``` +Master and `v*` pushes can publish Docker images. The tag build creates a draft +GitHub release. Verify authored text plus generated commits and all required +macOS, Windows, DEB, RPM, Pacman (`.pacman`/`.pkg.tar.*`), AppImage, Snap, +Flatpak, updater metadata, blockmaps, and +`linux-frame-copy-runtime-sources.tar.xz`. - Output goes to `apps/website/public/blog/v0-XX/screenshots/`. New feature - to showcase = new entry in `tools/release/screenshots.manifest.json` - (slug must match the note's `screenshot:` field). The run aborts and - deletes its frames on any guard violation (real-DB touch, external - request, credential-shaped text in frame, TMDB active). +After verification, manually publish the GitHub release. That publication +automatically verifies its Snap assets and uploads them to `edge`. +Installed-Snap smoke and candidate/stable promotion remain manual. Keep the +blog draft during artifact verification; publish it in a follow-up commit and +verify the website deployment. -6. **Consume the notes** (the only destructive step): +## Failure Safety - ```bash - node tools/release/build-release-notes.mjs --consume - ``` +Missing CHANGELOG section: regenerate, commit, delete the bad tag locally and +remotely only after resolving its exact target, then retag. Never publish a +draft until the source archive and Snap contract pass. -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. +The `.codex` and `.claude` copies of this skill must remain byte-identical. diff --git a/.claude/skills/release-notes/SKILL.md b/.claude/skills/release-notes/SKILL.md index 7ccb1338d..308456234 100644 --- a/.claude/skills/release-notes/SKILL.md +++ b/.claude/skills/release-notes/SKILL.md @@ -1,70 +1,49 @@ --- 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. +description: "Use when a change may need a .changes note, the Release note gate fails, or deciding whether type: internal or no-release-note applies." --- -# Release Notes (`.changes/`) +# Release Notes -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. +Every user-visible change gets one direct `.changes/-.md` file. The +area matches the conventional-commit scope; the body is present tense, user +language, one to three sentences, and at most 400 characters. -## File format - -Name: `.changes/-.md` — `area` matches the -conventional-commit scope of the PR. +## Format ```markdown --- -type: feature -area: playback -issues: [1187] -screenshot: up-next-rail +type: fix +area: stalker +issues: [1234] +screenshot: optional-manifest-slug --- -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. +Stalker series now resume the correct episode. ``` -| 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 | +`type` is `breaking`, `feature`, `fix`, `perf`, or `internal`. Omit optional +fields instead of inventing values. Never add a version or PR number. -- **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:`. +`internal` records invisible maintenance. It stays collapsed in `CHANGELOG.md` +but is omitted from the blog and the authored public GitHub body. GitHub's +generated commit list may still mention the underlying commits. -## Writing the body +## Skip or Label -One to three sentences, present tense, max 400 characters, **written for a -user, not a reviewer**: +The gate auto-exempts website, E2E and mock-server apps, `*.spec.{js,ts}`, +`*.e2e.{js,ts}`, snapshots, any `/testing/` path, and Markdown. Other test-only, +docs, CI, workflow, or pure-refactor PRs use `no-release-note` when the gate +would otherwise require a note. At least one newly added direct +`.changes/*.md` file satisfies the gate. -- ❌ "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 +## Verify ```bash pnpm run release:notes:validate ``` -Full format reference: `.changes/README.md`. Gate policy: +Full format: `.changes/README.md`. Gate policy: `tools/release/check-release-note-gate.mjs`. + +The `.codex` and `.claude` copies of this skill must remain byte-identical. diff --git a/.codex/skills/release-cut/SKILL.md b/.codex/skills/release-cut/SKILL.md index 62e07edd5..b62b77d96 100644 --- a/.codex/skills/release-cut/SKILL.md +++ b/.codex/skills/release-cut/SKILL.md @@ -1,88 +1,72 @@ --- 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. +description: Use when preparing, cutting, tagging, publishing, or verifying an IPTVnator release or its release assets. --- # 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. +The tag build takes authored public text from the new CHANGELOG section. Keep +the full changelog committed before tagging. -## Sequence +## Preflight -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. +Work from clean, current `master` with the intended remote named explicitly. +Confirm `package.json` contains bare semver, the exact `v` tag does not +exist locally or remotely, CI is green, and all notes validate. -2. **Review the notes** — read every file in `.changes/`. Fix wording (user - language, not reviewer language), then: +```bash +pnpm run release:notes:validate +pnpm run i18n:check +``` - ```bash - pnpm run release:notes:validate - ``` +## Generate -3. **Generate the changelog section** (idempotent per version — rerunning - replaces the section, so regenerate freely until it reads well): +1. Set `package.json.version`. +2. Run `pnpm run release:notes:changelog`. +3. Minor release: run `pnpm run release:notes:blog` and finish every editorial + field. Patch release: edit the existing `vX-Y` post; do not scaffold or + force-overwrite it. +4. Capture required manifest screenshots only against mock servers: + `pnpm nx run electron-backend:build-e2e`, then + `pnpm run release:screenshots`. +5. Consume notes only after reviewing all generated output: + `node tools/release/build-release-notes.mjs --consume`. - ```bash - pnpm run release:notes:changelog - ``` +The consume command is the destructive boundary: it deletes the direct note +files. Stage only release-owned files, including exact website post/assets and +`git add -A -- .changes`, then commit and create the exact tag. -4. **Scaffold the website post**: +```bash +git commit -m "chore(release): v0.24.0" +git tag v0.24.0 +``` - ```bash - pnpm run release:notes:blog - ``` +## Push and External Effects - 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`). +Push only the intended branch and tag; never use broad `git push --tags`. -5. **Screenshots** — only from the fail-closed capture script against the - mock servers, never from a real playlist or account: real streams, logos, - and TMDB artwork are copyrighted, and credentials must never reach a - published image. +```bash +git push --atomic origin \ + HEAD:refs/heads/master \ + refs/tags/v0.24.0 +``` - ```bash - pnpm nx run electron-backend:build-e2e # once - pnpm run release:screenshots # all manifest shots, dark+light - ``` +Master and `v*` pushes can publish Docker images. The tag build creates a draft +GitHub release. Verify authored text plus generated commits and all required +macOS, Windows, DEB, RPM, Pacman (`.pacman`/`.pkg.tar.*`), AppImage, Snap, +Flatpak, updater metadata, blockmaps, and +`linux-frame-copy-runtime-sources.tar.xz`. - Output goes to `apps/website/public/blog/v0-XX/screenshots/`. New feature - to showcase = new entry in `tools/release/screenshots.manifest.json` - (slug must match the note's `screenshot:` field). The run aborts and - deletes its frames on any guard violation (real-DB touch, external - request, credential-shaped text in frame, TMDB active). +After verification, manually publish the GitHub release. That publication +automatically verifies its Snap assets and uploads them to `edge`. +Installed-Snap smoke and candidate/stable promotion remain manual. Keep the +blog draft during artifact verification; publish it in a follow-up commit and +verify the website deployment. -6. **Consume the notes** (the only destructive step): +## Failure Safety - ```bash - node tools/release/build-release-notes.mjs --consume - ``` +Missing CHANGELOG section: regenerate, commit, delete the bad tag locally and +remotely only after resolving its exact target, then retag. Never publish a +draft until the source archive and Snap contract pass. -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. +The `.codex` and `.claude` copies of this skill must remain byte-identical. diff --git a/.codex/skills/release-notes/SKILL.md b/.codex/skills/release-notes/SKILL.md index 7ccb1338d..308456234 100644 --- a/.codex/skills/release-notes/SKILL.md +++ b/.codex/skills/release-notes/SKILL.md @@ -1,70 +1,49 @@ --- 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. +description: "Use when a change may need a .changes note, the Release note gate fails, or deciding whether type: internal or no-release-note applies." --- -# Release Notes (`.changes/`) +# Release Notes -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. +Every user-visible change gets one direct `.changes/-.md` file. The +area matches the conventional-commit scope; the body is present tense, user +language, one to three sentences, and at most 400 characters. -## File format - -Name: `.changes/-.md` — `area` matches the -conventional-commit scope of the PR. +## Format ```markdown --- -type: feature -area: playback -issues: [1187] -screenshot: up-next-rail +type: fix +area: stalker +issues: [1234] +screenshot: optional-manifest-slug --- -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. +Stalker series now resume the correct episode. ``` -| 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 | +`type` is `breaking`, `feature`, `fix`, `perf`, or `internal`. Omit optional +fields instead of inventing values. Never add a version or PR number. -- **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:`. +`internal` records invisible maintenance. It stays collapsed in `CHANGELOG.md` +but is omitted from the blog and the authored public GitHub body. GitHub's +generated commit list may still mention the underlying commits. -## Writing the body +## Skip or Label -One to three sentences, present tense, max 400 characters, **written for a -user, not a reviewer**: +The gate auto-exempts website, E2E and mock-server apps, `*.spec.{js,ts}`, +`*.e2e.{js,ts}`, snapshots, any `/testing/` path, and Markdown. Other test-only, +docs, CI, workflow, or pure-refactor PRs use `no-release-note` when the gate +would otherwise require a note. At least one newly added direct +`.changes/*.md` file satisfies the gate. -- ❌ "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 +## Verify ```bash pnpm run release:notes:validate ``` -Full format reference: `.changes/README.md`. Gate policy: +Full format: `.changes/README.md`. Gate policy: `tools/release/check-release-note-gate.mjs`. + +The `.codex` and `.claude` copies of this skill must remain byte-identical. diff --git a/.github/workflows/build-and-make.yaml b/.github/workflows/build-and-make.yaml index 43158d2d8..3638e3359 100644 --- a/.github/workflows/build-and-make.yaml +++ b/.github/workflows/build-and-make.yaml @@ -1450,15 +1450,12 @@ jobs: if [ "${IS_TAG_BUILD}" = "true" ]; then NAME="Release v${VERSION}" TAG="${GITHUB_REF_NAME}" - # Authored release notes: the release flow writes this - # CHANGELOG section from .changes/*.md before tagging - # (see .changes/README.md), so at tag time the changelog - # is the authored source of truth. The extractor exits - # non-zero when the section is missing, failing the - # release rather than silently shipping PR-title-only - # notes. generate_release_notes stays on below, so the - # GitHub commit list still renders under this body. - BODY="$(node tools/release/extract-changelog-section.mjs "${VERSION}")" + # The full committed CHANGELOG retains internal notes, + # while the tag release uses authored public extraction. + # An internal-only release intentionally has no authored + # public text. GitHub's generated commit list remains + # separate below. + BODY="$(node tools/release/extract-changelog-section.mjs --public "${VERSION}")" elif [ "${EVENT_NAME}" = "pull_request" ]; then NAME="v${VERSION} — PR #${PR_NUMBER} @ ${SHORT_SHA} [test]" TAG="test-pr-${PR_NUMBER}" diff --git a/AGENTS.md b/AGENTS.md index 4fa10bf23..1825f96f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,10 +40,13 @@ This file provides guidance to coding agents working in this repository. - Any change a user could notice — new behavior, changed behavior, bug fix, performance win, breaking change — must add one note file under `.changes/` in the same PR. Format, field table, and writing rules: `.changes/README.md`. - Name it `-.md`; `area` matches the conventional-commit scope. There is no version field — the release version is chosen at release time. - Write the body for a user, not a reviewer: "the player now remembers volume between episodes", not "hoist volume state into the session". Max 400 characters; depth belongs in the release blog post. +- `type: internal` records invisible maintenance. Internal notes stay collapsed in `CHANGELOG.md`, are omitted from the blog scaffold, and are removed from the authored public GitHub body by `extract-changelog-section.mjs --public`; GitHub's generated commit list remains separate, so an internal-only release can have an empty authored body. - Skip the note for test-only changes, docs, CI/workflow plumbing, and pure refactors with no behavior change. When skipping on a PR that touches `apps/**` or `libs/**`, apply the `no-release-note` label. - CI enforces this: the "Release note gate" job in `.github/workflows/ci.yml` fails PRs that change runtime code without an added `.changes/*.md` or the label (policy in `tools/release/check-release-note-gate.mjs`; tests/e2e/website/mock-server/docs paths are auto-exempt). - The `release-notes` skill covers writing notes; the `release-cut` skill covers the full release sequence. - Validate before finishing: `pnpm run release:notes:validate`. +- Pushes to `master` and `v*` can publish Docker images. A `v*` tag build creates a draft GitHub release. +- Publishing the GitHub release verifies its Snap assets and automatically uploads them to `edge`; installed-Snap smoke and candidate/stable promotion remain manual. - Release-post screenshots come only from the release capture script running against the mock servers. Never add a screenshot taken from a real playlist or account to `apps/website/public/blog/**` — real streams, logos, and metadata are copyrighted, and credentials must never reach a published image. - Final task summaries should state whether a release note was added or why it was skipped. diff --git a/CLAUDE.md b/CLAUDE.md index 5090c4a66..7fd7739c0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,10 +31,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - Any change a user could notice — new behavior, changed behavior, bug fix, performance win, breaking change — must add one note file under `.changes/` in the same PR. Format, field table, and writing rules: `.changes/README.md`. - Name it `-.md`; `area` matches the conventional-commit scope. There is no version field — the release version is chosen at release time. - Write the body for a user, not a reviewer: "the player now remembers volume between episodes", not "hoist volume state into the session". Max 400 characters; depth belongs in the release blog post. +- `type: internal` records invisible maintenance. Internal notes stay collapsed in `CHANGELOG.md`, are omitted from the blog scaffold, and are removed from the authored public GitHub body by `extract-changelog-section.mjs --public`; GitHub's generated commit list remains separate, so an internal-only release can have an empty authored body. - Skip the note for test-only changes, docs, CI/workflow plumbing, and pure refactors with no behavior change. When skipping on a PR that touches `apps/**` or `libs/**`, apply the `no-release-note` label. - CI enforces this: the "Release note gate" job in `.github/workflows/ci.yml` fails PRs that change runtime code without an added `.changes/*.md` or the label (policy in `tools/release/check-release-note-gate.mjs`; tests/e2e/website/mock-server/docs paths are auto-exempt). - The `release-notes` skill covers writing notes; the `release-cut` skill covers the full release sequence. - Validate before finishing: `pnpm run release:notes:validate`. +- Pushes to `master` and `v*` can publish Docker images. A `v*` tag build creates a draft GitHub release. +- Publishing the GitHub release verifies its Snap assets and automatically uploads them to `edge`; installed-Snap smoke and candidate/stable promotion remain manual. - Release-post screenshots come only from the release capture script running against the mock servers. Never add a screenshot taken from a real playlist or account to `apps/website/public/blog/**` — real streams, logos, and metadata are copyrighted, and credentials must never reach a published image. - Final task summaries should state whether a release note was added or why it was skipped. diff --git a/tools/release/project.json b/tools/release/project.json index 0dec473ec..e9003ea6a 100644 --- a/tools/release/project.json +++ b/tools/release/project.json @@ -15,6 +15,7 @@ "{workspaceRoot}/tools/release/build-release-notes.mjs", "{workspaceRoot}/tools/release/screenshot-guards.mjs", "{workspaceRoot}/tools/release/screenshots.manifest.json", + "{workspaceRoot}/.github/workflows/build-and-make.yaml", "{workspaceRoot}/tools/release/release-notes.test.mjs", "{workspaceRoot}/tools/release/release-note-gate.test.mjs", "{workspaceRoot}/tools/release/build-release-notes.test.mjs", diff --git a/tools/release/release-notes.test.mjs b/tools/release/release-notes.test.mjs index 9717bb964..86c0eb5ad 100644 --- a/tools/release/release-notes.test.mjs +++ b/tools/release/release-notes.test.mjs @@ -1,5 +1,5 @@ import assert from 'node:assert/strict'; -import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { after, describe, it } from 'node:test'; @@ -610,6 +610,21 @@ describe('extractPublicSection', () => { }); describe('extract-changelog-section CLI contracts', () => { + it('uses public extraction for authored tag-release text', () => { + const workflow = readFileSync( + new URL( + '../../.github/workflows/build-and-make.yaml', + import.meta.url + ), + 'utf8' + ); + + assert.match( + workflow, + /extract-changelog-section\.mjs --public "\$\{VERSION\}"/ + ); + }); + it('parses the public flag', () => { assert.deepEqual(parseExtractArguments(['--public', '0.24.0']), { version: '0.24.0',