This is the full developer documentation for PandaCI (https://pandaci.com).
# Start of PandaCI documentation
# [Quick Start](https://pandaci.com/docs/platform/overview/quick-start)
Create a CI/CD pipeline in minutes
{% alert level="check" %}
**Prerequisites**
You need an account before starting, create one [here](https://app.pandaci.com/signup).
{% /alert %}
## Create a new project
Once logged in, you can create a new project by clicking on the **New Project** button located at the top right corner of the projects page.

{% alert level="info" %}
If you can't see the repo you want, make sure our [GitHub app](https://github.com/apps/pandaci-com/installations/new) has access to it.
{% /alert %}
## Write your first workflow
PandaCI uses [Deno](https://deno.com) to define workflows. Don't worry, you don't need to know Deno to get started.
Create a new file called `.pandaci/hello-world.workflow.ts` and add the following code:
```typescript {% title=".pandaci/hello-world.workflow.ts" %}
import { docker, $, job, env } from "jsr:@pandaci/workflow";
// Creates a docker task on a 4-core machine
// same as wrapping with `job("some-name", () => { ... })`
docker("ubuntu:latest", { name: "hello world" }, () => {
// Runs a shell command
$`echo "Hello, world! from branch: ${env.PANDACI_BRANCH}"`;
});
// Creates 2 docker tasks on an 2-core machine
job("Job 2", { runner: "ubuntu-2x" }, async () => {
let files = '';
await docker("ubuntu:latest", async () => {
// Runs the command in the .pandaci directory
// and stores the output as a string
files = await $`ls`.cwd(".pandaci").text();
});
await docker("ubuntu:latest", () => {
// we can easily share data between tasks or even jobs
$`echo "files from another task: ${files}"`;
});
});
```
{% alert level="knowledge" %}
Learn more about the [workflow syntax](/docs/typescript/api/overview).
{% /alert %}
PandaCI will run any files in the **.pandaci** directory that end with **.workflow.ts**. By default, they're triggered on every push. Learn how to [configure triggers](/docs/typescript/config/triggers).
## Commit and push your changes
After pushing, PandaCI will automatically detect the new workflow and start running it. You can see the progress in the PandaCI dashboard.
### Congratulations 🎉
You've just created your first CI/CD pipeline with PandaCI.
# [Why PandaCI Exists](https://pandaci.com/docs/platform/overview/why-pandaci)
PandaCI introduces a radically new way to build CI/CD pipelines, why should you care?
## The Problem with Traditional CI/CD
Most CI/CD platforms use declarative languages like YAML to define workflows. While this approach works, it has its drawbacks:
- **Platform-Specific Learning Curve** – YAML syntax is unique to each CI/CD platform, meaning even experienced developers must spend time navigating extensive documentation just to structure workflows correctly.
- **YAML is Limiting** – Writing complex workflows in YAML quickly becomes messy and hard to maintain. It lacks built-in support for reusability, forcing duplication and making it difficult to follow the DRY (Don't Repeat Yourself) principle.
- **Script Hell** – Any logic beyond running basic commands often requires external scripts or intricate shell commands, adding unnecessary complexity and maintenance overhead.
- **Debugging is a Nightmare** – YAML offers no built-in debugging tools. Diagnosing issues means relying on slow trial-and-error processes, making troubleshooting frustrating and time-consuming.
- **Less Developer Control** – In many teams, DevOps engineers own the CI/CD pipeline, leaving developers with little control over their own workflows. Any changes require coordination, slowing down iteration speed.
## The PandaCI Solution: CI/CD as Code
PandaCI rethinks CI/CD by replacing YAML with TypeScript, enabling workflows to be defined using a real programming language.
{% alert level="info" %}
Reach out if you want us to support additional languages like Python, Ruby, or Go.
{% /alert %}
This provides several key advantages:
### 1. **Code, Not Configuration**
Instead of wrestling with YAML’s limitations, PandaCI lets you write workflows using TypeScript or JavaScript. This means:
- Use variables, loops, and conditionals naturally.
- Import and reuse functions across workflows.
- Leverage npm packages to extend functionality.
Example:
```ts {% title=".pandaci/hello-world.workflow.ts" %}
import { docker, $ } from "@pandaci/workflow";
docker("ubuntu:latest", async () => {
await $`echo "Hello, World!"`;
});
```
### 2. **Better Reusability**
With TypeScript, you can define reusable functions and modules, making it easy to share logic between workflows:
Example:
```ts {% title=".pandaci/build-utils.ts" %}
import { $ } from "@pandaci/workflow";
export async function initPnpm() {
await $`PNPM_HOME="/pnpm"`;
await $`PATH="$PNPM_HOME:$PATH"`;
await $`corepack enable`;
await $`pnpm i`;
}
```
Then reuse it in multiple jobs:
```ts {% title=".pandaci/ci.workflow.ts" %}
import { docker } from "@pandaci/workflow";
import { initPnpm } from "./build-utils.ts";
docker("ubuntu:latest", async () => {
await initPnpm();
// ...
});
```
### 3. **Powerful Extensibility**
Want to integrate with a custom API? Fetch data from your own system? Simply use JavaScript’s existing ecosystem:
```ts
const response = await fetch("https://api.example.com/build-params");
const buildParams = await response.json();
```
Even better, use an npm package to handle complex tasks:
```typescript {% title=".pandaci/slack.workflow.ts" %}
import { docker, $, env } from "jsr:@pandaci/workflow";
import { WebClient } from "npm:@slack/web-api";
const slack = new WebClient(env.SLACK_BOT_TOKEN);
docker("ubuntu:latest", async () => {
// A task which fails
await $`exit 1`;
}).catch(() =>
// Send a message to our Slack channel
slack.chat.postMessage({
channel: env.SLACK_CHANNEL_ID,
text: `Workflow failed!`,
})
);
```
### 4. More Developer Control
There's a growing trend where developers are losing control over their CI/CD pipelines with DevOps teams owning the process.
We believe that developers should have more control over their CI/CD pipelines.
By using the same language to define workflows as they do to write their applications, developers can own and iterate on their CI/CD pipelines without needing DevOps intervention for every change.
## Why Choose PandaCI?
- **Developer-Friendly** – Write workflows in TypeScript, a language you already know and love.
- **Better Reusability** – Define reusable functions and modules to share logic across workflows.
- **Powerful Extensibility** – Leverage the vast npm ecosystem to handle complex tasks with ease.
- **More Developer Control** – Own and iterate on your CI/CD pipelines without needing DevOps intervention.
- **Faster Iteration Speeds** – Write, test, and deploy changes faster than ever before.
PandaCI isn’t just another CI/CD tool – it’s a reimagined approach that brings the power of real code to automation. Ready to try it? [Get started today](https://app.pandaci.com/signup).
# [Runners](https://pandaci.com/docs/platform/overview/runners)
Runners are the agents that execute your workflows
## Cloud runners
Cloud runners are isolated virtual machines that execute your workflows. We use Fly.io to provide these runners. They are fast, secure, and can be scaled up to 16 cores.
### Pricing
We use build minutes to calculate the cost of running your workflows. Build minutes are calculated by multiplying the number of cores by the duration of the job in minutes (rounded up).
### Available runners
We currently only support Linux runners. The image is based on the Ubuntu 24.04 docker image.
It's a minimal image with only the necessary tools installed.
{% table %}
* Runner name
* Cores
* Memory
* Storage
* Build minute multiplier
---
* ubuntu-1x
* 1
* 4GB
* 32GB
* 1x
---
* ubuntu-2x
* 2
* 8GB
* 64GB
* 2x
---
* ubuntu-4x
* 4
* 16GB
* 128GB
* 4x
---
* ubuntu-8x
* 8
* 32GB
* 256GB
* 8x
---
* ubuntu-16x
* 16
* 64GB
* 512GB
* 16x
{% /table %}
Your plan determines which runners you can use, how many build minutes you have, and how much you pay for additional build minutes. Please refer to our [pricing page](/pricing) for more information.
### Default runner
The default runner is `ubuntu-4x`. You can change the runner by specifying the `runner` option in the `job` function.
## Self-hosted runners
We currently don't support self-hosted runners. If you need this feature, please let us know by [contacting us](mailto:support@pandaci.com).
# [Secrets](https://pandaci.com/docs/platform/overview/secrets)
Secrets in PandaCI securely store sensitive data like API keys and passwords for your builds.
Secrets are loaded as environment variables in your workflow (not jobs). If you need to use them in a job, you can pass them as arguments to the command.
{% alert level="knowledge" %}
Restrict secrets to specific branches using environments to keep them safe.
Since secrets load as environment variables, anyone editing the workflow could expose them.
{% /alert %}
## Creating a Secret
Navigate to the **Secrets** page in your project and click on the **New Secret** button.

## Sensitive Secrets
If you mark a secret as sensitive, you won't be able to view the value again (in our dashboard).
{% alert level="info" %}
Since all our variables are decrypted for your workflow, you still need to be careful with sensitive secrets.
{% /alert %}
## Environments
Environments are a way to restrict which branches can access secrets.
Navigate to the **Environments** page in your project and click on the **New Environment** button.

We use glob patterns to match branches. For example, `feature/*` will match all branches that start with `feature/`.
## Limits
Variables are limited to 64KB in size. If you need to store more data please reach out to our support team.
# [How It Works](https://pandaci.com/docs/platform/workflows/how-it-works)
Learn how PandaCI's workflows and jobs work together to build and test your code.
## Workflow Runner
The workflow runner is responsible for executing workflows. For each workflow, we provision an isolated VM running our Go agent, which orchestrates execution and manages communication between the workflow and other services.
### Repository Cloning
To ensure your workflow has access to your code, we perform a shallow clone of your repository, retrieving only the latest commit. While you can execute commands directly on the workflow runner, we recommend against this, as our cloning strategy may evolve.
## Job Runner
Jobs provide isolated environments for executing workflow steps. Each job runner is powered by our Go agent, which facilitates communication between the workflow and the job runner.
### Repository Cloning
Similar to workflows, job runners also perform a shallow clone of your repository. This cloned repository is then mounted into any Docker tasks, ensuring all tasks share the same files.
## Tasks
Tasks are collections of commands executed within an environment on the job runner. We currently support two task types:
- **Docker**: Runs commands inside a Docker container.
- **Native**: Runs commands directly on the job runner.
## Steps
Steps are individual commands executed within a task.
For Docker tasks, each step runs in a new execution context (similar to a fresh terminal session). This means certain data from previous steps may not be accessible. To mitigate this, we recommend either:
- Combining related commands into a single step.
- Handling data persistence and sharing in TypeScript.
By structuring your workflows efficiently, you can ensure smooth execution and maintainability across your CI/CD pipelines.
# [Workflow Environment](https://pandaci.com/docs/platform/workflows/env)
These are the variables accessible to your workflows and jobs.
## Workflow Environment
We prefix our platforms environment variables with `PANDACI_` to avoid conflicts with your existing environment variables.
{% alert level="knowledge" %}
Any secrets you create are also available as environment variables.
These aren't prefixed.
{% /alert %}
- **PANDACI_BRANCH**: The branch name of the current workflow.
- **PANDACI_COMMIT_SHA**: The commit hash of the current workflow.
- **PANDACI_WORKFLOW_ID**: The unique ID of the current workflow.
- **PANDACI_REPO_URL**: The URL of the repository.
{% alert level="info" %}
Please create an [issue](https://github.com/pandaci-com/pandaci/issues) if you need additional environment variables.
{% /alert %}
## Git fork protection
If you receive a pull request from a fork of your repository, we'll block the workflow from running.
This is to prevent malicious code from accessing your secrets and using your usage limits.
{% alert level="info" %}
We are planning on improving this feature in the near future.
We'd like to run forks if they're from a member of your organization and offer the ability to run workflows after
manual approval.
{% /alert %}
# [LLMs.txt](https://pandaci.com/docs/platform/other/llms-txt)
PandaCI Documentation for LLMs
We support the [llms.txt](https://llmstxt.org/) convention for making documentation available to large language models and the applications that make use of them.
Currently we only support the full documentation set.
Our available documentation sets are:
- **[/llms.txt](/llms.txt):** a listing of the available files
- **[/llms-full.txt](/llms-full.txt):** the full documentation set
# [Org members](https://pandaci.com/docs/platform/other/org-members)
All plans include unlimited org members. Invite your team to collaborate on your projects.
## Invite your team
All you need to do is navigate to the **Members** page in your org and click on the **Invite Member** button (top right).

If the user is already a PandaCI user, they will automatically be added to your org.
If they are not, they will receive an email invite to join your org. They will have 3 days to accept this invite.
## Roles and permissions
Currently we only support the owner & member role. Every member has full access to all projects and settings.
If this is a blocker for you, please [reach out to us](mailto:support@pandaci.com) and we'll fast track this feature.
# [Overview](https://pandaci.com/docs/typescript-syntax/overview/overview)
Learn how to create TypeScript workflows in Pandaci.
PandaCI uses Deno to run our TypeScript workflows. You don't need to know anything about Deno to get started.
{% alert level="info" %}
Read our [Quick Start](/docs/platform/overview/quick-start) guide to learn how to create a new project and write your first workflow.
{% /alert %}
## Creating a workflow
To create a workflow, you need to create a file with the `.workflow.ts` extension in the `.pandaci` directory. This file will contain your workflow code.
Here's an example of a simple workflow:
```typescript {% title=".pandaci/hello-world.workflow.ts" %}
import { docker, $, job, env } from "jsr:@pandaci/workflow";
// Creates a docker task on a 4-core machine
// same as wrapping with `job("some-name", () => { ... })`
docker("ubuntu:latest", { name: "hello world" }, () => {
// Runs a shell command
$`echo "Hello, world! from branch: ${env.PANDACI_BRANCH}"`;
});
// Creates 2 docker tasks on an 2-core machine
job("Job 2", { runner: "ubuntu-2x" }, async () => {
let files = '';
await docker("ubuntu:latest", async () => {
// Runs the command in the .pandaci directory
// and stores the output as a string
files = await $`ls`.cwd(".pandaci").text();
});
docker("ubuntu:latest", () => {
// we can easily share data between tasks or even jobs
$`echo "files from another task: ${files}"`;
});
});
```
We'll treat any files inside the `.pandaci` directory with the `.workflow.ts` extension as a workflow file. You can create as many workflow files as you need.
{% alert level="knowledge" %}
We recommend you keep any related files inside the **.pandaci** directory. This will help you keep your project organized.
{% /alert %}
## Workflow configuration
We allow you to export a configuration object from your workflow file. This lets you configure things like triggers or the name of the workflow.
Learn more about the configuration object in the [Configuration](/docs/typescript-syntax/config/workflow-config) section.
# [Editor setup](https://pandaci.com/docs/typescript-syntax/overview/editor-setup)
Setup local development environment for TypeScript workflows in Pandaci.
## Installing Deno
We recommend installing the Deno CLI and related tools for local development.
Follow the instructions in the [Deno installation guide](https://docs.deno.com/runtime/getting_started/installation) to install Deno on your machine.
## Editor setup
Deno have great docs covering a range of editors and IDEs, you can find them [here](https://docs.deno.com/runtime/getting_started/installation/).
## Only enable Deno for PandaCI files
If you aren't in a Deno only project, there's a good chance you'll have conflicts with other TypeScript projects. To avoid this, you can enable Deno only for PandaCI files.
The changes you need to make will depend on your editor. Here's an example for VS Code:
```json {% title=".vscode/settings.json" %}
{
"deno.enablePaths": [".pandaci"]
}
```
This will enable Deno only for files in the `.pandaci` directory.
# [Jobs](https://pandaci.com/docs/typescript-syntax/api/jobs)
Jobs are isolated machines that run your tasks
Jobs wrap your tasks, providing a runner and a context for your tasks to run in.
{% alert level="knowledge" %}
**Using tasks directly** - You can run tasks without wrapping with a job.
A default job with the **"ubuntu-4x"** runner is created for you.
{% /alert %}
## Example
```typescript {% title=".pandaci/example.workflow.ts" %}
import { job } from "jsr:@pandaci/workflow"
// Simple job
await job("A very cool job", () => {
// ...
})
// Job with a custom runner
await job("A very cool job", { runner: "ubuntu-8x" }, () => {
// ...
})
```
## Methods
### job.if
The `if` method allows you to skip a job based on a condition.
```typescript
job.if(env.PANDACI_BRANCH === "main")("Skip this job", () => {
// ...
})
```
### job.nothrow
The `nothrow` method allows you to suppress errors if the condition is met.
```typescript
job.nothrow("Skip this job", () => {
// ...
})
```
### job.skip
The `skip` method allows you to skip a job if the condition is met.
```typescript
job.skip("Skip this job", () => {
// ...
})
```
## Job options
- **runner** - The runner to use for this job. Defaults to **"ubuntu-4x"**.
- **skip** - Skip this job if the condition is met. Defaults to **false**.
- **throws** - Throw an error if the condition is met. Defaults to **true**.
## Return value
### JobPromise
The promise returned when a job is awaited. It's an extension of `Promise`.
### JobResult
The result of a job. This is whats returned when a job is awaited.
{% table %}
* Property
* Type
* Description
---
* name
* `string`
* The name of the job.
---
* conclusion
* `Conclusion` ('success' | 'failure' | 'skipped')
* The conclusion of the job.
---
* isFailure
* `boolean`
* True if the job failed.
---
* isSuccess
* `boolean`
* True if the job succeeded.
---
* isSkipped
* `boolean`
* True if the job was skipped.
---
* id
* `string`
* The id of the job.
---
* runner
* `string`
* The runner used for the job.
{% /table %}
### JobError
Thrown when a job fails.
{% table %}
* Property
* Type
* Description
---
* conclusion
* `Conclusion` ('success' | 'failure' | 'skipped')
* The conclusion of the job.
---
* isFailure
* `boolean`
* True if the job failed.
---
* isSuccess
* `boolean`
* True if the job succeeded.
---
* isSkipped
* `boolean`
* True if the job was skipped.
---
* id
* `string`
* The id of the job.
---
* runner
* `string`
* The runner used for the job.
---
* jobName
* `string`
* The name of the job.
{% /table %}
#### Usage
```typescript
import { job, type JobError } from "jsr:@pandaci/workflow"
job("A job", () => {
// A failing task
}).catch((error: JobError) => {
console.log(error.jobName)
})
```
# [Docker Tasks](https://pandaci.com/docs/typescript-syntax/api/docker-tasks)
Docker tasks let you execute commands in a Docker container
## Overview
Docker tasks are return a promise that resolves when the task is complete,
if there are unawaited exec promises, we'll wait for them to complete before resolving the task.
{% alert level="knowledge" %}
**Usage without a job** - You can run tasks without wrapping with a job.
We'll still create a default job with the **"ubuntu-4x"** runner for you.
{% /alert %}
## Example
```typescript {% title=".pandaci/example.workflow.ts" %}
import { docker, job } from "jsr:@pandaci/workflow"
// Simple docker task
await docker("ubuntu:latest", () => {
// ...
})
// Docker task with a custom runner
await job("A very cool job", { runner: "ubuntu-8x" }, () => {
docker("ubuntu:latest", { name: "A very cool task" }, () => {
// ...
})
})
```
## Volume Mounting
You can mount Docker volumes by passing an array of volumes to the `volumes` option.
{% alert level="info" %}
**Repo volume** - We always mount the repository root as **/home/pandaci/repo**.
This volume is a bind mount, so all tasks in the same job will see the same files.
{% /alert %}
```typescript {% title=".pandaci/volumes.workflow.ts" %}
import { docker, job, volume } from "jsr:@pandaci/workflow"
job("Volume Mounting", () => {
// Optionally pass a host path { host: "/path/on/host" }
const cache = volume()
docker("ubuntu:latest", { volumes: [cache('/path/on/container')] }, () => {
// ...
})
})
```
Volumes must be only be created inside a job, and they are only accessible to tasks in the same job.
## Methods
### docker.if
The `if` method allows you to skip a docker task based on a condition.
```typescript {% title=".pandaci/conditional.workflow.ts" %}
docker.if(env.PANDACI_BRANCH === "main")("Skip this task", () => {
// ...
})
```
### docker.nothrow
The `nothrow` method allows you to suppress errors if the condition is met.
```typescript {% title=".pandaci/nothrow.workflow.ts" %}
docker.nothrow("Skip this task", () => {
// ...
})
```
### docker.skip
The `skip` method allows you to skip a docker task if the condition is met.
```typescript {% title=".pandaci/skip.workflow.ts" %}
docker.skip("Skip this task", () => {
// ...
})
```
## Docker options
- **name** - The name of the task. Defaults to the name of the function.
- **skip** - Skip the task if the condition is met. Defaults to **false**.
- **throws** - Throw an error if the condition is met. Defaults to **true**.
- **volumes** - An array of volumes to mount in the container.
# [Exec Step](https://pandaci.com/docs/typescript-syntax/api/exec-step)
Execute a command in the current task.
## Overview
Exec steps must be wrapped in a [task](/docs/typescript-syntax/api/docker-tasks). They return a promise that resolves when the command is complete.
## Example
```typescript {% title=".pandaci/exec-step.workflow.ts" %}
import { docker, $ } from "jsr:@pandaci/workflow";
docker("ubuntu:latest", () => {
$`echo "Hello, world!"`;
const output = $`ls`.text();
});
```
## Piping
You can pipe the output of one command to another by using template literals.
```typescript {% title=".pandaci/exec-step-pipe.workflow.ts" %}
import { docker, $ } from "jsr:@pandaci/workflow";
docker("ubuntu:latest", () => {
const output = $`ls | grep "file"`;
$`echo ${output} > files.txt`;
});
```
We automatically escape the output of the command to prevent command injection.
## Global Exec
You can create a "global" exec by calling the `$` function without a string.
```typescript
const buildExec = $.cwd("./build");
buildExec`ls`;
const testExec = $({ cwd: "./test" });
testExec`ls`;
```
## Methods
### exec.cwd
The `cwd` method allows you to change the current working directory for the command.
```typescript
$`ls`.cwd("/home/pandaci");
```
### nothrow
The `nothrow` method allows you to ignore errors from the command.
```typescript
$`ls`.nothrow();
```
### throws
The `throws` method allows you to throw an error if the command fails.
```typescript
$`ls`.throws(true);
```
### env
The `env` method allows you to set environment variables for the command.
```typescript
$`echo $MY_ENV_VAR`.env({ MY_ENV_VAR: "Hello, world!" });
```
## Returns
The promise returned by the exec step resolves with the output of the command as an object with the following properties:
### stdout
- Type: **Uint8Array**
The standard output of the command.
### stderr
- Type: **Uint8Array**
The standard error of the command.
### stdall
- Type: **Uint8Array**
The combined standard output and standard error of the command.
### exitCode
- Type: **number**
The exit code of the command.
### text
- Type: **(encoding?: string = "utf-8") => string**
A method that returns the output as a string.
### json
- Type: **() => T**
A method that returns the output as a JSON object.
### lines
- Type: **(encoding?: string = "utf-8") => string[]**
A method that returns the output as an array of lines.
## Options
You can use also use an object to pass options to the exec step.
```typescript
$({ cwd: "/home/pandaci", throws: false })`ls`;
```
- **cwd** - The current working directory for the command.
- **throws** - Throw an error if the command fails. Defaults to **true**.
- **nothrow** - Ignore errors from the command. Defaults to **false**.
# [Environment Variables](https://pandaci.com/docs/typescript-syntax/api/env)
How to use environment variables in PandaCI
{% alert level="knowledge" %}
See all available environment variables in the [workflow environment](/docs/platform/workflows/env).
You can also set your own environment variables using [secrets](/docs/platform/overview/secrets).
{% /alert %}
## Example
```typescript {% title=".pandaci/env.workflow.ts" %}
import { docker, $, env } from "jsr:@pandaci/workflow";
docker("ubuntu:latest", () => {
$`echo "Hello, world! from branch: ${env.PANDACI_BRANCH}"`;
});
```
## Using custom types
You can use custom types to define your own environment variables.
```typescript {% title=".pandaci/custom-env.workflow.ts" %}
import { docker, $, env as rawEnv, type Env } from "jsr:@pandaci/workflow";
const env = rawEnv as Env<{
MY_ENV_VAR: string;
}>;
docker("ubuntu:latest", () => {
// typesafe environment variable
$`echo $MY_ENV_VAR`.env({ MY_ENV_VAR: env.MY_ENV_VAR });
});
```
{% alert level="info" %}
Whilst you can pass the **env** object directly to the **env** method,
we recommend only passing the specific environment variables you need to avoid leaking sensitive information.
{% /alert %}
# [Workflow triggers](https://pandaci.com/docs/typescript-syntax/config/triggers)
Configure which git events trigger your workflows
## Overview
You can configure the triggers using the workflow config object.
{% alert level="knowledge" %}
**Workflow configs are staticaly parsed** - This means that you can't use variables or functions to define the config.
{% /alert %}
We default to triggering on every push.
### Example
```typescript {% title=".pandaci/triggers.workflow.ts" %}
import { type Config } from "jsr:@pandaci/workflow";
export const config: Config = {
on: {
push: {
branches: ["main"]
},
pr: {
events: ["opened", "synchronize", "reopened"],
targetBranches: ["main"]
}
}
};
```
## Glob matching
You can use glob patterns to match branches.
### Examples
- `main` - Matches the branch named **main**
- `feature/*` - Matches all branches starting with **feature/**
- `*` - Matches all branches
## Push configuration
You can configure the push event to trigger on specific branches.
### branches
- **Type**: `string[]`
The branches to trigger the workflow on.
### branchesIgnore
- **Type**: `string[]`
The branches to ignore when triggering the workflow.
## Pull Request configuration
You can configure the pull request event to trigger on specific events and target branches.
### events
- **Type**: `('opened', 'closed', 'reopened', 'synchronize')[]`
The events to trigger the workflow on.
### targetBranches
- **Type**: `string[]`
The target branches to trigger the workflow on.
### targetBranchesIgnore
- **Type**: `string[]`
The target branches to ignore when triggering the workflow.
# [Workflow config](https://pandaci.com/docs/typescript-syntax/config/workflow-config)
Workflow config options
## Overview
You can configure the workflow by exporting a `config` object from your workflow file.
{% alert level="knowledge" %}
**Workflow configs are staticaly parsed** - This means that you can't use variables or functions to define the config.
{% /alert %}
## Example
```typescript {% title=".pandaci/config.workflow.ts" %}
import { type Config } from "jsr:@pandaci/workflow";
export const config: Config = {
name: "My workflow",
on: {
push: {
branches: ["main"]
}
}
};
```
## Config options
### name
- **Type**: `string`
- **Default**: the name of the workflow file
The name of the workflow.
### on
Learn more about [workflow triggers](/docs/platform/workflows/triggers).
- **Default**: `{ push: { branches: ["*"] } }`
The triggers that start the workflow.
# [Installing packages](https://pandaci.com/docs/typescript-syntax/guides/installing-packages)
Install jsr, npm or other packages into your workflow
## Overview
PandaCI workflows use the Deno (v2) runtime. This means you can use both Deno and Node.js packages in your workflows.
The easiest way is to just import the packages in your file. Deno will automatically download and cache the packages for you.
```typescript
import { WebClient } from "npm:@slack/web-api"; // installs from npm
import { $ } from "jsr:@pandaci/workflow"; // installs from jsr
```
You can learn more about the [Deno dependency system](https://docs.deno.com/runtime/fundamentals/modules/).
# [Sending notifications](https://pandaci.com/docs/typescript-syntax/guides/notifications)
Send notifications from your workflows
{% alert level="knowledge" %}
Whilst we don't have any built-in notification support, it's incredibly easy to send notifications from your workflows.
{% /alert %}
## Slack
To send a message to a Slack channel, we recommend using the slack npm package. See our [example workflow](/docs/typescript-syntax/examples/slack) for more information.
## Webhooks
Since webhooks are just HTTP requests, you can use the fetch API to send a request to a webhook URL.
```typescript
fetch("https://webhook.site/your-unique-id", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
message: "Hello, world!"
})
});
```
Depending on your destination, there may be an offical npm package to help you send the request.
## Email
To send an email, you can use the [nodemailer](https://www.npmjs.com/package/nodemailer) package.
[Learn how to install packages](/docs/typescript-syntax/guides/installing-packages).
# [Node project](https://pandaci.com/docs/typescript-syntax/examples/node)
A comprehensive guide to setting up CI/CD workflows for Node.js applications in PandaCI.
{% alert level="check" %}
**Prerequisites**
You need an account before starting, create one [here](https://app.pandaci.com/signup).
{% /alert %}
## Basic Node.js workflow
This simple workflow installs dependencies and runs tests and linting commands:
```typescript {% title=".pandaci/ci.workflow.ts" %}
import { docker, $ } from "jsr:@pandaci/workflow";
docker("node:20", async () => {
// Install dependencies
await $`npm ci`;
// Run tests and linting in parallel
await Promise.all([
$`npm run test`,
$`npm run lint`
]);
});
```
## Advanced Node.js workflow
This more advanced workflow uses caching, separate jobs, and only deploys on the main branch:
```typescript {% title=".pandaci/advanced-ci.workflow.ts" %}
import { docker, $, job, env, volume } from "jsr:@pandaci/workflow";
import { type Config } from "jsr:@pandaci/workflow";
// Configure workflow triggers
export const config: Config = {
name: "Node.js CI Pipeline",
on: {
push: {
branches: ["main", "develop", "feature/*"]
},
pr: {
events: ["opened", "synchronize", "reopened"],
targetBranches: ["main", "develop"]
}
}
};
await docker("node:20", async () => {
// Install dependencies
await $`npm ci`;
// Run tests and linting in parallel
await Promise.all([
$`npm run test`,
$`npm run lint`
]);
});
// Build & Deploy - only runs on main branch
job.if(env.PANDACI_BRANCH === "main")("Deploy", () => {
docker("node:20", async () => {
await $`npm ci`;
await $`npm run build`;
// Your deployment command here
// For example: $`npx firebase deploy --token ${env.FIREBASE_TOKEN}`;
});
});
```
## Package Manager Options
### Using npm
```typescript {% title=".pandaci/npm-ci.workflow.ts" %}
import { docker, $ } from "jsr:@pandaci/workflow";
docker("node:20", async () => {
// Use npm ci for clean installs in CI environments
await $`npm ci`;
// Run your project commands
await $`npm test`;
await $`npm run build`;
});
```
### Using PNPM
```typescript {% title=".pandaci/pnpm-ci.workflow.ts" %}
import { docker, $ } from "jsr:@pandaci/workflow";
// Helper function to set up PNPM
export async function setupPnpm() {
await $`PNPM_HOME="/pnpm"`;
await $`PATH="$PNPM_HOME:$PATH"`;
// Enable corepack (included with modern Node.js)
await $`corepack enable`;
await $`pnpm i`;
}
docker("node:20", async () => {
await setupPnpm();
// Run your project commands
await $`pnpm test`;
await $`pnpm build`;
});
```
### Using Yarn
```typescript {% title=".pandaci/yarn-ci.workflow.ts" %}
import { docker, $ } from "jsr:@pandaci/workflow";
// Helper function to set up Yarn
export async function setupYarn() {
// Enable corepack (included with modern Node.js)
await $`corepack enable`;
// Install dependencies with frozen lockfile
await $`yarn install --frozen-lockfile`;
}
docker("node:20", async () => {
await setupYarn();
// Run your project commands
await $`yarn test`;
await $`yarn build`;
});
```
## Testing with multiple Node.js versions
Test your application across different Node.js versions:
```typescript {% title=".pandaci/node-matrix.workflow.ts" %}
import { docker, $, job } from "jsr:@pandaci/workflow";
// Define Node.js versions to test against
const nodeVersions = ["16", "18", "20"];
// Run tests for each Node.js version
for (const version of nodeVersions) {
job(`Node ${version}`, () => {
docker(`node:${version}`, async () => {
await $`npm ci`;
await $`npm test`;
});
});
}
```
## Including code coverage reports
Generate and analyze code coverage reports:
```typescript {% title=".pandaci/code-coverage.workflow.ts" %}
import { docker, $ } from "jsr:@pandaci/workflow";
docker("node:20", async () => {
await $`npm ci`;
// Run tests with coverage
await $`npm run test:coverage`;
// Optional: Check coverage thresholds
await $`npx nyc check-coverage --lines 80 --functions 80 --branches 70`;
// Optional: Upload coverage report to a service like Codecov
if (env.CODECOV_TOKEN) {
await $`npx codecov --token=${env.CODECOV_TOKEN}`;
}
});
```
{% alert level="knowledge" %}
Learn more about the [workflow syntax](/docs/typescript-syntax/overview/overview).
{% /alert %}
# [Python project](https://pandaci.com/docs/typescript-syntax/examples/python)
A basic Python workflow for testing and package management
{% alert level="check" %}
**Prerequisites**
You need an account before starting, create one [here](https://app.pandaci.com/signup).
{% /alert %}
## Basic Python workflow
```typescript {% title=".pandaci/python-ci.workflow.ts" %}
import { docker, $ } from "jsr:@pandaci/workflow";
docker("python:3.11-slim", async () => {
// Install requirements
await $`pip install -r requirements.txt`;
// Run tests
await $`python -m pytest`;
// Run linting
await $`flake8 .`;
});
```
## Using virtual environments
For more complex projects, you might want to use a virtual environment:
```typescript {% title=".pandaci/python-venv.workflow.ts" %}
import { docker, $ } from "jsr:@pandaci/workflow";
docker("python:3.11-slim", async () => {
// Create and activate virtual environment
await $`python -m venv .venv`;
await $`source .venv/bin/activate`;
// Install requirements
await $`pip install -r requirements.txt`;
// Run tests
await $`python -m pytest`;
});
```
## Using Poetry
If you're using Poetry for dependency management:
```typescript {% title=".pandaci/python-poetry.workflow.ts" %}
import { docker, $ } from "jsr:@pandaci/workflow";
docker("python:3.11-slim", async () => {
// Install Poetry
await $`curl -sSL https://install.python-poetry.org | python3 -`;
await $`export PATH="/root/.local/bin:$PATH"`;
// Install dependencies
await $`poetry install`;
// Run tests
await $`poetry run pytest`;
// Run linting
await $`poetry run flake8 .`;
});
```
## Multiple Python versions
Test your code against multiple Python versions:
```typescript {% title=".pandaci/python-matrix.workflow.ts" %}
import { docker, $, job } from "jsr:@pandaci/workflow";
// Define Python versions to test against
const pythonVersions = ["3.8", "3.9", "3.10", "3.11"];
// Create a job for each Python version
for (const version of pythonVersions) {
job(`Python ${version}`, async () => {
docker(`python:${version}-slim`, async () => {
await $`pip install -r requirements.txt`;
await $`python -m pytest`;
});
});
}
```
## Building and Publishing
Build and publish your Python package to PyPI:
```typescript {% title=".pandaci/python-publish.workflow.ts" %}
import { docker, $, env } from "jsr:@pandaci/workflow";
// Only publish on main branch
// you could also just use an if statement
docker.if(env.PANDACI_BRANCH === "main")("python:3.11-slim", async () => {
// Install build tools
await $`pip install build twine`;
// Build package
await $`python -m build`;
// Publish to PyPI
await $`python -m twine upload dist/* --username __token__ --password ${env.PYPI_TOKEN}`;
});
```
{% alert level="knowledge" %}
Learn more about the [workflow syntax](/docs/typescript-syntax/overview/overview).
{% /alert %}
# [Slack notifications](https://pandaci.com/docs/typescript-syntax/examples/slack)
Send a Slack notification when a workflow fails
{% alert level="check" %}
**Prerequisites**
You need an account before starting, create one [here](https://app.pandaci.com/signup).
{% /alert %}
We can use the offical slack npm package to send notifications to a slack channel.
## Prerequisites
Since we're just using the offical slack npm package, there is not one specific way to set up slack notifications.
For this example, we're going to create a slack app and use a bot token to send messages.
### 1. Create a slack app
Go to the [slack api page](https://api.slack.com/apps) and click on **Create New App**.
For our example, we only need the `chat:write` permission. You should add more permissions depending on your use case.
Use the following manifest to quickly create a slack app with the necessary permissions:
```json
{
"_metadata": {
"major_version": 1,
"minor_version": 1
},
"display_information": {
"name": "PandaCI Slack App",
"description": "Sends workflow notifications from the greatest CI/CD platform"
},
"features": {
"bot_user": {
"display_name": "PandaCI Bot",
"always_online": true
},
"app_home": {
"home_tab_enabled": false
}
},
"oauth_config": {
"scopes": {
"bot": [
"chat:write"
]
}
}
}
```
### 2. Install the app
In your slack app settings, click on **Install app** and follow the instructions.
### 3. Save the bot token
Copy the bot token and add it to your PandaCI project variables. For this example, we're going to name it `SLACK_BOT_TOKEN`.
### 4. Get the channel id
{% alert level="knowledge" %}
You can skip this if you only want to send messages directly to users.
{% /alert %}
You can find the channel id in the channel details modal. For this example, we're going to store it in the `SLACK_CHANNEL_ID` variable.
But since this isn't a secret, you can also hardcode it in your workflow.
Learn more about [secrets](/docs/platform/overview/secrets).
{% alert level="info" %}
Make sure you add the slack app to the channel you want to send messages to.
{% /alert %}
## Simple example
```typescript {% title=".pandaci/slack.workflow.ts" %}
import { docker, $, env } from "jsr:@pandaci/workflow";
import { WebClient } from "npm:@slack/web-api";
const slack = new WebClient(env.SLACK_BOT_TOKEN);
docker("ubuntu:latest", async () => {
// A task which fails
await $`exit 1`;
}).catch(() =>
// Send a message to our Slack channel
slack.chat.postMessage({
channel: env.SLACK_CHANNEL_ID,
text: `Workflow failed!`,
})
);
```
## Sending markdown messages
You can send markdown messages to slack by setting the `mrkdwn` property to `true`.
```typescript
slack.chat.postMessage({
channel: env.SLACK_CHANNEL_ID,
text: `# Workflow failed!`,
mrkdwn: true,
});
```
## Sending messages with blocks
You can send messages with blocks to slack by setting the `blocks` property.
```typescript
slack.chat.postMessage({
channel: env.SLACK_CHANNEL_ID,
text: `Workflow failed!`,
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: "*Workflow failed!*",
},
},
],
});
```