CI/CD para scripts Bash: validação automatizada

1. Fundamentos da Integração Contínua para Shell Scripts

1.1. Por que aplicar CI/CD a scripts Bash? Riscos comuns em ambientes de produção

Scripts Bash são frequentemente tratados como "código descartável", mas em ambientes de produção, um erro de sintaxe ou uma variável não sanitizada pode causar falhas catastróficas. Aplicar CI/CD a scripts Bash reduz riscos como:

  • Quebra de pipelines de deploy devido a comandos mal formatados
  • Vazamento de dados sensíveis por expansão incorreta de variáveis
  • Inconsistências entre ambientes (desenvolvimento vs produção)
  • Dificuldade de rastreamento de alterações e rollback

1.2. Ferramentas de pipeline: GitHub Actions, GitLab CI, Jenkins e alternativas leves

Para scripts Bash, ferramentas leves são preferíveis. Exemplos:

# GitHub Actions - .github/workflows/validate.yml
name: Validate Bash Scripts
on: [push, pull_request]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run ShellCheck
        run: shellcheck scripts/*.sh
# GitLab CI - .gitlab-ci.yml
stages:
  - validate
shellcheck:
  stage: validate
  script:
    - shellcheck scripts/*.sh

1.3. Estrutura de repositório ideal: organização de diretórios e versionamento semântico

projeto-bash/
├── scripts/          # Scripts principais
│   ├── deploy.sh
│   └── backup.sh
├── tests/            # Testes BATS
│   ├── test_deploy.bats
│   └── helpers.bash
├── lib/              # Bibliotecas compartilhadas
│   └── utils.sh
├── .github/          # CI/CD configs
│   └── workflows/
├── CHANGELOG.md
└── VERSION           # Exemplo: 1.2.3

Versionamento semântico (MAJOR.MINOR.PATCH) aplicado a scripts permite rastrear mudanças críticas.

2. Análise Estática e Linting Automatizado

2.1. ShellCheck: configuração de regras, integração em pipelines e tratamento de falsos positivos

ShellCheck é a ferramenta padrão para análise estática de Bash. Integração básica:

# Instalação
sudo apt install shellcheck

# Uso em pipeline
shellcheck --severity=style scripts/*.sh

Para ignorar falsos positivos:

# shellcheck disable=SC2086
echo $var  # SC2086: Double quote to prevent globbing

2.2. Verificação de sintaxe com bash -n e detecção de erros comuns

# Verificação rápida de sintaxe
bash -n scripts/deploy.sh || echo "Erro de sintaxe detectado"

# Exemplo de erro comum: variável não definida
#!/bin/bash
set -u  # Erro se variável não definida
echo "$nome"

2.3. Linters complementares: shfmt para formatação consistente e bashate para boas práticas

# shfmt - formatação automática
shfmt -w scripts/*.sh

# bashate - verificação de estilo
pip install bashate
bashate -i E006 scripts/*.sh  # Ignora regra E006 (linhas muito longas)

3. Testes Unitários e de Integração com BATS

3.1. BATS (Bash Automated Testing System): instalação, estrutura de testes e asserções

# Instalação
git clone https://github.com/bats-core/bats-core.git
cd bats-core && ./install.sh /usr/local

# Exemplo de teste - test_utils.bats
#!/usr/bin/env bats

setup() {
  source ../lib/utils.sh
}

@test "funcao_soma retorna soma correta" {
  run funcao_soma 2 3
  [ "$output" -eq 5 ]
}

@test "funcao_valida_email rejeita formato invalido" {
  run funcao_valida_email "invalido"
  [ "$status" -eq 1 ]
}

3.2. Mocking de comandos externos e isolamento de funções com stub/bats-mock

# bats-mock - simular comandos externos
load 'bats-mock/stub'

@test "deploy chama ssh corretamente" {
  stub ssh "echo 'comando ssh executado'"
  run scripts/deploy.sh
  [ "$output" = "comando ssh executado" ]
  unstub ssh
}

3.3. Execução paralela de testes e geração de relatórios

# Execução paralela
bats --jobs 4 tests/*.bats

# Relatório JUnit XML (usando bats-report)
bats --formatter junit tests/*.bats > report.xml

4. Validação de Segurança e Boas Práticas

4.1. Análise dinâmica com ShellCheck --severity=error e regras customizadas

# Foco apenas em erros críticos
shellcheck --severity=error scripts/*.sh

# Regras customizadas via .shellcheckrc
cat > .shellcheckrc << EOF
disable=SC2154  # Variável referenciada mas não atribuída
enable=all
EOF

4.2. Verificação de injeção de comandos, variáveis não sanitizadas e uso seguro de eval

# Perigoso: injeção de comando
eval "echo $user_input"  # Evitar

# Seguro: usar arrays
args=("$user_input")
echo "${args[@]}"

# Validação de entrada
sanitize_input() {
  local input="$1"
  if [[ "$input" =~ [^a-zA-Z0-9_] ]]; then
    echo "Entrada inválida" >&2
    return 1
  fi
}

4.3. Scripts de hardening: validação de permissões, shebang correto e tratamento de signals

#!/bin/bash
set -euo pipefail  # Modo estrito
trap 'cleanup' EXIT SIGINT SIGTERM

cleanup() {
  rm -f /tmp/temp_file
  echo "Cleanup executado"
}

# Validação de permissões
if [ "$(stat -c %a scripts/deploy.sh)" -ne 755 ]; then
  echo "Permissão incorreta" >&2
  exit 1
fi

5. Automação de Build e Empacotamento

5.1. Compilação de scripts em binários com shc ou bash2exe

# shc - compilar script para binário
shc -f scripts/deploy.sh -o deploy.bin

# bash2exe (alternativa)
pip install bash2exe
bash2exe scripts/deploy.sh -o deploy.exe

5.2. Geração de documentação automática: extração de help text e man pages

# Extrair help text
grep -A999 '^# HELP' scripts/deploy.sh | sed 's/^# //' > docs/deploy.md

# Gerar man page
cat > docs/deploy.1 << EOF
.TH DEPLOY 1 "2024-01-01" "1.0" "Bash Scripts"
.SH NAME
deploy \- Automated deployment script
.SH SYNOPSIS
deploy [options]
EOF

5.3. Criação de pacotes distribuíveis no pipeline

# Pipeline GitLab CI para criar pacote deb
stages:
  - build
build-deb:
  stage: build
  script:
    - mkdir -p package/usr/local/bin
    - cp scripts/deploy.sh package/usr/local/bin/
    - dpkg-deb --build package deploy-1.0.0.deb
  artifacts:
    paths:
      - deploy-1.0.0.deb

6. Deploy Contínuo e Monitoramento

6.1. Estratégias de deploy: ambientes staging vs produção, rollback automático

#!/bin/bash
# scripts/deploy.sh
ENV="${1:-staging}"
VERSION=$(cat VERSION)

deploy_staging() {
  scp "deploy-${VERSION}.deb" user@staging-server:/tmp/
  ssh user@staging-server "dpkg -i /tmp/deploy-${VERSION}.deb"
}

deploy_production() {
  deploy_staging  # Primeiro testa em staging
  scp "deploy-${VERSION}.deb" user@prod-server:/tmp/
  ssh user@prod-server "dpkg -i /tmp/deploy-${VERSION}.deb"
}

rollback() {
  local prev_version="$1"
  scp "deploy-${prev_version}.deb" user@prod-server:/tmp/
  ssh user@prod-server "dpkg -i /tmp/deploy-${prev_version}.deb"
}

6.2. Validação pós-deploy: testes de fumaça e health checks com scripts Bash

#!/bin/bash
# tests/smoke_test.sh
check_service() {
  if curl -f http://localhost:8080/health; then
    echo "Health check OK"
    return 0
  else
    echo "Health check FAILED" >&2
    return 1
  fi
}

check_version() {
  local expected="$1"
  local actual=$(curl -s http://localhost:8080/version)
  [ "$actual" = "$expected" ]
}

6.3. Notificações e logging: integração com Slack, e-mail e sistemas de alerta

#!/bin/bash
notify_slack() {
  local message="$1"
  curl -X POST -H 'Content-type: application/json' \
    --data "{\"text\":\"$message\"}" \
    https://hooks.slack.com/services/TOKEN
}

log_event() {
  local level="$1"
  local msg="$2"
  echo "[$(date +%Y-%m-%dT%H:%M:%S)] [$level] $msg" >> /var/log/deploy.log
}

# Uso no pipeline
if ! deploy_production; then
  notify_slack "Deploy falhou! Iniciando rollback..."
  log_event "ERROR" "Deploy production falhou"
  rollback "$PREV_VERSION"
fi

7. Manutenção e Evolução do Pipeline

7.1. Versionamento do pipeline: arquivos .gitlab-ci.yml, action.yml e Jenkinsfile

# .gitlab-ci.yml versionado
include:
  - template: 'Workflows/MergeRequest-Pipelines.gitlab-ci.yml'

variables:
  CI_DEBUG_TRACE: "false"

stages:
  - lint
  - test
  - build
  - deploy

lint:
  stage: lint
  script:
    - shellcheck scripts/*.sh

7.2. Métricas de qualidade: cobertura de testes, tempo de execução e taxa de falhas

#!/bin/bash
# scripts/metrics.sh
calculate_coverage() {
  local total=$(grep -c '^@test' tests/*.bats)
  local passed=$(grep -c '^ok' test_report.tap)
  echo "Cobertura: $((passed * 100 / total))%"
}

track_execution_time() {
  local start=$(date +%s%N)
  bats tests/*.bats
  local end=$(date +%s%N)
  echo "Tempo de execução: $(( (end - start) / 1000000 )) ms"
}

7.3. Atualização contínua: adaptação a novas versões do Bash e ferramentas auxiliares

# Verificar versão do Bash no pipeline
bash --version | grep -oP 'version \K[0-9]+\.[0-9]+'

# Atualizar ferramentas automaticamente
apt-get update && apt-get install -y shellcheck bats

Referências