Skip to content

Commit ac8389a

Browse files
BYKGPT-6 Sol (OpenAI)
andauthored
Publish Toolkit CLI nightlies to GHCR (#1393)
## Summary - Stamp all five main-branch standalone binaries with the commit-timestamp nightly version. - Generate optional delta patches against the preceding Toolkit nightly and publish rolling plus versioned OCI manifests with job-scoped package permissions. - Verify anonymous access, source/version annotations, and all five compressed platform layers after publishing. Keep the installer on legacy GHCR until this public-read check succeeds on main. ## Validation - pnpm run tsc and pnpm run lint passed after generating the CLI schema. pnpm run test:ci-projects passed 16 tests. - Focused upgrade, GHCR, delta, installer, and CLI-upgrade suites passed 434 tests; the isolated ZIP suite passed 15 tests. - Full pnpm run test reached 10,254 passing CLI tests and 16 skipped; the unrelated ZipWriter large-content test timed out at 15 seconds under the full suite. Its isolated rerun passed. - pnpm run docs:check and pnpm run pre-commit:generated passed. Co-authored-by: GPT-6 Sol (OpenAI) <agent@openai.com>
1 parent 6054887 commit ac8389a

4 files changed

Lines changed: 187 additions & 2 deletions

File tree

‎.github/workflows/cli-build.yml‎

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ concurrency:
1313

1414
permissions:
1515
contents: read
16-
packages: write
1716

1817
env:
1918
SENTRY_CLIENT_ID: ${{ vars.SENTRY_CLIENT_ID }}
@@ -22,8 +21,20 @@ env:
2221
NODE_VERSION_24: "24.18.0"
2322

2423
jobs:
24+
nightly-version:
25+
name: Compute nightly version
26+
runs-on: ubuntu-latest
27+
outputs:
28+
version: ${{ steps.version.outputs.version }}
29+
steps:
30+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
31+
- name: Compute version from the commit timestamp
32+
id: version
33+
run: node scripts/cli-nightly-version.mjs
34+
2535
build-binary:
2636
name: Build Binary (${{ matrix.target }})
37+
needs: nightly-version
2738
runs-on: ${{ matrix.os }}
2839
environment: ${{ github.ref == 'refs/heads/main' && 'production' || startsWith(github.ref, 'refs/heads/release/cli/') && 'cli-release' || '' }}
2940
strategy:
@@ -49,6 +60,14 @@ jobs:
4960
cache: pnpm
5061
- name: Install dependencies
5162
run: pnpm install --frozen-lockfile
63+
- name: Stamp the nightly binary version
64+
if: github.ref == 'refs/heads/main'
65+
env:
66+
NIGHTLY_VERSION: ${{ needs.nightly-version.outputs.version }}
67+
shell: bash
68+
run: |
69+
jq --arg version "$NIGHTLY_VERSION" '.version = $version' packages/cli/package.json > "$RUNNER_TEMP/cli-package.json"
70+
mv "$RUNNER_TEMP/cli-package.json" packages/cli/package.json
5271
- name: Setup codesign dependencies
5372
env:
5473
APPLE_CERT_DATA: ${{ secrets.APPLE_CERT_DATA }}
@@ -105,8 +124,12 @@ jobs:
105124
SENTRY_AUTH_TOKEN: ""
106125
SENTRY_TOKEN: ""
107126
SENTRY_CONFIG_DIR: ${{ runner.temp }}/.sentry-smoke
127+
NIGHTLY_VERSION: ${{ needs.nightly-version.outputs.version }}
108128
run: |
109129
packages/cli/dist-bin/sentry-linux-x64 --help
130+
if [[ "$GITHUB_REF" == 'refs/heads/main' ]]; then
131+
test "$(packages/cli/dist-bin/sentry-linux-x64 --version)" = "$NIGHTLY_VERSION"
132+
fi
110133
output=$(packages/cli/dist-bin/sentry-linux-x64 auth status 2>&1) && status=$? || status=$?
111134
test "$status" -eq 10
112135
printf '%s\n' "$output" | grep -qi 'not authenticated'
@@ -123,6 +146,106 @@ jobs:
123146
name: sentry-${{ matrix.target }}-gz
124147
path: packages/cli/dist-bin/*.gz
125148

149+
generate-patches:
150+
name: Generate nightly delta patches
151+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
152+
needs: [nightly-version, build-binary]
153+
runs-on: ubuntu-latest
154+
continue-on-error: true
155+
permissions:
156+
contents: read
157+
packages: read
158+
outputs:
159+
from-version: ${{ steps.generate.outputs.from-version }}
160+
steps:
161+
- name: Download binaries and compressed artifacts
162+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
163+
with:
164+
pattern: sentry-*-*
165+
path: new-binaries
166+
merge-multiple: true
167+
- name: Generate patches against the preceding Toolkit nightly
168+
id: generate
169+
uses: BYK/binpatch/action@9ba6bbb8227fcbd2521852d2311c82afac5e9573 # 0.4.2
170+
with:
171+
mode: generate-ghcr
172+
version: ${{ needs.nightly-version.outputs.version }}
173+
registry: ghcr.io
174+
repo: getsentry/toolkit
175+
binary-glob: 'sentry-*'
176+
new-binaries-dir: new-binaries
177+
new-gz-dir: new-binaries
178+
patches-dir: patches
179+
- name: Upload patches
180+
if: steps.generate.outputs.has-patches == 'true'
181+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
182+
with:
183+
name: sentry-patches
184+
path: patches/*.patch
185+
186+
publish-nightly:
187+
name: Publish Toolkit nightly to GHCR
188+
if: >-
189+
always() && github.ref == 'refs/heads/main' && github.event_name == 'push' &&
190+
needs.nightly-version.result == 'success' && needs.build-binary.result == 'success' &&
191+
(needs.generate-patches.result == 'success' || needs.generate-patches.result == 'failure')
192+
needs: [nightly-version, build-binary, generate-patches]
193+
runs-on: ubuntu-latest
194+
permissions:
195+
contents: read
196+
packages: write
197+
steps:
198+
- name: Download compressed binaries
199+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
200+
with:
201+
pattern: sentry-*-gz
202+
path: artifacts
203+
merge-multiple: true
204+
- name: Download binaries for patch integrity annotations
205+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
206+
with:
207+
pattern: sentry-*-*
208+
path: binaries
209+
merge-multiple: true
210+
- name: Download optional delta patches
211+
id: patches
212+
continue-on-error: true
213+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
214+
with:
215+
name: sentry-patches
216+
path: patches
217+
- name: Publish rolling and versioned manifests
218+
uses: BYK/binpatch/action@9ba6bbb8227fcbd2521852d2311c82afac5e9573 # 0.4.2
219+
with:
220+
mode: publish-ghcr
221+
version: ${{ needs.nightly-version.outputs.version }}
222+
registry: ghcr.io
223+
repo: getsentry/toolkit
224+
binary-glob: 'sentry-*'
225+
artifacts-dir: artifacts
226+
binaries-dir: binaries
227+
patches-dir: patches
228+
from-version: ${{ needs.generate-patches.outputs.from-version }}
229+
- name: Verify public nightly and immutable version tag
230+
env:
231+
NIGHTLY_VERSION: ${{ needs.nightly-version.outputs.version }}
232+
shell: bash
233+
run: |
234+
set -euo pipefail
235+
token=$(curl -fsS 'https://ghcr.io/token?scope=repository:getsentry/toolkit:pull' | jq -er '.token')
236+
for tag in nightly "nightly-${NIGHTLY_VERSION}"; do
237+
manifest=$(curl -fsS \
238+
-H "Authorization: Bearer ${token}" \
239+
-H 'Accept: application/vnd.oci.image.manifest.v1+json' \
240+
"https://ghcr.io/v2/getsentry/toolkit/manifests/${tag}")
241+
jq -e --arg version "$NIGHTLY_VERSION" '
242+
.annotations.version == $version and
243+
.annotations["org.opencontainers.image.source"] == "https://github.com/getsentry/toolkit" and
244+
([.layers[].annotations["org.opencontainers.image.title"]] | sort) ==
245+
(["sentry-darwin-arm64.gz", "sentry-darwin-x64.gz", "sentry-linux-arm64.gz", "sentry-linux-x64.gz", "sentry-windows-x64.exe.gz"] | sort)
246+
' <<< "$manifest" > /dev/null
247+
done
248+
126249
build-npm:
127250
name: Build npm Package
128251
runs-on: ubuntu-latest

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
},
2424
"scripts": {
2525
"docs:check": "node scripts/check-doc-links.mjs",
26-
"test:ci-projects": "node --test scripts/ci-projects.test.mjs scripts/cloudflare-deployment.test.mjs scripts/deploy-workflow.test.mjs",
26+
"test:ci-projects": "node --test scripts/ci-projects.test.mjs scripts/cli-nightly-version.test.mjs scripts/cloudflare-deployment.test.mjs scripts/deploy-workflow.test.mjs",
2727
"dev": "pnpm --filter '@sentry/mcp-cloudflare...' --if-present run build && dotenv -e .env -e .env.local -- pnpm --parallel --filter @sentry/mcp-cloudflare --filter @sentry/mcp-core --filter @sentry/mcp-server-mocks --if-present run dev",
2828
"dev:stdio": "pnpm --filter '@sentry/mcp-server...' --if-present run build && dotenv -e .env -e .env.local -- pnpm --parallel --filter @sentry/mcp-server --filter @sentry/mcp-core --filter @sentry/mcp-server-mocks --if-present run dev",
2929
"build": "dotenv -e .env -e .env.local -- pnpm -r --filter '!sentry' --filter '!sentry-cli-docs' --if-present run build",

‎scripts/cli-nightly-version.mjs‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { execFileSync } from "node:child_process";
2+
import { appendFileSync, readFileSync } from "node:fs";
3+
import { pathToFileURL } from "node:url";
4+
5+
export function computeNightlyVersion(version, timestamp) {
6+
const base = /^(\d+\.\d+\.\d+)(?:-dev\.\d+)?$/.exec(version);
7+
if (
8+
!base ||
9+
!/^[1-9]\d*$/.test(timestamp) ||
10+
!Number.isSafeInteger(Number(timestamp))
11+
) {
12+
throw new Error(
13+
"Invalid CLI version or commit timestamp for nightly build",
14+
);
15+
}
16+
return `${base[1]}-dev.${timestamp}`;
17+
}
18+
19+
if (
20+
process.argv[1] &&
21+
import.meta.url === pathToFileURL(process.argv[1]).href
22+
) {
23+
const { version } = JSON.parse(
24+
readFileSync("packages/cli/package.json", "utf8"),
25+
);
26+
const timestamp = execFileSync(
27+
"git",
28+
["show", "-s", "--format=%ct", "HEAD"],
29+
{ encoding: "utf8" },
30+
).trim();
31+
const nightly = computeNightlyVersion(version, timestamp);
32+
if (process.env.GITHUB_OUTPUT) {
33+
appendFileSync(process.env.GITHUB_OUTPUT, `version=${nightly}\n`);
34+
}
35+
console.log(`Nightly version: ${nightly}`);
36+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
import { computeNightlyVersion } from "./cli-nightly-version.mjs";
4+
5+
test("stamps development and release versions with the same commit timestamp", () => {
6+
assert.equal(
7+
computeNightlyVersion("0.47.0-dev.0", "1791028800"),
8+
"0.47.0-dev.1791028800",
9+
);
10+
assert.equal(
11+
computeNightlyVersion("0.47.0", "1791028800"),
12+
"0.47.0-dev.1791028800",
13+
);
14+
});
15+
16+
test("rejects invalid inputs before they can become OCI tags", () => {
17+
for (const [version, timestamp] of [
18+
["0.47.0-dev.bad", "1791028800"],
19+
["0.47.0-rc.1", "1791028800"],
20+
["0.47.0", "0"],
21+
["0.47.0", "1791028800\nmalicious"],
22+
["0.47.0", "9007199254740992"],
23+
]) {
24+
assert.throws(() => computeNightlyVersion(version, timestamp));
25+
}
26+
});

0 commit comments

Comments
 (0)