GitLab CI/CD: A Alternativa Enterprise ao GitHub Actions

[325] GitLab CI/CD: A Alternativa Enterprise ao GitHub Actions

A plataforma que nasceu DevOps completa, e não como hospedagem de código que foi crescendo: a estrutura de stages e jobs do .gitlab-ci.yml, a distinção entre cache e artifacts, as variáveis automáticas, os templates nativos de SAST e scanning, os environments com aprovação manual e os runners.
DevOps

20 min de leitura

O GitHub é a plataforma padrão para projetos open-source e para a maioria dos times que trabalham com código público ou semi-público. Mas o GitLab ocupa um espaço diferente no mercado: é a escolha preferida de organizações que precisam de uma plataforma DevOps completa, auto-suficiente e, frequentemente, instalada em infraestrutura própria.

A diferença filosófica é importante. O GitHub nasceu como plataforma de hospedagem de código e foi adicionando funcionalidades ao longo do tempo — Actions, Packages, Security, Projects. O GitLab nasceu com a proposta de ser uma plataforma DevOps completa desde o início: repositório, CI/CD, registro de containers, gerenciamento de issues, wiki, monitoramento, segurança e muito mais, tudo integrado em um único produto.

Para o engenheiro que já domina o GitHub Actions, o GitLab CI/CD é a segunda plataforma mais importante para conhecer — não porque seja "melhor", mas porque ela aparece com frequência em contextos enterprise e em organizações que valorizam soberania sobre seus dados e pipelines.

A Estrutura do GitLab CI/CD

O arquivo de pipeline no GitLab é o .gitlab-ci.yml, que fica na raiz do repositório — equivalente ao .github/workflows/nome.yml do GitHub Actions. Mas há uma diferença estrutural importante: no GitHub Actions, cada arquivo de workflow é independente. No GitLab, há um único arquivo de pipeline por projeto (embora ele possa incluir outros arquivos via include).

O mapeamento de conceitos é o seguinte. No GitHub Actions, um workflow contém jobs que contêm steps. No GitLab CI/CD, um pipeline contém stages que agrupam jobs, e cada job contém um script. Os stages definem a ordem de execução — todos os jobs de um stage executam em paralelo, e o próximo stage só começa quando todos os jobs do stage anterior terminam.

# .gitlab-ci.yml

# Stages definem a ordem de execução
stages:
  - verificacao
  - build
  - testes
  - seguranca
  - deploy-staging
  - deploy-producao

# Variáveis globais (equivalente ao env: global do GitHub Actions)
variables:
  NODE_VERSION: "20"
  DOCKER_DRIVER: overlay2
  # Variáveis sensíveis vêm das CI/CD Variables do projeto (Settings → CI/CD)

# ── Job: Lint ────────────────────────────────────────────────────
lint:
  stage: verificacao
  image: node:20-alpine
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - node_modules/
  script:
    - npm ci
    - npm run lint
  rules:
    # Executar em qualquer branch ou merge request
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'

# ── Job: Testes Unitários ────────────────────────────────────────
testes-unitarios:
  stage: testes
  image: node:20-alpine
  services:
    # Services são containers auxiliares — equivalente ao services: do GitHub Actions
    - name: postgres:16-alpine
      alias: postgres
      variables:
        POSTGRES_DB: testdb
        POSTGRES_USER: testuser
        POSTGRES_PASSWORD: testpass
    - name: redis:7-alpine
      alias: redis
  variables:
    DATABASE_URL: "postgresql://testuser:testpass@postgres:5432/testdb"
    REDIS_URL: "redis://redis:6379"
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - node_modules/
  script:
    - npm ci
    - npm test -- --coverage
  coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'  # Regex para extrair cobertura do output
  artifacts:
    when: always
    reports:
      # Equivalente ao PublishTestResults do Azure DevOps
      junit: test-results.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
    paths:
      - coverage/
    expire_in: 7 days
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'

Cache e Artifacts: Conceitos Distintos

O GitLab separa explicitamente dois conceitos que o GitHub Actions trata de maneira mais unificada com actions/cache e actions/upload-artifact.

O cache no GitLab é para acelerar execuções — node_modules, dependências Maven, cache do pip. Ele é armazenado no runner e reutilizado entre jobs e pipelines. A chave do cache pode ser baseada em arquivos (como o package-lock.json) para invalidar automaticamente quando as dependências mudam.

Os artifacts são saídas do job que precisam ser passadas para jobs subsequentes ou preservadas para download. Um job de build gera o binário como artifact; um job de deploy consome esse artifact. A propriedade expire_in define por quanto tempo o artifact é mantido — sem isso, o GitLab usa a configuração padrão da instância.

# Job de build que gera artifact para o job de deploy
build-imagem:
  stage: build
  image: docker:24
  services:
    - docker:24-dind  # Docker-in-Docker
  variables:
    IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
  before_script:
    # Login no GitLab Container Registry (equivalente ao ECR/ACR)
    # CI_REGISTRY, CI_REGISTRY_USER e CI_REGISTRY_PASSWORD são variáveis automáticas
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build -t $IMAGE_TAG .
    - docker push $IMAGE_TAG
    # Também taggear como latest na branch main
    - |
      if [ "$CI_COMMIT_BRANCH" == "main" ]; then
        docker tag $IMAGE_TAG $CI_REGISTRY_IMAGE:latest
        docker push $CI_REGISTRY_IMAGE:latest
      fi
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

Variáveis Automáticas do GitLab

O GitLab injeta dezenas de variáveis de ambiente automaticamente em cada job. Para quem vem do GitHub Actions (onde as variáveis equivalentes ficam no contexto github.*), é útil conhecer as mais usadas:

CI_COMMIT_SHA é o SHA completo do commit — equivalente ao ${{ github.sha }}. CI_COMMIT_SHORT_SHA são os primeiros 8 caracteres — muito usado para taggear imagens Docker. CI_COMMIT_BRANCH é o nome da branch atual — equivalente ao ${{ github.ref_name }}. CI_COMMIT_TAG é a tag do commit, se existir — equivalente ao ${{ github.ref }} quando é uma tag. CI_PIPELINE_SOURCE indica o que disparou o pipeline: push, merge_request_event, schedule, web e outros — equivalente ao ${{ github.event_name }}. CI_REGISTRY_IMAGE é a URL da imagem no GitLab Container Registry deste projeto — sem equivalente direto no GitHub, que precisa construir a URL manualmente. CI_PROJECT_PATH é o caminho do projeto no formato grupo/subgrupo/projeto. CI_ENVIRONMENT_URL é a URL do environment de deploy, configurada no job.

Segurança no Pipeline: SAST, DAST e Dependency Scanning

Um diferencial significativo do GitLab em relação ao GitHub Actions é a integração nativa de ferramentas de segurança. O GitLab oferece SAST (Static Application Security Testing), DAST (Dynamic Application Security Testing), Dependency Scanning e Container Scanning como templates prontos para incluir no pipeline — sem precisar configurar actions de terceiros ou criar scripts customizados.

# Incluir templates de segurança do GitLab
include:
  - template: Security/SAST.gitlab-ci.yml
  - template: Security/Dependency-Scanning.gitlab-ci.yml
  - template: Security/Container-Scanning.gitlab-ci.yml
  - template: Security/Secret-Detection.gitlab-ci.yml

# Os jobs de segurança são adicionados automaticamente pelo include
# e executam no stage 'test' por padrão

# Configuração do Container Scanning (scan da imagem Docker)
container_scanning:
  stage: seguranca
  variables:
    CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
    CS_SEVERITY_THRESHOLD: HIGH  # Falhar apenas em HIGH ou CRITICAL
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

# Configuração do SAST
sast:
  stage: seguranca
  variables:
    SAST_EXCLUDED_PATHS: "spec,test,tests,tmp,node_modules"

Os resultados de todas as ferramentas de segurança aparecem na interface do GitLab no painel de Security do projeto e nos Merge Requests — o revisor vê os findings de segurança diretamente no MR, sem precisar navegar para outro sistema.

Deploy com Environments e Revisão Manual

O GitLab tem um sistema de Environments robusto que rastreia o histórico de deploys, permite rollback visual e suporta revisão manual antes de deploys em ambientes críticos.

# ── Deploy em Staging ────────────────────────────────────────────
deploy-staging:
  stage: deploy-staging
  image: bitnami/kubectl:latest
  environment:
    name: staging
    url: https://staging.minha-app.com
  variables:
    KUBECONFIG: $KUBECONFIG_STAGING  # Secret file configurada nas CI/CD Variables
  script:
    - kubectl set image deployment/minha-app
        app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
        -n staging
    - kubectl rollout status deployment/minha-app -n staging --timeout=5m
    - |
      # Smoke test
      HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://staging.minha-app.com/health)
      if [ "$HTTP_STATUS" != "200" ]; then
        echo "Smoke test falhou: $HTTP_STATUS"
        exit 1
      fi
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

# ── Deploy em Produção com Revisão Manual ─────────────────────────
deploy-producao:
  stage: deploy-producao
  image: bitnami/kubectl:latest
  environment:
    name: producao
    url: https://minha-app.com
  variables:
    KUBECONFIG: $KUBECONFIG_PRODUCAO
  # when: manual — o job só executa quando alguém clicar no botão na interface
  # Equivalente ao environment com required reviewers do GitHub Actions
  when: manual
  # allow_failure: false garante que o pipeline fica em pausa esperando a aprovação
  allow_failure: false
  script:
    - kubectl set image deployment/minha-app
        app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
        -n producao
    - kubectl rollout status deployment/minha-app -n producao --timeout=10m
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual

# ── Rollback Manual ───────────────────────────────────────────────
rollback-producao:
  stage: deploy-producao
  image: bitnami/kubectl:latest
  environment:
    name: producao
    action: stop  # Marca o environment como parado no histórico
  when: manual
  script:
    - kubectl rollout undo deployment/minha-app -n producao
    - kubectl rollout status deployment/minha-app -n producao
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual

Pipelines Agendados e Multi-projeto

O GitLab suporta pipelines agendados (equivalente ao schedule trigger do GitHub Actions) configurados via interface ou API — úteis para testes noturnos, relatórios periódicos e rotações de credenciais.

Para projetos que precisam disparar pipelines em outros projetos (por exemplo, um repositório de infraestrutura que, ao mudar, deve redesenhar aplicações dependentes), o GitLab oferece o trigger:

# Pipeline que dispara outro pipeline em outro projeto
notificar-downstream:
  stage: deploy-producao
  trigger:
    project: minha-empresa/infraestrutura-kubernetes
    branch: main
    strategy: depend  # Aguardar o pipeline downstream terminar
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

GitLab Runners: Onde o Pipeline Executa

Os GitLab Runners são os agentes que executam os jobs — equivalente aos runners hospedados do GitHub (ubuntu-latest, windows-latest) e aos self-hosted runners. O GitLab.com oferece runners compartilhados gratuitos para projetos públicos e com limites para projetos privados. Para ambientes on-premises ou com requisitos específicos de segurança e performance, é comum instalar runners próprios.

Um runner próprio é instalado em qualquer máquina Linux, Windows ou macOS com o gitlab-runner e registrado no projeto ou grupo GitLab com um token. O executor (como o job executa) pode ser Shell (diretamente na máquina), Docker (em containers isolados — o mais comum), Kubernetes (em pods do cluster — excelente para escalonamento automático) ou VirtualBox/Parallels (em VMs).

Para registrar um runner Kubernetes no cluster:

# helm values para o GitLab Runner no Kubernetes
# helm install gitlab-runner gitlab/gitlab-runner -f values.yml

gitlabUrl: https://gitlab.com/  # Ou a URL do GitLab self-hosted
runnerToken: "TOKEN-DO-RUNNER"  # Obtido em Settings → CI/CD → Runners

rbac:
  create: true

runners:
  config: |
    [[runners]]
      [runners.kubernetes]
        namespace = "gitlab-runners"
        image = "ubuntu:22.04"
        privileged = false
        [[runners.kubernetes.volumes.empty_dir]]
          name = "docker-certs"
          mount_path = "/certs/client"
          medium = "Memory"

O Que Vem a Seguir

Este artigo cobriu o GitLab CI/CD como plataforma SaaS e o padrão de pipelines para times que hospedam código no gitlab.com. O próximo artigo vai mais fundo: o GitLab instalado em infraestrutura própria (self-hosted), que é onde a plataforma mostra todo o seu potencial para organizações com requisitos de soberania de dados, compliance rigoroso ou simplesmente custos de licença mais previsíveis.

Referências para Aprofundamento

Exercícios

Exercício 1

Os jobs lint e testes-unitarios declaram cache assim, e ambos executam npm ci:

  cache:
    key:
      files:
        - package-lock.json
    paths:
      - node_modules/

O time observa que o cache é baixado e restaurado a cada job, mas o tempo de execução não melhora. Por quê?

Ver resposta

✓ Resposta: Porque npm ci apaga o diretório node_modules/ antes de instalar. O cache é restaurado e destruído em seguida — o trabalho de baixar e extrair é feito, e o resultado é jogado fora.

Essa é justamente a diferença entre os dois comandos: npm install instala de forma incremental e reaproveita o que já existe; npm ci parte do zero por design, para garantir que a instalação corresponde exatamente ao package-lock.json. É a escolha certa para CI — reprodutibilidade acima de velocidade —, mas ela é incompatível com cachear node_modules/.

O que deve ser cacheado é o cache interno do npm, que fica em ~/.npm. O npm ci o consulta antes de ir à rede, e é aí que o ganho acontece: os tarballs dos pacotes não precisam ser baixados de novo, embora a extração continue ocorrendo.

# o npm precisa ser instruído a usar um diretório dentro do workspace,
# porque o cache do GitLab só cobre caminhos do projeto
variables:
  npm_config_cache: "$CI_PROJECT_DIR/.npm"

cache:
  key:
    files:
      - package-lock.json
  paths:
    - .npm/
  policy: pull        # jobs que só consomem não precisam reenviar o cache

Repare no detalhe que costuma passar despercebido: o cache do GitLab só arquiva caminhos dentro do diretório do projeto. Apontar ~/.npm diretamente não funciona — daí a variável npm_config_cache redirecionando para dentro do workspace.

O policy: pull merece atenção pelo custo: por padrão a política é pull-push, e todo job recompacta e reenvia o cache ao terminar, mesmo sem ter alterado nada. Em um pipeline com cinco jobs consumindo as mesmas dependências, são cinco uploads desnecessários. O padrão eficiente é um job inicial com pull-push preparando o cache e os demais com pull.

A chave baseada em files: package-lock.json, essa sim, está correta: o GitLab calcula o hash do arquivo, e qualquer mudança de dependência invalida o cache automaticamente — sem depender de alguém lembrar de mudar uma string de versão.

Exercício 2

O job build-imagem usa services: - docker:24-dind para construir a imagem. O runner Kubernetes é instalado com o values.yml do próprio artigo, que declara privileged = false. O que acontece quando esse pipeline executa?

Ver resposta

✓ Resposta: O job falha. O Docker-in-Docker exige modo privilegiado, e as duas configurações do artigo se contradizem.

O docker:dind sobe um daemon Docker completo dentro do container de serviço, e para isso precisa manipular cgroups, montar sistemas de arquivos e criar interfaces de rede — operações que exigem capacidades que o Kubernetes remove por padrão. Sem privileged = true, o daemon não inicia, e o erro que aparece no job é enganoso: Cannot connect to the Docker daemon at tcp://docker:2375, que parece problema de rede.

Só que privileged = true não é uma correção aceitável. Um container privilegiado tem acesso praticamente irrestrito ao nó: enxerga todos os dispositivos, pode montar o sistema de arquivos do host e escapar do isolamento. Como o runner executa código arbitrário vindo de merge requests, isso equivale a conceder root no nó a qualquer pessoa capaz de abrir um MR. É também incompatível com o perfil restricted do Pod Security Standards adotado no capstone.

A saída correta é não usar Docker para construir imagens dentro do cluster. Duas ferramentas resolvem isso sem privilégio:

build-imagem:
  stage: build
  image:
    name: gcr.io/kaniko-project/executor:debug
    entrypoint: [""]
  script:
    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
    - /kaniko/executor
        --context "$CI_PROJECT_DIR"
        --dockerfile "$CI_PROJECT_DIR/Dockerfile"
        --destination "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"

O Kaniko interpreta o Dockerfile e monta as camadas em espaço de usuário, sem daemon e sem privilégio. O BuildKit rootless é a alternativa, com melhor suporte a cache de camadas. Ambos publicam direto no registry, o que dispensa o docker push.

Note que o artigo já acerta em um ponto relacionado: o volume docker-certs montado em /certs/client com medium = "Memory" existe para o TLS entre o job e o dind — sinal de que a configuração do runner foi pensada para dind, mas ficou incoerente com o privileged = false. É o tipo de contradição que só aparece na primeira execução real.

Exercício 3

O pipeline declara os stages verificacao, build, testes, seguranca, deploy-staging, deploy-producao e inclui quatro templates de segurança, mas sobrescreve o stage: de apenas dois deles (container_scanning e sast). O comentário registra que os jobs dos templates "executam no stage test por padrão". Qual o problema?

Ver resposta

✓ Resposta: Os dois templates não sobrescritosDependency-Scanning e Secret-Detection — trazem jobs que apontam para o stage test, e test não existe na lista de stages declarada. O pipeline é rejeitado inteiro na validação, com chosen stage does not exist.

O detalhe cruel é quando o erro aparece: não é um job que falha, é o pipeline que não chega a ser criado. O push acontece, e não surge pipeline algum — nem vermelho, nem verde. Para quem espera ver a execução falhar, a ausência de qualquer coisa é mais confusa que um erro. A mensagem fica na aba de validação do CI, e não no lugar onde se costuma olhar.

Repare também no descompasso de nomenclatura que causa isso: os stages foram nomeados em português (testes, seguranca), enquanto os templates do GitLab usam os nomes canônicos em inglês (test, build, deploy). Nada impede traduzir, mas então todo job importado precisa ser realocado.

Há duas formas de resolver. A mais simples é incluir os stages canônicos na lista, deixando os jobs dos templates caírem onde esperam:

stages:
  - verificacao
  - build
  - test          # exigido pelos templates de segurança do GitLab
  - seguranca
  - deploy-staging
  - deploy-producao

A mais explícita é realocar cada job importado, o que também dá controle sobre a ordem — e essa ordem importa. Como está, o container_scanning roda no stage seguranca, depois do build que já fez docker push: é o mesmo problema do capstone, em que a imagem vulnerável é publicada antes de ser escaneada. Já o Secret-Detection deveria rodar o mais cedo possível — de nada adianta detectar uma credencial vazada depois que a imagem foi construída com ela dentro.

secret_detection:
  stage: verificacao      # antes de tudo

dependency_scanning:
  stage: seguranca

A lição geral sobre include: templates trazem jobs com premissas próprias — stage, imagem, regras, variáveis. Incluir não é apenas adicionar verificações; é herdar decisões que precisam ser conferidas contra a estrutura do seu pipeline.

Exercício 4

O job rollback-producao declara:

  environment:
    name: producao
    action: stop

com o comentário "Marca o environment como parado no histórico". Um engenheiro executa o rollback durante um incidente e, depois, o ambiente producao desaparece da lista de ambientes ativos do GitLab. O que aconteceu?

Ver resposta

✓ Resposta: O action: stop não significa "registrar um evento" — ele encerra o ambiente. O GitLab o marca como stopped, remove-o da lista de ambientes ativos e perde o rastreamento de qual versão está implantada. A aplicação continua rodando no cluster; o que se perdeu foi o registro do GitLab sobre ela.

Esse valor existe para ambientes efêmeros — as review apps, criadas por merge request e destruídas quando o MR é fechado. Nesse fluxo, o job com action: stop é quem executa a limpeza: derruba o namespace, remove o deploy, libera o subdomínio. Aplicá-lo a um ambiente permanente é usar a ferramenta de demolição para trocar um pneu.

As consequências práticas são todas de perda de informação, e aparecem justamente quando mais se precisa dela: o histórico de deploys do ambiente fica órfão; o botão de re-deploy e o rollback pela interface deixam de funcionar; e a tela de Environments — que responde "qual versão está em produção agora?" — deixa de mostrar produção.

Para um rollback, o correto é declarar um deploy normal, porque é isso que ele é: uma nova implantação, de uma versão anterior.

rollback-producao:
  stage: deploy-producao
  image: bitnami/kubectl:latest
  environment:
    name: producao          # sem action — é um deploy como outro qualquer
    url: https://minha-app.com
  when: manual
  script:
    - kubectl rollout undo deployment/minha-app -n producao
    - kubectl rollout status deployment/minha-app -n producao

Assim o rollback entra no histórico como o evento que de fato é, e o ambiente permanece ativo e rastreável.

Vale a ressalva do artigo sobre GitOps: com o Flux ou o ArgoCD sincronizando o cluster a partir do Git, um kubectl rollout undo é revertido pelo controlador na reconciliação seguinte — o rollback precisa acontecer no repositório. Este pipeline usa kubectl set image diretamente, então o comando funciona; mas o Git deixa de refletir o que está em produção, e o próximo deploy reintroduz a versão problemática.

Exercício 5

O job deploy-producao declara when: manual e allow_failure: false, com o comentário de que este último "garante que o pipeline fica em pausa esperando a aprovação". Explique por que essa segunda linha é necessária — e qual a diferença de fundo entre esse mecanismo e o environment com required reviewers do GitHub Actions.

Ver resposta

✓ Resposta: É necessária porque, para jobs manuais, o padrão do GitLab é allow_failure: true — o inverso do padrão dos jobs comuns.

A lógica por trás disso é que um job manual costuma ser opcional (rodar um teste de carga, gerar um relatório sob demanda), e não faria sentido o pipeline ficar eternamente pendente esperando alguém que talvez nunca clique. Com allow_failure: true, o pipeline é marcado como bem-sucedido mesmo sem o job manual ter sido executado.

Num job de deploy em produção, esse padrão é justamente o oposto do desejado: o pipeline apareceria verde, sugerindo que a entrega foi concluída, enquanto nada foi para produção. O allow_failure: false transforma o job em bloqueante: o pipeline fica no estado manual, aguardando de fato a decisão, e só conclui quando alguém a toma.

A diferença de fundo em relação ao GitHub é quem controla a autorização, e ela importa:

  • No GitLab, when: manual é um gatilho: qualquer pessoa com permissão de execução naquele ambiente pode acionar o botão. A restrição vem das protected environments, configuradas à parte, que definem quem pode implantar onde.
  • No GitHub, o environment com required reviewers é uma aprovação: o job só inicia depois que uma pessoa específica de uma lista autoriza, e o registro guarda quem aprovou.

São modelos distintos com implicações de auditoria diferentes. O GitLab registra quem executou; o GitHub registra quem autorizou — e isso permite separar as duas figuras, de modo que quem escreveu o código não seja quem libera o deploy. Onde há exigência de segregação de funções, essa distinção deixa de ser detalhe.

Repare, por fim, na redundância do trecho: o when: manual aparece duas vezes, no nível do job e dentro de rules. Quando rules está presente, é ela que decide — o when no nível do job é ignorado. Não causa erro, mas induz a acreditar que remover uma das duas basta para mudar o comportamento.

Comentários

Mais em DevOps

Capstone: Pipeline Completo de CI/CD
Capstone: Pipeline Completo de CI/CD

O pipeline tratado como produto de engenharia, em que o desenvolvedor é o…

DevSecOps: Integrando Segurança em Todo o Pipeline
DevSecOps: Integrando Segurança em Todo o Pipeline

Segurança deslocada para a esquerda, em seis camadas: análise estática com…

Publicando Imagens no Docker Hub e GitHub Container Registry
Publicando Imagens no Docker Hub e GitHub Container Registry

Publicando imagens em registros de container: convenção de nomes no Docker Hub…