Skip to content

Running in CI

Terminal window
oren generate gitlab entrega
oren generate github entrega
oren generate cloudbuild entrega

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

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_PROPERTIES
rules:
- 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.

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.

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.

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.

Terminal window
oren generate gitlab entrega --resume

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

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 .tar it 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 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.

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.

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.

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.

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:`.

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.