Pular para o conteúdo

Templates de pipeline

Quando a mesma sequência de steps aparece num terceiro projeto, copiar pela terceira vez deixa de ser razoável. Um template é um bloco de steps com nome e parâmetros:

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

Um template é açúcar de escrita. Ele some antes de qualquer coisa olhar o pipeline: quando o resolver, o cache, o consent, o oren.lock e o gerador de CI veem seu arquivo, não existe use: em lugar nenhum — só os steps que você teria digitado à mão.

Essa é a decisão de projeto inteira, e a alternativa era tentadora: uma task composta, um step que roda vários por dentro. Daria menos código aqui e muito pior em todo o resto, porque uma composição vive na execução e apaga justamente o que este projeto existe para tornar visível:

  • O consent viraria tudo-ou-nada. Aprovar “constrói e publica” não é a mesma coisa que aprovar um build e depois aprovar um push que carrega uma credencial.
  • O cache viraria tudo-ou-nada. Um bloco com um step effects: external nunca pode vir do cache, então um build puro lá dentro deixaria de ser cacheado para sempre.
  • effects viraria o pior caso entre os steps.
  • Uma falha reportaria “a composição falhou” em vez de dizer qual push não subiu.

Expandido, nada disso acontece. A abstração vive só onde é escrita.

apiVersion: oren.sh/v1
kind: PipelineTemplate
metadata:
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}

Ele fica no registry como *.template.yaml, ao lado dos contratos — e é resolvido por faixa, @^1.0.0, como uma task.

Mesmo rigor dos inputs de uma task: parâmetro desconhecido é erro, senão um registy: digitado errado sumiria em silêncio; obrigatório ausente é erro; e o default: é aplicado aqui.

Um parâmetro opcional não fornecido some da chave inteira. buildArgs: ${parameters.buildArgs} sem with.buildArgs gera um step sem buildArgs nenhum — e não um com valor vazio.

Sozinho na string, ${parameters.x} devolve o valor, com o tipo preservado: um mapa continua mapa. No meio de um texto, concatena.

parameters. é substituído na expansão. outputs, env e properties são resolvidos na execução, com valores que ainda não existem — então um template tanto recebe quanto produz expressões:

with:
versao: ${outputs['versao'].version} # entra como texto, resolvido na execução

spec.outputs dá nome ao que o template produz, como um apelido para o campo de um dos steps dele:

outputs:
image: ${outputs['push'].image}
digest: ${outputs['push'].digest}

Quem usa referencia então o id que ele mesmo escreveu:

- use: techlite/imagem@^1.1.0
id: web
with: { ... }
- id: deploy
inputs:
images:
app: ${outputs['web'].image} # e não web-push

Sem isso, todo id interno vira API pública do template: renomear push para publicar lá dentro quebraria todo pipeline que referenciasse web-push, transformando um rename cosmético em major version.

Apelido, e nunca valor computado — ${outputs['push'].registry}/... é recusado. Computar é trabalho de step, e um valor composto não caberia no meio de um texto do consumidor sem gerar ${} aninhado.

Quatro recusas que vale conhecer:

situação o que acontece
outputs['web'].inventado erro, listando as saídas que existem
step de verdade com id: web recusado — outputs['web'] teria dois donos
saída apontando para fora do template recusada, listando os steps dele
saída que não é apelido recusada, dizendo a forma esperada

Referenciar ${outputs['web-push'].image} direto continua funcionando. Nada fica escondido — a saída declarada é a superfície estável, não a única. Se você precisar do archive do build, que o template não declara, o id gerado está lá.

Com id: web, o build e o push do template viram web-build e web-push. As referências entre os steps do próprio template são reescritas junto.

Essa reescrita não é cosmética. Sem ela, dois usos do mesmo template gerariam dois pushes apontando ambos para o primeiro build — resolvendo sem erro, porque o id ainda existiria.

Sem id, os ids do template são mantidos como estão. É a escolha certa quando o template é usado uma vez e o resto do pipeline referencia a saída dele pelo nome:

- use: techlite/versionar@^1.0.0 # gera `analise` e `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}

O oren validate mostra todos os steps gerados, com os ids gerados. Um template não esconde nada:

[3/7] Constrói acme-web · web-build · build-docker-image
[4/7] Publica acme-web · web-push · push-docker-image

Não step a step. Quem desliga “publicar a imagem” não quer que o push suba sozinho.

Enquanto ele não é publicado, todo projeto que usa o template precisa de uma cópia do arquivo — que é o oposto do que um template existe para fazer. Dez projetos seriam dez cópias, e corrigir um bug seriam dez commits.

Terminal window
oren publish ./imagem.template.yaml

Um arquivo só, e não um diretório: um template não tem um par contrato-e-implementação ao lado. O que decide é o kind dentro do documento, nunca o nome do arquivo — o nome é convenção para varrer um diretório, e nada além disso.

O portal recusa um template cujas tasks referenciadas não existam lá. Mesma regra da implementação, e pela mesma razão: quem consumisse receberia um use: que não expande, e só descobriria na primeira execução.

Depois disso o oren install baixa o template como baixa uma task — e antes de expandir, porque a expansão é o que o consome.

Um template quase não tem superfície visível própria. O portal a segue:

  • As saídas ganham o tipo do contrato do step que as produz, resolvido agora. O template guarda só o apelido.
  • As dependências são a união do que os steps dele exigem, mostrando a mais forte de cada: no techlite/imagem, artifacts é escrita pelo build e só lida pelo push, então aparece como escrita — é o que você está concedendo.
  • Os efeitos ficam por step. Não existe agregado. Um template com um build (none) e um push (external) mostra os dois, porque colapsar diria “isto age fora dos próprios outputs” — verdade sobre o conjunto, mentira sobre o build, e deixaria quem lê achando que nada ali é cacheável.

Como a referência é uma faixa, @^1.0.0 resolve para o que estiver publicado no momento em que você olha. Publicar push@1.1.0 depois do template muda o que o catálogo mostra — de propósito: é a versão que vai executar.

Usar outro template. A profundidade é de um nível, pela mesma razão do extends: aninhar devolve a opacidade que a expansão existe para evitar.

Iterar. Não existe “gere N builds a partir de uma lista”. Duas imagens são dois blocos use: — que são quatro linhas, e mantêm todo step contável no arquivo.

Escolher a implementação por você. O implementation: é obrigatório dentro do template como é num step: é uma decisão, e um template a toma uma vez, em nome de quem o escreveu, por escrito.