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 afd8c9ef9..fce48a79e 100644 --- a/.github/workflows/cli-build.yml +++ b/.github/workflows/cli-build.yml @@ -5,20 +5,18 @@ 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 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: @@ -28,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 @@ -42,23 +40,14 @@ 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: ${{ env.NODE_VERSION_22 }} + node-version-file: .github/node-versions/22 cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile @@ -114,14 +103,15 @@ jobs: fi - name: Build env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - RELEASE_BUILD: "1" + # Pull request builds skip the sourcemap upload and the .gz copies. + SENTRY_AUTH_TOKEN: ${{ github.event_name != 'pull_request' && secrets.SENTRY_AUTH_TOKEN || '' }} + RELEASE_BUILD: ${{ github.event_name != 'pull_request' && '1' || '' }} FOSSILIZE_SIGN: ${{ (github.ref_name == 'main' || startsWith(github.ref_name, 'release/cli/')) && 'y' || 'n' }} APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }} 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: "" @@ -143,14 +133,26 @@ 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 + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v7 with: name: sentry-${{ matrix.target }}-gz path: packages/cli/dist-bin/*.gz @@ -169,7 +171,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 @@ -188,7 +190,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 @@ -207,13 +209,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 @@ -221,7 +223,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 @@ -263,29 +265,23 @@ 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: ${{ env.NODE_VERSION_22 }} + node-version-file: .github/node-versions/22 cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile - name: Bundle env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_AUTH_TOKEN: ${{ github.event_name != 'pull_request' && secrets.SENTRY_AUTH_TOKEN || '' }} run: pnpm --filter sentry run bundle - - name: Smoke test - env: - SENTRY_AUTH_TOKEN: "" - SENTRY_TOKEN: "" - SENTRY_CONFIG_DIR: ${{ runner.temp }}/.sentry-smoke - run: node packages/cli/dist/bin.cjs --help - name: Pack 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 @@ -298,7 +294,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 @@ -313,28 +309,71 @@ 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: ${{ 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 + uses: actions/download-artifact@v8 with: name: npm-package - path: packages/cli + path: ${{ runner.temp }}/npm-package - name: Test packaged CLI env: - SENTRY_AUTH_TOKEN: "" - SENTRY_TOKEN: "" - SENTRY_CONFIG_DIR: ${{ runner.temp }}/.sentry-smoke + RUNTIME_BIN: sentry + RUNTIME_PACKAGE: sentry run: | - package_file=$(find packages/cli -maxdepth 1 -name 'sentry-*.tgz' -print -quit) - test -n "$package_file" - 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 + packages=("$RUNNER_TEMP"/npm-package/sentry-*.tgz) + [[ "${#packages[@]}" == 1 ]] + RUNTIME_ARCHIVE="${packages[0]}" bash scripts/validate-npm-runtime.sh + + 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 @@ -343,11 +382,11 @@ 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: ${{ env.NODE_VERSION_24 }} + node-version-file: .github/node-versions/24 cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile @@ -363,7 +402,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 @@ -390,7 +429,26 @@ 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 + timeout-minutes: 5 + 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 47402b236..3c34d83a0 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,19 +122,16 @@ 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 - - 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 }} @@ -192,17 +184,16 @@ 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 != '' env: PROJECT_NAME: ${{ matrix.name }} CHECK_SCRIPT: ${{ matrix.e2e }} - SENTRY_CLI_BINARY: ${{ github.workspace }}/packages/cli/dist-bin/sentry-linux-x64 run: pnpm --filter "$PROJECT_NAME" run "$CHECK_SCRIPT" - name: Verify coverage if: matrix.coverage != '' @@ -214,6 +205,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 + npm-runtime: name: NPM package (${{ matrix.name }}) needs: discover-projects @@ -227,19 +296,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 +319,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 +355,8 @@ jobs: project, npm-runtime, cli-asset-catalog, + cli-unit, + cli-coverage, ] runs-on: ubuntu-latest steps: @@ -304,22 +371,26 @@ 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 }} 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 - [[ "$ASSET_CATALOG_RESULT" == "$expected" ]] + test "$ASSET_CATALOG_RESULT" = "$expected" + test "$CLI_UNIT_RESULT" = "$expected" + test "$CLI_COVERAGE_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 69d9dc84a..4a58b6995 100644 --- a/docs/operations/github-actions.md +++ b/docs/operations/github-actions.md @@ -22,6 +22,44 @@ Package-specific checks and exceptions live in `package.json#sentryCi`; see [Package CI checks](#package-ci-checks). 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. + +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. + +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. +Pull request builds get no `SENTRY_AUTH_TOKEN`, so they upload no sourcemaps, +and they skip the compressed release copies. `Build npm Package` packs the npm package, and `Test npm Package` validates that +package on Node 20, 22, and 24 with `scripts/validate-npm-runtime.sh`. +`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 Runs after a successful `Test` push run on `main`. Checks out the tested commit @@ -142,6 +180,10 @@ 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 and npm package checks 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`, leaves +`sentryCi.npmRuntime` unset, 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/packages/cli/package.json b/packages/cli/package.json index ebf27bbad..4f766c19c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -51,9 +51,10 @@ }, "packageManager": "pnpm@10.11.0", "sentryCi": { - "coverage": "coverage/lcov.info", - "npmRuntime": "bundle", - "policy": "ci:policy" + "build": false, + "e2e": false, + "test": false, + "typecheck": "ci:typecheck" }, "scripts": { "tsx": "tsx --env-file-if-exists=.env.local --import ./script/require-shim.mjs", @@ -66,7 +67,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", @@ -95,6 +96,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..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,21 +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.policy, "ci:policy"); - assert.equal(cli.e2e, "test: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", () => { @@ -161,99 +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*\]/, - ); - 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/); - 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"/); - 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("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), - }, - }); - assert.equal( - result.status, - passes ? 0 : 1, - `selected=${selected}, native=${nativeResult}, projects=${count}`, - ); - } - }); - it("requires safe metadata and a single owner of each specialized role", () => { assert.throws( () => 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",