Skip to content
Source

Your first workflow

Your repository has tests, and nothing runs them when you push. You want each pull request into main to show a green check before anyone merges it.

A workflow is a TypeScript file in .source/workflows/. The forge runs its jobs for the pushes and pull requests that it names.

You need:

  • Write access to the repository on TanStack Source. Step 5 needs admin access.
  • The src CLI. Install the npm package @tanstack-source/cli, which has the src binary.
  • A pnpm repository with vitest tests. For other tools, change the commands in step 1.

1. Write the workflow

Create .source/workflows/ci.ts:

import { createHash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
import { cache, defineWorkflow, pullRequest, push, report, setup } from '@tanstack-source/ci'

export default defineWorkflow('ci', {
  on: [pullRequest({ branches: ['main'] }), push({ branches: ['main'] })],
})
  .job('test', {
    egress: ['registry.npmjs.org'],
    steps: [
      setup.pnpm(),
      {
        name: 'Cache the pnpm store',
        do: async (ctx) => {
          const lockfile = await readFile(join(ctx.workspace, 'pnpm-lock.yaml'))
          const hash = createHash('sha256').update(lockfile).digest('hex').slice(0, 16)
          await cache({ key: `pnpm-${hash}`, paths: ['~/.local/share/pnpm/store'], restoreKeys: ['pnpm-'] }).do(ctx)
        },
      },
      { name: 'Install', run: 'pnpm install --frozen-lockfile' },
      {
        name: 'Test',
        do: async (ctx) => {
          const { code } = await ctx.sh.nothrow`pnpm vitest run --reporter=default --reporter=junit --outputFile.junit=junit.xml`
          await report({ file: 'junit.xml', format: 'junit' }).do(ctx)
          if (code !== 0) throw new Error(`vitest exited with code ${code}`)
        },
      },
    ],
  })
  .job('gate', {
    needs: ['test'],
    when: 'always',
    steps: [
      {
        name: 'Check the jobs',
        do: (ctx) => {
          if (ctx.needs.test.result !== 'success') throw new Error(`test ended with ${ctx.needs.test.result}`)
        },
      },
    ],
  })

What each part does:

  • on: a run starts for each pull request into main, and for each push to main.
  • egress: the job reaches only these hosts, on port 443. pnpm install needs registry.npmjs.org.
  • setup.pnpm() and the cache step: pnpm comes from packageManager in package.json. The pnpm store comes from the cache, and the job saves it when it passes. The key is a hash of the lockfile, because the forge writes an entry only once.
  • The test step: report() adds each failed test, with its file and line, to the findings of the run. It runs before the step fails, so the findings of a failed test run reach the forge.
  • gate: it runs after test, also when test failed (when: 'always'), and it fails when test did not pass. So one required check on gate covers the whole workflow.

For types in your editor, add the package as a dev dependency: pnpm add -D @tanstack-source/ci. The forge does not need it, because its runner brings its own copy.

2. Run it on your machine

src ci plan
src ci run ci test

src ci plan loads each workflow file and shows its jobs, or the errors with the file to fix:

ci (.source/workflows/ci.ts)
  on: pull request (branches: main), push (branches: main)
  jobs:
    test
    gate: needs test; runs always
1 workflow, 2 jobs.

src ci run ci test runs the job on your machine. When it prints Job test passed., go to the next step. If it fails, Run CI jobs on your machine shows how to fix it there.

3. Push it to main

Push the workflow to the default branch before you open a pull request. A pull request run takes the secrets, permissions and egress of each job from the base branch. So a job that is new in a pull request runs with none of them, and pnpm install cannot reach the registry.

git add .source/workflows/ci.ts
git commit -m "ci: add the ci workflow"
git push origin main

If a branch rule refuses the push, merge the workflow through a pull request. The run of that pull request fails, because its jobs are new there.

4. See the run

main is the default branch, so the push starts a run of ci at the trust level protected.

  • In the browser: open the CI tab of the repository (/<owner>/<repo>/ci). Open the run to read the log, the findings and the summary of each job. The log shows each step by its name, like Install, with its result and how long it took.
  • In a terminal: set the forge variables, then list the runs.
export TSS_FORGE_URL=https://forge.example/api/v1
export TSS_TOKEN=<your personal access token>
export TSS_REPO=acme/app
src ci runs --branch main
ID                                    STATUS   WORKFLOW  EVENT  REF   COMMIT
7c1e2d3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f  success  ci        push   main  4e5f6a7

If a job failed, src ci view <run> --log-failed prints the logs of the failed jobs. To let an agent fix a failed run for you, turn on CI self-heal.

5. Make the check required

A branch rule blocks a merge into main until ci/gate passes on the head commit of the pull request. A required check is <workflow>/<job>.

src ci rule set main --require-pull --check ci/gate
Set the branch rule main: requires a pull request; required checks ci/gate.

Branch rules has the settings page and every option of a rule.

6. Open a pull request

  1. Create a branch, change a file, and push the branch.
  2. Open a pull request into main.
  3. Wait until the ci run of the pull request ends.

The checks of the pull request say All checks passed, and list the jobs of the ci workflow. Below them, above the merge button, Required checks shows ci/gate as Passed. The CLI shows the same result:

src ci checks 1
Pull request #1 into main, head 9f8e7d6
NAME     STATE   REASON
ci/gate  passed  Passed on 9f8e7d6
May merge: yes

Your pull request has a green check, and the merge button is available. The list of pull requests shows the same green check next to its title.