Pipeline variables (environment-configuration)
Audience: operators configuring per-branch CI/CD variables for a repo via
Pipelines → Environment configuration
(/<owner>/<repo>/pipelines/environment-configuration).
Per-branch pipeline variables let you inject values into pipeline jobs without
hardcoding them in the committed vetrix-ci.yml — e.g. an environment-specific
base URL, a token, or any value that must differ between branches/stacks. Values
are stored encrypted (AES-256-GCM) in the branch-tier secrets store and are
masked from job logs.
Prerequisite: the worker must be able to inject them
A worker only injects these variables when it is wired with the branch-variable resolver AND has the decryption key. Until both are true, variables saved on the environment-configuration page are stored correctly but never reach jobs — jobs run with the committed
vetrix-ci.ymlvalues only.
Two requirements on every worker that should inject pipeline variables:
- Resolver wiring — the worker process resolves and overlays per-branch variables onto each job. (Tracked under Epic Pipeline variables via environment-configuration; verify your worker build includes it.)
SECRET_ENC_KEY— see below.
A worker missing SECRET_ENC_KEY logs a clear line at startup
(branch pipeline variables disabled: SECRET_ENC_KEY not set) and runs normally
with committed-YAML values only. This is intentional fail-open behaviour: a missing
key must never stop the worker from running jobs.
SECRET_ENC_KEY on the worker
The worker decrypts pipeline variables itself, so it needs the same
SECRET_ENC_KEY the server uses to seal them (a 64-character hex string = 32
bytes). It must match the server's key, or decryption fails.
# worker service (e.g. appmerc_vetrixworker) in the deployment compose
environment:
- SECRET_ENC_KEY=${SECRET_ENC_KEY} # same value as the server; env-only
- Env-only. Never put
SECRET_ENC_KEY(orDATABASE_URL,JWT_SECRET) in a committed config file. - Trust implication. Giving the worker this key lets it decrypt any secret
sealed with it, not just pipeline-variable values. The worker is already a trusted
node (it holds
JWT_SECRETand drives the host Docker socket), so this is consistent — but treat the worker host as in-scope for secret material and restrict access accordingly.
Setting a variable
Web UI
Pipelines → Environment configuration → Add variable. Provide the branch, the name (POSIX env-var shape: a leading letter/underscore, then letters/digits/underscores), and the value. Values are write-only — the UI never displays a stored value, ciphertext, or nonce again; rotate to change it.
API
# Create (ci:write). Body carries `value`; no response ever echoes it back.
curl -X POST "https://api.gitvetrix.com/api/v1/repos/<owner>/<repo>/pipelines/variables" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "X-Vetrix-Scope: api.write" \
-d '{"branch":"develop","name":"E2E_BASE_URL","value":"https://www.gitvetrix.com"}'
# List metadata for a branch (ci:read) — names only, never values.
curl "https://api.gitvetrix.com/api/v1/repos/<owner>/<repo>/pipelines/variables?branch=develop" \
-H "Authorization: Bearer <token>"
PUT /pipelines/variables/{id} rotates a value; DELETE /pipelines/variables/{id}
removes one. Values are capped at 64 KiB; names must match the env-var pattern or the
request is rejected with 400.
Referencing the variable from vetrix-ci.yml
Reference it as a shell-style $NAME in a job's variables: block. A per-branch
variable overrides a committed-YAML value of the same name:
a11y-gate:
variables:
E2E_BASE_URL: "$E2E_BASE_URL" # resolved from the env-config value for the branch
Resolution precedence (lowest → highest): worker-host process env → committed
vetrix-ci.yml job variables → per-branch environment-configuration variable. So a
branch variable wins on a name clash.
Security behaviour
- Fork-PR exclusion. Per-branch (and environment) variables are never materialised for fork-originated PR runs. A fork PR sees only the committed YAML.
- Always masked in logs. Every per-branch variable value is redacted from job
logs, regardless of any
secret:flag. This covers genuine secrets — but note it also masks non-secret values: e.g. anE2E_BASE_URLwill appear as***in the job log (waiting for stack at ***/privacy). That is expected. - Audited. Each successful decrypt during a job emits a
secret.accessedaudit event scoped to the variable, so you can answer "which runs read variable X". - Encrypted at rest. Values are sealed with AES-256-GCM under
SECRET_ENC_KEY; the store never returns plaintext, ciphertext, or nonce over the API.
Worked example: per-environment base URL for the e2e/a11y jobs
The live-stack e2e/a11y jobs (a11y-gate, web-a11y-e2e,
repositories-landing-responsive, security-scannerfix-cross-browser,
slow-chromium-e2e) must target a different stack per environment. Instead of
hardcoding https://www.gitvetrix.test in vetrix-ci.yml, reference
$E2E_BASE_URL/$A11Y_BASE_URL and set the value per branch:
| Stack | Branch(es) | E2E_BASE_URL |
|---|---|---|
| prod | develop/master/staging |
https://www.gitvetrix.com |
| dev | develop/master/staging |
https://www.gitvetrix.test |
Set these on the worker/instance that serves each stack. A prod worker reads the prod repo's env-config values; a dev worker reads the dev repo's.
Reachability caveat: the job container must be able to reach the URL. The dev
.test host only routes inside the dev bridge network; gitvetrix.com is public.
Confirm a prod job container can reach https://www.gitvetrix.com/privacy (the
bootstrap health path) before relying on it — if it cannot, the job-container egress
needs network work independent of this variable.
Verification
# 1. Worker has the key and wired the resolver:
docker compose logs appmerc_vetrixworker | grep -i "branch pipeline variables"
# (absence of the "disabled: SECRET_ENC_KEY not set" line + a normal boot = wired)
# 2. Set a branch variable, then trigger a pipeline on that branch and confirm the
# job behaves as if the value were present (the value itself is masked as *** in logs).
See also: runners.md (runner deployment) and
worker-docker-socket.md (worker Docker-socket access).