Skip to main content

Workflows

Compass uses GitHub Actions for continuous integration, Docker Hub for image distribution, and a VPS for staging.

WorkflowTriggerPurpose
Unit (test-unit.yml)Push / PR / merge_group to mainRuns static (lint, knip, type-check) and unit tests. PRs run only the packages they touched (packages/core, root package.json / bun.lock / tsconfig* still turn every leg on). Merge queue and main always run every leg.
E2E (test-e2e.yml)Push / PR / merge_group to mainPlaywright e2e in four shards behind one required e2e gate (PRs that touch only docs, or only packages/backend, packages/sync, packages/scripts, skip the shards; merge queue and main always run them)
CodeQLPush / PR to mainStatic security analysis
Performance budgetPush to main (web/core/lock/budget), nightly schedule, workflow_dispatch; PR only when .github/perf/** or the workflow file changesLighthouse budget (not a required merge check). Desktop script transfer is calibrated to 1,060 KB as of 2026-09-07 (main measured 1,034,338 bytes).
Error autofix (error-autofix.yml)posthog[bot] issue opened/reopened, hourly sweep, workflow_dispatchGoverned Routine: triage or fix PostHog error issues; sweep re-enters missed recurrences
Error autofix post-deploy (error-autofix-postdeploy.yml)Release on main completedNotifies Discord/GitHub of autofix release outcome
Agent review (agent-review.yml)workflow_dispatch only (parked). Kill switch AGENT_REVIEW_ENABLED still in the job if:. Restore pull_request types to turn it on.Independent read-only diff review posted as a PR comment (not a required check)
Agent loop (agent-loop.yml)workflow_dispatch / hourly cron / agent-automerge labeled or merged; Release on main smokes onlyGoverned Routine: next milestone WP → Cursor API → merge-guard → merge queue → staging smoke
Release on mainPush to mainAuto-increments patch version, publishes Docker images, then deploys staging
Publish Docker imagesReusable workflow / manual dispatch / manual v*.*.* tag pushBuilds and pushes Docker images only
Deploy stagingReusable workflow / manual dispatchPulls published images on staging, restarts the stack, then runs deploy health checks
Deploy productionManual dispatchDeploys a release tag to production, then runs cloud deploy health checks
Deploy health checkReusable workflowValidates the deployed staging stack and alerts Discord on failure
Sync docs to compass-docsPush to main touching docs/**Mirrors this docs/ directory to docs.compasscalendar.com
Live provider smoke (live-provider-smoke.yml)Nightly schedule / workflow_dispatch only (never on PRs)Live adapter contract against compass-smoke test calendars; see live-provider-smoke.md

Error autofix is a governed Routine. Contract, drills, and recovery packet: error-autofix-routine.md. Kill switch (ERROR_AUTOFIX_ENABLED) and mode (AUTOFIX_MODE) stay as repo variables.

Agent loop is a governed Routine. Contract, kill switch, Cursor API launch, and staging smoke: agent-loop-routine.md. Kill switch (AGENT_LOOP_ENABLED) stays a repo variable (default off). Ordered queue: AGENT_LOOP_MILESTONES (empty idles). Two launch paths: hourly/workflow_dispatch kick, and launch-next on automerge merge. post-deploy smokes only.


Unit workflow

Source: .github/workflows/test-unit.yml

static (lint, knip, and type-check as separate steps) and each unit-leg carry a 10-minute job timeout. A rollup unit job requires every leg, so the ruleset needs only static, unit, and e2e; legs can change without touching branch protection. Unit test-run steps are separately capped at 5 minutes each, leaving headroom for checkout, setup-bun, cache, and install. These bound a hung job to a fixed, small window instead of GitHub's 360-minute default. GitHub Actions has no native way to give several built-in action steps (checkout, setup-bun, cache) one shared budget without reimplementing them by hand, so the job-level timeout is the practical equivalent.

Jobs that only run Bun do not install Node. unit (web, 1) and unit (web, 2) each cover half the web suite. CI uses WEB_TEST_SHARDS=6 with WEB_TEST_SHARD_INDEX=1,2,3 and 4,5,6 so each leg runs three sequential processes (a 111-file process on four shards exceeded the 5 GB RSS guard). Local bun test:web still runs every shard sequentially. WEB_TEST_SHARDS=2 WEB_TEST_SHARD_INDEX=2 bun test:web still runs only the second half.

On pull requests, detect-code-changes.sh also emits per-package outputs (core, web, backend, sync, scripts). A backend-only PR still runs static and the backend leg, and skips the web runners. Changes under packages/core, or to root package.json, bun.lock, or tsconfig*.json, turn every leg on. merge_group and push to main always run every leg.

Known flakiness: a job can occasionally hang for several minutes on runner/network flakiness unrelated to the diff, while the identical job on the identical commit in a parallel run passes quickly. If a check looks stuck well past its normal duration, cancel and rerun just that job rather than investigating the diff first:

gh run cancel <run-id>
gh run rerun <run-id> --failed

Release Flow

Every PR merge to main triggers a fully automated chain:

PR merged to main
└─► release-on-main.yml
├─► tag-release — reads latest tag, pushes v1.2.X+1
├─► publish-docker-images — builds and pushes Docker Hub images
└─► deploy-staging — SSHes into VPS, runs ./compass update
├─► staging-cloud deploy health check
└─► staging-selfhosted deploy health check

The automatic path calls reusable workflows directly. It uses GITHUB_TOKEN to push the git tag, then passes that tag to the publish and deploy workflows. It does not rely on the workflow-created tag push to trigger another workflow. The release workflow remains blocked until both staging deploy health checks pass. If a health check fails, the reusable workflow sends one Discord alert for that environment with the release tag, run URL, failed check names, and redacted excerpts.

Monthly minor/major releases remain manual: a maintainer pushes a tag like v1.3.0 or v2.0.0, which skips the bump step and runs Publish Docker images. Staging deploys for manual tags are explicit: run Deploy staging with the existing tag after the images are published.

Removing a test tag

git push origin --delete v1.2.3
git tag -d v1.2.3

Publish Docker Images

Source: .github/workflows/publish-docker-images.yml

How it works

  1. A semver tag is provided by release-on-main.yml, by manual workflow dispatch, or by a manually pushed tag matching v[0-9]+.[0-9]+.[0-9]+.
  2. The workflow strips the v prefix and derives two tag aliases:
    • 1.2.3 — exact patch version
    • 1.2 — floating minor alias
  3. It builds and pushes three images to our Docker Hub:
    • switchbacktech/compass-backend
    • switchbacktech/compass-mongo
    • switchbacktech/compass-web
  4. Each image gets all three tags: 1.2.3, 1.2, and latest.

Each docker/build-push-action step exports a per-image GitHub Actions cache (cache-from/cache-to type=gha,mode=max). Bun images copy package.json, bun.lock, bunfig.toml, patches/, and workspace package.json files and run bun install --frozen-lockfile before COPY . .. Every oven/bun: tag must match the bun-version in .github/workflows/test-unit.yml (packages/scripts/src/testing/check-agent-constraints.ts).

Tag pattern rules

Only clean semver tags trigger this workflow from a tag push. Tags with suffixes (e.g. v1.2.3-test) do not match and are safe to push for local testing without publishing images.


Staging Deploy

Source: .github/workflows/deploy-staging.yml

The deploy workflow SSHes into the staging VPS and runs ./compass update, which pulls the Docker Hub image tag configured by the staging compass.yaml file and restarts the stack. The workflow accepts a release tag input so the Actions logs show which release triggered or motivated the deploy.

After each staging environment deploy, deploy-staging.yml calls deploy-health-check.yml. The health check SSHes into the same host, verifies ~/compass, ./compass status, bounded Docker Compose logs, service health, expected Docker image tags, frontend HTML, backend /health, and data-service reachability. The staging-cloud profile checks the external MongoDB URI. The staging-selfhosted profile additionally checks MongoDB, SuperTokens, and the expected Docker volumes inside the self-hosted stack.

Manual staging redeploys do not rebuild images. Run Deploy staging with an existing tag after confirming the desired image tags already exist on Docker Hub.

Staging booking-web only

Source: .github/workflows/deploy-staging-booking-web.yml

Use this when a change touches only apps/booking-web (or its static assets) and you want staging to pick up a new guest container without rebuilding calendar-web or restarting the full stack.

  1. In GitHub Actions, run Deploy staging booking-web.
  2. Set tag to a semver release tag that contains the booking-web change (usually the latest v*.*.* on main after merge).
  3. The workflow builds switchbacktech/compass-booking-web:staging-cloud-<version>, patches bookingWeb.image in the existing ~/compass/compass.yaml on the staging VPS, refreshes self-host compose files from COMPOSE_GIT_REF, and runs ./compass update-booking-web with the booking and sync profiles.

The full Deploy staging workflow and Release on main path are unchanged. Staging Caddy already routes public /meet/* to booking-web; after deploy, verify the container with curl http://127.0.0.1:9081/ on the host (or the port in bookingWeb.port) and curl -I https://staging.compasscalendar.com/meet/.

Optional GitHub Environment variable on staging-cloud: BOOKING_WEB_PORT (default 9081 when unset).

Required secrets and variables

Secrets and variables are split between repository level (shared across workflows) and the Staging GitHub Environment (scoped to the deploy job).

Repository-level — GitHub → Settings → Secrets and variables → Actions:

NameValue
DOCKERHUB_USERNAMEDocker Hub username for the switchbacktech org
DOCKERHUB_TOKENDocker Hub personal access token (Read & Write)
DISCORD_DEPLOY_WEBHOOK_URLDiscord webhook for deploy health check failure alerts

Staging environments (staging-cloud, staging-selfhosted) — GitHub → Settings → Environments:

SecretValue
SSH_PRIVATE_KEYPrivate key from the deploy keypair
COMPASS_SYNC_TOKENToken for compass sync
GOOGLE_CLIENT_SECRETOAuth client secret
MICROSOFT_CLIENT_SECRETEntra app client secret
APPLE_SIGNIN_PRIVATE_KEYSign in with Apple .p8 private key
SYNC_CREDENTIAL_ENCRYPTION_KEY32-byte base64 key for provider credentials at rest (required; the deploy fails without it, generate with openssl rand -base64 32)
MONGO_PASSWORDMongoDB compass user password
MONGO_REPLICA_SET_KEYMongoDB replica set key (staging-selfhosted)
MONGO_URIBackend MongoDB URI
SUPERTOKENS_KEYSuperTokens API key
SUPERTOKENS_POSTGRES_PASSWORDSuperTokens PostgreSQL password (staging-selfhosted)
SUPERTOKENS_URISuperTokens connection URI
SYNC_INTERNAL_AUTH_TOKENShared secret between backend and Sync (required)
SYNC_MONGO_URIIsolated Sync MongoDB URI (staging-cloud / production; selfhosted derives this from MONGO_PASSWORD)
VariableValue
SSH_HOSTVPS IP address or hostname
SSH_USERLinux user on the VPS that owns ~/compass
BACKEND_API_URLStaging backend API URL
FRONTEND_URLStaging frontend URL
GOOGLE_CLIENT_IDOAuth client ID
MICROSOFT_CLIENT_IDEntra app client ID
APPLE_SIGNIN_SERVICES_IDSign in with Apple Services ID
APPLE_SIGNIN_TEAM_IDApple Developer team ID
APPLE_SIGNIN_KEY_IDSign in with Apple key ID
COMPOSE_PROFILESCompose profiles (selfhosted on staging-selfhosted; sync is always appended by the deploy)

Production Deploy

Source: .github/workflows/deploy-production.yml

Production deploys are manual-only. Run Deploy production from GitHub Actions with an existing release tag, such as v1.2.3; it is not called by release-on-main.yml and does not run automatically after PR merges.

The workflow deploys to the GitHub production environment through _deploy-environment.yml, builds an environment-specific web image tagged switchbacktech/compass-web:production-<version>, then runs deploy-health-check.yml with the cloud profile. Production is expected to use external MongoDB and SuperTokens Cloud rather than self-hosted data services.

For cloud web image builds, _deploy-environment.yml uses .github/docker/Dockerfile.web so frontend-only cloud config, such as PostHog, is baked into the bundle without adding cloud-only settings to the self-host Dockerfile.