diff --git a/.github/workflows/release-start.yml b/.github/workflows/release-start.yml index f2e2137..fd5405a 100644 --- a/.github/workflows/release-start.yml +++ b/.github/workflows/release-start.yml @@ -502,9 +502,20 @@ jobs: ${{ inputs.body-extras }} - # Mirror into Linear: the release is created with its issues linked (sync scans the history - # since the previous tag) and moved to in_progress; only when the named package was bumped, - # since the Linear version is the tag. Linear never fails a release that is already cut. + # Mirror into Linear: the release is created with its issues linked and moved to in_progress; + # only when the named package was bumped, since the Linear version is the tag. The sync scans + # from the latest '*' tag — the summary's boundary — not from the pipeline's last + # release: a fresh pipeline has none, and its first sync would then cover the release commit + # alone. Linear never fails a release that is already cut. + - name: Resolve the previous release tag + id: previous-tag + if: ${{ steps.version.outputs.released == 'true' && steps.version.outputs.tag != '' && inputs.op-linear-pipeline-key-path != '' }} + env: + TAG_PREFIX: ${{ inputs.tag-prefix }} + run: | + set -euo pipefail + echo "tag=$(git tag --list "${TAG_PREFIX}[0-9]*" --sort=-v:refname | head -n 1)" >> "$GITHUB_OUTPUT" + - name: Linear — create the release if: ${{ steps.version.outputs.released == 'true' && steps.version.outputs.tag != '' && inputs.op-linear-pipeline-key-path != '' }} uses: aragon/github-templates/steps/linear-release@main @@ -512,6 +523,7 @@ jobs: access-key: ${{ fromJSON(steps.op.outputs.secrets).LINEAR_PIPELINE_ACCESS_KEY }} command: sync version: ${{ steps.version.outputs.tag }} + base-ref: ${{ steps.previous-tag.outputs.tag }} - name: Linear — move the release to in_progress if: ${{ steps.version.outputs.released == 'true' && steps.version.outputs.tag != '' && inputs.op-linear-pipeline-key-path != '' }} diff --git a/README.md b/README.md index d609c72..52ac2f3 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ downloads, an inline npm publish). Nothing that a single repo needs belongs here | `slack-notify` | post / thread / edit a Slack message | | `extract-slack-ts` | parse the `` marker from text | | `resolve-slack-thread` | find the release thread from an explicit ts, a PR number or a release tag (+ the body without markers) | -| `linear-release` | mirror a release into a Linear release pipeline: `sync`, `update` (stage), `complete`; a no-op without an access key | +| `linear-release` | mirror a release into a Linear release pipeline: `sync` (optionally from an explicit `base-ref`), `update` (stage), `complete`; a no-op without an access key | | `parse-playwright-results` | classify a Playwright JSON report | | `gh-ensure-pr` / `gh-ensure-tag` / `gh-ensure-release` | idempotent PR / tag / release | | `gh-ensure-pr-comment` | one tagged PR comment that reruns edit in place (preview URLs, test results) | diff --git a/contracts/interfaces.json b/contracts/interfaces.json index d0a1aa2..1602658 100644 --- a/contracts/interfaces.json +++ b/contracts/interfaces.json @@ -1174,6 +1174,10 @@ "required": false, "default": "" }, + "base-ref": { + "required": false, + "default": "" + }, "command": { "required": false, "default": "sync" diff --git a/docs/release-design.md b/docs/release-design.md index 72c4971..346d587 100644 --- a/docs/release-design.md +++ b/docs/release-design.md @@ -204,7 +204,7 @@ versus the per-repo originals, which assumed the script lived in the same checko | Module | Purpose | Key inputs → outputs | |---|---|---| | `credential-retrieval` *(existing, extended)* | the only module that talks to 1Password: bulk vault → env/file, or named `op://` refs → JSON | `op-token`, `mode`, `op-vault` (bulk modes), `secret-refs` (`byref`) → `secrets` (JSON, `byref`) | -| `setup` | checkout + pnpm + Node + install | `ref`, `node-version`, `registry-url` | +| `setup` | checkout + pnpm + Node (`.nvmrc` when present) + install | `ref`, `node-version-file`, `node-version`, `registry-url` | | `compute-version` | next version + bump + changelog(s); scope→`--ignore` inversion in monorepos | `engine`, `package-dir`, `scope`/`ignore-packages`, `tag-prefix`, `prettier-changelog` → `version`, `released`, `tag` | | `generate-release-summary` | git-log → categorised summary (+Linear); per-package tag boundary + path filtering | `tag-glob`, `path-filter`/`path-patterns`, `linear-api-token`, `base-ref`, `repo` → `summary` | | `generate-version-summary` | per-package `## name@version` + CHANGELOG sections after `changeset version` | `scope`/`packages`/`package-dir` → `summary` | @@ -288,9 +288,11 @@ the PR number on PR events, the release tag on `release: published`, an explicit Without a thread the first message opens one and the run threads under it (a manual deploy). The Linear mirror follows the same moments with `steps/linear-release`: `sync` + `in_progress` at -start, `canceled` on cancel, `complete` on complete. One Linear release pipeline per release -lineage, its access key in the repo's vault (`op-linear-pipeline-key-path`); without a key the -steps are skipped. Linear never fails a release. +start, `canceled` on cancel, `complete` on complete. The sync scans from the previous release tag +(the summary's boundary), not from the pipeline's last release, so a fresh pipeline's first +release is linked in full. One Linear release pipeline per release lineage, its access key in the +repo's vault (`op-linear-pipeline-key-path`); without a key the steps are skipped. Linear never +fails a release. Schedules are the consumer's: a cron on the caller plus `on-active-release: skip`, so a train with an open release PR or nothing to release green-skips — never silently: the channel gets the ⏭️ diff --git a/steps/linear-release/action.yml b/steps/linear-release/action.yml index cd9c8bc..71df972 100644 --- a/steps/linear-release/action.yml +++ b/steps/linear-release/action.yml @@ -4,7 +4,8 @@ description: | the release and links the issues the commits reference, `update` moves it to a stage (`in_progress`, `canceled`), `complete` marks it released. Linear is a mirror, never a gate: the step is skipped without an access key and a Linear error only warns. `sync` scans the git - history, so the checkout must have full depth. + history from `base-ref` (or, without one, from the pipeline's last release — the current commit + alone when the pipeline has none yet), so the checkout must have full depth. inputs: access-key: @@ -22,6 +23,10 @@ inputs: description: "Stage for 'update' (in_progress, canceled)" required: false default: "" + base-ref: + description: "Exclusive lower bound of the 'sync' scan (..HEAD), e.g. the previous release tag; empty lets the CLI pick the pipeline's last release" + required: false + default: "" runs: using: composite @@ -57,3 +62,4 @@ runs: version: ${{ inputs.version }} name: ${{ inputs.command == 'sync' && inputs.version || '' }} stage: ${{ inputs.stage }} + base_ref: ${{ inputs.command == 'sync' && inputs.base-ref || '' }} diff --git a/steps/setup/action.yml b/steps/setup/action.yml index e7a5b12..beae3b1 100644 --- a/steps/setup/action.yml +++ b/steps/setup/action.yml @@ -47,11 +47,14 @@ inputs: required: false default: "" node-version: - description: "The version of Node.js to use (ignored when node-version-file is set)" + description: "The version of Node.js to use when no version file applies (see node-version-file)" required: false default: "24" node-version-file: - description: "Path to a file containing the Node version (e.g. .nvmrc). Takes precedence over node-version." + description: | + Path to a file containing the Node version. Takes precedence over node-version. Left empty, the + repository's own .nvmrc is used when it exists, so a consumer pins Node once; node-version is + the fallback for repositories without one. required: false default: "" node-cache: @@ -112,11 +115,22 @@ runs: shell: bash run: corepack enable + # The version file wins over node-version: the explicit input, else the repository's .nvmrc. + - name: Resolve the Node version file + id: node-version-file + shell: bash + env: + NODE_VERSION_FILE: ${{ inputs.node-version-file }} + run: | + file="$NODE_VERSION_FILE" + if [ -z "$file" ] && [ -f .nvmrc ]; then file=".nvmrc"; fi + echo "file=$file" >> "$GITHUB_OUTPUT" + - name: Setup Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: ${{ inputs.node-version-file == '' && inputs.node-version || '' }} - node-version-file: ${{ inputs.node-version-file }} + node-version: ${{ steps.node-version-file.outputs.file == '' && inputs.node-version || '' }} + node-version-file: ${{ steps.node-version-file.outputs.file }} cache: ${{ inputs.node-cache }} registry-url: ${{ inputs.registry-url }}