diff --git a/.github/workflows/pnpm-release-changeset.yml b/.github/workflows/pnpm-release-changeset.yml index 05dbb67..eda5003 100644 --- a/.github/workflows/pnpm-release-changeset.yml +++ b/.github/workflows/pnpm-release-changeset.yml @@ -11,7 +11,7 @@ on: value: ${{ jobs.release.outputs.published }} permissions: id-token: write - contents: read + contents: write concurrency: ${{ github.workflow }}-${{ github.ref }} @@ -55,9 +55,10 @@ jobs: id: changesets uses: changesets/action@v2.0.0 with: - commit: 'chore: version packages' - version: pnpm run version - publish: pnpm run release + github-token: ${{ secrets.CI_GITHUB_TOKEN }} + commit-message: 'chore: version packages' + version-script: pnpm run version + publish-script: pnpm run release env: GITHUB_TOKEN: ${{ secrets.CI_GITHUB_TOKEN }} NPM_TOKEN: '' diff --git a/README.md b/README.md index e2f89c2..8f2d211 100644 --- a/README.md +++ b/README.md @@ -11,3 +11,212 @@ Use the workflow templates to create workflows for each repository. References: - + +## Versioning + +### Which ref to pin + +**Pin `@v1`.** + +```yaml +jobs: + verify: + uses: repobuddy/.github/.github/workflows/pnpm-verify.yml@v1 +``` + +`v1` is a *moving* tag. It is re-pointed forward on every backward-compatible +release, so you get fixes without doing anything. It is never moved across a +breaking change — that is what `v2` would be for. + +If you need a ref that never moves under you, pin the exact version instead +(`@v1.0.0`) and accept that you must bump it by hand to get any fix. + +Do **not** pin `@main`. `main` is the development branch: it carries unreleased +and possibly broken work, and every consumer on it takes every change the +instant it merges. That is how a silently-broken release path reached three +repos ([#43](https://github.com/repobuddy/.github/issues/43)). + +### The scheme + +Semver tags plus a moving major alias — the GitHub Actions ecosystem +convention, and what `actions/*` itself does: + +| Tag | Mutable? | Meaning | +| --- | --- | --- | +| `v1.0.0` | no | One exact release. Never re-pointed. | +| `v1` | yes | Latest `v1.x.y`. Re-pointed on each compatible release. | + +This was chosen over an immutable-only scheme with full deliberation of the +tradeoff: a moving `v1` does hand back some of the instant-propagation problem +that `@main` had. It is still a large improvement, because the two are not +equivalent: + +- `@main` propagates **everything**, including breaking changes and + work-in-progress, with no one having decided it was safe. +- `@v1` propagates only what a maintainer explicitly judged backward + compatible. A breaking change stops at the `v1` boundary and requires + consumers to opt in by moving to `@v2`. + +The moving alias is also what Dependabot and Renovate understand, and what +anyone reading a `uses:` line in this org will expect. An immutable-only scheme +is strictly safer but only if someone actually bumps the pins; a stale pin +nobody updates is a worse outcome than a moving tag. Consumers who want the +stricter guarantee can opt into `@v1.0.0` individually. + +### Why this matters more here than in most repos + +`main` in this repo is *not* hand-curated. `.mergify.yml` auto-merges Renovate +PRs, so dependency bumps to the actions these workflows call land on `main` +without a human in the loop — and under `@main` they reach every consumer the +moment they merge. + +That is not hypothetical. `changesets/action` v1 → **v2**, a major upgrade with +renamed inputs, arrived as Renovate PR +[#42](https://github.com/repobuddy/.github/pull/42) on branch +`renovate/changesets-action-2.x` and was auto-merged. The Mergify rule intends +to hold majors back with `head~=^(?!major-)`, but Renovate does not prefix these +branches with `major-`, so the guard did not match and the major merged like any +patch. The result was [#43](https://github.com/repobuddy/.github/issues/43): a +release path that silently stopped publishing, in three repos at once. + +Tagging fixes the consumer half of this — an auto-merged bump now lands on +`main` and waits there until someone cuts a release. The Mergify rule not +actually excluding majors is a separate defect and should be fixed on its own. + +### Starting version: `v1.0.0` + +Not `v0.x`. Two reasons: + +1. These workflows are already in production use by three repos and have been + for a long time. `v0.x` would advertise "expect this to break", which is a + less honest description of the status quo than `v1` — and `@main` offered no + stability guarantee whatsoever, so anything we tag is an improvement rather + than a regression in stability. +2. The moving-major-alias convention is only coherent at `>= 1`. Under semver, + `0.x` minor bumps are permitted to break, so a moving `v0` alias would carry + breaking changes to every consumer automatically — precisely the failure mode + this change exists to stop. + +### What one version covers + +**Everything in this repo shares one tag** — all eight reusable workflows plus +the `setup-playwright` composite action. They live in one repo, and a git tag +names a commit of the whole repo; there is no per-workflow versioning. + +The consequence, stated plainly: **a change to any one workflow bumps the tag +for all consumers of all of them.** A repo that only uses `pnpm-verify.yml` +will still see version churn from an edit to `pnpm-release-semantic.yml`. +That churn is noise, not risk — the unchanged workflows are byte-identical +across the two tags. + +If that churn ever becomes a real problem, the escape hatch is per-workflow tag +prefixes (`pnpm-verify/v1`). Do not reach for it pre-emptively; it multiplies +the release procedure by eight. + +### What counts as breaking + +A major bump is required when a change would break a consumer that changed +nothing on its side: + +- removing or renaming a workflow, an action, or one of their inputs +- making a previously optional input required +- changing a default such that existing behavior changes +- requiring a secret that consumers did not previously have to set +- **requiring a new precondition in the consumer's own repo** + +That last one is easy to miss and is live right now: the `pnpm-release-changeset.yml` +workflow uses `changesets/action@v2`, which **validates that the consumer is on +`@changesets/cli` v3 and fails for v2 users.** So `v1`'s contract includes +"consumers of `pnpm-release-changeset.yml` must be on `@changesets/cli` v3". + +As of 2026-08-12 **all four consumers are still on `@changesets/cli` v2** +(`repobuddy` `^2.26.0`, `storybook` `^2.29.7`, `visual-testing` `^2.29.8`, +`jest-watch-toggle-config-2` `^2.25.2`). None of them can adopt this workflow at +`v1` until it upgrades. They are not losing anything by waiting — `main` already +carries `changesets/action@v2`, so their release path is already mismatched +today; pinning a tag does not create that problem, it just stops the next one. + +### Will Renovate keep consumer pins current? + +Checked, because a pin nobody bumps is worse than `@main`. + +All four consumers extend `github>unional/renovate-preset`, which is: + +```json +{ + "description": "Preserving Semver", + "extends": ["config:base", ":preserveSemverRanges"] +} +``` + +No `enabledManagers`, no `packageRules`, nothing disabling `github-actions`. +`config:base` enables all managers, and Renovate's `github-actions` manager +handles both `steps[].uses` and reusable-workflow `jobs..uses`. **So yes — +tagged refs will be picked up.** (`:preserveSemverRanges` applies to npm ranges, +not Actions refs. Renovate ignores `@main`, which is why nothing bumps today.) + +One consequence worth understanding before it surprises someone: pinned to the +moving `@v1`, Renovate has **nothing to bump** for ordinary releases — `v1` is +still `v1` after `v1.0.1`. You will see no update PRs, and you do not need them, +because the alias moves on its own. Renovate only opens a PR when `v2` appears, +which is exactly the moment a human should be looking. Repos that pin exact +(`@v1.0.0`) get a PR per release instead. + +Dependabot will not help here: `visual-testing` and `storybook` have a +`.github/dependabot.yml` with an `npm` entry only and no `github-actions` +ecosystem. Renovate is doing this job. + +Two config problems found while checking, neither blocking: + +- `repobuddy/storybook` has **two** Renovate configs — a root `renovate.json` + (`config:recommended`) and `.github/renovate.json` (the org preset). Root wins + by Renovate's precedence order and the other is ignored. The github-actions + manager is enabled either way, so tag bumping still works, but the duplicate + should be removed. +- `config:base` is a deprecated alias for `config:recommended`; worth updating + the preset. + +### Cutting a release + +Manual. This repo is tagged rarely, and a release workflow for that cadence is +scaffolding that needs its own maintenance. Two commands and a force-move: + +```bash +# 1. From the merge commit on main that you intend to release: +git checkout main && git pull + +# 2. Immutable version tag + release notes. +git tag -a v1.0.0 -m "v1.0.0" +git push origin v1.0.0 +gh release create v1.0.0 --generate-notes + +# 3. Move the major alias forward. -f on both sides is required: the tag +# already exists and is intentionally being re-pointed. +git tag -f v1 v1.0.0 +git push -f origin v1 +``` + +Step 3 is the one that is easy to get wrong or forget. If `v1` is not moved, the +release is invisible to everyone pinning `@v1`. + +#### Known gap: internal refs still float on `@main` + +Two workflows in this repo consume this repo's own composite action: + +```text +.github/workflows/pnpm-verify.yml:43 +.github/workflows/pnpm-release-changeset.yml:42 + uses: repobuddy/.github/.github/actions/setup-playwright@main +``` + +A reusable workflow cannot reference a sibling action by relative path — `./` +resolves against the *caller's* checkout, not this repo's — so these must be +fully-qualified `owner/repo/path@ref`, and today that ref is `@main`. **A +consumer pinned at `@v1` therefore still picks up `setup-playwright` from +`main`,** which partially defeats the pin. + +This is deliberately not fixed in the same change that introduces the scheme: +re-pointing them to `@v1` before `v1` exists would break every current consumer +immediately. Once `v1.0.0` is cut, change both lines to `@v1` — the moving +alias, so they do not need touching on every subsequent release — and include +that edit in the release. Until then, treat it as a known hole.