CONTRACT · contracts/workflows.md
Contract: GitHub Actions workflows
Raw markdown
# Contract: GitHub Actions workflows
All workflows: `permissions: {}` at the top, minimum per job; third-party actions pinned to a
commit SHA (`# vN.N.N` comment); `pnpm install --frozen-lockfile`; `actions/checkout` with
`persist-credentials: false` unless the job pushes. Research R9–R12.
## `ci.yml` (changed)
| Trigger | Notes |
|---|---|
| `push` (`main` only), `pull_request` | `push` limited to `main` so a PR branch is not tested twice per push |
| `workflow_call` | **new**: `release.yml` runs the full suite on the release commit |
| `workflow_dispatch` | **new**: `bump.yml` starts CI on `release/next` (GITHUB_TOKEN pushes start no workflows) |
Jobs unchanged (unit Node 22/24, E2E on 3 OSes, WSL) plus the new `tooling` Playwright project in
the E2E job. Job permissions: `contents: read`.
## `bump.yml` (new)
- Trigger: `workflow_dispatch` only, inputs:
`kind` (choice: `patch`, `minor`, `major`, `prepatch`, `preminor`, `premajor`, `prerelease`,
`explicit`), `version` (string, required when `kind` = `explicit`), `preid` (string, default `rc`).
- Runs on `main` only (fails otherwise).
- Steps: checkout `main` → `release.js bump` → commit `chore(release): vX.Y.Z` as
`github-actions[bot]` → force-push `release/next` → create PR `Release vX.Y.Z` (base `main`) or
edit the open one → `gh workflow run ci.yml --ref release/next`.
- PR body: the release notes (`release.js notes`) and the reminder that merging tags and starts
the release, which waits for approval.
- Permissions: `contents: write`, `pull-requests: write`, `actions: write`.
- `concurrency: bump` (no parallel bumps).
## `release.yml` (new)
| Trigger | Condition |
|---|---|
| `push` to `main` | paths `package.json`, `CHANGELOG.md` |
| `push` tags `v*` | tag pushed by hand (recovery) |
| `workflow_dispatch` | must be run on a tag ref; no inputs (re-runs the release for that tag; there is no dry-run mode) |
| Job | Needs | Permissions | Environment | Does |
|---|---|---|---|---|
| `prepare` | — | `contents: read`, `pull-requests: read` | — | `release.js release-commit` (push-main) or tag from ref; outputs `release`, `version`, `tag`, `sha`, `dist-tag` |
| `ci` | prepare (release) | `contents: read` | — | `uses: ./.github/workflows/ci.yml` |
| `verify` | prepare (release) | `contents: read` | — | ancestor of `origin/main`; `release.js verify`; not on npm; `pnpm pack`; `release.js pack-check`; `release.js notes`; upload artifact `release` (tarball, notes) |
| `publish` | ci, verify | `contents: write`, `id-token: write` | `npm` (owner approval) | create + push annotated tag if missing; Node 24; `npm publish <tgz> --access public --provenance --tag <dist-tag>` |
| `github-release` | publish | `contents: write` | — | `gh release create <tag> --verify-tag --title <tag> --notes-file notes.md [--prerelease]` |
`concurrency: { group: release, cancel-in-progress: false }`.
Secrets: `NPM_TOKEN` in environment `npm` only for the first publish (research R7); the job maps
it to `NODE_AUTH_TOKEN` and npm prefers OIDC when a trusted publisher is configured.
## `pages.yml` (new)
| Trigger | Condition |
|---|---|
| `push` to `main` | paths `docs/**`, `docmd.config.js`, `scripts/build-site.js`, `src/**`, `bin/**`, `specs/**`, `.specify/**`, `package.json`, `pnpm-lock.yaml`, `.github/workflows/pages.yml` |
| `pull_request` | same paths (build + validate only) |
| `workflow_dispatch` | — |
| Job | Permissions | Does |
|---|---|---|
| `build` | `contents: read` | install; `pnpm run docs:build`; `pnpm exec docmd validate`; `actions/upload-pages-artifact` (path `site`) |
| `deploy` (not on PRs) | `pages: write`, `id-token: write` | `actions/deploy-pages`, environment `github-pages` |
`concurrency: { group: pages, cancel-in-progress: false }`.
## `dependabot.yml` (new)
`npm` and `github-actions` ecosystems, weekly, minor + patch updates grouped per ecosystem.
All workflows: permissions: {} at the top, minimum per job; third-party actions pinned to a
commit SHA (# vN.N.N comment); pnpm install --frozen-lockfile; actions/checkout with
persist-credentials: false unless the job pushes. Research R9–R12.
01 ci.yml (changed)
| Trigger | Notes |
|---|---|
push (main only), pull_request |
push limited to main so a PR branch is not tested twice per push |
workflow_call |
new: release.yml runs the full suite on the release commit |
workflow_dispatch |
new: bump.yml starts CI on release/next (GITHUB_TOKEN pushes start no workflows) |
Jobs unchanged (unit Node 22/24, E2E on 3 OSes, WSL) plus the new tooling Playwright project in
the E2E job. Job permissions: contents: read.
02 bump.yml (new)
- Trigger:
workflow_dispatchonly, inputs:kind(choice:patch,minor,major,prepatch,preminor,premajor,prerelease,explicit),version(string, required whenkind=explicit),preid(string, defaultrc). - Runs on
mainonly (fails otherwise). - Steps: checkout
main→release.js bump→ commitchore(release): vX.Y.Zasgithub-actions[bot]→ force-pushrelease/next→ create PRRelease vX.Y.Z(basemain) or edit the open one →gh workflow run ci.yml --ref release/next. - PR body: the release notes (
release.js notes) and the reminder that merging tags and starts the release, which waits for approval. - Permissions:
contents: write,pull-requests: write,actions: write. concurrency: bump(no parallel bumps).
03 release.yml (new)
| Trigger | Condition |
|---|---|
push to main |
paths package.json, CHANGELOG.md |
push tags v* |
tag pushed by hand (recovery) |
workflow_dispatch |
must be run on a tag ref; no inputs (re-runs the release for that tag; there is no dry-run mode) |
| Job | Needs | Permissions | Environment | Does |
|---|---|---|---|---|
prepare |
— | contents: read, pull-requests: read |
— | release.js release-commit (push-main) or tag from ref; outputs release, version, tag, sha, dist-tag |
ci |
prepare (release) | contents: read |
— | uses: ./.github/workflows/ci.yml |
verify |
prepare (release) | contents: read |
— | ancestor of origin/main; release.js verify; not on npm; pnpm pack; release.js pack-check; release.js notes; upload artifact release (tarball, notes) |
publish |
ci, verify | contents: write, id-token: write |
npm (owner approval) |
create + push annotated tag if missing; Node 24; npm publish <tgz> --access public --provenance --tag <dist-tag> |
github-release |
publish | contents: write |
— | gh release create <tag> --verify-tag --title <tag> --notes-file notes.md [--prerelease] |
concurrency: { group: release, cancel-in-progress: false }.
Secrets: NPM_TOKEN in environment npm only for the first publish (research R7); the job maps
it to NODE_AUTH_TOKEN and npm prefers OIDC when a trusted publisher is configured.
04 pages.yml (new)
| Trigger | Condition |
|---|---|
push to main |
paths docs/**, docmd.config.js, scripts/build-site.js, src/**, bin/**, specs/**, .specify/**, package.json, pnpm-lock.yaml, .github/workflows/pages.yml |
pull_request |
same paths (build + validate only) |
workflow_dispatch |
— |
| Job | Permissions | Does |
|---|---|---|
build |
contents: read |
install; pnpm run docs:build; pnpm exec docmd validate; actions/upload-pages-artifact (path site) |
deploy (not on PRs) |
pages: write, id-token: write |
actions/deploy-pages, environment github-pages |
concurrency: { group: pages, cancel-in-progress: false }.
05 dependabot.yml (new)
npm and github-actions ecosystems, weekly, minor + patch updates grouped per ecosystem.