The CWA is in heavy development
The CWA is still in alpha and not ready for production - some code and implementations are likely to change. If you would like to try out the CWA, please enjoy what we have provided and feel free to provide feedback, or get involved on GitHub.
Deployment

CI/CD

The template's GitLab CI pipeline — Docker Buildx builds, PHPUnit + Behat tests, per-branch review apps, and staged Kubernetes deployments via Helm.

The template ships a complete .gitlab-ci.yml with eight stages. Every step delegates to shell functions in bin/devops/ — readable, modifiable scripts you own. There is no GitLab Auto DevOps magic here.

GitLab only. CI/CD support is currently GitLab CI only. GitHub Actions support is planned but not yet implemented. If you need GitHub Actions, the shell functions in bin/devops/ are reusable — you would need to write the workflow YAML that calls them.

Pipeline Overview

On every branch: build then test then spin up a review app on Kubernetes. When a branch merges to main: build and test run again, then the pipeline auto-deploys to staging, from where you manually promote through canary to production.

Stages

Build

Two parallel jobs — build api and build app — each run docker buildx and push to the GitLab Container Registry. Layer caching is handled with --cache-to / --cache-from against a dedicated cache image tag, so incremental builds are fast.

$CI_REGISTRY_IMAGE/php:$CI_COMMIT_REF_SLUG    ← FrankenPHP image (--target frankenphp_prod)
$CI_REGISTRY_IMAGE/app:$CI_COMMIT_REF_SLUG    ← Nuxt image       (--target prod)

The PHP image build uses api/Dockerfile; the Nuxt image uses app/Dockerfile. Build logic lives in build_api() and build_app() in bin/devops/k8s.sh.

Test

Two parallel jobs against the image built in the previous stage:

JobWhat runs
unit testssimple-phpunit tests/Unit — fast, no database
behat testsBehat feature suite with a live PostgreSQL service; migrations run before the suite

Both export JUnit XML for GitLab's test report UI.

Review Apps

Every branch (except main) gets its own Kubernetes namespace and Helm release. The URL follows the pattern https://$CI_COMMIT_REF_SLUG-review.$KUBE_INGRESS_BASE_DOMAIN.

Namespaces must be pre-created with the appropriate RBAC — the pipeline's runner permissions don't include namespace creation. See the comment in ensure_namespace().

Review apps are torn down manually via the stop review job (triggered from the GitLab environment list), or automatically when the branch is deleted.

Staging

Runs automatically on every merge to main (controlled by $STAGING_ENABLED, default "true"). Calls deploy staging which runs helm upgrade --install in the staging namespace.

Canary

A manual job that runs deploy canary. Useful for routing a portion of production traffic to the new image before full rollout. Enable/disable via $CANARY_ENABLED.

Production

A manual job requiring explicit trigger from GitLab. Calls deploy (stable track), then delete canary and delete staging to clean up the intermediate environments.

Performance (optional)

A manual job that runs sitespeed.io against the production environment URL. Reads URLs from .gitlab-urls.txt if present, otherwise tests the environment root. Results upload as GitLab artifacts.


The Devops Scripts

All CI logic lives in two scripts sourced at the top of every job:

bin/devops/setup.sh

Sets shared environment variables:

  • CI_APPLICATION_REPOSITORY / CI_APPLICATION_TAG — image repo and SHA tag
  • PHP_REPOSITORY / APP_REPOSITORY — full image paths
  • PHP_REPOSITORY_CACHE / APP_REPOSITORY_CACHE — layer cache image paths
  • DOMAIN — derived from CI_ENVIRONMENT_URL
  • DEPLOYMENT_BRANCH — defaults to main

bin/devops/k8s.sh

Contains all the functions the pipeline calls:

FunctionWhat it does
install_dependenciesInstalls Helm, kubectl, curl, and other tools on the Alpine CI runner
generate_jwt_keysGenerates RSA key pair and Mercure JWT secret if not set as CI variables
setup_docker_environmentHandles Docker-in-Docker host config for Kubernetes runners
build_api / build_appdocker buildx build --push with registry layer caching
run_test_phpunitRuns PHPUnit unit tests; outputs JUnit XML
run_test_behatConfigures test DB, runs Behat; outputs JUnit XML
helm_initUpdates and builds Helm chart dependencies
ensure_namespaceVerifies the K8s namespace exists (fails fast if not)
create_docker_pull_secretCreates an imagePullSecret for the GitLab registry
deploy [track]Generates values.tmp.yaml from CI variables and runs helm upgrade --install
delete [track]Runs helm uninstall for review/canary/staging cleanup

Required CI/CD Variables

Set these in Settings → CI/CD → Variables in GitLab. Variables marked auto-generated are created by generate_jwt_keys() at pipeline start if not set — useful for review apps, but for production you should pin them as CI secrets so they don't rotate on every deploy.

Kubernetes

VariableRequiredNotes
KUBE_CONTEXTYesGitLab agent context, e.g. my-group/my-project:my-agent
KUBE_NAMESPACEYesPre-created namespace for this environment
KUBE_INGRESS_BASE_DOMAINYesBase domain for ingress URLs, e.g. k8s.example.com
CI_ENVIRONMENT_URLYesFull URL of this environment (GitLab sets this for named environments)
CLUSTER_ISSUERNocert-manager ClusterIssuer name (default: letsencrypt-staging)
KUBE_INGRESS_ALIAS_DOMAINSNoComma-separated extra domains to alias in ingress
INGRESS_ENABLEDNoSet "true" to enable the ingress resource (default: "false")

JWT & Mercure

VariableNotes
JWT_PASSPHRASEAuto-generated if not set
JWT_SECRET_KEYAuto-generated if not set
JWT_PUBLIC_KEYDerived from secret key if not set
MERCURE_JWT_SECRETAuto-generated if not set

For production, generate these once and save them as protected, masked CI variables. Otherwise they regenerate on every deploy, invalidating all active user sessions.

Application

VariableDefaultNotes
CORS_ALLOW_ORIGINRegex, e.g. ^https?://(.*\.)?example\.com
TRUSTED_HOSTSSymfony trusted hosts regex
ADMIN_USERNAMEadminInitial admin account username
ADMIN_PASSWORDadminInitial admin account password — change this
ADMIN_EMAILhello@cwa.rocksInitial admin account email
MAILER_DSNSMTP or SES DSN for transactional email
MAILER_EMAILFrom address for outgoing email

Database

VariableDefaultNotes
POSTGRESQL_ENABLEDtrueSet "false" to use an external DB (disables the bundled Postgres pod)
DATABASE_URLConnection string when using external Postgres
DATABASE_SSL_MODEpreferdisable, prefer, require, verify-full
DATABASE_CA_CERTCA cert PEM for SSL verification

Autoscaling

VariableDefault
REPLICA_COUNT1
AUTOSCALEtrue
AUTOSCALE_MIN1
AUTOSCALE_MAX3
AUTOSCALE_CPU_PERCENT90
AUTOSCALE_MEMORY_PERCENT90

Pipeline Flags

VariableDefaultEffect
BUILD_DISABLEDfalseSkip the build stage entirely
TEST_DISABLEDfalseSkip the test stage
STAGING_ENABLEDtrueAuto-deploy staging on merge to main
CANARY_ENABLEDtrueShow the canary job
REVIEW_DISABLEDSet "true" to disable review apps on branches
PERFORMANCE_DISABLEDfalseSkip the sitespeed.io performance job

Rollback

# List Helm history
helm history cwa --namespace production

# Roll back to a specific revision
helm rollback cwa 3 --namespace production

# Roll back to the previous release
helm rollback cwa --namespace production

The previous image tag (the git SHA) is baked into the Helm release history, so Helm's rollback restores both the configuration and the image version in one command.