Pierre CI

Pierre’s CI is a simple, scriptable (TypeScript), event based system, that allows you to run jobs and manage rich integrations with Pierre’s UI.


$ npm i pierre

What it looks like

At it’s simplest this is Pierre CI:

// .pierre/ci/helloworld.ts

import { run } from "pierre";

export const label = "Hello world";

export default async () => {
	await run(`echo "hello world"`);

Every top-level file in .pierre/ci is automatically run as a job in our default Docker container. To create a new job, create a new file. All top-level jobs are run in parallel. Files included in nested directories (e.g. .pierre/ci/util) are not run as jobs.

Provide a label to give your job a name, otherwise the filename will be used.

Job handlers are executed every time a branch detects a new push. In the future we will likely add additional events for “comments”, “merges”, etc.


Job files in Pierre export a label (used to name your job) and a default function or array of functions.

If you return an array, each function will be evaluated serially:

export const label = "Serial Tasks";

export default [firstTask, secondTask, thirdTask];

Each Job is called with an id and a branch:

export default async ({ id, branch }) => {
	if ( === "🍔") throw new Error("No burgers allowed");
	console.log("Burger free zone");

Everything logged from the default job will be streamed to the jobs pages in Pierre (e.g,[team]/[repo]/jobs/[…branch]).

Failing a job

There are a few ways to fail a job. The first, and the most recommended, is to simply throw an error:

export default async () => {
	throw new Error("This job failed… 😢");

This will result in the job failing with an exit code of 1. You can alternatively return a custom error code to indicate certain failure states, eg.

export default async () => {
	return 123;

Note: An exit code of 0 indicates success, while anything else indicates failure.


Pierre exports a special function for executing commands in your container called run. Run automatically logs special information to your UI and makes your logs more readable.

import { run } from "pierre"

export default () => {
  await run("tsc -p ./tsconfig.json --noEmit", {
    label: "Run typescript typechecker"

The run command also takes the following options:

export interface RunOptions {
	// Optional cwd to set
	cwd?: string;

	// When set to `true`, avoid passing through `process.env`
	clearEnv?: boolean;

	// Additional environment variables to set
	env?: Record<string, string>;

	// What exit code to assert for each command
	expectedCode?: number;

	// If set to true then no assertions are made on expected exit code
	allowAnyCode?: boolean;

	// Whether we should pipe stdout and stderr
	pipe?: boolean;

	// Optional label to print
	label?: string;

	// Timeout in milliseconds the command has to run
	timeout?: number;

Run itself provides a couple of return objects. Useful for extracting specific results or processing output for strings.

export interface RunResult {
	exitCode: number;
	stdmerged: string;
	stderr: string;
	stdout: string;


Pierre runs all of your CI jobs in its default docker image. This container is modeled to be as similar to a regular dev machine as possible (with the latest stable version of node and chrome). By default it has your secrets and local branch cloned into it.

In the future we will allow you to provide custom containers for more advanced use cases and dependencies.

Here’s a partial list of installed tools:

node 20.15.0
npm 10.7.0
pnpm 9.4.0
yarn (follows packageManager in package.json)
bun 1.1.17
go 1.21.6
php 8.2.20
python 3.12.4 (miniconda)
ruby 3.1.2p20
chromium binary, plus dependencies for chromium, firefox, and webkit for test tools that install/vendor their own browsers

K/V Store

Pierre has it’s own Redis-powered KV store. This is useful for quickly storing information between CI runs such as performance metrics, bundle sizes, and other notes.

import { Store } from "pierre";

export default async () => {
	await Store.set("dog", "🐕");
	await Store.get("dog");


Secrets are managed through repo settings in Pierre and are made available in your local container as env variables accessible in process.env and on the command line via $.

import { run } from "pierre"

export default () => {
  await run("echo $VERCEL_ACCESS_TOKEN");

Running Locally

You can use the Pierre CLI to debug your jobs locally. If you haven’t already, install Pierre’s CLI:

$ npm install pierre -g


# Ex: pierre run .pierre/ci/typecheck.ts
$ pierre run <path>


# Ex: pierre run typecheck
$ pierre run <name>

Pierre is the product engineering tool

Code hosting, review, docs, and CI. One place for product engineers and their teams to focus on what they do best—building products.

Join the Waitlist
Skip the line! Join our Discord for early access