Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
609e3cc
Dynamic CI: a complete GitHub Actions setup guide
EliSchleifer Nov 4, 2026
31850d0
Update Getting started
EliSchleifer Nov 4, 2026
fd08873
Update Getting started
EliSchleifer Nov 4, 2026
84bc851
Update Getting started
EliSchleifer Nov 4, 2026
13a9902
Update Getting started
EliSchleifer Nov 4, 2026
3dd59b0
Update Getting started
EliSchleifer Nov 4, 2026
68ab51e
Update Getting started
EliSchleifer Nov 4, 2026
00186d9
Update GitHub Actions
EliSchleifer Nov 4, 2026
8a7543d
Update Getting started and GitHub Actions
EliSchleifer Nov 4, 2026
bb0426b
Update GitHub Actions
EliSchleifer Nov 4, 2026
5fa1b0c
Update GitHub Actions
EliSchleifer Nov 4, 2026
27a21ce
Update GitHub Actions
EliSchleifer Nov 4, 2026
3a296a0
Update GitHub Actions
EliSchleifer Nov 4, 2026
190f849
Update GitHub Actions
EliSchleifer Nov 4, 2026
b4e7382
Update GitHub Actions
EliSchleifer Nov 4, 2026
9584577
Update GitHub Actions
EliSchleifer Nov 4, 2026
76ec4fe
Update GitHub Actions
EliSchleifer Nov 4, 2026
80b5ccd
Update GitHub Actions
EliSchleifer Nov 4, 2026
6230f51
Update GitHub Actions
EliSchleifer Nov 4, 2026
f695e4b
Update GitHub Actions
EliSchleifer Nov 4, 2026
b173a9c
Update GitHub Actions
EliSchleifer Nov 4, 2026
5a31ed4
Update GitHub Actions
EliSchleifer Nov 4, 2026
86c7ecb
Update GitHub Actions
EliSchleifer Nov 4, 2026
49703b0
Update GitHub Actions
EliSchleifer Nov 4, 2026
2ff3411
Update GitHub Actions
EliSchleifer Nov 4, 2026
e6e31f4
Update GitHub Actions
EliSchleifer Nov 4, 2026
1972810
Update GitHub Actions
EliSchleifer Nov 4, 2026
1ff60d8
Update GitHub Actions and 2 more pages
EliSchleifer Nov 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ci/dynamic-ci/configuration.mdx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
title: "Configure how Dynamic CI schedules each CI job"
sidebarTitle: "Configuration"
description: "Tune Dynamic CI per job in the Trunk web app: set schedules for pull requests and the merge queue, and add path filters that feed the scoring engine."
description: "Tune Dynamic CI per job in the Trunk app"
---

Dynamic CI is configured job by job on the [Configuration tab](https://app.trunk.io/ci/dynamic-ci?tab=configuration) in the Trunk web app. Each job has a schedule that controls how it is evaluated on pull requests and in the merge queue, plus optional path filters that tell Dynamic CI which changes are relevant to it.
Dynamic CI is configured job by job on the [Configuration tab](https://app.trunk.io/ci/dynamic-ci/configuration) in the Trunk web app. Each job has a schedule that controls how it is evaluated on pull requests and in the merge queue, plus optional path filters that tell Dynamic CI which changes are relevant to it.

## Find a job

Expand Down
66 changes: 66 additions & 0 deletions ci/dynamic-ci/dynamic-ci-action-reference.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: "Dynamic CI Action Reference"
sidebarTitle: "Action reference"
description: "Inputs, outputs, modes, and fail-open behavior of the trunk-io/dynamic-ci GitHub Action."
og:title: "trunk-io/dynamic-ci GitHub Action reference"
---

The [`trunk-io/dynamic-ci`](https://github.com/trunk-io/dynamic-ci) action connects a GitHub Actions workflow to Dynamic CI. It asks Trunk which jobs the current change needs and writes one output per job. Your jobs read those outputs in their `if:` conditions to decide whether to run.

## Usage

The action sets one output per job in the workflow, named by the job key. Outputs are set at runtime, so re-export each one from the filter job's `outputs:` under the same key:

```yaml
jobs:
dynamic-ci:
runs-on: ubuntu-latest
outputs:
# 'false': Trunk recommends skipping the job.
# Any other value: Trunk recommends running the job.
# Empty: no verdict (Trunk unreachable, missing token, or no history
# yet). The job should run.
unit-tests: ${{ steps.dynamic-ci.outputs.unit-tests }}
steps:
- uses: actions/checkout@v7
with:
clean: false
- uses: trunk-io/dynamic-ci@v1
with:
# Your organization API token. Store it as a repository or organization
# secret, such as TRUNK_API_TOKEN
# Required.
token: ${{ secrets.TRUNK_API_TOKEN }}

# Token used to read the pull request's changed files when the checkout
# does not contain both the base and head commits
# Default: ${{ github.token }}
github-token: ''

# Post the recommendation for each job to the run's annotations and job
# summary, so verdicts show on the run's summary page
# Default: false
enable-annotation: ''
```

## Fail-open behavior

The action is built so a problem never causes a job to be skipped by mistake. It retries requests to Trunk, and when it still cannot get a verdict, the output is empty and the job will run.

## Checkout

The action compares the pull request's base and head commits to find the changed files.`actions/checkout` with `clean: false` is enough. When the checkout does not contain both commits, the action falls back to `github-token` to read the changed files.

## Versioning

Pin to the major version tag, `trunk-io/dynamic-ci@v1`, to receive compatible updates automatically.

## GitHub token permissions

Grant the workflow `pull-requests: read` so `github-token` can read the pull request's changed files:

```yaml
permissions:
contents: read
pull-requests: read
```
154 changes: 74 additions & 80 deletions ci/dynamic-ci/getting-started/github-actions.mdx
Original file line number Diff line number Diff line change
@@ -1,105 +1,99 @@
---
title: "GitHub Actions"
description: "Add the Dynamic CI filter job to a GitHub Actions workflow."
description: "Adding Dynamic CI to your existing GitHub Actions workflows"
og:title: "Set up Trunk Dynamic CI for GitHub Actions"
---

<Note>
Before you start, [set up Trunk CI for GitHub Actions](/ci/get-started/github-actions)
and copy your organization API token from the **Organization API token (v1)**
section of [Settings → Developer → API Keys](https://app.trunk.io/settings/developer/api-tokens). Store it
as a repository secret named `TRUNK_API_TOKEN`.
</Note>
## How it works

## Add the filter job to the top of your workflow
A filter job at the top of your workflow runs the `trunk-io/dynamic-ci` action. The action asks Trunk which jobs this change needs, and writes one output per job. The `outputs` of the Dynamic CI step will store the verdict for each job specified. By listing a job in the list you are asking Dynamic CI to weigh in and attempt to manage that job.<br /><br />If Dynamic CI determines a job should be skipped it will set the output variable named after the job to `'false'.`At each job level you will need to add an `if: `clause to read that matching variable.

List every job you want to gate under `outputs`. Check out first, with full
history, so the action can read the changed files.
If Trunk is offline, the token is missing, or a job has no verdict yet, the output will be empty and the job will run. This allows Dynamic CI to "fail open" so you will not inadvertently skip jobs.

## Workflow Example

This workflow gates two jobs. Note the job ids are the critical matched. values in this flow

```yaml
name: CI

on:
pull_request: {}

jobs:
dynamic-ci-filter:
dynamic-ci:
name: Dynamic CI
runs-on: ubuntu-latest
timeout-minutes: 5
if: github.event_name == 'pull_request'
# One output per gated job, named by its job key.
outputs:
unit-tests: ${{ steps.ci-filter.outputs.unit-tests }}
integration-tests: ${{ steps.ci-filter.outputs.integration-tests }}
unit-tests: ${{ steps.dynamic-ci.outputs.unit-tests }}
playwright-tests: ${{ steps.dynamic-ci.outputs.playwright-tests }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0
filter: blob:none
- name: Dynamic CI
id: ci-filter
uses: trunk-io/dynamic-ci@v1
clean: false
- uses: trunk-io/dynamic-ci@v1
with:
token: ${{ secrets.TRUNK_API_TOKEN }}
```

## Gate your jobs on it
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
needs: [dynamic-ci]
if: >-
!cancelled() &&
needs.dynamic-ci.outputs.unit-tests != 'false'
steps:
- uses: actions/checkout@v7
- run: make test

```yaml
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
needs: [dynamic-ci-filter]
if: >-
!cancelled() &&
needs.dynamic-ci-filter.outputs.unit-tests != 'false'
steps:
- run: make test
playwright-tests:
name: Playwright Tests
runs-on: ubuntu-latest
needs: [dynamic-ci]
if: >-
!cancelled() &&
needs.dynamic-ci.outputs.playwright-tests != 'false'
steps:
- uses: actions/checkout@v7
- run: make test
```

<Warning>
Gate on `!= 'false'`, never on `== 'true'`. A job with no verdict has no
output, and `!= 'false'` makes it run.
</Warning>
## Guidance for gating a job correctly

Use each job's key from `jobs:`, not its display `name:`.
<Steps>
<Step title="Use the job key, not the display name">
Outputs are named by the key under `jobs:` In the example above playwright-tests ad unit-tests are the job ids.

<Accordion title="Gating a single job instead (pre-step mode)">
A job can ask about itself by passing its own key in `job-keys`, and skip its
own steps. It saves less, because the runner has already started.
Re-export each gated job's output from the filter job's `outputs:`, under the same key. The action sets its outputs at runtime, so GitHub cannot pass them on unless you list them there.
</Step>

```yaml
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
filter: blob:none
- name: Dynamic CI
id: ci-filter
uses: trunk-io/dynamic-ci@v1
with:
token: ${{ secrets.TRUNK_API_TOKEN }}
job-keys: unit-tests
- if: steps.ci-filter.outputs.unit-tests != 'false'
run: make test
```
</Accordion>

<Accordion title="Send your build tool's impact">
If your build tool measures how much of the codebase a change affects, such
as a count of affected packages, pass it as a JSON list so Trunk runs broad
changes:

```yaml
with:
token: ${{ secrets.TRUNK_API_TOKEN }}
impacts: '[{"source":"tainted-packages","count":${{ steps.affected.outputs.count }}}]'
```

Use `"all": true` in place of `count` when everything is affected, and leave
`impacts` unset when your tool computed nothing. A bad entry is dropped with
a warning. Ask us to turn the signal on and set each job's limit.
</Accordion>

## Next

[Turn on enforcement](/ci/dynamic-ci/getting-started) once the
recommendations look right. For how Dynamic CI behaves in a merge queue, see
[Merge queues](/ci/dynamic-ci/merge-queue).
<Step title="Export the verdict for each job you want to dynamically manage in the outputs section of the Dynamic CI job ">
```yaml
outputs:
unit-tests: ${{ steps.dynamic-ci.outputs.unit-tests }}
playwright-tests: ${{ steps.dynamic-ci.outputs.playwright-tests
```
</Step>

<Step title="List the filter job in needs ">
Each managed job has should specify `needs: [dynamic-ci-filter]`. This ensures the dynamic ci verdict will be available to assess before the job ever spins up.
</Step>

<Step title="Start the condition with !cancelled()">
By default GitHub skips a job when a job it needs failed or was skipped. Without `!cancelled()`, a failed filter job (a bad token, an outage) skips every gated job, and nothing is tested.
</Step>

<Step title="Compare with != 'false' on the output of the matching job">
Join other conditions with &&
</Step>
</Steps>

When your workflow files are updated and start calling into Dynamic CI a background linter will run to verify the format of your yaml file and call out problems.

## Check your setup

1. Open a pull request. The **Dynamic CI's** verdict will be reported on each pull request under the dynamic ci tab. Additionally you can see all decisions made by the system in the [Dynamic CI page](https://app.trunk.io/dynamic-ci).
2. Your repository starts in **Learning** mode, which records verdicts but will not actively skip anything. Once the recommendations look right you can [turn on enforcement](/ci/dynamic-ci/settings).
13 changes: 6 additions & 7 deletions ci/dynamic-ci/getting-started/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@ og:title: "Get started with Trunk Dynamic CI"
---

<Warning>
Dynamic CI is in **private beta**, and your organization has to be enabled for
it first. [Reach out to Trunk](https://slack.trunk.io) for access.
Dynamic CI is in **private beta**. [Reach out to Trunk](https://slack.trunk.io) for access.
</Warning>

<Steps>
<Step title="Set up Trunk CI">
Dynamic CI decides from your CI history, so Trunk has to be collecting it.
Follow [Set up Trunk CI](/ci/get-started) for your provider, then copy your
organization API token from the **Organization API token (v1)** section of
[Settings → Developer → API Keys](https://app.trunk.io/settings/developer/api-tokens).
<Step title="[Set up Trunk CI](/ci/get-started): this allows trunk to learn the performance, reliability and runtime behavior os your CI jobs. Critical for making informed decisions on which jobs to skip.">
decides from your CI history, Follow [Set up Trunk CI](/ci/get-started) for your provider, then copy your organization API token from the **Organization API token (v1)** section of [Settings → Developer → API Keys](https://app.trunk.io/settings/developer/api-tokens).
</Step>

<Step title="Add Organization Token to GitHub secrets : Get your org token [here](https://app.trunk.io/settings/developer/api-tokens) and store the org token as a repository secret named `TRUNK_API_TOKEN`">
</Step>

<Step title="Add Dynamic CI to your CI">
Expand Down
1 change: 1 addition & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@
"ci/dynamic-ci/settings",
"ci/dynamic-ci/force-run",
"ci/dynamic-ci/merge-queue",
"ci/dynamic-ci/dynamic-ci-action-reference",
"ci/dynamic-ci/faq"
]
}
Expand Down
Loading