Skip to content

feat(nightly): distribute via GHCR instead of GitHub Releases#298

Merged
BYK merged 6 commits intomainfrom
feat/ghcr-nightly-distribution-rebased
Feb 26, 2026
Merged

feat(nightly): distribute via GHCR instead of GitHub Releases#298
BYK merged 6 commits intomainfrom
feat/ghcr-nightly-distribution-rebased

Conversation

@BYK
Copy link
Member

@BYK BYK commented Feb 26, 2026

Summary

GitHub Releases prevents a rolling nightly tag — assets can't be modified and tags can't be reused after deletion. GHCR supports freely overwritable OCI artifact tags, making it the right host for nightly binaries.

This PR replaces the nightly distribution channel with GHCR while leaving stable releases (GitHub Releases + Craft) untouched.

Changes

CI (publish-nightly job)

  • Compresses binaries with gzip, generates 0.0.0-nightly.<unix_timestamp> version
  • Pushes .gz binaries to ghcr.io/getsentry/cli:nightly via ORAS with the version in the manifest annotations.version field
  • Tag is overwritten on every main-branch push; unchanged files are deduplicated by GHCR

src/lib/ghcr.ts (new)

  • getAnonymousToken() — anonymous token exchange with ghcr.io
  • fetchNightlyManifest(token) — fetches the :nightly OCI manifest
  • getNightlyVersion(manifest) — extracts version from manifest annotations (2 requests total for a version check: token + manifest)
  • downloadNightlyBlob(token, digest) — downloads a blob with manual 307 redirect; auth header must not be forwarded to the Azure Blob Storage redirect target (returns 404 otherwise)

src/lib/upgrade.ts

  • fetchLatestNightlyVersion() now fetches from GHCR manifest annotation instead of GitHub version.json
  • downloadBinaryToTemp() routes nightly versions through downloadNightlyToPath() (GHCR) and stable through downloadStableToPath() (GitHub Releases)

src/lib/version-check.ts

  • Background version check uses GHCR for nightly channel, GitHub Releases for stable

install script

  • --nightly flag triggers a 6-step GHCR flow using only curl and awk (no jq)
  • --nightly and --version are mutually exclusive

Notes

  • The ghcr.io/getsentry/cli package already exists and is public (one-time setup done)
  • Stable release distribution (GitHub Releases, Homebrew, npm) is unaffected

Replace the nightly binary distribution channel from GitHub Releases to
GitHub Container Registry (GHCR) using the OCI artifact protocol via ORAS.

Stable releases are unchanged — they continue to use GitHub Releases and Craft.

Key changes:
- CI: new publish-nightly job pushes .gz binaries + version annotation to
  ghcr.io/getsentry/cli:nightly using ORAS; tag is freely overwritable
- src/lib/ghcr.ts: new GHCR/OCI client (token exchange, manifest fetch,
  blob download with manual 307 redirect — auth must not forward to Azure)
- src/lib/upgrade.ts: nightly upgrades fetch version from manifest annotation
  and download the matching OCI layer blob; stable path unchanged
- src/lib/version-check.ts: background version check uses GHCR for nightly
  builds, GitHub Releases for stable
- install: --nightly flag triggers GHCR flow using only curl/awk (no jq)
@github-actions
Copy link
Contributor

github-actions bot commented Feb 26, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

  • (issue-list) Add --period flag, pagination progress, and count abbreviation by BYK in #289
  • (nightly) Distribute via GHCR instead of GitHub Releases by BYK in #298
  • (upgrade) Add nightly release channel by BYK in #292

Bug Fixes 🐛

  • (brew) Handle root-owned config dir from sudo installs by BYK in #288
  • (errors) Add ResolutionError for not-found/ambiguous resolution failures by BYK in #293
  • (issue) Improve numeric issue ID resolution with org context and region routing by BYK in #294
  • (setup) Show actual shell name instead of "unknown" for unsupported shells by BYK in #287
  • Optimized the docs images by MathurAditya724 in #291

Internal Changes 🔧

  • Correct nightly artifact path in publish-nightly job by BYK in #295
  • Only showing status about changed files in codecov by MathurAditya724 in #286

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 26, 2026

Codecov Results 📊

✅ Patch coverage is 97.62%. Project has 3607 uncovered lines.
✅ Project coverage is 76.42%. Comparing base (base) to head (head).

Files with missing lines (2)
File Patch % Lines
upgrade.ts 64.72% ⚠️ 127 Missing
version-check.ts 92.08% ⚠️ 8 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
+ Coverage    75.38%    76.42%    +1.04%
==========================================
  Files          115       117        +2
  Lines        14843     15298      +455
  Branches         0         0         —
==========================================
+ Hits         11189     11691      +502
- Misses        3654      3607       -47
- Partials         0         0         —

Generated by Codecov Action

BYK added 2 commits February 26, 2026 13:40
- Remove redundant job-level permissions (workflow-level already covers it)
- Remove checkout step from publish-nightly (not needed — no repo files used)
- Add sha256 verification for ORAS CLI tarball download
- Add comment explaining why binaries are compressed in publish-nightly
- Use -dev.<timestamp> version format (consistent with build-binary job)
- Add AbortError class to errors.ts, replace Object.assign hack in upgrade.ts
- Use Record mapping for platform name in getNightlyGzFilename
- Revert --nightly flag to --version nightly in install script
…inpunched .gz

The build script runs binpunch (zeroes unused ICU data) then creates .gz
files when CI=true. These are already in the uploaded artifacts, so the
separate compress step in publish-nightly was redundant.
BYK added 2 commits February 26, 2026 14:11
- build-binary matrix now comes from changes job: PRs get linux-x64
  only, main/release get all 5 targets (darwin, linux, windows)
- binpunch + gzip gated on RELEASE_BUILD env var (set on non-PR events)
  so PR builds skip both post-processing steps entirely
- artifact uploads split: raw binaries go to sentry-<target>, compressed
  .gz files go to sentry-<target>-gz (only created on release builds)
- publish-nightly downloads only sentry-*-gz artifacts instead of all
  artifacts including unused raw binaries
- Nightly version computed once in the changes job, output as
  nightly-version. Both build-binary (to bake into package.json) and
  publish-nightly (for GHCR annotation) consume the same output.
  Eliminates redundant re-derivation and curl to raw.githubusercontent.
- PLATFORM_NAMES extracted to frozen module-level constant with
  DEFAULT_PLATFORM fallback.
- Build matrix JSON now uses multiline heredoc format with aligned
  columns for readability instead of single-line escaped JSON strings
- binpunch always runs (not gated on RELEASE_BUILD) so the smoke test
  exercises the same binary that ships in releases
- Only gzip creation is gated on RELEASE_BUILD
@BYK BYK marked this pull request as ready for review February 26, 2026 14:39
@BYK
Copy link
Member Author

BYK commented Feb 26, 2026

@cursoragent review

@cursor
Copy link

cursor bot commented Feb 26, 2026

Code Review: GHCR Nightly Distribution

I've completed a thorough review of the PR. Here's my analysis:

Strengths

1. Architecture & Design

  • Clean separation of concerns with dedicated ghcr.ts module
  • Well-documented OCI download protocol implementation
  • Manual redirect handling properly addresses Azure Blob Storage auth issue
  • Single source of truth for nightly version (computed once in CI)

2. Code Quality

  • Excellent JSDoc documentation throughout
  • Proper use of Bun APIs (following project conventions)
  • Comprehensive error handling with typed UpgradeError variants
  • Frozen PLATFORM_NAMES constant with explicit DEFAULT_PLATFORM

3. Test Coverage

  • 22 tests for ghcr.ts covering all functions and edge cases
  • 100+ tests for upgrade.ts including nightly scenarios
  • Proper mocking of fetch to avoid network calls
  • Tests verify both success and failure paths

4. CI/CD

  • Nightly version computed once in changes job (single source of truth)
  • Clean matrix definition using heredoc format
  • binpunch always runs (not gated on RELEASE_BUILD)
  • Proper artifact separation (sentry-* vs sentry-*-gz)

5. Install Script

  • Pure bash implementation with no jq dependency
  • 6-step GHCR flow using only curl and awk
  • Proper handling of auth header forwarding (must NOT go to Azure)
  • --nightly and --version mutual exclusion

🔍 Observations & Minor Suggestions

1. Test Coverage Gap

// src/lib/upgrade.ts:480
function getNightlyGzFilename(): string { ... }

This function is tested indirectly through executeUpgrade integration tests but has no direct unit tests. Consider adding:

describe("getNightlyGzFilename", () => {
  test("builds correct filename for current platform", () => {
    const filename = getNightlyGzFilename();
    expect(filename).toMatch(/^sentry-(darwin|linux|windows)-(x64|arm64)(\.exe)?\.gz$/);
  });
});

2. Bun Bug Workaround

// src/lib/upgrade.ts:456-469
async function streamDecompressToFile(...) {
  // Workaround for https://github.com/oven-sh/bun/issues/13237

This is properly documented. Consider adding a TODO comment to revisit when the Bun bug is fixed:

// TODO: Replace manual streaming with `await Bun.write(destPath, response)`
// once https://github.com/oven-sh/bun/issues/13237 is fixed

3. Install Script JSON Parsing
The awk-based JSON parsing is fragile but acceptable given the no-jq requirement:

# install:104
awk -F'"' '{for(i=1;i<=NF;i++) if($i=="token"){print $(i+2);exit}}'

This works for the known JSON structure but could break with whitespace changes. Consider adding a comment:

# Parse JSON with awk (no jq dependency). Assumes standard formatting.

4. CLI_VERSION Injection
The version is properly injected via package.json update in CI:

# .github/workflows/ci.yml:212
jq --arg v "${{ needs.changes.outputs.nightly-version }}" '.version = $v' package.json

This ensures sentry --version shows the correct nightly version. ✅

Security & Reliability

1. Auth Header Handling

// src/lib/ghcr.ts:258
// Follow redirect WITHOUT Authorization header (Azure rejects it)
redirectResponse = await fetch(redirectUrl, {
  headers: { "User-Agent": getUserAgent() },
});

Correctly avoids forwarding auth to Azure. ✅

2. Anonymous Access

// src/lib/ghcr.ts:82
export async function getAnonymousToken(): Promise<string> {
  const url = `${GHCR_REGISTRY}/token?scope=repository:${GHCR_REPO}:pull`;

Public package requires no credentials. ✅

3. Error Handling
All network errors properly wrapped in UpgradeError with descriptive messages. ✅

📊 Test Results

✅ GHCR module: 22/22 tests pass
✅ Upgrade module: 101/101 tests pass
✅ Version check: 22/22 tests pass
✅ Linter: No issues
✅ TypeScript: No errors

The 129 failures in the full test suite appear to be pre-existing and unrelated to this PR (likely test isolation issues based on the error patterns).

🎯 Verdict

LGTM with minor suggestions

The implementation is solid, well-tested, and follows project conventions. The GHCR integration is properly designed with good error handling and documentation. The only improvements would be:

  1. Add direct unit test for getNightlyGzFilename()
  2. Add TODO comment for Bun bug workaround
  3. Add comment about awk JSON parsing fragility

These are minor polish items and don't block merging.

📝 Summary

This PR successfully replaces GitHub Releases with GHCR for nightly distribution while leaving stable releases untouched. The implementation handles all edge cases (redirects, auth, decompression) correctly and is well-tested. The CI changes ensure a single source of truth for nightly versions. Great work! 🚀

Open in Web Open in Cursor 

@BYK BYK merged commit 204bb5c into main Feb 26, 2026
19 checks passed
@BYK BYK deleted the feat/ghcr-nightly-distribution-rebased branch February 26, 2026 14:51
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable autofix in the Cursor dashboard.

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]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nightly publish no longer gated on e2e tests

Medium Severity

The publish-nightly job previously depended on [build-binary, test-e2e], ensuring nightlies were only published after e2e tests passed. The new dependency list [changes, build-binary] drops test-e2e, so broken nightly binaries can be pushed to GHCR before integration tests validate them. Since test-e2e and publish-nightly both depend on build-binary, they now run in parallel.

Fix in Cursor Fix in Web

# packages:write is needed for publish-nightly to push to GHCR
permissions:
contents: read
packages: write
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workflow-level packages:write grants excess permissions to all jobs

Low Severity

The packages:write permission is set at the workflow level, granting it to every job that doesn't define its own permissions block (e.g., build-binary, lint, build-docs). Only publish-nightly needs this permission. The old code used a job-level permissions block solely on the publishing job, following the principle of least privilege.

Fix in Cursor Fix in Web

if [[ -z "$redir_url" ]]; then
echo -e "${RED}Failed to get blob redirect URL from GHCR${NC}"
exit 1
fi
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Install script discards blob body on non-redirect response

Low Severity

The install script's step 5 uses curl -o /dev/null to discard the response body, expecting only a redirect URL via %{redirect_url}. If GHCR ever serves the blob directly (HTTP 200 instead of 307), the binary data is silently discarded and the script exits with "Failed to get blob redirect URL." The TypeScript downloadNightlyBlob correctly handles both 200 and redirect responses.

Fix in Cursor Fix in Web

betegon pushed a commit that referenced this pull request Feb 26, 2026
## Summary

GitHub Releases prevents a rolling `nightly` tag — assets can't be
modified and tags can't be reused after deletion. GHCR supports freely
overwritable OCI artifact tags, making it the right host for nightly
binaries.

This PR replaces the nightly distribution channel with GHCR while
leaving stable releases (GitHub Releases + Craft) untouched.

## Changes

**CI (`publish-nightly` job)**
- Compresses binaries with gzip, generates
`0.0.0-nightly.<unix_timestamp>` version
- Pushes `.gz` binaries to `ghcr.io/getsentry/cli:nightly` via ORAS with
the version in the manifest `annotations.version` field
- Tag is overwritten on every main-branch push; unchanged files are
deduplicated by GHCR

**`src/lib/ghcr.ts`** (new)
- `getAnonymousToken()` — anonymous token exchange with ghcr.io
- `fetchNightlyManifest(token)` — fetches the `:nightly` OCI manifest
- `getNightlyVersion(manifest)` — extracts version from manifest
annotations (2 requests total for a version check: token + manifest)
- `downloadNightlyBlob(token, digest)` — downloads a blob with manual
307 redirect; auth header must **not** be forwarded to the Azure Blob
Storage redirect target (returns 404 otherwise)

**`src/lib/upgrade.ts`**
- `fetchLatestNightlyVersion()` now fetches from GHCR manifest
annotation instead of GitHub `version.json`
- `downloadBinaryToTemp()` routes nightly versions through
`downloadNightlyToPath()` (GHCR) and stable through
`downloadStableToPath()` (GitHub Releases)

**`src/lib/version-check.ts`**
- Background version check uses GHCR for nightly channel, GitHub
Releases for stable

**`install` script**
- `--nightly` flag triggers a 6-step GHCR flow using only `curl` and
`awk` (no `jq`)
- `--nightly` and `--version` are mutually exclusive

## Notes

- The `ghcr.io/getsentry/cli` package already exists and is public
(one-time setup done)
- Stable release distribution (GitHub Releases, Homebrew, npm) is
unaffected
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant