Skip to content

Commit 78b41ee

Browse files
committed
ci(cli): publish pkg.pr.new previews on demand
1 parent 8680362 commit 78b41ee

6 files changed

Lines changed: 358 additions & 13 deletions

File tree

.github/workflows/pkg-pr-new.yml

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
name: pkg.pr.new
2+
3+
on:
4+
workflow_run:
5+
workflows:
6+
- Test
7+
- Smoke Test (PR)
8+
types:
9+
- completed
10+
pull_request_review:
11+
types:
12+
- submitted
13+
14+
permissions:
15+
actions: read
16+
checks: read
17+
contents: read
18+
issues: write
19+
pull-requests: read
20+
21+
concurrency:
22+
group: pkg-pr-new-${{ github.event.workflow_run.head_sha || github.event.pull_request.head.sha || github.run_id }}
23+
cancel-in-progress: false
24+
25+
jobs:
26+
publish:
27+
name: Publish preview package
28+
runs-on: ubuntu-latest
29+
if: >-
30+
(
31+
github.event_name == 'workflow_run' &&
32+
github.event.workflow_run.event == 'pull_request' &&
33+
github.event.workflow_run.conclusion == 'success'
34+
) ||
35+
(
36+
github.event_name == 'pull_request_review' &&
37+
github.event.review.state == 'approved' &&
38+
github.event.pull_request.draft == false &&
39+
github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name
40+
)
41+
env:
42+
GH_TOKEN: ${{ github.token }}
43+
steps:
44+
- name: Resolve publish context
45+
id: context
46+
env:
47+
EVENT_NAME: ${{ github.event_name }}
48+
REPOSITORY: ${{ github.repository }}
49+
run: |
50+
set -euo pipefail
51+
52+
should_publish=false
53+
pr_number=""
54+
pr_head_sha=""
55+
smoke_run_id=""
56+
test_run_id=""
57+
58+
if [[ "${EVENT_NAME}" == "workflow_run" ]]; then
59+
pr_number="$(jq -r '.workflow_run.pull_requests[0].number // ""' "$GITHUB_EVENT_PATH")"
60+
pr_head_sha="$(jq -r '.workflow_run.head_sha // ""' "$GITHUB_EVENT_PATH")"
61+
pr_head_branch="$(jq -r '.workflow_run.head_branch // ""' "$GITHUB_EVENT_PATH")"
62+
triggering_workflow="$(jq -r '.workflow_run.name // ""' "$GITHUB_EVENT_PATH")"
63+
64+
if [[ -z "${pr_head_sha}" ]]; then
65+
echo "Workflow run has no head SHA; skipping."
66+
echo "should_publish=false" >> "$GITHUB_OUTPUT"
67+
exit 0
68+
fi
69+
70+
if [[ -z "${pr_number}" && -n "${pr_head_branch}" ]]; then
71+
pr_number="$(
72+
gh pr list \
73+
--repo "${REPOSITORY}" \
74+
--head "${pr_head_branch}" \
75+
--state open \
76+
--json number,headRefOid \
77+
--jq 'map(select(.headRefOid == "'"${pr_head_sha}"'")) | .[0].number // ""'
78+
)"
79+
fi
80+
81+
if [[ -z "${pr_number}" ]]; then
82+
echo "Workflow run is not associated with an open pull request; skipping."
83+
echo "should_publish=false" >> "$GITHUB_OUTPUT"
84+
exit 0
85+
fi
86+
87+
if [[ "${triggering_workflow}" == "Smoke Test (PR)" ]]; then
88+
smoke_run_id="$(jq -r '.workflow_run.id' "$GITHUB_EVENT_PATH")"
89+
elif [[ "${triggering_workflow}" == "Test" ]]; then
90+
test_run_id="$(jq -r '.workflow_run.id' "$GITHUB_EVENT_PATH")"
91+
else
92+
echo "Unexpected triggering workflow: ${triggering_workflow}; skipping."
93+
echo "should_publish=false" >> "$GITHUB_OUTPUT"
94+
exit 0
95+
fi
96+
else
97+
pr_number="$(jq -r '.pull_request.number' "$GITHUB_EVENT_PATH")"
98+
pr_head_sha="$(jq -r '.pull_request.head.sha' "$GITHUB_EVENT_PATH")"
99+
fi
100+
101+
pr_json="$(gh api "repos/${REPOSITORY}/pulls/${pr_number}")"
102+
current_head_sha="$(jq -r '.head.sha' <<< "${pr_json}")"
103+
draft="$(jq -r '.draft' <<< "${pr_json}")"
104+
head_repo="$(jq -r '.head.repo.full_name' <<< "${pr_json}")"
105+
base_repo="$(jq -r '.base.repo.full_name' <<< "${pr_json}")"
106+
107+
if [[ "${draft}" == "true" ]]; then
108+
echo "PR #${pr_number} is draft; skipping."
109+
echo "should_publish=false" >> "$GITHUB_OUTPUT"
110+
exit 0
111+
fi
112+
113+
if [[ "${head_repo}" != "${base_repo}" ]]; then
114+
echo "PR #${pr_number} comes from fork ${head_repo}; skipping."
115+
echo "should_publish=false" >> "$GITHUB_OUTPUT"
116+
exit 0
117+
fi
118+
119+
if [[ "${pr_head_sha}" != "${current_head_sha}" ]]; then
120+
echo "Workflow SHA ${pr_head_sha} is stale; current PR head is ${current_head_sha}. Skipping."
121+
echo "should_publish=false" >> "$GITHUB_OUTPUT"
122+
exit 0
123+
fi
124+
125+
review_decision="$(
126+
gh api graphql \
127+
-f owner="${REPOSITORY%/*}" \
128+
-f repo="${REPOSITORY#*/}" \
129+
-F number="${pr_number}" \
130+
-f query='
131+
query($owner: String!, $repo: String!, $number: Int!) {
132+
repository(owner: $owner, name: $repo) {
133+
pullRequest(number: $number) {
134+
reviewDecision
135+
}
136+
}
137+
}
138+
' \
139+
--jq '.data.repository.pullRequest.reviewDecision // ""'
140+
)"
141+
142+
if [[ "${review_decision}" != "APPROVED" ]]; then
143+
echo "PR #${pr_number} review decision is ${review_decision:-<none>}; skipping."
144+
echo "should_publish=false" >> "$GITHUB_OUTPUT"
145+
exit 0
146+
fi
147+
148+
if [[ -z "${smoke_run_id}" ]]; then
149+
smoke_run_id="$(
150+
gh run list \
151+
--workflow smoke-test-pr.yml \
152+
--event pull_request \
153+
--commit "${pr_head_sha}" \
154+
--status success \
155+
--json databaseId,conclusion,headSha \
156+
--jq 'map(select(.headSha == "'"${pr_head_sha}"'" and .conclusion == "success")) | .[0].databaseId // ""'
157+
)"
158+
fi
159+
160+
if [[ -z "${test_run_id}" ]]; then
161+
test_run_id="$(
162+
gh run list \
163+
--workflow test.yml \
164+
--event pull_request \
165+
--commit "${pr_head_sha}" \
166+
--status success \
167+
--json databaseId,conclusion,headSha \
168+
--jq 'map(select(.headSha == "'"${pr_head_sha}"'" and .conclusion == "success")) | .[0].databaseId // ""'
169+
)"
170+
fi
171+
172+
if [[ -z "${smoke_run_id}" ]]; then
173+
echo "No successful Smoke Test (PR) run found for ${pr_head_sha}; skipping."
174+
echo "should_publish=false" >> "$GITHUB_OUTPUT"
175+
exit 0
176+
fi
177+
178+
if [[ -z "${test_run_id}" ]]; then
179+
echo "No successful Test run found for ${pr_head_sha}; skipping."
180+
echo "should_publish=false" >> "$GITHUB_OUTPUT"
181+
exit 0
182+
fi
183+
184+
artifact_name="$(
185+
gh api "repos/${REPOSITORY}/actions/runs/${smoke_run_id}/artifacts" \
186+
--paginate \
187+
--jq '.artifacts[] | select(.name | startswith("cli-build-legacy-")) | .name' \
188+
| head -n 1
189+
)"
190+
191+
if [[ -z "${artifact_name}" ]]; then
192+
echo "No legacy CLI build artifact found on Smoke Test (PR) run ${smoke_run_id}; skipping."
193+
echo "should_publish=false" >> "$GITHUB_OUTPUT"
194+
exit 0
195+
fi
196+
197+
preview_version="${artifact_name#cli-build-legacy-}"
198+
should_publish=true
199+
200+
{
201+
echo "should_publish=${should_publish}"
202+
echo "pr_number=${pr_number}"
203+
echo "pr_head_sha=${pr_head_sha}"
204+
echo "preview_version=${preview_version}"
205+
echo "artifact_name=${artifact_name}"
206+
echo "smoke_run_id=${smoke_run_id}"
207+
} >> "$GITHUB_OUTPUT"
208+
209+
- name: Checkout
210+
if: steps.context.outputs.should_publish == 'true'
211+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
212+
with:
213+
ref: ${{ steps.context.outputs.pr_head_sha }}
214+
persist-credentials: false
215+
216+
- name: Setup
217+
if: steps.context.outputs.should_publish == 'true'
218+
uses: ./.github/actions/setup
219+
220+
- name: Download smoke artifacts
221+
if: steps.context.outputs.should_publish == 'true'
222+
run: gh run download "${SMOKE_RUN_ID}" --name "${ARTIFACT_NAME}"
223+
env:
224+
ARTIFACT_NAME: ${{ steps.context.outputs.artifact_name }}
225+
SMOKE_RUN_ID: ${{ steps.context.outputs.smoke_run_id }}
226+
227+
- name: Prepare package files
228+
if: steps.context.outputs.should_publish == 'true'
229+
run: |
230+
set -euo pipefail
231+
pnpm exec bun apps/cli/scripts/sync-versions.ts --version "${PREVIEW_VERSION}"
232+
pnpm --dir apps/cli build:shim
233+
find packages -path '*/bin/supabase*' -type f -exec chmod +x {} +
234+
env:
235+
PREVIEW_VERSION: ${{ steps.context.outputs.preview_version }}
236+
237+
- name: Publish preview package
238+
id: publish
239+
if: steps.context.outputs.should_publish == 'true'
240+
run: |
241+
pnpm exec pkg-pr-new publish \
242+
--pnpm \
243+
--bin \
244+
--comment=off \
245+
--json pkg-pr-new.json \
246+
--no-template \
247+
'./packages/cli-darwin-arm64' \
248+
'./packages/cli-darwin-x64' \
249+
'./packages/cli-linux-arm64' \
250+
'./packages/cli-linux-arm64-musl' \
251+
'./packages/cli-linux-x64' \
252+
'./packages/cli-linux-x64-musl' \
253+
'./packages/cli-windows-arm64' \
254+
'./packages/cli-windows-x64' \
255+
'./apps/cli'
256+
257+
- name: Smoke test preview command
258+
if: steps.context.outputs.should_publish == 'true'
259+
run: |
260+
set -euo pipefail
261+
preview_url="https://pkg.pr.new/supabase@${PR_NUMBER}"
262+
npx --yes "${preview_url}" --version
263+
env:
264+
PR_NUMBER: ${{ steps.context.outputs.pr_number }}
265+
266+
- name: Update PR comment
267+
if: steps.context.outputs.should_publish == 'true'
268+
env:
269+
PR_HEAD_SHA: ${{ steps.context.outputs.pr_head_sha }}
270+
PR_NUMBER: ${{ steps.context.outputs.pr_number }}
271+
PREVIEW_VERSION: ${{ steps.context.outputs.preview_version }}
272+
REPOSITORY: ${{ github.repository }}
273+
run: |
274+
set -euo pipefail
275+
preview_url="https://pkg.pr.new/supabase@${PR_NUMBER}"
276+
short_sha="${PR_HEAD_SHA:0:7}"
277+
marker="<!-- pkg-pr-new-preview -->"
278+
cat > comment.md <<EOF
279+
${marker}
280+
## pkg.pr.new preview
281+
282+
Published version \`${PREVIEW_VERSION}\` from commit [\`${short_sha}\`](https://github.com/${REPOSITORY}/commit/${PR_HEAD_SHA}) after the PR test and release smoke workflows passed.
283+
284+
\`\`\`sh
285+
npx ${preview_url}
286+
\`\`\`
287+
EOF
288+
289+
jq -n --rawfile body comment.md '{ body: $body }' > comment.json
290+
comment_id="$(
291+
gh api "repos/${REPOSITORY}/issues/${PR_NUMBER}/comments" \
292+
--paginate \
293+
--jq '.[] | select(.body | contains("'"${marker}"'")) | .id' \
294+
| head -n1
295+
)"
296+
297+
if [[ -n "${comment_id}" ]]; then
298+
gh api --method PATCH "repos/${REPOSITORY}/issues/comments/${comment_id}" --input comment.json >/dev/null
299+
else
300+
gh api --method POST "repos/${REPOSITORY}/issues/${PR_NUMBER}/comments" --input comment.json >/dev/null
301+
fi

.github/workflows/release-shared.yml

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,13 @@ jobs:
113113
needs: build
114114
strategy:
115115
fail-fast: false
116-
# macos-15-intel is the slowest smoke leg and the only one not on
117-
# Blacksmith (Blacksmith macOS is ARM-only). Drop it from the matrix
118-
# on prereleases (PR smoke + develop -> beta) so beta wall-clock isn't
119-
# gated by it; stable releases on main still run the full matrix.
120-
# The matrix list is built via fromJSON because GitHub Actions does
121-
# not allow the `matrix` context in a job-level `if:` (matrix
122-
# expansion happens after job conditions are evaluated).
123116
matrix:
124-
runner: ${{ fromJSON(inputs.prerelease && '["blacksmith-8vcpu-ubuntu-2404","blacksmith-6vcpu-macos-latest","blacksmith-8vcpu-windows-2025"]' || '["blacksmith-8vcpu-ubuntu-2404","blacksmith-6vcpu-macos-latest","macos-15-intel","blacksmith-8vcpu-windows-2025"]') }}
117+
runner:
118+
- blacksmith-8vcpu-ubuntu-2404
119+
- blacksmith-6vcpu-macos-latest
120+
- macos-15-intel
121+
- blacksmith-8vcpu-windows-2025
122+
- windows-11-arm
125123
runs-on: ${{ matrix.runner }}
126124
env:
127125
NPM_TAG: ${{ inputs.npm_tag }}

.github/workflows/smoke-test-pr.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ jobs:
4646
if: github.event.pull_request.draft == false
4747
uses: ./.github/workflows/release-shared.yml
4848
with:
49-
# PR-scoped version so concurrent PRs don't collide on the build artifact
50-
# name (release-shared.yml uploads `cli-build-${shell}-${version}`).
51-
version: 0.0.0-pr-${{ github.event.pull_request.number }}
49+
# PR-scoped version so each PR has one preview package while every
50+
# approved commit can update that package.
51+
version: 0.0.0-pr.${{ github.event.pull_request.number }}
5252
shell: legacy
5353
npm_tag: latest
5454
channel: beta

apps/cli/tests/smoke-test-windows.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { $ } from "bun";
2+
import { mkdtemp, rm } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
24
import path from "node:path";
35
import process from "node:process";
46
import { parseArgs } from "node:util";
@@ -32,9 +34,11 @@ console.log(`\n${"=".repeat(60)}`);
3234
console.log("Native binary tests");
3335
console.log("=".repeat(60));
3436

37+
const arch = process.arch === "arm64" ? "arm64" : "x64";
38+
3539
{
36-
const name = "native-windows-x64";
37-
const binPath = path.join(root, "packages", "cli-windows-x64", "bin", "supabase.exe");
40+
const name = `native-windows-${arch}`;
41+
const binPath = path.join(root, "packages", `cli-windows-${arch}`, "bin", "supabase.exe");
3842

3943
console.log(`[${name}] Running ${binPath} --version...`);
4044
try {
@@ -51,6 +55,38 @@ console.log("=".repeat(60));
5155
}
5256
}
5357

58+
// --- Release tarball ---
59+
60+
console.log(`\n${"=".repeat(60)}`);
61+
console.log("Release tarball test");
62+
console.log("=".repeat(60));
63+
64+
{
65+
const archiveArch = arch === "arm64" ? "arm64" : "amd64";
66+
const name = `windows-${archiveArch}-tarball`;
67+
const archivePath = path.join(root, "dist", `supabase_${version}_windows_${archiveArch}.tar.gz`);
68+
const extractDir = await mkdtemp(path.join(tmpdir(), "supabase-windows-tarball-"));
69+
70+
console.log(`[${name}] Extracting ${archivePath}...`);
71+
try {
72+
await $`tar -xzf ${archivePath} -C ${extractDir}`;
73+
const binPath = path.join(extractDir, "supabase.exe");
74+
const output = await $`${binPath} --version`.text();
75+
const trimmed = output.trim();
76+
const shellCheck = await verifyExpectedShell(binPath);
77+
const passed = /^\d+\.\d+\.\d+/.test(trimmed) && shellCheck.passed;
78+
79+
console.log(`[${name}] ${passed ? "PASS" : "FAIL"}${trimmed}`);
80+
console.log(`[${name}] ${shellCheck.detail}`);
81+
results.push({ name, status: passed ? "pass" : "fail" });
82+
} catch (e) {
83+
console.error(`[${name}] Error: ${e}`);
84+
results.push({ name, status: "fail" });
85+
} finally {
86+
await rm(extractDir, { recursive: true, force: true });
87+
}
88+
}
89+
5490
// --- Scoop ---
5591

5692
console.log(`\n${"=".repeat(60)}`);

0 commit comments

Comments
 (0)