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 47402b236..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 != '' @@ -214,6 +209,47 @@ 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: + 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 @@ -227,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 }} @@ -254,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 }} @@ -290,6 +322,7 @@ jobs: project, npm-runtime, cli-asset-catalog, + cli-e2e, ] runs-on: ubuntu-latest steps: @@ -304,6 +337,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 +356,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..bf43f198a 100644 --- a/docs/operations/github-actions.md +++ b/docs/operations/github-actions.md @@ -21,6 +21,20 @@ 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 +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. + ### 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..71fc1aa0b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -50,9 +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", @@ -65,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", @@ -93,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 cc1396274..1f9e971d7 100644 --- a/scripts/ci-projects.test.mjs +++ b/scripts/ci-projects.test.mjs @@ -43,8 +43,12 @@ 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.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, "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 +178,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, @@ -192,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/); @@ -202,6 +211,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 +267,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 +278,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( () =>