Skip to content

Commit 7f7a368

Browse files
committed
ci(publish): add manual preview publish flow
1 parent bed338f commit 7f7a368

4 files changed

Lines changed: 76 additions & 93 deletions

File tree

.github/workflows/publish.yaml

Lines changed: 10 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,31 @@
22
# Publish
33
# =============================================================================
44
#
5-
# Single workflow for preview publish (PRs + main pushes) AND release cuts
6-
# (workflow_dispatch). The build, npm publish, R2 upload, and Docker manifest
7-
# steps run identically for all three triggers. Only the npm dist-tag,
5+
# Single workflow for preview publish AND release cuts. Both triggers are
6+
# manual (workflow_dispatch). The build, npm publish, R2 upload, and Docker
7+
# manifest steps run identically for both modes. Only the npm dist-tag,
88
# release-specific retagging, git tag, and GitHub release differ.
99
#
1010
# Triggers and mapping:
11-
# pull_request → trigger=pr npm_tag=pr-N build_mode=debug
12-
# push: main → trigger=main npm_tag=main build_mode=debug
13-
# workflow_dispatch → trigger=release npm_tag=latest build_mode=release
14-
# (npm_tag becomes `rc` if version contains `-rc.`, or
15-
# `next` if `latest=false`)
11+
# workflow_dispatch (no version) → trigger=branch npm_tag=<sanitized branch> build_mode=debug
12+
# workflow_dispatch (with version) → trigger=release npm_tag=latest build_mode=release
13+
# (npm_tag becomes `rc` if version contains `-rc.`, or
14+
# `next` if `latest=false`)
1615
#
1716
# See .agent/specs/publish-flow-unification.md for the design.
1817
# ============================================================================
1918

2019
name: publish
2120

2221
on:
23-
pull_request:
24-
push:
25-
branches:
26-
- main
2722
workflow_dispatch:
2823
inputs:
2924
version:
30-
description: "Version to release (e.g. 2.5.0 or 2.5.0-rc.1)"
31-
required: true
25+
description: "Version to release (e.g. 2.5.0 or 2.5.0-rc.1). Leave empty for preview publish on the dispatched branch."
26+
required: false
3227
type: string
3328
latest:
34-
description: "Tag as @latest"
29+
description: "Tag as @latest (release only)"
3530
required: true
3631
type: boolean
3732
default: true
@@ -53,8 +48,6 @@ jobs:
5348
npm_tag: ${{ steps.ctx.outputs.npm_tag }}
5449
sha: ${{ steps.ctx.outputs.sha }}
5550
latest: ${{ steps.ctx.outputs.latest }}
56-
pr_number: ${{ steps.ctx.outputs.pr_number }}
57-
is_fork: ${{ steps.fork.outputs.is_fork }}
5851
steps:
5952
- uses: actions/checkout@v4
6053
with:
@@ -69,22 +62,13 @@ jobs:
6962
- id: ctx
7063
name: Resolve publish context
7164
run: pnpm --filter=publish exec tsx src/ci/bin.ts context-output
72-
- name: Compute is_fork flag
73-
id: fork
74-
run: |
75-
if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ github.event.pull_request.head.repo.fork }}" = "true" ]; then
76-
echo "is_fork=true" >> $GITHUB_OUTPUT
77-
else
78-
echo "is_fork=false" >> $GITHUB_OUTPUT
79-
fi
8065

8166
# ---------------------------------------------------------------------------
8267
# build — matrix of native/engine artifacts
8368
# ---------------------------------------------------------------------------
8469
build:
8570
needs: [context]
8671
name: "Build ${{ matrix.name }}"
87-
if: needs.context.outputs.is_fork != 'true'
8872
strategy:
8973
fail-fast: false
9074
matrix:
@@ -238,7 +222,6 @@ jobs:
238222
build-wasm:
239223
needs: [context]
240224
name: "Build rivetkit-wasm"
241-
if: needs.context.outputs.is_fork != 'true'
242225
runs-on: depot-ubuntu-24.04-8
243226
permissions:
244227
contents: read
@@ -277,7 +260,6 @@ jobs:
277260
docker-images:
278261
needs: [context]
279262
name: "Docker ${{ matrix.arch_suffix }}"
280-
if: needs.context.outputs.is_fork != 'true'
281263
strategy:
282264
fail-fast: false
283265
matrix:
@@ -341,7 +323,6 @@ jobs:
341323
name: "Publish"
342324
if: |
343325
!cancelled() &&
344-
needs.context.outputs.is_fork != 'true' &&
345326
needs.build.result == 'success' &&
346327
needs.build-wasm.result == 'success' &&
347328
needs.docker-images.result == 'success'
@@ -530,15 +511,3 @@ jobs:
530511
env:
531512
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
532513
run: pnpm --filter=publish exec tsx src/ci/bin.ts gh-release --version ${{ needs.context.outputs.version }}
533-
534-
# ---- preview-only tail ----
535-
- name: Comment on PR
536-
if: needs.context.outputs.trigger == 'pr'
537-
env:
538-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
539-
GITHUB_REPOSITORY: ${{ github.repository }}
540-
run: |
541-
pnpm --filter=publish exec tsx src/ci/bin.ts comment-pr \
542-
--pr-number ${{ needs.context.outputs.pr_number }} \
543-
--version ${{ needs.context.outputs.version }} \
544-
--tag ${{ needs.context.outputs.npm_tag }}

justfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
release *ARGS:
33
pnpm --filter=publish release {{ ARGS }}
44

5+
[group('release')]
6+
preview-publish:
7+
gh workflow run .github/workflows/publish.yaml --ref "$(git rev-parse --abbrev-ref HEAD)"
8+
59
[group('docker')]
610
docker-build:
711
docker build -f engine/docker/universal/Dockerfile --target engine-full -t rivetdev/engine:local --platform linux/x86_64 .

scripts/publish/src/ci/bin.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,21 +65,21 @@ program.name("ci").description("CI subcommands for the publish flow");
6565
program
6666
.command("context-output")
6767
.description("Resolve publish context and write to $GITHUB_OUTPUT")
68-
.option("--trigger <trigger>", "Override trigger (pr|main|release)")
68+
.option("--trigger <trigger>", "Override trigger (branch|release)")
6969
.option("--version <version>", "Override version")
7070
.option("--latest <bool>", "Override latest")
71-
.option("--pr-number <number>", "Override PR number")
71+
.option("--branch <name>", "Override branch name")
7272
.action(async (opts) => {
7373
const overrides: Parameters<typeof resolveContext>[0] = {};
7474
if (opts.trigger) overrides.trigger = opts.trigger as Trigger;
7575
if (opts.version) overrides.version = opts.version;
7676
if (opts.latest !== undefined) {
7777
overrides.latest = opts.latest === "true";
7878
}
79-
if (opts.prNumber) overrides.prNumber = Number(opts.prNumber);
79+
if (opts.branch) overrides.branch = opts.branch;
8080
const ctx = await resolveContext(overrides);
8181
log.info(
82-
`resolved: trigger=${ctx.trigger} version=${ctx.version} npm_tag=${ctx.npmTag} sha=${ctx.sha} latest=${ctx.latest}${ctx.prNumber !== undefined ? ` pr=${ctx.prNumber}` : ""}`,
82+
`resolved: trigger=${ctx.trigger} version=${ctx.version} npm_tag=${ctx.npmTag} sha=${ctx.sha} latest=${ctx.latest}${ctx.branch !== undefined ? ` branch=${ctx.branch}` : ""}`,
8383
);
8484
writeContextToGithubOutput(ctx);
8585
});
@@ -296,16 +296,16 @@ program
296296
program
297297
.command("comment-pr")
298298
.description("Upsert the preview PR comment with install instructions")
299-
.option("--pr-number <number>", "PR number (defaults to context)")
299+
.requiredOption("--pr-number <number>", "PR number to comment on")
300300
.option("--version <version>", "Version (defaults to context)")
301301
.option("--tag <tag>", "npm dist-tag (defaults to context)")
302302
.action(async (opts) => {
303303
const ctx = await resolveContext();
304-
const prNumber = opts.prNumber ? Number(opts.prNumber) : ctx.prNumber;
304+
const prNumber = Number(opts.prNumber);
305305
const version: string = opts.version ?? ctx.version;
306306
const tag: string = opts.tag ?? ctx.npmTag;
307-
if (typeof prNumber !== "number") {
308-
throw new Error("comment-pr requires a PR number");
307+
if (!Number.isFinite(prNumber)) {
308+
throw new Error("comment-pr requires a numeric --pr-number");
309309
}
310310
const repo = process.env.GITHUB_REPOSITORY;
311311
if (!repo) {

scripts/publish/src/lib/context.ts

Lines changed: 54 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { $ } from "execa";
88
* subcommand and passed through every subsequent step via GitHub Actions job
99
* outputs + per-step flags.
1010
*/
11-
export type Trigger = "pr" | "main" | "release";
11+
export type Trigger = "branch" | "release";
1212

1313
export interface PublishContext {
1414
trigger: Trigger;
@@ -20,7 +20,8 @@ export interface PublishContext {
2020
sha: string;
2121
/** Only meaningful when trigger === "release". */
2222
latest: boolean;
23-
prNumber?: number;
23+
/** Branch name. Only set when trigger === "branch". */
24+
branch?: string;
2425
repoRoot: string;
2526
}
2627

@@ -29,7 +30,7 @@ export interface ResolveOverrides {
2930
trigger?: Trigger;
3031
version?: string;
3132
latest?: boolean;
32-
prNumber?: number;
33+
branch?: string;
3334
sha?: string;
3435
}
3536

@@ -47,10 +48,11 @@ function findRepoRoot(): string {
4748

4849
/**
4950
* Base for all preview pre-release strings. Hardcoded to `0.0.0` so preview
50-
* versions like `0.0.0-pr.4600.abc1234` never look like real releases and
51+
* versions like `0.0.0-my-branch.abc1234` never look like real releases and
5152
* always sort below any published `X.Y.Z`. Using a committed `package.json`
5253
* version as the base would just embed whatever stale number happened to be
53-
* committed there — it has no semantic relationship to the PR being previewed.
54+
* committed there. It has no semantic relationship to the branch being
55+
* previewed.
5456
*/
5557
const PREVIEW_BASE_VERSION = "0.0.0";
5658

@@ -61,22 +63,31 @@ async function readShortSha(repoRoot: string): Promise<string> {
6163
return stdout.trim().slice(0, 7);
6264
}
6365

64-
function readPrNumberFromEvent(): number | undefined {
65-
const path = process.env.GITHUB_EVENT_PATH;
66-
if (!path || !existsSync(path)) return undefined;
67-
try {
68-
const event = JSON.parse(readFileSync(path, "utf-8")) as {
69-
pull_request?: { number?: number };
70-
number?: number;
71-
};
72-
if (typeof event.pull_request?.number === "number") {
73-
return event.pull_request.number;
74-
}
75-
if (typeof event.number === "number") return event.number;
76-
} catch {
77-
// fall through
66+
async function readBranchName(repoRoot: string): Promise<string> {
67+
const envRef = process.env.GITHUB_REF_NAME;
68+
if (envRef) return envRef;
69+
const { stdout } = await $({
70+
cwd: repoRoot,
71+
})`git rev-parse --abbrev-ref HEAD`;
72+
return stdout.trim();
73+
}
74+
75+
/**
76+
* Sanitize a branch name into something safe to use as both an npm dist-tag
77+
* and a semver prerelease identifier. Lowercases, replaces any
78+
* non-alphanumeric character (other than hyphens) with a hyphen, collapses
79+
* runs of hyphens, and trims leading/trailing hyphens.
80+
*/
81+
function sanitizeBranch(branch: string): string {
82+
const cleaned = branch
83+
.toLowerCase()
84+
.replace(/[^a-z0-9-]+/g, "-")
85+
.replace(/-+/g, "-")
86+
.replace(/^-+|-+$/g, "");
87+
if (cleaned.length === 0) {
88+
throw new Error(`branch name "${branch}" sanitized to empty string`);
7889
}
79-
return undefined;
90+
return cleaned;
8091
}
8192

8293
function readInputFromEvent<T = unknown>(name: string): T | undefined {
@@ -105,10 +116,11 @@ function parseBoolInput(v: unknown, fallback: boolean): boolean {
105116
function deriveTrigger(overrides: ResolveOverrides | undefined): Trigger {
106117
if (overrides?.trigger) return overrides.trigger;
107118
const eventName = process.env.GITHUB_EVENT_NAME;
108-
if (eventName === "pull_request" || eventName === "pull_request_target")
109-
return "pr";
110-
if (eventName === "workflow_dispatch") return "release";
111-
if (eventName === "push") return "main";
119+
if (eventName === "workflow_dispatch") {
120+
const version = readInputFromEvent<string>("version");
121+
if (typeof version === "string" && version.length > 0) return "release";
122+
return "branch";
123+
}
112124
// Default for local invocation without overrides (unusual): assume release
113125
// so missing fields are caught loudly.
114126
return "release";
@@ -118,15 +130,14 @@ function computeNpmTag(
118130
trigger: Trigger,
119131
version: string,
120132
latest: boolean,
121-
prNumber?: number,
133+
branch?: string,
122134
): string {
123-
if (trigger === "pr") {
124-
if (typeof prNumber !== "number") {
125-
throw new Error("PR trigger requires prNumber to compute npm tag");
135+
if (trigger === "branch") {
136+
if (!branch) {
137+
throw new Error("branch trigger requires branch to compute npm tag");
126138
}
127-
return `pr-${prNumber}`;
139+
return sanitizeBranch(branch);
128140
}
129-
if (trigger === "main") return "main";
130141
// release
131142
if (version.includes("-rc.")) return "rc";
132143
return latest ? "latest" : "next";
@@ -136,17 +147,16 @@ function computeVersion(
136147
trigger: Trigger,
137148
base: string,
138149
sha: string,
139-
prNumber: number | undefined,
150+
branch: string | undefined,
140151
overrideVersion: string | undefined,
141152
): string {
142153
if (overrideVersion) return overrideVersion;
143-
if (trigger === "pr") {
144-
if (typeof prNumber !== "number") {
145-
throw new Error("PR trigger requires prNumber to compute version");
154+
if (trigger === "branch") {
155+
if (!branch) {
156+
throw new Error("branch trigger requires branch to compute version");
146157
}
147-
return `${base}-pr.${prNumber}.${sha}`;
158+
return `${base}-${sanitizeBranch(branch)}.${sha}`;
148159
}
149-
if (trigger === "main") return `${base}-main.${sha}`;
150160
throw new Error("release trigger requires an explicit version override");
151161
}
152162

@@ -164,9 +174,9 @@ export async function resolveContext(
164174

165175
const sha = overrides.sha ?? (await readShortSha(repoRoot));
166176

167-
let prNumber = overrides.prNumber;
168-
if (trigger === "pr" && prNumber === undefined) {
169-
prNumber = readPrNumberFromEvent();
177+
let branch = overrides.branch;
178+
if (trigger === "branch" && !branch) {
179+
branch = await readBranchName(repoRoot);
170180
}
171181

172182
// Release version: override > workflow_dispatch input > error.
@@ -181,7 +191,7 @@ export async function resolveContext(
181191
trigger,
182192
PREVIEW_BASE_VERSION,
183193
sha,
184-
prNumber,
194+
branch,
185195
version,
186196
);
187197
} else if (!version) {
@@ -198,15 +208,15 @@ export async function resolveContext(
198208
}
199209
if (trigger !== "release") latest = false;
200210

201-
const npmTag = computeNpmTag(trigger, version, latest, prNumber);
211+
const npmTag = computeNpmTag(trigger, version, latest, branch);
202212

203213
return {
204214
trigger,
205215
version,
206216
npmTag,
207217
sha,
208218
latest,
209-
prNumber,
219+
branch,
210220
repoRoot,
211221
};
212222
}
@@ -221,7 +231,7 @@ export function writeContextToGithubOutput(ctx: PublishContext): void {
221231
console.log(`npm_tag=${ctx.npmTag}`);
222232
console.log(`sha=${ctx.sha}`);
223233
console.log(`latest=${ctx.latest}`);
224-
if (ctx.prNumber !== undefined) console.log(`pr_number=${ctx.prNumber}`);
234+
if (ctx.branch !== undefined) console.log(`branch=${ctx.branch}`);
225235
return;
226236
}
227237
const lines = [
@@ -231,7 +241,7 @@ export function writeContextToGithubOutput(ctx: PublishContext): void {
231241
`sha=${ctx.sha}`,
232242
`latest=${ctx.latest}`,
233243
];
234-
if (ctx.prNumber !== undefined) lines.push(`pr_number=${ctx.prNumber}`);
244+
if (ctx.branch !== undefined) lines.push(`branch=${ctx.branch}`);
235245
// Append (do not overwrite) in case other steps also wrote to GITHUB_OUTPUT.
236246
appendFileSync(path, `${lines.join("\n")}\n`);
237247
}

0 commit comments

Comments
 (0)