Pipeline templates
When the same sequence of steps shows up in a third project, copying it a third time stops being reasonable. A template is a named block of steps with parameters:
steps: - use: techlite/imagem@^1.0.0 id: web with: registry: us-central1-docker.pkg.dev/acme/apps nome: acme-web versao: ${outputs['versao'].version} credential: ../service-account.jsonIt is expanded, not executed
Section titled “It is expanded, not executed”A template is writing sugar. It disappears before anything looks at the
pipeline: by the time the resolver, the cache, the consent gate, oren.lock and
the CI generator see your file, there is no use: anywhere — only the steps you
would have typed by hand.
This is the whole design decision, and the alternative was tempting: a composite task, one step that runs several inside it. That would have been less code here and much worse everywhere else, because a composition lives at run time and erases exactly what this project exists to make visible:
- Consent would become all-or-nothing. Approving “build and publish” is not the same as approving a build, then approving a push that carries a credential.
- Cache would become all-or-nothing. A block containing one
effects: externalstep can never be served from cache, so a pure build inside it would stop being cached forever. effectswould collapse to the worst case among the steps.- A failure would report “the composition failed” instead of naming the push that did not go up.
Expanded, none of that happens. The abstraction lives only where it is written.
What it looks like
Section titled “What it looks like”apiVersion: oren.sh/v1kind: PipelineTemplatemetadata: namespace: techlite name: imagem version: 1.0.0
spec: parameters: required: [registry, nome, versao, credential] properties: registry: { type: string } nome: { type: string } versao: { type: string } credential: { type: string } context: { type: string, default: "." } buildArgs: { type: object }
outputs: image: ${outputs['push'].image} digest: ${outputs['push'].digest}
steps: - id: build task: techlite/build-docker-image@^1.0.0 implementation: techlite/build-docker-image-kaniko inputs: image: ${parameters.registry}/${parameters.nome}:${parameters.versao} context: ${parameters.context} archiveName: ${parameters.nome}.tar buildArgs: ${parameters.buildArgs} dependencies: source: "." artifacts: ./.oren/artefatos
- id: push task: techlite/push-docker-image@^1.0.0 implementation: techlite/push-docker-image-skopeo inputs: image: ${outputs['build'].image} dependencies: artifacts: ./.oren/artefatos registryCredential: ${parameters.credential}It goes in the registry as *.template.yaml, next to the contracts — and is
resolved by range, @^1.0.0, like a task.
parameters is a JSON Schema
Section titled “parameters is a JSON Schema”The same rigour as a task’s inputs: an unknown parameter is an error, because a
registy: typed wrong would otherwise vanish in silence; a missing required one
is an error; and default: is applied here.
An optional parameter that was not supplied disappears from the key.
buildArgs: ${parameters.buildArgs} with no with.buildArgs produces a step
with no buildArgs at all — not one with an empty value.
Alone in a string, ${parameters.x} yields the value, keeping its type: a
map stays a map. In the middle of text, it concatenates.
${outputs[...]} passes straight through
Section titled “${outputs[...]} passes straight through”Only parameters. is substituted at expansion time. outputs, env and
properties are resolved during execution, with values that do not exist yet —
so a template both receives and produces expressions:
with: versao: ${outputs['versao'].version} # goes in as text, resolved at run timeDeclared outputs
Section titled “Declared outputs”spec.outputs names what the template produces, as an alias for a field of
one of its steps:
outputs: image: ${outputs['push'].image} digest: ${outputs['push'].digest}Whoever uses it then references the id they wrote themselves:
- use: techlite/imagem@^1.1.0 id: web with: { ... }
- id: deploy inputs: images: app: ${outputs['web'].image} # not web-pushWithout this, every internal step id is the template’s public API: renaming
push to publish inside it would break every pipeline that referenced
web-push, turning a cosmetic rename into a major version.
An alias, and never a computed value — ${outputs['push'].registry}/... is
refused. Computing is a step’s job, and a composed value could not be spliced
into the middle of a consumer’s text without producing nested ${}.
Four refusals worth knowing:
| situation | what happens |
|---|---|
outputs['web'].unknown |
error, listing the outputs that exist |
a real step with id: web |
refused — outputs['web'] would have two owners |
| an output pointing outside the template | refused, listing its steps |
| an output that is not an alias | refused, stating the expected form |
Referencing ${outputs['web-push'].image} directly still works. Nothing is
hidden — the declared output is the stable surface, not the only one. If you
need the build’s archive, which the template does not declare, the generated id
is there.
id and the generated names
Section titled “id and the generated names”With id: web, the template’s build and push become web-build and
web-push. References between the template’s own steps are rewritten along
with them.
That rewrite is not cosmetic. Without it, two uses of the same template would produce two pushes both pointing at the first build — resolving with no error, because the id would still exist.
Without an id, the template’s step ids are kept as they are. That is the right
choice when a template is used once and the rest of the pipeline references its
output by name:
- use: techlite/versionar@^1.0.0 # generates `analise` and `versao` with: { manifest: package.json }
- id: tag task: techlite/commit-and-tag@^2.0.0 implementation: techlite/commit-and-tag-alpine inputs: tagName: v${outputs['versao'].version}oren validate shows every generated step, with its generated id. A template
hides nothing:
[3/7] Constrói acme-web · web-build · build-docker-image[4/7] Publica acme-web · web-push · push-docker-imageenabled: false removes the whole block
Section titled “enabled: false removes the whole block”Not step by step. Whoever switches off “publish the image” does not want the push going up on its own.
Publishing it
Section titled “Publishing it”Until it is published, every project that uses the template needs a copy of the file — which is the opposite of what a template exists for. Ten projects would mean ten copies, and fixing one bug would mean ten commits.
oren publish ./imagem.template.yamlA single file, and not a directory: a template has no contract-and-implementation
pair beside it. What decides it is the kind inside the document, never the
file name — the name is a convention for scanning a directory, and nothing more.
The portal refuses a template whose referenced tasks do not exist there. Same
rule as an implementation, and for the same reason: whoever consumed it would
receive a use: that does not expand, and would only find out on the first run.
After that, oren install downloads it like a task — and before expanding,
because expansion is what consumes it.
What the catalogue shows
Section titled “What the catalogue shows”A template has almost none of its own visible surface. The portal follows it:
- Outputs get their type from the contract of the step that produces them, resolved now. The template holds only the alias.
- Dependencies are the union of what its steps require, showing the
strongest of each: in
techlite/imagem,artifactsis written by the build and only read by the push, so it appears as write — that is what you are granting. - Effects stay per step. There is no aggregate. A template with a build
(
none) and a push (external) shows both, because collapsing them would say “this acts beyond its outputs” — true of the set, false of the build, and it would leave the reader thinking nothing there is cacheable.
Because the reference is a range, @^1.0.0 resolves to whatever is published at
the moment you look. Publishing push@1.1.0 after the template changes what the
catalogue shows — deliberately: that is the version that will run.
What a template cannot do
Section titled “What a template cannot do”Use another template. Depth is one level, for the same reason extends is:
nesting gives back the opacity that expansion exists to avoid.
Iterate. There is no “generate N builds from a list”. Two images means two
use: blocks — which is four lines, and keeps every step countable in the file.
Choose an implementation for you. implementation: is required inside the
template just as it is in a step: it is a decision, and a template makes it
once, on the author’s behalf, in writing.