fix(ci): fix set-commits --auto and add checkout/URL to sentry-release workflow #2978
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build | |
| on: | |
| push: | |
| branches: [main, release/**] | |
| pull_request: | |
| workflow_call: | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| # packages:write is needed for publish-nightly to push to GHCR | |
| # issues:write is needed for generate-patches to file issues on failure | |
| permissions: | |
| contents: read | |
| issues: write | |
| packages: write | |
| env: | |
| # Commit timestamp used for deterministic nightly version strings. | |
| # Defined at workflow level so build-binary and publish-nightly always agree. | |
| COMMIT_TIMESTAMP: ${{ github.event.head_commit.timestamp }} | |
| 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/') }} | |
| build-targets: ${{ steps.targets.outputs.matrix }} | |
| nightly-version: ${{ steps.nightly.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: dorny/paths-filter@v4 | |
| id: filter | |
| with: | |
| filters: | | |
| skill: | |
| - 'src/**' | |
| - 'docs/**' | |
| - 'plugins/**' | |
| - 'script/generate-skill.ts' | |
| - 'script/eval-skill.ts' | |
| - 'test/skill-eval/**' | |
| code: | |
| - 'src/**' | |
| - 'test/**' | |
| - 'script/**' | |
| - 'patches/**' | |
| - 'docs/**' | |
| - 'plugins/**' | |
| - 'package.json' | |
| - 'bun.lock' | |
| - '.github/workflows/ci.yml' | |
| - name: Compute build matrix | |
| id: targets | |
| run: | | |
| { | |
| echo 'matrix<<MATRIX_EOF' | |
| if [[ "${{ github.event_name }}" == "pull_request" ]]; then | |
| # PRs only need linux-x64 for smoke test and e2e — skip macOS/Windows | |
| echo '{"include":[ | |
| {"target":"linux-x64", "os":"ubuntu-latest", "can-test":true} | |
| ]}' | |
| else | |
| # main, release/**, workflow_call: full cross-platform matrix | |
| echo '{"include":[ | |
| {"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}, | |
| {"target":"darwin-x64", "os":"macos-latest", "can-test":false}, | |
| {"target":"linux-arm64", "os":"ubuntu-latest", "can-test":false} | |
| ]}' | |
| fi | |
| echo 'MATRIX_EOF' | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Compute nightly version | |
| id: nightly | |
| if: github.ref == 'refs/heads/main' && github.event_name == 'push' | |
| run: | | |
| TS=$(date -d "$COMMIT_TIMESTAMP" +%s) | |
| CURRENT=$(jq -r .version package.json) | |
| VERSION=$(echo "$CURRENT" | sed "s/-dev\.[0-9]*$/-dev.${TS}/") | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "Nightly version: ${VERSION}" | |
| check-skill: | |
| name: Check skill files | |
| needs: [changes] | |
| if: needs.changes.outputs.skill == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Get auth token | |
| id: token | |
| # Fork PRs don't have access to secrets, so this step is skipped | |
| if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request' | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} | |
| private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} | |
| - uses: actions/checkout@v6 | |
| with: | |
| token: ${{ steps.token.outputs.token || github.token }} | |
| ref: ${{ github.head_ref || github.ref_name }} | |
| - uses: oven-sh/setup-bun@v2 | |
| - uses: actions/cache@v5 | |
| 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: Generate API Schema | |
| run: bun run generate:schema | |
| - name: Check skill files | |
| id: check-skill | |
| run: bun run check:skill | |
| continue-on-error: true | |
| - name: Check command docs | |
| id: check-docs | |
| run: bun run check:command-docs | |
| continue-on-error: true | |
| - name: Auto-commit regenerated files | |
| if: (steps.check-skill.outcome == 'failure' || steps.check-docs.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/ docs/public/.well-known/skills/index.json docs/src/content/docs/commands/ | |
| git diff --cached --quiet || (git commit -m "chore: regenerate skill files and command docs" && git push) | |
| - name: Fail for fork PRs with stale generated files | |
| if: (steps.check-skill.outcome == 'failure' || steps.check-docs.outcome == 'failure') && steps.token.outcome != 'success' | |
| run: | | |
| echo "::error::Generated files are out of date. Run 'bun run generate:skill' and 'bun run generate:command-docs' locally and commit the result." | |
| exit 1 | |
| eval-skill: | |
| name: Eval SKILL.md | |
| needs: [changes] | |
| if: needs.changes.outputs.skill == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| # For fork PRs: check if eval has already passed via commit status | |
| - name: Detect fork | |
| id: detect-fork | |
| run: | | |
| if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]]; then | |
| echo "is_fork=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Check fork eval status | |
| if: steps.detect-fork.outputs.is_fork == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| SHA="${{ github.event.pull_request.head.sha }}" | |
| STATUS=$(gh api "repos/${{ github.repository }}/commits/$SHA/statuses" \ | |
| --jq '[.[] | select(.context == "eval-skill/fork")] | first | .state // "none"') | |
| if [[ "$STATUS" != "success" ]]; then | |
| echo "::error::Fork PR modifies skill files but eval has not passed for commit $SHA." | |
| echo "::error::A maintainer must review the code and add the 'eval-skill' label." | |
| exit 1 | |
| fi | |
| echo "Fork eval passed for $SHA" | |
| # For internal PRs: run the eval directly | |
| - uses: actions/checkout@v6 | |
| if: steps.detect-fork.outputs.is_fork != 'true' | |
| - uses: oven-sh/setup-bun@v2 | |
| if: steps.detect-fork.outputs.is_fork != 'true' | |
| - uses: actions/cache@v5 | |
| if: steps.detect-fork.outputs.is_fork != 'true' | |
| id: cache | |
| with: | |
| path: node_modules | |
| key: node-modules-${{ hashFiles('bun.lock', 'patches/**') }} | |
| - if: steps.detect-fork.outputs.is_fork != 'true' && steps.cache.outputs.cache-hit != 'true' | |
| run: bun install --frozen-lockfile | |
| - name: Eval SKILL.md | |
| if: steps.detect-fork.outputs.is_fork != 'true' | |
| run: bun run eval:skill | |
| env: | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| - name: Upload eval results | |
| if: always() && steps.detect-fork.outputs.is_fork != 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: skill-eval-results | |
| path: test/skill-eval/results.json | |
| lint: | |
| name: Lint & Typecheck | |
| needs: [changes] | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: oven-sh/setup-bun@v2 | |
| - uses: actions/cache@v5 | |
| 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 generate:schema | |
| - run: bun run lint | |
| - run: bun run typecheck | |
| - run: bun run check:deps | |
| - run: bun run check:errors | |
| test-unit: | |
| name: Unit Tests | |
| needs: [changes] | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| actions: read | |
| pull-requests: write | |
| statuses: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: oven-sh/setup-bun@v2 | |
| - uses: actions/cache@v5 | |
| 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: Generate API Schema | |
| run: bun run generate:schema | |
| - name: Unit Tests | |
| run: bun run test:unit | |
| - name: Isolated Tests | |
| run: bun run test:isolated --coverage --coverage-reporter=lcov --coverage-dir=coverage-isolated | |
| - name: Merge Coverage Reports | |
| run: bun run script/merge-lcov.ts coverage/lcov.info coverage-isolated/lcov.info > coverage/merged.lcov | |
| - name: Coverage Report | |
| uses: getsentry/codecov-action@main | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| files: ./coverage/merged.lcov | |
| informational-patch: ${{ github.event_name == 'push' }} | |
| build-binary: | |
| name: Build Binary (${{ matrix.target }}) | |
| needs: [changes, lint, test-unit] | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJSON(needs.changes.outputs.build-targets) }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: oven-sh/setup-bun@v2 | |
| - uses: actions/cache@v5 | |
| 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: Set nightly version | |
| # Inject the nightly version (computed once in the changes job) into | |
| # package.json before the build so it gets baked into the binary. | |
| if: needs.changes.outputs.nightly-version != '' | |
| shell: bash | |
| run: | | |
| jq --arg v "${{ needs.changes.outputs.nightly-version }}" '.version = $v' package.json > package.json.tmp | |
| mv package.json.tmp package.json | |
| - name: Build | |
| env: | |
| SENTRY_CLIENT_ID: ${{ vars.SENTRY_CLIENT_ID }} | |
| # Sourcemap upload to Sentry (non-fatal, skipped when token is absent) | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| # Set on main/release branches so build.ts runs binpunch + creates .gz | |
| RELEASE_BUILD: ${{ github.event_name != 'pull_request' && '1' || '' }} | |
| 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 binary artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: sentry-${{ matrix.target }} | |
| path: | | |
| dist-bin/sentry-* | |
| !dist-bin/*.gz | |
| - name: Upload compressed artifact | |
| if: github.event_name != 'pull_request' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: sentry-${{ matrix.target }}-gz | |
| path: dist-bin/*.gz | |
| generate-patches: | |
| name: Generate Delta Patches | |
| needs: [changes, build-binary] | |
| # Only on main (nightlies) and release branches (stable) — skip PRs | |
| if: github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| steps: | |
| - name: Install ORAS CLI | |
| # Only needed on main (to fetch previous nightly from GHCR) | |
| if: github.ref == 'refs/heads/main' | |
| run: | | |
| VERSION=1.2.3 | |
| EXPECTED_SHA256="b4efc97a91f471f323f193ea4b4d63d8ff443ca3aab514151a30751330852827" | |
| TARBALL="oras_${VERSION}_linux_amd64.tar.gz" | |
| curl -sfLo "$TARBALL" "https://github.com/oras-project/oras/releases/download/v${VERSION}/${TARBALL}" | |
| echo "${EXPECTED_SHA256} ${TARBALL}" | sha256sum -c - | |
| tar -xz -C /usr/local/bin oras < "$TARBALL" | |
| rm "$TARBALL" | |
| - name: Install zig-bsdiff | |
| run: | | |
| VERSION=0.1.19 | |
| EXPECTED_SHA256="9f1ac75a133ee09883ad2096a86d57791513de5fc6f262dfadee8dcee94a71b9" | |
| TARBALL="zig-bsdiff-linux-x64.tar.gz" | |
| curl -sfLo "$TARBALL" "https://github.com/blackboardsh/zig-bsdiff/releases/download/v${VERSION}/${TARBALL}" | |
| echo "${EXPECTED_SHA256} ${TARBALL}" | sha256sum -c - | |
| tar -xz -C /usr/local/bin < "$TARBALL" | |
| rm "$TARBALL" | |
| - name: Download current binaries | |
| uses: actions/download-artifact@v8 | |
| with: | |
| # Use sentry-*-* to match platform binaries (sentry-linux-x64, etc.) | |
| # and their -gz variants, but not sentry-patches | |
| pattern: sentry-*-* | |
| path: new-binaries | |
| merge-multiple: true | |
| - name: Download previous nightly binaries (main) | |
| if: github.ref == 'refs/heads/main' | |
| run: | | |
| VERSION="${{ needs.changes.outputs.nightly-version }}" | |
| REPO="ghcr.io/getsentry/cli" | |
| echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u ${{ github.actor }} --password-stdin | |
| # Find previous nightly tag | |
| TAGS=$(oras repo tags "${REPO}" 2>/dev/null | grep '^nightly-' | sort -V || echo "") | |
| PREV_TAG="" | |
| for tag in $TAGS; do | |
| # Current tag may not exist yet (publish-nightly hasn't run), | |
| # but that's fine — we want the latest existing nightly tag | |
| if [ "$tag" = "nightly-${VERSION}" ]; then | |
| break | |
| fi | |
| PREV_TAG="$tag" | |
| done | |
| if [ -z "$PREV_TAG" ]; then | |
| echo "No previous versioned nightly found, skipping patches" | |
| exit 0 | |
| fi | |
| echo "HAS_PREV=true" >> "$GITHUB_ENV" | |
| echo "Previous nightly: ${PREV_TAG}" | |
| # Download and decompress previous nightly binaries | |
| mkdir -p old-binaries | |
| PREV_MANIFEST_JSON=$(oras manifest fetch "${REPO}:${PREV_TAG}") | |
| echo "$PREV_MANIFEST_JSON" | jq -r '.layers[] | select(.annotations["org.opencontainers.image.title"] | endswith(".gz")) | .annotations["org.opencontainers.image.title"] + " " + .digest' | while read -r filename digest; do | |
| basename="${filename%.gz}" | |
| echo " Downloading previous ${basename}..." | |
| oras blob fetch "${REPO}@${digest}" --output "old-binaries/${filename}" | |
| gunzip -f "old-binaries/${filename}" | |
| done | |
| - name: Download previous release binaries (release/**) | |
| if: startsWith(github.ref, 'refs/heads/release/') | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| PREV_TAG=$(gh api "repos/${{ github.repository }}/releases?per_page=5" \ | |
| --jq '[.[] | select(.prerelease == false and .draft == false)] | .[0].tag_name // empty') | |
| if [ -z "$PREV_TAG" ]; then | |
| echo "No previous stable release found — skipping patch generation" | |
| exit 0 | |
| fi | |
| echo "HAS_PREV=true" >> "$GITHUB_ENV" | |
| echo "Previous release: ${PREV_TAG}" | |
| shopt -s nullglob | |
| mkdir -p old-binaries | |
| for gz in new-binaries/*.gz; do | |
| name=$(basename "${gz%.gz}") | |
| echo " Downloading ${name}.gz from ${PREV_TAG}..." | |
| gh release download "${PREV_TAG}" \ | |
| --repo "${{ github.repository }}" \ | |
| --pattern "${name}.gz" \ | |
| --dir old-binaries || echo " Warning: not found, skipping" | |
| done | |
| gunzip old-binaries/*.gz 2>/dev/null || true | |
| - name: Generate delta patches | |
| if: env.HAS_PREV == 'true' | |
| run: | | |
| mkdir -p patches | |
| GENERATED=0 | |
| for new_binary in new-binaries/sentry-*; do | |
| name=$(basename "$new_binary") | |
| case "$name" in *.gz) continue ;; esac | |
| old_binary="old-binaries/${name}" | |
| [ -f "$old_binary" ] || continue | |
| echo "Generating patch: ${name}.patch" | |
| bsdiff "$old_binary" "$new_binary" "patches/${name}.patch" --use-zstd | |
| patch_size=$(stat --printf='%s' "patches/${name}.patch") | |
| new_size=$(stat --printf='%s' "$new_binary") | |
| ratio=$(awk "BEGIN { printf \"%.1f\", ($patch_size / $new_size) * 100 }") | |
| echo " ${patch_size} bytes (${ratio}% of binary)" | |
| GENERATED=$((GENERATED + 1)) | |
| done | |
| echo "Generated ${GENERATED} patches" | |
| - name: Upload patch artifacts | |
| if: env.HAS_PREV == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: sentry-patches | |
| path: patches/*.patch | |
| - name: File issue on failure | |
| if: failure() | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| TITLE="Delta patch generation failed" | |
| BODY="The \`generate-patches\` job failed on [\`${GITHUB_REF_NAME}\`](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}). | |
| **Branch:** \`${GITHUB_REF_NAME}\` | |
| **Commit:** ${GITHUB_SHA} | |
| **Run:** ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" | |
| # Check for existing open issue with same title to avoid duplicates | |
| EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "$TITLE" --json number --jq '.[0].number // empty') | |
| if [ -n "$EXISTING" ]; then | |
| echo "Issue #${EXISTING} already open, adding comment" | |
| gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body "$BODY" | |
| else | |
| gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body "$BODY" | |
| fi | |
| publish-nightly: | |
| name: Publish Nightly to GHCR | |
| # Only run on pushes to main, not on PRs or release branches | |
| if: github.ref == 'refs/heads/main' && github.event_name == 'push' | |
| needs: [changes, build-binary, generate-patches] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download compressed artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: sentry-*-gz | |
| path: artifacts | |
| merge-multiple: true | |
| - name: Download uncompressed artifacts (for SHA-256 computation) | |
| uses: actions/download-artifact@v8 | |
| with: | |
| # Use sentry-*-* to match platform binaries (sentry-linux-x64, etc.) | |
| # but not sentry-patches (which is downloaded separately) | |
| pattern: sentry-*-* | |
| path: binaries | |
| merge-multiple: true | |
| - name: Download patch artifacts | |
| uses: actions/download-artifact@v8 | |
| continue-on-error: true | |
| id: download-patches | |
| with: | |
| name: sentry-patches | |
| path: patches | |
| - name: Install ORAS CLI | |
| run: | | |
| VERSION=1.2.3 | |
| EXPECTED_SHA256="b4efc97a91f471f323f193ea4b4d63d8ff443ca3aab514151a30751330852827" | |
| TARBALL="oras_${VERSION}_linux_amd64.tar.gz" | |
| curl -sfLo "$TARBALL" "https://github.com/oras-project/oras/releases/download/v${VERSION}/${TARBALL}" | |
| echo "${EXPECTED_SHA256} ${TARBALL}" | sha256sum -c - | |
| tar -xz -C /usr/local/bin oras < "$TARBALL" | |
| rm "$TARBALL" | |
| - name: Log in to GHCR | |
| run: echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u ${{ github.actor }} --password-stdin | |
| - name: Push binaries to GHCR | |
| # Push from inside the artifacts directory so ORAS records bare | |
| # filenames (e.g. "sentry-linux-x64.gz") as layer titles, not | |
| # "artifacts/sentry-linux-x64.gz". The CLI matches layers by | |
| # filename in findLayerByFilename(). | |
| working-directory: artifacts | |
| run: | | |
| VERSION="${{ needs.changes.outputs.nightly-version }}" | |
| oras push ghcr.io/getsentry/cli:nightly \ | |
| --artifact-type application/vnd.sentry.cli.nightly \ | |
| --annotation "org.opencontainers.image.source=https://github.com/getsentry/cli" \ | |
| --annotation "version=${VERSION}" \ | |
| *.gz | |
| - name: Tag versioned nightly | |
| # Create an immutable versioned tag via zero-copy oras tag. | |
| # This enables patch chain resolution to find specific nightly versions. | |
| run: | | |
| VERSION="${{ needs.changes.outputs.nightly-version }}" | |
| oras tag ghcr.io/getsentry/cli:nightly "nightly-${VERSION}" | |
| - name: Push delta patches to GHCR | |
| # Upload pre-generated patches from generate-patches job as a | |
| # :patch-<version> manifest with SHA-256 annotations. | |
| # Failure here is non-fatal — delta upgrades are optional. | |
| if: steps.download-patches.outcome == 'success' | |
| continue-on-error: true | |
| run: | | |
| VERSION="${{ needs.changes.outputs.nightly-version }}" | |
| REPO="ghcr.io/getsentry/cli" | |
| # Compute SHA-256 annotations from new binaries and collect patch files | |
| shopt -s nullglob | |
| ANNOTATIONS="" | |
| PATCH_FILES="" | |
| for patch_file in patches/*.patch; do | |
| basename_patch=$(basename "$patch_file" .patch) | |
| new_binary="binaries/${basename_patch}" | |
| if [ -f "$new_binary" ]; then | |
| sha256=$(sha256sum "$new_binary" | cut -d' ' -f1) | |
| ANNOTATIONS="${ANNOTATIONS} --annotation sha256-${basename_patch}=${sha256}" | |
| fi | |
| PATCH_FILES="${PATCH_FILES} $(basename "$patch_file")" | |
| done | |
| if [ -z "$PATCH_FILES" ]; then | |
| echo "No patches to push" | |
| exit 0 | |
| fi | |
| # Find from-version by listing GHCR nightly tags | |
| TAGS=$(oras repo tags "${REPO}" 2>/dev/null | grep '^nightly-' | sort -V || echo "") | |
| PREV_TAG="" | |
| for tag in $TAGS; do | |
| if [ "$tag" = "nightly-${VERSION}" ]; then break; fi | |
| PREV_TAG="$tag" | |
| done | |
| if [ -z "$PREV_TAG" ]; then | |
| echo "No previous nightly tag found, skipping patch push" | |
| exit 0 | |
| fi | |
| PREV_VERSION="${PREV_TAG#nightly-}" | |
| cd patches | |
| eval oras push "${REPO}:patch-${VERSION}" \ | |
| --artifact-type application/vnd.sentry.cli.patch \ | |
| --annotation "from-version=${PREV_VERSION}" \ | |
| ${ANNOTATIONS} \ | |
| ${PATCH_FILES} | |
| echo "Pushed patch manifest: patch-${VERSION} (from ${PREV_VERSION})" | |
| test-e2e: | |
| name: E2E Tests | |
| needs: [build-binary] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: oven-sh/setup-bun@v2 | |
| - uses: actions/cache@v5 | |
| 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@v8 | |
| with: | |
| name: sentry-linux-x64 | |
| path: dist-bin | |
| - name: Make binary executable | |
| run: chmod +x dist-bin/sentry-linux-x64 | |
| - name: Generate API Schema | |
| run: bun run generate:schema | |
| - name: E2E Tests | |
| env: | |
| SENTRY_CLI_BINARY: ${{ github.workspace }}/dist-bin/sentry-linux-x64 | |
| run: bun run test:e2e | |
| 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@v6 | |
| - uses: oven-sh/setup-bun@v2 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ matrix.node }} | |
| - uses: actions/cache@v5 | |
| 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@v7 | |
| with: | |
| name: npm-package | |
| path: "*.tgz" | |
| build-docs: | |
| name: Build Docs | |
| needs: [lint] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - 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@v7 | |
| with: | |
| name: gh-pages | |
| path: gh-pages.zip | |
| ci-status: | |
| name: CI Status | |
| if: always() | |
| needs: [changes, check-skill, eval-skill, build-binary, build-npm, build-docs, test-e2e, generate-patches, publish-nightly] | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| steps: | |
| - name: Check CI status | |
| run: | | |
| # Check for explicit failures or cancellations in all jobs | |
| # generate-patches and publish-nightly are skipped on PRs — that's expected | |
| results="${{ needs.check-skill.result }} ${{ needs.eval-skill.result }} ${{ needs.build-binary.result }} ${{ needs.build-npm.result }} ${{ needs.build-docs.result }} ${{ needs.test-e2e.result }} ${{ needs.generate-patches.result }} ${{ needs.publish-nightly.result }}" | |
| for result in $results; do | |
| if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then | |
| echo "::error::CI failed" | |
| exit 1 | |
| fi | |
| done | |
| # Detect upstream failures: if changes were detected but jobs were skipped, | |
| # it means an upstream job failed (skipped jobs cascade to dependents) | |
| if [[ "${{ needs.changes.outputs.code }}" == "true" && "${{ needs.test-e2e.result }}" == "skipped" ]]; then | |
| echo "::error::CI failed - upstream job failed causing test-e2e to be skipped" | |
| exit 1 | |
| fi | |
| if [[ "${{ needs.changes.outputs.skill }}" == "true" && "${{ needs.check-skill.result }}" == "skipped" ]]; then | |
| echo "::error::CI failed - upstream job failed causing check-skill to be skipped" | |
| exit 1 | |
| fi | |
| if [[ "${{ needs.changes.outputs.skill }}" == "true" && "${{ needs.eval-skill.result }}" == "skipped" ]]; then | |
| echo "::error::CI failed - upstream job failed causing eval-skill to be skipped" | |
| exit 1 | |
| fi | |
| echo "CI passed" |