From 0af6b365cef983935413ec6a9fb1ca911ca3b65b Mon Sep 17 00:00:00 2001 From: betegon Date: Thu, 29 Oct 2026 18:36:30 +0100 Subject: [PATCH 1/8] ci(cli): run E2E tests in parallel --- .github/workflows/test.yml | 47 +++++++++++++++++++++++- docs/operations/github-actions.md | 6 ++++ packages/cli/package.json | 1 + scripts/ci-projects.test.mjs | 60 +++++++++++++++++++++++++++++-- 4 files changed, 111 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 47402b236..14bda6903 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -214,6 +214,48 @@ jobs: git diff --exit-code [[ -z "$(git status --porcelain --untracked-files=all)" ]] + cli-e2e: + name: CLI E2E (Node 24) + needs: discover-projects + if: contains(fromJSON(needs.discover-projects.outputs.matrix).include.*.name, 'sentry') + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + BRAINTRUST_API_KEY: ci-test-key + GITHUB_TOKEN: ci-test-token + HONEYCOMB_API_KEY: ci-test-key + SENTRY_CLIENT_ID: ${{ vars.SENTRY_CLIENT_ID }} + SENTRY_OAUTH_CLIENT_SECRET: ci-test-secret + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: ${{ env.NODE_VERSION_22 }} + cache: pnpm + - run: cp .env.example .env.local + - run: pnpm install --frozen-lockfile + - name: Build project dependencies + run: pnpm --filter 'sentry^...' --if-present run build + - name: Build standalone CLI + run: pnpm --filter sentry run build + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: ${{ env.NODE_VERSION_24 }} + cache: pnpm + - name: Run E2E tests on Node 24 + env: + SENTRY_CLI_BINARY: ${{ github.workspace }}/packages/cli/dist-bin/sentry-linux-x64 + run: | + test -x "$SENTRY_CLI_BINARY" + pnpm --filter sentry run test:e2e + - name: Check generated files + run: | + git diff --exit-code + [[ -z "$(git status --porcelain --untracked-files=all)" ]] + npm-runtime: name: NPM package (${{ matrix.name }}) needs: discover-projects @@ -290,6 +332,7 @@ jobs: project, npm-runtime, cli-asset-catalog, + cli-e2e, ] runs-on: ubuntu-latest steps: @@ -304,6 +347,7 @@ jobs: RUNTIME_COUNT: ${{ needs.discover-projects.outputs.runtime-count }} ASSET_CATALOG_RESULT: ${{ needs.cli-asset-catalog.result }} ASSET_CATALOG_SELECTED: ${{ contains(fromJSON(needs.discover-projects.outputs.matrix).include.*.name, 'sentry') }} + CLI_E2E_RESULT: ${{ needs.cli-e2e.result }} run: | [[ "$DISCOVER_RESULT" == success ]] [[ "$INSTALL_RESULT" == success ]] @@ -322,4 +366,5 @@ jobs: if [[ "$ASSET_CATALOG_SELECTED" == false ]]; then expected=skipped fi - [[ "$ASSET_CATALOG_RESULT" == "$expected" ]] + test "$ASSET_CATALOG_RESULT" = "$expected" + test "$CLI_E2E_RESULT" = "$expected" diff --git a/docs/operations/github-actions.md b/docs/operations/github-actions.md index b0e0484bb..a7a8bd7c7 100644 --- a/docs/operations/github-actions.md +++ b/docs/operations/github-actions.md @@ -21,6 +21,12 @@ discovery, installation, repository quality, and every selected project. Package-specific exceptions live in `package.json#sentryCi`; the standalone smoke-test suite remains in its own workflow. +`CLI E2E (Node 24)` runs independently of the CLI project job. It builds the +standalone CLI on Node 22, then builds the npm bundle and tests both on Node 24. +Unit tests and coverage remain in the project job. The final `test` gate +requires E2E to pass whenever the CLI is selected, and requires it to be skipped +otherwise. + ### deploy.yml Runs after a successful `Test` push run on `main`. Checks out the tested commit diff --git a/packages/cli/package.json b/packages/cli/package.json index 9e5f41a5c..eee8ddf72 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -51,6 +51,7 @@ "packageManager": "pnpm@10.11.0", "sentryCi": { "coverage": "coverage/lcov.info", + "e2e": false, "npmRuntime": "bundle", "policy": "ci:policy" }, diff --git a/scripts/ci-projects.test.mjs b/scripts/ci-projects.test.mjs index cc1396274..a0dcfaf92 100644 --- a/scripts/ci-projects.test.mjs +++ b/scripts/ci-projects.test.mjs @@ -43,8 +43,10 @@ describe("workspace CI selection", () => { assert.match(docsScripts[docs.prepare], /sentry run generate:schema/); assert.match(docsScripts[docs.prepare], /sentry run generate:docs/); const cli = matrix.include.find(({ name }) => name === "sentry"); + assert.equal(cli.build, "build"); + assert.equal(cli.test, "test:unit"); assert.equal(cli.policy, "ci:policy"); - assert.equal(cli.e2e, "test:e2e"); + assert.equal(cli.e2e, ""); assert.equal(cli.coverage, "packages/cli/coverage/lcov.info"); assert.equal( matrix.include.find(({ name }) => name === "@sentry/mcp-core").junit, @@ -174,7 +176,7 @@ describe("workspace CI selection", () => { assert.doesNotMatch(workflow, /matrix\.name == 'sentry-cli-docs'/); assert.match( workflow, - /needs:\s*\[\s*discover-projects,\s*install,\s*quality,\s*project,\s*npm-runtime,\s*cli-asset-catalog,?\s*\]/, + /needs:\s*\[\s*discover-projects,\s*install,\s*quality,\s*project,\s*npm-runtime,\s*cli-asset-catalog,\s*cli-e2e,?\s*\]/, ); assert.match( workflow, @@ -202,6 +204,20 @@ describe("workspace CI selection", () => { assert.doesNotMatch(workflow.split("\njobs:", 1)[0], /paths(?:-ignore)?:/); }); + it("runs CLI E2E independently after project discovery", () => { + const workflow = readFileSync( + resolve(root, ".github/workflows/test.yml"), + "utf8", + ); + const job = workflow.split("\n cli-e2e:\n")[1]?.split(/\n [\w-]+:\n/)[0]; + assert.ok(job); + assert.match(job, /\n needs: discover-projects\n/); + assert.match( + job, + /if: contains\(fromJSON\(needs\.discover-projects\.outputs\.matrix\)\.include\.\*\.name, 'sentry'\)/, + ); + }); + it("requires native asset-catalog success when selected and a skip otherwise", () => { const workflow = readFileSync( resolve(root, ".github/workflows/test.yml"), @@ -244,6 +260,7 @@ describe("workspace CI selection", () => { RUNTIME_RESULT: count === "0" ? "skipped" : "success", ASSET_CATALOG_RESULT: nativeResult, ASSET_CATALOG_SELECTED: String(selected), + CLI_E2E_RESULT: selected ? "success" : "skipped", }, }); assert.equal( @@ -254,6 +271,45 @@ describe("workspace CI selection", () => { } }); + it("requires CLI E2E success when selected and a skip otherwise", () => { + const workflow = readFileSync( + resolve(root, ".github/workflows/test.yml"), + "utf8", + ); + const gate = workflow.split("\n test:\n")[1]; + assert.ok(gate); + assert.match(gate, /CLI_E2E_RESULT: \$\{\{ needs\.cli-e2e\.result \}\}/); + const script = gate.split(" run: |\n")[1]; + assert.ok(script); + + for (const selected of [true, false]) { + const expected = selected ? "success" : "skipped"; + for (const status of ["success", "failure", "cancelled", "skipped"]) { + const result = spawnSync("bash", ["-euc", script], { + encoding: "utf8", + env: { + ...process.env, + DISCOVER_RESULT: "success", + INSTALL_RESULT: "success", + QUALITY_RESULT: "success", + PROJECT_COUNT: "1", + PROJECT_RESULT: "success", + RUNTIME_COUNT: selected ? "1" : "0", + RUNTIME_RESULT: expected, + ASSET_CATALOG_RESULT: expected, + ASSET_CATALOG_SELECTED: String(selected), + CLI_E2E_RESULT: status, + }, + }); + assert.equal( + result.status, + status === expected ? 0 : 1, + `selected=${selected}, e2e=${status}`, + ); + } + } + }); + it("requires safe metadata and a single owner of each specialized role", () => { assert.throws( () => From 2c1cde73a6c97f4a828bc2031987a482c2eff9ee Mon Sep 17 00:00:00 2001 From: betegon Date: Thu, 29 Oct 2026 20:47:31 +0100 Subject: [PATCH 2/8] ci(cli): remove unused E2E environment setup --- .github/workflows/test.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 14bda6903..8d3d50087 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -221,11 +221,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 env: - BRAINTRUST_API_KEY: ci-test-key - GITHUB_TOKEN: ci-test-token - HONEYCOMB_API_KEY: ci-test-key SENTRY_CLIENT_ID: ${{ vars.SENTRY_CLIENT_ID }} - SENTRY_OAUTH_CLIENT_SECRET: ci-test-secret steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: @@ -235,7 +231,6 @@ jobs: with: node-version: ${{ env.NODE_VERSION_22 }} cache: pnpm - - run: cp .env.example .env.local - run: pnpm install --frozen-lockfile - name: Build project dependencies run: pnpm --filter 'sentry^...' --if-present run build From 756b806115166c38d08dee8bc2e92cdd9da96ad1 Mon Sep 17 00:00:00 2001 From: betegon Date: Fri, 30 Oct 2026 10:36:08 +0100 Subject: [PATCH 3/8] ci(cli): prepare E2E and unit inputs once --- .github/workflows/test.yml | 16 ++++++++++------ docs/operations/github-actions.md | 5 ++++- packages/cli/package.json | 10 ++++++++-- scripts/ci-projects.test.mjs | 6 ++++-- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8d3d50087..a867fe29f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -223,29 +223,33 @@ jobs: env: SENTRY_CLIENT_ID: ${{ vars.SENTRY_CLIENT_ID }} steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@v7 with: node-version: ${{ env.NODE_VERSION_22 }} cache: pnpm - run: pnpm install --frozen-lockfile - name: Build project dependencies run: pnpm --filter 'sentry^...' --if-present run build + - run: pnpm --filter sentry run ci:prepare - name: Build standalone CLI - run: pnpm --filter sentry run build - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + run: pnpm --filter sentry run tsx script/build.ts --single + - uses: actions/setup-node@v7 with: node-version: ${{ env.NODE_VERSION_24 }} - cache: pnpm + - name: Build npm bundle + run: pnpm --filter sentry run tsx script/bundle.ts - name: Run E2E tests on Node 24 env: SENTRY_CLI_BINARY: ${{ github.workspace }}/packages/cli/dist-bin/sentry-linux-x64 run: | test -x "$SENTRY_CLI_BINARY" - pnpm --filter sentry run test:e2e + test -s packages/cli/dist/bin.cjs + test -s packages/cli/dist/index.cjs + pnpm --filter sentry exec vitest run test/e2e - name: Check generated files run: | git diff --exit-code diff --git a/docs/operations/github-actions.md b/docs/operations/github-actions.md index a7a8bd7c7..896b8691b 100644 --- a/docs/operations/github-actions.md +++ b/docs/operations/github-actions.md @@ -23,7 +23,10 @@ smoke-test suite remains in its own workflow. `CLI E2E (Node 24)` runs independently of the CLI project job. It builds the standalone CLI on Node 22, then builds the npm bundle and tests both on Node 24. -Unit tests and coverage remain in the project job. The final `test` gate +Dependencies and generated sources are prepared once per job. The CLI project +job runs typechecks, unit tests, and coverage without rebuilding the standalone +or repeating generation. Public build and test commands still prepare their own +inputs for local use. The final `test` gate requires E2E to pass whenever the CLI is selected, and requires it to be skipped otherwise. diff --git a/packages/cli/package.json b/packages/cli/package.json index eee8ddf72..71fc1aa0b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -50,10 +50,13 @@ }, "packageManager": "pnpm@10.11.0", "sentryCi": { + "build": false, "coverage": "coverage/lcov.info", "e2e": false, "npmRuntime": "bundle", - "policy": "ci:policy" + "policy": "ci:policy", + "test": "ci:test:unit", + "typecheck": "ci:typecheck" }, "scripts": { "tsx": "tsx --env-file-if-exists=.env.local --import ./script/require-shim.mjs", @@ -66,7 +69,7 @@ "lint": "pnpm -w exec oxlint --report-unused-disable-directives packages/cli && pnpm -w exec oxfmt --check packages/cli", "lint:fix": "pnpm -w exec oxlint --fix packages/cli && pnpm -w exec oxfmt --write packages/cli", "test": "pnpm run test:unit", - "test:unit": "pnpm run generate:docs && pnpm run generate:sdk && vitest run test/lib test/commands test/types test/script --coverage", + "test:unit": "pnpm run generate:docs && pnpm run generate:sdk && pnpm run ci:test:unit", "test:changed": "pnpm run generate:docs && pnpm run generate:sdk && vitest run --changed", "test:e2e": "pnpm tsx script/prepare-e2e-bundle.ts && vitest run test/e2e", "test:init-eval": "vitest run test/init-eval --testTimeout 600000", @@ -94,6 +97,9 @@ "check:doc-examples": "pnpm tsx script/check-doc-examples.ts", "check:installer-docs": "pnpm tsx script/check-installer-docs.ts", "check:destructive": "pnpm tsx script/check-destructive-commands.ts", + "ci:prepare": "pnpm run generate:schema && pnpm run generate:docs && pnpm run generate:sdk", + "ci:typecheck": "tsc --noEmit", + "ci:test:unit": "vitest run test/lib test/commands test/types test/script --coverage", "ci:policy": "pnpm run check:patches && pnpm run check:deps && pnpm run check:errors && pnpm run check:fragments && pnpm run check:docs-sections && pnpm run check:env-coverage && pnpm run check:stale-refs && pnpm run check:doc-examples && pnpm run check:installer-docs && pnpm run check:destructive" }, "devDependencies": { diff --git a/scripts/ci-projects.test.mjs b/scripts/ci-projects.test.mjs index a0dcfaf92..45fe2b9ac 100644 --- a/scripts/ci-projects.test.mjs +++ b/scripts/ci-projects.test.mjs @@ -43,8 +43,10 @@ describe("workspace CI selection", () => { assert.match(docsScripts[docs.prepare], /sentry run generate:schema/); assert.match(docsScripts[docs.prepare], /sentry run generate:docs/); const cli = matrix.include.find(({ name }) => name === "sentry"); - assert.equal(cli.build, "build"); - assert.equal(cli.test, "test:unit"); + assert.equal(cli.prepare, "ci:prepare"); + assert.equal(cli.build, ""); + assert.equal(cli.typecheck, "ci:typecheck"); + assert.equal(cli.test, "ci:test:unit"); assert.equal(cli.policy, "ci:policy"); assert.equal(cli.e2e, ""); assert.equal(cli.coverage, "packages/cli/coverage/lcov.info"); From 1b42251feb06944d17cdd89e029c26dae43727d0 Mon Sep 17 00:00:00 2001 From: betegon Date: Fri, 30 Oct 2026 13:39:49 +0100 Subject: [PATCH 4/8] ci(cli): reuse npm build setup and share Node pins --- .github/node-versions/20 | 1 + .github/node-versions/22 | 1 + .github/node-versions/24 | 1 + .github/workflows/cli-build.yml | 11 ++---- .github/workflows/test.yml | 65 +++++++++++++------------------ docs/operations/github-actions.md | 5 +++ scripts/ci-projects.test.mjs | 11 ++++-- 7 files changed, 48 insertions(+), 47 deletions(-) create mode 100644 .github/node-versions/20 create mode 100644 .github/node-versions/22 create mode 100644 .github/node-versions/24 diff --git a/.github/node-versions/20 b/.github/node-versions/20 new file mode 100644 index 000000000..ccc4c6c7f --- /dev/null +++ b/.github/node-versions/20 @@ -0,0 +1 @@ +20.20.2 diff --git a/.github/node-versions/22 b/.github/node-versions/22 new file mode 100644 index 000000000..b8b23ca93 --- /dev/null +++ b/.github/node-versions/22 @@ -0,0 +1 @@ +22.23.4 diff --git a/.github/node-versions/24 b/.github/node-versions/24 new file mode 100644 index 000000000..ce62013c3 --- /dev/null +++ b/.github/node-versions/24 @@ -0,0 +1 @@ +24.22.1 diff --git a/.github/workflows/cli-build.yml b/.github/workflows/cli-build.yml index 33ffe585c..733de89fe 100644 --- a/.github/workflows/cli-build.yml +++ b/.github/workflows/cli-build.yml @@ -16,9 +16,6 @@ permissions: env: SENTRY_CLIENT_ID: ${{ vars.SENTRY_CLIENT_ID }} - NODE_VERSION_20: "20.20.2" - NODE_VERSION_22: "22.23.4" - NODE_VERSION_24: "24.22.1" jobs: nightly-version: @@ -58,7 +55,7 @@ jobs: - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: - node-version: ${{ env.NODE_VERSION_22 }} + node-version-file: .github/node-versions/22 cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile @@ -267,7 +264,7 @@ jobs: - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: - node-version: ${{ env.NODE_VERSION_22 }} + node-version-file: .github/node-versions/22 cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile @@ -303,7 +300,7 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: - node-version: ${{ matrix.node == 24 && env.NODE_VERSION_24 || matrix.node == 20 && env.NODE_VERSION_20 || env.NODE_VERSION_22 }} + node-version-file: .github/node-versions/${{ matrix.node }} - name: Download npm artifact uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: @@ -334,7 +331,7 @@ jobs: - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: - node-version: ${{ env.NODE_VERSION_24 }} + node-version-file: .github/node-versions/24 cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a867fe29f..004fbccb6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,11 +9,6 @@ on: permissions: contents: read -env: - NODE_VERSION_20: "20.20.2" - NODE_VERSION_22: "22.23.4" - NODE_VERSION_24: "24.22.1" - jobs: discover-projects: name: Discover projects @@ -24,14 +19,14 @@ jobs: runtime-matrix: ${{ steps.projects.outputs.runtime-matrix }} runtime-count: ${{ steps.projects.outputs.runtime-count }} steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@v7 with: persist-credentials: false fetch-depth: 0 - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@v7 with: - node-version: ${{ env.NODE_VERSION_22 }} + node-version-file: .github/node-versions/22 - name: Build project matrix id: projects env: @@ -53,13 +48,13 @@ jobs: name: Install runs-on: ubuntu-latest steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@v7 with: - node-version: ${{ env.NODE_VERSION_22 }} + node-version-file: .github/node-versions/22 cache: pnpm - run: pnpm install --frozen-lockfile @@ -67,13 +62,13 @@ jobs: name: Repository quality runs-on: ubuntu-latest steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@v7 with: - node-version: ${{ env.NODE_VERSION_22 }} + node-version-file: .github/node-versions/22 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm run test:ci-projects @@ -96,13 +91,13 @@ jobs: SENTRY_CLI_NO_TELEMETRY: "1" FOSSILIZE_SIGN: "n" steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@v7 with: - node-version: ${{ env.NODE_VERSION_22 }} + node-version-file: .github/node-versions/22 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm --filter sentry run build -- --target darwin-arm64 @@ -127,13 +122,13 @@ jobs: SENTRY_CLIENT_ID: ${{ vars.SENTRY_CLIENT_ID }} SENTRY_OAUTH_CLIENT_SECRET: ci-test-secret steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@v7 with: - node-version: ${{ env.NODE_VERSION_22 }} + node-version-file: .github/node-versions/22 cache: pnpm - run: cp .env.example .env.local - run: pnpm install --frozen-lockfile @@ -192,10 +187,10 @@ jobs: PROJECT_NAME: ${{ matrix.name }} CHECK_SCRIPT: ${{ matrix.policy }} run: pnpm --filter "$PROJECT_NAME" run "$CHECK_SCRIPT" - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@v7 if: matrix.e2e != '' with: - node-version: ${{ env.NODE_VERSION_24 }} + node-version-file: .github/node-versions/24 cache: pnpm - name: Run E2E tests on Node 24 if: matrix.e2e != '' @@ -229,7 +224,7 @@ jobs: - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 - uses: actions/setup-node@v7 with: - node-version: ${{ env.NODE_VERSION_22 }} + node-version-file: .github/node-versions/22 cache: pnpm - run: pnpm install --frozen-lockfile - name: Build project dependencies @@ -239,7 +234,7 @@ jobs: run: pnpm --filter sentry run tsx script/build.ts --single - uses: actions/setup-node@v7 with: - node-version: ${{ env.NODE_VERSION_24 }} + node-version-file: .github/node-versions/24 - name: Build npm bundle run: pnpm --filter sentry run tsx script/bundle.ts - name: Run E2E tests on Node 24 @@ -268,19 +263,15 @@ jobs: SENTRY_CLIENT_ID: ${{ vars.SENTRY_CLIENT_ID }} SENTRY_CLI_NO_TELEMETRY: "1" steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@v7 with: - node-version: ${{ env.NODE_VERSION_22 }} + node-version-file: .github/node-versions/22 cache: pnpm - run: pnpm install --frozen-lockfile - - name: Build package dependencies - env: - PROJECT_NAME: ${{ matrix.name }} - run: pnpm --filter "${PROJECT_NAME}^..." --if-present run build - name: Build and pack once on Node 22 env: BUILD_SCRIPT: ${{ matrix.npmRuntime }} @@ -295,25 +286,25 @@ jobs: test -s "${packages[0]}" [[ "$(stat -c %s "${packages[0]}")" -le 52428800 ]] mv "${packages[0]}" "$RUNNER_TEMP/npm-package/package.tgz" - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@v7 with: - node-version: ${{ env.NODE_VERSION_20 }} + node-version-file: .github/node-versions/20 - name: Validate packaged runtime on Node 20 env: RUNTIME_BIN: ${{ matrix.runtimeBin }} RUNTIME_PACKAGE: ${{ matrix.name }} run: bash scripts/validate-npm-runtime.sh - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@v7 with: - node-version: ${{ env.NODE_VERSION_22 }} + node-version-file: .github/node-versions/22 - name: Validate packaged runtime on Node 22 env: RUNTIME_BIN: ${{ matrix.runtimeBin }} RUNTIME_PACKAGE: ${{ matrix.name }} run: bash scripts/validate-npm-runtime.sh - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@v7 with: - node-version: ${{ env.NODE_VERSION_24 }} + node-version-file: .github/node-versions/24 - name: Validate packaged runtime on Node 24 env: RUNTIME_BIN: ${{ matrix.runtimeBin }} diff --git a/docs/operations/github-actions.md b/docs/operations/github-actions.md index 896b8691b..bf43f198a 100644 --- a/docs/operations/github-actions.md +++ b/docs/operations/github-actions.md @@ -21,6 +21,11 @@ discovery, installation, repository quality, and every selected project. Package-specific exceptions live in `package.json#sentryCi`; the standalone smoke-test suite remains in its own workflow. +`test.yml` and `cli-build.yml` read their exact Node 20, 22, and 24 versions +from `.github/node-versions/`. The npm runtime job calls the self-contained +`sentryCi.npmRuntime` build command once, then validates the same package on +all three Node versions. + `CLI E2E (Node 24)` runs independently of the CLI project job. It builds the standalone CLI on Node 22, then builds the npm bundle and tests both on Node 24. Dependencies and generated sources are prepared once per job. The CLI project diff --git a/scripts/ci-projects.test.mjs b/scripts/ci-projects.test.mjs index 45fe2b9ac..1f9e971d7 100644 --- a/scripts/ci-projects.test.mjs +++ b/scripts/ci-projects.test.mjs @@ -196,9 +196,14 @@ describe("workspace CI selection", () => { assert.match(workflow, /Validate packaged runtime on Node 20/); assert.match(workflow, /Validate packaged runtime on Node 22/); assert.match(workflow, /Validate packaged runtime on Node 24/); - assert.match(workflow, /NODE_VERSION_20: "20\.20\.2"/); - assert.match(workflow, /NODE_VERSION_22: "22\.23\.4"/); - assert.match(workflow, /NODE_VERSION_24: "24\.22\.1"/); + for (const major of [20, 22, 24]) { + const versionFile = `.github/node-versions/${major}`; + assert.ok(workflow.includes(`node-version-file: ${versionFile}`)); + assert.match( + readFileSync(resolve(root, versionFile), "utf8").trim(), + new RegExp(`^${major}\\.\\d+\\.\\d+$`), + ); + } assert.match(workflow, /RUNTIME_RESULT.*needs\.npm-runtime\.result/); assert.match(workflow, /name: Verify JUnit/); assert.match(workflow, /name: Verify coverage/); From c2e83c059f9c60f4f638f2831373cea561fe7311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Mon, 2 Nov 2026 07:16:12 +0100 Subject: [PATCH 5/8] ci(cli): shard unit tests and merge coverage (#1727) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runs the CLI unit tests in three parallel shards and merges their Vitest blob reports into one coverage report, instead of running the whole suite inside the `sentry` project job. Stacked on #1726. | Job | #1726 (`1b42251`, unsharded) | This PR (`1608137`) | | --- | --- | --- | | `Test` workflow | 10m18s | 5m48s | | `sentry` project job | 9m57s | 1m57s | | CLI unit tests | inside `sentry` | 2m24s / 3m48s / 3m33s (3 shards) | | CLI coverage merge | inside `sentry` | 0m25s | | CLI E2E (Node 24) | 4m30s | 3m30s | Runs: [#1726](https://github.com/getsentry/toolkit/actions/runs/41057051286), [this PR](https://github.com/getsentry/toolkit/actions/runs/41126676840). One run each; workflow time is first job start to last job done. The last job to finish was `CLI asset catalog (macOS arm64)`. Shards install only the CLI and its workspace dependencies: 1,508 → 774 packages (`pnpm install` vs `pnpm --filter 'sentry...' install`, both `--frozen-lockfile`), with install scripts kept. Sharing preparation through artifacts was no faster in a [one-off benchmark](https://github.com/getsentry/toolkit/actions/runs/41088577518) (4m41s vs 4m37s), so each job prepares its own inputs. Same tests and coverage scope; CodeQL is unchanged. Drops the `ci-projects` tests that only asserted `test.yml`'s own text or ran its bash from Node.
Recompute job times from any run ```sh gh run view -R getsentry/toolkit --json jobs --jq '.jobs[] | select(.name | test("^sentry$|^CLI")) | ((.completedAt | fromdate) - (.startedAt | fromdate)) as $s | "\($s / 60 | floor)m\($s % 60)s\t\(.name)"' ```
--- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude Opus 5.5 --- .github/workflows/test.yml | 97 +++++++++++++++-- docs/operations/github-actions.md | 24 +++-- packages/cli/package.json | 4 +- scripts/ci-projects.test.mjs | 174 +----------------------------- 4 files changed, 107 insertions(+), 192 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 004fbccb6..7ba31316f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -132,9 +132,6 @@ jobs: cache: pnpm - run: cp .env.example .env.local - run: pnpm install --frozen-lockfile - - name: Install zsh for CLI completion regression tests - if: matrix.name == 'sentry' - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends zsh - name: Build project dependencies env: PROJECT_NAME: ${{ matrix.name }} @@ -209,6 +206,84 @@ jobs: git diff --exit-code [[ -z "$(git status --porcelain --untracked-files=all)" ]] + cli-unit: + name: CLI unit tests (${{ matrix.shard }}/${{ strategy.job-total }}) + needs: discover-projects + if: contains(fromJSON(needs.discover-projects.outputs.matrix).include.*.name, 'sentry') + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + shard-count: ${{ strategy.job-total }} + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3] + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 + - uses: actions/setup-node@v7 + with: + node-version-file: .github/node-versions/22 + cache: pnpm + - run: pnpm --filter 'sentry...' install --frozen-lockfile + - name: Install zsh for CLI completion regression tests + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends zsh + - name: Build project dependencies + run: pnpm --filter 'sentry^...' --if-present run build + - run: pnpm --filter sentry run ci:prepare + - name: Test CLI shard + env: + SHARD: ${{ matrix.shard }} + SHARD_COUNT: ${{ strategy.job-total }} + run: pnpm --filter sentry run ci:test:unit --shard="$SHARD/$SHARD_COUNT" --reporter=default --reporter=blob --outputFile.blob="coverage/shard-$SHARD.json" + - uses: actions/upload-artifact@v7 + with: + name: cli-unit-coverage-${{ matrix.shard }} + path: packages/cli/coverage/shard-${{ matrix.shard }}.json + if-no-files-found: error + overwrite: true + retention-days: 7 + - name: Check generated files + run: | + git diff --exit-code + [[ -z "$(git status --porcelain --untracked-files=all)" ]] + + cli-coverage: + name: CLI coverage + needs: [discover-projects, cli-unit] + if: contains(fromJSON(needs.discover-projects.outputs.matrix).include.*.name, 'sentry') + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 + - uses: actions/setup-node@v7 + with: + node-version-file: .github/node-versions/22 + cache: pnpm + - run: pnpm --filter sentry install --frozen-lockfile --ignore-scripts + - uses: actions/download-artifact@v8 + with: + pattern: cli-unit-coverage-* + path: ${{ runner.temp }}/cli-unit-reports + merge-multiple: true + - name: Require every shard report + env: + SHARD_COUNT: ${{ needs.cli-unit.outputs.shard-count }} + run: | + test "$SHARD_COUNT" -gt 0 + for ((shard = 1; shard <= SHARD_COUNT; shard++)); do + test -s "$RUNNER_TEMP/cli-unit-reports/shard-$shard.json" + done + - name: Merge CLI coverage + run: pnpm --filter sentry exec vitest --merge-reports="$RUNNER_TEMP/cli-unit-reports" --coverage + - name: Verify coverage + run: test -s packages/cli/coverage/lcov.info + cli-e2e: name: CLI E2E (Node 24) needs: discover-projects @@ -322,6 +397,8 @@ jobs: project, npm-runtime, cli-asset-catalog, + cli-unit, + cli-coverage, cli-e2e, ] runs-on: ubuntu-latest @@ -337,24 +414,28 @@ jobs: RUNTIME_COUNT: ${{ needs.discover-projects.outputs.runtime-count }} ASSET_CATALOG_RESULT: ${{ needs.cli-asset-catalog.result }} ASSET_CATALOG_SELECTED: ${{ contains(fromJSON(needs.discover-projects.outputs.matrix).include.*.name, 'sentry') }} + CLI_UNIT_RESULT: ${{ needs.cli-unit.result }} + CLI_COVERAGE_RESULT: ${{ needs.cli-coverage.result }} CLI_E2E_RESULT: ${{ needs.cli-e2e.result }} run: | - [[ "$DISCOVER_RESULT" == success ]] - [[ "$INSTALL_RESULT" == success ]] - [[ "$QUALITY_RESULT" == success ]] + test "$DISCOVER_RESULT" = success + test "$INSTALL_RESULT" = success + test "$QUALITY_RESULT" = success expected=success if [[ "$PROJECT_COUNT" == 0 ]]; then expected=skipped fi - [[ "$PROJECT_RESULT" == "$expected" ]] + test "$PROJECT_RESULT" = "$expected" expected=success if [[ "$RUNTIME_COUNT" == 0 ]]; then expected=skipped fi - [[ "$RUNTIME_RESULT" == "$expected" ]] + test "$RUNTIME_RESULT" = "$expected" expected=success if [[ "$ASSET_CATALOG_SELECTED" == false ]]; then expected=skipped fi test "$ASSET_CATALOG_RESULT" = "$expected" + test "$CLI_UNIT_RESULT" = "$expected" + test "$CLI_COVERAGE_RESULT" = "$expected" test "$CLI_E2E_RESULT" = "$expected" diff --git a/docs/operations/github-actions.md b/docs/operations/github-actions.md index bf43f198a..a117509ad 100644 --- a/docs/operations/github-actions.md +++ b/docs/operations/github-actions.md @@ -26,14 +26,22 @@ from `.github/node-versions/`. The npm runtime job calls the self-contained `sentryCi.npmRuntime` build command once, then validates the same package on all three Node versions. -`CLI E2E (Node 24)` runs independently of the CLI project job. It builds the -standalone CLI on Node 22, then builds the npm bundle and tests both on Node 24. -Dependencies and generated sources are prepared once per job. The CLI project -job runs typechecks, unit tests, and coverage without rebuilding the standalone -or repeating generation. Public build and test commands still prepare their own -inputs for local use. The final `test` gate -requires E2E to pass whenever the CLI is selected, and requires it to be skipped -otherwise. +When the CLI is selected, its lint, typecheck, and policy checks run in the +project job. Unit tests run on Node 22 in three parallel shards, with the same +test directories and coverage as `pnpm --filter sentry test:unit`. Shards install +only the CLI and its workspace dependencies, retaining install scripts. Each +uploads a Vitest blob report; `CLI coverage` requires a report from every shard +before merging them into `packages/cli/coverage/lcov.info`. +The merge job installs the CLI dependency set without install scripts; it does +not build packages or rerun tests. + +`CLI E2E (Node 24)` runs independently of the unit tests and project checks. It +builds the standalone CLI on Node 22 and the npm bundle on Node 24, then tests +both on Node 24. The project, unit-test, and E2E jobs each build their workspace +dependencies and run `ci:prepare` once before using the prepared sources. Public +build and test commands still prepare their own inputs for local use. The final +`test` gate requires the unit shards, coverage merge, and E2E job whenever the +CLI is selected, and requires them to be skipped otherwise. ### deploy.yml diff --git a/packages/cli/package.json b/packages/cli/package.json index 71fc1aa0b..c42c0fa1b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -51,11 +51,9 @@ "packageManager": "pnpm@10.11.0", "sentryCi": { "build": false, - "coverage": "coverage/lcov.info", "e2e": false, "npmRuntime": "bundle", - "policy": "ci:policy", - "test": "ci:test:unit", + "test": false, "typecheck": "ci:typecheck" }, "scripts": { diff --git a/scripts/ci-projects.test.mjs b/scripts/ci-projects.test.mjs index 1f9e971d7..39e7a8fd3 100644 --- a/scripts/ci-projects.test.mjs +++ b/scripts/ci-projects.test.mjs @@ -1,6 +1,5 @@ import assert from "node:assert/strict"; -import { execFileSync, spawnSync } from "node:child_process"; -import { readFileSync } from "node:fs"; +import { execFileSync } from "node:child_process"; import { resolve } from "node:path"; import { describe, it } from "node:test"; @@ -35,25 +34,6 @@ describe("workspace CI selection", () => { assert.ok(names.includes("@sentry/mcp-core")); assert.ok(!names.includes("@sentry/mcp-smoke-tests")); assert.equal(new Set(names).size, names.length); - const docs = matrix.include.find(({ name }) => name === "sentry-cli-docs"); - assert.equal(docs.prepare, "ci:prepare"); - const docsScripts = JSON.parse( - readFileSync(resolve(root, "apps/cli-docs/package.json"), "utf8"), - ).scripts; - assert.match(docsScripts[docs.prepare], /sentry run generate:schema/); - assert.match(docsScripts[docs.prepare], /sentry run generate:docs/); - const cli = matrix.include.find(({ name }) => name === "sentry"); - assert.equal(cli.prepare, "ci:prepare"); - assert.equal(cli.build, ""); - assert.equal(cli.typecheck, "ci:typecheck"); - assert.equal(cli.test, "ci:test:unit"); - assert.equal(cli.policy, "ci:policy"); - assert.equal(cli.e2e, ""); - assert.equal(cli.coverage, "packages/cli/coverage/lcov.info"); - assert.equal( - matrix.include.find(({ name }) => name === "@sentry/mcp-core").junit, - "packages/mcp-core/tests.junit.xml", - ); }); it("runs only affected projects and their transitive consumers", () => { @@ -165,158 +145,6 @@ describe("workspace CI selection", () => { } }); - it("keeps a stable CI status and one dynamic job per package", () => { - const workflow = readFileSync( - resolve(root, ".github/workflows/test.yml"), - "utf8", - ); - assert.match(workflow, /merge_group:/); - assert.match(workflow, /node scripts\/ci-projects\.mjs --event/); - assert.match(workflow, /name: \$\{\{ matrix\.name \}\}/); - assert.match(workflow, /if: matrix\.prepare != ''/); - assert.match(workflow, /CHECK_SCRIPT: \$\{\{ matrix\.prepare \}\}/); - assert.doesNotMatch(workflow, /matrix\.name == 'sentry-cli-docs'/); - assert.match( - workflow, - /needs:\s*\[\s*discover-projects,\s*install,\s*quality,\s*project,\s*npm-runtime,\s*cli-asset-catalog,\s*cli-e2e,?\s*\]/, - ); - assert.match( - workflow, - /runtime-matrix: \$\{\{ steps\.projects\.outputs\.runtime-matrix \}\}/, - ); - assert.match( - workflow, - /matrix: \$\{\{ fromJSON\(needs\.discover-projects\.outputs\.runtime-matrix\) \}\}/, - ); - assert.match( - workflow, - /if: needs\.discover-projects\.outputs\.runtime-count != '0'/, - ); - assert.match(workflow, /Build and pack once on Node 22/); - assert.match(workflow, /Validate packaged runtime on Node 20/); - assert.match(workflow, /Validate packaged runtime on Node 22/); - assert.match(workflow, /Validate packaged runtime on Node 24/); - for (const major of [20, 22, 24]) { - const versionFile = `.github/node-versions/${major}`; - assert.ok(workflow.includes(`node-version-file: ${versionFile}`)); - assert.match( - readFileSync(resolve(root, versionFile), "utf8").trim(), - new RegExp(`^${major}\\.\\d+\\.\\d+$`), - ); - } - assert.match(workflow, /RUNTIME_RESULT.*needs\.npm-runtime\.result/); - assert.match(workflow, /name: Verify JUnit/); - assert.match(workflow, /name: Verify coverage/); - assert.match(workflow, /if: always\(\)/); - assert.doesNotMatch(workflow.split("\njobs:", 1)[0], /paths(?:-ignore)?:/); - }); - - it("runs CLI E2E independently after project discovery", () => { - const workflow = readFileSync( - resolve(root, ".github/workflows/test.yml"), - "utf8", - ); - const job = workflow.split("\n cli-e2e:\n")[1]?.split(/\n [\w-]+:\n/)[0]; - assert.ok(job); - assert.match(job, /\n needs: discover-projects\n/); - assert.match( - job, - /if: contains\(fromJSON\(needs\.discover-projects\.outputs\.matrix\)\.include\.\*\.name, 'sentry'\)/, - ); - }); - - it("requires native asset-catalog success when selected and a skip otherwise", () => { - const workflow = readFileSync( - resolve(root, ".github/workflows/test.yml"), - "utf8", - ); - const gate = workflow.split("\n test:\n")[1]; - assert.ok(gate); - assert.match( - gate, - /ASSET_CATALOG_RESULT: \$\{\{ needs\.cli-asset-catalog\.result \}\}/, - ); - assert.match( - gate, - /ASSET_CATALOG_SELECTED: \$\{\{ contains\(fromJSON\(needs\.discover-projects\.outputs\.matrix\)\.include\.\*\.name, 'sentry'\) \}\}/, - ); - const script = gate.split(" run: |\n")[1]; - assert.ok(script); - - for (const [selected, nativeResult, count, passes] of [ - [true, "success", "1", true], - [true, "failure", "1", false], - [true, "cancelled", "1", false], - [true, "skipped", "1", false], - [false, "skipped", "1", true], - [false, "success", "1", false], - [false, "failure", "1", false], - [false, "cancelled", "1", false], - [false, "skipped", "0", true], - ]) { - const result = spawnSync("bash", ["-euc", script], { - encoding: "utf8", - env: { - ...process.env, - DISCOVER_RESULT: "success", - INSTALL_RESULT: "success", - QUALITY_RESULT: "success", - PROJECT_COUNT: count, - PROJECT_RESULT: count === "0" ? "skipped" : "success", - RUNTIME_COUNT: count, - RUNTIME_RESULT: count === "0" ? "skipped" : "success", - ASSET_CATALOG_RESULT: nativeResult, - ASSET_CATALOG_SELECTED: String(selected), - CLI_E2E_RESULT: selected ? "success" : "skipped", - }, - }); - assert.equal( - result.status, - passes ? 0 : 1, - `selected=${selected}, native=${nativeResult}, projects=${count}`, - ); - } - }); - - it("requires CLI E2E success when selected and a skip otherwise", () => { - const workflow = readFileSync( - resolve(root, ".github/workflows/test.yml"), - "utf8", - ); - const gate = workflow.split("\n test:\n")[1]; - assert.ok(gate); - assert.match(gate, /CLI_E2E_RESULT: \$\{\{ needs\.cli-e2e\.result \}\}/); - const script = gate.split(" run: |\n")[1]; - assert.ok(script); - - for (const selected of [true, false]) { - const expected = selected ? "success" : "skipped"; - for (const status of ["success", "failure", "cancelled", "skipped"]) { - const result = spawnSync("bash", ["-euc", script], { - encoding: "utf8", - env: { - ...process.env, - DISCOVER_RESULT: "success", - INSTALL_RESULT: "success", - QUALITY_RESULT: "success", - PROJECT_COUNT: "1", - PROJECT_RESULT: "success", - RUNTIME_COUNT: selected ? "1" : "0", - RUNTIME_RESULT: expected, - ASSET_CATALOG_RESULT: expected, - ASSET_CATALOG_SELECTED: String(selected), - CLI_E2E_RESULT: status, - }, - }); - assert.equal( - result.status, - status === expected ? 0 : 1, - `selected=${selected}, e2e=${status}`, - ); - } - } - }); - it("requires safe metadata and a single owner of each specialized role", () => { assert.throws( () => From 0eb2b50db66e57f7ff0bae03a702cb556dcb00ed Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 2 Nov 2026 10:40:09 +0000 Subject: [PATCH 6/8] ci(cli): cap the E2E job at 10 minutes The job takes 3m28s-4m45s, the same range the unit shards cap at 10 minutes. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7ba31316f..40ce84d05 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -289,7 +289,7 @@ jobs: needs: discover-projects if: contains(fromJSON(needs.discover-projects.outputs.matrix).include.*.name, 'sentry') runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 10 env: SENTRY_CLIENT_ID: ${{ vars.SENTRY_CLIENT_ID }} steps: From 4dbe4f1a603825ba61cc2e67e1d6a07e214d0ca8 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 2 Nov 2026 18:54:53 +0000 Subject: [PATCH 7/8] ci(cli): run E2E in Build against the nightly build artifacts Build now also runs on pull requests, building only linux-x64 there. A new E2E job downloads the Linux binary, the npm package and the generated sources from that run and tests them without building, so pull requests, nightlies and releases test the artifacts Build ships. E2E replaces the Linux smoke test, and the CLI E2E job leaves test.yml. Build status fails unless the builds, the npm package tests and E2E pass. Every actions/ step in cli-build.yml uses its latest major tag. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/cli-build.yml | 134 ++++++++++++++++++++++-------- .github/workflows/test.yml | 44 ---------- docs/cli/README.md | 6 +- docs/operations/github-actions.md | 30 +++++-- scripts/workflow-policy.test.mjs | 4 + 5 files changed, 131 insertions(+), 87 deletions(-) diff --git a/.github/workflows/cli-build.yml b/.github/workflows/cli-build.yml index b6e25f656..d10981d5c 100644 --- a/.github/workflows/cli-build.yml +++ b/.github/workflows/cli-build.yml @@ -5,11 +5,12 @@ on: branches: - main - release/cli/** + pull_request: workflow_dispatch: concurrency: group: cli-build-${{ github.ref }} - cancel-in-progress: false + cancel-in-progress: ${{ github.event_name == 'pull_request' }} permissions: contents: read @@ -25,7 +26,7 @@ jobs: outputs: version: ${{ steps.version.outputs.version }} steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@v7 - name: Compute version from the commit timestamp id: version run: node scripts/cli-nightly-version.mjs @@ -39,21 +40,12 @@ jobs: strategy: fail-fast: false matrix: - include: - - target: darwin-arm64 - os: macos-latest - - target: darwin-x64 - os: ubuntu-latest - - target: linux-x64 - os: ubuntu-latest - - target: linux-arm64 - os: ubuntu-latest - - target: windows-x64 - os: ubuntu-latest + # Pull requests build the Linux binary the E2E tests run against. + include: ${{ fromJSON(github.event_name == 'pull_request' && '[{"target":"linux-x64","os":"ubuntu-latest"}]' || '[{"target":"darwin-arm64","os":"macos-latest"},{"target":"darwin-x64","os":"ubuntu-latest"},{"target":"linux-x64","os":"ubuntu-latest"},{"target":"linux-arm64","os":"ubuntu-latest"},{"target":"windows-x64","os":"ubuntu-latest"}]') }} steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@v7 - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@v7 with: node-version-file: .github/node-versions/22 cache: pnpm @@ -118,7 +110,7 @@ jobs: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: pnpm --filter sentry run build -- --target ${{ matrix.target }} - name: Smoke test - if: matrix.target == 'linux-x64' || matrix.target == 'darwin-arm64' + if: matrix.target == 'darwin-arm64' env: SENTRY_AUTH_TOKEN: "" SENTRY_TOKEN: "" @@ -140,14 +132,25 @@ jobs: shell: bash run: rcodesign verify packages/cli/dist-bin/sentry-darwin-arm64 - name: Upload binary artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@v7 with: name: sentry-${{ matrix.target }} path: | packages/cli/dist-bin/sentry-* !packages/cli/dist-bin/*.gz + - name: Upload generated CLI sources + if: matrix.target == 'linux-x64' + uses: actions/upload-artifact@v7 + with: + name: cli-generated + path: | + packages/cli/src/generated/ + packages/cli/src/sdk.generated.ts + packages/cli/src/sdk.generated.d.cts + if-no-files-found: error + retention-days: 7 - name: Upload compressed artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@v7 with: name: sentry-${{ matrix.target }}-gz path: packages/cli/dist-bin/*.gz @@ -166,7 +169,7 @@ jobs: from-version: ${{ steps.generate.outputs.from-version }} steps: - name: Download binaries and compressed artifacts - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@v8 with: pattern: sentry-*-* path: new-binaries @@ -185,7 +188,7 @@ jobs: patches-dir: patches - name: Upload patches if: steps.generate.outputs.has-patches == 'true' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@v7 with: name: sentry-patches path: patches/*.patch @@ -204,13 +207,13 @@ jobs: packages: write steps: - name: Download compressed binaries - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@v8 with: pattern: sentry-*-gz path: artifacts merge-multiple: true - name: Download binaries for patch integrity annotations - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@v8 with: pattern: sentry-*-* path: binaries @@ -218,7 +221,7 @@ jobs: - name: Download optional delta patches id: patches continue-on-error: true - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@v8 with: name: sentry-patches path: patches @@ -260,9 +263,9 @@ jobs: timeout-minutes: 20 environment: ${{ github.ref == 'refs/heads/main' && 'production' || startsWith(github.ref, 'refs/heads/release/cli/') && 'cli-release' || '' }} steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@v7 - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@v7 with: node-version-file: .github/node-versions/22 cache: pnpm @@ -282,7 +285,7 @@ jobs: working-directory: packages/cli run: npm pack - name: Upload npm artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@v7 with: name: npm-package path: packages/cli/*.tgz @@ -295,7 +298,7 @@ jobs: working-directory: packages/mcp-server run: npm pack - name: Upload mcp-server npm artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@v7 with: name: mcp-server-npm-package path: packages/mcp-server/*.tgz @@ -310,12 +313,12 @@ jobs: matrix: node: [20, 22, 24] steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 with: node-version-file: .github/node-versions/${{ matrix.node }} - name: Download npm artifact - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@v8 with: name: npm-package path: packages/cli @@ -330,8 +333,53 @@ jobs: npm install --ignore-scripts --prefix "$RUNNER_TEMP/npm-smoke" "$GITHUB_WORKSPACE/$package_file" node "$RUNNER_TEMP/npm-smoke/node_modules/sentry/dist/bin.cjs" --help + test-e2e: + name: E2E Tests (Node 24) + needs: [build-binary, build-npm] + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 + - uses: actions/setup-node@v7 + with: + node-version-file: .github/node-versions/24 + cache: pnpm + - run: pnpm --filter 'sentry...' install --frozen-lockfile + # The tests import CLI sources that read the generated files. + - name: Download generated CLI sources + uses: actions/download-artifact@v8 + with: + name: cli-generated + path: packages/cli/src + - name: Download Linux binary + uses: actions/download-artifact@v8 + with: + name: sentry-linux-x64 + path: packages/cli/dist-bin + - name: Download npm package + uses: actions/download-artifact@v8 + with: + name: npm-package + path: ${{ runner.temp }}/npm-package + - name: Unpack the binary and npm bundle + run: | + chmod +x packages/cli/dist-bin/sentry-linux-x64 + tar -xzf "$RUNNER_TEMP"/npm-package/sentry-*.tgz -C packages/cli --strip-components=1 package/dist + - name: Run E2E tests + env: + SENTRY_CLI_BINARY: ${{ github.workspace }}/packages/cli/dist-bin/sentry-linux-x64 + run: | + test -x "$SENTRY_CLI_BINARY" + test -s packages/cli/dist/bin.cjs + test -s packages/cli/dist/index.cjs + pnpm --filter sentry exec vitest run test/e2e + build-docs: name: Build Docs + if: github.event_name != 'pull_request' needs: build-binary runs-on: ubuntu-latest timeout-minutes: 30 @@ -340,9 +388,9 @@ jobs: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} PUBLIC_SENTRY_ENVIRONMENT: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/cli/')) && 'production' || 'development' }} steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@v7 - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@v7 with: node-version-file: .github/node-versions/24 cache: pnpm @@ -360,7 +408,7 @@ jobs: - name: Build docs run: pnpm --filter sentry-cli-docs run build - name: Download Linux binary - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@v8 with: name: sentry-linux-x64 path: dist-bin @@ -387,7 +435,25 @@ jobs: 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@v7 with: name: vercel path: vercel.zip + + build-status: + name: Build status + if: always() + needs: [build-binary, build-npm, test-npm, test-e2e] + runs-on: ubuntu-latest + permissions: {} + steps: + - name: Require the CLI build and E2E tests + env: + RESULTS: ${{ join(needs.*.result, ' ') }} + run: | + for result in $RESULTS; do + if [[ "$result" != success ]]; then + echo "::error::A required Build job finished with: $result" + exit 1 + fi + done diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 40ce84d05..613573107 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -284,47 +284,6 @@ jobs: - name: Verify coverage run: test -s packages/cli/coverage/lcov.info - cli-e2e: - name: CLI E2E (Node 24) - needs: discover-projects - if: contains(fromJSON(needs.discover-projects.outputs.matrix).include.*.name, 'sentry') - runs-on: ubuntu-latest - timeout-minutes: 10 - env: - SENTRY_CLIENT_ID: ${{ vars.SENTRY_CLIENT_ID }} - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 - - uses: actions/setup-node@v7 - with: - node-version-file: .github/node-versions/22 - cache: pnpm - - run: pnpm install --frozen-lockfile - - name: Build project dependencies - run: pnpm --filter 'sentry^...' --if-present run build - - run: pnpm --filter sentry run ci:prepare - - name: Build standalone CLI - run: pnpm --filter sentry run tsx script/build.ts --single - - uses: actions/setup-node@v7 - with: - node-version-file: .github/node-versions/24 - - name: Build npm bundle - run: pnpm --filter sentry run tsx script/bundle.ts - - name: Run E2E tests on Node 24 - env: - SENTRY_CLI_BINARY: ${{ github.workspace }}/packages/cli/dist-bin/sentry-linux-x64 - run: | - test -x "$SENTRY_CLI_BINARY" - test -s packages/cli/dist/bin.cjs - test -s packages/cli/dist/index.cjs - pnpm --filter sentry exec vitest run test/e2e - - name: Check generated files - run: | - git diff --exit-code - [[ -z "$(git status --porcelain --untracked-files=all)" ]] - npm-runtime: name: NPM package (${{ matrix.name }}) needs: discover-projects @@ -399,7 +358,6 @@ jobs: cli-asset-catalog, cli-unit, cli-coverage, - cli-e2e, ] runs-on: ubuntu-latest steps: @@ -416,7 +374,6 @@ jobs: ASSET_CATALOG_SELECTED: ${{ contains(fromJSON(needs.discover-projects.outputs.matrix).include.*.name, 'sentry') }} CLI_UNIT_RESULT: ${{ needs.cli-unit.result }} CLI_COVERAGE_RESULT: ${{ needs.cli-coverage.result }} - CLI_E2E_RESULT: ${{ needs.cli-e2e.result }} run: | test "$DISCOVER_RESULT" = success test "$INSTALL_RESULT" = success @@ -438,4 +395,3 @@ jobs: test "$ASSET_CATALOG_RESULT" = "$expected" test "$CLI_UNIT_RESULT" = "$expected" test "$CLI_COVERAGE_RESULT" = "$expected" - test "$CLI_E2E_RESULT" = "$expected" diff --git a/docs/cli/README.md b/docs/cli/README.md index 2cdffa6f9..4249c7403 100644 --- a/docs/cli/README.md +++ b/docs/cli/README.md @@ -14,8 +14,10 @@ CLI releases build it from that workspace. Start here for repository guides: [command conventions](../../packages/cli/CONTRIBUTING.md) — implementation details and command design. - [CLI build workflow](../../.github/workflows/cli-build.yml) and - [release configuration](../../.craft.yml) — binaries, npm packaging, and - release-gated website artifacts. + [release configuration](../../.craft.yml) — binaries, npm packaging, + release-gated website artifacts, and the E2E tests that run against those + artifacts on pull requests, `main`, and release branches. A failing E2E run on + `main` fails the Build run, which the nightly watchdog below reports. - [CLI nightly watchdog](../../.github/workflows/cli-nightly-watchdog.yml) — hourly and after each main Build, checks for stuck or failed Build runs and a GHCR `nightly` that lags main. It keeps one open alert issue while unhealthy, diff --git a/docs/operations/github-actions.md b/docs/operations/github-actions.md index 188d42427..628a1b804 100644 --- a/docs/operations/github-actions.md +++ b/docs/operations/github-actions.md @@ -36,13 +36,26 @@ before merging them into `packages/cli/coverage/lcov.info`. The merge job installs the CLI dependency set without install scripts; it does not build packages or rerun tests. -`CLI E2E (Node 24)` runs independently of the unit tests and project checks. It -builds the standalone CLI on Node 22 and the npm bundle on Node 24, then tests -both on Node 24. The project, unit-test, and E2E jobs each build their workspace -dependencies and run `ci:prepare` once before using the prepared sources. Public -build and test commands still prepare their own inputs for local use. The final -`test` gate requires the unit shards, coverage merge, and E2E job whenever the -CLI is selected, and requires them to be skipped otherwise. +The project and unit-test jobs each build their workspace dependencies and run +`ci:prepare` once before using the prepared sources. Public build and test +commands still prepare their own inputs for local use. The final `test` gate +requires the unit shards and coverage merge whenever the CLI is selected, and +requires them to be skipped otherwise. The CLI E2E tests run in +[cli-build.yml](#cli-buildyml), against the artifacts that workflow builds. + +### cli-build.yml + +Runs on pull requests, pushes to `main` and `release/cli/**`, and manual +dispatch. It builds the CLI artifacts that ship: `Build Binary` builds every +target on `main` and release branches, and only `linux-x64` on pull requests. +`Build npm Package` packs the npm package. `E2E Tests (Node 24)` downloads the +Linux binary, the npm package, and the generated sources from those jobs and +runs `test/e2e` against them without building anything. `Build status` fails +unless the builds, the npm package tests, and the E2E tests passed. + +Nightly delta patches and the GHCR nightly publish only on pushes to `main`. +Releases take this workflow's artifacts (see `.craft.yml`), so a release ships +the binaries and package that its E2E run tested. ### deploy.yml @@ -164,6 +177,9 @@ project: A new workflow fits only when the matrix job cannot run the check: another operating system, a deployment, secrets, or pull request comments and labels. +The CLI's E2E tests are the exception: they must run against the binaries and +npm package that `cli-build.yml` builds for nightlies and releases, so +`packages/cli` sets `sentryCi.e2e` to `false` and that workflow runs them. `scripts/workflow-policy.test.mjs` fails for a workflow that runs on pull requests unless its list names that workflow and the reason. diff --git a/scripts/workflow-policy.test.mjs b/scripts/workflow-policy.test.mjs index c592b7513..854c35b4d 100644 --- a/scripts/workflow-policy.test.mjs +++ b/scripts/workflow-policy.test.mjs @@ -8,6 +8,10 @@ import { describe, it } from "node:test"; // per-project matrix. See docs/operations/github-actions.md#package-ci-checks. const PULL_REQUEST_WORKFLOWS = new Map([ ["test.yml", "Builds and runs the per-project matrix."], + [ + "cli-build.yml", + "Builds the CLI release artifacts and runs the CLI E2E tests against them.", + ], ["eval.yml", "Runs AI evaluations against model providers."], [ "install-scripts.yml", From 029c6db47cd0300bdb67fe743752fa5b073d1197 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 2 Nov 2026 19:04:16 +0000 Subject: [PATCH 8/8] ci(cli): give the Build status job a timeout Every cli-build.yml job needs timeout-minutes, which cli-nightly-watchdog.test.mjs checks. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/cli-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cli-build.yml b/.github/workflows/cli-build.yml index d10981d5c..84cd6ac7f 100644 --- a/.github/workflows/cli-build.yml +++ b/.github/workflows/cli-build.yml @@ -445,6 +445,7 @@ jobs: if: always() needs: [build-binary, build-npm, test-npm, test-e2e] runs-on: ubuntu-latest + timeout-minutes: 5 permissions: {} steps: - name: Require the CLI build and E2E tests