CI/CD Setup
This guide walks through setting up the GitHub Actions CI/CD pipeline for automated Cloudflare Workers deployment.
Prerequisites
Section titled “Prerequisites”- Cloudflare account with Workers enabled
- GitHub repository with Actions enabled
- SOPS installed locally (
nix profile install nixpkgs#sops) - Age key pair for encryption
Step 1: Create and Encrypt Secrets
Section titled “Step 1: Create and Encrypt Secrets”1.1 Create Cloudflare API Token
Section titled “1.1 Create Cloudflare API Token”- Visit https://dash.cloudflare.com/profile/api-tokens
- Click “Create Token”
- Use “Edit Cloudflare Workers” template or create custom token with:
- Account.Workers Scripts (Edit)
- Account.Workers Routes (Edit)
- Copy the generated token
1.2 Get Cloudflare Account ID
Section titled “1.2 Get Cloudflare Account ID”- Visit https://dash.cloudflare.com/
- Select your account
- Go to Workers & Pages
- Find Account ID in the right sidebar
1.3 Get Other Service Tokens
Section titled “1.3 Get Other Service Tokens”Optional but recommended for full CI functionality:
- CACHIX_AUTH_TOKEN: Get from https://app.cachix.org/cache/YOUR_CACHE/settings
- CACHIX_CACHE_NAME: Your Cachix cache name
1.4 Create Unencrypted Secrets File
Section titled “1.4 Create Unencrypted Secrets File”Create secrets/shared.yaml with your secrets:
CLOUDFLARE_ACCOUNT_ID: your-actual-account-idCLOUDFLARE_API_TOKEN: your-actual-api-tokenCACHIX_AUTH_TOKEN: your-actual-cachix-tokenCACHIX_CACHE_NAME: your-cache-nameCI_AGE_KEY: age-secret-key-1... # CI age private key from .sops.yamlThe CI_AGE_KEY should be the private key corresponding to the public key:
age1m9m8h5vqr7dqlmvnzcwshmm4uk8umcllazum6eaulkdp3qc88ugs22j3p8
1.5 Encrypt the Secrets File
Section titled “1.5 Encrypt the Secrets File”# Verify you have the correct age keys configuredcat .sops.yaml
# Encrypt the file in placesops --encrypt --in-place secrets/shared.yaml
# Verify encryption succeededhead secrets/shared.yaml# Should show encrypted content starting with ENC[...]1.6 Commit Encrypted Secrets
Section titled “1.6 Commit Encrypted Secrets”git add secrets/shared.yamlgit commit -m "build: add encrypted secrets for CI/CD"git pushStep 2: Configure GitHub Repository
Section titled “Step 2: Configure GitHub Repository”2.1 Upload SOPS Age Key to GitHub Secrets
Section titled “2.1 Upload SOPS Age Key to GitHub Secrets”The CI needs the private age key to decrypt secrets/shared.yaml:
# Extract the CI_AGE_KEY from the encrypted filesops --decrypt --extract '["CI_AGE_KEY"]' secrets/shared.yaml | gh secret set SOPS_AGE_KEYOr manually:
- Decrypt the file:
sops secrets/shared.yaml - Copy the
CI_AGE_KEYvalue - Go to https://github.com/YOUR_USERNAME/YOUR_REPO/settings/secrets/actions
- Click “New repository secret”
- Name:
SOPS_AGE_KEY - Value: Paste the age private key
- Click “Add secret”
2.2 Set GitHub Variables (Optional)
Section titled “2.2 Set GitHub Variables (Optional)”If using Cachix, set these as repository variables (not secrets):
- Go to https://github.com/YOUR_USERNAME/YOUR_REPO/settings/variables/actions
- Add variable
CACHIX_CACHE_NAMEwith your cache name
Alternatively, the workflow will read from the encrypted secrets/shared.yaml.
2.3 Configure Fast-forward Merge Workflow
Section titled “2.3 Configure Fast-forward Merge Workflow”The repository enforces fast-forward-only merges to maintain linear history. Two workflows handle this:
pr-check.yaml: Validates that PRs can be fast-forward merged (runs automatically)pr-merge.yaml: Performs the fast-forward merge when/fast-forwardis commented on a PR
To enable the /fast-forward command functionality:
-
Create a fine-grained Personal Access Token (PAT):
- Go to https://github.com/settings/personal-access-tokens/new
- Token name:
Fast-forward merge token - Expiration: Set according to your security policy (90 days recommended)
- Repository access: Select only this repository
- Repository permissions:
- Contents: Read and write (required for merging)
- Issues: Read and write (required for commenting)
- Pull requests: Read and write (required for PR updates)
- Click “Generate token” and copy the value immediately
-
Add the PAT as a repository secret:
Terminal window gh secret set FAST_FORWARD_PAT# Paste the token when promptedOr manually:
- Go to https://github.com/YOUR_USERNAME/YOUR_REPO/settings/secrets/actions
- Click “New repository secret”
- Name:
FAST_FORWARD_PAT - Value: Paste the PAT
- Click “Add secret”
-
Set the GitHub actor as a repository variable:
Terminal window gh variable set FAST_FORWARD_ACTOR -b"YOUR_GITHUB_USERNAME"Or manually:
- Go to https://github.com/YOUR_USERNAME/YOUR_REPO/settings/variables/actions
- Click “New repository variable”
- Name:
FAST_FORWARD_ACTOR - Value: Your GitHub username
- Click “Add variable”
-
Usage:
- PRs will automatically be checked for fast-forward compatibility
- If checks fail, rebase your branch:
git rebase main - When ready to merge, comment
/fast-forwardon the PR - The workflow will automatically perform the fast-forward merge
Token rotation:
Fine-grained PATs expire and must be rotated periodically.
The token is stored in secrets/shared.yaml as FAST_FORWARD_PAT.
To rotate: update with just edit-secrets, upload with just ghsecrets, then revoke the old token.
2.4 Configure Mergify (Optional but Recommended)
Section titled “2.4 Configure Mergify (Optional but Recommended)”Mergify provides automated merge queue functionality with fast-forward-only enforcement.
The repository includes a .github/mergify.yml configuration that:
- Enforces fast-forward merges exclusively
- Automatically queues approved PRs
- Handles skipped CI checks from duplicate detection
- Validates fast-forward compatibility before merging
To enable Mergify:
-
Install the Mergify GitHub App:
- Visit https://github.com/apps/mergify
- Click “Install”
- Select your repository
- Grant required permissions
-
Verify configuration:
Terminal window # The .github/mergify.yml file is already configured# Check the configuration at:cat .github/mergify.yml -
Configure the update bot account:
- The configuration uses
update_bot_account: cameronraysmith - This allows Mergify to push rebased commits on your behalf
- Ensure Mergify has write access to the repository
- The configuration uses
-
Usage:
- When a PR is approved and all checks pass, Mergify automatically adds it to the merge queue
- The queue uses
merge_method: fast-forwardandupdate_method: rebase - PRs are merged only if they can be fast-forwarded
- Conflicting PRs are automatically rebased by the bot account
For more details, see the Mergify documentation.
2.5 Configure Production Environment
Section titled “2.5 Configure Production Environment”- Go to https://github.com/YOUR_USERNAME/YOUR_REPO/settings/environments
- Click “New environment”
- Name:
production - Add protection rules as desired (e.g., required reviewers)
- Save
Step 3: Test the Workflow
Section titled “Step 3: Test the Workflow”3.1 Manual Test with workflow_dispatch
Section titled “3.1 Manual Test with workflow_dispatch”Test the workflow manually before enabling automatic deployment:
# Trigger workflow with deployment disabled (safe test)gh workflow run ci.yaml
# Or with deployment enabledgh workflow run ci.yaml -f deploy_enabled=true
# Force all jobs to run (ignore caching)gh workflow run ci.yaml -f force_run=true3.2 Monitor Workflow Execution
Section titled “3.2 Monitor Workflow Execution”# Watch the workflow rungh run watch
# Or view in browsergh run view --web3.3 Verify Each Job
Section titled “3.3 Verify Each Job”The workflow executes these jobs with intelligent per-job caching (jobs skip if already succeeded for this commit). See Caching Architecture for details on the content-addressed caching mechanism.
Core jobs (always run on PR/push):
- secrets-scan — Gitleaks secret scanning (security critical, no caching)
- set-variables — Configure workflow variables and discover packages
Preview jobs (PR only, fast feedback):
- preview-release-version — Show what version would be released (per package matrix)
- preview-docs-deploy — Deploy docs to branch-specific preview URL
Validation jobs (run based on file changes):
- bootstrap-verification — Validate Makefile bootstrap workflow
- secrets-workflow — Test sops-nix mechanics with ephemeral keys
- flake-validation — Validate flake structure and justfile recipes
Build jobs (run based on file changes, with matrix):
- cache-overlay-packages — Pre-cache resource-intensive overlay packages (x86_64-linux, aarch64-linux)
- nix — Build flake outputs by category (packages, checks-devshells, home, nixos) per system
- typescript — Test TypeScript packages (per package matrix)
Production jobs (main/beta only):
- production-release-packages — Release packages via semantic-release
- production-docs-deploy — Deploy documentation to production
Jobs use paths-ignore filtering to skip on markdown-only changes.
Each job uses content-addressed caching to skip if source files haven’t changed since last success.
3.4 Reusable Workflows
Section titled “3.4 Reusable Workflows”The CI system includes several reusable workflows that can be called from other workflows or used as building blocks for custom pipelines.
package-test.yaml — TypeScript package testing workflow.
Runs Vitest tests for all packages in the packages/ directory.
Called by ci.yaml via the typescript job.
Can be called independently for focused package testing.
package-release.yaml — Semantic release workflow for package publishing.
Integrates with semantic-release to version, tag, and publish packages based on conventional commits.
Called by ci.yaml via the production-release-packages job on main/beta branches.
Requires NPM_TOKEN or similar registry credentials.
deploy-docs.yaml — Cloudflare Workers deployment workflow.
Deploys documentation site to Cloudflare Workers using Wrangler.
Called by ci.yaml for both preview deployments (PR branches) and production (main branch).
Supports branch-specific URLs for preview deployments.
All reusable workflows accept workflow_call trigger and expose inputs for customization.
See individual workflow files in .github/workflows/ for input schemas and usage examples.
3.5 Check Deployment
Section titled “3.5 Check Deployment”If deployment succeeded, verify at:
- Cloudflare Dashboard: https://dash.cloudflare.com/
- Your Workers URL (shown in workflow deployment step)
Step 4: Enable Automatic Deployment
Section titled “Step 4: Enable Automatic Deployment”Once manual testing succeeds, automatic deployment on push to main is already configured.
Push to main branch:
git checkout maingit pull# Make changes...git add .git commit -m "your changes"git pushThe workflow will automatically:
- Run all CI checks
- Build the site
- Deploy to Cloudflare Workers
Workflow Triggers
Section titled “Workflow Triggers”The CI/CD workflow runs on:
-
Manual dispatch (
workflow_dispatch) — supports interactive control via GitHub UI orgh workflow run ci.yamljob: Run a specific job only (e.g.,flake-validation,nix)debug_enabled: Enable tmate debugging session for troubleshootingdeploy_enabled: Force deployment even on non-main branch (use cautiously)force_run: Force all jobs to run, ignoring content-addressed caching
-
Workflow call (
workflow_call) — allows reuse as a callable workflow from other workflows- Accepts same inputs as
workflow_dispatch - Enables composition of CI workflows across repositories
- Accepts same inputs as
-
Pull requests (
pull_request) — runs on all PR events except those matchingpaths-ignore- Runs CI checks only (no deployment)
- Skips on markdown-only changes via
paths-ignore: ['**/*.md', 'docs/**'] - Jobs use content-addressed caching to skip if source files unchanged
- Preview jobs deploy to branch-specific URLs
-
Push to main (
pushtomainbranch) — production deployment trigger- Runs full CI with content-addressed caching
- Automatically deploys to production environment
- Runs semantic-release for package publishing
-
Force-run override — bypass caching for specific workflow runs
- Add
force-cilabel to PR to force all jobs to run - Use
workflow_dispatchwithforce_run: truefor manual runs - Useful when cache corruption suspected or after dependency updates
- Add
Caching Architecture
Section titled “Caching Architecture”The CI system implements a three-tier caching strategy to minimize redundant work and reduce workflow execution time from hours to minutes.
Content-Addressed Job Caching
Section titled “Content-Addressed Job Caching”Jobs use the cached-ci-job composite action (.github/actions/cached-ci-job/action.yaml) to implement content-addressed caching at the job level.
This is the first tier of caching and determines whether a job needs to run at all.
The mechanism works by computing a cache key from source files that affect the job’s output, then checking GitHub’s cache for a previous successful run with the same key. If found, the job is skipped entirely. If not found or if the job previously failed, it runs and caches the success state upon completion.
Each job declares its hash-sources — glob patterns identifying files that affect its output.
For example, the flake-validation job uses:
hash-sources: 'justfile flake.nix flake.lock .github/actions/setup-nix/action.yml'When any of these files change, the cache key changes, forcing the job to re-run. When none have changed since the last successful run, the job skips immediately.
The cache key format is: job-result-{sanitized-job-name}-{12-char-content-hash}.
This ensures cache isolation per job and per source state, enabling independent caching for matrix job variants.
Nix Store Cache
Section titled “Nix Store Cache”The second tier caches the Nix store itself across workflow runs.
The setup-nix composite action (.github/actions/setup-nix/action.yml) manages this through two installer strategies:
Full mode (default) — includes disk space reclamation via nothing-but-nix on Linux (or manual cleanup on macOS) and enables DeterminateSystems/magic-nix-cache-action for store path persistence.
This mode reclaims 40-60GB of disk space on GitHub runners and maintains a cached Nix store across runs.
Quick mode (installer: quick) — skips space reclamation and store caching for faster initialization.
Both modes use cachix/install-nix-action for Nix installation but quick mode omits the caching overhead, useful for simple validation jobs.
In full mode, the magic-nix-cache-action transparently caches /nix/store paths between runs using GitHub Actions cache.
FlakeHub is disabled in favor of Cachix for binary caching.
Binary Cache (Cachix)
Section titled “Binary Cache (Cachix)”The third tier uses Cachix as a remote binary cache for pre-built derivations. This cache is shared across all developers and CI runs, not scoped to a single repository or workflow.
The cache-overlay-packages job pre-builds resource-intensive overlay packages (like LLVM, GCC, Rust toolchains) and pushes them to Cachix.
Subsequent jobs pull from Cachix instead of rebuilding from source, reducing build times from hours to minutes.
Cachix configuration is managed via secrets/shared.yaml:
CACHIX_CACHE_NAME— the cache to push/pull fromCACHIX_AUTH_TOKEN— authentication for write access (read access is public)
Cache Invalidation
Section titled “Cache Invalidation”Caches invalidate when their key inputs change:
- Job cache — invalidates when hash sources change (per-job source patterns)
- Nix store cache — invalidates when
flake.lockchanges or Nix version updates - Cachix — never invalidates (content-addressed), but may evict old entries per cache policy
Force-run overrides (force_run input or force-ci label) bypass only the job cache.
Nix store cache and Cachix remain active to accelerate builds even when forcing job re-execution.
Local Development
Section titled “Local Development”Running CI Locally
Section titled “Running CI Locally”The repository maintains parity between CI and local environments through the nix develop -c just [recipe] pattern.
Every CI job has a corresponding justfile recipe that runs the same commands the workflow executes, using the same Nix flake environment.
This enables fast iteration: reproduce CI failures locally without waiting for GitHub Actions, validate fixes immediately, and push with confidence.
CI Job to Local Command Mapping
Section titled “CI Job to Local Command Mapping”| CI Job | Local Equivalent | Purpose |
|---|---|---|
flake-validation | just check or just check-fast | Flake validation (full ~7 min, fast ~1-2 min) |
nix (packages) | just ci-build-category x86_64-linux packages | Build all packages for a specific system |
nix (checks) | just ci-build-category x86_64-linux checks-devshells | Run all checks and build devShells |
typescript | just test-package <name> | Test a specific TypeScript package |
bootstrap-verification | make bootstrap && make verify | Validate Makefile bootstrap workflow |
secrets-workflow | nix develop -c sops -d secrets/test.yaml | Test sops-nix decryption |
| All jobs | just ci-run-watch | Trigger full CI and watch progress |
Key Justfile Recipes
Section titled “Key Justfile Recipes”ci-run-watch — triggers the CI workflow via gh workflow run and watches execution in real-time.
Useful for validating changes before merging.
ci-status — shows current CI run status and summary.
Quickly check if CI is passing without opening GitHub.
ci-logs — downloads and displays CI logs, optionally filtering to failed jobs only via ci-logs-failed.
Faster than navigating GitHub UI for debugging.
ci-build-category <system> <category> — builds a specific category of flake outputs for a specific system.
Example: just ci-build-category x86_64-linux packages builds all packages.
Categories: packages, checks-devshells, home, nixos.
check-fast — runs fast flake validation (1-2 minutes) vs nix flake check (7+ minutes).
Validates flake structure, evaluates outputs, checks formatting without building everything.
Debugging CI Failures
Section titled “Debugging CI Failures”When a CI job fails:
- Identify the failing job — use
just ci-statusor check GitHub Actions UI - Run locally — use the corresponding justfile recipe from the table above
- Reproduce the failure — the local environment should match CI exactly
- Fix and validate — iterate locally until the recipe succeeds
- Force re-run — if needed, add
force-cilabel to PR or usejust ci-run-watch
For Nix-specific failures, enter the development shell explicitly:
nix develop# Then run the failing command manuallyFor failures in specific packages, use targeted builds:
# Build a specific packagenix build .#packages.x86_64-linux.some-package
# Build a specific checknix build .#checks.x86_64-linux.some-checkIf the failure is environment-specific (e.g., macOS vs Linux), use remote builders or platform-specific machines.
The justfile recipes respect the local platform but can be overridden via Nix’s --system flag.
Troubleshooting
Section titled “Troubleshooting”Workflow fails at “Decrypt secrets”
Section titled “Workflow fails at “Decrypt secrets””Check:
SOPS_AGE_KEYis set correctly in GitHub secretssecrets/shared.yamlexists and is encrypted- Age key has permissions to decrypt the file
# Test decryption locallyexport SOPS_AGE_KEY_FILE=/path/to/your/age/keysops --decrypt secrets/shared.yamlDeployment fails with “Invalid API token”
Section titled “Deployment fails with “Invalid API token””Check:
- Token has correct permissions (Workers Scripts Edit, Workers Routes Edit)
- Token hasn’t expired
- Account ID matches your Cloudflare account
Build fails with “Module not found”
Section titled “Build fails with “Module not found””Check:
bun installsucceeded- All dependencies in
package.jsonare correct - Nix flake is up to date
Run locally:
nix developbun installbun run buildSOPS decryption shows wrong age key
Section titled “SOPS decryption shows wrong age key”Ensure the CI_AGE_KEY in secrets/shared.yaml matches the public key in .sops.yaml:
keys: - &ci age1m9m8h5vqr7dqlmvnzcwshmm4uk8umcllazum6eaulkdp3qc88ugs22j3p8Generate the public key from private key:
echo "YOUR_PRIVATE_KEY" | age-keygen -ySecurity Notes
Section titled “Security Notes”- Never commit unencrypted secrets to the repository
- Rotate API tokens regularly
- Use minimal required permissions for tokens
- Enable branch protection on main branch
- Review workflow logs for exposed secrets
- Use environment protection rules for production
Next Steps
Section titled “Next Steps”After successful setup:
- Configure custom domain in Cloudflare
- Set up monitoring and alerts
- Add status badges to README
- Configure additional environments (staging, preview)
- Add deployment notifications (Slack, Discord, etc.)
Useful Commands
Section titled “Useful Commands”GitHub CLI Workflow Commands
Section titled “GitHub CLI Workflow Commands”# List workflowsgh workflow list
# View workflow runsgh run list --workflow=ci.yaml
# Trigger manual deploymentgh workflow run ci.yaml -f deploy_enabled=true
# Force all jobs to run (ignore caching)gh workflow run ci.yaml -f force_run=true
# Run a specific job onlygh workflow run ci.yaml -f job=flake-validation
# View latest rungh run view
# View latest run in browsergh run view --web
# Download workflow artifactsgh run download
# Re-run failed workflowgh run rerun <run-id>Justfile CI Commands
Section titled “Justfile CI Commands”# Trigger CI and watch progress in real-timejust ci-run-watch
# View current CI statusjust ci-status
# View all CI logsjust ci-logs
# View only failed job logsjust ci-logs-failed
# Build specific category locally (matches CI nix job)just ci-build-category x86_64-linux packagesjust ci-build-category x86_64-linux checks-devshellsjust ci-build-category aarch64-darwin home
# Fast flake validation (~1-2 min vs ~7 min nix flake check)just check-fast
# Full flake validation (includes VM tests, ~7 min)just check
# Test a specific TypeScript package (matches CI typescript job)just test-package docs
# Bootstrap verification (matches CI bootstrap-verification job)make bootstrap && make verify
# Secrets workflow test (matches CI secrets-workflow job)nix develop -c sops -d secrets/test.yamlLocal Development Parity
Section titled “Local Development Parity”# Enter development environment (same as CI uses)nix develop
# Run any justfile recipe in dev environmentnix develop -c just <recipe>
# Build specific flake outputnix build .#packages.x86_64-linux.some-packagenix build .#checks.x86_64-linux.some-check
# Evaluate flake outputs without buildingnix eval .#packages.x86_64-linux --apply builtins.attrNames
# Show flake structurenix flake showReferences
Section titled “References”- Cloudflare Workers: https://developers.cloudflare.com/workers/
- Wrangler CLI: https://developers.cloudflare.com/workers/wrangler/
- SOPS: https://github.com/getsops/sops
- GitHub Actions: https://docs.github.com/actions