diff --git a/.agents/skills/release/SKILL.md b/.agents/skills/release/SKILL.md new file mode 100644 index 00000000..92057772 --- /dev/null +++ b/.agents/skills/release/SKILL.md @@ -0,0 +1,41 @@ +--- +name: release +description: Cut a release of the WordPress Contributor Toolkit, stable or beta, from the version bump to the published GitHub release and docs deploy. Use when the user asks to release, cut, ship or publish a version, bump the version for a release, or names a version such as 2.0.0 or 2.1.0-beta.1 to release. +--- + +# Release + +Drive a release through `RELEASING.md`, pausing wherever a step is public or needs a person. + +## Procedure + +**Read [`RELEASING.md`](../../../RELEASING.md) and follow it.** It is the specification for this task, not background reading: every step and every command lives there, including the artifact checks, how to pick the base for the release notes, and the checks after publishing. Do not restate it here; run it. Changes to how releases work go there, not into this wrapper. + +The skill takes the version as its argument (`2.0.0`, `2.1.0-beta.1`). If none is given, ask. A version of the form `X.Y.Z` is stable and `X.Y.Z-beta.N` is a beta. Stop and ask about any other form, such as `2.1.0-rc.1`, rather than treating it as stable. + +What this skill adds on top: + +**Check the starting state first.** Run the checks in section 1, item 1 of `RELEASING.md`, and stop and report if any fails. If a milestone named for the full version exists, report any issues and pull requests still open in it, without stopping. + +**Wait for the person's yes before each of these:** + +1. Creating the release branch, after confirming its name. +2. Pushing the branch and opening the bump pull request. +3. Pushing to the branch again, when it has to be updated with trunk before the merge. Then repeat the artifact checks on the new head's build. +4. `gh release create`, after showing the final notes and the exact command. +5. Dispatching the docs workflow for a stable release (a beta skips it), and closing the milestone if the release has one. + +The merge is the person's. Wait to be told it is merged, unless the person asks this skill to merge: then merge with the command in section 3 of `RELEASING.md`, guarded by the head commit the artifacts were checked on. However it was merged, run the checks that follow the merge in section 3 before anything else. If the check after the merge prints anything, stop and report it; do not tag until the artifacts have been checked again as that section says. + +**The bump pull request is an ordinary pull request.** Fill in `.github/pull_request_template.md` and run the review AGENTS.md requires before opening it. + +**The person downloads the artifacts; this skill checks them.** This skill cannot see Buildkite: the build reports nothing to the pull request. Ask the person to confirm that the build's commit matches the pull request's current head, and to download the three files from it into a directory they name. Then run the checks in section 2 of `RELEASING.md`, *Check the signed artifacts*, and report each result. Do not report the build as matching the head until the person has said it does. + +**Draft the release notes once the pull request is merged.** Write them plainly, save them to a temporary directory outside the repository, and show them in the chat for editing before `gh release create` (item 4 above). + +**Run the checks in section 7 of `RELEASING.md`, *Check the result*, before reporting done**, and report each one. + +## Notes + +- This is the same skill as [`.claude/skills/release/`](../../../.claude/skills/release/), placed here because agents built on the Agent Skills open standard discover skills under `.agents/skills/`. Both are thin wrappers over `RELEASING.md`. +- If a step in `RELEASING.md` turns out wrong or incomplete during a release, say so at the end. That file is meant to accumulate what each release teaches. diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md new file mode 100644 index 00000000..a547e5b4 --- /dev/null +++ b/.claude/skills/release/SKILL.md @@ -0,0 +1,41 @@ +--- +name: release +description: Cut a release of the WordPress Contributor Toolkit, stable or beta, from the version bump to the published GitHub release and docs deploy. Use when the user asks to release, cut, ship or publish a version, bump the version for a release, or names a version such as 2.0.0 or 2.1.0-beta.1 to release. +--- + +# Release + +Drive a release through `RELEASING.md`, pausing wherever a step is public or needs a person. + +## Procedure + +**Read `RELEASING.md` and follow it.** It is the specification for this task, not background reading: every step and every command lives there, including the artifact checks, how to pick the base for the release notes, and the checks after publishing. Do not restate it here; run it. Changes to how releases work go there, not into this wrapper. + +The skill takes the version as its argument (`2.0.0`, `2.1.0-beta.1`). If none is given, ask. A version of the form `X.Y.Z` is stable and `X.Y.Z-beta.N` is a beta. Stop and ask about any other form, such as `2.1.0-rc.1`, rather than treating it as stable. + +What this skill adds on top: + +**Check the starting state first.** Run the checks in section 1, item 1 of `RELEASING.md`, and stop and report if any fails. If a milestone named for the full version exists, report any issues and pull requests still open in it, without stopping. + +**Wait for the person's yes before each of these:** + +1. Creating the release branch, after confirming its name. +2. Pushing the branch and opening the bump pull request. +3. Pushing to the branch again, when it has to be updated with trunk before the merge. Then repeat the artifact checks on the new head's build. +4. `gh release create`, after showing the final notes and the exact command. +5. Dispatching the docs workflow for a stable release (a beta skips it), and closing the milestone if the release has one. + +The merge is the person's. Wait to be told it is merged, unless the person asks this skill to merge: then merge with the command in section 3 of `RELEASING.md`, guarded by the head commit the artifacts were checked on. However it was merged, run the checks that follow the merge in section 3 before anything else. If the check after the merge prints anything, stop and report it; do not tag until the artifacts have been checked again as that section says. + +**The bump pull request is an ordinary pull request.** Fill in `.github/pull_request_template.md` and run the review AGENTS.md requires before opening it. + +**The person downloads the artifacts; this skill checks them.** This skill cannot see Buildkite: the build reports nothing to the pull request. Ask the person to confirm that the build's commit matches the pull request's current head, and to download the three files from it into a directory they name. Then run the checks in section 2 of `RELEASING.md`, *Check the signed artifacts*, and report each result. Do not report the build as matching the head until the person has said it does. + +**Draft the release notes once the pull request is merged.** Write them plainly, save them to a temporary directory outside the repository, and show them in the chat for editing before `gh release create` (item 4 above). + +**Run the checks in section 7 of `RELEASING.md`, *Check the result*, before reporting done**, and report each one. + +## Notes + +- This is the same skill as [`.agents/skills/release/`](../../../.agents/skills/release/), for agents that read the Agent Skills open-standard path. Both are thin wrappers over `RELEASING.md`. +- If a step in `RELEASING.md` turns out wrong or incomplete during a release, say so at the end. That file is meant to accumulate what each release teaches. diff --git a/AGENTS.md b/AGENTS.md index e5fb3df5..e5dbc851 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ Instructions for AI coding agents working in this repository. Tool-neutral and c ## Where things are, whatever agent you use -Two files, and neither is specific to one tool despite what their paths suggest: +Three things, none of them specific to one tool despite what their paths suggest: **The review standard** — [`.github/instructions/code-review.instructions.md`](.github/instructions/code-review.instructions.md). The five dimensions, this project's invariants, the procedure to run them, and the reporting format. Read it directly if your agent has not already. @@ -15,12 +15,14 @@ It sits under `.github/instructions/` because Copilot code review reads that dir - [`.claude/skills/self-review/`](.claude/skills/self-review/) — Claude Code and Copilot. In Claude Code it is `/self-review`. - [`.agents/skills/self-review/`](.agents/skills/self-review/) — the Agent Skills open-standard path that Command Code and others scan. -Both are pointers to the one instructions file, not copies of the standard. If your agent looks somewhere else again (`.github/skills/`, `.cursor/skills/`, `.codex/skills/`, or its own convention), add a wrapper there or skip it entirely and follow the instructions file — that is where all the content lives. The wrapper only adds two things: run the judgement pass in a fresh context, and report without touching GitHub. +Both are pointers to the one instructions file, not copies of the standard. If your agent looks somewhere else again (`.github/skills/`, `.cursor/skills/`, `.codex/skills/`, or its own convention), add a wrapper there or skip it entirely and follow the instructions file — that is where all the content lives. The review wrapper only adds two things: run the judgement pass in a fresh context, and report without touching GitHub. In Codex, `/review` is the built-in user-facing review command. Whenever `/review` runs, read and follow [`.github/instructions/code-review.instructions.md`](.github/instructions/code-review.instructions.md) in full as the review standard. `self-review` is the local skill name, not a Codex command; do not tell users to invoke `self-review`. There is deliberately no per-tool copy of the *standard*. A wrapper is a few lines that defer to it; duplicating the standard itself is the thing to avoid — if you find yourself doing that, fix the pointer, not the number of copies. +**Releasing** — [`RELEASING.md`](RELEASING.md) is the procedure. The `release` skill wraps it the same way, in [`.claude/skills/release/`](.claude/skills/release/) (`/release X.Y.Z` in Claude Code) and [`.agents/skills/release/`](.agents/skills/release/). The wrapper adds the stops before each public step and leaves the merge to the person unless they ask it to merge. + ## What this is An Electron desktop app ("WordPress Contributor Toolkit") that sets up a full WordPress core (`wordpress-develop`) dev environment with zero prerequisites — no Git, Node, npm, or Docker required on the host. Everything is bundled and run as JS/WASM inside the Electron process. Built to fix a Contributor Day problem: newcomers burning the whole session on local setup instead of contributing. Still labeled "experimental." diff --git a/RELEASING.md b/RELEASING.md index 5a10b611..17c21e8f 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,13 +2,20 @@ How to cut a release of the WordPress Contributor Toolkit. Run every command from the repository root unless a step says otherwise. -Versions are `X.Y.Z` for a stable release and `X.Y.Z-beta.N` for a beta. Tags add a `v`: `v1.2.0`, `v1.2.0-beta.2`. +Versions are `X.Y.Z` for a stable release and `X.Y.Z-beta.N` for a beta. Tags add a `v`: `v1.2.0`, `v1.2.0-beta.2`. Any other suffix, such as `-rc.1`, is not covered here: stop and decide how to release it before using these steps. In the commands below, `X.Y.Z` stands for the full version, `-beta.N` included. ## 1. Open the version-bump pull request `electron-builder` names the artifacts after the version in `package.json`, so the version moves before anything is built. -1. Branch from an up-to-date trunk. +1. Branch `release/X.Y.Z` from an up-to-date trunk. Check first that trunk matches `origin`, that nothing tracked has changed (untracked files are fine), and that the tag does not exist yet. The two `rev-parse` lines print the same commit, and the last two commands print nothing: + + ```bash + git fetch origin trunk --tags + git rev-parse trunk origin/trunk + git status --porcelain --untracked-files=no + git ls-remote --tags origin refs/tags/vX.Y.Z + ``` 2. Change the version in three places: `"version"` in `package.json`, and the two root `"version"` fields at the top of `package-lock.json` (the top-level one and `packages[""]`). No dependency version changes. 3. Check the two files agree: @@ -19,53 +26,144 @@ Versions are `X.Y.Z` for a stable release and `X.Y.Z-beta.N` for a beta. Tags ad 4. Run `npm run i18n:download` and commit `src/languages/`. It writes a catalog for every locale at least 80% translated on translate.wordpress.org, removes every other catalog in `src/languages/` (an untracked one included), and prints a coverage table. Right-to-left locales are held back even at 80%, and the output lists them after the table under `Not shipped:`. When no locale reaches 80% and there is no catalog to remove, it prints `No locales downloaded.` instead of a table. If it exits with an error, nothing in `src/languages/` changed; run it again. Skipping this step ships the previous release's translations, or none, and nothing fails. See [Translatable strings](CONTRIBUTING.md#translatable-strings) in `CONTRIBUTING.md`. 5. Run `npm run lint` and `npm test`. -6. Open the pull request, titled `Bump version to X.Y.Z`, and paste the coverage table into its description, or the script's message if it printed no table. [#533](https://github.com/WordPress/contributor-toolkit/pull/533) is a good model. `git diff trunk` must show exactly three changed lines outside `src/languages/`. +6. Open the pull request, titled `Bump version to X.Y.Z`, and paste the coverage table into its description, or the script's message if it printed no table. [#533](https://github.com/WordPress/contributor-toolkit/pull/533) is a good model. Outside `src/languages/`, the diff must be the three version lines, plus any change to `build.electronLanguages` in `package.json`, and nothing else: + + ```bash + git diff trunk -- . ':!src/languages' + ``` + + `npm run i18n:download` rewrites `build.electronLanguages` whenever the Chromium locales the catalogs need change, and prints the resulting list as `Chromium locales kept`. Name the locales it added or removed in the pull request description. ## 2. Check the signed artifacts -Buildkite builds signed Windows, macOS and Linux artifacts for the pull request's branch. Before merging, open the build for the pull request's **current head commit** and check: +Buildkite builds signed Windows, macOS and Linux artifacts for the pull request's branch. Before merging, open the build for the pull request's **current head commit** (`gh pr view --json headRefOid --jq .headRefOid`), download the three files into one directory, and check them. Buildkite reports nothing back to the pull request (`.buildkite/pipeline.yml` asks for a commit status per platform, but none reached the 2.0.0 bump pull request), so the commit can only be read on the build's page in Buildkite: compare it with `headRefOid` there, by eye. -- The files are named `wordpress-contributor-toolkit-X.Y.Z-win-x64.exe`, `wordpress-contributor-toolkit-X.Y.Z-mac-arm64.dmg` and `wordpress-contributor-toolkit-X.Y.Z-linux-x86_64.AppImage`. +- The files are named `wordpress-contributor-toolkit-X.Y.Z-win-x64.exe`, `wordpress-contributor-toolkit-X.Y.Z-mac-arm64.dmg` and `wordpress-contributor-toolkit-X.Y.Z-linux-x86_64.AppImage`. The Windows file may download as `dist_wordpress-contributor-toolkit-X.Y.Z-win-x64.exe`, because the Windows build uploads it from `dist\`. Rename it to remove the `dist_` prefix, so its name matches the other two. +- The macOS app is signed with Automattic's Developer ID, notarized, and has its ticket stapled. +- The Windows installer carries an Authenticode signature from Automattic Inc. - The app's `app.asar` root holds exactly `node_modules`, `package.json` and `src`, with npm 11 under `node_modules/npm` and no `.codesigning` directory. - On macOS, `CFBundleShortVersionString` in the app's `Info.plist` reads `X.Y.Z`. +All but the name and Windows checks run on the macOS artifact, on a Mac, from the repository root (so `@electron/asar` resolves from `node_modules`). Replace `` with the directory holding the downloads: + +```bash +mnt="$(mktemp -d)" +hdiutil attach -nobrowse -readonly -mountpoint "$mnt" "/wordpress-contributor-toolkit-X.Y.Z-mac-arm64.dmg" +app="$mnt/WordPress Contributor Toolkit.app" +codesign --verify --deep --strict "$app" && echo signed +spctl -a -vv -t exec "$app" +xcrun stapler validate "$app" +npx asar list "$app/Contents/Resources/app.asar" | grep -E '^/[^/]+$' +node -p "JSON.parse(require('@electron/asar').extractFile(process.argv[1], 'node_modules/npm/package.json')).version" "$app/Contents/Resources/app.asar" +plutil -extract CFBundleShortVersionString raw "$app/Contents/Info.plist" +hdiutil detach "$mnt" && rmdir "$mnt" +``` + +`codesign` prints `signed`. `spctl` prints `accepted`, `source=Notarized Developer ID` and an `origin` naming `Automattic, Inc.`. `stapler` prints `The validate action worked!`. The `asar list` command prints `/node_modules`, `/package.json` and `/src`, and nothing else. The `node` command prints a version starting with `11.`. `plutil` prints `X.Y.Z`. + +Read the npm version with `extractFile` as above, in memory. `npx asar extract-file` writes the file into the current directory, which from the repository root replaces its own `package.json`. + +Verify the Windows signature on Windows, in PowerShell: `Get-AuthenticodeSignature "\wordpress-contributor-toolkit-X.Y.Z-win-x64.exe"` prints `Status` `Valid` and a signer naming Automattic Inc. This is the check that counts, because it confirms the signature matches the file. On a Mac, `openssl` can show who signed the installer before that check, but it does not verify the signature: + +```bash +node -e 'const fs=require("fs");const b=fs.readFileSync(process.argv[1]);const o=b.readUInt32LE(0x3c)+24;const d=o+(b.readUInt16LE(o)===0x20b?112:96)+32;const at=b.readUInt32LE(d),n=b.readUInt32LE(d+4);if(!n)throw new Error("not signed");process.stdout.write(b.subarray(at+8,at+n))' "/wordpress-contributor-toolkit-X.Y.Z-win-x64.exe" | openssl pkcs7 -inform DER -print_certs -noout | grep subject +``` + +It prints a `subject` line naming `O=Automattic Inc.`, and fails with `not signed` when the installer has no signature. A tampered installer that kept the certificate prints the same, so this does not replace the Windows check. + ## 3. Merge -Update the branch with trunk first if it has fallen behind, and check the artifacts again on the new head. Download the three files from the build you checked last. They are what the release ships. +Check that the head you checked already contains trunk. The second command prints `up to date`; if it prints nothing, update the branch with trunk, wait for the new head's build, and download and check the artifacts again from it: -The Windows file may download as `dist_wordpress-contributor-toolkit-X.Y.Z-win-x64.exe`, because the Windows build uploads it from `dist\`. Rename it to remove the `dist_` prefix before you attach it to the release, so its name matches the other two and the one in step 2. +```bash +git fetch origin trunk "pull//head" +git merge-base --is-ancestor origin/trunk && echo up to date +``` + +The files from the build you checked last are what the release ships. -Then squash-merge. The merged commit has the same content as that build, and it is the one you tag. +Then squash-merge, with the head you checked as a guard, so the merge is refused if anything was pushed after the check: + +```bash +gh pr merge --squash --match-head-commit +``` + +The merged commit has the same content as that build, and it is the one you tag. Find it with: + +```bash +gh pr view --json mergeCommit --jq .mergeCommit.oid +``` + +Check that it matches the build: after `git fetch origin trunk`, `git diff --stat ` prints nothing. If it prints anything, trunk moved before the merge, and the artifacts are not the merged code. Do not tag yet: the release needs artifacts built from the merged commit. Buildkite builds every push to trunk, so open its trunk build for the merged commit, download its three files and repeat section 2 on them, comparing the build's commit with the merged commit instead of `headRefOid`. Tag only once they pass. ## 4. Publish the GitHub release -Create the tag and the release together, on the bump commit: +Write the notes first. They open with what changed for a contributor, grouped by theme, with the pull request number on each line, and end with a **Downloads** section naming the file for each platform. The [v1.2.0 notes](https://github.com/WordPress/contributor-toolkit/releases/tag/v1.2.0) are a good model for a stable release, and the previous beta's notes for a beta. + +They cover everything since the previous stable release for a stable release, the betas' changes included, and since the previous tag of any kind for a beta. Find the base tag, and the commit count since it that the opening paragraph gives (it includes the bump commit): + +```bash +git fetch origin trunk --tags +git describe --tags --abbrev=0 --exclude '*-*' ^ # stable; drop --exclude for a beta +git rev-list --count .. +``` + +Then create the tag and the release together, on the bump commit. Every file is named by its path and `--repo` names the repository, so the command can run from anywhere: ```bash gh release create vX.Y.Z \ + --repo WordPress/contributor-toolkit \ --target \ --title vX.Y.Z \ - --notes-file notes.md \ - wordpress-contributor-toolkit-X.Y.Z-win-x64.exe \ - wordpress-contributor-toolkit-X.Y.Z-mac-arm64.dmg \ - wordpress-contributor-toolkit-X.Y.Z-linux-x86_64.AppImage + --notes-file "/notes.md" \ + "/wordpress-contributor-toolkit-X.Y.Z-win-x64.exe" \ + "/wordpress-contributor-toolkit-X.Y.Z-mac-arm64.dmg" \ + "/wordpress-contributor-toolkit-X.Y.Z-linux-x86_64.AppImage" ``` - **A beta** adds `--prerelease`, so `releases/latest` stays on the last stable release. -- **A stable release** takes `releases/latest`. - -The notes open with what changed for a contributor, grouped by theme, with the pull request number on each line, and end with a **Downloads** section naming the file for each platform. The [v1.2.0 notes](https://github.com/WordPress/contributor-toolkit/releases/tag/v1.2.0) are a good model. +- **A stable release** adds `--latest`, so it takes `releases/latest`. ## 5. Deploy the docs -The docs site tracks the latest release, not trunk, so publish it for the new tag: +Skip this step for a beta. The docs site tracks the latest release, not trunk, and a beta does not take `releases/latest`, so its docs would describe UI the Download button's build does not have. + +For a stable release, publish the docs for the new tag: ```bash gh workflow run docs.yml --ref trunk -f ref=vX.Y.Z ``` -The dispatch has to be on trunk; the `ref` input is the tag. See [the documentation site](CONTRIBUTING.md#the-documentation-site) in `CONTRIBUTING.md`. +The dispatch has to be on trunk; the `ref` input is the tag. A dispatch on any other branch finishes green without deploying, so check that the run's **deploy to Pages** job succeeded. A new run takes a few seconds to appear, so check that the run listed was created after the dispatch, not the previous release's: + +```bash +gh run list --workflow docs.yml --event workflow_dispatch --branch trunk -L 1 --json databaseId,createdAt +gh run watch --exit-status +gh run view --json jobs --jq '.jobs[] | select(.name=="deploy to Pages") | .conclusion' +``` + +The last command prints `success`. + +See [the documentation site](CONTRIBUTING.md#the-documentation-site) in `CONTRIBUTING.md`. ## 6. Close the milestone -If the release has a milestone, close it and move anything still open in it to the next one. A beta often has none: leave the stable release's milestone open until the stable release ships. +If the release has a milestone named `vX.Y.Z`, close it and move anything still open in it to the next one. A beta often has none; close the beta's own milestone if it has one, and leave the stable release's milestone open until the stable release ships. + +```bash +gh issue list --milestone vX.Y.Z --state open +gh pr list --search 'milestone:vX.Y.Z' --state open +gh api 'repos/WordPress/contributor-toolkit/milestones?state=open' --jq '.[] | "\(.number) \(.title)"' +gh api -X PATCH repos/WordPress/contributor-toolkit/milestones/ -f state=closed +``` + +The first two list the issues and pull requests still open, the third finds the milestone's number, and the fourth closes it. + +## 7. Check the result + +- `gh release view vX.Y.Z` lists the three files under the names from step 2, and shows the release as a pre-release only for a beta. +- `gh api repos/WordPress/contributor-toolkit/releases/latest --jq .tag_name` prints `vX.Y.Z` for a stable release, and the previous stable tag for a beta. +- `git ls-remote origin refs/tags/vX.Y.Z` points at the bump commit. +- For a stable release, the docs run from step 5 finished with its deploy job. +- The Download button on [the docs site](https://wordpress.github.io/contributor-toolkit/) reads `vX.Y.Z` for a stable release, and the previous stable tag for a beta. Open the site in a new tab or a private window: the button keeps the release it read in the tab's session storage (`docs/.vitepress/theme/DownloadButton.vue`), so a tab opened before the release goes on showing the old one, reloads included. +- The milestone, if the release has one, is closed.