Docs
Docs
CI

Workflow Examples

Compatible Trylle CI examples for filters, matrices, dependencies, reusable workflows, manual runs, caches, and artifacts.

These examples use the workflow surface currently supported by Trylle CI. Store each top-level workflow in .github/workflows or .trylle/workflows, according to the repository's Actions policy.

Push and pull request filters

name: Validate changes

on:
  push:
    branches:
      - main
      - "release/**"
    paths:
      - "src/**"
      - "package-lock.json"
      - "!src/generated/**"
  pull_request:
    types: [opened, reopened, synchronize]
    branches: [main]
    paths-ignore:
      - "docs/**"

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

Patterns are evaluated in order, so a later ! entry can remove a path included by an earlier entry. Pull request branch filters apply to the base branch.

Static matrix

name: Test matrix

on:
  push:
  pull_request:

jobs:
  test:
    strategy:
      fail-fast: false
      max-parallel: 3
      matrix:
        runner: [ubuntu-latest, ubuntu-latest-arm]
        node: [20, 22]
        exclude:
          - runner: ubuntu-latest-arm
            node: 20
        include:
          - runner: ubuntu-latest
            node: 24
            experimental: true
    continue-on-error: ${{ matrix.experimental == true }}
    runs-on: ${{ matrix.runner }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci
      - run: npm test
      - uses: actions/upload-artifact@v4
        with:
          name: results-${{ matrix.runner }}-node-${{ matrix.node }}
          path: test-results/
          if-no-files-found: ignore

Keep matrices static. A matrix such as ${{ fromJSON(needs.plan.outputs.matrix) }} is not currently expanded.

Dependencies and job outputs

name: Build and publish metadata

on:
  push:
    branches: [main]

jobs:
  version:
    runs-on: ubuntu-latest
    outputs:
      value: ${{ steps.version.outputs.value }}
    steps:
      - id: version
        run: echo "value=1.0.${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT"

  build:
    needs: version
    runs-on: ubuntu-latest
    env:
      RELEASE_VERSION: ${{ needs.version.outputs.value }}
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/build.sh "$RELEASE_VERSION"

  report:
    if: ${{ always() }}
    needs: [version, build]
    runs-on: ubuntu-latest
    steps:
      - run: echo "Build result was ${{ needs.build.result }}"

Trylle runs independent jobs in the same dependency wave and starts a later wave after its needs requirements resolve.

Local reusable workflow

Caller, for example .github/workflows/ci.yml:

name: CI

on:
  push:
  pull_request:

jobs:
  test:
    uses: ./.github/workflows/reusable-test.yml
    with:
      node-version: "22"

Called workflow, .github/workflows/reusable-test.yml:

name: Reusable test

on:
  workflow_call:
    inputs:
      node-version:
        required: true
        type: string

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm ci
      - run: npm test

Local calls may target either workflow root. Remote calls use owner/repository/.github/workflows/file.yml@ref. Reusable workflows can nest up to four levels.

Manual workflow with inputs

name: Release candidate

on:
  workflow_dispatch:
    inputs:
      channel:
        description: Release channel
        required: true
        type: choice
        options: [preview, stable]
        default: preview
      publish:
        description: Publish artifacts
        type: boolean
        default: false
      retries:
        description: Retry count
        type: number
        default: 1
      note:
        description: Release note
        type: string
        required: false

jobs:
  release:
    runs-on: ubuntu-latest
    env:
      CHANNEL: ${{ inputs.channel }}
      PUBLISH: ${{ inputs.publish }}
      RETRIES: ${{ inputs.retries }}
      NOTE: ${{ inputs.note }}
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/release-candidate.sh

Open the repository's Actions page, select Run workflow, choose a branch or tag, select this workflow, complete the inputs, and run it. Manual dispatch requires repository write access.

Cache and pass artifacts between jobs

name: Cached build

on:
  push:
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            npm-${{ runner.os }}-
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: web-dist
          path: dist/
          if-no-files-found: error

  inspect:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: web-dist
          path: downloaded-dist
      - run: find downloaded-dist -type f -maxdepth 2 -print

Uploaded artifacts appear on the workflow run page and can be downloaded after the job completes.

Read another repository

Acme keeps deployment settings in a private acme/platform-config repository. The acme/web pipeline validates them on every pull request without a personal token or a copied deploy key.

First, create a read-only bot and install it on the organization. Run this once with your own CLI key:

try bot create --slug config-reader --name "Config reader" \
  --scope repo:read --scope files:read --install acme

Then reference the bot in the workflow. The validate job reads both repositories; the comment job writes to the pull request with a different bot, because a job that reads other repositories must stay read-only:

name: Validate config

on:
  pull_request:

jobs:
  validate:
    bot: config-reader
    bot-repositories: [acme/platform-config]
    permissions:
      contents: read
    runs-on: ubuntu-latest
    outputs:
      report: ${{ steps.check.outputs.report }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/checkout@v4
        with:
          repository: acme/platform-config
          path: platform-config
      - id: check
        run: |
          ./scripts/validate-config.sh platform-config > report.txt
          echo "report=$(head -c 900 report.txt | tr '\n' ' ')" >> "$GITHUB_OUTPUT"

  comment:
    needs: validate
    bot: review-bot
    permissions:
      pull-requests: write
    runs-on: ubuntu-latest
    steps:
      - run: gh pr comment "$PR_NUMBER" --body "Config check: $REPORT"
        env:
          PR_NUMBER: ${{ github.event.pull_request.number }}
          REPORT: ${{ needs.validate.outputs.report }}

The run is refused if config-reader is not installed on acme, or if the person who opened the pull request cannot read acme/platform-config. See Read other repositories for every rule.

Run a job inside Docker

Job-level container: is not supported. Use Docker from a Linux step instead:

name: Container test

on:
  push:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: >-
          docker run --rm
          -v "$PWD:/workspace"
          -w /workspace
          node:22
          npm test

On this page