Includes
Dependendo o que você esta fazendo, a quantidade de steps, variáções de regras, etc, irá chegar um momento que o arquivo .gitlab-ci.yml irá ficar bem grande.
Podemos separar os códigos em diferentes arquivos yaml e manter .gitlab-ci.yml limpo, só fazendo inclusões e declarando as coisas principais.
Recomendo manter o bloco default
sempre na raíz para evitar sobreescrita. Se o yaml é lido de cima pra baixo, então faça os includes depois para evitar problemas.
default:
tags:
- general
image: node:22-alpine
stages:
- check # Novo stage
- build
- deploy
Vou criar uma árvore de pastas para dividir os stages.
❯ tree cicd
cicd
├── globals.yaml
├── build
│ └── build.yaml
├── check
│ └── check.yaml
└── deploy
└── deploy.yaml
Como ficam os arquivos.
❯ cat cicd/check/check.yaml
.check:
stage: check
before_script:
- env
- npm ci
artifacts:
when: always
expire_in: "3 months"
# ISSO VAI PARA DENTRO DE globals.yaml pois usamos para vários stages
# .rules-only-main-mr:
# rules:
# - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
# when: always
unit-test:
extends:
- .check
- .rules-only-main-mr
script:
- npm test
artifacts:
reports:
junit: reports/junit.xml
lint-test:
extends: [.check,.rules-only-main-mr]
script:
- npm run lint
artifacts:
reports:
codequality: gl-codequality.json
vulnerability-test:
extends: [.check,.rules-only-main-mr]
script:
- npm audit --audit-level=high --json > vulnerability-report.json
artifacts:
paths:
- vulnerability-report.json
❯ cat cicd/build/build.yaml
# ISSO VAI PARA DENTRO DE globals.yaml pois usamos para vários stages
# .rules-only-main-mr:
# rules:
# - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
# when: always
build:
stage: build
extends: [.rules-only-main-mr]
script:
- node --version
- npm --version
- npm ci
- npm run build
artifacts:
when: on_success
expire_in: "1 hour"
paths:
- build/
❯ cat cicd/deploy/deploy.yaml
deploy:
stage: deploy
image: alpine
script:
- echo "Deploy iniciando com TOKEN=$TOKEN"
environment:
name: $DEPLOY_ENV
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
variables:
DEPLOY_ENV: develop
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
DEPLOY_ENV: production
O nosso stage ficaria assim então, de forma que pudesse chamar todos esses arquivos.
default:
tags:
- general
image: node:22-alpine
stages:
- check # Novo stage
- build
- deploy
include:
- local: '/cicd/globals.yaml' # Um arquivo em specifico
- local: '/cicd/check/check.yaml' # Um arquivo em specifico
- local: '/cicd/build/*.yaml' # Qualquer arquivo .yaml na pasta build
- local: '/cicd/deploy/*.yaml'
Eu preferi separar em pastas por stage pois futuramente posso resolver separar cada um dos jobs em diferentes arquivos yaml. Até agora os nosso jobs são simples, mas podem existir jobs muito maiores futuramente.
Ainda podemos fazer uso de wildcards para facilitar.
default:
tags:
- general
image: node:22-alpine
stages:
- check
- build
- deploy
# include:
# - local: '/cicd/**/*.yaml' # Isso NÃO FUNCIONA
include: # Isso funciona
- 'cicd/globals.yaml'
- 'cicd/**/*.yaml'
Cuidado com essa pegadinha ai que a própria documentação oficial nos confunde.
Includes De Outros Repositórios
Podemos fazer includes diretamente de outro repositório se leu bem a documentação. Te deve ter passado pela cabeça em ter um repositório central que podemos reaproveitar código ao invés de desenvolver para cada repositório todos os jobs.
Vantagens:
- Padronização: Times diferentes seguem os mesmos pipelines (build, test, deploy).
- DRY (Don't Repeat Yourself): Centraliza templates e reduz repetição.
- Manutenção centralizada: Atualizar uma pipeline (ex: nova regra de segurança) afeta todos os projetos automaticamente.
- Segurança/controle: Você pode controlar exatamente o que é executado nos projetos, importante em ambientes mais regulados.
Desvantagens:
- Acoplamento: Todos os projetos dependem de um único repositório. Uma mudança errada quebra tudo.
- Complexidade no debug
- Menos autonomia: Equipes podem reclamar de não poder adaptar facilmente a pipeline às suas necessidades.
Casos de uso comuns:
- Empresas com muitos projetos e times que precisam seguir padrões (ex: microserviços).
- Organizações com foco em compliance/segurança.
- Plataformas internas tipo PaaS interno, onde a esteira é parte da plataforma.
Você pode adotar uma abordagem híbrida: cada projeto mantém um .gitlab-ci.yml simples, que inclui templates do repositório central, mas permite sobrescrever ou extender jobs quando necessário. Isso oferece flexibilidade sem abrir mão da padronização.
Pensando como peças de LEGO, é possível montar pipelines sob medida a partir de blocos reutilizáveis. Além disso, dá pra criar templates de repositórios — por exemplo, um boilerplate para projetos Node já configurado com a pipeline padrão de Node, e o mesmo para projetos em Python.
Como seria uma estrutura de repositório desse? É só uma sugestão, não uma regra.
ci-templates/
├── templates/
│ ├── node/
│ │ ├── install.yml
│ │ ├── test.yml
│ │ └── build.yml
│ ├── python/
│ │ ├── lint.yml
│ │ ├── test.yml
│ │ └── package.yml
│ └── common/
│ ├── security-scan.yml
│ ├── docker-build.yml
│ └── notify-slack.yml
├── full-pipelines/
│ ├── node.yml # Inclui os templates de node + common
│ ├── python.yml
│ └── microservice.yml
Ao final do projeto podemos pensar nisso, pois nesse momento manter o pipeline junto como repositório facilita a nossa evolução. Essa idéia é muito interessante principalmente para empresas que possuem uma equipe só para pipelines.
Imaginemos que temos um repositório centralizado com jobs em platform/ci-templates e um outro repositório que irá aproveitar isso.
include:
- project: 'platform/ci-templates'
file: '/templates/node/install.yml'
ref: main
- project: 'platform/ci-templates'
file: '/templates/node/test.yml'
ref: main
- project: 'platform/ci-templates'
file: '/templates/common/docker-build.yml'
ref: main
# Exemplo de sobrescrita opcional (se quiser customizar algo)
custom_test:
extends: .node_test_template
variables:
RUN_TESTS: "true"
script:
- echo "Rodando testes com cobertura"
- npm run test:coverage
Mas para que isso funcione, é necessário um padrão de desenvolvimento.