Modular, reusable CI/CD framework for frontend projects. Config-file driven, with pluggable E2E tests, Vercel 0% canary deployments, Datadog deployment gates, and progressive Slack notifications.
Each frontend repo adds a config file (.github/cicd.yml) and two thin workflow files. Repos that track an upstream repo add one more thin upstream workflow. The framework handles everything else.
Your Repo ci-cd-workflows (this repo)
────────── ────────────────────────────
.github/cicd.yml ──reads──► config/read action
.github/workflows/
deploy.yml (25 lines) ──uses──► deploy.yml orchestrator
ci.yml (10 lines) ──uses──► ci.yml orchestrator
upstream-track.yml ──uses──► upstream-sync.yml orchestrator (optional)
Push to main
└─► Slack: "Deploying v1.42.3"
└─► Vercel: Deploy at 0% canary
└─► E2E tests + Lighthouse (parallel, against canary)
└─► Datadog gate check
└─► Execute rollout plan (auto/manual)
└─► Slack: "Deployed" or "Aborted"
# yaml-language-server: $schema=https://raw.githubusercontent.com/dydxopsdao/ci-cd-workflows/v1/schemas/cicd.schema.json
project:
name: "my-app"
emoji: ":rocket:"
slack:
channels:
deploy: "C0DEPLOY00"
alerts: "C0ALERTS00"
ci: "C0CINOTIFY"
notify_users:
- "U0YOURSLACKID"
vercel:
project_id: "prj_xxxx"
production_url: "https://my-app.example.com"
modules:
e2e:
enabled: true
lighthouse:
enabled: false
datadog_gate:
enabled: false
json_validation:
enabled: falsename: Deploy
on:
push:
branches: [main]
jobs:
pipeline:
uses: dydxopsdao/ci-cd-workflows/.github/workflows/deploy.yml@v1
secrets: inherit
e2e:
needs: [pipeline]
if: needs.pipeline.outputs.canary_ready == 'true' && needs.pipeline.outputs.e2e_enabled == 'true'
runs-on: ubuntu-latest
outputs:
result: ${{ steps.tests.outputs.result }}
steps:
- uses: your-org/your-e2e-repo/.github/actions/run@v1
id: tests
with:
base_url: "${{ needs.pipeline.outputs.canary_url }}"
canary_cookie: "${{ needs.pipeline.outputs.canary_cookie }}"
- uses: dydxopsdao/ci-cd-workflows/actions/slack/update@v1
if: always()
with:
slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"
channel_id: "${{ needs.pipeline.outputs.slack_channel_id }}"
message_ts: "${{ needs.pipeline.outputs.slack_message_ts }}"
step_name: "E2E Tests"
status: "${{ steps.tests.outputs.result }}"
promote:
needs: [pipeline, e2e]
if: always() && needs.pipeline.outputs.canary_ready == 'true'
uses: dydxopsdao/ci-cd-workflows/.github/workflows/promote.yml@v1
with:
deployment_id: "${{ needs.pipeline.outputs.deployment_id }}"
e2e_result: "${{ needs.e2e.result == 'skipped' && '' || (needs.e2e.result == 'success' && 'passed' || 'failed') }}"
lighthouse_result: "${{ needs.pipeline.outputs.lighthouse_result == 'skipped' && '' || needs.pipeline.outputs.lighthouse_result }}"
datadog_result: "${{ needs.pipeline.outputs.datadog_result == 'skipped' && '' || needs.pipeline.outputs.datadog_result }}"
secrets: inheritImportant: The promote job must wire through all gate results. Missing results for enabled modules will cause an automatic abort.
name: CI
on:
pull_request:
types: [opened, edited, reopened, synchronize]
jobs:
ci:
uses: dydxopsdao/ci-cd-workflows/.github/workflows/ci.yml@v1
secrets: inheritEnable Rolling Releases on your Vercel project with a 0% first stage.
Use this only for repos with modules.upstream_tracker.enabled: true:
name: Upstream Track
on:
repository_dispatch:
types: [upstream_release_detected]
workflow_dispatch:
inputs:
upstream_repo:
required: true
type: string
upstream_tag:
required: true
type: string
jobs:
sync:
uses: dydxopsdao/ci-cd-workflows/.github/workflows/upstream-sync.yml@v1
with:
upstream_repo: ${{ github.event.client_payload.upstream_repo || inputs.upstream_repo }}
upstream_tag: ${{ github.event.client_payload.upstream_tag || inputs.upstream_tag }}
production_branch: main
secrets: inherit| Secret | Required | Description |
|---|---|---|
SLACK_BOT_TOKEN |
Yes | Slack bot token with chat:write scope |
VERCEL_TOKEN |
Yes | Vercel API token |
DD_API_KEY |
If using Datadog gate | Datadog API key |
DD_APP_KEY |
If using Datadog gate | Datadog application key |
GH_APP_ID |
If using upstream tracker | GitHub App ID |
GH_APP_PRIVATE_KEY |
If using upstream tracker | GitHub App private key |
UPSTREAM_WEBHOOK_SECRET |
If using upstream tracker webhook receiver | HMAC secret used to validate incoming GitHub webhooks |
All project config lives in .github/cicd.yml. See schemas/cicd.schema.json for the full schema, or examples/cicd.yml for a complete example.
Control how canaries are promoted after tests pass:
# Auto mode: advance on a timer
rollout:
mode: "auto"
stages:
- percent: 0 # tests run here
- percent: 10
duration: 5m
- percent: 50
duration: 10m
- percent: 100
timeout: 15m
# Manual mode: hold at each stage until approved
rollout:
mode: "manual"
stages:
- percent: 0
- percent: 25
- percent: 50
- percent: 100
timeout: 24h
# Mixed: auto for early stages, manual approval for the big jump
rollout:
mode: "auto"
stages:
- percent: 0
- percent: 10
duration: 5m
- percent: 50
require_approval: true
- percent: 100Default (when rollout is omitted): auto, 0% -> 100%, 15m timeout.
.github/workflows/
ci.yml Reusable CI orchestrator
deploy.yml Reusable deploy orchestrator
promote.yml Promote or abort canary
upstream-sync.yml Reusable upstream sync orchestrator
actions/
config/read/ Parse .github/cicd.yml
slack/init/ Post initial Slack progress message
slack/update/ Update Slack message in-place
deploy/vercel-canary/ Manage Vercel Rolling Releases
deploy/datadog-gate/ Check Datadog monitors
deploy/upstream-tracker/ Detect upstream releases (for forks)
test/lighthouse/ Lighthouse audits against canary
test/json-validate/ JSON config validation
schemas/
cicd.schema.json JSON Schema for config validation
templates/slack-blocks/ Block Kit templates
examples/ Example config and workflow files
webhook-api/ Webhook receiver (Vercel serverless) for upstream release events
E2E tests live in separate repos. Any test repo that plugs into this framework must expose a composite action with this interface:
inputs:
base_url: # URL to test against (required)
canary_cookie: # Cookie param for canary pinning (optional)
outputs:
result: # "passed" or "failed"
duration: # Total duration in seconds
report_url: # URL to HTML report artifactSee examples/e2e-action.yml for a complete Playwright example.
The upstream tracking path is event-driven:
- GitHub sends
release.publishedorcreate(tag)webhook towebhook-api/api/github/upstream-release. - The webhook API validates
X-Hub-Signature-256usingUPSTREAM_WEBHOOK_SECRET. - The API scans repos in
TARGET_ORG, reads each.github/cicd.yml, and selects repos where:modules.upstream_tracker.enabled: truemodules.upstream_tracker.upstream_repomatches the webhook source repo.
- For each match, the API sends
repository_dispatchwith:event_type: upstream_release_detectedclient_payload: { upstream_repo, upstream_tag, upstream_version, source_event, delivery_id, occurred_at }
- Consumer repo
upstream-track.ymlcallsupstream-sync.yml, which:- classifies patch vs minor/major using
actions/deploy/upstream-tracker - auto-creates/updates a sync PR for patches when
auto_deploy_patches: true - posts Slack alert only for minor/major updates when
notify_on_minor_major: true.
- classifies patch vs minor/major using
Webhook API deployment env vars:
GH_APP_IDGH_APP_PRIVATE_KEYUPSTREAM_WEBHOOK_SECRETUPSTREAM_ALLOWED_REPOS(comma-separatedowner/repo)TARGET_ORG(consumer repo org to scan)
Consumer repos pin to a major version tag:
uses: dydxopsdao/ci-cd-workflows/.github/workflows/deploy.yml@v1v1always points to the latestv1.x.x(non-breaking updates)- Pin to
@v1.2.0for strict version control - See CHANGELOG.md for breaking changes