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
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 file filename (e.g. vercel.ts
will be named Vercel
).
Jobs are executed every time a branch detects a new push.
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]
).
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;
}
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
.
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:
Size | Memory | Cores |
---|---|---|
micro | 2048MB | 1 |
small | 4096MB | 1 |
medium | 8192MB | 2 |
large | 16384MB | 4 |
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:
If you specify values outside these ranges, they will be automatically adjusted to the nearest valid value.
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.5
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.
You can provide non-secret environment variables in the configuration file using the env
property.
{
"env": {
"FOO": "bar"
}
}
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.
When starting a job will checkout your repo into the directory /code
and run your job from there.
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");
}
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.