Skip to content

Commit 204bb5c

Browse files
authored
feat(nightly): distribute via GHCR instead of GitHub Releases (#298)
## 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
1 parent ad2f896 commit 204bb5c

File tree

10 files changed

+1394
-347
lines changed

10 files changed

+1394
-347
lines changed

.github/workflows/ci.yml

Lines changed: 94 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ concurrency:
1010
group: ci-${{ github.ref }}
1111
cancel-in-progress: true
1212

13+
# packages:write is needed for publish-nightly to push to GHCR
14+
permissions:
15+
contents: read
16+
packages: write
17+
1318
env:
1419
# Commit timestamp used for deterministic nightly version strings.
1520
# Defined at workflow level so build-binary and publish-nightly always agree.
@@ -24,6 +29,8 @@ jobs:
2429
outputs:
2530
skill: ${{ steps.filter.outputs.skill == 'true' || startsWith(github.ref, 'refs/heads/release/') }}
2631
code: ${{ steps.filter.outputs.code == 'true' || startsWith(github.ref, 'refs/heads/release/') }}
32+
build-targets: ${{ steps.targets.outputs.matrix }}
33+
nightly-version: ${{ steps.nightly.outputs.version }}
2734
steps:
2835
- uses: actions/checkout@v4
2936
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
@@ -45,6 +52,37 @@ jobs:
4552
- 'package.json'
4653
- 'bun.lock'
4754
- '.github/workflows/ci.yml'
55+
- name: Compute build matrix
56+
id: targets
57+
run: |
58+
{
59+
echo 'matrix<<MATRIX_EOF'
60+
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
61+
# PRs only need linux-x64 for smoke test and e2e — skip macOS/Windows
62+
echo '{"include":[
63+
{"target":"linux-x64", "os":"ubuntu-latest", "can-test":true}
64+
]}'
65+
else
66+
# main, release/**, workflow_call: full cross-platform matrix
67+
echo '{"include":[
68+
{"target":"darwin-arm64", "os":"macos-latest", "can-test":true},
69+
{"target":"linux-x64", "os":"ubuntu-latest", "can-test":true},
70+
{"target":"windows-x64", "os":"windows-latest","can-test":true},
71+
{"target":"darwin-x64", "os":"macos-latest", "can-test":false},
72+
{"target":"linux-arm64", "os":"ubuntu-latest", "can-test":false}
73+
]}'
74+
fi
75+
echo 'MATRIX_EOF'
76+
} >> "$GITHUB_OUTPUT"
77+
- name: Compute nightly version
78+
id: nightly
79+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
80+
run: |
81+
TS=$(date -d "$COMMIT_TIMESTAMP" +%s)
82+
CURRENT=$(jq -r .version package.json)
83+
VERSION=$(echo "$CURRENT" | sed "s/-dev\.[0-9]*$/-dev.${TS}/")
84+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
85+
echo "Nightly version: ${VERSION}"
4886
4987
check-skill:
5088
name: Check SKILL.md
@@ -138,29 +176,11 @@ jobs:
138176

139177
build-binary:
140178
name: Build Binary (${{ matrix.target }})
141-
needs: [lint, test-unit]
179+
needs: [changes, lint, test-unit]
142180
runs-on: ${{ matrix.os }}
143181
strategy:
144182
fail-fast: false
145-
matrix:
146-
include:
147-
# Native builds (can run smoke test)
148-
- target: darwin-arm64
149-
os: macos-latest
150-
can-test: true
151-
- target: linux-x64
152-
os: ubuntu-latest
153-
can-test: true
154-
- target: windows-x64
155-
os: windows-latest
156-
can-test: true
157-
# Cross-compiled builds (cannot run smoke test)
158-
- target: darwin-x64
159-
os: macos-latest
160-
can-test: false
161-
- target: linux-arm64
162-
os: ubuntu-latest
163-
can-test: false
183+
matrix: ${{ fromJSON(needs.changes.outputs.build-targets) }}
164184
steps:
165185
- uses: actions/checkout@v4
166186
- uses: oven-sh/setup-bun@v2
@@ -184,22 +204,18 @@ jobs:
184204
echo "All install attempts failed"
185205
exit 1
186206
- name: Set nightly version
187-
# Inject a nightly version into package.json before the build so it gets
188-
# baked into the binary. Only runs on direct pushes to main.
189-
# Uses the commit timestamp (seconds since epoch) as a deterministic
190-
# value so every matrix leg and publish-nightly produce the same version
191-
# string for a given commit.
192-
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
207+
# Inject the nightly version (computed once in the changes job) into
208+
# package.json before the build so it gets baked into the binary.
209+
if: needs.changes.outputs.nightly-version != ''
193210
shell: bash
194211
run: |
195-
TS=$(date -d "$COMMIT_TIMESTAMP" +%s 2>/dev/null || date -jf "%Y-%m-%dT%H:%M:%SZ" "$COMMIT_TIMESTAMP" +%s)
196-
CURRENT=$(jq -r .version package.json)
197-
NIGHTLY=$(echo "$CURRENT" | sed "s/-dev\.[0-9]*$/-dev.${TS}/")
198-
jq --arg v "$NIGHTLY" '.version = $v' package.json > package.json.tmp
212+
jq --arg v "${{ needs.changes.outputs.nightly-version }}" '.version = $v' package.json > package.json.tmp
199213
mv package.json.tmp package.json
200214
- name: Build
201215
env:
202216
SENTRY_CLIENT_ID: ${{ vars.SENTRY_CLIENT_ID }}
217+
# Set on main/release branches so build.ts runs binpunch + creates .gz
218+
RELEASE_BUILD: ${{ github.event_name != 'pull_request' && '1' || '' }}
203219
run: bun run build --target ${{ matrix.target }}
204220
- name: Smoke test
205221
if: matrix.can-test
@@ -210,11 +226,56 @@ jobs:
210226
else
211227
./dist-bin/sentry-${{ matrix.target }} --help
212228
fi
213-
- name: Upload artifact
229+
- name: Upload binary artifact
214230
uses: actions/upload-artifact@v4
215231
with:
216232
name: sentry-${{ matrix.target }}
217-
path: dist-bin/sentry-*
233+
path: |
234+
dist-bin/sentry-*
235+
!dist-bin/*.gz
236+
237+
- name: Upload compressed artifact
238+
if: env.RELEASE_BUILD == '1'
239+
uses: actions/upload-artifact@v4
240+
with:
241+
name: sentry-${{ matrix.target }}-gz
242+
path: dist-bin/*.gz
243+
244+
publish-nightly:
245+
name: Publish Nightly to GHCR
246+
# Only run on pushes to main, not on PRs or release branches
247+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
248+
needs: [changes, build-binary]
249+
runs-on: ubuntu-latest
250+
steps:
251+
- name: Download compressed artifacts
252+
uses: actions/download-artifact@v4
253+
with:
254+
pattern: sentry-*-gz
255+
path: artifacts
256+
merge-multiple: true
257+
258+
- name: Install ORAS CLI
259+
run: |
260+
VERSION=1.2.3
261+
EXPECTED_SHA256="b4efc97a91f471f323f193ea4b4d63d8ff443ca3aab514151a30751330852827"
262+
TARBALL="oras_${VERSION}_linux_amd64.tar.gz"
263+
curl -sfLo "$TARBALL" "https://github.com/oras-project/oras/releases/download/v${VERSION}/${TARBALL}"
264+
echo "${EXPECTED_SHA256} ${TARBALL}" | sha256sum -c -
265+
tar -xz -C /usr/local/bin oras < "$TARBALL"
266+
rm "$TARBALL"
267+
268+
- name: Log in to GHCR
269+
run: echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u ${{ github.actor }} --password-stdin
270+
271+
- name: Push to GHCR
272+
run: |
273+
VERSION="${{ needs.changes.outputs.nightly-version }}"
274+
oras push ghcr.io/getsentry/cli:nightly \
275+
--artifact-type application/vnd.sentry.cli.nightly \
276+
--annotation "org.opencontainers.image.source=https://github.com/getsentry/cli" \
277+
--annotation "version=${VERSION}" \
278+
artifacts/*.gz
218279
219280
test-e2e:
220281
name: E2E Tests
@@ -301,69 +362,6 @@ jobs:
301362
name: gh-pages
302363
path: gh-pages.zip
303364

304-
publish-nightly:
305-
name: Publish Nightly
306-
# Only publish after a successful main-branch build+test. Skipped on PRs
307-
# and release branches.
308-
needs: [build-binary, test-e2e]
309-
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
310-
runs-on: ubuntu-latest
311-
permissions:
312-
contents: write
313-
steps:
314-
- uses: actions/checkout@v4
315-
with:
316-
sparse-checkout: package.json
317-
- name: Compute nightly version
318-
# Uses the commit timestamp — the same value as build-binary — so
319-
# version.json exactly matches the version baked into the binaries.
320-
id: version
321-
run: |
322-
TS=$(date -d "$COMMIT_TIMESTAMP" +%s)
323-
CURRENT=$(jq -r .version package.json)
324-
VERSION=$(echo "$CURRENT" | sed "s/-dev\.[0-9]*$/-dev.${TS}/")
325-
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
326-
- name: Download all binary artifacts
327-
uses: actions/download-artifact@v4
328-
with:
329-
pattern: sentry-*
330-
path: artifacts
331-
merge-multiple: true
332-
- name: Create version.json
333-
run: |
334-
cat > version.json <<EOF
335-
{"version":"${{ steps.version.outputs.version }}","sha":"${{ github.sha }}","date":"$(date -u +%Y-%m-%dT%H:%M:%SZ)"}
336-
EOF
337-
- name: Create or update nightly release
338-
env:
339-
GH_TOKEN: ${{ github.token }}
340-
GH_REPO: ${{ github.repository }}
341-
run: |
342-
# Create the release the first time; subsequent runs are a no-op
343-
gh release create nightly \
344-
--prerelease \
345-
--title "Nightly" \
346-
--notes "" \
347-
2>/dev/null || true
348-
349-
# Update release notes with the latest version + commit
350-
gh release edit nightly \
351-
--prerelease \
352-
--notes "Latest nightly build from the \`main\` branch.
353-
354-
**Version:** \`${{ steps.version.outputs.version }}\`
355-
**Commit:** ${{ github.sha }}"
356-
357-
# Delete all existing assets first so removed/renamed files don't linger
358-
gh release view nightly --json assets --jq '.assets[].name' | while read -r name; do
359-
gh release delete-asset nightly "$name" --yes
360-
done
361-
362-
# Upload the new .gz binaries and the version manifest
363-
gh release upload nightly \
364-
artifacts/*.gz \
365-
version.json
366-
367365
ci-status:
368366
name: CI Status
369367
if: always()
@@ -374,6 +372,7 @@ jobs:
374372
- name: Check CI status
375373
run: |
376374
# Check for explicit failures or cancellations in all jobs
375+
# publish-nightly is skipped on PRs (if: github.ref == 'refs/heads/main') — that's expected
377376
results="${{ needs.check-skill.result }} ${{ needs.build-binary.result }} ${{ needs.build-npm.result }} ${{ needs.build-docs.result }} ${{ needs.test-e2e.result }} ${{ needs.publish-nightly.result }}"
378377
for result in $results; do
379378
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then

0 commit comments

Comments
 (0)