GitHub Actions workflows, branch protection, verification jobs, and CLI release flow.
CentraKit uses a small set of focused GitHub Actions workflows.
.github/workflows/ci.yml runs on pull requests, pushes to develop and main, and manual dispatch. A PR title job runs commitlint on the pull request title. It calls verify.yml (affected workspaces only on pull requests), then builds and checks payload budgets..github/workflows/verify.yml is the reusable gate with four jobs:
Static checks installs once and runs lint, format, Knip, Turbo boundaries, pnpm audit:high, OpenAPI drift and the i18n catalog test as one parallel step group, so each check keeps its own log.Typecheck and Test each get their own runner, because they are CPU-bound on the 2 vCPU standard runner. Test uploads the Turbo run summary, so per-suite timings show when a suite needs sharding (above about 5 minutes).Integration (1/3) to (3/3) shard the cross-workspace @workspace/integration-tests suite with Vitest --shard, because it took about 7 minutes on one runner. Test runs every other workspace.Live database suites starts the local stack on the native runtime, restores the service archives that database.yml caches, loads the Stripe demo fixture, and runs tests/integration/supabase/ with REQUIRE_LOCAL_SUPABASE=true and RUN_LIVE_RLS_TESTS=true. The shards have no database, so the live RLS and repository suites skip there. In this job a missing stack throws from readLocalStatus instead of skipping. It runs on every CI run, not only on affected workspaces, because a repository or policy change anywhere can break these suites.Build job in ci.yml builds Next.js apps one at a time (--concurrency=1), because concurrent Turbopack builds exhaust the 7 GB runner. The Turbopack build cache is off on GitHub Actions (ADR 0016), because a cold SaaS build with it runs the runner out of memory. A background step logs memory and disk usage every 15 seconds during the build..github/workflows/database.yml runs when supabase/** or the Supabase CLI pin changes. It starts the local stack on the Supabase CLI native runtime, without Docker, and caches the downloaded service archives in ~/.supabase/cache/stack per CLI version (saved only on develop). It then runs the pgTAP suite with pnpm supabase:test, checks the declarative schemas against the migrations, and runs the read-only checks (SQL modules, generated types, supabase db lint) in parallel..github/workflows/security.yml runs zizmor and actionlint on workflow changes, and a non-blocking pnpm audit signatures when the lockfile changes. Run both linters locally from their release binaries before pushing a workflow change; .github/actionlint.yaml ignores the syntax actionlint 1.7.12 cannot parse yet ($/, parallel, cache-mode, concurrency.queue)..github/workflows/release.yml runs as the last job of CI on pushes to main, after Verify and Build pass, so it does not check, build or test the same commit again. It uses Changesets to open or update the chore: version packages PR. Merging that PR tags the release, creates the GitHub Releases, and merges main back into develop. Release runs queue (queue: max) instead of cancelling each other, and release jobs use cache-mode: none, so a cache entry never reaches a release..github/workflows/release-cli.yml runs for published CLI releases and manual retries to package the tarball and attach it to the GitHub release..github/workflows/react-doctor.yml runs React Doctor on pull requests and on pushes to develop and main. On a pull request it scans the changed files, posts a summary comment with the new findings, and fails on new warnings or errors (blocking: warning). Run the same scan locally with pnpm doctor:react; pnpm doctor also runs the PermDock and better-supabase doctors..github/dependabot.yml opens one grouped weekly PR each for npm and GitHub Actions, after a two-day cooldown..github/actions/setup is the shared composite action: Turborepo Remote Cache over OIDC, then pnpm/setup for pnpm, the Node 24 runtime, the store cache, and a frozen install. Only the Build job saves the pnpm store, and only on develop; every other job passes save-cache: "false" and restores that entry.$/ self-repository syntax. GitHub-owned actions use major tags, and third-party actions are pinned to a commit SHA with a # vX.Y.Z comment (.github/zizmor.yml enforces this). Every checkout sets persist-credentials: false, except the release job, which pushes.TURBO_TEAM: Vercel team slug for Turborepo remote cache. Use scaledock.NEXT_PUBLIC_SITE_URL: optional CI override for Next.js env validation. Defaults to https://localhost.NEXT_PUBLIC_SUPABASE_URL: optional CI placeholder override for Next.js env validation.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: optional CI placeholder override for Next.js env validation.SENTRY_AUTH_TOKEN: optional. CI does not upload source maps or create Sentry releases; Vercel production builds do.CI uses vercel/setup-turborepo-remote-cache-action to exchange a GitHub OIDC token for a short-lived Turborepo token. That token is team-scoped and can write cache artifacts. Workflows need id-token: write.
Create the matching Vercel policy once. In the team Build and Deployment settings, open OIDC Policies for CLI Access and add a Turborepo CLI policy for ScaleDockHQ/CentraKit. Workflows do not set TURBO_TOKEN themselves, so the token from the OIDC exchange is the only one Turborepo sees. Without the policy the setup step logs Failed to exchange OIDC token and every task runs without remote cache.
gh variable set TURBO_TEAM --body "scaledock" --repo ScaleDockHQ/CentraKit
gh variable set NEXT_PUBLIC_SITE_URL --body "https://localhost" --repo ScaleDockHQ/CentraKitVercel deployments do not need TURBO_TOKEN: when builds run turbo run build (configured per service in root vercel.json), Vercel Remote Cache is authenticated automatically. Root turbo.json sets remoteCache.teamSlug / teamId for the linked centrakit project.
From the repo root (one-time per machine):
pnpm exec turbo login
pnpm exec turbo linkThis writes gitignored .turbo/config.json. After linking, local turbo run build shares artifacts with Vercel and GitHub Actions.
develop is the default branch. Commit and push to it directly; feature branches are the exception and get no Vercel preview.main moves only through the pull request from develop to main, merged whenever a release is wanted. Never commit to main.develop as preview (app.centrakit.dev, plus the brand previews mechup.centrakit.dev, stabledock.centrakit.dev, caresly.centrakit.dev, propdock.centrakit.dev, marryduck.centrakit.dev, signaldock.centrakit.dev, spacelio.centrakit.dev and legaldock.centrakit.dev) and main as production (app.centrakit.com, app.mechup.com, app.stabledock.app and app.caresly.app). The API and docs are the /api, /mcp and /docs paths on those hosts. git.deploymentEnabled in root vercel.json denies every other branch.main maps to the production project and develop to the persistent Supabase branch. The Supabase GitHub integration applies migrations and config.toml on push; do not supabase db push from Actions.develop. The Changesets version PR targets main from changeset-release/main; vercel.json skips Vercel deployments for changeset-release/* branches.The repository plan is private without rulesets, so there is no enforced branch protection. The checks below are the ones to require on the release PR once protection is available:
PR titleVerify / Static checksVerify / TypecheckVerify / TestVerify / Integration (1/3), (2/3) and (3/3)BuildReact DoctorThe Build job also captures SaaS and CLI payload artifacts and compares deterministic SaaS build metrics with the latest successful artifact on the base branch. Regressions emit warnings by default. Set the PAYLOAD_COMPARE_MODE repository variable to enforce, or choose enforce for a manual workflow run, to make budget regressions fail the job.
The Build job records shipped-cost artifacts after turbo run build, so the payload steps reuse that build instead of compiling the SaaS app again:
pnpm --filter saas baseline:payload reads the next build output, writes .next/diagnostics/payload-artifact.json, and uploads static CSS/chunks, manifests, and .nft.json traces.pnpm --filter saas analyze:payload runs next experimental-analyze --output, a second full compile of about eight minutes on CI. It runs only on a manual workflow run, and its output lands in the same saas-payload-baseline artifact.pnpm --filter centrakit baseline:payload packs a baseline tarball and prints dist/cli.js / tarball byte sizes.Locally:
pnpm exec turbo run baseline:payload --filter=saas --filter=./apps/cliThe versioned SaaS artifact records, per audited route, uncompressed production JS, route-only JS after removing chunks shared by every audited route, obtainable prerendered Flight/RSC bytes, server entry bytes, and the unique files/bytes referenced by its NFT trace. Audited routes are Dashboard, Planning, Tasks/all, Products, Admin subscriptions, Audit log, Agenda detail, and Product detail.
CI selects only audited routes affected by changed source paths. A shared App Shell change selects all audited routes. Shared production JS is compared once with a zero-growth budget, so the same shared regression is not repeated for every route. Route budgets allow the larger of the absolute or percentage allowance: 25 KiB/5% route JS, 10 KiB/5% Flight and server entry, and 100 KiB/2% server trace. Missing or schema-incompatible base artifacts warn and skip comparison, including in enforce mode. Runtime timings are intentionally excluded because CI compares build artifacts only.
develop and main run the same check set. Payload baselines compare against the latest successful run on the PR base branch (or the pushed branch).parallel steps.pnpm openapi:generate and fails if committed API artifacts drift.Every user-visible change gets a changeset (pnpm changeset). The apps share one fixed version (.changeset/config.json fixed group with privatePackages.version and tag), so the product has a single release number.
pnpm version-packages runs scripts/root-changelog.ts, which reads the pending changesets, runs changeset version (per-app CHANGELOG.md files), and prepends a dated section with every changeset summary to the root CHANGELOG.md. It then formats the changelogs. The docs site renders the root changelog.
Release automation is split into two workflows:
release.yml opens or updates the chore: version packages PR from the pending changesets.centrakit@<version> and publishes the GitHub release, and release-cli.yml packages the tarball and uploads it to the release.release-cli.yml also keeps workflow_dispatch for retrying an asset upload against an existing release tag.
Last updated on