CI/CD

Explainer uses a single GitHub Actions workflow for continuous deployment. All three apps (Docs, Blog, Website) are managed in one unified workflow with smart change detection.

Workflow file

FileAppsTriggers
.github/workflows/deploy.ymlDocs, Blog, Websitepush to main, workflow_dispatch

Trigger modes

The workflow supports two trigger modes:

  • Push to main — only builds and deploys apps affected by the changed files (path-based filtering)
  • Manual (workflow_dispatch) — allows selecting which apps to deploy via checkboxes
on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      docs:
        description: 'Deploy Docs'
        type: boolean
        default: true
      blog:
        description: 'Deploy Blog'
        type: boolean
        default: true
      website:
        description: 'Deploy Website'
        type: boolean
        default: true

Change detection

A changes job uses dorny/paths-filter to detect which apps need rebuilding:

AppTriggers on changes to
Docsapps/docs/**, packages/**, pnpm-lock.yaml
Blogapps/blog/**, packages/**, pnpm-lock.yaml
Websiteapps/website/**, packages/**, pnpm-lock.yaml

Changes to packages/** trigger all app builds. This is intentional — shared packages like @explainer/ui or @explainer/mdx affect all apps, so they all need to be rebuilt and deployed.

Build and deploy pattern

Each app follows a two-stage pattern: build then deploy. Build jobs run in parallel when multiple apps are affected.

build-docs:
  needs: changes
  if: needs.changes.outputs.docs == 'true' || (github.event_name == 'workflow_dispatch' && inputs.docs)
  steps:
    - uses: actions/checkout@v4
    - uses: pnpm/action-setup@v4
    - uses: actions/setup-node@v4
      with:
        node-version: 22
        cache: 'pnpm'
    - run: pnpm install --frozen-lockfile
    - run: pnpm --filter @explainer/docs build
      env:
        PUBLIC_WEBSITE_URL: ${{ vars.PUBLIC_WEBSITE_URL }}
        PUBLIC_DOCS_URL: ${{ vars.PUBLIC_DOCS_URL }}
        PUBLIC_BLOG_URL: ${{ vars.PUBLIC_BLOG_URL }}
    - uses: actions/upload-artifact@v4
      with:
        name: docs-dist
        path: apps/docs/dist/

The built artifacts are uploaded and consumed by the deployment job.

Environment variables and secrets

Public variables

These are required for cross-app navigation links. Set them in GitHub Settings → Variables:

VariableDescriptionExample
PUBLIC_WEBSITE_URLWebsite app URLhttps://explainer.dev
PUBLIC_DOCS_URLDocs app URLhttps://docs.explainer.dev
PUBLIC_BLOG_URLBlog app URLhttps://blog.explainer.dev
DEPLOY_TARGETDeployment platformcloudflare

All three PUBLIC_* variables are passed to every app build. This ensures navbar links between apps always point to the correct URLs, regardless of the deployment platform.

Secrets

TypeNameUsed by
SecretCLOUDFLARE_API_TOKENCloudflare
SecretCLOUDFLARE_ACCOUNT_IDCloudflare
SecretVERCEL_TOKENVercel
SecretVERCEL_ORG_IDVercel
SecretVERCEL_DOCS_PROJECT_IDVercel (docs)

GitHub Pages uses built-in GITHUB_TOKEN permissions — no additional secrets are needed.

DEPLOY_TARGET pattern

The workflow uses a repository variable DEPLOY_TARGET to select the deployment platform:

deploy-docs:
  needs: build-docs
  if: vars.DEPLOY_TARGET == 'cloudflare'
  # ...

Set DEPLOY_TARGET once in your repository variables and the correct deployment job runs automatically.