GitHub Workflows Deep Dive
The Ever Works platform uses GitHub Actions workflows in .github/workflows/ to automate CI, container
image builds, Kubernetes deployments, CLI publishing, and Trigger.dev deployments across three environments
(dev, stage, prod).
Which pipeline is actually live? Images are built by
k8s-build.ymland pushed to GHCR, then deployed to the self-hostedever-k8scluster by ArgoCD, which syncs manifests from theever-co/k8s-gitopsrepo (ArgoCD Image Updater bumps the image digests automatically).The
deploy-do-*.ymlworkflows below deploy to DigitalOcean and are gated off behindvars.DO_ENABLED == 'true'. They are retained deliberately (no-removal policy), not active. Treat any DigitalOcean hostname, registry or IP in this document as historical.
Workflow Inventory
| Workflow File | Name | Trigger | Purpose |
|---|---|---|---|
ci.yml | CI | Push/PR to main, develop, stage | Lint, build, test |
docker-build-publish-dev.yml | Build and Publish Docker Images Dev | Push to develop | Build Docker images for dev |
docker-build-publish-stage.yml | Build and Publish Docker Images Stage | Push to stage | Build Docker images for stage |
docker-build-publish-prod.yml | Build and Publish Docker Images Prod | Push to main | Build Docker images for prod |
k8s-build.yml | k8s-build | Push to develop, stage, main | LIVE — build api/web/mcp/docs images → GHCR |
docker-build-publish-mcp-{dev,stage,prod}.yml | Build and Publish MCP Images | Push to branch | Build MCP images (DigitalOcean path) |
deploy-do-dev.yml | Deploy to DO Dev | After Docker Dev completes | GATED (DO_ENABLED) — legacy DigitalOcean deploy |
deploy-do-stage.yml | Deploy to DO Stage | After Docker Stage completes | GATED (DO_ENABLED) — legacy DigitalOcean deploy |
deploy-do-prod.yml | Deploy to DO Prod | After Docker Prod completes | GATED (DO_ENABLED) — legacy DigitalOcean deploy |
release-trigger-dev.yml | Deploy to Trigger.dev Dev | After CI on develop | Deploy Trigger.dev dev |
release-trigger-stage.yml | Deploy to Trigger.dev Stage | After CI on stage | Deploy Trigger.dev staging |
release-trigger-prod.yml | Deploy to Trigger.dev Prod | After CI on main | Deploy Trigger.dev prod |
publish-cli.yml | Build and Publish CLIs | Push to main, tags, manual | Publish CLI packages |
Pipeline Flow
CI Workflow (ci.yml)
The foundational workflow that all other workflows depend on.
Triggers:
- Push to
main,develop,stage - Pull requests targeting those branches
- Manual
workflow_dispatch
Concurrency: Groups by workflow + ref with cancel-in-progress: true to avoid redundant runs.
Runner: ubicloud-standard-8 (high-performance runner)
Steps:
| Step | Command | Purpose |
|---|---|---|
| Checkout | actions/checkout@v4 | Clone repository |
| Install pnpm | pnpm/action-setup@v3 (v10.13.1) | Package manager setup |
| Setup Node.js | actions/setup-node@v4 (20.x) | Node.js with pnpm cache |
| Install deps | pnpm install --frozen-lockfile | Reproducible install |
| Format check | pnpm format:check | Prettier validation |
| Build all | pnpm build | Turborepo build |
| Run tests | pnpm test | All test suites |
| Build Internal CLI | pnpm build:cli (apps/internal-cli) | CLI compilation |
| Build External CLI | pnpm build:cli (apps/cli) | CLI compilation |
| Test Internal CLI | pnpm test:cli (apps/internal-cli) | CLI tests |
| Test External CLI | pnpm test:cli (apps/cli) | CLI tests |
Secrets Used: API_URL, WEB_URL
Docker Build Workflows
Three identical workflows for dev/stage/prod that build and push Docker images to multiple registries.
Build Process (per environment)
Two parallel jobs build the API and Web images:
Job: ever-works-api
- Build with Docker Buildx
- File: .deploy/docker/api/Dockerfile
- Platform: linux/amd64
- Tags: ghcr.io/ever-works/ever-works-api-{env}:latest
everco/ever-works-api-{env}:latest
registry.digitalocean.com/ever/ever-works-api-{env}:latest
- Cache: registry-based layer caching
- Build args: NODE_ENV={environment}
Job: ever-works-web (same pattern for web image)
Registry Push Order
Each image is pushed to up to four registries:
| Registry | Action | Failure Policy |
|---|---|---|
| GitHub Container Registry | docker/login-action + push | Required |
| Docker Hub | docker/login-action + push | continue-on-error: true |
| DigitalOcean Registry | doctl registry login + push | continue-on-error: true |
| CW Container Registry | docker/login-action (push commented out) | continue-on-error: true |
Secrets Used: DOCKERHUB_USERNAME, DOCKERHUB_TOKEN, DIGITALOCEAN_ACCESS_TOKEN, GITHUB_TOKEN, CW_DOCKER_REGISTRY, CW_DOCKER_USER, CW_DOCKER_USER_PASSWORD
Kubernetes Deploy Workflows
Legacy — not the live path. These three workflows deploy to DigitalOcean Kubernetes and every step is gated behind
vars.DO_ENABLED == 'true', which is not set. The live deployment path isk8s-build.yml→ GHCR → ArgoCD →ever-k8s(manifests inever-co/k8s-gitops). This section is kept for reference and in case the DigitalOcean path is ever re-enabled.
Three workflows (deploy-do-dev.yml, deploy-do-stage.yml, deploy-do-prod.yml) deploy to DigitalOcean Kubernetes.
Trigger
Each runs after its corresponding Docker build workflow completes:
on:
workflow_run:
workflows: ['Build and Publish Docker Images {Env}']
branches: [{ branch }]
types: [completed]
Deployment Steps
| Step | Description |
|---|---|
| Install doctl | DigitalOcean CLI setup |
| Save kubeconfig | Short-lived credentials (600s expiry) |
| Write DB certificate | Decode base64 CA certificate |
| Generate TLS secrets | Create API and Web TLS secrets for ingress |
| Apply K8s manifests | envsubst + kubectl apply |
| Restart pods | Rolling restart to pick up :latest images |
Environment Variables (injected via envsubst)
The manifests receive a comprehensive set of environment variables:
Application:
| Variable | Example |
|---|---|
WEB_URL | https://app.ever.works (prod) / https://app-dev.ever.works (dev) |
ALLOWED_ORIGINS | https://app.ever.works,https://api.ever.works |
JWT_SECRET | From secrets |
AUTH_SECRET | From secrets |
Trigger.dev:
| Variable | Description |
|---|---|
TRIGGER_ENABLED | Enable/disable Trigger.dev |
TRIGGER_SECRET_KEY | Trigger.dev authentication |
TRIGGER_INTERNAL_SECRET | Internal API secret |
OAuth:
| Variable | Description |
|---|---|
GH_CLIENT_ID / GH_CLIENT_SECRET | GitHub OAuth |
GH_CALLBACK_URL | GitHub OAuth callback |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET | Google OAuth |
GOOGLE_CALLBACK_URL | Google OAuth callback |
Plugins:
| Variable | Description |
|---|---|
PLUGIN_OPENROUTER_API_KEY | OpenRouter AI provider |
PLUGIN_OPENROUTER_DEFAULT_MODEL | Default model |
PLUGIN_OPENROUTER_SIMPLE_MODEL | Simple tasks model |
PLUGIN_OPENROUTER_MEDIUM_MODEL | Medium tasks model |
PLUGIN_OPENROUTER_COMPLEX_MODEL | Complex tasks model |
PLUGIN_GITHUB_CLIENT_ID / CLIENT_SECRET | GitHub plugin OAuth |
PLUGIN_TAVILY_API_KEY | Tavily search |
PLUGIN_SCREENSHOTONE_ACCESS_KEY / SECRET_KEY | Screenshot provider |
Database:
| Variable | Description |
|---|---|
DATABASE_TYPE | Database driver |
DATABASE_URL | Connection string |
DATABASE_HOST / PORT / USERNAME / PASSWORD / NAME | Individual connection params |
DATABASE_SSL_MODE | Enable SSL/TLS |
DATABASE_CA_CERT | CA certificate (base64) |
Mail:
| Variable | Description |
|---|---|
MAILER_PROVIDER | Mail transport (smtp or resend) |
EMAIL_FROM | Sender address |
SMTP_HOST / PORT / SECURE / USER / PASSWORD | SMTP config |
RESEND_APIKEY / RESEND_EMAIL_FROM | Resend config |
Environment URLs
These are the hostnames actually served by ever-k8s (ingress rules live in ever-co/k8s-gitops under
apps/ever-works-app-{dev,stage,prod}):
| Environment | Web URL | API URL | Admin URL | MCP URL |
|---|---|---|---|---|
| dev | https://app-dev.ever.works | https://api-dev.ever.works | https://admin-dev.ever.works | https://mcpdev.ever.works |
| stage | https://app-stage.ever.works | https://api-stage.ever.works | https://admin-stage.ever.works | https://mcpstage.ever.works |
| prod | https://app.ever.works | https://api.ever.works | https://admin.ever.works | https://mcp.ever.works |
Legacy aliases. The pre-migration DigitalOcean hostnames appdev / apidev / appstage /
apistage.ever.works (no hyphen) are still served as additional ingress rules on the same backends, so old
links and bookmarks keep working. Prefer the hyphenated names above for anything new — they are the ones the
manifests are keyed on.
Trigger.dev Deploy Workflows
Three workflows deploy background tasks to Trigger.dev across environments.
Trigger
Each runs after CI completes on the corresponding branch:
on:
workflow_run:
workflows: ['CI']
branches: [{ branch }]
types: [completed]
Runner
All use ubicloud-standard-2 (lighter runner, as deployment is simpler).
Steps
| Step | Dev | Stage/Prod |
|---|---|---|
| Build packages | pnpm build --filter './packages/**' | Same |
| Prepare plugins | -- | pnpm prepare:plugins |
| Deploy | Login only (npx [email protected] login) | npx [email protected] deploy --env {env} |
The dev workflow only verifies Trigger.dev connectivity (login) without deploying, because the dev environment uses NestJS built-in scheduling instead.
Secrets Used: TRIGGER_ACCESS_TOKEN
CLI Publish Workflow (publish-cli.yml)
The most complex workflow, handling builds, version bumps, publishing, and GitHub releases.
Triggers
- Push to
mainbranch - Tags:
v*,cli-v*,internal-cli-v* - Manual dispatch with options:
| Input | Type | Description |
|---|---|---|
publish_internal_cli | boolean | Publish internal CLI |
publish_external_cli | boolean | Publish external CLI |
do_version_bump | boolean | Bump version before publish |
version_bump | choice | patch, minor, major |
Jobs
Build Job: Builds all packages, then builds and tests both CLIs. Uploads dist/ as artifacts.
Publish Jobs: Download artifacts, optionally bump version, publish to npm:
- Internal CLI:
npm publish --access restricted(private) - External CLI:
npm publish --access public(public)
Release Jobs: Create GitHub releases with installation instructions using softprops/action-gh-release@v2.
| Tag Pattern | Release Type | Packages |
|---|---|---|
v* | Combined | Both CLIs |
cli-v* | External only | ever-works-cli |
internal-cli-v* | Internal only | @ever-works/cli |
Secrets Used: NPM_TOKEN, API_URL, WEB_URL
Complete Secrets Reference
| Secret | Used In |
|---|---|
API_URL | CI, CLI publish |
WEB_URL | CI, CLI publish |
JWT_SECRET | K8s deploys |
AUTH_SECRET | K8s deploys |
DIGITALOCEAN_ACCESS_TOKEN | Docker builds, K8s deploys |
DOCKERHUB_USERNAME / DOCKERHUB_TOKEN | Docker builds |
GITHUB_TOKEN | Docker builds (auto-provided) |
NPM_TOKEN | CLI publish |
TRIGGER_ACCESS_TOKEN | Trigger.dev deploys |
TRIGGER_ENABLED / SECRET_KEY / INTERNAL_SECRET | K8s deploys |
DATABASE_* (7 vars) | K8s deploys |
SMTP_* (6 vars) | K8s deploys |
RESEND_* (2 vars) | K8s deploys |
PLUGIN_* (10 vars) | K8s deploys |
GH_* / GOOGLE_* (6 vars) | K8s deploys |
INGRESS_* (4 vars) | K8s deploys |
CW_DOCKER_* (3 vars) | Docker builds |
Source Files
| File | Purpose |
|---|---|
.github/workflows/ci.yml | CI pipeline |
.github/workflows/docker-build-publish-dev.yml | Docker build (dev) |
.github/workflows/docker-build-publish-stage.yml | Docker build (stage) |
.github/workflows/docker-build-publish-prod.yml | Docker build (prod) |
.github/workflows/deploy-do-dev.yml | K8s deploy (dev) |
.github/workflows/deploy-do-stage.yml | K8s deploy (stage) |
.github/workflows/deploy-do-prod.yml | K8s deploy (prod) |
.github/workflows/release-trigger-dev.yml | Trigger.dev (dev) |
.github/workflows/release-trigger-stage.yml | Trigger.dev (stage) |
.github/workflows/release-trigger-prod.yml | Trigger.dev (prod) |
.github/workflows/publish-cli.yml | CLI build and publish |