Skip to content

CI/CD setup

This guide walks through setting up the GitHub Actions CI/CD pipeline for automated Cloudflare Workers deployment.

  1. Cloudflare account with Workers enabled
  2. GitHub repository with Actions enabled
  3. SOPS installed locally (nix profile install nixpkgs#sops)
  4. Age key pair for encryption
  1. Visit https://dash.cloudflare.com/profile/api-tokens
  2. Click “Create Token”
  3. Use “Edit Cloudflare Workers” template or create custom token with:
    • Account.Workers Scripts (Edit)
    • Account.Workers Routes (Edit)
  4. Copy the generated token
  1. Visit https://dash.cloudflare.com/
  2. Select your account
  3. Go to Workers & Pages
  4. Find Account ID in the right sidebar

Optional but recommended for full CI functionality:

Create vars/shared.yaml with your secrets:

CLOUDFLARE_ACCOUNT_ID: your-actual-account-id
CLOUDFLARE_API_TOKEN: your-actual-api-token
CACHIX_AUTH_TOKEN: your-actual-cachix-token
CACHIX_CACHE_NAME: your-cache-name
CI_AGE_KEY: age-secret-key-1... # CI age private key from .sops.yaml

The CI_AGE_KEY should be the private key corresponding to the public key: age1m9m8h5vqr7dqlmvnzcwshmm4uk8umcllazum6eaulkdp3qc88ugs22j3p8

Terminal window
# Verify you have the correct age keys configured
cat .sops.yaml
# Encrypt the file in place
sops --encrypt --in-place vars/shared.yaml
# Verify encryption succeeded
head vars/shared.yaml
# Should show encrypted content starting with ENC[...]
Terminal window
git add vars/shared.yaml
git commit -m "build: add encrypted secrets for CI/CD"
git push

The CI needs the private age key to decrypt vars/shared.yaml:

Terminal window
# Extract the CI_AGE_KEY from the encrypted file
sops --decrypt --extract '["CI_AGE_KEY"]' vars/shared.yaml | gh secret set SOPS_AGE_KEY

Or manually:

  1. Decrypt the file: sops vars/shared.yaml
  2. Copy the CI_AGE_KEY value
  3. Go to https://github.com/YOUR_USERNAME/YOUR_REPO/settings/secrets/actions
  4. Click “New repository secret”
  5. Name: SOPS_AGE_KEY
  6. Value: Paste the age private key
  7. Click “Add secret”

If using Cachix, set these as repository variables (not secrets):

  1. Go to https://github.com/YOUR_USERNAME/YOUR_REPO/settings/variables/actions
  2. Add variable CACHIX_CACHE_NAME with your cache name

Alternatively, the workflow will read from the encrypted vars/shared.yaml.

  1. Go to https://github.com/YOUR_USERNAME/YOUR_REPO/settings/environments
  2. Click “New environment”
  3. Name: production
  4. Add protection rules as desired (e.g., required reviewers)
  5. Save

Test the workflow manually before enabling automatic deployment:

Terminal window
# Trigger workflow with deployment disabled (safe test)
gh workflow run ci.yaml
# Or with deployment enabled
gh workflow run ci.yaml -f deploy_enabled=true
Terminal window
# Watch the workflow run
gh run watch
# Or view in browser
gh run view --web

The workflow should complete these jobs in order:

  1. secrets-scan: Gitleaks secret scanning
  2. set-variables: Configure workflow variables
  3. bootstrap-verification: Validate bootstrap workflow
  4. config-validation: Test config.nix user definitions
  5. autowiring-validation: Verify nixos-unified autowiring
  6. secrets-workflow: Test sops-nix mechanics
  7. justfile-activation: Validate justfile recipes
  8. cache-overlay-packages: Pre-cache overlay packages
  9. nix: Build all flake outputs
  10. docs-test: Test documentation site
  11. docs-deploy: Deploy to Cloudflare Workers (only if enabled)

If deployment succeeded, verify at:

Once manual testing succeeds, automatic deployment on push to main is already configured.

Push to main branch:

Terminal window
git checkout main
git pull
# Make changes...
git add .
git commit -m "your changes"
git push

The workflow will automatically:

  1. Run all CI checks
  2. Build the site
  3. Deploy to Cloudflare Workers

The CI/CD workflow runs on:

  1. Manual dispatch (workflow_dispatch)

    • debug_enabled: Enable tmate debugging session
    • deploy_enabled: Force deployment even on non-main branch
  2. Pull requests (pull_request)

    • Runs CI checks only (no deployment)
    • Skip with label: skip-ci
    • Enable debug with label: actions-debug
  3. Push to main (push to main branch)

    • Runs full CI
    • Automatically deploys to production

Check:

  • SOPS_AGE_KEY is set correctly in GitHub secrets
  • vars/shared.yaml exists and is encrypted
  • Age key has permissions to decrypt the file
Terminal window
# Test decryption locally
export SOPS_AGE_KEY_FILE=/path/to/your/age/key
sops --decrypt vars/shared.yaml

Deployment 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

Check:

  • bun install succeeded
  • All dependencies in package.json are correct
  • Nix flake is up to date

Run locally:

Terminal window
nix develop
bun install
bun run build

Ensure the CI_AGE_KEY in vars/shared.yaml matches the public key in .sops.yaml:

keys:
- &ci age1m9m8h5vqr7dqlmvnzcwshmm4uk8umcllazum6eaulkdp3qc88ugs22j3p8

Generate the public key from private key:

Terminal window
echo "YOUR_PRIVATE_KEY" | age-keygen -y
  1. Never commit unencrypted secrets to the repository
  2. Rotate API tokens regularly
  3. Use minimal required permissions for tokens
  4. Enable branch protection on main branch
  5. Review workflow logs for exposed secrets
  6. Use environment protection rules for production

After successful setup:

  1. Configure custom domain in Cloudflare
  2. Set up monitoring and alerts
  3. Add status badges to README
  4. Configure additional environments (staging, preview)
  5. Add deployment notifications (Slack, Discord, etc.)
Terminal window
# List workflows
gh workflow list
# View workflow runs
gh run list --workflow=ci.yaml
# Trigger manual deployment
gh workflow run ci.yaml -f deploy_enabled=true
# View latest run
gh run view
# Download workflow artifacts
gh run download
# Re-run failed workflow
gh run rerun <run-id>