-
Notifications
You must be signed in to change notification settings - Fork 5
401 lines (362 loc) · 15 KB
/
Copy pathrelease.yml
File metadata and controls
401 lines (362 loc) · 15 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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
name: Release
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
# Publishing to npm is irreversible — never let a commit that fails CI or
# Nix Build ship. Those workflows run in parallel on the same push, so this
# job blocks the release until both conclude successfully for this SHA.
wait-for-checks:
name: Wait for CI and Nix Build
runs-on: ubuntu-latest
if: github.repository_owner == 'Pythoughts-labs'
timeout-minutes: 45
permissions:
actions: read
contents: read
steps:
- name: Wait for required workflows on this commit
env:
GH_TOKEN: ${{ github.token }}
SHA: ${{ github.sha }}
REPO: ${{ github.repository }}
run: |
for workflow in "CI" "Nix Build"; do
echo "Waiting for workflow: $workflow"
while true; do
# Transient API failures must not kill the gate (step shell is -e).
pair=$(gh api "repos/$REPO/actions/runs?head_sha=$SHA&per_page=50" \
--jq "[.workflow_runs[] | select(.name==\"$workflow\")][0] | \"\(.status)/\(.conclusion)\"" \
2>/dev/null) || pair="api-error/null"
case "$pair" in
completed/success)
echo "$workflow: success"; break ;;
completed/*)
echo "::error::$workflow concluded '${pair#completed/}' for $SHA — refusing to release."
exit 1 ;;
*)
echo "$workflow: $pair — waiting..."; sleep 30 ;;
esac
done
done
release:
name: Release
needs: wait-for-checks
runs-on: ubuntu-latest
timeout-minutes: 30
if: github.repository_owner == 'Pythoughts-labs'
outputs:
packages_published: ${{ steps.changesets.outputs.published }}
pythinker_native_release: ${{ steps.pythinker-release.outputs.should_publish }}
pythinker_release_tag: ${{ steps.pythinker-release.outputs.tag }}
permissions:
contents: write
pull-requests: write
id-token: write # Required for NPM Trusted Publishing (OIDC)
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: "pnpm"
registry-url: "https://registry.npmjs.org"
- name: Upgrade npm for Trusted Publishing
run: npm install -g npm@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Generate Pythinker Code built-in catalog
shell: bash
run: |
CATALOG_FILE="$RUNNER_TEMP/pythinker-code-built-in-catalog.json"
node apps/pythinker-code/scripts/update-catalog.mjs --out "$CATALOG_FILE"
echo "PYTHINKER_CODE_BUILT_IN_CATALOG_FILE=$CATALOG_FILE" >> "$GITHUB_ENV"
- name: Build packages
run: pnpm build
- name: Mint release-bot token
id: release-bot
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
with:
publish: node scripts/release/changeset-publish-idempotent.mjs
version: pnpm run version:release
commit: "ci: release packages"
title: "ci: release packages"
env:
# App token (not GITHUB_TOKEN) so the version PR triggers pull_request
# workflows and required status checks / CodeRabbit run on it.
GITHUB_TOKEN: ${{ steps.release-bot.outputs.token }}
# No NPM_TOKEN on purpose: changesets prefers it over OIDC when set, so
# defining it would silently downgrade publishing to a long-lived token.
- name: Request CodeRabbit review on version PR
if: steps.changesets.outputs.published != 'true'
env:
GH_TOKEN: ${{ steps.release-bot.outputs.token }}
run: |
pr=$(gh pr list --head changeset-release/main --state open --json number --jq '.[0].number' || true)
if [ -n "$pr" ] && [ "$pr" != "null" ]; then
gh pr comment "$pr" --body '@coderabbitai review'
fi
- name: Resolve Pythinker Code native release
if: steps.changesets.outputs.published == 'true'
id: pythinker-release
run: node apps/pythinker-code/scripts/native/resolve-release.mjs
env:
CHANGESETS_PUBLISHED_PACKAGES: ${{ steps.changesets.outputs.publishedPackages }}
# The VS Code extension is a private workspace package: changesets bumps its
# version but never publishes it to npm, so it ships from here instead. Both
# publish scripts skip packages that already exist in the registry, so this
# job is a no-op on releases that did not touch the extension.
publish-vscode-extension:
timeout-minutes: 45
name: Publish VS Code extension
needs: release
if: needs.release.outputs.packages_published == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Generate Pythinker Code built-in catalog
shell: bash
run: |
CATALOG_FILE="$RUNNER_TEMP/pythinker-code-built-in-catalog.json"
node apps/pythinker-code/scripts/update-catalog.mjs --out "$CATALOG_FILE"
echo "PYTHINKER_CODE_BUILT_IN_CATALOG_FILE=$CATALOG_FILE" >> "$GITHUB_ENV"
- name: Build workspace packages
run: pnpm build
# Packages all six platform targets and runs the VSIX audit on each one.
# A failure here must stop the job before anything reaches a registry.
- name: Package and verify VSIX targets
run: pnpm --filter pythinker-code run package:platform
- name: Publish to the Visual Studio Marketplace
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
run: |
if [ -z "$VSCE_PAT" ]; then
echo "::warning::VSCE_PAT secret not set — skipping Marketplace publish."
exit 0
fi
pnpm --filter pythinker-code run publish:vsix
# Open VSX serves Cursor / VSCodium / Windsurf. A failure here must not
# undo an already-successful Marketplace publish, so it only warns.
- name: Publish to Open VSX
continue-on-error: true
env:
OVSX_PAT: ${{ secrets.OVSX_PAT }}
run: |
if [ -z "$OVSX_PAT" ]; then
echo "::warning::OVSX_PAT secret not set — skipping Open VSX publish."
exit 0
fi
pnpm --filter pythinker-code run publish:ovsx
- name: Upload VSIX artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: pythinker-code-vsix
path: apps/vscode/artifacts/vsix/*.vsix
retention-days: 7
if-no-files-found: error
# code.pythinker.com redeploys via Dokploy autodeploy on push to main (app
# Pythinker/code builds apps/site/Dockerfile from the repo). That autodeploy
# fires on the `ci: release packages` push — which STARTS the release — while
# apps/site/scripts/build-cdn.mjs reads the version from npm's dist-tag, which
# only moves when the publish FINISHES. The first build therefore bakes in the
# previous version and nothing rebuilds it, so the release stays invisible to
# every installed client. This job fires a second deploy after the publish.
#
# It must run after publish-native-assets: latest.json only gets its
# per-platform `platforms` block once the native zips exist on the release.
# That job is itself conditional and SKIPS on an npm-only release, and a job
# whose `needs` includes a skipped job is skipped too — hence `always()`, and
# hence the explicit upstream result assertions it forces us to spell out.
redeploy-cdn:
timeout-minutes: 10
name: Redeploy CDN
needs:
- release
- publish-native-assets
if: >-
always()
&& needs.release.result == 'success'
&& needs.publish-native-assets.result != 'failure'
&& needs.publish-native-assets.result != 'cancelled'
&& (needs.release.outputs.packages_published == 'true'
|| startsWith(github.event.head_commit.message, 'ci: release packages'))
runs-on: ubuntu-latest
steps:
- name: Trigger Dokploy rebuild
env:
WEBHOOK: ${{ secrets.DOKPLOY_CDN_DEPLOY_WEBHOOK }}
run: |
if [ -z "$WEBHOOK" ]; then
echo "::warning::DOKPLOY_CDN_DEPLOY_WEBHOOK not set — skipping CDN redeploy."
exit 0
fi
# The URL is itself the deploy credential, so never send it over a
# scheme that puts it on the wire in cleartext. Warn rather than fail:
# verify-cdn-release runs only if this job succeeds, and failing here
# would drop the consistency gate instead of tripping it.
case "$WEBHOOK" in
https://*) ;;
*)
echo "::warning::DOKPLOY_CDN_DEPLOY_WEBHOOK is not an https:// URL — refusing to send the deploy credential in cleartext."
exit 0
;;
esac
# The webhook reads the branch from the body, but only when the
# request also carries `X-GitHub-Event`: Dokploy's extractBranchName
# returns null without that header, so the request answers
# 301 {"message":"Branch Not Match"} and deploys nothing.
#
# 301 is not an error status, so `--fail` does not see it and curl
# exits 0. Capture the status code and treat anything but 2xx as a
# failed deploy.
#
# A transient failure must never fail the workflow. npm has already
# published by now and that is irreversible, so dying here buys
# nothing — an earlier version of this job was deleted because a
# curl exit-28 timeout failed the 0.5.0 release. verify-cdn-release
# polls the manifest and is the gate that fails loudly.
status=$(curl -sS -o /dev/stderr -w '%{http_code}' -X POST "$WEBHOOK" \
-H 'Content-Type: application/json' \
-H 'X-GitHub-Event: push' \
-d '{"ref":"refs/heads/main"}' \
--retry 3 --retry-all-errors --retry-delay 10 --max-time 60) || status=000
case "$status" in
2*) echo "CDN redeploy triggered (HTTP $status)." ;;
*) echo "::warning::CDN redeploy webhook returned HTTP $status — verify-cdn-release will catch a stale CDN." ;;
esac
# Verifies that the published release is internally consistent and that the
# CDN caught up with npm. It polls, so it must run after redeploy-cdn.
#
# It also runs on a `ci: release packages` merge that published nothing: that
# commit bumps the version on main, so gating the check on a successful
# publish hid the one case where the version and the published artifacts
# diverge — and every client polled the CDN for a release that never existed.
verify-cdn-release:
timeout-minutes: 15
name: Verify release consistency
needs:
- release
- redeploy-cdn
if: >-
needs.release.outputs.packages_published == 'true'
|| startsWith(github.event.head_commit.message, 'ci: release packages')
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Verify release consistency
run: node scripts/release/verify-release-consistency.mjs
update-brew-tap:
timeout-minutes: 15
name: Update Homebrew tap
needs: release
if: needs.release.outputs.packages_published == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Bump formula
env:
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
run: |
if [ -z "$TAP_GITHUB_TOKEN" ]; then
echo "TAP_GITHUB_TOKEN secret not set — skipping tap update" >&2
exit 0
fi
node scripts/release/update-brew-formula.mjs
deploy-docs:
name: Deploy docs
needs: release
if: needs.release.outputs.packages_published == 'true'
uses: ./.github/workflows/docs-deploy.yml
permissions:
contents: read
pages: write
id-token: write
native-artifacts:
name: Native release artifact
needs: release
if: needs.release.outputs.pythinker_native_release == 'true'
uses: ./.github/workflows/_native-build.yml
with:
upload-artifact-prefix: pythinker-code-native
retention-days: 7
sign-macos: true
secrets:
APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_KEY_P8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
publish-native-assets:
timeout-minutes: 15
name: Publish native release assets
needs:
- release
- native-artifacts
if: needs.release.outputs.pythinker_native_release == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Download native artifacts
uses: actions/download-artifact@v8
with:
pattern: pythinker-code-native-*
path: dist-native-release
merge-multiple: true
- name: Assert all native targets are present
run: |
missing=0
for target in darwin-arm64 darwin-x64 linux-arm64 linux-x64 win32-arm64 win32-x64; do
if ! ls dist-native-release/pythinker-code-"$target".zip >/dev/null 2>&1; then
echo "::error::Missing native bundle for $target — refusing to publish a partial release."
missing=1
fi
done
exit $missing
- name: Produce manifest.json
env:
RELEASE_TAG: ${{ needs.release.outputs.pythinker_release_tag }}
run: node apps/pythinker-code/scripts/native/produce-manifest.mjs dist-native-release "$RELEASE_TAG"
- name: Upload assets to GitHub Release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.release.outputs.pythinker_release_tag }}
run: gh release upload "$RELEASE_TAG" dist-native-release/* --clobber