A small demo repo for the Trunk PlatformCon workshop covering Merge Queue and Flaky Tests. It's a Next.js + TypeScript app with Vitest unit tests and Playwright e2e tests. Both suites emit JUnit XML for Trunk to ingest, and each suite contains one intentionally flaky test so the Flaky Tests dashboard has something real to detect and quarantine.
app/— Next.js App Router pages (/home,/cart).lib/money.ts—formatCurrency(cents, currency).lib/cart.ts—cartTotal(items)reducer +CartItemtype.
This repo manages its own Node/npm hermetically with Trunk, so you
don't need a matching Node installed — everyone (and CI) uses the exact version pinned in
.trunk/trunk.yaml (Node 22.16.0). It's wired through
direnv: on entering the repo, .envrc puts Trunk's node/npm/npx shims
on your PATH.
One-time setup:
# 1. Install the Trunk CLI
curl https://get.trunk.io -fsSL | bash
# 2. Install direnv and hook it into your shell (bash/zsh/fish):
# https://direnv.net/docs/hook.html
# 3. From the repo root, approve the .envrc:
direnv allowAfter that, node and npm resolve to the Trunk-managed versions automatically (the first run
downloads Node on demand). Verify with:
which node # → .trunk/tools/node
node --version # → v22.16.0Not using direnv? You can still get the same tools on
PATHmanually: runtrunk installonce (generates the shims in.trunk/tools/), then add.trunk/toolsto yourPATH. CI usesactions/setup-nodepinned to the same version.
npm install # node/npm are Trunk-managed once direnv is set up (above)
# Unit (Vitest) → writes test-results/unit-junit.xml
npm run test:unit
# E2E (Playwright) → writes test-results/e2e-junit.xml
npx playwright install chromium # first time only
npm run test:e2e # boots `next dev` automaticallyJUnit reports land in test-results/. The CI workflows clear that directory before each run so a
reused runner can't re-upload stale results (which would corrupt flake detection).
tests/unit/flaky.test.ts and tests/e2e/flaky.spec.ts each fail ~30% of the time via a random
roll. They are isolated and never affect real app behavior — they exist only to produce a flake
signal for the workshop. Playwright is configured with retries: 0 so Trunk sees the true
pass/fail signal.
scripts/open-prs.ts opens throwaway PRs against main to fill the Merge Queue. It uses the
gh CLI, which must be installed and authenticated (gh auth login, or
a GITHUB_TOKEN / GH_TOKEN env var).
# Open 6 PRs (leave them for a manual `/trunk merge` in the demo)
npm run open-prs -- --count 6
# Open 3 PRs and post `/trunk merge` on each so they enter the queue automatically
npm run open-prs -- --count 3 --queueEach PR branches off the latest main and makes a tiny safe change scoped to one target — a unique
frontend/notes-*.md or backend/notes-*.md file (alternating, see Graph mode below) — with a
realistic conventional-commit title. Branch names are timestamped so reruns don't collide.
The repo defines two merge-queue targets, frontend/ and backend/.
Each PR alternates which target it touches, and .github/workflows/impacted-targets.yml computes the
impacted target from the PR's changed files and uploads it to Trunk
(POST /v1/setImpactedTargets, via scripts/upload-impacted-targets.sh).
Impacted targets are uploaded on every PR, regardless of queue mode — the queue only uses them in graph mode. So you can:
- Queue ~5 PRs with the queue in linear mode → they merge one at a time.
- Flip the queue to graph mode in Trunk and re-run the same thing → PRs impacting disjoint
targets (
frontendvsbackend) merge in parallel lanes.
.github/workflows/unit-tests.yml/e2e-tests.yml— run on push + PR, execute the suites withcontinue-on-error: true(so quarantine can keep CI green), then upload to Trunk viatrunk-io/analytics-uploader@v1. The upload step is guarded so it no-ops until the Trunk secrets exist (the token is lifted into a jobenvvar and the step'sifchecks it), so CI is green before setup and active once the secrets are added..github/workflows/impacted-targets.yml— runs on PR; uploads the PR's impacted targets for graph mode (above). SameTRUNK_API_TOKENguard, so it's inert until secrets exist..github/workflows/generate-traffic.yml— scheduled + manual; runsopen-prs --queue. Gated on theTRAFFIC_ENABLEDrepo variable, so it's off by default.
See SETUP.md for the steps to wire up Trunk and reach the Final state.