From a merge request to an approved SecureTransport change.
This GitLab chain works without an AI assistant. Your teams describe the SecureTransport configuration in YAML under flows/, version it in GitLab, and a pipeline applies it after an authorized approver clicks. The helixflow-ci worker starts the local HelixFlow MCP itself, which remains the only write path to SecureTransport. This first version creates and modifies objects; it deletes none.
1. What the chain does
This site connects neither to your GitLab nor to your SecureTransport: the chain runs on your runners, with your secrets vault.
qualityoffline check of flows/, on the merge request<target>:planflow_plan, summary to review<target>:applymanual job, flow_apply<target>:verifyflow_diff, no driftOn your GitLab runner:
helixflow-ci· starts the MCPhelixflow· the only write path to SecureTransport- OpenBao · JWT authentication and ST credentials
Four jobs run in this order:
quality- On the merge request and on main: offline check of the files changed under flows/ (schema, modeling rules, governance, plain-text secrets), without a profile or a SecureTransport connection.
<target>:plan- After the merge on main: plans each file added or changed under flows/<target>/ with flow_plan and publishes a summary to review. A pipeline started by hand on main (Build > Pipelines > Run pipeline) plans every file of every target; unchanged files return noop.
<target>:apply- Manual job: an authorized approver clicks ▶ in GitLab to run it. That click is the approval; there is no other. Before calling flow_apply, helixflow-ci checks the job context, the approver’s current membership of the target’s group and the file fingerprints again.
<target>:verify- Compares the target with the repository after the apply, using flow_diff; any drift fails the job and lists the differing fields.
Each job publishes helixflow-report.json and helixflow-report.md as artifacts for 30 days, even when it fails. The report never contains a secret value.
2. Prerequisites
- A self-managed GitLab instance. The chain is qualified on GitLab CE 19.4.1; GitLab.com and Premium features are not covered by this first version.
- A Linux machine that can reach SecureTransport, with GitLab Runner using the shell executor: an unprotected quality runner without secrets (tag helixflow-quality), and one protected runner per target (tags helixflow-nonprod and helixflow-prod in the pipeline template). Only Ubuntu 24.04 LTS amd64 is qualified; the arm64 build is provided but not qualified.
- One dedicated system account per target on that machine (for example helixflow-prod): it runs the target’s runner and owns its cible.yaml and its HelixFlow configuration, state and license folders. No target account reads another one’s files.
- An OpenBao server, or an existing vault compatible with the Vault API, reachable from that machine.
- Two GitLab projects: the flows project, where teams version their files under flows/<target>/, and a separate administration project that holds the pipeline file.
- One GitLab approver group per target, and a GitLab service account whose read_api token, stored in OpenBao, reads membership of these groups.
3. Install the binaries on the runner machine
Download the HelixFlow Linux binary, check its SHA-256 hash against the published value, then install it at a fixed location that cible.yaml will point to. Downloads
sha256sum helixflow-linux-amd64
sudo install -d -m 0755 /opt/helixflow/bin
sudo install -m 0755 helixflow-linux-amd64 /opt/helixflow/bin/helixflowOn ARM64, replace amd64 with arm64 in the file names.
The license is bound to the machine, not to the account: request a single one for this machine, as in section 2 of this guide, from a session of one of the target accounts. 2. Activate the machine license
export HELIXFLOW_LICENSE_DIR=/var/lib/helixflow-prod/licenses
/opt/helixflow/bin/helixflow license request --output request.jsonAfter issuance, copy the same helixflow.lic to each target account (outside its license_dir, owned by that account), import it as that account (the import writes it into its license_dir), check it, then delete the copy. Repeat for each target (for example helixflow-nonprod):
sudo install -o helixflow-prod -g helixflow-prod -m 0600 helixflow.lic /var/lib/helixflow-prod/helixflow.lic
sudo -u helixflow-prod env HELIXFLOW_LICENSE_DIR=/var/lib/helixflow-prod/licenses /opt/helixflow/bin/helixflow license import /var/lib/helixflow-prod/helixflow.lic
sudo rm /var/lib/helixflow-prod/helixflow.lic
sudo -u helixflow-prod env HELIXFLOW_LICENSE_DIR=/var/lib/helixflow-prod/licenses /opt/helixflow/bin/helixflow license statushelixflow-ci is not published on the Downloads page: the publisher provides it on request, for Linux amd64 or arm64, with its SHA-256 hash and the pipeline.yml pipeline template. Ask for the version that matches the installed helixflow. Check the hash, then place it in a folder on the runner accounts’ PATH: the pipeline calls it by name. Request helixflow-ci
sha256sum helixflow-ci-linux-amd64
sudo install -m 0755 helixflow-ci-linux-amd64 /usr/local/bin/helixflow-cihelixflow-ci needs no license; the MCP it starts uses the target account’s license. helixflow-ci refuses an MCP that does not announce the expected capabilities: always update both together.
4. Configure GitLab
These settings are mandatory. “Checked by doctor” marks those that helixflow-ci doctor checks through the API (step 8 of this section); check the others in GitLab.
- Teams are Developer in the flows project’s group and may merge to main. The Maintainer role is reserved for administrators: a Maintainer can set CI variables (BASH_ENV, LD_PRELOAD…) that would run before the job script, in the account that obtains the secrets.
- Minimum role to override pipeline variables: no one. Checked by doctor.
- main protected: no direct push, merge through merge requests only. Checked by doctor.
- Settings > Merge requests: “Pipelines must succeed” checked. Without it, a merge request whose quality job fails can still be merged. Checked by doctor.
- Same page: “Skipped pipelines are considered successful” unchecked. Otherwise a merge request whose pipeline is skipped (a [skip ci] commit) merges without quality having run. Checked by doctor.
- Settings > CI/CD > CI/CD configuration file of the flows project: the pipeline file of the administration project (step 7 of this section). Checked by doctor.
- The administration project is shared with the flows project’s group at exactly the Reporter level. Below it, every pipeline fails with config_error and no message; above it, teams could act on the administration project. Checked by doctor when pipeline_config is set.
- One approver group per target; it is the group the target’s apply job checks at the click. Checked by doctor (the group exists).
- The service account of the read_api token is Guest of each approver group and absent from the flows group: it reads approver membership and runs no job.
- Target runners protected, locked to the flows project, with their dedicated tags and the shell executor; quality runner unprotected, with no access to a target’s files or OpenBao role. Not checked by doctor.
5. Prepare OpenBao
Each target job declares a GitLab OIDC token, HELIXFLOW_CI_ID_TOKEN, with the audience helixflow-ci. helixflow-ci exchanges it for a vault token valid for a few minutes, using OpenBao’s JWT authentication method. The quality job declares no token and never talks to the vault.
- JWT method
- Enabled at the auth_mount of cible.yaml (jwt-gitlab in the example) and configured to accept the tokens issued by your GitLab.
- Target role
- One role per target (role in cible.yaml), with bound_audiences: helixflow-ci and bound_claims that must all match: project_id of the flows project, ref: main, ref_type: branch, ref_protected: "true", environment equal to the target name, and ci_config_ref_uri. GitLab builds the latter from the flows project and its CI/CD configuration file setting, for example:
gitlab.example.test/teams/flows//pipeline.yml@helixflow-admin/admin@refs/heads/mainChanging the flows project’s CI/CD configuration file therefore closes vault access until the role is updated.
- Policy
- Read-only access to the target’s secrets, never to another target’s.
path "helixflow/data/prod/*" { capabilities = ["read"] }- KV v2 paths
- Under the kv_mount (helixflow in the example):
helixflow/<target>/st— SecureTransport credentials (fields login and password);helixflow/<target>/gitlab— read_api token of the service account (field token);helixflow/<target>/flows/— secrets referenced by flows (step 9 of this section).
Enter these secrets in OpenBao yourself. Never put a SecureTransport credential, a token or a password in the repository, in a GitLab CI/CD variable, in cible.yaml, on this site or in a conversation.
After a restart, OpenBao starts sealed: an operator must unseal it before the chain resumes. Until then, every target job fails and states that the vault is sealed.
6. Describe the target: cible.yaml
One file per target, owned by the target’s system account with mode 0600 (refused otherwise). It contains no secret, only references and absolute paths.
Default location, and the option that points to another one: /etc/helixflow-ci/<target>/cible.yaml · --config
target: prod
flows_dir: flows/prod
gitlab:
url: https://gitlab.example.test
ca_path: /etc/helixflow-ci/prod/gitlab-ca.pem
branch: main
flows_project_id: 42
approver_group_id: 7
pipeline_config: pipeline.yml@helixflow-admin/admin
helixflow:
binary: /opt/helixflow/bin/helixflow
profile: st-prod
config_dir: /var/lib/helixflow-prod/config
state_dir: /var/lib/helixflow-prod/state
license_dir: /var/lib/helixflow-prod/licenses
approval_window: 60m
openbao:
address: https://openbao.example.test:8200
ca_path: /etc/helixflow-ci/prod/openbao-ca.pem
auth_mount: jwt-gitlab
role: helixflow-prod
kv_mount: helixflowtarget·flows_dir- The target name and its folder in the flows project, which must be flows/<target name>. The name starts with a lowercase letter, followed by lowercase letters, digits or hyphens, 32 characters at most. It is the value passed to the option:
--target gitlab.url·gitlab.ca_path·gitlab.branch- The HTTPS address of your GitLab; ca_path (optional) points to the authority that signed its certificate; branch is the protected branch, main by default.
flows_project_id·approver_group_id- Numeric IDs of the flows project and of the target’s approver group.
pipeline_config- Optional: the expected value of the CI/CD configuration file setting. doctor compares it with the actual setting and derives the administration project from it.
helixflow.binary·helixflow.profile- Path of the helixflow binary installed in step 3 of this section and name of the target’s HelixFlow profile.
config_dir·state_dir·license_dir- HelixFlow configuration, state and license folders of the target account. Plan sets awaiting approval are kept under state_dir.
approval_window- Time allowed for approval after the plan: between 5 minutes and 24 hours, 60 minutes by default.
openbao- HTTPS address and authority of the vault, JWT authentication mount, target role and KV v2 mount of its secrets (step 5 of this section).
The target’s HelixFlow profile
The profile named by profile lives in the target account’s config_dir. The chain is strict on every target: class prod for production, preprod for a non-production target; a profile of any other class is refused at plan time. admin_actors names the target’s system account, and human_confirmation is false because the approval is the GitLab click.
base_url: https://st-prod.example.test:8444
env_class: prod
tls:
mode: ca
ca_path: /var/lib/helixflow-prod/config/ca/st-ca.pem
credentials:
source: env
admin:
production:
enabled: true
admin_actors: [helixflow-prod]
strict_stop: true
human_confirmation: false
write:
actions: [create, modify]
families: [account, application, certificate, route, site, subscription, transfer-profile, transfer-site]With credentials: source: env, helixflow-ci passes the credentials read from OpenBao to the MCP; they appear in no file.
7. Connect the pipeline
The flows project contains no .gitlab-ci.yml. The pipeline.yml template, provided on request with helixflow-ci, goes into the administration project, and teams do not change it. The flows project points to it through its CI/CD configuration file setting, in the form pipeline.yml@<admin group>/<admin project>:
pipeline.yml@helixflow-admin/adminThe template declares two targets, nonprod (folder flows/nonprod, class preprod) and prod (folder flows/prod, class prod). It only runs on a merge request, a push to main or a pipeline started by hand on main. These template lines carry the guarantees described below:
variables:
GIT_DEPTH: "0"
.target:
id_tokens:
HELIXFLOW_CI_ID_TOKEN:
aud: helixflow-ci
.prod:
extends: .target
tags: [helixflow-prod]
resource_group: helixflow-prod
variables: { HF_TARGET: prod }GIT_DEPTH: "0"requests the full history. With GitLab’s default limited depth, a comparison base that is too old (a push of several commits, a long merge request) would make quality and plan fail (code 2).resource_group: helixflow-<target>prevents two pipelines from writing to the same target at the same time.id_tokensgives each target job the OIDC token exchanged for an OpenBao token. The plan, apply and verify jobs also declare their target’s environment, which the role’s environment claim checks.
8. Check the installation with doctor
After each installation or change, run doctor outside any job, in a session of the target account. It only reads and writes no report.
helixflow-ci doctor --target prod --gitlab-token-file ./gitlab-read-tokenWithout a GitLab token, it checks:
- that the helixflow binary exists and the license is valid;
- that the MCP starts and announces the expected capabilities;
- that the profile is strict, with no human question, and that flow_apply is permitted there for the target account (a refusal is named by its policy_key);
- that OpenBao responds and is not sealed.
To also check the GitLab settings marked “checked by doctor” in step 4 of this section, add the option: --gitlab-token-file
The file, with mode 0600, holds a read_api token that must be able to read the flows project, the administration project and the approver group; the service account token stored in OpenBao is not enough, and the minimal GitLab role that suffices has not been measured. Delete the file after running doctor, or use a short-lived token and revoke it. Without this option, doctor says so and skips these checks.
Each line starts with OK, KO or -- (check not performed). A single KO makes doctor exit with code 1. doctor does not read the SecureTransport credentials from the vault: the first plan job tests them.
9. Day-to-day use
- Write or change a YAML flow document on a branch, directly in flows/<target>/ (no subfolder), with the .yaml or .yml extension, then open a merge request. Promoting to production means copying the file to flows/prod/ through a merge request.
- quality checks the merge request offline. If it fails, the merge request stays blocked and the report names the file, the field and the rule.
- After the merge on main, the target’s plan job publishes its summary: one line per object and action, the counters and the approval deadline. Read it before approving; “nothing to apply” means everything is noop.
- To approve, a member of the approver group clicks ▶ on the target’s apply job. The click approves the plan set of the latest successful plan job in that pipeline: re-running plan replaces it, so read the most recent report.
- The verify job then compares the target with the repository; any drift fails it and lists the differing fields.
A plan that is not approved expires at the deadline set by approval_window (24 hours at most). apply also refuses a plan that is stale or already consumed, or whose commit, a file or a secret has changed. In these cases nothing is written: re-run the plan job.
Reference a secret in a flow
A secret is never written in plain text in a flow. Put a ${secret:<path>#<field>} reference under a secret key (a name containing password, secret, token…); the path is relative to helixflow/<target>/flows/ in the vault.
partnerPassword: ${secret:pesit/partner-a#password}quality refuses a plain-text value under a secret key, and a reference under any other key. References are resolved at plan time and again at apply time, in memory: the rendered document is never written to disk, and no resolved value appears in a report or a log.
10. Troubleshoot the chain
- Code 0 · success
- Everything was applied, or the plan set is entirely noop.
- Code 1 · refusal or known failure
- For example: policy refused at plan time, non-strict profile, approver outside the group or inactive account, expired or stale plan, dangerous environment, MCP or vault unreachable. The message gives the cause; when it asks to re-run the plan job, nothing was written.
- Code 2 · invalid configuration
- cible.yaml unreadable, malformed, not 0600 or with a relative path; missing or malformed option; unreadable TLS authority; unreadable Git repository.
- Code 3 · uncertain outcome
- helixflow-ci does not know what was written to the target (MCP response lost, execution not confirmed). The MCP blocks the target (execution.pending_reconciliation) and nothing is replayed automatically.
- One file fails among several
- apply stops at the first failure; the report separates applied, failed and not-attempted files. Retry the plan job, then click apply in the same pipeline: files already applied return noop. A pipeline started by hand on main also works.
Reconcile after an uncertain outcome
After code 3, an operator, not the pipeline, reconciles the target:
- Note the execution_ref of the affected file in helixflow-report.md.
- Read execution_status on the MCP, with your usual assistant or a direct MCP client.
- Check the actual state in SecureTransport, then call execution_reconcile to unblock the target.
- Only then start a new pipeline to plan and apply.
Clicking apply again without this reconciliation hits the same blocked plan set. If execution_status shows no execution at all for that plan, nothing was written: re-run the plan job, then approve again.