Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 56 additions & 6 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
# (npm_tag becomes `rc` if version contains `-rc.`, or
# `next` if `latest=false`)
#
# Selective preview builds: the `targets` input (all | rivetkit | container-runner
# | engine | cli) narrows a preview to one target family so unrelated builds are
# skipped. The `context` job resolves it into `build_targets` / `build_wasm` /
# `build_docker` outputs that gate the matrix and jobs. Skipped npm families have
# their inbound dependency (rivetkit-family → @rivetkit/engine-cli) repinned to the
# latest published version so previews still install. Releases always build every
# target; a partial `targets` on a release is rejected.
#
# See ~/.agents/specs/publish-flow-unification.md for the design.
# ============================================================================

Expand All @@ -30,6 +38,17 @@ on:
required: true
type: boolean
default: true
targets:
description: "Preview only: which targets to build/publish. Releases always build everything."
required: false
type: choice
default: all
options:
- all
- rivetkit
- container-runner
- engine
- cli

env:
# Depot project ID (rivet-dev/rivet-engine).
Expand All @@ -48,6 +67,10 @@ jobs:
npm_tag: ${{ steps.ctx.outputs.npm_tag }}
sha: ${{ steps.ctx.outputs.sha }}
latest: ${{ steps.ctx.outputs.latest }}
targets: ${{ steps.ctx.outputs.targets }}
build_targets: ${{ steps.ctx.outputs.build_targets }}
build_wasm: ${{ steps.ctx.outputs.build_wasm }}
build_docker: ${{ steps.ctx.outputs.build_docker }}
steps:
- uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -218,14 +241,20 @@ jobs:
id-token: write # required for depot OIDC trust
packages: read
steps:
- name: Skip release-only targets on non-release triggers
- name: Skip release-only and out-of-scope targets
id: gate
run: |
skip=false
# Release-only targets (e.g. Windows) only build on release triggers.
if [ "${{ matrix.release_only }}" = "true" ] && [ "${{ needs.context.outputs.trigger }}" != "release" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
skip=true
fi
# Selective preview: skip build targets not in the resolved scope.
case " ${{ needs.context.outputs.build_targets }} " in
*" ${{ matrix.build_target }} "*) : ;;
*) skip=true ;;
esac
echo "skip=$skip" >> $GITHUB_OUTPUT
- uses: actions/checkout@v4
if: steps.gate.outputs.skip != 'true'
with:
Expand Down Expand Up @@ -276,6 +305,7 @@ jobs:
# ---------------------------------------------------------------------------
build-wasm:
needs: [context]
if: needs.context.outputs.build_wasm == 'true'
name: "Build rivetkit-wasm"
runs-on: depot-ubuntu-24.04-8
permissions:
Expand Down Expand Up @@ -314,6 +344,7 @@ jobs:
# ---------------------------------------------------------------------------
docker-images:
needs: [context]
if: needs.context.outputs.build_docker == 'true'
name: "Docker ${{ matrix.arch_suffix }}"
strategy:
fail-fast: false
Expand Down Expand Up @@ -357,6 +388,8 @@ jobs:
build-args: |
BUILD_FRONTEND=${{ steps.mode.outputs.build_frontend }}
CARGO_BUILD_MODE=${{ steps.mode.outputs.cargo_build_mode }}
secrets: |
DEPOT_TOKEN=${{ secrets.DEPOT_TOKEN }}
- name: Build & Push (rivetdev/engine:slim)
uses: docker/build-push-action@v4
with:
Expand All @@ -369,18 +402,22 @@ jobs:
build-args: |
BUILD_FRONTEND=${{ steps.mode.outputs.build_frontend }}
CARGO_BUILD_MODE=${{ steps.mode.outputs.cargo_build_mode }}
secrets: |
DEPOT_TOKEN=${{ secrets.DEPOT_TOKEN }}

# ---------------------------------------------------------------------------
# publish — npm publish + R2 upload + Docker manifest + release tail
# ---------------------------------------------------------------------------
publish:
needs: [context, build, build-wasm, docker-images]
name: "Publish"
# build-wasm / docker-images are skipped (not 'success') when their target
# is out of scope for a selective preview; treat skipped as acceptable.
if: |
!cancelled() &&
needs.build.result == 'success' &&
needs.build-wasm.result == 'success' &&
needs.docker-images.result == 'success'
(needs.build-wasm.result == 'success' || needs.build-wasm.result == 'skipped') &&
(needs.docker-images.result == 'success' || needs.docker-images.result == 'skipped')
# npm trusted publishing currently requires a GitHub-hosted runner.
runs-on: ubuntu-24.04
permissions:
Expand Down Expand Up @@ -409,35 +446,43 @@ jobs:

# ---- download + place native + engine artifacts ----
- name: Download native artifacts
if: contains(needs.context.outputs.targets, 'rivetkit')
uses: actions/download-artifact@v4
with:
path: native-artifacts
pattern: native-*
merge-multiple: true
# Engine binaries are also needed by the `cli` group (bundled into CLI
# platform packages), so key this off build_targets, not the group.
- name: Download engine artifacts
if: contains(needs.context.outputs.build_targets, 'engine')
uses: actions/download-artifact@v4
with:
path: engine-artifacts
pattern: engine-*
merge-multiple: true
- name: Download CLI artifacts
if: contains(needs.context.outputs.targets, 'cli')
uses: actions/download-artifact@v4
with:
path: cli-artifacts
pattern: cli-*
merge-multiple: true
- name: Download container runner artifacts
if: contains(needs.context.outputs.targets, 'container-runner')
uses: actions/download-artifact@v4
with:
path: container-runner-artifacts
pattern: container-runner-*
merge-multiple: true
- name: Download wasm package artifact
if: needs.context.outputs.build_wasm == 'true'
uses: actions/download-artifact@v4
with:
name: wasm-package
path: rivetkit-typescript/packages/rivetkit-wasm/pkg
- name: Place native binaries in platform packages
if: contains(needs.context.outputs.targets, 'rivetkit')
run: |
NATIVE_DIR=rivetkit-typescript/packages/rivetkit-napi
for f in native-artifacts/*.node; do
Expand Down Expand Up @@ -556,6 +601,7 @@ jobs:

# ---- build TypeScript packages (turbo dep graph picks up native) ----
- name: Build TypeScript packages
if: contains(needs.context.outputs.targets, 'rivetkit')
env:
SKIP_WASM_BUILD: "1"
run: |
Expand All @@ -581,19 +627,22 @@ jobs:
${{ needs.context.outputs.trigger == 'release' && '--release-mode' || '' }}

- name: Generate engine checksum manifest
if: contains(needs.context.outputs.targets, 'engine')
run: |
(
cd engine-artifacts
sha256sum rivet-engine-* > SHA256SUMS
)

- name: Upload engine binaries to R2
if: contains(needs.context.outputs.targets, 'engine')
env:
R2_RELEASES_ACCESS_KEY_ID: ${{ secrets.R2_RELEASES_ACCESS_KEY_ID }}
R2_RELEASES_SECRET_ACCESS_KEY: ${{ secrets.R2_RELEASES_SECRET_ACCESS_KEY }}
run: pnpm --filter=publish exec tsx src/ci/bin.ts upload-r2 --source "$GITHUB_WORKSPACE/engine-artifacts" --sha ${{ needs.context.outputs.sha }}

- name: Upload container runner binaries to R2
if: contains(needs.context.outputs.targets, 'container-runner')
env:
R2_RELEASES_ACCESS_KEY_ID: ${{ secrets.R2_RELEASES_ACCESS_KEY_ID }}
R2_RELEASES_SECRET_ACCESS_KEY: ${{ secrets.R2_RELEASES_SECRET_ACCESS_KEY }}
Expand Down Expand Up @@ -636,6 +685,7 @@ jobs:
--allow-dirty

- name: Create Docker multi-arch manifests
if: needs.context.outputs.build_docker == 'true'
run: pnpm --filter=publish exec tsx src/ci/bin.ts docker-manifest --sha ${{ needs.context.outputs.sha }}

# ---- release-only tail ----
Expand Down
32 changes: 30 additions & 2 deletions docker/engine/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@ ARG VITE_APP_API_URL=__SAME__
ARG VITE_APP_TURNSTILE_SITE_KEY=
ARG OVERRIDE_GIT_SHA
ARG RUST_TOOLCHAIN=1.91.1
ARG SCCACHE_VERSION=v0.8.2

# Install sccache so the engine compile shares the Depot WebDAV cache with the
# matrix builds (docker/build/*.Dockerfile). The engine base image does not
# ship it, unlike the builder-base-linux-* images. Installed before COPY so the
# layer stays cached across source changes. Match the binary to the native
# build arch (each docker-images matrix leg runs on its own arch runner).
RUN if [ "$TARGETARCH" = "arm64" ]; then SCCACHE_ARCH=aarch64; else SCCACHE_ARCH=x86_64; fi && \
wget -q "https://github.com/mozilla/sccache/releases/download/${SCCACHE_VERSION}/sccache-${SCCACHE_VERSION}-${SCCACHE_ARCH}-unknown-linux-musl.tar.gz" && \
tar -xzf "sccache-${SCCACHE_VERSION}-${SCCACHE_ARCH}-unknown-linux-musl.tar.gz" && \
mv "sccache-${SCCACHE_VERSION}-${SCCACHE_ARCH}-unknown-linux-musl/sccache" /usr/local/bin/sccache && \
chmod +x /usr/local/bin/sccache && \
rm -rf "sccache-${SCCACHE_VERSION}-${SCCACHE_ARCH}-unknown-linux-musl"*

ENV RUSTC_WRAPPER=sccache \
SCCACHE_WEBDAV_ENDPOINT=https://cache.depot.dev \
SCCACHE_IDLE_TIMEOUT=0

WORKDIR /app

Expand All @@ -40,19 +57,30 @@ RUN if [ "$BUILD_FRONTEND" = "true" ]; then \
# included in the output because of cache mount)
RUN \
--mount=type=secret,id=netrc,target=/root/.netrc,mode=0600 \
--mount=type=secret,id=DEPOT_TOKEN,env=SCCACHE_WEBDAV_TOKEN \
--mount=type=cache,target=/usr/local/cargo/git,id=univseral-cargo-git \
--mount=type=cache,target=/usr/local/cargo/registry,id=univseral-cargo-registry \
--mount=type=cache,target=/app/target,id=univseral-target \
--mount=type=cache,target=/root/.cache,id=universal-user-cache \
set -e && \
if [ -z "${SCCACHE_WEBDAV_TOKEN:-}" ]; then \
echo "[sccache] no DEPOT_TOKEN, disabling"; unset RUSTC_WRAPPER; \
elif ! (sccache --start-server 2>/tmp/sccache-start.err && sccache --show-stats >/dev/null 2>&1); then \
echo "[sccache] backend health check failed, disabling:"; cat /tmp/sccache-start.err 2>/dev/null || true; \
sccache --stop-server >/dev/null 2>&1 || true; \
unset RUSTC_WRAPPER SCCACHE_WEBDAV_ENDPOINT SCCACHE_WEBDAV_TOKEN; \
else \
echo "[sccache] enabled via ${SCCACHE_WEBDAV_ENDPOINT}"; \
fi && \
if [ "$CARGO_BUILD_MODE" = "release" ]; then \
RUSTFLAGS="--cfg tokio_unstable" cargo build -p rivet-engine --bin rivet-engine --release; \
else \
RUSTFLAGS="--cfg tokio_unstable" cargo build -p rivet-engine --bin rivet-engine; \
fi && \
# cargo install --locked tokio-console && \
mkdir /app/dist/ && \
echo "Copying binary" && \
cp target/$CARGO_BUILD_MODE/rivet-engine /app/dist/
cp target/$CARGO_BUILD_MODE/rivet-engine /app/dist/ && \
(sccache --show-stats 2>/dev/null || true)

# MARK: Engine (full, base)
FROM ghcr.io/rivet-dev/rivet/engine-base-runtime-full:a36b881 AS engine-full-base
Expand Down
7 changes: 6 additions & 1 deletion scripts/publish/src/ci/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ program
.option("--version <version>", "Override version")
.option("--latest <bool>", "Override latest")
.option("--branch <name>", "Override branch name")
.option("--targets <list>", "Override target groups (comma-separated or 'all')")
.action(async (opts) => {
const overrides: Parameters<typeof resolveContext>[0] = {};
if (opts.trigger) overrides.trigger = opts.trigger as Trigger;
Expand All @@ -169,9 +170,10 @@ program
overrides.latest = opts.latest === "true";
}
if (opts.branch) overrides.branch = opts.branch;
if (opts.targets) overrides.targets = opts.targets;
const ctx = await resolveContext(overrides);
log.info(
`resolved: trigger=${ctx.trigger} version=${ctx.version} npm_tag=${ctx.npmTag} sha=${ctx.sha} latest=${ctx.latest}${ctx.branch !== undefined ? ` branch=${ctx.branch}` : ""}`,
`resolved: trigger=${ctx.trigger} version=${ctx.version} npm_tag=${ctx.npmTag} sha=${ctx.sha} latest=${ctx.latest} targets=${ctx.targets.join(",")}${ctx.branch !== undefined ? ` branch=${ctx.branch}` : ""}`,
);
writeContextToGithubOutput(ctx);
});
Expand Down Expand Up @@ -201,6 +203,7 @@ program
includeReleaseOnlyPackages: ctx.trigger === "release",
versionOnly: !!opts.versionOnly,
repository: opts.repository ?? process.env.GITHUB_REPOSITORY,
targets: ctx.targets,
});
await bumpCargoVersions(repoRoot, version, {
dryRun: !!opts.dryRun,
Expand Down Expand Up @@ -235,12 +238,14 @@ program
parallel: Number(opts.parallel),
retries: Number(opts.retries),
releaseMode,
targets: ctx.targets,
});
if (!releaseMode) {
await repairBranchPreviewLatestTags(repoRoot, {
tag,
version: ctx.version,
includeReleaseOnlyPackages: releaseMode,
targets: ctx.targets,
});
}
});
Expand Down
48 changes: 48 additions & 0 deletions scripts/publish/src/lib/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import { appendFileSync, existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { $ } from "execa";
import {
ALL_GROUPS,
buildScope,
isAllGroups,
parseTargetGroups,
type TargetGroup,
} from "./scope.js";

/**
* Publish context. Resolved once per workflow run by the `context-output` CI
Expand All @@ -23,6 +30,17 @@ export interface PublishContext {
/** Branch name. Only set when trigger === "branch". */
branch?: string;
repoRoot: string;
/**
* Selected target groups. Narrowed by the `targets` input on preview
* publishes; always every group on release so a cut is never partial.
*/
targets: TargetGroup[];
/** Space-separated native build targets in scope (matrix gate reads this). */
buildTargets: string;
/** Whether the `build-wasm` job runs. */
buildWasm: boolean;
/** Whether the `docker-images` job runs. */
buildDocker: boolean;
}

/** Override set accepted by the local release cutter. */
Expand All @@ -32,6 +50,7 @@ export interface ResolveOverrides {
latest?: boolean;
branch?: string;
sha?: string;
targets?: string;
}

function findRepoRoot(): string {
Expand Down Expand Up @@ -210,6 +229,23 @@ export async function resolveContext(

const npmTag = computeNpmTag(trigger, version, latest, branch);

// Resolve selective-publish scope. Releases always cover every group so a
// cut is never partial; only preview publishes may narrow scope.
const targetsInput =
overrides.targets ?? readInputFromEvent<string>("targets");
let targets: TargetGroup[];
if (trigger === "release") {
if (targetsInput && !isAllGroups(parseTargetGroups(targetsInput))) {
throw new Error(
`release publishes must build every target; refusing partial targets="${targetsInput}"`,
);
}
targets = [...ALL_GROUPS];
} else {
targets = parseTargetGroups(targetsInput);
}
const scope = buildScope(targets);

return {
trigger,
version,
Expand All @@ -218,6 +254,10 @@ export async function resolveContext(
latest,
branch,
repoRoot,
targets,
buildTargets: scope.buildTargets.join(" "),
buildWasm: scope.buildWasm,
buildDocker: scope.buildDocker,
};
}

Expand All @@ -231,6 +271,10 @@ export function writeContextToGithubOutput(ctx: PublishContext): void {
console.log(`npm_tag=${ctx.npmTag}`);
console.log(`sha=${ctx.sha}`);
console.log(`latest=${ctx.latest}`);
console.log(`targets=${ctx.targets.join(",")}`);
console.log(`build_targets=${ctx.buildTargets}`);
console.log(`build_wasm=${ctx.buildWasm}`);
console.log(`build_docker=${ctx.buildDocker}`);
if (ctx.branch !== undefined) console.log(`branch=${ctx.branch}`);
return;
}
Expand All @@ -240,6 +284,10 @@ export function writeContextToGithubOutput(ctx: PublishContext): void {
`npm_tag=${ctx.npmTag}`,
`sha=${ctx.sha}`,
`latest=${ctx.latest}`,
`targets=${ctx.targets.join(",")}`,
`build_targets=${ctx.buildTargets}`,
`build_wasm=${ctx.buildWasm}`,
`build_docker=${ctx.buildDocker}`,
];
if (ctx.branch !== undefined) lines.push(`branch=${ctx.branch}`);
// Append (do not overwrite) in case other steps also wrote to GITHUB_OUTPUT.
Expand Down
Loading
Loading