diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 52833137..1d54b2cf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -180,15 +180,102 @@ jobs: fi echo "Cut ${tag} at ${GITHUB_SHA} (desktop ${prev} -> ${curr})." - - name: Request CodeRabbit review on version PR + # Release cadence switch. With this on, the version PR merges itself once + # every required check passes, so one merge to main becomes one release + # and the version tracks each change instead of collapsing a backlog of + # changesets into a single jump. Turn it off to go back to releasing by + # hand: `gh variable set AUTO_MERGE_RELEASE_PR --body false`. + # + # This is deliberately not a blanket auto-merge. It only ever targets the + # changesets-authored branch, the repository requires its status checks + # before any merge, and a major bump is gated separately on the pull + # request that introduces the changeset — so an unattended release can + # still never rename the major version on its own. + # + # Never fail the release over this: a version PR that stays open costs a + # manual merge, while a failure here would block npm, the Marketplace and + # the CDN behind it. + # Resolve the version PR once, for every step that acts on it. + # + # The changesets action reports the pull request it just created or + # updated, which is the only unambiguous identifier. The fallback covers + # the run where changesets had nothing to change, and it is scoped to a + # head branch in THIS repository by full `owner/name`: `--head` matches a + # branch name only, so a head-name match on its own can select a fork's + # branch of the same name, and matching the owner alone would still accept + # a different repository belonging to that owner. + - name: Resolve the version PR + id: version-pr if: steps.changesets.outputs.published != 'true' env: GH_TOKEN: ${{ steps.release-bot.outputs.token }} + CHANGESETS_PR: ${{ steps.changesets.outputs.pullRequestNumber }} run: | - pr=$(gh pr list --head changeset-release/main --state open --json number --jq '.[0].number' || true) - if [ -n "$pr" ] && [ "$pr" != "null" ]; then - gh pr comment "$pr" --body '@coderabbitai review' + set -uo pipefail + if [ -n "${CHANGESETS_PR}" ] && [ "${CHANGESETS_PR}" != "null" ]; then + echo "number=${CHANGESETS_PR}" >> "${GITHUB_OUTPUT}" + echo "Version PR #${CHANGESETS_PR}, as reported by the changesets action." + exit 0 + fi + if ! open_prs=$(gh pr list \ + --repo "${GITHUB_REPOSITORY}" \ + --head changeset-release/main \ + --base main \ + --state open \ + --json number,headRepository); then + echo "::warning::Could not look up the version PR. Nothing downstream will run; check it by hand." + exit 0 fi + number=$(printf '%s' "${open_prs}" \ + | jq -r --arg repo "${GITHUB_REPOSITORY}" \ + '[.[] | select(.headRepository.nameWithOwner == $repo)][0].number // ""') + if [ -z "${number}" ]; then + echo "::notice::No open version PR in this repository." + exit 0 + fi + echo "number=${number}" >> "${GITHUB_OUTPUT}" + echo "Version PR #${number}." + + # Release cadence switch. With this on, the version PR merges itself once + # every required check passes, so one merge to main becomes one release + # and the version tracks each change instead of collapsing a backlog of + # changesets into a single jump. Turn it off to go back to releasing by + # hand: `gh variable set AUTO_MERGE_RELEASE_PR --body false`. + # + # This is deliberately not a blanket auto-merge. It only ever targets the + # changesets-authored branch in this repository, the repository requires + # its status checks before any merge, and a major bump is gated separately + # on the pull request that introduces the changeset — so an unattended + # release can still never rename the major version on its own. + # + # A changeset that lands while this pull request is waiting on its checks + # joins the same release rather than starting the next one. That window is + # how changesets works, not something this step can close; keeping one + # changeset per pull request keeps it small. + # + # Never fail the release over this: a version PR that stays open costs a + # manual merge, while a failure here would block npm, the Marketplace and + # the CDN behind it. + - name: Enable auto-merge on the version PR + if: steps.version-pr.outputs.number != '' && vars.AUTO_MERGE_RELEASE_PR == 'true' + continue-on-error: true + env: + GH_TOKEN: ${{ steps.release-bot.outputs.token }} + PR: ${{ steps.version-pr.outputs.number }} + run: | + set -uo pipefail + if gh pr merge "${PR}" --squash --auto; then + echo "Auto-merge armed on #${PR}; it lands when its required checks pass." + else + echo "::warning::Could not arm auto-merge on #${PR}. Merge it by hand to cut the release." + fi + + - name: Request CodeRabbit review on version PR + if: steps.version-pr.outputs.number != '' + env: + GH_TOKEN: ${{ steps.release-bot.outputs.token }} + PR: ${{ steps.version-pr.outputs.number }} + run: gh pr comment "${PR}" --body '@coderabbitai review' - name: Resolve Pythinker Code native release if: steps.changesets.outputs.published == 'true' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 79105f13..72a4d638 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -80,6 +80,29 @@ This repo uses [changesets](https://github.com/changesets/changesets) to manage - Generate one with `pnpm changeset` and follow the prompts (which packages are touched, which bump level). - For repo-specific conventions on package selection and bump levels, see `.changeset/README.md`. When working in this repo with coding agents, use the `gen-changesets` skill. +### Bump levels + +| Level | Use for | Example | +| --- | --- | --- | +| `patch` | A fix, or a small addition to something that already exists | `2.1.2` → `2.1.3` | +| `minor` | A capability a user could not reach before | `2.1.3` → `2.2.0` | +| `major` | A break: something that worked stops working, or works differently | `2.2.0` → `3.0.0` | + +Prefer one changeset per pull request. A pull request that needs several is usually carrying several separate releases, and once they are versioned together the changelog can no longer say which change each entry came from. + +A `major` needs a maintainer's sign-off: the `changeset-policy` workflow fails a pull request that adds a major changeset, or edits an existing one up to `major`, unless it carries the `breaking-change-approved` label. A pinned install keeps working, but every consumer who upgrades has to deal with the break, and an npm publish cannot be taken back — so it is a decision, never a side effect of a large branch. + +### Release cadence + +Changesets keeps a `ci: release packages` pull request open on `main` and rewrites it as changesets land. Merging it cuts exactly one release, so how often it is merged is what decides the version sequence: + +- Merged per change, versions follow each change: `2.1.2`, `2.1.3`, `2.1.4`, `2.2.0`. +- Left to accumulate, a backlog collapses into one bump and the numbers in between never exist. + +The repository variable `AUTO_MERGE_RELEASE_PR` chooses between the two. Set to `true`, the release pull request merges itself once its required checks pass, giving one release per change. Unset or `false`, a maintainer merges it when a release is wanted. + +Either way there is a window: a changeset that lands while the release pull request is waiting on its checks joins that release instead of starting the next one. That is how changesets works, so a release can still carry more than one change — one changeset per pull request keeps the window small. + ## Pull Requests Every PR opens with the [PR template](.github/pull_request_template.md). PR titles must follow [Conventional Commits](#commit-convention); CI runs `pnpm lint`, `pnpm typecheck`, and `pnpm test` on every PR. Update user-facing docs in `docs/` when behavior changes — use the `gen-docs` skill when working with coding agents.