SearchCtrlK

  • ↑↓navigate
  • ↵open
  • escclose

Can’t find it? Contact us

Run CI on your machine

Write your app's CI as a TypeScript script, run it with sdxc ci before you push, and let GitHub Actions skip its own run for a commit you already signed off.

After reading this guide, you will know

  • How to write a CI script with steps, parallel groups and a matrix over workspaces
  • How sdxc ci --push tests, pushes and signs a commit off in one go
  • How the GitHub workflow skips its run for a commit a contributor signed off

sdxc create starts every app with .config/sdxc/ci.ts, a CI script that runs on your machine and on GitHub Actions alike. Run it before you push: when it passes, it marks the commit green on GitHub, and the workflow skips its own run.

Run it

Terminal
sdxc ci

sdxc ci installs dependencies with a frozen lockfile, then runs each step and prints one line per step as it finishes. A failed step shows its output under it. The run exits 0 when every step passed and 1 otherwise.

While you work, narrow the run to the steps you care about:

Terminal
sdxc ci --only Test

A run with --only, --skip or --scope never signs off, so a narrowed run can't mark a commit green.

Write steps

The script is a function that receives ci. await runs steps one after another, and ci.parallel runs a group at once:

typescript
import { defineCI } from "@sdxc/ci";

export default defineCI(async (ci) => {
	await ci.step("Install browsers", "bunx playwright install chromium", {
		halt: true,
	});

	await ci.parallel("Checks", [
		() => ci.step("Doctor", "bunx sdxc doctor"),
		() => ci.step("Format, lint and types", "bun run check"),
	]);

	await ci.step("Test", "bun run test", { cache: true });

	if (ci.status === "passing") await ci.signoff("All systems go.");
	else
		ci.failure(
			"CI failed. Do not push.",
			"Fix the steps above and run `sdxc ci` again.",
		);
});
  • A failed step leaves the run going, so you see every failure in one pass. Add halt: true when nothing after the step can succeed without it.

  • cache: true skips a step whose inputs haven't changed since it last passed. The step's output from that earlier pass is kept.

  • ci.runner is "remote" on GitHub Actions and "local" on your machine. Use it for steps that belong in only one place, such as copying .env.example to .dev.vars on CI.

Test every workspace

In a monorepo, ci.matrix runs one entry per workspace, each with its own steps and status:

typescript
let tested = (await ci.workspaces()).filter((ws) => ws.has("**/*.test.ts"));

await ci.matrix("Workspaces", tested, async (ws) => {
	await ci.step(`Tests for ${ws.name}`, "bun run test", {
		cwd: ws.path,
		cache: true,
	});
});

Run sdxc ci from inside a workspace and only that workspace's entry runs, plus every workspace that depends on it.

Sign off and push

Terminal
sdxc ci --push

When every step passes, --push pushes exactly the tested commit to its upstream branch, then posts an sdxc/ci commit status on GitHub. It signs off only when the run:

  • ran every step,

  • started and ended with no uncommitted or untracked files,

  • tested the same commit from start to end.

If someone pushed first, the push is rejected; pull, then run sdxc ci --push again.

The token comes from GITHUB_TOKEN, or from gh auth token when you're signed in with the GitHub CLI.

Skip the hosted run

The workflow sdxc create writes, .github/workflows/ci.yml, starts with a gate job:

Terminal
bunx sdxc ci verify "$SHA" --wait 60s

It exits 0 when someone with write access to the repository signed the commit off, and the workflow then skips its CI job. Every other commit runs the full script on GitHub Actions with sdxc ci --runner remote.

Where to go next

Documentation

Can’t find it? Contact us