Repository navigation
238 lines (219 loc) · 9.64 KB
/
Copy pathrelease.yml
File metadata and controls
238 lines (219 loc) · 9.64 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
name: Release
# **This filename is part of the release credentials. Do not rename it.**
#
# npm binds a trusted publisher to an exact repository and an exact workflow
# filename. A rename revokes nothing and gives no warning. The next run
# passes every gate and then fails at the publish step with npm's 404
# (`404 PUT /@rootnative%2fdevhub`), which names no cause.
#
# Two workflows publish this package, and npmjs.com must have **both** as
# trusted publishers:
#
# .github/workflows/release.yml this file, the normal path
# .github/workflows/release-manual.yml a custom version, or a publish
# of the committed version
#
# This workflow makes the release commit: it bumps the version and builds
# llms.txt. It does not change the CHANGELOG. A person commits the
# CHANGELOG section of the new version to main before the run, with its
# heading and its link. check-release.mjs stops the run when the section is
# missing.
on:
workflow_dispatch:
inputs:
version:
description: "Version bump type. On an alpha, prerelease gives the next 0.0.0-alpha.N."
required: true
default: prerelease
type: choice
options:
- prerelease
- prepatch
- preminor
- premajor
- patch
- minor
- major
preid:
description: "Prerelease identifier (alpha, beta, or rc). Only the pre* types use it."
required: false
default: alpha
type: string
tag:
description: "npm dist-tag."
required: true
default: latest
type: choice
options:
- latest
- alpha
- beta
- rc
dry_run:
description: "Make the release commit and run each gate, but push nothing and publish nothing."
required: false
default: false
type: boolean
# `id-token: write` gives the OIDC token that npm exchanges for the publish.
# `contents: write` pushes the release commit and the tag, and makes the
# GitHub Release.
permissions:
contents: write
id-token: write
# No `cancel-in-progress`. A cancelled publish can leave a version on npm
# with no tag in git, and npm never accepts the same version again.
concurrency:
group: release
cancel-in-progress: false
jobs:
release:
name: Release @rootnative/devhub
runs-on: ubuntu-latest
steps:
# The run pushes the release commit to main, so it must start from
# main. A commit from another branch would put that branch on main.
- name: Check the branch
if: ${{ github.ref != 'refs/heads/main' }}
run: |
echo "::error::Run this workflow from main. It pushes the release commit to main."
exit 1
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# check-release.mjs reads the tags to see if the version exists.
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: Setup Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
cache: "pnpm"
# Necessary also with trusted publishing: npm exchanges the OIDC
# token at this registry.
registry-url: https://registry.npmjs.org
# An exact version, not a range. A new npm can change the release path
# with no change in the repository. Change this pin in its own commit.
- name: Install npm (staged publishing requires >=11.15.0)
run: npm install -g npm@11.19.1
- name: Install dependencies
run: pnpm install --frozen-lockfile
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
# The inputs go through the environment, not into the script text, so
# a value cannot change the command.
- name: Bump the version
id: version
working-directory: packages/devhub
env:
BUMP: ${{ inputs.version }}
PREID: ${{ inputs.preid }}
run: |
case "$BUMP" in
pre*) npm version "$BUMP" --preid "$PREID" --no-git-tag-version ;;
*) npm version "$BUMP" --no-git-tag-version ;;
esac
echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
# llms.txt names the version, and check:llms in preflight compares it
# with the build.
- name: Build llms.txt for the new version
run: pnpm run build && pnpm run build:llms
- name: Check the release
env:
VERSION: ${{ steps.version.outputs.version }}
DIST_TAG: ${{ inputs.tag }}
run: node scripts/check-release.mjs --version "$VERSION" --dist-tag "$DIST_TAG"
# The same script that a laptop runs. No input skips it.
- name: Preflight
run: pnpm run preflight
# A dry run makes the commit too, so its log shows the release commit.
- name: Commit and tag
env:
TAG: devhub@${{ steps.version.outputs.version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add packages/devhub/package.json packages/devhub/llms.txt
git commit -m "release: $TAG"
git tag "$TAG"
if [ -n "$(git status --porcelain)" ]; then
echo "::error::The release changed files that the commit does not hold. A build step writes a tracked file."
git status --short
exit 1
fi
git show --stat HEAD
# Push before the publish. A push can fail, for example when a commit
# reached main during the run, and a publish cannot be undone. In this
# order, a failed push stops the run before npm gets anything.
# `--atomic` pushes the commit and the tag together, or neither.
- name: Push the commit and the tag
if: ${{ !inputs.dry_run }}
env:
TAG: devhub@${{ steps.version.outputs.version }}
run: git push --atomic origin HEAD:main "refs/tags/$TAG"
# OIDC only. Do not add `NODE_AUTH_TOKEN` or an `NPM_TOKEN` secret: a
# token takes precedence over OIDC and hides a registration fault.
# Both publishers on npmjs.com are stage-only, so npm refuses a plain
# `npm publish`. `npm stage publish` uploads the tarball with
# provenance and holds it. Nothing installs until a maintainer approves
# it with 2FA. The dist-tag cannot change at the approval. `prepack`
# builds the package and the page before the upload.
- name: Stage the publish on npm
id: stage
if: ${{ !inputs.dry_run }}
working-directory: packages/devhub
env:
DIST_TAG: ${{ inputs.tag }}
run: npm stage publish --provenance --access public --tag "$DIST_TAG"
- name: Explain a publish failure
if: ${{ failure() && steps.stage.outcome == 'failure' }}
env:
TAG: devhub@${{ steps.version.outputs.version }}
run: |
echo "::error::A 404 on PUT /@rootnative%2fdevhub is almost always an authentication fault, not a missing package."
echo "Check these, in this order:"
echo " 1. Is '.github/workflows/release.yml' a trusted publisher of @rootnative/devhub on npmjs.com? npm binds to the exact filename. The release-manual.yml entry does not cover this file."
echo " 2. Does the job still have 'id-token: write'?"
echo " 3. Did the step run 'npm publish'? Both publishers are stage-only, so the step must run 'npm stage publish'."
echo "Main has the release commit and the tag $TAG. After the fix:"
echo " 1. Delete the tag: git push --delete origin $TAG"
echo " 2. Run release-manual.yml with the version 'skip'. It publishes the committed version, with no new commit."
- name: Create the GitHub Release
if: ${{ !inputs.dry_run }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: devhub@${{ steps.version.outputs.version }}
VERSION: ${{ steps.version.outputs.version }}
run: |
prerelease=""
case "$VERSION" in *-*) prerelease="--prerelease" ;; esac
gh release create "$TAG" --title "$VERSION" --generate-notes $prerelease
- name: Summary
if: ${{ always() && steps.version.outputs.version != '' }}
env:
VERSION: ${{ steps.version.outputs.version }}
DIST_TAG: ${{ inputs.tag }}
DRY: ${{ inputs.dry_run }}
OUTCOME: ${{ job.status }}
run: |
{
if [ "$OUTCOME" != "success" ]; then
echo "### The release of \`$VERSION\` failed"
echo ""
echo "Read the failed step. The step 'Explain a publish failure' gives the recovery for a failed publish."
elif [ "$DRY" = "true" ]; then
echo "### Dry run: nothing was pushed or published"
echo ""
echo "Each gate passed for \`$VERSION\`. The release commit stayed on the runner:"
echo ""
echo '```text'
git show --stat --format='%s' HEAD
echo '```'
else
echo "### Staged \`$VERSION\`: not installable yet"
echo ""
echo "- npm: \`@rootnative/devhub@$VERSION\` is staged for the dist-tag \`$DIST_TAG\`. Approve it with 2FA on npmjs.com (Staged Packages), or with \`npm stage approve\`."
echo "- Commit on main: \`$(git rev-parse --short HEAD)\` release: devhub@$VERSION"
echo "- Tag: \`devhub@$VERSION\`"
fi
} >> "$GITHUB_STEP_SUMMARY"