-
Notifications
You must be signed in to change notification settings - Fork 0
278 lines (259 loc) · 12.5 KB
/
Copy pathrelease.yml
File metadata and controls
278 lines (259 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
name: Release
# Manual, one-button release. Pick a version (a semver bump keyword or an exact
# version), and this workflow: runs the full CI gate, bumps package.json,
# writes the changelog from the commits since the last tag, commits + tags,
# publishes to public npm, and cuts a GitHub Release whose body is that
# changelog. Run `dry_run: true` first to preview without pushing anything.
#
# Publishing uses npm Trusted Publishing (OIDC) — no NPM_TOKEN secret. Configure
# a trusted publisher for @copse/streaming-markdown at npmjs.com (Settings ->
# Trusted Publisher) pointing at this repo + workflow file (release.yml). The
# package must exist first, so the initial publish is a one-time manual
# `npm publish` (interactive 2FA); every release after that flows through here.
on:
workflow_dispatch:
inputs:
bump:
description: 'Semver bump to apply (ignored when "exact version" is set below)'
required: true
default: patch
type: choice
options:
- patch
- minor
- major
- prepatch
- preminor
- premajor
- prerelease
exact_version:
description: 'Optional: exact version to release (e.g. 1.2.0). Overrides the bump dropdown when set.'
required: false
default: ''
type: string
prerelease:
description: 'Mark the GitHub Release as a pre-release'
required: false
default: false
type: boolean
dry_run:
description: 'Build the release notes and version bump but do not commit, tag, publish, or release'
required: false
default: false
type: boolean
# Least privilege: write to the repo (commit + tag + release). id-token: write
# lets the runner mint the GitHub OIDC token that npm Trusted Publishing uses to
# authenticate the publish (and to attach provenance automatically).
permissions:
contents: write
id-token: write
concurrency:
group: release
cancel-in-progress: false
jobs:
release:
# Deliberately NOT routed to the self-hosted pool (vars.CHECKS_RUNNER):
# npm Trusted Publishing / provenance requires a GitHub-hosted runner, and
# hosted minutes are free on a public repo anyway.
runs-on: ubuntu-latest
steps:
# Mint a short-lived installation token from the release GitHub App. The
# App is on the `main` branch (and tag) ruleset bypass list, so the
# version-bump commit + tag pushed below clear the "changes must be made
# through a pull request" rule (GH013) that rejects a direct push by the
# default GITHUB_TOKEN (github-actions[bot]). The token expires in ~1h and
# is scoped to this repo's contents — nothing long-lived lives in CI.
- uses: actions/create-github-app-token@v3
id: app-token
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
# Pin the grant instead of inheriting whatever the installation
# happens to hold. The App is installed org-wide and agent-pane's
# workflows need Pull requests: write and Actions: write from it, so
# an inherited token would silently widen the moment those are added
# — and this one is persisted to disk by the checkout below.
permission-contents: write
- uses: actions/checkout@v7
with:
# Check out with the App token so `actions/checkout` persists it as the
# push credential — the later `git push` then authenticates as the App
# (a ruleset bypass actor), not as github-actions[bot].
token: ${{ steps.app-token.outputs.token }}
# Full history + tags so the changelog can diff against the last tag.
fetch-depth: 0
- uses: actions/setup-node@v7
with:
# The gate below must mirror ci.yml EXACTLY, and that includes the
# Node version: V8's coverage attribution shifts between majors, so
# the coverage-baseline ratchet pinned under ci.yml's Node 22 reads
# ~0.1% lower under Node 24 and fails a release from a green tree
# (seen cutting v1.0.0-rc.1: 99.9% vs the 99.98% baseline). Gate on
# 22; the publish step switches to Node 24 just-in-time.
node-version: 22
cache: npm
- run: npm ci
# Release gate: never cut a release from a red tree. Mirrors CI exactly —
# `coverage:ci` runs the suite under c8 and enforces the coverage-baseline
# ratchet, so a release can't ship a commit CI would have blocked on a PR.
- name: Typecheck
run: npm run typecheck
# Fetch the GFM spec.txt (gitignored, not on npm) exactly like ci.yml does
# before ITS coverage run: without it the GFM conformance suite self-skips
# and the lines only it exercises read as uncovered — which failed the
# v1.0.0-rc.1 release gate at 99.9% vs the 99.98% baseline from a
# perfectly green tree. "Mirrors CI exactly" includes the fixtures.
- name: Fetch + verify GFM spec
run: npm run check:gfm-spec
- name: Test + coverage
run: npm run coverage:ci
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v7
with:
name: coverage-report
path: coverage/lcov-report
if-no-files-found: ignore
- name: Build
run: npm run build
- name: Normalizer parity
run: npm run check:normalizer-parity
- name: Resolve version
id: version
# Pass the free-text input through `env:` rather than interpolating
# `${{ inputs.exact_version }}` into the script body — inline expansion
# would let a value like `"; rm -rf … ; "` execute at template-render time.
env:
EXACT_VERSION: ${{ inputs.exact_version }}
BUMP: ${{ inputs.bump }}
run: |
set -euo pipefail
# Prefer the free-text exact version when provided; otherwise use the
# bump keyword from the dropdown. `npm version` accepts both forms and
# prints the resulting `vX.Y.Z`. --no-git-tag-version edits
# package.json + package-lock.json without tagging (we tag ourselves).
TARGET="${EXACT_VERSION:-$BUMP}"
TAG=$(npm version "$TARGET" --no-git-tag-version)
VERSION="${TAG#v}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Releasing $TAG"
- name: Generate changelog
run: npx tsx scripts/gen-changelog.mts "${{ steps.version.outputs.version }}" > RELEASE_NOTES.md
- name: Update CHANGELOG.md
run: |
set -euo pipefail
DATE=$(date -u +%Y-%m-%d)
npx tsx scripts/update-changelog.mts "${{ steps.version.outputs.version }}" "$DATE" RELEASE_NOTES.md
- name: Preview release notes
run: |
{
echo "## Release ${{ steps.version.outputs.tag }}"
echo
cat RELEASE_NOTES.md
} >> "$GITHUB_STEP_SUMMARY"
- name: Commit and tag
if: ${{ !inputs.dry_run }}
run: |
set -euo pipefail
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git add package.json package-lock.json CHANGELOG.md
git commit -m "chore(release): ${{ steps.version.outputs.tag }}"
git tag -a "${{ steps.version.outputs.tag }}" -m "${{ steps.version.outputs.tag }}"
# Switch to Node 24 ONLY for the publish: it bundles npm 11.x
# (>= 11.5.1), which npm Trusted Publishing requires. Doing the switch
# here rather than up top keeps the release gate on ci.yml's exact Node
# version (see the first setup-node step), and avoids upgrading npm in
# place — that in-place self-upgrade could prune npm's own bundled
# `sigstore` dependency and break provenance generation. dist/ is
# already built (plain JS) and no install runs after the switch, so the
# Node-22-built node_modules stays valid.
- uses: actions/setup-node@v7
if: ${{ !inputs.dry_run }}
with:
node-version: 24
registry-url: https://registry.npmjs.org
scope: '@copse'
# Publish BEFORE pushing so a failed publish (e.g. bad token) leaves main
# and the tag list untouched — the commit + tag exist only locally on the
# runner and vanish with it, leaving nothing to clean up before a retry.
- name: Publish to npm
if: ${{ !inputs.dry_run }}
run: |
set -euo pipefail
# Trusted Publishing authenticates via the GitHub OIDC token (see the
# id-token permission above) — no NPM_TOKEN needed. It requires npm
# >= 11.5.1, which the Node 24 runner ships out of the box (see the
# setup-node step above), so no npm self-upgrade is needed here.
# publishConfig pins the public registry + `--access public`, and
# provenance is attached automatically under trusted publishing.
#
# Prereleases (any version with a `-` segment, e.g. 1.0.0-rc.1)
# publish under the `next` dist-tag so a plain
# `npm install @copse/streaming-markdown` keeps resolving to the
# last stable release; stable versions publish as `latest` (npm's
# default). Promote an rc later with
# `npm dist-tag add @copse/streaming-markdown@<ver> latest`.
case "${{ steps.version.outputs.version }}" in
*-*) npm publish --tag next ;;
*) npm publish ;;
esac
- name: Push commit and tag
if: ${{ !inputs.dry_run }}
run: |
set -euo pipefail
git push origin HEAD
git push origin "${{ steps.version.outputs.tag }}"
- name: Create GitHub Release
if: ${{ !inputs.dry_run }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
gh release create "${{ steps.version.outputs.tag }}" \
--title "${{ steps.version.outputs.tag }}" \
--notes-file RELEASE_NOTES.md \
${{ inputs.prerelease && '--prerelease' || '' }}
# Push-model dependency sync: poke copse-dev/agent-pane so its
# sync-streaming-markdown workflow bumps @copse/streaming-markdown and
# opens a PR immediately. Done HERE, as an explicit step, rather than
# relying on the `release: published` event picked up by
# notify-agent-pane.yml: the release above is created with the default
# GITHUB_TOKEN, and GitHub deliberately does not fire workflow-triggering
# events for actions taken with GITHUB_TOKEN (an anti-recursion
# safeguard), so that event never fires for automated releases. Skipped for
# pre-releases — consumers opt into those manually. Failure here does not
# roll back the release; re-notify via notify-agent-pane.yml's
# workflow_dispatch if needed.
#
# A SECOND App token, scoped to agent-pane. The one minted at the top of
# this job is scoped to this repository (create-github-app-token's default
# when `repositories` is empty) and cannot reach agent-pane. Minting a
# separate one, rather than widening the first, keeps the credential
# `actions/checkout` persisted as this job's git push credential narrow: a
# token good for two repos would sit on disk for the whole release.
- uses: actions/create-github-app-token@v3
id: dispatch-token
if: ${{ !inputs.dry_run && !inputs.prerelease }}
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
owner: copse-dev
repositories: agent-pane
# Actions: write is all a workflow_dispatch needs, and unlike the
# Contents: write the old PAT carried, it cannot push code there.
permission-actions: write
- name: Notify agent-pane
if: ${{ !inputs.dry_run && !inputs.prerelease }}
env:
GH_TOKEN: ${{ steps.dispatch-token.outputs.token }}
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
# See notify-agent-pane.yml for why this is a workflow_dispatch rather
# than a repository_dispatch, and why there is no --ref.
gh workflow run sync-streaming-markdown.yml \
--repo copse-dev/agent-pane \
-f "version=$VERSION"
echo "Dispatched sync-streaming-markdown for $VERSION to copse-dev/agent-pane"