main
yml 219 lines 10.3 KB
Raw
1 name: (Runtime) Release From CI
2
3 on:
4 workflow_dispatch:
5 inputs:
6 type:
7 required: true
8 description: Type of release to publish
9 type: choice
10 # The `─── ... ───` entries are visual separators in the dropdown.
11 # They're rejected by the `resolve` job below, so picking one fails
12 # the workflow up front instead of silently doing the wrong release.
13 # GitHub Actions requires choice values to be unique, so the strings
14 # differ by the number of dashes.
15 options:
16 # publishes canary + experimental.
17 - nightly
18 - "────────────────"
19 - "─────────────────"
20 # semver stable published with @latest.
21 - stable-latest
22 - "──────────────────"
23 - "───────────────────"
24 # semver stable published with @backport (used for patches to
25 # older release lines that shouldn't move @latest).
26 - stable-backport
27 - "────────────────────"
28 - "─────────────────────"
29 # only experimental is published.
30 - experimental_only
31 only_packages:
32 description: Packages to publish (space separated allow-list; empty means all)
33 type: string
34 dry:
35 description: Dry run
36 type: boolean
37 default: false
38 force_notify:
39 description: Force a Discord notification?
40 type: boolean
41 default: false
42 schedule:
43 # At 10 minutes past 16:00 on Mon, Tue, Wed, Thu, and Fri.
44 # Scheduled runs always publish a nightly (see `resolve` job).
45 - cron: 10 16 * * 1,2,3,4,5
46
47 permissions: {}
48
49 env:
50 TZ: /usr/share/zoneinfo/America/Los_Angeles
51 # https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
52 SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
53
54 jobs:
55 resolve:
56 name: Resolve release inputs
57 runs-on: ubuntu-latest
58 outputs:
59 release_type: ${{ steps.resolve.outputs.release_type }}
60 steps:
61 - name: Resolve release inputs
62 id: resolve
63 run: |
64 # Scheduled runs always publish a nightly. Manual dispatches always
65 # supply `inputs.type`. Anything else is unsupported and fails fast.
66 if [ "${{ github.event_name }}" = "schedule" ]; then
67 release_type=nightly
68 elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
69 release_type="${{ inputs.type }}"
70 else
71 echo "Unsupported event: ${{ github.event_name }}" >&2
72 exit 1
73 fi
74 # Reject the dropdown's separator entries (and anything else that
75 # isn't one of the four real release types). Without this, picking
76 # a separator would fall through to all of publish.js's `if:` gates
77 # being false and the job would succeed without doing anything.
78 case "$release_type" in
79 nightly|stable-latest|stable-backport|experimental_only) ;;
80 *)
81 echo "Invalid release type: '$release_type' — pick one of nightly, stable-latest, stable-backport, experimental_only." >&2
82 exit 1
83 ;;
84 esac
85 echo "release_type=$release_type" >> "$GITHUB_OUTPUT"
86
87 notify_starting:
88 name: Notify Discord (release starting)
89 # Manual dispatches always notify before the release starts so the team
90 # has a heads-up that a release is incoming. Scheduled (nightly) runs
91 # don't notify up front; we only notify on failure (see `notify` job).
92 if: ${{ github.event_name == 'workflow_dispatch' && vars.DISABLE_DISCORD_NOTIFICATIONS != 'true' }}
93 runs-on: ubuntu-latest
94 steps:
95 - uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
96 with:
97 webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
98 embed-author-name: ${{ github.event.sender.login }}
99 embed-author-url: ${{ github.event.sender.html_url }}
100 embed-author-icon-url: ${{ github.event.sender.avatar_url }}
101 embed-title: "⚠️ Publishing ${{ inputs.type }} release from source"
102 embed-description: |
103 ```json
104 ${{ toJson(inputs) }}
105 ```
106 embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
107
108 publish:
109 name: Publish release
110 needs: resolve
111 if: ${{ !cancelled() && needs.resolve.result == 'success' }}
112 runs-on: ubuntu-latest
113 # Protected environment — requires reviewer approval before the publish
114 # job starts running.
115 environment: npm
116 permissions:
117 id-token: write
118 contents: read
119 # `actions: read` lets prepare-release-from-ci.js fetch the
120 # runtime_build_and_test workflow's artifacts for github.sha.
121 actions: read
122 env:
123 # Required by scripts/release/shared-commands/download-build-artifacts.js.
124 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
125 steps:
126 # Always check out the commit the workflow file itself was loaded from.
127 # Crucially, no user-supplied ref is accepted, since this job runs in a
128 # protected environment.
129 - uses: actions/checkout@v4
130 with:
131 ref: ${{ github.sha }}
132 - uses: actions/setup-node@v4
133 with:
134 # Using modern Node.js that ships with NPM supporting trusted publishing
135 node-version: "24"
136 # scripts/release is the only thing this job actually needs to run —
137 # publish.js and prepare-release-from-ci.js both live there and use its
138 # own node_modules. Cache is owned by this workflow (this is the only
139 # consumer), so on a miss we install and save.
140 - name: Restore cached scripts/release node_modules
141 uses: actions/cache@v4
142 id: release_node_modules
143 with:
144 path: scripts/release/node_modules
145 key: release-scripts-node_modules-v1-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('scripts/release/yarn.lock') }}
146 - run: yarn --cwd scripts/release install --frozen-lockfile
147 if: steps.release_node_modules.outputs.cache-hit != 'true'
148
149 # ----- stable (semver) — either @latest or @backport -----
150 - name: Stage semver stable artifacts
151 if: ${{ startsWith(needs.resolve.outputs.release_type, 'stable-') }}
152 run: scripts/release/prepare-release-from-ci.js --skipTests -r latest --commit=${{ github.sha }}
153 - name: Publish semver stable to @latest
154 if: ${{ needs.resolve.outputs.release_type == 'stable-latest' }}
155 run: |
156 scripts/release/publish.js \
157 --tag=latest \
158 ${{ inputs.only_packages && format('--onlyPackages={0}', inputs.only_packages) || '' }} \
159 ${{ inputs.dry && '--dry' || '' }}
160 # Backport releases stay on the `@backport` dist-tag — we don't move
161 # @latest, and (under OIDC) we can't add/remove dist-tags after publish
162 # anyway. The tag goes on at publish time and is left in place.
163 - name: Publish semver stable to @backport
164 if: ${{ needs.resolve.outputs.release_type == 'stable-backport' }}
165 run: |
166 scripts/release/publish.js \
167 --tag=backport \
168 ${{ inputs.only_packages && format('--onlyPackages={0}', inputs.only_packages) || '' }} \
169 ${{ inputs.dry && '--dry' || '' }}
170
171 # ----- nightly: canary first, then experimental -----
172 # NOTE: Intentionally running sequentially because npm will sometimes
173 # fail if you try to concurrently publish two different versions of the
174 # same package, even if they use different dist tags.
175 - name: Stage canary artifacts
176 if: ${{ needs.resolve.outputs.release_type == 'nightly' }}
177 run: scripts/release/prepare-release-from-ci.js --skipTests -r stable --commit=${{ github.sha }}
178 # Single tag only — OIDC publish tokens can't add additional dist-tags
179 # after publish, so the historical `canary,next` aliasing is gone.
180 - name: Publish canary to @canary
181 if: ${{ needs.resolve.outputs.release_type == 'nightly' }}
182 run: |
183 scripts/release/publish.js \
184 --tag=canary \
185 ${{ inputs.only_packages && format('--onlyPackages={0}', inputs.only_packages) || '' }} \
186 ${{ inputs.dry && '--dry' || '' }}
187
188 # ----- experimental (nightly + experimental_only) -----
189 - name: Stage experimental artifacts
190 if: ${{ needs.resolve.outputs.release_type == 'nightly' || needs.resolve.outputs.release_type == 'experimental_only' }}
191 run: scripts/release/prepare-release-from-ci.js --skipTests -r experimental --commit=${{ github.sha }}
192 - name: Publish experimental to @experimental
193 if: ${{ needs.resolve.outputs.release_type == 'nightly' || needs.resolve.outputs.release_type == 'experimental_only' }}
194 run: |
195 scripts/release/publish.js \
196 --tag=experimental \
197 ${{ inputs.only_packages && format('--onlyPackages={0}', inputs.only_packages) || '' }} \
198 ${{ inputs.dry && '--dry' || '' }}
199
200 notify:
201 name: Notify Discord on failure
202 needs: [resolve, publish]
203 # Runs for every workflow run (manual + scheduled) and only fires when
204 # something didn't complete successfully — i.e. an actual failure or a
205 # cancellation. Successful runs stay silent.
206 if: ${{ always() && (needs.resolve.result == 'failure' || needs.resolve.result == 'cancelled' || needs.publish.result == 'failure' || needs.publish.result == 'cancelled') && vars.DISABLE_DISCORD_NOTIFICATIONS != 'true' }}
207 runs-on: ubuntu-latest
208 steps:
209 - name: Discord Webhook Action
210 uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
211 with:
212 webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
213 embed-author-name: "GitHub Actions"
214 embed-title: " [Runtime] Release from source failed (${{ needs.resolve.outputs.release_type || inputs.type || 'nightly' }})"
215 embed-description: |
216 resolve: `${{ needs.resolve.result }}`
217 publish: `${{ needs.publish.result }}`
218 event: `${{ github.event_name }}`
219 embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}