-
-
Notifications
You must be signed in to change notification settings - Fork 12
811 lines (784 loc) · 35.3 KB
/
Copy pathci.yml
File metadata and controls
811 lines (784 loc) · 35.3 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
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
name: Build
on:
push:
branches: [main, release/**]
pull_request:
workflow_call:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
# packages:write is needed for publish-nightly to push to GHCR
# issues:write is needed for generate-patches to file issues on failure
permissions:
contents: read
issues: write
packages: write
env:
# Commit timestamp used for deterministic nightly version strings.
# Defined at workflow level so build-binary and publish-nightly always agree.
COMMIT_TIMESTAMP: ${{ github.event.head_commit.timestamp }}
# SENTRY_CLIENT_ID is baked into the binary at build time. Fork PRs can't
# read repo vars (getsentry org policy); fall back to a dummy. The resulting
# binary is only smoke-tested (--help) and never shipped, so any non-empty
# value works; tests tolerate the dummy via test/preload.ts.
SENTRY_CLIENT_ID: ${{ vars.SENTRY_CLIENT_ID || 'ci-fork-pr-dummy' }}
# Disable io_uring in libuv — GitHub Actions runners may use kernels that
# don't fully support it, causing SIGABRT in Node.js processes.
# See: https://github.com/actions/runner-images/issues/13602
UV_USE_IO_URING: "0"
# CI Node.js versions, pinned to exact patches and used as the single source
# of truth for every actions/setup-node step (referenced as
# ${{ env.NODE_VERSION_xx }}). Node 24.17.0 / 22.23.0 (CVE-2026-48931's
# http.Agent fix) added a `data` listener on idle agent sockets that makes
# keep-alive fetch reuse throw false ERR_STREAM_PREMATURE_CLOSE
# ("Premature close" — hit by the skill-eval planner talking to external
# APIs). Fixed in 24.18.0 / 22.23.1 (nodejs/node#64004). A floating major
# ("24"/"22") silently reuses the runner's pre-cached buggy patch, so pin the
# exact patched versions here and bump them in one place.
#
# Node 20 is the WASM-SQLite floor: the npm package supports Node >= 18, and
# on < 22.15 (which lacks node:sqlite) it falls back to the bundled
# node-sqlite3-wasm driver. The npm-package job runs a Node 20 matrix entry
# so that fallback path is exercised in CI rather than only in production.
NODE_VERSION_20: "20.20.2"
NODE_VERSION_22: "22.23.1"
NODE_VERSION_24: "24.18.0"
jobs:
changes:
name: Detect Changes
runs-on: ubuntu-latest
permissions:
pull-requests: read
outputs:
skill: ${{ steps.filter.outputs.skill == 'true' || startsWith(github.ref, 'refs/heads/release/') }}
code: ${{ steps.filter.outputs.code == 'true' || startsWith(github.ref, 'refs/heads/release/') }}
codemod: ${{ steps.filter.outputs.codemod == 'true' || startsWith(github.ref, 'refs/heads/release/') }}
build-targets: ${{ steps.targets.outputs.matrix }}
nightly-version: ${{ steps.nightly.outputs.version }}
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v4
id: filter
with:
filters: |
skill:
- 'packages/cli/src/**'
- 'apps/cli-docs/**'
- 'packages/cli/package.json'
- 'packages/cli/README.md'
- 'packages/cli/DEVELOPMENT.md'
- 'packages/cli/script/generate-skill.ts'
- 'packages/cli/script/generate-command-docs.ts'
- 'packages/cli/script/generate-docs-sections.ts'
- 'packages/cli/script/eval-skill.ts'
- 'packages/cli/test/skill-eval/**'
code:
- 'packages/cli/src/**'
- 'packages/cli/test/**'
- 'packages/cli/script/**'
- 'packages/cli/patches/**'
- 'apps/cli-docs/**'
- 'packages/cli/plugins/**'
- 'packages/cli/package.json'
- 'pnpm-lock.yaml'
- '.github/workflows/ci.yml'
codemod:
- 'codemods/**'
- '.github/workflows/ci.yml'
- name: Compute build matrix
id: targets
run: |
{
echo 'matrix<<MATRIX_EOF'
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
# PRs build linux-x64 (smoke test + e2e)
echo '{"include":[
{"target":"linux-x64", "os":"ubuntu-latest", "can-test":true}
]}'
else
# main, release/**, workflow_call: full cross-platform matrix.
# darwin-arm64 builds natively on macOS for V8 code cache (~15% faster startup).
# All other targets cross-compile from ubuntu-latest via fossilize.
echo '{"include":[
{"target":"darwin-arm64", "os":"macos-latest", "can-test":true},
{"target":"darwin-x64", "os":"ubuntu-latest", "can-test":false},
{"target":"linux-x64", "os":"ubuntu-latest", "can-test":true},
{"target":"linux-arm64", "os":"ubuntu-latest", "can-test":false},
{"target":"windows-x64", "os":"ubuntu-latest", "can-test":false}
]}'
fi
echo 'MATRIX_EOF'
} >> "$GITHUB_OUTPUT"
- name: Compute nightly version
id: nightly
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: |
TS=$(date -d "$COMMIT_TIMESTAMP" +%s)
CURRENT=$(jq -r .version packages/cli/package.json)
VERSION=$(echo "$CURRENT" | sed "s/-dev\.[0-9]*$/-dev.${TS}/")
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Nightly version: ${VERSION}"
codemod-test:
name: Codemod tests
needs: [changes]
if: needs.changes.outputs.codemod == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION_22 }}
# jssg fixtures live under codemods/sentry-v3-to-v4/tests/<case>/. The
# codemod CLI is pinned for reproducible runs (bump deliberately).
- name: Run jssg codemod tests
working-directory: codemods/sentry-v3-to-v4
run: npx codemod@1.12.7 jssg test -l typescript ./scripts/codemod.ts
check-generated:
name: Validate generated files
needs: [changes]
if: needs.changes.outputs.skill == 'true'
runs-on: ubuntu-latest
steps:
- name: Get auth token
id: token
# Fork PRs don't have access to secrets, so this step is skipped
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request'
uses: actions/create-github-app-token@v3
with:
app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }}
private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }}
- uses: actions/checkout@v6
with:
token: ${{ steps.token.outputs.token || github.token }}
# Same-repo PRs (token step succeeded): check out the branch head so
# the auto-commit step can push regenerated docs back. Fork PRs leave
# `ref` empty so checkout defaults to GITHUB_REF (the pull_request
# merge SHA, always fetchable from the base repo with github.token).
ref: ${{ steps.token.outcome == 'success' && (github.head_ref || github.ref_name) || '' }}
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION_22 }}
- uses: actions/cache@v5
id: cache
with:
path: |
node_modules
packages/*/node_modules
apps/*/node_modules
key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }}
- if: steps.cache.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfile
- name: Generate API Schema
run: pnpm run generate:schema
- name: Generate docs and skill files
run: pnpm run generate:docs
- name: Validate fragments
run: pnpm run check:fragments
- name: Check skill files
id: check-skill
run: |
if git diff --quiet packages/cli/plugins/sentry-cli/skills/sentry-cli/; then
echo "Skill files are up to date"
else
echo "stale=true" >> "$GITHUB_OUTPUT"
echo "Skill files are out of date"
fi
- name: Check docs sections
id: check-sections
run: |
if git diff --quiet packages/cli/README.md packages/cli/DEVELOPMENT.md apps/cli-docs/src/content/docs/contributing.md apps/cli-docs/src/content/docs/self-hosted.md apps/cli-docs/src/content/docs/getting-started.mdx; then
echo "Docs sections are up to date"
else
echo "stale=true" >> "$GITHUB_OUTPUT"
echo "Docs sections are out of date"
fi
- name: Auto-commit regenerated files
if: (steps.check-skill.outputs.stale == 'true' || steps.check-sections.outputs.stale == 'true') && steps.token.outcome == 'success'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add packages/cli/plugins/sentry-cli/skills/sentry-cli/ packages/cli/README.md packages/cli/DEVELOPMENT.md apps/cli-docs/src/content/docs/contributing.md apps/cli-docs/src/content/docs/self-hosted.md apps/cli-docs/src/content/docs/getting-started.mdx
git diff --cached --quiet || (git commit -m "chore: regenerate docs" && git push)
- name: Fail for fork PRs with stale generated files
if: (steps.check-skill.outputs.stale == 'true' || steps.check-sections.outputs.stale == 'true') && steps.token.outcome != 'success'
run: |
echo "::error::Generated files are out of date. Run 'pnpm run generate:docs' locally and commit the result."
exit 1
lint:
name: Lint & Typecheck
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION_22 }}
- uses: actions/cache@v5
id: cache
with:
path: |
node_modules
packages/*/node_modules
apps/*/node_modules
key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }}
- if: steps.cache.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfile
- run: pnpm run generate:schema
- run: pnpm run lint
- run: pnpm run typecheck
- run: pnpm run check:deps
- run: pnpm run check:errors
- run: pnpm run check:patches
- run: pnpm run check:stale-refs
test-unit:
name: Unit Tests
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
pull-requests: write
statuses: write
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION_24 }}
- uses: actions/cache@v5
id: cache
with:
path: |
node_modules
packages/*/node_modules
apps/*/node_modules
key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }}
- if: steps.cache.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfile
- name: Generate API Schema
run: pnpm run generate:schema
- name: Unit Tests
run: pnpm run test:unit
- name: Coverage Report
uses: getsentry/codecov-action@main
with:
token: ${{ secrets.GITHUB_TOKEN }}
files: ./coverage/lcov.info
informational-patch: ${{ github.event_name == 'push' }}
build-binary:
name: Build Binary (${{ matrix.target }})
needs: [changes, lint, test-unit]
runs-on: ${{ matrix.os }}
# SENTRY_AUTH_TOKEN is scoped to the production environment so sourcemap
# upload works on main/release branches. Without this, every binary build
# skips the upload and Sentry stack traces show minified names (e.g., xE).
environment: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && 'production' || '' }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.changes.outputs.build-targets) }}
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION_22 }}
- uses: actions/cache@v5
id: cache
with:
path: |
node_modules
packages/*/node_modules
apps/*/node_modules
key: node-modules-${{ matrix.os }}-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
shell: bash
run: pnpm install --frozen-lockfile
- name: Setup codesign dependencies
env:
APPLE_CERT_DATA: ${{ secrets.APPLE_CERT_DATA }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
shell: bash
run: |
# Install rcodesign (platform-aware binary + checksum tool)
if [[ "$RUNNER_OS" == "macOS" && "$RUNNER_ARCH" == "ARM64" ]]; then
RCODESIGN_ARCHIVE="apple-codesign-0.29.0-aarch64-apple-darwin.tar.gz"
RCODESIGN_SHA256="d1a532150adaf90048260d76359261aa716abafc45c53c5dc18845029184334a"
elif [[ "$RUNNER_OS" == "macOS" ]]; then
RCODESIGN_ARCHIVE="apple-codesign-0.29.0-x86_64-apple-darwin.tar.gz"
RCODESIGN_SHA256="14ef11bedd51a8d95eafd767939ae96d5900e5a61511bef75bb21db6e7c74140"
else
RCODESIGN_ARCHIVE="apple-codesign-0.29.0-x86_64-unknown-linux-musl.tar.gz"
RCODESIGN_SHA256="dbe85cedd8ee4217b64e9a0e4c2aef92ab8bcaaa41f20bde99781ff02e600002"
fi
curl -L "https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.29.0/${RCODESIGN_ARCHIVE}" -o 'rcodesign.tar.gz'
echo "${RCODESIGN_SHA256} rcodesign.tar.gz" | shasum -a 256 -c
tar -xzf rcodesign.tar.gz --strip-components=1
mv rcodesign /usr/local/bin/rcodesign
rm rcodesign.tar.gz
# Decode Apple signing credentials
if [ -n "$APPLE_CERT_DATA" ]; then
echo "$APPLE_CERT_DATA" | base64 --decode > /tmp/certs.p12
echo 'APPLE_CERT_PATH=/tmp/certs.p12' >> $GITHUB_ENV
fi
if [ -n "$APPLE_API_KEY" ]; then
echo "$APPLE_API_KEY" | base64 -d > /tmp/apple_key.json
cat /tmp/apple_key.json | jq .private_key -r > /tmp/apple_key.pem
echo "APPLE_API_KEY_ISSUER_ID=$(cat /tmp/apple_key.json | jq .issuer_id -r | tr -d '\n\r')" >> $GITHUB_ENV
echo "APPLE_API_KEY_ID=$(cat /tmp/apple_key.json | jq .key_id -r | tr -d '\n\r')" >> $GITHUB_ENV
echo "APPLE_API_KEY_P8_PATH=/tmp/apple_key.pem" >> $GITHUB_ENV
echo 'APPLE_API_KEY_PATH=/tmp/apple_key.json' >> $GITHUB_ENV
fi
- name: Set nightly version
# Inject the nightly version (computed once in the changes job) into
# package.json before the build so it gets baked into the binary.
if: needs.changes.outputs.nightly-version != ''
shell: bash
run: |
jq --arg v "${{ needs.changes.outputs.nightly-version }}" '.version = $v' packages/cli/package.json > packages/cli/package.json.tmp
mv packages/cli/package.json.tmp packages/cli/package.json
- name: Build
env:
# Environment-scoped (production) — must be set at step level to
# resolve correctly; workflow-level env evaluates before the job's
# environment: is applied.
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
# Set on main/release branches so build.ts creates .gz archives
RELEASE_BUILD: ${{ github.event_name != 'pull_request' && '1' || '' }}
# Codesigning: only on main/release pushes (fork PRs lack secrets)
FOSSILIZE_SIGN: ${{ github.event_name == 'push' && (github.ref_name == 'main' || startsWith(github.ref_name, 'release/')) && 'y' || 'n' }}
APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: pnpm run build -- --target ${{ matrix.target }}
- name: Smoke test
if: matrix.can-test
shell: bash
run: |
if [[ "${{ matrix.target }}" == "windows-x64" ]]; then
./packages/cli/dist-bin/sentry-windows-x64.exe --help
else
./packages/cli/dist-bin/sentry-${{ matrix.target }} --help
fi
- name: Smoke test (deep — SQLite, telemetry, auth DB)
if: matrix.can-test
shell: bash
env:
SENTRY_AUTH_TOKEN: ""
SENTRY_TOKEN: ""
run: |
if [[ "${{ matrix.target }}" == "windows-x64" ]]; then
BIN=./packages/cli/dist-bin/sentry-windows-x64.exe
else
BIN=./packages/cli/dist-bin/sentry-${{ matrix.target }}
fi
# auth status without a token exercises SQLite init, schema
# migrations, telemetry lazy import, and the CJS require chain.
# Expected: exit 10 (AUTH_NOT_AUTHENTICATED), NOT a crash/syntax error.
OUTPUT=$($BIN auth status 2>&1) && EXIT_CODE=$? || EXIT_CODE=$?
if [[ $EXIT_CODE -ne 10 ]]; then
echo "::error::Expected exit code 10 (not authenticated), got $EXIT_CODE"
echo "$OUTPUT"
exit 1
fi
if ! echo "$OUTPUT" | grep -qi "not authenticated"; then
echo "::error::Expected 'not authenticated' in output, got:"
echo "$OUTPUT"
exit 1
fi
- name: Verify code signature (darwin)
# Only runs when signing was active (main/release pushes) and target is darwin.
# The smoke test alone does NOT catch invalid signatures — AMFI only SIGKILLs
# quarantined (downloaded) binaries, not freshly-built ones on the build machine.
if: >-
startsWith(matrix.target, 'darwin') &&
github.event_name == 'push' &&
(github.ref_name == 'main' || startsWith(github.ref_name, 'release/'))
shell: bash
run: |
BIN=./packages/cli/dist-bin/sentry-${{ matrix.target }}
echo "Verifying code signature on $BIN..."
rcodesign verify "$BIN"
- name: Upload binary artifact
uses: actions/upload-artifact@v7
with:
name: sentry-${{ matrix.target }}
path: |
packages/cli/dist-bin/sentry-*
!packages/cli/dist-bin/*.gz
- name: Upload compressed artifact
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v7
with:
name: sentry-${{ matrix.target }}-gz
path: packages/cli/dist-bin/*.gz
generate-patches:
name: Generate Delta Patches
needs: [changes, build-binary]
# Only on main (nightlies) and release branches (stable) — skip PRs
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
continue-on-error: true
# `from-version`: the previous version the patches were actually generated
# FROM, as computed by the binpatch action. Carried through to
# publish-nightly so it stamps the `from-version` annotation on the
# pushed patch manifest with the SAME value. See
# https://github.com/BYK/binpatch/blob/main/website/src/content/docs/security.md#from-version-annotation-trust
#
# Note: the `release/**` path uses `generate-release` (different mode) and
# its `from-version` is captured only for the `Upload patch artifacts` step
# gate below. `publish-nightly` only runs on `main`, so the release-branch
# from-version is intentionally not exposed as a job output.
outputs:
from-version: ${{ steps.gen-ghcr.outputs.from-version }}
steps:
- name: Download current binaries
uses: actions/download-artifact@v8
with:
# Use sentry-*-* to match platform binaries (sentry-linux-x64, etc.)
# and their -gz variants, but not sentry-patches
pattern: sentry-*-*
path: new-binaries
merge-multiple: true
- name: Generate delta patches (nightly)
id: gen-ghcr
if: github.ref == 'refs/heads/main'
uses: BYK/binpatch/action@0.4.2
with:
mode: generate-ghcr
version: ${{ needs.changes.outputs.nightly-version }}
registry: ghcr.io
repo: getsentry/cli
binary-glob: 'sentry-*'
new-binaries-dir: new-binaries
new-gz-dir: new-binaries
patches-dir: patches
- name: Generate delta patches (stable)
id: gen-release
if: startsWith(github.ref, 'refs/heads/release/')
uses: BYK/binpatch/action@0.4.2
with:
mode: generate-release
repo: ${{ github.repository }}
binary-glob: 'sentry-*'
new-binaries-dir: new-binaries
new-gz-dir: new-binaries
patches-dir: patches
- name: Upload patch artifacts
if: steps.gen-ghcr.outputs.has-patches == 'true' || steps.gen-release.outputs.has-patches == 'true'
uses: actions/upload-artifact@v7
with:
name: sentry-patches
path: patches/*.patch
- name: File issue on failure
if: failure()
env:
GH_TOKEN: ${{ github.token }}
run: |
TITLE="Delta patch generation failed"
BODY="The \`generate-patches\` job failed on [\`${GITHUB_REF_NAME}\`](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}).
**Branch:** \`${GITHUB_REF_NAME}\`
**Commit:** ${GITHUB_SHA}
**Run:** ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
# Check for existing open issue with same title to avoid duplicates
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "$TITLE" --json number --jq '.[0].number // empty')
if [ -n "$EXISTING" ]; then
echo "Issue #${EXISTING} already open, adding comment"
gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body "$BODY"
else
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body "$BODY"
fi
publish-nightly:
name: Publish Nightly to GHCR
# Only run on pushes to main, not on PRs or release branches
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
needs: [changes, build-binary, generate-patches]
runs-on: ubuntu-latest
steps:
- name: Download compressed artifacts
uses: actions/download-artifact@v8
with:
pattern: sentry-*-gz
path: artifacts
merge-multiple: true
- name: Download uncompressed artifacts (for SHA-256 computation)
uses: actions/download-artifact@v8
with:
# Use sentry-*-* to match platform binaries (sentry-linux-x64, etc.)
# but not sentry-patches (which is downloaded separately)
pattern: sentry-*-*
path: binaries
merge-multiple: true
- name: Download patch artifacts
uses: actions/download-artifact@v8
continue-on-error: true
id: download-patches
with:
name: sentry-patches
path: patches
- name: Push to GHCR (binpatch action)
uses: BYK/binpatch/action@0.4.2
with:
mode: publish-ghcr
version: ${{ needs.changes.outputs.nightly-version }}
registry: ghcr.io
repo: getsentry/cli
binary-glob: 'sentry-*'
artifacts-dir: artifacts
binaries-dir: binaries
patches-dir: patches
# CRITICAL: stamp the annotation with the value the generate step
# actually used, NOT a re-derived tag listing. binpatch verifies
# only the final output SHA, so a mismatched `from-version` annotation
# silently bypasses the user's expected upgrade path. See
# https://github.com/BYK/binpatch/blob/main/website/src/content/docs/security.md#from-version-annotation-trust
from-version: ${{ needs.generate-patches.outputs.from-version }}
test-e2e:
name: E2E Tests
needs: [build-binary, changes]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
# Pinned to a patched 24 — the skill-eval planner makes keep-alive
# HTTPS calls and would hit the ERR_STREAM_PREMATURE_CLOSE regression
# on a floating "24". See NODE_VERSION_24 in the top-level env block.
node-version: ${{ env.NODE_VERSION_24 }}
- uses: actions/cache@v5
id: cache
with:
path: |
node_modules
packages/*/node_modules
apps/*/node_modules
key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }}
- if: steps.cache.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfile
- name: Download Linux binary
uses: actions/download-artifact@v8
with:
name: sentry-linux-x64
path: dist-bin
- name: Make binary executable
run: chmod +x dist-bin/sentry-linux-x64
- name: Generate API Schema
run: pnpm run generate:schema
- name: E2E Tests
env:
SENTRY_CLI_BINARY: ${{ github.workspace }}/dist-bin/sentry-linux-x64
# Pass the eval provider key only when skill files changed — the
# skill-eval e2e test auto-skips when no key is present, so non-skill
# PRs aren't affected. OpenRouter is preferred; ANTHROPIC_API_KEY is
# kept as a fallback for direct-Anthropic runs.
OPENROUTER_API_KEY: ${{ needs.changes.outputs.skill == 'true' && secrets.OPENROUTER_API_KEY || '' }}
ANTHROPIC_API_KEY: ${{ needs.changes.outputs.skill == 'true' && secrets.ANTHROPIC_API_KEY || '' }}
run: pnpm run test:e2e
build-npm:
name: Build npm Package (smoke Node ${{ matrix.node }})
needs: [lint, test-unit]
runs-on: ubuntu-latest
environment: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && 'production' || '' }}
strategy:
fail-fast: false
matrix:
node: ["20", "22", "24"]
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
# Build under our development Node floor (22.15+). The build tooling
# (tsx loader hooks in script/require-shim.mjs) uses node:module APIs
# only present on 22.15+, so the bundle is always produced on a modern
# Node — matching how we actually publish. The matrix Node version is
# only used to *run* the produced artifact below.
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION_22 }}
- uses: actions/cache@v5
id: cache
with:
path: |
node_modules
packages/*/node_modules
apps/*/node_modules
key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }}
- if: steps.cache.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfile
- name: Bundle
env:
# Environment-scoped (production) — see note in build-binary.
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: pnpm run bundle
# Switch to the matrix Node to run the produced artifact. This is the
# consumer's perspective: Node 20 exercises the WASM SQLite fallback
# (no node:sqlite before 22.15); 22/24 use the native driver.
- uses: actions/setup-node@v6
with:
# Matrix entries stay bare majors ("20"/"22"/"24") for the job name
# and the `matrix.node == '22'` artifact guard below; map each to its
# exact patched version from the central env block here.
node-version: ${{ matrix.node == '24' && env.NODE_VERSION_24 || matrix.node == '20' && env.NODE_VERSION_20 || env.NODE_VERSION_22 }}
- name: Smoke test (Node.js)
env:
# Clear auth tokens — on main the job inherits SENTRY_AUTH_TOKEN
# from the production environment (for the Bundle step's sourcemap
# upload). The smoke tests must not use it.
SENTRY_AUTH_TOKEN: ""
SENTRY_TOKEN: ""
# Use a temp config dir so the WASM SQLite driver (Node 20) can
# create and open the database reliably. The runner's ~/.sentry
# may not exist under the switched Node 20 runtime.
SENTRY_CONFIG_DIR: ${{ runner.temp }}/.sentry-smoke
run: node packages/cli/dist/bin.cjs --help
- name: Smoke test (Node.js — deep)
shell: bash
env:
SENTRY_AUTH_TOKEN: ""
SENTRY_TOKEN: ""
SENTRY_CONFIG_DIR: ${{ runner.temp }}/.sentry-smoke
run: |
# auth status without a token exercises SQLite init, schema
# migrations, telemetry lazy import, and the CJS require chain.
# On Node 20 this runs entirely through the bundled WASM SQLite
# driver. Expected: exit 10 (AUTH_NOT_AUTHENTICATED), NOT a crash.
OUTPUT=$(node packages/cli/dist/bin.cjs auth status 2>&1) && EXIT_CODE=$? || EXIT_CODE=$?
if [[ $EXIT_CODE -ne 10 ]]; then
echo "::error::Expected exit code 10 (not authenticated), got $EXIT_CODE"
echo "$OUTPUT"
exit 1
fi
if ! echo "$OUTPUT" | grep -qi "not authenticated"; then
echo "::error::Expected 'not authenticated' in output, got:"
echo "$OUTPUT"
exit 1
fi
- run: npm pack
working-directory: packages/cli
- name: Upload artifact
if: matrix.node == '22'
uses: actions/upload-artifact@v7
with:
name: npm-package
path: "packages/cli/*.tgz"
build-docs:
name: Build Docs
needs: [lint, build-binary]
runs-on: ubuntu-latest
# SENTRY_AUTH_TOKEN is scoped to the production environment. Needed by
# the "Inject debug IDs and upload sourcemaps" step below.
environment: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && 'production' || '' }}
# Hoisted to job level (not step) so the `if: env.SENTRY_AUTH_TOKEN != ''`
# guard on the sourcemap-upload step can see it. Job-level env is resolved
# after `environment:` is applied, so the production-scoped secret resolves
# correctly.
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
# Astro 6 requires Node >= 22.12. Pin an explicit version so the docs
# build doesn't rely on whatever ships on the runner image.
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION_24 }}
- uses: actions/cache@v5
id: cache
with:
path: |
node_modules
packages/*/node_modules
apps/*/node_modules
key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }}
- if: steps.cache.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfile
- name: Get CLI version
id: version
run: echo "version=$(node -p 'require("./packages/cli/package.json").version')" >> "$GITHUB_OUTPUT"
- name: Download compiled CLI binary
uses: actions/download-artifact@v8
with:
name: sentry-linux-x64
path: dist-bin
- name: Make binary executable
run: chmod +x dist-bin/sentry-linux-x64
- name: Generate docs content
run: pnpm run generate:schema && pnpm run generate:docs
- name: Build Docs
working-directory: apps/cli-docs
env:
PUBLIC_SENTRY_ENVIRONMENT: production
SENTRY_RELEASE: ${{ steps.version.outputs.version }}
PUBLIC_SENTRY_RELEASE: ${{ steps.version.outputs.version }}
run: |
pnpm install --frozen-lockfile
pnpm run build
# Inject debug IDs and upload sourcemaps. The inject step adds
# //# debugId= and the _sentryDebugIds IIFE to deployed JS files.
# Both steps require SENTRY_AUTH_TOKEN (the CLI checks auth on startup).
- name: Inject debug IDs and upload sourcemaps
if: github.event_name == 'push' && env.SENTRY_AUTH_TOKEN != ''
env:
SENTRY_ORG: sentry
SENTRY_PROJECT: cli-website
run: |
./dist-bin/sentry-linux-x64 sourcemap inject apps/cli-docs/dist/
./dist-bin/sentry-linux-x64 sourcemap upload apps/cli-docs/dist/ \
--release "${{ steps.version.outputs.version }}" \
--url-prefix "~/"
# Remove .map files — they were uploaded to Sentry but shouldn't
# be deployed to production.
- name: Remove sourcemaps from output
run: find apps/cli-docs/dist -name '*.map' -delete
# Package the built site using Vercel's Build Output API so Craft's
# release-gated Vercel target can deploy it without rebuilding.
- name: Package Docs
run: |
OUTPUT_DIR="$RUNNER_TEMP/vercel-output"
rm -rf "$OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR/.vercel/output/static"
cp -R apps/cli-docs/dist/. "$OUTPUT_DIR/.vercel/output/static/"
printf '%s\n' '{"version":3}' > "$OUTPUT_DIR/.vercel/output/config.json"
(cd "$OUTPUT_DIR" && zip -qr "$GITHUB_WORKSPACE/vercel.zip" .vercel)
- name: Upload docs artifact
uses: actions/upload-artifact@v7
with:
name: vercel
path: vercel.zip
ci-status:
name: CI Status
if: always()
needs: [changes, check-generated, build-binary, build-npm, build-docs, test-e2e, generate-patches, codemod-test]
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Check CI status
run: |
# Check for explicit failures or cancellations in all jobs
# generate-patches is skipped on PRs — that's expected
# publish-nightly is excluded: it's infrastructure (GHCR push), not code quality
results="${{ needs.check-generated.result }} ${{ needs.build-binary.result }} ${{ needs.build-npm.result }} ${{ needs.build-docs.result }} ${{ needs.test-e2e.result }} ${{ needs.generate-patches.result }} ${{ needs.codemod-test.result }}"
for result in $results; do
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "::error::CI failed"
exit 1
fi
done
# Detect upstream failures: if changes were detected but jobs were skipped,
# it means an upstream job failed (skipped jobs cascade to dependents)
if [[ "${{ needs.changes.outputs.code }}" == "true" && "${{ needs.test-e2e.result }}" == "skipped" ]]; then
echo "::error::CI failed - upstream job failed causing test-e2e to be skipped"
exit 1
fi
if [[ "${{ needs.changes.outputs.skill }}" == "true" && "${{ needs.check-generated.result }}" == "skipped" ]]; then
echo "::error::CI failed - upstream job failed causing check-generated to be skipped"
exit 1
fi
if [[ "${{ needs.changes.outputs.codemod }}" == "true" && "${{ needs.codemod-test.result }}" == "skipped" ]]; then
echo "::error::CI failed - upstream job failed causing codemod-test to be skipped"
exit 1
fi
echo "CI passed"