A worked CI pipeline, end to end

One repository, one workflow, from install to a gated land.

Written By Dustin

Last updated About 3 hours ago

A complete GitHub Actions workflow that checks out a TOVIO repository with no stored secret, runs a build, and gates the land. Adapt the names; the shape is the point.

1. Trust the runner's issuer, once

tovio agent issuer-policy ci-main \
  --issuer https://token.actions.githubusercontent.com \
  --audience tovio-forge \
  --subject repo:my-org/my-repo:ref:refs/heads/main \
  --path "src/**" --path "tests/**" \
  --op read --op relay:fetch \
  --max-session-seconds 900 \
  --file ci-main.vex

Commit ci-main.vex to the served repository at .tovio/forge/runner-issuers/ci-main.vex. The owner key stays on your machine.

2. The workflow

name: CI
on: [pull_request]
permissions:
  contents: read
  id-token: write
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Install a verified TOVIO release
        id: tovio
        uses: Tovio-VCS/tovio/packages/tovio-ci-bridge/setup@<full-commit-sha>
        with:
          version: 1.0.0
        env:
          GH_TOKEN: ${{ github.token }}
      - name: Checkout the change
        uses: Tovio-VCS/tovio/packages/tovio-ci-bridge@<full-commit-sha>
        with:
          operation: checkout
          tovio-path: ${{ steps.tovio.outputs.tovio-path }}
          remote: https://forge.example.test
          repository-id: ${{ vars.TOVIO_REPO_ID }}
          destination: ${{ github.workspace }}
          sparse: |
            src/**
            tests/**
          oidc-policy: ci-main
          oidc-audience: tovio-forge
          timeout-seconds: "900"
      - name: Conflict-free gate
        working-directory: ${{ github.workspace }}
        run: ${{ steps.tovio.outputs.tovio-path }} build-check
      - name: Build and test
        working-directory: ${{ github.workspace }}
        run: |
          cargo build --release
          cargo test --workspace

The gate is a plain run: step, not a bridge operation — the bridge's operation set is closed to checkout, sync, git-bridge, health, and status, and health is a diagnostic that exits 0 even on a conflicted lane. build-check is the one that actually fails.

3. What each piece is buying you

PieceWhy
Pinned commit SHAsA mutable tag on a third-party action is a supply-chain hole. Pin both actions to the same immutable SHA.
Separate setup actionThe binary is checksum- and provenance-verified before any operation runs. The operation wrapper deliberately cannot install anything.
id-token: writeLets the job mint an OIDC token. No TOVIO credential is stored in GitHub secrets at all.
sparseFetches only the paths the build needs. Also matches the issuer policy's path scope, so the runner's token could not read more even if the workflow asked.
timeout-secondsExplicit bound; the wrapper terminates the exact child process tree on expiry rather than hanging the job.
build-check before the buildFails fast on a conflicted lane tip instead of burning build minutes to discover it. Use this, not healthhealth is a diagnostic and exits 0 either way.

4. Make the check required

Declare the check as required on the target lane. The land gate is default-closed: a required check that is missing counts as pending and blocks, so a runner that crashes blocks the land rather than letting the change through.

5. Reading a failure

The wrapper preserves the child's output and never replaces a successful child exit with an invented result. A bridge-level refusal is TVO-CI-007 (bad input, nothing started) or TVO-CI-008 (the child would not start, exited non-zero, or timed out). A more specific TOVIO code from the CLI itself appears in the preserved output — read that one first.

Variations

  • No OIDC available? Export a passphrase-encrypted runner bundle and mount the bundle and passphrase as two separate secret files, passing their paths.
  • Spacelift? Same executable from a before_init hook: tovio-ci-bridge sync then tovio-ci-bridge health.
  • Mirroring to Git? The git-bridge operation always runs bidirectionally and never force-pushes or collapses the two ref namespaces.