Skip to content

feat: Pack CLI as .NET tool in NuGet packages #1088

feat: Pack CLI as .NET tool in NuGet packages

feat: Pack CLI as .NET tool in NuGet packages #1088

Workflow file for this run

name: Build
on:
push:
branches: [main, release/**]
pull_request:
workflow_call:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
changes:
name: Detect Changes
runs-on: ubuntu-latest
permissions:
pull-requests: read
outputs:
skill: ${{ steps.filter.outputs.skill == 'true' || startsWith(github.ref, 'refs/heads/release/') }}
code: ${{ steps.filter.outputs.code == 'true' || startsWith(github.ref, 'refs/heads/release/') }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: filter
with:
filters: |
skill:
- 'src/**'
- 'docs/**'
- 'plugins/**'
- 'script/generate-skill.ts'
code:
- 'src/**'
- 'test/**'
- 'script/**'
- 'patches/**'
- 'docs/**'
- 'plugins/**'
- 'package.json'
- 'bun.lock'
- '.github/workflows/ci.yml'
check-skill:
name: Check SKILL.md
needs: [changes]
if: needs.changes.outputs.skill == 'true'
runs-on: ubuntu-latest
steps:
- name: Get auth token
id: token
# Fork PRs don't have access to secrets, so this step is skipped
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request'
uses: actions/create-github-app-token@v2.2.1
with:
app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }}
private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }}
- uses: actions/checkout@v4
with:
token: ${{ steps.token.outputs.token || github.token }}
ref: ${{ github.head_ref || github.ref_name }}
- uses: oven-sh/setup-bun@v2
- uses: actions/cache@v4
id: cache
with:
path: node_modules
key: node-modules-${{ hashFiles('bun.lock', 'patches/**') }}
- if: steps.cache.outputs.cache-hit != 'true'
run: bun install --frozen-lockfile
- name: Check SKILL.md
id: check
run: bun run check:skill
continue-on-error: true
- name: Auto-commit regenerated SKILL.md
if: steps.check.outcome == 'failure' && steps.token.outcome == 'success'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add plugins/sentry-cli/skills/sentry-cli/SKILL.md
git commit -m "chore: regenerate SKILL.md"
git push
- name: Fail for fork PRs with stale SKILL.md
if: steps.check.outcome == 'failure' && steps.token.outcome != 'success'
run: |
echo "::error::SKILL.md is out of date. Run 'bun run generate:skill' locally and commit the result."
exit 1
lint:
name: Lint & Typecheck
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- uses: actions/cache@v4
id: cache
with:
path: node_modules
key: node-modules-${{ hashFiles('bun.lock', 'patches/**') }}
- if: steps.cache.outputs.cache-hit != 'true'
run: bun install --frozen-lockfile
- run: bun run lint
- run: bun run typecheck
- run: bun run check:deps
lint-dotnet:
name: .NET Lint
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
- run: dotnet format src/dotnet/Sentry.Cli.slnx --verify-no-changes
test-unit:
name: Unit Tests
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
pull-requests: write
statuses: write
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- uses: actions/cache@v4
id: cache
with:
path: node_modules
key: node-modules-${{ hashFiles('bun.lock', 'patches/**') }}
- if: steps.cache.outputs.cache-hit != 'true'
run: bun install --frozen-lockfile
- name: Unit Tests
run: bun run test:unit
- name: Upload Coverage
uses: getsentry/codecov-action@main
with:
token: ${{ secrets.GITHUB_TOKEN }}
post-pr-comment: true
build-binary:
name: Build Binary (${{ matrix.target }})
needs: [lint, test-unit]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
# Native builds (can run smoke test)
- target: darwin-arm64
os: macos-latest
can-test: true
- target: linux-x64
os: ubuntu-latest
can-test: true
- target: windows-x64
os: windows-latest
can-test: true
# Cross-compiled builds (cannot run smoke test)
- target: darwin-x64
os: macos-latest
can-test: false
- target: linux-arm64
os: ubuntu-latest
can-test: false
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- uses: actions/cache@v4
id: cache
with:
path: node_modules
key: node-modules-${{ matrix.os }}-${{ hashFiles('bun.lock', 'patches/**') }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
shell: bash
run: |
# Retry logic for Windows Bun patch bug (ENOTEMPTY errors)
for i in 1 2 3; do
if bun install --frozen-lockfile; then
exit 0
fi
echo "Attempt $i failed, clearing Bun cache and retrying..."
bun pm cache rm 2>/dev/null || true
done
echo "All install attempts failed"
exit 1
- name: Build
env:
SENTRY_CLIENT_ID: ${{ vars.SENTRY_CLIENT_ID }}
run: bun run build --target ${{ matrix.target }}
- name: Smoke test
if: matrix.can-test
shell: bash
run: |
if [[ "${{ matrix.target }}" == "windows-x64" ]]; then
./dist-bin/sentry-windows-x64.exe --help
else
./dist-bin/sentry-${{ matrix.target }} --help
fi
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: sentry-${{ matrix.target }}
path: dist-bin/sentry-*
build-nuget:
name: Pack NuGet (${{ matrix.name }})
needs: [changes, lint-dotnet, build-binary]
if: needs.changes.outputs.code == 'true'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: linux-x64
os: ubuntu-latest
pack-target: linux-x64
pack-agnostic: true
- name: linux-arm64
os: ubuntu-24.04-arm
pack-target: linux-arm64
pack-agnostic: false
- name: macos-arm64
os: macos-latest
pack-target: darwin-arm64
pack-agnostic: false
- name: macos-x64
os: macos-latest
pack-target: darwin-x64
pack-agnostic: false
- name: windows-x64
os: windows-latest
pack-target: windows-x64
pack-agnostic: false
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- uses: actions/cache@v4
id: cache
with:
path: node_modules
key: node-modules-${{ matrix.os }}-${{ hashFiles('bun.lock', 'patches/**') }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
shell: bash
run: bun install --frozen-lockfile
- uses: actions/setup-dotnet@v4
- name: Download binary
uses: actions/download-artifact@v4
with:
name: sentry-${{ matrix.pack-target }}
path: dist-bin
- name: Make binary executable
if: runner.os != 'Windows'
run: chmod +x dist-bin/sentry-*
- name: Pack NuGet packages
run: bun run script/pack.ts --target ${{ matrix.pack-target }}
- name: Pack agnostic NuGet packages
if: matrix.pack-agnostic
run: bun run script/pack.ts --no-clean --agnostic
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: nuget-${{ matrix.name }}
path: dist-pkg/*.nupkg
test-e2e:
name: E2E Tests
needs: [build-binary]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- uses: actions/cache@v4
id: cache
with:
path: node_modules
key: node-modules-${{ hashFiles('bun.lock', 'patches/**') }}
- if: steps.cache.outputs.cache-hit != 'true'
run: bun install --frozen-lockfile
- name: Download Linux binary
uses: actions/download-artifact@v4
with:
name: sentry-linux-x64
path: dist-bin
- name: Make binary executable
run: chmod +x dist-bin/sentry-linux-x64
- name: E2E Tests
env:
SENTRY_CLI_BINARY: ${{ github.workspace }}/dist-bin/sentry-linux-x64
run: bun run test:e2e
test-dotnet:
name: .NET Tests (${{ matrix.target }})
needs: [changes, lint-dotnet, build-binary]
if: needs.changes.outputs.code == 'true'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- target: darwin-arm64
os: macos-latest
- target: linux-x64
os: ubuntu-latest
- target: windows-x64
os: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
- name: Download binary
uses: actions/download-artifact@v4
with:
name: sentry-${{ matrix.target }}
path: dist-bin
- name: Make binary executable
if: runner.os != 'Windows'
run: chmod +x dist-bin/sentry-${{ matrix.target }}
- name: .NET Tests
run: dotnet test --project src/dotnet/Sentry.Cli.Tests/Sentry.Cli.Tests.csproj
build-npm:
name: Build npm Package (Node ${{ matrix.node }})
needs: [lint, test-unit]
runs-on: ubuntu-latest
environment: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && 'production' || '' }}
strategy:
fail-fast: false
matrix:
node: ["22", "24"]
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- uses: actions/cache@v4
id: cache
with:
path: node_modules
key: node-modules-${{ hashFiles('bun.lock', 'patches/**') }}
- if: steps.cache.outputs.cache-hit != 'true'
run: bun install --frozen-lockfile
- name: Bundle
env:
SENTRY_CLIENT_ID: ${{ vars.SENTRY_CLIENT_ID }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: bun run bundle
- name: Smoke test (Node.js)
run: node dist/bin.cjs --help
- run: npm pack
- name: Upload artifact
if: matrix.node == '22'
uses: actions/upload-artifact@v4
with:
name: npm-package
path: "*.tgz"
build-docs:
name: Build Docs
needs: [lint]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Build Docs
working-directory: docs
run: |
bun install --frozen-lockfile
bun run build
- name: Package Docs
run: |
cp .nojekyll docs/dist/
cd docs/dist && zip -r ../../gh-pages.zip .
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: gh-pages
path: gh-pages.zip
ci-status:
name: CI Status
if: always()
needs: [changes, check-skill, build-binary, build-npm, build-docs, test-e2e, test-dotnet, build-nuget]
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Check CI status
run: |
# Check for explicit failures or cancellations in all jobs
results="${{ needs.check-skill.result }} ${{ needs.build-binary.result }} ${{ needs.build-npm.result }} ${{ needs.build-docs.result }} ${{ needs.test-e2e.result }} ${{ needs.test-dotnet.result }} ${{ needs.build-nuget.result }}"
for result in $results; do
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "::error::CI failed"
exit 1
fi
done
# Detect upstream failures: if changes were detected but jobs were skipped,
# it means an upstream job failed (skipped jobs cascade to dependents)
if [[ "${{ needs.changes.outputs.code }}" == "true" && "${{ needs.test-e2e.result }}" == "skipped" ]]; then
echo "::error::CI failed - upstream job failed causing test-e2e to be skipped"
exit 1
fi
if [[ "${{ needs.changes.outputs.code }}" == "true" && "${{ needs.test-dotnet.result }}" == "skipped" ]]; then
echo "::error::CI failed - upstream job failed causing test-dotnet to be skipped"
exit 1
fi
if [[ "${{ needs.changes.outputs.code }}" == "true" && "${{ needs.build-nuget.result }}" == "skipped" ]]; then
echo "::error::CI failed - upstream job failed causing build-nuget to be skipped"
exit 1
fi
if [[ "${{ needs.changes.outputs.skill }}" == "true" && "${{ needs.check-skill.result }}" == "skipped" ]]; then
echo "::error::CI failed - upstream job failed causing check-skill to be skipped"
exit 1
fi
echo "CI passed"