Pipeline Overview¶
The Twycis shared CI/CD setup builds container images on GitHub Actions and ships them to two long-lived environments — test and production — over an SSH connection tunnelled through Cloudflare. This page consolidates the trigger model, the shared quality gates, and the gating logic that protects each deploy job. Drill into the per-workflow pages for the full step-by-step walkthroughs.
Two environments only
This setup runs exactly two tiers: test (test.<your-domain>, deploy path /opt/app-name) and production (<your-domain>, deploy path /opt/app-name-prod). Any "acceptance" tier from older docs has been retired.
Trigger model¶
The environment you deploy to is decided entirely by how the pipeline is triggered:
| Environment | Trigger | Image tag | Deploy path | GitHub Environment |
|---|---|---|---|---|
| Test | Push or PR to main (plus workflow_dispatch) |
:${{ github.sha }} |
/opt/app-name |
test |
| Production | Push a v* tag (plus workflow_dispatch) |
:${{ github.ref_name }} (the tag) |
/opt/app-name-prod |
production |
Pull requests and pushes to main run the quality gates and (on push to main) deploy to test. Production deploys are deliberately gated behind a version tag — see Deploy to Production for how to cut a release.
Concurrency
The test workflow declares concurrency with cancel-in-progress: true, keyed on ${{ github.workflow }}-${{ github.ref }}. A newer push to the same branch cancels the in-flight run so the latest commit always wins.
Shared quality gates¶
Both workflows run the same family of gate jobs before any deploy job is allowed to start. Each gate is an independent job; the deploy job needs all of them.
| Gate | Job | What it enforces |
|---|---|---|
| SonarCloud Quality Gate | quality-gate / sonar |
Runs frontend + backend coverage, scans with SonarCloud, then verifies the Quality Gate status is OK for the exact commit SHA. Only runs when vars.SONAR_ENABLED == 'true'. See SonarCloud. |
| E2E results | e2e-gate |
Reads tests/e2e/.results.json, confirms the recorded commit is an ancestor of HEAD, that no untested code changed since, and that status == passed. |
| Security audit | security-audit |
Runs npm audit --audit-level=high on dependencies. |
| Architecture check | (deploy job step) | Runs node scripts/check-architecture.js to enforce module/layering rules before images are built. |
| Trivy scans | (deploy job steps) | Scans built images and the filesystem (scan-type: fs) for CRITICAL vulnerabilities; exit-code: 1 fails the deploy. |
Dependabot exception
On the test workflow, the SonarCloud and E2E gates skip when the PR actor is dependabot[bot], since Dependabot PRs cannot access the required secrets and do not carry fresh E2E results.
Deploy job gating¶
The deploy job uses a compound if: so it only runs when every gate passed (or was legitimately skipped) and the trigger is correct.
For test (deploy-test):
if: |
!cancelled() &&
(needs.quality-gate.result == 'success' || needs.quality-gate.result == 'skipped') &&
needs.e2e-gate.result == 'success' &&
needs.security-audit.result == 'success' &&
(github.event_name == 'push' || github.event_name == 'workflow_dispatch') &&
github.ref == 'refs/heads/main'
For production (deploy-production), the same gate logic applies but the trigger is the v* tag, so there is no github.ref == 'refs/heads/main' clause:
if: |
!cancelled() &&
(needs.sonar.result == 'success' || needs.sonar.result == 'skipped') &&
needs.e2e-gate.result == 'success' &&
needs.security-audit.result == 'success'
Note
The (... == 'success' || ... == 'skipped') pattern on the Sonar gate is what lets SONAR_ENABLED=false repos and Dependabot PRs proceed without weakening the gate when Sonar is enabled.
Pipeline diagram¶
flowchart TD
A[Push / PR to main] --> G
B[Push v* tag] --> G
subgraph G[Quality gates]
direction LR
S[SonarCloud gate]
E[E2E results gate]
SA[npm audit]
end
G --> D{All gates pass?}
D -- no --> X[Pipeline fails]
D -- yes --> ARCH[check-architecture.js]
ARCH --> BUILD[Build & push images to GHCR<br/>buildx + gha cache]
BUILD --> TRIVY[Trivy image + fs scans]
TRIVY --> SSH[Configure SSH via Cloudflare Tunnel]
SSH --> RSYNC[rsync nginx / scripts / docker-compose.yml]
RSYNC --> DEPLOY[Remote deploy.sh:<br/>pull / down / migrate / sync-manual / up --wait]
DEPLOY --> HC{Healthy?}
HC -- no --> ROLL[Tail logs + non-zero exit]
HC -- yes --> DONE[Deployment complete]
Where to go next¶
- Deploy to Test — the
deploy.ymlwalkthrough (push/PR tomain). - Deploy to Production — the
deploy-production.ymlwalkthrough (v*tags). - Docker & GHCR — the container and registry model.
- Rollback & Incidents — recovery runbook.
- Skills Check — the vendored-skills hard gate.
- Cloudflare Tunnel and the Secrets Matrix for the connection and credential details.