Skip to content

Commit 5ab1d4c

Browse files
committed
Unified release process
1 parent d5736f0 commit 5ab1d4c

22 files changed

Lines changed: 391 additions & 1485 deletions
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
name: (Runtime) Release From Source
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
commit_sha:
7+
required: true
8+
type:
9+
required: true
10+
description: Type of release to publish
11+
type: choice
12+
options:
13+
- nightly
14+
- stable-latest
15+
- stable-untagged
16+
- experimental_only
17+
only_packages:
18+
description: Packages to publish (space separated)
19+
type: string
20+
skip_packages:
21+
description: Packages to NOT publish (space separated)
22+
type: string
23+
dry:
24+
required: true
25+
description: Dry run instead of publish?
26+
type: boolean
27+
default: true
28+
force_notify:
29+
description: Force a Discord notification?
30+
type: boolean
31+
default: false
32+
schedule:
33+
# At 10 minutes past 16:00 on Mon, Tue, Wed, Thu, and Fri.
34+
# Scheduled runs always publish a nightly (see `Resolve release inputs`).
35+
- cron: 10 16 * * 1,2,3,4,5
36+
37+
permissions: {}
38+
39+
env:
40+
TZ: /usr/share/zoneinfo/America/Los_Angeles
41+
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
42+
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
43+
44+
jobs:
45+
prepare:
46+
name: Prepare release
47+
runs-on: ubuntu-latest
48+
outputs:
49+
release_type: ${{ steps.resolve.outputs.release_type }}
50+
commit_sha: ${{ steps.resolve.outputs.commit_sha }}
51+
steps:
52+
# Manual dispatches always notify before the release starts so the team
53+
# has a heads-up that a release is incoming. Scheduled (nightly) runs
54+
# don't notify up front; we only notify on failure (see `notify` job).
55+
- name: Notify Discord (release starting)
56+
if: ${{ github.event_name == 'workflow_dispatch' }}
57+
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
58+
with:
59+
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
60+
embed-author-name: ${{ github.event.sender.login }}
61+
embed-author-url: ${{ github.event.sender.html_url }}
62+
embed-author-icon-url: ${{ github.event.sender.avatar_url }}
63+
embed-title: "⚠️ Publishing ${{ inputs.type }} release from source${{ (inputs.dry && ' (dry run)') || '' }}"
64+
embed-description: |
65+
```json
66+
${{ toJson(inputs) }}
67+
```
68+
embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
69+
70+
- name: Resolve release inputs
71+
id: resolve
72+
run: |
73+
# Scheduled runs always publish a nightly. Manual dispatches always
74+
# supply `inputs.type`. Anything else is unsupported and fails fast.
75+
if [ "${{ github.event_name }}" = "schedule" ]; then
76+
release_type=nightly
77+
commit_sha="${{ github.sha }}"
78+
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
79+
release_type="${{ inputs.type }}"
80+
commit_sha="${{ inputs.commit_sha }}"
81+
else
82+
echo "Unsupported event: ${{ github.event_name }}" >&2
83+
exit 1
84+
fi
85+
echo "release_type=$release_type" >> "$GITHUB_OUTPUT"
86+
echo "commit_sha=$commit_sha" >> "$GITHUB_OUTPUT"
87+
88+
- uses: actions/checkout@v4
89+
with:
90+
ref: ${{ steps.resolve.outputs.commit_sha }}
91+
- uses: actions/setup-node@v4
92+
with:
93+
node-version-file: '.nvmrc'
94+
cache: yarn
95+
cache-dependency-path: yarn.lock
96+
- name: Restore cached node_modules
97+
uses: actions/cache/restore@v4
98+
id: node_modules
99+
with:
100+
path: |
101+
**/node_modules
102+
key: runtime-release-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
103+
fail-on-cache-miss: true
104+
- name: Ensure clean build directory
105+
run: rm -rf build
106+
107+
# Build only the channels we'll actually publish. `yarn build -r stable`
108+
# produces build/oss-stable (canary-tagged) and build/oss-stable-semver
109+
# (semver/@latest-tagged). `yarn build -r experimental` produces
110+
# build/oss-experimental.
111+
#
112+
# - stable-latest → semver stable published with @latest. Build stable channel.
113+
# - stable-untagged → semver stable published without a dist-tag. Build stable channel.
114+
# - experimental_only → only experimental is published. Build experimental.
115+
# - nightly → publishes canary + experimental. Build both channels.
116+
- name: Build stable channel
117+
if: ${{ steps.resolve.outputs.release_type == 'stable-latest' || steps.resolve.outputs.release_type == 'stable-untagged' || steps.resolve.outputs.release_type == 'nightly' }}
118+
run: yarn build -r stable
119+
- name: Build experimental channel
120+
if: ${{ steps.resolve.outputs.release_type == 'experimental_only' || steps.resolve.outputs.release_type == 'nightly' }}
121+
run: yarn build -r experimental
122+
123+
- name: Inspect prepared build folders
124+
run: ls -1 ./build
125+
126+
# Upload only the channel folders the publish job needs. Each is uploaded
127+
# under its own artifact so the publish job can pick the right one without
128+
# an extra rename step.
129+
- name: Archive semver stable artifacts
130+
if: ${{ steps.resolve.outputs.release_type == 'stable-latest' || steps.resolve.outputs.release_type == 'stable-untagged' }}
131+
uses: actions/upload-artifact@v4
132+
with:
133+
name: release-build-stable-semver
134+
path: ./build/oss-stable-semver
135+
retention-days: 7
136+
if-no-files-found: error
137+
- name: Archive canary artifacts
138+
if: ${{ steps.resolve.outputs.release_type == 'nightly' }}
139+
uses: actions/upload-artifact@v4
140+
with:
141+
name: release-build-canary
142+
path: ./build/oss-stable
143+
retention-days: 7
144+
if-no-files-found: error
145+
- name: Archive experimental artifacts
146+
if: ${{ steps.resolve.outputs.release_type == 'nightly' || steps.resolve.outputs.release_type == 'experimental_only' }}
147+
uses: actions/upload-artifact@v4
148+
with:
149+
name: release-build-experimental
150+
path: ./build/oss-experimental
151+
retention-days: 7
152+
if-no-files-found: error
153+
154+
publish:
155+
name: Publish release
156+
needs: prepare
157+
runs-on: ubuntu-latest
158+
# Protected environment — requires reviewer approval before the publish
159+
# job starts running, and gates access to NPM_TOKEN. Both stable variants
160+
# (stable-latest, stable-untagged) go through the stricter `npm-stable`
161+
# environment (different reviewers / protection rules); nightly and
162+
# experimental_only share `npm-nightly`.
163+
environment: ${{ startsWith(needs.prepare.outputs.release_type, 'stable-') && 'npm-stable' || 'npm-nightly' }}
164+
env:
165+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
166+
steps:
167+
- uses: actions/checkout@v4
168+
with:
169+
ref: ${{ needs.prepare.outputs.commit_sha }}
170+
- uses: actions/setup-node@v4
171+
with:
172+
node-version-file: '.nvmrc'
173+
cache: yarn
174+
cache-dependency-path: yarn.lock
175+
- name: Restore cached node_modules
176+
uses: actions/cache/restore@v4
177+
id: node_modules
178+
with:
179+
path: |
180+
**/node_modules
181+
key: runtime-release-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
182+
fail-on-cache-miss: true
183+
- run: cp ./scripts/release/ci-npmrc ~/.npmrc
184+
- name: Ensure clean build directory
185+
run: rm -rf ./build && mkdir ./build
186+
187+
# publish.js always reads from build/node_modules. For each channel we
188+
# download the matching artifact into build/node_modules, publish, then
189+
# clean up before the next channel.
190+
191+
# ----- stable (semver) — either @latest or untagged -----
192+
- name: Download semver stable artifacts
193+
if: ${{ startsWith(needs.prepare.outputs.release_type, 'stable-') }}
194+
uses: actions/download-artifact@v4
195+
with:
196+
name: release-build-stable-semver
197+
path: ./build/node_modules
198+
- name: Publish semver stable to @latest
199+
if: ${{ needs.prepare.outputs.release_type == 'stable-latest' }}
200+
run: |
201+
ls -1 build/node_modules
202+
scripts/release/publish.js \
203+
--ci \
204+
--tags=latest \
205+
${{ inputs.only_packages && format('--onlyPackages={0}', inputs.only_packages) || '' }} \
206+
${{ inputs.skip_packages && format('--skipPackages={0}', inputs.skip_packages) || '' }} \
207+
${{ inputs.dry && '--dry' || '' }}
208+
# `--tags=untagged` makes publish.js attach the temporary `untagged` tag
209+
# at publish time and then `npm dist-tag rm <pkg> untagged` to leave the
210+
# version published but pointed-to by no dist-tag.
211+
- name: Publish semver stable without a dist-tag
212+
if: ${{ needs.prepare.outputs.release_type == 'stable-untagged' }}
213+
run: |
214+
ls -1 build/node_modules
215+
scripts/release/publish.js \
216+
--ci \
217+
--tags=untagged \
218+
${{ inputs.only_packages && format('--onlyPackages={0}', inputs.only_packages) || '' }} \
219+
${{ inputs.skip_packages && format('--skipPackages={0}', inputs.skip_packages) || '' }} \
220+
${{ inputs.dry && '--dry' || '' }}
221+
222+
# ----- nightly: canary first, then experimental -----
223+
# NOTE: Intentionally running sequentially because npm will sometimes
224+
# fail if you try to concurrently publish two different versions of the
225+
# same package, even if they use different dist tags.
226+
- name: Download canary artifacts
227+
if: ${{ needs.prepare.outputs.release_type == 'nightly' }}
228+
uses: actions/download-artifact@v4
229+
with:
230+
name: release-build-canary
231+
path: ./build/node_modules
232+
- name: Publish canary to @canary,@next
233+
if: ${{ needs.prepare.outputs.release_type == 'nightly' }}
234+
run: |
235+
ls -1 build/node_modules
236+
scripts/release/publish.js \
237+
--ci \
238+
--tags=canary,next \
239+
${{ inputs.only_packages && format('--onlyPackages={0}', inputs.only_packages) || '' }} \
240+
${{ inputs.skip_packages && format('--skipPackages={0}', inputs.skip_packages) || '' }} \
241+
${{ inputs.dry && '--dry' || '' }}
242+
- name: Swap canary out of build/node_modules
243+
if: ${{ needs.prepare.outputs.release_type == 'nightly' }}
244+
run: rm -rf ./build/node_modules
245+
246+
# ----- experimental (nightly + experimental_only) -----
247+
- name: Download experimental artifacts
248+
if: ${{ needs.prepare.outputs.release_type == 'nightly' || needs.prepare.outputs.release_type == 'experimental_only' }}
249+
uses: actions/download-artifact@v4
250+
with:
251+
name: release-build-experimental
252+
path: ./build/node_modules
253+
- name: Publish experimental to @experimental
254+
if: ${{ needs.prepare.outputs.release_type == 'nightly' || needs.prepare.outputs.release_type == 'experimental_only' }}
255+
run: |
256+
ls -1 build/node_modules
257+
scripts/release/publish.js \
258+
--ci \
259+
--tags=experimental \
260+
${{ inputs.only_packages && format('--onlyPackages={0}', inputs.only_packages) || '' }} \
261+
${{ inputs.skip_packages && format('--skipPackages={0}', inputs.skip_packages) || '' }} \
262+
${{ inputs.dry && '--dry' || '' }}
263+
264+
notify:
265+
name: Notify Discord on failure
266+
needs: [prepare, publish]
267+
# Runs for every workflow run (manual + scheduled) and only fires when
268+
# something didn't complete successfully — i.e. an actual failure or a
269+
# cancellation. Successful runs stay silent.
270+
if: ${{ always() && (needs.prepare.result == 'failure' || needs.prepare.result == 'cancelled' || needs.publish.result == 'failure' || needs.publish.result == 'cancelled') }}
271+
runs-on: ubuntu-latest
272+
steps:
273+
- name: Discord Webhook Action
274+
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
275+
with:
276+
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
277+
embed-author-name: 'GitHub Actions'
278+
embed-title: "❌ [Runtime] Release from source failed (${{ needs.prepare.outputs.release_type || inputs.type || 'nightly' }})"
279+
embed-description: |
280+
prepare: `${{ needs.prepare.result }}`
281+
publish: `${{ needs.publish.result }}`
282+
event: `${{ github.event_name }}`
283+
embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}

.github/workflows/runtime_prereleases.yml

Lines changed: 0 additions & 110 deletions
This file was deleted.

0 commit comments

Comments
 (0)