One pipeline for every app, pinned to a release.
uBixCore ships its GitLab pipeline as a template. A host project includes it at a release tag and keeps only what is specific to itself. This page covers what the template brings, what each job does, the variables that tune it, and how to change or drop a job.
include: - project: ubixsys/ubixcore ref: v0.47.0 file: ci/ubixcore-pipeline.yml variables: UBIX_APPROVAL_OWNERS: "your-gitlab-username"
Every uBixCore app needs the same pipeline: build the images, run the checks, migrate the database, deploy per environment, promote the branch, and say so when it fails. Copied into each repository, that pipeline drifts, and a fix has to be made again in every copy.
So the pipeline lives in uBixCore, in ci/ubixcore-pipeline.yml. The deploy, promote and notify scripts live there too, in ci/bin. A host repository includes the template and states only its differences. Upgrading is a version bump, not a merge.
Pin ref to a release tag, never main. Keep it in step with the ubixsys/ubixcore version in composer.lock: the template also fetches ci/bin at that locked version, so the scripts always match the framework the image was built with. The skeleton marks the line ubixcore-ref so a tool can find it.
To upgrade, run composer update ubixsys/ubixcore, bump ref, and commit both.
The shared scripts ship from 0.47.0. On an older locked version the jobs stop with a message telling you to update, or to point UBIX_DEPLOY_SCRIPT, UBIX_PROMOTE_SCRIPT and UBIX_NOTIFY_SCRIPT at your own.
Pipelines run for merge requests, for schedules (maintenance jobs only) and for pushes to the dev, staging and production branches. A push to a feature branch runs nothing: its checks belong to the merge request.
build → test → migrate → deploy → promote → notify approval (merge requests) maintenance (schedules) dev push build → test → migrate → deploy-dev → promote-to-staging staging push build → test → migrate → deploy-staging → promote-to-prod main push build → test → migrate → deploy-prod (manual by default) merge request build-mr → static-checks-mr, phpunit-mr, ai-review-mr → require-approval-mr
Promotion is a fast-forward of the next branch, so what reached production is the commit that went green on the tiers before it.
<tier> is dev, staging or prod. Each tier's jobs exist only while that tier is in UBIX_TIERS.
| Stage | Job | What it does |
|---|---|---|
| build | build-<tier>, build-mr | Builds the runtime image (tagged by branch and by commit) and a -test image layered on it. For each app/*Js and app/*Go directory it builds a runtime image and a -ci image for the checks. Composer credentials go in as a BuildKit secret, never a layer. The merge-request build is amd64 only; the tier builds use UBIX_BUILD_PLATFORMS. Build jobs also retry script failures, since a registry blip looks like one. |
| test | static-checks-* | phpcs and phpstan inside the test image. phpstan paths come from UBIX_PHPSTAN_PATHS. |
| test | phpunit-* | PHPUnit against the unit-test database, whose credentials are read from uBixVault. The schema is rebuilt from the baseline and migrations on every run. phpunit-mr runs the suite only: no vault read, because the vault token is protected and absent on an unprotected feature branch. |
| test | js-lint-*, go-checks-* | Per-app gates in the -ci images: npm run lint, npm run check and npm test for JS (apps named in UBIX_JS_TEST_SKIP skip the tests); golangci-lint and go test for Go. They exist only where the repository has such an app. |
| test | ai-review-mr | Merge requests only. An advisory LLM review through ci:aiReview, posted as one merge-request note. It never blocks (allow_failure), and with no keys set it skips green. Host rules are read from UBIX_AI_REVIEW_GUIDE if the file exists. |
| migrate | migrate-verify-<tier> | Read-only migrate:status --verify: checks that the checksums of applied migrations still match the files. Gates the deploy. |
| migrate | migrate-apply-<tier> | Applies pending migrations before the deploy. It first cross-checks that the database host the migration reads from the vault is the host the app's manifests give the pods, and refuses if they differ or none is found. On staging and prod a pending migration marked Destructive: holds: the job fails, the deploy does not run, and the ops channel is told. migrate-apply-prod is manual. |
| migrate | migrate-apply-<tier>-destructive | Manual, staging and prod. The same run with the destructive acknowledgement; the framework snapshots the affected tables first. Afterwards, retry migrate-apply-<tier> to unblock the deploy. |
| migrate | drift-<tier> | Off by default (UBIX_DRIFT_ENABLED). Replays every migration into scratch schemas on the unit-test server and diffs the result against the live tier. Extra objects in live are drift; missing ones mean the tier is behind. |
| deploy | deploy-<tier> | Runs ci/bin/deploy.sh, which applies the app's per-tier manifests (including -cronjob*.yaml) and waits for each rollout. It needs the build, the migrate jobs and the test jobs, so a held migration or a red check stops it. Production is manual by default and always uses the protected production environment. |
| promote | promote-to-staging, promote-to-prod | After a green deploy, fast-forwards the next branch (dev to staging, staging to main). Refuses when it cannot check the batch for destructive migrations. |
| promote | promote-to-*-destructive | Manual. The same promotion with the destructive migrations acknowledged. |
| notify | notify-failure-<tier> | On a red tier pipeline, posts branch, commit, author and pipeline link to the project's alerts channel. |
| approval | require-approval-mr | Merge requests only. Fails until an owner has Approved or given a 👍, which with Pipelines must succeed on keeps the merge button blocked. It is in its own late stage, so a red verdict neither hides nor delays the real checks. GitLab does not re-run a pipeline on approval, so approve and then retry this job; the sign-off guide covers the gap and the webhook that closes it. |
| maintenance | .ubix_maintenance | A hidden base, not a job. A schedule carrying MAINTENANCE_TASK runs cron:<task> in the image the tier is deployed from, with the database's DDL account. You define the concrete job. |
Every job retries twice on infrastructure failures (runner, timeout, API) so a hiccup never needs a click; a genuine failure stays red. Jobs are interruptible so a superseded pipeline frees its runner, except migrate, deploy, promote and notify: half a deploy is worse than a slow one.
Everything a host tunes is a UBIX_* variable, declared with its default at the top of the template. Set it in your variables: or in the project's CI/CD settings; project settings win.
| Variable | Default | Meaning |
|---|---|---|
| UBIX_APPROVAL_OWNERS | empty | Required. GitLab usernames whose Approve or 👍 counts. require-approval-mr fails without it. |
| UBIX_TIERS | dev,staging,prod | Tiers that get jobs. Drop one to drop its jobs. |
| UBIX_DEV_BRANCH, UBIX_STAGING_BRANCH, UBIX_PROD_BRANCH | dev, staging, main | The tier branches. master is also accepted for prod. |
| UBIX_PROD_DEPLOY_WHEN | manual | manual or on_success. |
| UBIX_MIGRATE_ENABLED | true | false removes every migrate job, for an app with no database. |
| UBIX_DRIFT_ENABLED | false | true adds drift-<tier>. |
| UBIX_BUILD_TAG, UBIX_DEPLOY_TAG | shell | Runner tags: jobs that build or run an image, and jobs that call kubectl. |
| UBIX_RUNTIME_DOCKERFILE_DEV, _STAGING, _PROD | Dockerfile, Dockerfile_Staging, Dockerfile_Main | Runtime Dockerfile per tier. |
| UBIX_TEST_DOCKERFILE, UBIX_NODE_DOCKERFILE, UBIX_GO_DOCKERFILE | Dockerfile_Test, Dockerfile_Node, Dockerfile_Go | The test image, and the per-app JS and Go images (parameterised by APP_DIR). |
| UBIX_BUILD_PLATFORMS | linux/amd64,linux/arm64 | Platforms for tier builds. |
| UBIX_CACHE_IMAGE | empty | A registry repository for build cache. Empty means no cache. A cache outage never fails a build. |
| UBIX_VAULT_PREFIX | the project name | Secrets are read from secret/<prefix>/… in uBixVault. |
| UBIX_STAGING_VAULT | prod | Which uBixVault the staging branch reads: prod or dev. |
| UBIX_APP_DB_HOST | empty | Host part for the app's database grants. Empty means % on dev and a private-network pattern elsewhere. |
| UBIX_PHPSTAN_PATHS | app bin php public tests | What phpstan analyses. |
| UBIX_JS_TEST_SKIP | empty | app/*Js names whose npm test cannot run in the plain node image. |
| UBIX_AI_REVIEW_GUIDE | bin/ci/ai-review-guide.md | Your own review rules for ai-review-mr; ignored if the file is absent. |
| UBIX_CORE_PROJECT, UBIX_CORE_REF | ubixsys/ubixcore, empty | Where ci/bin comes from and at which version. Empty means the version in composer.lock. |
| UBIX_DEPLOY_SCRIPT, UBIX_PROMOTE_SCRIPT, UBIX_NOTIFY_SCRIPT | empty | A path in your repository to use instead of uBixCore's script. |
- Set a variable. Almost everything above is a one-line change.
- Change a job. Redefine it by name. Keys merge and
scriptreplaces:phpunit-dev: { variables: { MY_FLAG: "1" } }. - Drop a job.
ai-review-mr: { rules: [{ when: never }] }. - Add a job. Extend a hidden base, which is public API.
.ubix_static_checks,.ubix_phpunit,.ubix_migrate,.ubix_deployand.ubix_maintenanceare the job bodies;.ubix_on_dev,.ubix_on_staging,.ubix_on_prodand.ubix_on_mrpick the tier and its rules. - Bring your own script. Point
UBIX_DEPLOY_SCRIPT,UBIX_PROMOTE_SCRIPTorUBIX_NOTIFY_SCRIPTat your own. A host with no staging tier defines its own promote job.
include: - project: ubixsys/ubixcore ref: v0.47.0 # ubixcore-ref file: ci/ubixcore-pipeline.yml variables: UBIX_APPROVAL_OWNERS: "your-gitlab-username"
variables: UBIX_APPROVAL_OWNERS: "your-gitlab-username" UBIX_TIERS: "dev,prod" # no staging UBIX_MIGRATE_ENABLED: "false" # no database UBIX_PROD_DEPLOY_WHEN: on_success # drop a job ai-review-mr: rules: [{ when: never }] # add a job from a hidden base extra-static-checks: extends: [.ubix_static_checks, .ubix_on_dev] needs: [build-dev] # a scheduled maintenance task purge: extends: .ubix_maintenance rules: - if: '$CI_PIPELINE_SOURCE == "schedule" && $MAINTENANCE_TASK == "purge"'
The template holds no credentials. A first green pipeline needs:
- A runner carrying the
shelltag (or your own tags), with Docker and a buildx builder, and cluster credentials for the deploy and promote jobs. - Image files and manifests: the Dockerfiles named above, and per-app manifests named for the branch, such as
app/<App>/dev-deploy.yaml. - A uBixVault tree under
secret/<prefix>/:test-db,db-<tier>,composeranddiscord. The jobs read it with read-only tokens held in the masked CI variablesUBIXVAULT_CI_TOKEN_DEVandUBIXVAULT_CI_TOKEN_PROD.ci/bin/vault-set-key.shwrites a single key safely. - A token for promote:
GITLAB_PROMOTE_TOKEN, a project access token that can push the tier branches. - A token for the sign-off:
APPROVAL_GITLAB_TOKEN(orAI_REVIEW_GITLAB_TOKEN), Reporter withapi, masked and not protected, since merge-request branches are unprotected. - Optionally,
GEMINI_API_KEYandAI_REVIEW_GITLAB_TOKENforai-review-mr, masked and not protected for the same reason. - Access to uBixCore: your project on uBixCore's job-token allowlist, because the template clones
ci/binwith the job token.
The skeleton's docs/ci-setup.md lists each of these in the order to create them, and bin/vault-ci-setup.sh creates the vault policy, the secrets and the CI token in one go.
Written against uBixCore v0.47.0. The variable list is read from ci/ubixcore-pipeline.yml at that tag; if you pin another, check the top of that file.