Skip to main content

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.yml and pushed to GHCR, then deployed to the self-hosted ever-k8s cluster by ArgoCD, which syncs manifests from the ever-co/k8s-gitops repo (ArgoCD Image Updater bumps the image digests automatically).

The deploy-do-*.yml workflows below deploy to DigitalOcean and are gated off behind vars.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 FileNameTriggerPurpose
ci.ymlCIPush/PR to main, develop, stageLint, build, test
docker-build-publish-dev.ymlBuild and Publish Docker Images DevPush to developBuild Docker images for dev
docker-build-publish-stage.ymlBuild and Publish Docker Images StagePush to stageBuild Docker images for stage
docker-build-publish-prod.ymlBuild and Publish Docker Images ProdPush to mainBuild Docker images for prod
k8s-build.ymlk8s-buildPush to develop, stage, mainLIVE — build api/web/mcp/docs images → GHCR
docker-build-publish-mcp-{dev,stage,prod}.ymlBuild and Publish MCP ImagesPush to branchBuild MCP images (DigitalOcean path)
deploy-do-dev.ymlDeploy to DO DevAfter Docker Dev completesGATED (DO_ENABLED) — legacy DigitalOcean deploy
deploy-do-stage.ymlDeploy to DO StageAfter Docker Stage completesGATED (DO_ENABLED) — legacy DigitalOcean deploy
deploy-do-prod.ymlDeploy to DO ProdAfter Docker Prod completesGATED (DO_ENABLED) — legacy DigitalOcean deploy
release-trigger-dev.ymlDeploy to Trigger.dev DevAfter CI on developDeploy Trigger.dev dev
release-trigger-stage.ymlDeploy to Trigger.dev StageAfter CI on stageDeploy Trigger.dev staging
release-trigger-prod.ymlDeploy to Trigger.dev ProdAfter CI on mainDeploy Trigger.dev prod
publish-cli.ymlBuild and Publish CLIsPush to main, tags, manualPublish 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:

StepCommandPurpose
Checkoutactions/checkout@v4Clone repository
Install pnpmpnpm/action-setup@v3 (v10.13.1)Package manager setup
Setup Node.jsactions/setup-node@v4 (20.x)Node.js with pnpm cache
Install depspnpm install --frozen-lockfileReproducible install
Format checkpnpm format:checkPrettier validation
Build allpnpm buildTurborepo build
Run testspnpm testAll test suites
Build Internal CLIpnpm build:cli (apps/internal-cli)CLI compilation
Build External CLIpnpm build:cli (apps/cli)CLI compilation
Test Internal CLIpnpm test:cli (apps/internal-cli)CLI tests
Test External CLIpnpm 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:

RegistryActionFailure Policy
GitHub Container Registrydocker/login-action + pushRequired
Docker Hubdocker/login-action + pushcontinue-on-error: true
DigitalOcean Registrydoctl registry login + pushcontinue-on-error: true
CW Container Registrydocker/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 is k8s-build.yml → GHCR → ArgoCD → ever-k8s (manifests in ever-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

StepDescription
Install doctlDigitalOcean CLI setup
Save kubeconfigShort-lived credentials (600s expiry)
Write DB certificateDecode base64 CA certificate
Generate TLS secretsCreate API and Web TLS secrets for ingress
Apply K8s manifestsenvsubst + kubectl apply
Restart podsRolling restart to pick up :latest images

Environment Variables (injected via envsubst)

The manifests receive a comprehensive set of environment variables:

Application:

VariableExample
WEB_URLhttps://app.ever.works (prod) / https://app-dev.ever.works (dev)
ALLOWED_ORIGINShttps://app.ever.works,https://api.ever.works
JWT_SECRETFrom secrets
AUTH_SECRETFrom secrets

Trigger.dev:

VariableDescription
TRIGGER_ENABLEDEnable/disable Trigger.dev
TRIGGER_SECRET_KEYTrigger.dev authentication
TRIGGER_INTERNAL_SECRETInternal API secret

OAuth:

VariableDescription
GH_CLIENT_ID / GH_CLIENT_SECRETGitHub OAuth
GH_CALLBACK_URLGitHub OAuth callback
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRETGoogle OAuth
GOOGLE_CALLBACK_URLGoogle OAuth callback

Plugins:

VariableDescription
PLUGIN_OPENROUTER_API_KEYOpenRouter AI provider
PLUGIN_OPENROUTER_DEFAULT_MODELDefault model
PLUGIN_OPENROUTER_SIMPLE_MODELSimple tasks model
PLUGIN_OPENROUTER_MEDIUM_MODELMedium tasks model
PLUGIN_OPENROUTER_COMPLEX_MODELComplex tasks model
PLUGIN_GITHUB_CLIENT_ID / CLIENT_SECRETGitHub plugin OAuth
PLUGIN_TAVILY_API_KEYTavily search
PLUGIN_SCREENSHOTONE_ACCESS_KEY / SECRET_KEYScreenshot provider

Database:

VariableDescription
DATABASE_TYPEDatabase driver
DATABASE_URLConnection string
DATABASE_HOST / PORT / USERNAME / PASSWORD / NAMEIndividual connection params
DATABASE_SSL_MODEEnable SSL/TLS
DATABASE_CA_CERTCA certificate (base64)

Mail:

VariableDescription
MAILER_PROVIDERMail transport (smtp or resend)
EMAIL_FROMSender address
SMTP_HOST / PORT / SECURE / USER / PASSWORDSMTP config
RESEND_APIKEY / RESEND_EMAIL_FROMResend 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}):

EnvironmentWeb URLAPI URLAdmin URLMCP URL
devhttps://app-dev.ever.workshttps://api-dev.ever.workshttps://admin-dev.ever.workshttps://mcpdev.ever.works
stagehttps://app-stage.ever.workshttps://api-stage.ever.workshttps://admin-stage.ever.workshttps://mcpstage.ever.works
prodhttps://app.ever.workshttps://api.ever.workshttps://admin.ever.workshttps://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

StepDevStage/Prod
Build packagespnpm build --filter './packages/**'Same
Prepare plugins--pnpm prepare:plugins
DeployLogin 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 main branch
  • Tags: v*, cli-v*, internal-cli-v*
  • Manual dispatch with options:
InputTypeDescription
publish_internal_clibooleanPublish internal CLI
publish_external_clibooleanPublish external CLI
do_version_bumpbooleanBump version before publish
version_bumpchoicepatch, 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 PatternRelease TypePackages
v*CombinedBoth CLIs
cli-v*External onlyever-works-cli
internal-cli-v*Internal only@ever-works/cli

Secrets Used: NPM_TOKEN, API_URL, WEB_URL

Complete Secrets Reference

SecretUsed In
API_URLCI, CLI publish
WEB_URLCI, CLI publish
JWT_SECRETK8s deploys
AUTH_SECRETK8s deploys
DIGITALOCEAN_ACCESS_TOKENDocker builds, K8s deploys
DOCKERHUB_USERNAME / DOCKERHUB_TOKENDocker builds
GITHUB_TOKENDocker builds (auto-provided)
NPM_TOKENCLI publish
TRIGGER_ACCESS_TOKENTrigger.dev deploys
TRIGGER_ENABLED / SECRET_KEY / INTERNAL_SECRETK8s 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

FilePurpose
.github/workflows/ci.ymlCI pipeline
.github/workflows/docker-build-publish-dev.ymlDocker build (dev)
.github/workflows/docker-build-publish-stage.ymlDocker build (stage)
.github/workflows/docker-build-publish-prod.ymlDocker build (prod)
.github/workflows/deploy-do-dev.ymlK8s deploy (dev)
.github/workflows/deploy-do-stage.ymlK8s deploy (stage)
.github/workflows/deploy-do-prod.ymlK8s deploy (prod)
.github/workflows/release-trigger-dev.ymlTrigger.dev (dev)
.github/workflows/release-trigger-stage.ymlTrigger.dev (stage)
.github/workflows/release-trigger-prod.ymlTrigger.dev (prod)
.github/workflows/publish-cli.ymlCLI build and publish