NNO Docs
ArchitectureCross cutting

NNO CI/CD Architecture

Documentation for NNO CI/CD Architecture

Blueprint for the continuous integration and deployment pipeline. All NNO services and apps are deployed via GitHub Actions using a self-hosted runner on DigitalOcean.


1. Infrastructure

Self-Hosted Runner

NNO uses a single self-hosted GitHub Actions runner to avoid GitHub's free-tier build minute limits and to benefit from a persistent dependency cache.

PropertyValue
Namenno-do-sgp1
Labels[self-hosted, nno-runner]
ProviderDigitalOcean Droplet
RegionSGP1 (Singapore)
Node.js20.18 (pinned via .nvmrc)
pnpm10.5.2 (pinned via packageManager in package.json)
pnpm storePersistent across runs (fast installs via cache hits)
ConcurrencySingle runner — jobs queue sequentially

Why self-hosted? GitHub Actions free tier provides 2,000 minutes/month. The NNO monorepo has 7 Workers + 1 Pages app across 2 environments. A single deploy cycle takes ~10 minutes, exhausting free minutes within a week of active development. The DigitalOcean droplet costs ~$12/month and provides unlimited build minutes with faster installs (persistent pnpm store).

GitHub-Hosted Runner

Low-criticality workflows that don't need infrastructure access use ubuntu-latest:

  • Publish Packages — Changesets versioning + GitHub Packages
  • Claude Code — AI-assisted PR reviews and issue responses

2. Workflows

Overview

WorkflowFileTriggerRunnerTimeoutPurpose
CIci.ymlPR, merge_groupself-hosted15mQuality gates: lint, typecheck, format, build, test
Deploy (Pages)deploy.ymlPush to main/developself-hosted15mBuild + deploy console app to CF Pages
Deploy Workersdeploy-workers.ymlPush to main/developself-hosted20mDeploy 7 Workers + D1 migrations in dependency order
Build Auth Bundlebuild-auth-bundle.ymlPush to main (services/auth/src/**), manualself-hosted10mESBuild auth template → R2 upload
Publishpublish.ymlPush to main, release, manualubuntu-latest-Changesets version bump / publish to GitHub Packages
Claude Codeclaude.ymlIssue comment, PR reviewubuntu-latest-AI assistance on @claude mention
Claude Code Reviewclaude-code-review.ymlPR opened/syncedubuntu-latest-Automatic PR review

Concurrency Strategy

All workflows use concurrency groups to prevent duplicate runs:

WorkflowConcurrency GroupCancel In-Progress
CIci-\{ref\}Yes (non-main only)
Deploydeploy-\{ref\}Yes (non-main only)
Deploy Workersdeploy-workers-\{ref\}Yes (non-main only)
Build Auth Bundleauth-bundle-\{ref\}Never (R2 upload safety)
Publishpublish-\{ref\}Never (versioning safety)

Single runner bottleneck: Since all self-hosted workflows share one runner, jobs queue when the runner is busy. Production deploys (main) are never cancelled. Staging deploys (develop) can be cancelled by newer pushes to reduce queue buildup.


3. Deploy Workers Pipeline

The most complex workflow. Deploys all 7 Cloudflare Workers in strict dependency order.

Service Deployment Sequence

IAM ──> Billing ──> Registry ──> Provisioning ──> CLI Service ──> Stack Registry ──> Gateway
 │         │           │              │
 │         │           │              ├── Ensure CF Queues exist
 │         │           │              └── D1 migrations
 │         │           └── D1 migrations (depends on IAM + Billing bindings)
 │         └── D1 migrations
 └── D1 migrations

Why this order?

  1. IAM first — all services depend on IAM for auth
  2. Billing before Registry — Registry has NNO_BILLING service binding. CF API rejects deploys referencing Workers that don't exist yet
  3. Provisioning after Registry — needs CF Queues pre-created before deploy
  4. Gateway last — proxies all other services; must deploy after its upstream targets exist

Environment Resolution

The workflow resolves the target environment from the branch:

# Branch → Environment mapping
main    → is_prod=true,  env_flag=""        # wrangler deploy (no --env)
develop → is_prod=false, env_flag="--env stg"  # wrangler deploy --env stg

Each service's wrangler.toml defines [env.stg] with:

  • Worker name with -stg suffix (e.g., nno-k3m9p2xw7q-gateway-stg)
  • Service bindings pointing to -stg variants
  • Separate D1 database IDs, KV namespace IDs
  • ENVIRONMENT = "stg" var

Queue Provisioning

The Provisioning service requires CF Queues to exist before deploy. A pre-deploy step ensures they're created:

- name: Ensure CF Queues exist (Provisioning)
  run: |
    wrangler queues create "$QUEUE" 2>/dev/null || echo "Already exists"
    wrangler queues create "$DLQ"   2>/dev/null || echo "Already exists"
EnvironmentQueueDLQ
Productionnno-k3m9p2xw7q-provision-queuenno-k3m9p2xw7q-provision-dlq
Stagingnno-k3m9p2xw7q-provision-queue-stgnno-k3m9p2xw7q-provision-dlq-stg

Queue names follow the Cloudflare naming convention: production = no suffix, staging = -stg.


4. Deploy Pages Pipeline

Builds and deploys the NNO Console app (React 19 + Vite) to Cloudflare Pages.

Environment Isolation

Each environment uses a separate CF Pages project (see dns-naming.md):

BranchBuild CommandPages ProjectCustom Domain
developbuild:stg (.env.stg)nno-k3m9p2xw7q-console-stgconsole.app.stg.nno.app
mainbuild:prod (.env.prod)nno-k3m9p2xw7q-consoleconsole.app.nno.app, console.nno.app

Why separate projects? CF Pages custom domains always route to the production deployment. A single project with preview branches cannot serve staging on a custom domain. See dns-naming.md for the Phase 3 routing worker alternative.

Build Pipeline

checkout → pnpm install → turbo build (packages) → vite build --mode {stg|prod} → wrangler pages deploy

Vite embeds VITE_* environment variables at build time. The .env.stg file points all service URLs to *.svc.stg.nno.app, while .env.prod points to *.svc.nno.app.


5. Auth Bundle Pipeline

Builds the auth Worker template (services/auth/) into a self-contained ESM bundle for per-platform deployment by the Provisioning service.

Trigger

  • Push to main when services/auth/src/** files change
  • Manual dispatch (workflow_dispatch)

Steps

checkout → pnpm install → esbuild (ESM, minified, ES2022) → R2 upload (versioned + latest) → update NNO_AUTH_BUNDLE_URL secret

R2 Storage

PathPurpose
auth/v\{SHORT_SHA\}.jsVersioned bundle (immutable)
auth/latest.jsLatest alias (overwritten on each build)

The NNO_AUTH_BUNDLE_URL secret is updated on both prod and stg Provisioning Workers to point to the latest bundle URL.

Prerequisites: R2 bucket nno-k3m9p2xw7q-auth-bundles must exist. Custom domain auth-bundles.nno.app must be configured on the bucket.


6. CI Pipeline

Runs on pull requests and merge groups. Does not run on main/develop push (Deploy Workers handles validation for those branches).

Quality Gates

steps:
  - pnpm install --frozen-lockfile
  - pnpm turbo build          # Build all packages
  - pnpm lint                 # ESLint across workspace
  - pnpm typecheck            # tsc --noEmit across workspace
  - pnpm format:check         # Prettier check
  - pnpm test                 # Vitest (filtered to: core, sdk, ui-auth, billing, iam)

Test Filtering

Tests run only for packages with stable test suites:

"test": "turbo test --filter=@neutrino-io/{core,sdk,ui-auth,service-billing,service-iam}"

This avoids CI failures from packages with incomplete or flaky test coverage.


7. Package Publishing

Manages versioning and publishing of @neutrino-io/* packages to GitHub Packages.

ToolPurpose
ChangesetsSemantic versioning based on change descriptions
GitHub Packagesnpm registry at https://npm.pkg.github.com

Flow

  1. Developer adds a changeset: pnpm changeset
  2. On merge to main, changesets/action creates a version bump PR
  3. When the version PR is merged, packages are published to GitHub Packages

8. Secrets

SecretUsed ByPurpose
CF_API_TOKENDeploy, Deploy Workers, Build Auth BundleCloudflare API (Workers, R2, D1, Queues, Pages)
CF_ACCOUNT_IDDeploy, Deploy Workers, Build Auth BundleCloudflare account identifier
NPM_TOKENCI, Deploy, PublishGitHub Packages authentication
CLAUDE_CODE_OAUTH_TOKENClaude workflowsClaude Code AI action
TURBO_TEAM / TURBO_TOKENCI, Deploy WorkersTurborepo remote caching

Service-Level Secrets (set via wrangler secret put)

SecretServicesPurpose
AUTH_SECRETIAMBetter Auth signing key
AUTH_API_KEYAll servicesService-to-service static API key
NNO_INTERNAL_API_KEYRegistry, GatewayInternal auth for operator endpoints
STRIPE_SECRET_KEYBillingStripe API access
STRIPE_WEBHOOK_SECRETBillingStripe webhook signature verification
NNO_AUTH_BUNDLE_URLProvisioningURL to pre-built auth Worker JS bundle
GITHUB_APP_PRIVATE_KEYCLI ServiceGitHub App authentication for repo management

9. Build System (Turborepo)

The monorepo uses Turborepo for task orchestration with dependency-aware caching.

Task Graph

         ┌──────────┐
         │  build   │ ← depends on ^build (upstream packages)
         └────┬─────┘

    ┌─────────┼──────────┐
    │         │          │
┌───▼──┐ ┌───▼────┐ ┌───▼───┐
│ lint │ │typecheck│ │ test  │ ← all depend on ^build
└──────┘ └────────┘ └───────┘

Configuration (turbo.json)

{
  "globalEnv": ["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID"],
  "globalDependencies": [".env", "apps/console/.env.stg", "apps/console/.env.prod"],
  "tasks": {
    "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
    "lint": { "dependsOn": ["^build"] },
    "typecheck": { "dependsOn": ["^build"] },
    "test": { "dependsOn": ["^build"] },
    "dev": { "persistent": true, "cache": false }
  },
  "concurrency": "50%"
}

10. Scaling Considerations

Current Limitations

LimitationImpactMitigation
Single self-hosted runnerJobs queue sequentially; ~10-15 min wait during active developmentConcurrency groups cancel stale staging deploys
Single CF accountAll services share account-level quotasNaming convention provides logical isolation
Sequential Worker deploysFull deploy takes ~8-10 minutesService binding order is mandatory; cannot parallelize

Future Improvements (Phase 3)

ImprovementBenefit
Staging environment routing WorkerEliminate separate -stg Pages projects; proxy *.stg.nno.app to branch preview URLs
Second self-hosted runnerParallel staging + production deploys; reduce queue wait times
Cloudflare Workers BuildsReplace GitHub Actions entirely; deploy Workers from within the CF ecosystem
CF Pages Direct UploadEliminate per-client GitHub repos for platform builds

On this page