← Guides
uBixCore · v0.47.0

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.

Synopsis
.gitlab-ci.yml
include:
  - project: ubixsys/ubixcore
    ref: v0.47.0
    file: ci/ubixcore-pipeline.yml

variables:
  UBIX_APPROVAL_OWNERS: "your-gitlab-username"
Why this exists

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.

Including it

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.

The flow
stages
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.

The jobs

<tier> is dev, staging or prod. Each tier's jobs exist only while that tier is in UBIX_TIERS.

StageJobWhat it does
buildbuild-<tier>, build-mrBuilds 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.
teststatic-checks-*phpcs and phpstan inside the test image. phpstan paths come from UBIX_PHPSTAN_PATHS.
testphpunit-*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.
testjs-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.
testai-review-mrMerge 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.
migratemigrate-verify-<tier>Read-only migrate:status --verify: checks that the checksums of applied migrations still match the files. Gates the deploy.
migratemigrate-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.
migratemigrate-apply-<tier>-destructiveManual, 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.
migratedrift-<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.
deploydeploy-<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.
promotepromote-to-staging, promote-to-prodAfter a green deploy, fast-forwards the next branch (dev to staging, staging to main). Refuses when it cannot check the batch for destructive migrations.
promotepromote-to-*-destructiveManual. The same promotion with the destructive migrations acknowledged.
notifynotify-failure-<tier>On a red tier pipeline, posts branch, commit, author and pipeline link to the project's alerts channel.
approvalrequire-approval-mrMerge 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_maintenanceA 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.

Variables

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.

VariableDefaultMeaning
UBIX_APPROVAL_OWNERSemptyRequired. GitLab usernames whose Approve or 👍 counts. require-approval-mr fails without it.
UBIX_TIERSdev,staging,prodTiers that get jobs. Drop one to drop its jobs.
UBIX_DEV_BRANCH, UBIX_STAGING_BRANCH, UBIX_PROD_BRANCHdev, staging, mainThe tier branches. master is also accepted for prod.
UBIX_PROD_DEPLOY_WHENmanualmanual or on_success.
UBIX_MIGRATE_ENABLEDtruefalse removes every migrate job, for an app with no database.
UBIX_DRIFT_ENABLEDfalsetrue adds drift-<tier>.
UBIX_BUILD_TAG, UBIX_DEPLOY_TAGshellRunner tags: jobs that build or run an image, and jobs that call kubectl.
UBIX_RUNTIME_DOCKERFILE_DEV, _STAGING, _PRODDockerfile, Dockerfile_Staging, Dockerfile_MainRuntime Dockerfile per tier.
UBIX_TEST_DOCKERFILE, UBIX_NODE_DOCKERFILE, UBIX_GO_DOCKERFILEDockerfile_Test, Dockerfile_Node, Dockerfile_GoThe test image, and the per-app JS and Go images (parameterised by APP_DIR).
UBIX_BUILD_PLATFORMSlinux/amd64,linux/arm64Platforms for tier builds.
UBIX_CACHE_IMAGEemptyA registry repository for build cache. Empty means no cache. A cache outage never fails a build.
UBIX_VAULT_PREFIXthe project nameSecrets are read from secret/<prefix>/… in uBixVault.
UBIX_STAGING_VAULTprodWhich uBixVault the staging branch reads: prod or dev.
UBIX_APP_DB_HOSTemptyHost part for the app's database grants. Empty means % on dev and a private-network pattern elsewhere.
UBIX_PHPSTAN_PATHSapp bin php public testsWhat phpstan analyses.
UBIX_JS_TEST_SKIPemptyapp/*Js names whose npm test cannot run in the plain node image.
UBIX_AI_REVIEW_GUIDEbin/ci/ai-review-guide.mdYour own review rules for ai-review-mr; ignored if the file is absent.
UBIX_CORE_PROJECT, UBIX_CORE_REFubixsys/ubixcore, emptyWhere ci/bin comes from and at which version. Empty means the version in composer.lock.
UBIX_DEPLOY_SCRIPT, UBIX_PROMOTE_SCRIPT, UBIX_NOTIFY_SCRIPTemptyA path in your repository to use instead of uBixCore's script.
Overriding, from least to most invasive
  1. Set a variable. Almost everything above is a one-line change.
  2. Change a job. Redefine it by name. Keys merge and script replaces: phpunit-dev: { variables: { MY_FLAG: "1" } }.
  3. Drop a job. ai-review-mr: { rules: [{ when: never }] }.
  4. Add a job. Extend a hidden base, which is public API. .ubix_static_checks, .ubix_phpunit, .ubix_migrate, .ubix_deploy and .ubix_maintenance are the job bodies; .ubix_on_dev, .ubix_on_staging, .ubix_on_prod and .ubix_on_mr pick the tier and its rules.
  5. Bring your own script. Point UBIX_DEPLOY_SCRIPT, UBIX_PROMOTE_SCRIPT or UBIX_NOTIFY_SCRIPT at your own. A host with no staging tier defines its own promote job.
Examples
minimal .gitlab-ci.yml
include:
  - project: ubixsys/ubixcore
    ref: v0.47.0   # ubixcore-ref
    file: ci/ubixcore-pipeline.yml

variables:
  UBIX_APPROVAL_OWNERS: "your-gitlab-username"
with overrides
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"'
What the host provides

The template holds no credentials. A first green pipeline needs:

  1. A runner carrying the shell tag (or your own tags), with Docker and a buildx builder, and cluster credentials for the deploy and promote jobs.
  2. Image files and manifests: the Dockerfiles named above, and per-app manifests named for the branch, such as app/<App>/dev-deploy.yaml.
  3. A uBixVault tree under secret/<prefix>/: test-db, db-<tier>, composer and discord. The jobs read it with read-only tokens held in the masked CI variables UBIXVAULT_CI_TOKEN_DEV and UBIXVAULT_CI_TOKEN_PROD. ci/bin/vault-set-key.sh writes a single key safely.
  4. A token for promote: GITLAB_PROMOTE_TOKEN, a project access token that can push the tier branches.
  5. A token for the sign-off: APPROVAL_GITLAB_TOKEN (or AI_REVIEW_GITLAB_TOKEN), Reporter with api, masked and not protected, since merge-request branches are unprotected.
  6. Optionally, GEMINI_API_KEY and AI_REVIEW_GITLAB_TOKEN for ai-review-mr, masked and not protected for the same reason.
  7. Access to uBixCore: your project on uBixCore's job-token allowlist, because the template clones ci/bin with 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.