Push-to-main, lane-aware, artifact-driven. Detect what changed, build it once, run e2e against the built artifact, then promote that same artifact through a GitHub Environment. Deployment specifics belong in the target platform or infrastructure repo; this skill owns the workflow contract, trust boundaries, and verification loop.
Pipeline Shape
push to main
-> detect-changes (paths-filter or graph-aware affected detection)
-> verify-<lane> (lint + typecheck + test + build -> upload artifact)
-> e2e-<lane> (download artifact, run e2e against it)
-> deploy-<env>-<lane> (environment-scoped credentials + OIDC -> promote artifact)
-> smoke-<env>-<lane> (read-only job, no deploy credentials -> hit deployed URL)
Each lane is independent: a web-only change builds and deploys only web, leaving api untouched. Verify and e2e jobs may cancel superseded runs; deploy jobs use a non-cancellable concurrency group per (environment, lane) so two pushes never race the same target.
A separate deploy.yml (workflow_dispatch) may re-deploy an existing artifact or image for a validated ref, environment, and lane. It reuses the same environment, concurrency group, and provenance checks; it does not rebuild arbitrary input code.
Workflow
- Inspect the repo first: existing
.github/workflows/*, environment names, deploy scripts, infra entrypoints, release dashboards, and recent failing runs. Preserve a proven workflow contract only when it already uses environment gates, OIDC or scoped credentials, immutable artifacts, and post-deploy proof. - Confirm prerequisites: deploy branch, default-branch merge policy, target environments, environment protection rules, OIDC trust policy or scoped environment credentials, artifact/image retention, and smoke endpoint.
- Author
.github/workflows/main.ymland optionally.github/workflows/deploy.ymlper references/workflows.md. Keepchanges -> verify -> e2e -> deploy -> smoke; manual dispatch promotes existing verified artifacts/images. - Add the standard hardening stack before inventing bespoke checks:
actionlintfor syntax,zizmorfor GitHub Actions security, and scanner-backed gates for secret/security checks. See references/workflows.md. - Stand up change detection:
dorny/paths-filterpinned to a full SHA for simple per-app rules, or a graph-aware affected walker for monorepos. Output one boolean per deploy lane. - Wire trust boundaries via references/environments.md and references/secrets.md: GitHub Environments hold deploy-scoped secrets/vars, OIDC handles cloud identity where available, and repo-level secrets remain bootstrap-only.
- Keep deploy steps provider-thin. Prefer a repo-owned deploy layer such as SST, an IaC apply, release-environment promotion API, or small platform CLI wrapper only after credentials are loaded for the environment and the artifact provenance is checked.
- Set concurrency: cancellable for verify/e2e, non-cancellable for deploy. Group by
(environment, lane)so a staging web deploy does not block production web, but two production web deploys serialize. - Add a separate smoke job after deploy: hit the environment URL or health endpoint without cloud/deploy credentials and fail if it is not healthy. A green deploy that does not serve traffic is a failed deploy.
- Validate end-to-end: PR (verify only, no deploy) -> merge a lane-scoped change -> watch detect -> verify -> e2e -> deploy -> smoke -> summary. Confirm only the touched lane and intended environment ran.
- Cross-check references/troubleshooting.md when a deploy is stuck, racing, or shipping the wrong artifact.
Hardening Stack
- Run
actionlintfor workflow syntax and expression mistakes. - Run
zizmorfor GitHub Actions security before writing custom grep, awk, or TypeScript workflow validators. - Prefer scanner-backed gates (
zizmor,actionlint, gitleaks, TruffleHog, CodeQL, dependency scanners) over repo-specific guard scripts unless a scanner cannot express the risk. - Pin
zizmorcore/zizmor-actionby full SHA with a same-line version comment. Example:zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6. - For repos without GitHub Advanced Security, set
advanced-security: false,annotations: true, and block on at leastmin-severity: mediumplusmin-confidence: medium.
Minimal Anchor
deploy-web:
needs: [verify-web, e2e-web]
if: ${{ needs.verify-web.result == 'success' && needs.e2e-web.result == 'success' }}
environment:
name: production
url: ${{ steps.smoke.outputs.url }}
concurrency:
group: deploy-production-web
cancel-in-progress: false
permissions:
contents: read
id-token: write
env:
AWS_DEPLOY_ROLE_ARN: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
AWS_REGION: ${{ vars.AWS_REGION }}
steps:
- uses: actions/download-artifact@<full-sha> # v8.x.y
with: { name: web-dist, path: apps/web/dist }
- run: vp exec -- sst --config infra/web/sst.config.ts deploy --stage production
smoke-web:
needs: deploy-web
if: ${{ needs.deploy-web.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- run: ./scripts/assert-no-cloud-credentials
- run: ./scripts/smoke-web --environment production
Guardrails
- One artifact end-to-end: e2e and deploy both consume the artifact verify uploaded.
- Default branch merge policy is part of the deploy surface. If direct pushes to
mainmust remain allowed, prefer branch protection with onlyrequired_conversation_resolutionenabled. Use organization or repository rulesets for conversation resolution only when making default-branch changes go through pull requests is intended. Preserve existing approvals, status checks, signed-commit, and actor restrictions when changing branch policy. - Deploy credentials are environment-scoped. Prefer OIDC or short-lived federation; use static tokens only when the provider has no supported federation path.
- SST is a good default when the repo already uses it or wants app-owned infrastructure. Run it as the deploy layer behind the same Environment, OIDC, artifact provenance, concurrency, and separate no-credential smoke rules.
- For Cloudflare/SST deployments with static Cloudflare tokens, keep tokens on the GitHub Environment and split
stagingandproductionstages/projects/state so one deploy path cannot delete or mutate the other environment's resources. - Repo-level secrets are bootstrap-only. Runtime and production deploy secrets live on GitHub Environments or the provider's secret system.
- Deploy concurrency is non-cancellable per
(environment, lane)and shared betweenmain.ymlanddeploy.yml. - A deploy pipeline is not green until a separate read-only smoke job has hit the deployed environment.
- Secret-bearing manual deploy jobs validate inputs before checkout or credential loading, then deploy a previously built artifact or immutable image with known provenance.