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 |
docker-hub-publish.yml | Publish Images to Docker Hub | After every k8s-build, manual | Copy the k8s-build images to Docker Hub (everco) |
publish-plugins.yml | Publish Plugins | Push to main, manual | Publish SDK, contracts and plugins to npm + GH Pkgs |
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
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 three registries:
| Registry | Action | Failure Policy |
|---|---|---|
| GitHub Container Registry | docker/login-action + push | Required |
| DigitalOcean Registry | doctl registry login + push | Required when vars.DO_ENABLED=true |
| CW Container Registry | docker/login-action (push commented out) | continue-on-error: true |
Docker Hub is not pushed from these workflows any more — see
Docker Hub Publish Workflow. The
continue-on-error Docker Hub push that used to live here never succeeded: the repository had
no DOCKERHUB_* secrets, so every run logged Username and password required and still went
green.
Secrets Used: DIGITALOCEAN_ACCESS_TOKEN, GITHUB_TOKEN, CW_DOCKER_REGISTRY, CW_DOCKER_USER, CW_DOCKER_USER_PASSWORD
Docker Hub Publish Workflow (docker-hub-publish.yml)
Publishes the platform images to Docker Hub under the everco organisation (the org Ever
Gauzy and Ever Teams publish to), so self-hosters can docker pull everco/ever-works-api.
- Trigger:
workflow_runon every completedk8s-buildfordevelop,stageandmain, plus manual dispatch (sha,move_latest). - Mechanism: a registry-to-registry copy of
ghcr.io/ever-works/<image>:sha-<commit>withdocker buildx imagetools create— the byte-identical image the clusters run, no rebuild. - Images:
ever-works-api,ever-works-web,ever-works-mcp,ever-works-docs.
| Branch | Docker Hub repository | Tags |
|---|---|---|
main | everco/ever-works-<image> | latest, <release version>, sha-<commit> |
stage | everco/ever-works-<image>-stage | latest, <release version>, sha-<commit> |
develop | everco/ever-works-<image>-dev | latest, <release version>, sha-<commit> |
latest only moves when the commit is still the branch tip. Missing credentials, a failed copy,
a digest mismatch or a repository that is not anonymously visible are hard errors.
Secrets Used: DOCKERHUB_USERNAME, DOCKERHUB_TOKEN (repository secrets — keep them off
the org level, see the workflow header). Variables: DOCKERHUB_NAMESPACE (default
everco), DOCKERHUB_PUBLISH_ENABLED (false switches it off).
One-time setup: add the two repository secrets (an everco Organization Access Token with
"repository create" + push, username everco; or a Read/Write/Delete PAT of an Owner/Editor of
everco), keep everco's default repository privacy at Public, then backfill with
gh workflow run docker-hub-publish.yml --ref <develop|stage|main>.
npm Package Publish Workflow (publish-plugins.yml)
Publishes @ever-works/contracts, @ever-works/plugin and every distributable plugin to
npmjs.org (with provenance) and GitHub Packages, publicly, on every push to main.
scripts/release-npm-packages.mjs fingerprints what each package would ship: an unchanged
package is skipped, a changed one gets the next patch automatically (or its package.json
version, when that was bumped on purpose — including by a pending changeset: the script reads
.changeset/*.md itself and bumps only the packages a changeset names; pnpm changeset version
is never run, because it would bump every plugin that peer-depends on the SDK to a major). Both registries receive the same tarball; sibling @ever-works/*
dependencies are published as caret ranges.
- Manual dispatch:
plan(decide versions only) anddry-runrun from any branch;publishis refused anywhere butmain. - Every released package needs a
filesallow-list ("files": ["dist"]). Without one npm ships sources, tests and turbo's per-run build log, the fingerprint changes on every build, and the package is refused. - Private packages. Every npm publish carries
--access public, including for a package that is currently private: npm applies the flag to an existing package exactly asnpm access set status=publicwould, so the publish is what makes it public. That is the only route CI has. Since 2026-07-31 an npm granular access token configured to bypass 2FA — exactly whatNPM_TOKENis — cannot change package access:POST /-/package/<pkg>/accessanswers403, and npm's changelog lists "changing package access" among the operations that now need an interactive 2FA challenge. The CLI's fallback is a browser approval per package, and npmjs.com sits behind bot detection, so neither can be scripted. If npm ever refuses the flag, the version is published anyway, without it, and the row is marked⚠ published without --access. GitHub Packages has no API for visibility at all — flip a package under Package settings → Change visibility; the summary lists the ones still private.
Secrets Used: NPM_TOKEN — an npm granular access token, read and write on the
@ever-works scope, "bypass 2FA", at most 90 days (npm's cap for write tokens; classic tokens
were revoked on 2025-12-09). Rotate it when the job fails with 401 or E404 PUT. npm tries
trusted publishing (OIDC) first, so packages configured with
npm trust github <name> --file publish-plugins.yml --repo ever-works/ever-works --allow-publish
need no token at all. GITHUB_TOKEN covers GitHub Packages.
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 trigger.dev@4.4.1 login) | npx trigger.dev@4.4.1 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 Hub publish |
GITHUB_TOKEN | Docker builds (auto-provided) |
NPM_TOKEN | CLI + npm package 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 |