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 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.

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 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 (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

Custom Image

By default each job runs on a standard image with:

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

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.

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 your code reviews. So, we built a ~*~NEW~*~ git platform. Join the waitlist to get early access.

Orrr… skip the line! Join our Discord for early access