Running in CI
oren generate gitlab entregaoren generate github entregaoren generate cloudbuild entregaThe same pipeline, the same contracts, the same images pinned by digest — run in CI.
Without --output, each platform gets the file where it expects to find it.
With --properties <alias> you generate a single environment, and with no
destination the output goes to stdout — pipe it if you like.
One environment per branch
Section titled “One environment per branch”When the pipeline has propertyFiles, oren.yaml can say which branch uses
each alias:
propertyFiles: main: file: deploy/oren-properties/main.yaml branches: [main] develop: file: deploy/oren-properties/develop.yaml branches: [develop, 'feature/*']The short form (main: deploy/main.yaml) still works. Anyone who never
generates per-branch CI never needs the long one.
With the map declared, oren generate produces everything at once — but the
shape differs per platform, and not by our choice:
| Platform | Output | Why |
|---|---|---|
| GitLab | .gitlab-ci.yml, one file with rules |
the config file path is a project setting, not a branch one |
| GitHub | .github/workflows/oren-<pipeline>.yml, one file |
each workflow declares its own triggers |
| Cloud Build | cloudbuild-<alias>.yaml, one per alias |
the trigger is what names the file |
On GitLab this becomes one rule per branch, setting the alias:
script: - oren run entrega --properties $OREN_PROPERTIESrules: - if: $CI_COMMIT_BRANCH == "main" variables: { OREN_PROPERTIES: main }The map serves only the generator. oren run --properties stays an explicit
choice by whoever runs it: it never reads git to guess the branch, or the same
command would start meaning different things depending on where it is typed.
Why the map lives in oren.yaml
Section titled “Why the map lives in oren.yaml”Because the properties file has no schema — it is free on purpose, and you
put whatever keys you want in it. A reserved branch: field inside it would
create a reserved vocabulary, and “can I have a property called branch?” would
start having a bad answer.
Two refusals
Section titled “Two refusals”The same branch in two aliases is an error, not a tie-break. Both conditions would be true, the platform would take the first, and the deploy would go out with the wrong environment’s properties — silently, which is what makes it the worst outcome.
--mode native cannot do this. There the set of jobs is decided at
generation time and changes with the alias: steps with enabled by expression
are in or out. A single file cannot serve two branches that produce different
jobs.
GitHub Actions
Section titled “GitHub Actions”The job does not use container:. GitHub knows how to run a job inside an
image, but to do so it injects its own runner into it: that needs Node and
glibc. A lean image — like the alpine ones of the reference workers — breaks,
and breaks with an error that does not connect to the cause.
Instead, the job runs an explicit docker run. The image is executed the way
oren run would execute it: only what it declares it needs, nothing injected.
Secrets arrive through the repository secrets, mapped into env in the YAML —
secrets is not indexable by a variable name inside run, so that mapping has
to exist, and it is what makes explicit what needs configuring.
Without the branch map, the generated workflow only triggers on
workflow_dispatch. A generator has no way of guessing on which event your
pipeline should run, and assuming push would make the first generation fire a
deploy nobody asked for — add the trigger you want.
With the map, push is declared with exactly the mapped branches, because you
said what they are. A step before oren run picks the alias from the branch,
and a branch outside the map fails instead of running with no properties —
which would deploy with the wrong values.
Resuming a run that failed
Section titled “Resuming a run that failed”oren generate gitlab entrega --resumeThe job starts running with --resume, and the platform caches what the
previous run produced. On a retry, steps that had already succeeded are skipped.
Off by default, for three reasons: the cache costs transfer on every run for a benefit that only shows up when something fails; turning it on widens the trust boundary (see below); and it is refused for common pipelines, so a default would make generation fail for people who never asked.
What is cached
Section titled “What is cached”Two sets, and the second is the one usually forgotten:
.oren/runs/— each step’s output and status, which is how resuming knows what it may skip;- the mutable directories declared in the pipeline. The state holds the
output as JSON, but what the step wrote to disk is not in it: resuming past a
build without the
.tarit wrote would leave the next step looking for a file that is not there.
.oren/registry/ and .oren/consent.json never go in. Both are versioned
and come from the checkout.
The key is the commit
Section titled “The key is the commit”The state describes what ran over a specific piece of code, and resuming refuses to cross commits:
error The previous run is from a different commit (062fc199) Resuming would reuse output from steps that ran over other code — an already-built image would be published under the new version, with nothing showing the swap.The check does not depend on the CI file being keyed correctly: it lives in the
CLI. Without git, see OREN_REVISION in CLI commands.
Resuming with no previous run is not an error — the first run in CI never has state. It warns and runs everything.
Two refusals
Section titled “Two refusals”A step that writes to the project root. Caching the root would restore
.git, .oren/registry/ and .oren/consent.json from another run — and then
the cache would decide which worker runs and would claim the access was
authorised. Point the mutable dependency at a subdirectory, or generate without
--resume.
Cloud Build. It has no native cache, and steps stop at the first failure — the step that would save the state would not run exactly when the run failed, which is the only time resuming matters.
What the generator handles
Section titled “What the generator handles”Output passing. On GitLab, each job writes .oren-outputs/<id>.json as an
artifact and the later ones receive it via needs. An artifact rather than
reports:dotenv because outputs are structured JSON, and dotenv only carries
string pairs.
Dependencies. Credentials become project variables, with a check that fails early if they are not set. The value never appears in the YAML.
Mutable directories. They become artifacts between jobs, since runners do not share disk.
Requires oren install
Section titled “Requires oren install”Without pinned images, the generated configuration would run whatever the tag points at on any given day — losing exactly the reproducibility that justifies generating it.
Local implementations are refused
Section titled “Local implementations are refused”error Cannot generate CI: some steps use a locally built implementation A CI job references a published image — the runner does not have the worker directory. Publish the image and swap `build:` for `image:`.Limitations
Section titled “Limitations”The worker image needs jq when an input comes from another step — that is what
composes input.json inside the job, including mixed-text interpolation
(v${outputs['x'].y}-rc), which is supported.
${properties.x} is supported, and resolved at generation time — the value
is baked into the job, because a properties file does not exist on the runner.
That is why --properties is required when the pipeline has a conditional step:
the generated CI has a fixed set of jobs, and there is nothing there to evaluate
the expression later.
What the CLI does and the generated CI does not
Section titled “What the CLI does and the generated CI does not”The generator is a compiler, not a runtime: the job runs the worker image
directly, and the runner needs neither Node, nor the oren binary, nor the
portal. That is the point — but three things live in the CLI and do not come
along.
The consent gate. It is the CLI asking you. A generated job runs what the
file says. This is why .oren/consent.json is worth committing: in CI it is the
record that someone authorised the access, not a control that runs.
The cache. Oren’s cache is the CLI’s. In CI, whatever the platform offers.
Output validation against the contract. Locally, an output outside the
contract fails the step even with exit code zero. In the generated job the
output is copied as it came: a missing output.json still fails the step,
because the copy fails — but an output with a wrong type or an undeclared field
goes through.
That is a real difference, and the one to keep in mind: the contract is checked
at oren validate and at oren run, and it is those two that hold the line. A
worker whose output only drifts in CI drifts silently.