GitHub Actions workflows
Seven workflows live under .github/workflows/. Most run on pull requests
(docs checks, security scans, the assistant CI, and the preview-environment
lifecycle), one runs on a schedule: (the cron trigger), and one runs on
pushes to main (production deploy).
At a glance
| File | Trigger | What it does | Required check? |
|---|---|---|---|
docs-checks.yml | pull_request (opened, sync, reopened, edited, labelled, unlabelled) | Runs the spec 003 path-overlap check + Jira-link check | Yes — docs-required |
security.yml | pull_request (opened, sync, reopened) | gitleaks secret-scan on the PR diff + per-app npm audit | No |
assistant-ci.yml | pull_request touching assistant/** | Lint, typecheck, unit tests, build for the assistant app | No |
pr-preview-create.yml | pull_request (labelled, sync) | Provisions a DO app + optional isolated DB when deploy-preview / deploy-preview-with-db is set | No — only runs when labelled |
pr-preview-cleanup.yml | pull_request (closed, unlabelled) | Tears down the DO app + drops the isolated DB | No |
do-deployment.yml | push to main | Production deploy to DigitalOcean | n/a — runs after merge |
crons.yml | schedule: (cron) + workflow_dispatch | Fires the IBA API cron jobs by curling /crons/run/<name> | No |
migrations-guard.yml | pull_request touching **/sql/migrations/** | Fails the PR if it edits, renames, or deletes an already-committed .sql migration | No |
db-backup.yml | schedule: nightly + workflow_dispatch | Dumps the production MySQL database to s3://…/db-backup/; the source of every local/preview restore | n/a — runs after merge |
docs-checks.yml
The seatbelt for the docs-update flow. On every PR event, it:
- Computes the diff between the PR branch and
main. - For each changed file, asks whether any
/business-logic/pageowns:it via frontmatter glob. - Checks whether any of those owning pages were also edited on the
branch, OR whether a
docs-update: not-neededsentinel is present in any commit message. - Reports the result as the
docs-requiredstatus check.
It also runs a jira-link check that warns (non-blocking) if the PR
title has no Jira ref. The warning is posted as a sticky comment, not a
blocking check.
Common failure mode — “docs-required: failed”. Open the workflow
log; it lists exactly which files changed and which /business-logic/
page should be updated. Either update that page, or add the sentinel.
pr-preview-create.yml
Runs only when a PR has the deploy-preview or deploy-preview-with-db
label. Concurrency-grouped per PR — a fresh push cancels the in-flight
deploy and re-runs from scratch.
Steps in order:
- Install
doctl— DigitalOcean’s CLI, used for everything else. - Sanitise the branch name to fit DO hostname rules: lowercase,
non-alphanumerics → hyphens, max 18 chars.
TUN-1234-suspend-logic→tun-1234-suspend-l. The DB name (if needed) replaces hyphens with underscores and appends_iba. - (With
-with-dbonly) Install MySQL client, fetch the runner’s public IP, add it to the production DB cluster’s firewall, wait 15s for propagation. - (With
-with-dbonly) Get connection details for the DO managed DB, restore the latest production S3 backup into an isolated DB named for the branch. - (With
-with-dbonly) Apply the branch’s migrations against the isolated DB with the real runner (npm run migrate) — a dry-run that fails the job (and the PR) if any migration errors, before it could reach a production Pre-Deploy. - Provision the DO app pointing at the right database — isolated
if
-with-db, sharedIBA-DEVotherwise. - Post a sticky comment on the PR with the live URL.
Common failure modes:
- Firewall propagation race — the workflow waits 15s after adding the runner IP. On rare congested days that isn’t enough; the MySQL connection step fails with “host not allowed”. Re-run the workflow.
- DB restore taking >5 minutes — the production backup is sometimes large. The job has generous timeouts but if it’s stuck, check the DO managed-database dashboard for restore progress.
- DO app spec rejected — usually because the team’s DO account ran out of app slots. Tear down a stale preview from a closed PR.
pr-preview-cleanup.yml
The cleanup partner. Fires when:
- A PR is closed (merged or otherwise), OR
- The
deploy-previewordeploy-preview-with-dblabel is removed.
It:
- Deletes the DO app named after the branch.
- Drops the isolated DB (if there was one).
- Removes the runner-IP firewall rule.
You should never need to run this manually. If it fails, the DO
dashboard has a one-click delete for the app and a SQL DROP DATABASE
gets the DB.
do-deployment.yml
Production deploy. Triggered by every push to main — i.e. every PR
merge. Pushes the api/, www/, and admin/ apps to their DigitalOcean
App Platform slots; image rebuilds happen there, not in CI.
The docs site (docs/) is not deployed by this workflow — that’s
handled separately by Cloudflare Pages, which auto-builds from the
GitHub repo. See the Cloudflare Pages integration below.
Common failure modes:
- Out-of-date secrets — the workflow uses Infisical secrets stored as GitHub Actions secrets. If those rotate, the workflow fails at the deploy step.
- Image build timeout — DO App Platform has a build timeout. Large npm installs occasionally exceed it; the fix is usually pruning devDependencies that ended up in production.
security.yml
Runs on every PR. Two jobs: secret-scan (gitleaks over the PR diff — blocks
the PR on a hit) and dependency-audit (a per-app npm audit matrix). Keeps
leaked credentials and known-vulnerable dependencies out of the codebase.
assistant-ci.yml
CI for the Assistant app, triggered only by PRs touching
assistant/**. Runs lint → typecheck → unit tests → build. Concurrency-grouped
so a new push cancels the in-flight run.
crons.yml
The scheduled trigger for the IBA API’s cron jobs. Runs on a
schedule: (one cron: line per job, UTC) and on workflow_dispatch for
manual runs. Each run curls GET /api/crons/run/<name> with the
CRONS_API_KEY repo secret in the token: header; the job itself executes
inside the API container. It replaces the legacy hand-maintained crontab on the
Dev-DB-Redis droplet.
Setup: needs the CRONS_API_KEY repo secret. Scheduled runs fire only from
the default branch; use Run workflow to test a single job on demand.
migrations-guard.yml
Enforces the forward-only rule for the migration runner:
a .sql file already committed under api/sql/migrations/ or
assistant/sql/migrations/ must never be edited, renamed, or deleted. The runner
records a SHA-256 checksum per applied file and aborts on mismatch, so
rewriting an applied migration silently breaks the migrate step in every
environment (the cause of the “Unknown column validation_attempt” incident).
Adding a new NNNN_*.sql is fine; touching an existing one fails the PR.
READMEs and templates in those dirs stay editable — only .sql files are guarded.
db-backup.yml
The nightly production database backup. A full mysqldump of the managed MySQL
cluster (routines, triggers, events, and the api_schema_migrations table) is
gzipped, timestamped, and uploaded to s3://…/db-backup/, with a 14-day
retention prune. This replaces the untracked droplet cron that previously
produced these dumps and silently went stale — freezing every local/preview
restore at an old schema. Because local dev and the preview databases
(above) both restore the latest object here, the job’s freshness is
load-bearing: a failure posts a Slack alert. Runs on a nightly
schedule: and on workflow_dispatch (use Run workflow to seed a fresh dump
on demand).
Setup: OIDC-fetched secrets from Infisical /github-ci
(DO_API_TOKEN, DO_DB_CLUSTER_ID, S3_*, SLACK_WEBHOOK_URL). The runner’s
IP is added to the DB firewall for the dump and removed afterward.
Cloudflare Pages (separate)
The docs site has its own status check labelled Cloudflare Pages
on every PR — Cloudflare’s GitHub integration. It runs the docs build
(npm run prebuild && npm run build) against the PR branch and
auto-deploys a preview URL. On merge to main, the same Cloudflare
build promotes to production at docs.tunnelflight.com.
This is not a GitHub Actions workflow — it’s configured entirely in the Cloudflare dashboard. The repo has no YAML controlling it.
See also
- Labels — what each label triggers.
- Deploy previews — the user-facing walkthrough of the preview lifecycle.