Skip to content

Commit a1fcdc9

Browse files
committed
feat(nightly): distribute via GHCR instead of GitHub Releases
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)
1 parent ad2f896 commit a1fcdc9

File tree

8 files changed

+1363
-309
lines changed

8 files changed

+1363
-309
lines changed

.github/workflows/ci.yml

Lines changed: 73 additions & 63 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.
@@ -216,6 +221,73 @@ jobs:
216221
name: sentry-${{ matrix.target }}
217222
path: dist-bin/sentry-*
218223

224+
publish-nightly:
225+
name: Publish Nightly to GHCR
226+
# Only run on pushes to main, not on PRs or release branches
227+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
228+
needs: [build-binary]
229+
runs-on: ubuntu-latest
230+
permissions:
231+
contents: read
232+
packages: write
233+
steps:
234+
- uses: actions/checkout@v4
235+
236+
- name: Download all binary artifacts
237+
uses: actions/download-artifact@v4
238+
with:
239+
pattern: sentry-*
240+
path: artifacts
241+
merge-multiple: true
242+
243+
- name: Install ORAS CLI
244+
run: |
245+
VERSION=1.2.3
246+
curl -sfL "https://github.com/oras-project/oras/releases/download/v${VERSION}/oras_${VERSION}_linux_amd64.tar.gz" \
247+
| tar -xz -C /usr/local/bin oras
248+
249+
- name: Compress binaries
250+
run: |
251+
ls artifacts/
252+
for f in artifacts/sentry-linux-* artifacts/sentry-darwin-*; do
253+
gzip -k "$f"
254+
done
255+
# Windows binary — compress but keep .exe name visible in title
256+
for f in artifacts/sentry-windows-*.exe; do
257+
gzip -k "$f"
258+
done
259+
ls artifacts/*.gz
260+
261+
- name: Compute nightly version
262+
id: version
263+
run: |
264+
# Derive a deterministic Unix timestamp from the commit so that all
265+
# jobs in this workflow run produce the same version string.
266+
TS=$(date -d '${{ github.event.head_commit.timestamp }}' +%s 2>/dev/null \
267+
|| date -j -f "%Y-%m-%dT%H:%M:%S%z" '${{ github.event.head_commit.timestamp }}' +%s 2>/dev/null \
268+
|| echo ${{ github.run_number }})
269+
VERSION="0.0.0-nightly.${TS}"
270+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
271+
echo "Nightly version: ${VERSION}"
272+
273+
- name: Create version.json
274+
run: |
275+
echo '{"version":"${{ steps.version.outputs.version }}"}' > version.json
276+
cat version.json
277+
278+
- name: Log in to GHCR
279+
run: echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u ${{ github.actor }} --password-stdin
280+
281+
- name: Push to GHCR
282+
run: |
283+
VERSION="${{ steps.version.outputs.version }}"
284+
oras push ghcr.io/getsentry/cli:nightly \
285+
--artifact-type application/vnd.sentry.cli.nightly \
286+
--annotation "org.opencontainers.image.source=https://github.com/getsentry/cli" \
287+
--annotation "version=${VERSION}" \
288+
artifacts/*.gz \
289+
version.json
290+
219291
test-e2e:
220292
name: E2E Tests
221293
needs: [build-binary]
@@ -301,69 +373,6 @@ jobs:
301373
name: gh-pages
302374
path: gh-pages.zip
303375

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-
367376
ci-status:
368377
name: CI Status
369378
if: always()
@@ -374,6 +383,7 @@ jobs:
374383
- name: Check CI status
375384
run: |
376385
# Check for explicit failures or cancellations in all jobs
386+
# publish-nightly is skipped on PRs (if: github.ref == 'refs/heads/main') — that's expected
377387
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 }}"
378388
for result in $results; do
379389
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then

install

Lines changed: 104 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ Usage: install [options]
1313
1414
Options:
1515
-h, --help Display this help message
16-
-v, --version <version> Install a specific version (e.g., 0.2.0) or "nightly"
16+
-v, --version <version> Install a specific version (e.g., 0.2.0)
17+
--nightly Install the latest nightly build from GHCR
1718
--no-modify-path Don't modify shell config files (.zshrc, .bashrc, etc.)
1819
--no-completions Don't install shell completions
1920
@@ -24,11 +25,13 @@ Examples:
2425
curl -fsSL https://cli.sentry.dev/install | bash
2526
curl -fsSL https://cli.sentry.dev/install | bash -s -- --version nightly
2627
curl -fsSL https://cli.sentry.dev/install | bash -s -- --version 0.2.0
28+
curl -fsSL https://cli.sentry.dev/install | bash -s -- --nightly
2729
SENTRY_INSTALL_DIR=~/.local/bin curl -fsSL https://cli.sentry.dev/install | bash
2830
EOF
2931
}
3032

3133
requested_version=""
34+
nightly=false
3235
no_modify_path=false
3336
no_completions=false
3437
while [[ $# -gt 0 ]]; do
@@ -43,6 +46,10 @@ while [[ $# -gt 0 ]]; do
4346
exit 1
4447
fi
4548
;;
49+
--nightly)
50+
nightly=true
51+
shift
52+
;;
4653
--no-modify-path)
4754
no_modify_path=true
4855
shift
@@ -81,61 +88,111 @@ if [[ "$os" == "windows" ]]; then
8188
fi
8289
fi
8390

84-
# Resolve version and download tag.
85-
#
86-
# "nightly" is a special value that installs from the rolling nightly prerelease
87-
# built from the main branch. In this case both `version` and `download_tag`
88-
# are set to the literal string "nightly".
89-
#
90-
# For stable releases both are the same version string (e.g. "0.5.0").
91-
channel="stable"
92-
download_tag=""
93-
94-
if [[ -z "$requested_version" ]]; then
95-
version=$(curl -fsSL https://api.github.com/repos/getsentry/cli/releases/latest | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p')
96-
if [[ -z "$version" ]]; then
97-
echo -e "${RED}Failed to fetch latest version${NC}"
98-
exit 1
99-
fi
100-
download_tag="$version"
101-
elif [[ "$requested_version" == "nightly" ]]; then
102-
channel="nightly"
103-
download_tag="nightly"
104-
version="nightly"
105-
else
106-
version="$requested_version"
107-
download_tag="$requested_version"
91+
# Validate flag combinations
92+
if [[ "$nightly" == "true" && -n "$requested_version" ]]; then
93+
echo -e "${RED}Error: --nightly and --version are mutually exclusive${NC}"
94+
exit 1
10895
fi
10996

110-
# Strip leading 'v' if present (releases use version without 'v' prefix)
111-
version="${version#v}"
112-
download_tag="${download_tag#v}"
113-
114-
filename="sentry-${os}-${arch}${suffix}"
115-
url="https://github.com/getsentry/cli/releases/download/${download_tag}/${filename}"
11697

11798
# Download binary to a temp location
11899
tmpdir="${TMPDIR:-${TMP:-${TEMP:-/tmp}}}"
119100
tmp_binary="${tmpdir}/sentry-install-$$${suffix}"
101+
version=""
120102

121103
# Clean up temp binary on failure (setup handles cleanup on success)
122104
trap 'rm -f "$tmp_binary"' EXIT
123105

124-
# For nightly the version string is literally "nightly", not a semver, so
125-
# skip the "v" prefix that's only meaningful for numbered releases.
126-
if [[ "$version" == "nightly" ]]; then
127-
echo -e "${MUTED}Downloading sentry nightly...${NC}"
106+
if [[ "$nightly" == "true" ]]; then
107+
# Nightly build: download from GHCR via OCI blob protocol.
108+
# No jq needed — parse JSON with awk.
109+
# ghcr.io blob downloads redirect to Azure Blob Storage. curl -L would
110+
# forward the Authorization header to Azure, which returns 404. Instead,
111+
# extract the redirect URL and follow it without the auth header.
112+
113+
echo -e "${MUTED}Fetching nightly build from GHCR...${NC}"
114+
115+
# Step 1: Get anonymous pull token
116+
GHCR_TOKEN=$(curl -sf \
117+
"https://ghcr.io/token?scope=repository:getsentry/cli:pull" \
118+
| awk -F'"' '{for(i=1;i<=NF;i++) if($i=="token"){print $(i+2);exit}}')
119+
if [[ -z "$GHCR_TOKEN" ]]; then
120+
echo -e "${RED}Failed to get GHCR token${NC}"
121+
exit 1
122+
fi
123+
124+
# Step 2: Fetch the OCI manifest for the :nightly tag
125+
MANIFEST=$(curl -sf \
126+
-H "Authorization: Bearer $GHCR_TOKEN" \
127+
-H "Accept: application/vnd.oci.image.manifest.v1+json" \
128+
"https://ghcr.io/v2/getsentry/cli/manifests/nightly")
129+
if [[ -z "$MANIFEST" ]]; then
130+
echo -e "${RED}Failed to fetch nightly manifest from GHCR${NC}"
131+
exit 1
132+
fi
133+
134+
# Step 3: Extract version from manifest annotation
135+
version=$(echo "$MANIFEST" \
136+
| awk -F'"' '{for(i=1;i<=NF;i++) if($i=="version"){print $(i+2);exit}}')
137+
if [[ -z "$version" ]]; then
138+
echo -e "${RED}Failed to extract version from nightly manifest${NC}"
139+
exit 1
140+
fi
141+
142+
echo -e "${MUTED}Installing nightly sentry ${version}...${NC}"
143+
144+
# Step 4: Find the blob digest for this platform's .gz file
145+
gz_filename="sentry-${os}-${arch}${suffix}.gz"
146+
digest=$(echo "$MANIFEST" \
147+
| sed 's/},{/}\n{/g' \
148+
| awk -F'"' "/\"${gz_filename//./\\.}"'/{for(i=1;i<=NF;i++) if($i=="digest"){print $(i+2);exit}}')
149+
if [[ -z "$digest" ]]; then
150+
echo -e "${RED}No nightly build found for ${gz_filename}${NC}"
151+
exit 1
152+
fi
153+
154+
# Step 5: Get the redirect URL from the blob endpoint (don't use -L: auth
155+
# header must NOT be forwarded to the Azure Blob Storage redirect target)
156+
redir_url=$(curl -s -w '\n%{redirect_url}' -o /dev/null \
157+
-H "Authorization: Bearer $GHCR_TOKEN" \
158+
"https://ghcr.io/v2/getsentry/cli/blobs/${digest}" | tail -1)
159+
if [[ -z "$redir_url" ]]; then
160+
echo -e "${RED}Failed to get blob redirect URL from GHCR${NC}"
161+
exit 1
162+
fi
163+
164+
# Step 6: Download the .gz blob and decompress (without auth header)
165+
curl -sf "$redir_url" | gunzip > "$tmp_binary"
166+
128167
else
168+
# Stable build: resolve version and download from GitHub Releases.
169+
170+
if [[ -z "$requested_version" ]]; then
171+
version=$(curl -fsSL https://api.github.com/repos/getsentry/cli/releases/latest \
172+
| sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p')
173+
if [[ -z "$version" ]]; then
174+
echo -e "${RED}Failed to fetch latest version${NC}"
175+
exit 1
176+
fi
177+
else
178+
version="$requested_version"
179+
fi
180+
181+
# Strip leading 'v' if present (releases use version without 'v' prefix)
182+
version="${version#v}"
183+
filename="sentry-${os}-${arch}${suffix}"
184+
url="https://github.com/getsentry/cli/releases/download/${version}/${filename}"
185+
129186
echo -e "${MUTED}Downloading sentry v${version}...${NC}"
130-
fi
131187

132-
# Try gzip-compressed download first (~60% smaller, ~37 MB vs ~99 MB).
133-
# gunzip is POSIX and available on all Unix systems.
134-
# Falls back to raw binary if the .gz asset doesn't exist yet.
135-
if curl -fsSL "${url}.gz" 2>/dev/null | gunzip > "$tmp_binary" 2>/dev/null; then
136-
: # Compressed download succeeded
137-
else
138-
curl -fsSL --progress-bar "$url" -o "$tmp_binary"
188+
# Try gzip-compressed download first (~60% smaller, ~37 MB vs ~99 MB).
189+
# gunzip is POSIX and available on all Unix systems.
190+
# Falls back to raw binary if the .gz asset doesn't exist yet.
191+
if curl -fsSL "${url}.gz" 2>/dev/null | gunzip > "$tmp_binary" 2>/dev/null; then
192+
: # Compressed download succeeded
193+
else
194+
curl -fsSL --progress-bar "$url" -o "$tmp_binary"
195+
fi
139196
fi
140197

141198
chmod +x "$tmp_binary"
@@ -145,6 +202,10 @@ chmod +x "$tmp_binary"
145202
# completions, agent skills, and the welcome message.
146203
# --channel persists the release channel so future `sentry cli upgrade`
147204
# calls track the same channel without requiring a flag.
205+
channel="nightly"
206+
if [[ "$nightly" != "true" ]]; then
207+
channel="stable"
208+
fi
148209
setup_args="--install --method curl --channel $channel"
149210
if [[ "$no_modify_path" == "true" ]]; then
150211
setup_args="$setup_args --no-modify-path"

0 commit comments

Comments
 (0)