Docs/CI

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.

Install

$ npm i pierre

What it looks like

At it’s simplest this is Pierre CI:

// .pierre/ci/helloworld.ts

import { run } from 'pierre';

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

Every top-level file in .pierre/ci is automatically run as a job. 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.

Jobs are named after the filename (e.g. vercel.ts will be named Vercel), unless a custom label is provided in the job's configuration file.

Jobs are executed every time a branch detects a new push.

Jobs

Job files in Pierre export a default function or array of functions.

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

export default [firstTask, secondTask, thirdTask];

Each Job is called with an id and a branch:

export default async ({ id, branch }) => {
  if (branch.name === '🍔') 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, https://pierre.co/[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.

Run

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;
}

Configuration

You can provide a custom configuration file for each job. This file is located at .pierre/ci/[job].config.json. This file is also merged with the global configuration file at .pierre/ci/base.config.json.

Machine Size

Each job runs on a machine with 1 core and 2048MB of memory by default. You can provide a custom machine description in the configuration file though by using the size property.

{
  "size": "large"
}

The machine size can be one of the following:

SizeMemoryCores
micro2048MB1
small4096MB1
medium8192MB2
large16384MB4

You can also specify custom machine sizes with specific memory and core requirements:

{
  "size": {
    "memory_mb": 4096,
    "cores": 2
  }
}

When specifying custom sizes, the following constraints apply:

  • CPU cores must be one of: 1, 2, 4, 8, or 16
  • Memory must be between 2048MB and 8192MB per CPU core
    • Minimum memory: 2048MB × cores
    • Maximum memory: 8192MB × cores

If you specify values outside these ranges, they will be automatically adjusted to the nearest valid value.

Custom Image

By default each job runs on a standard image based off of Ubuntu 24.04 with:

node 22.14.0
npm 10.9.2
pnpm 10.6.3
yarn (follows packageManager in package.json)
bun 1.2.8
go 1.24.1
php 8.3.6
python 3.12.3
ruby 3.2.3
rustup
aws-cli
chromium binary, plus dependencies for chromium, firefox, and webkit for test tools that install/vendor their own browsers

If you need to run an image with a different set of tools, you can provide a custom image in the configuration file using the image property.

{
  "image": "node@sha256:a81372dcb7a0d4a183f453f04a8eba1f47ff44089294dcf73e5e368ec56d58d4"
}

The image provided must have a bash shell available, curl and git installed. It is recommended to use a specific version of the image (rather than the latest tag for example). When running we will look for a machine with the same image name provided in the configuration file. Using a tag could lead to different runners having different versions of the image at the same time.

Environment Variables

You can provide non-secret environment variables in the configuration file using the env property.

{
  "env": {
    "FOO": "bar"
  }
}

Extends

You can extend the configuration of a job by using the extends property. This will merge the configuration of the current job with the configuration of the job specified in the extends property.

{
  "extends": "large.config.json",
  "env": {
    "FOO": "bar"
  }
}

This looks for the file .pierre/ci/large.config.json and merges the configuration with the current job.

Label

You can provide a custom display name for your job using the label property in the configuration file. If not provided, the job name will be automatically generated from the filename (e.g., typescript.ts becomes Typescript).

{
  "label": "TypeScript Type Checking"
}

This is useful when you want more descriptive names for your jobs in the Pierre UI.

Directory structure

When starting a job will checkout your repo into the directory /code and run your job from there.

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

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");
}

Joyful code review

Pierre wants you to enjoy code review with your team. So, we built a ~NEW~*~ Git platform to do just that. Get started today for free.

We're in public beta! Join our Discord to share feedback and chat with the Pierre team.