Bizu de Git: Git Flow e CI/CD no Forgejo

· CaioSimioni · Artigos

Este bizu mostra, passo a passo, como trabalhar com Git usando o fluxo Git Flow, mensagens no padrão Conventional Commits (em português) e um CI/CD no Forgejo Actions. Como exemplo, usamos um site de uma única página (HTML e CSS puros) chamado meu-site, hospedado em cosmos.simioni.dev.br, na organização projetos-web. Ajuste esses nomes para os seus.

Visão geral

Branch Para que serve Quem escreve nela
main Código estável. Cada versão publicada tem uma tag aqui. Só recebe merge de release/* e hotfix/*
develop Integração do trabalho em andamento. Recebe merge de feature/*
feature/* Uma tarefa ou funcionalidade. Você, no dia a dia
release/* Preparação de uma versão (changelog, ajustes finais). Você, na hora de lançar
hotfix/* Correção urgente, criada a partir da main. Você, quando algo quebra em produção
deploy Espelho do que deve ir para o servidor. Só avança com merge --ff-only a partir da main

E o que o Forgejo faz em cada evento:

Evento O que o CI/CD executa
Push em develop, feature/*, release/*, hotfix/* ou pull request Lint, testes e build
Push de uma tag vX.Y.Z Lint, testes, build e pacote publicado no registry do Forgejo
Push na branch deploy Deploy via SSH, que só roda se a variável DEPLOY_ENABLED for true (hoje fica desativado)

O Git guarda o histórico e o Forgejo reage a ele. Quem comanda o pipeline é o que você envia: branches, pull requests e tags.

Configuração única (por máquina)

1. Instalar o Git no Windows

winget install --id Git.Git -e

Feche e abra o terminal depois da instalação e confirme com git --version.

2. Identificar-se e ajustar o básico

git config --global user.name "Seu Nome"
git config --global user.email "Este endereço de email está sendo protegido de spambots. Você precisa do JavaScript ativado para vê-lo."
git config --global init.defaultBranch main
git config --global pull.rebase false
git config --global fetch.prune true
  • Use no user.email o mesmo e-mail cadastrado no Forgejo, para os commits ficarem ligados ao seu usuário.
  • fetch.prune remove automaticamente as referências de branches que já foram apagadas no servidor.

3. Criar uma chave SSH

ssh-keygen -t ed25519 -C "Este endereço de email está sendo protegido de spambots. Você precisa do JavaScript ativado para vê-lo."
type $env:USERPROFILE\.ssh\id_ed25519.pub

O segundo comando (PowerShell) mostra a chave pública. Copie o conteúdo e cadastre em Configurações → Chaves SSH/GPG no Forgejo. Teste a conexão:

ssh -T Este endereço de email está sendo protegido de spambots. Você precisa do JavaScript ativado para vê-lo.

Se o seu Forgejo usa uma porta SSH diferente da 22, use a URL no formato ssh://Este endereço de email está sendo protegido de spambots. Você precisa do JavaScript ativado para vê-lo.:PORTA/projetos-web/meu-site.git.

Criar o repositório de exemplo

1. Criar o repositório vazio no Forgejo

Crie projetos-web/meu-site sem README, sem .gitignore e sem licença. Ele precisa estar vazio para receber o primeiro push sem conflito.

2. Estrutura do projeto

meu-site/
├── .forgejo/
│   └── workflows/
│       ├── ci.yml
│       └── deploy.yml
├── scripts/
│   ├── build.sh
│   └── check.sh
├── src/
│   ├── index.html
│   └── css/
│       └── style.css
├── .gitattributes
├── .gitignore
├── .stylelintrc.json
├── CHANGELOG.md
└── README.md

3. Arquivos da página

src/index.html

<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Meu Site</title>
  <link rel="stylesheet" href="/css/style.css">
</head>
<body>
  <main>
    <h1>Olá, mundo!</h1>
    <p>Página publicada com Git e Forgejo.</p>
  </main>
</body>
</html>

src/css/style.css

body {
  font-family: system-ui, sans-serif;
  margin: 0;
  display: grid;
  min-height: 100vh;
  place-items: center;
}

h1 {
  color: #1d4ed8;
}

4. Arquivos de apoio

.gitignore

dist/
node_modules/
*.tar.gz
.env
Thumbs.db

.gitattributes (evita que o Windows grave os scripts com final de linha CRLF, que quebra no Linux do runner)

* text=auto
*.sh text eol=lf
*.yml text eol=lf

.stylelintrc.json

{
  "rules": {
    "block-no-empty": true,
    "color-no-invalid-hex": true,
    "declaration-block-no-duplicate-properties": true,
    "property-no-unknown": true,
    "unit-no-unknown": true
  }
}

scripts/check.sh (o "teste" da página: confere o essencial)

#!/usr/bin/env sh
set -eu

fail() {
  echo "ERRO: $1" >&2
  exit 1
}

[ -f src/index.html ] || fail "src/index.html não existe"
grep -q "<title>" src/index.html || fail "falta a tag <title>"
grep -q "<html lang=" src/index.html || fail "falta o atributo lang no <html>"
grep -q 'name="viewport"' src/index.html || fail "falta a meta viewport"

for arquivo in $(grep -o 'href="css/[^"]*"' src/index.html | cut -d'"' -f2); do
  [ -f "src/$arquivo" ] || fail "arquivo referenciado não existe: $arquivo"
done

echo "Testes OK"

scripts/build.sh

#!/usr/bin/env sh
set -eu

rm -rf dist
mkdir -p dist
cp -r src/. dist/
printf '%s\n' "${BUILD_VERSION:-dev}" > dist/version.txt

echo "Build pronto em dist/"

Os workflows chamam os scripts com sh scripts/build.sh, então não é preciso dar permissão de execução (o que no Windows costuma dar dor de cabeça).

5. Primeiro commit e as branches fixas

git init -b main
git add .
git commit -m "chore: cria estrutura inicial do site"
git remote add origin Este endereço de email está sendo protegido de spambots. Você precisa do JavaScript ativado para vê-lo.:projetos-web/meu-site.git
git push -u origin main

git switch -c develop
git push -u origin develop

git switch -c deploy main
git push -u origin deploy

git switch develop

Depois disso o repositório tem as três branches permanentes: main, develop e deploy. Em Configurações → Branches do repositório, vale definir develop como branch padrão, para os pull requests já nascerem apontando para ela.

Padrão de commits (Conventional Commits em português)

O formato é tipo: descrição curta no imperativo. O tipo fica em inglês (é o padrão da convenção) e a descrição em português.

Tipo Quando usar Exemplo
feat Funcionalidade nova feat: adiciona seção de contato
fix Correção de erro fix: corrige link quebrado do rodapé
docs Só documentação docs: explica como rodar o build
style Formatação, sem mudar o comportamento style: padroniza indentação do CSS
refactor Reorganização sem mudar o resultado refactor: separa estilos do cabeçalho
test Testes test: verifica meta viewport
ci Pipeline e workflows ci: inclui etapa de lint do CSS
chore Tarefas de manutenção e releases chore: prepara release 1.0.0

Para uma mudança que quebra compatibilidade, coloque ! depois do tipo (feat!: troca a estrutura de pastas) ou escreva BREAKING CHANGE: no rodapé da mensagem. Se quiser escopo, use feat(css): ....

O CI/CD no Forgejo Actions

Os workflows ficam em .forgejo/workflows/. O runner do servidor atende pelos labels ubuntu-latest e debian, e os exemplos usam ubuntu-latest.

Pipeline principal: .forgejo/workflows/ci.yml

name: CI

on:
  push:
    branches: [main, develop, 'feature/**', 'release/**', 'hotfix/**']
    tags: ['v*']
  pull_request:
    branches: [main, develop]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - name: Lint do HTML
        run: npx --yes htmlhint "src/**/*.html"
      - name: Lint do CSS
        run: npx --yes stylelint --config .stylelintrc.json "src/**/*.css"

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Testes
        run: sh scripts/check.sh

  build:
    runs-on: ubuntu-latest
    needs: [lint, test]
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: sh scripts/build.sh

  package:
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    needs: [build]
    env:
      VERSAO: ${{ github.ref_name }}
      SERVIDOR: ${{ github.server_url }}
      DONO: ${{ github.repository_owner }}
      PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
    steps:
      - uses: actions/checkout@v4
      - name: Build com a versão da tag
        run: BUILD_VERSION="${VERSAO#v}" sh scripts/build.sh
      - name: Empacotar
        run: tar -czf "meu-site-${VERSAO#v}.tar.gz" -C dist .
      - name: Publicar no registry do Forgejo
        run: |
          curl --fail --silent --show-error \
            -H "Authorization: token ${PACKAGE_TOKEN}" \
            --upload-file "meu-site-${VERSAO#v}.tar.gz" \
            "${SERVIDOR}/api/packages/${DONO}/generic/meu-site/${VERSAO#v}/meu-site-${VERSAO#v}.tar.gz"
  • O job package só roda quando o push é de uma tag que começa com v.
  • O pacote vai para o registry Generic da organização, em Pacotes, e fica com o nome meu-site.
  • Crie o segredo PACKAGE_TOKEN em Configurações do repositório → Actions → Segredos. Ele deve ser um token de um usuário membro da organização, com leitura e escrita em package, igual ao do registry Cargo.

Deploy desativado: .forgejo/workflows/deploy.yml

name: Deploy

on:
  push:
    branches: [deploy]

jobs:
  deploy:
    if: ${{ vars.DEPLOY_ENABLED == 'true' }}
    runs-on: ubuntu-latest
    env:
      SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
      KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
      DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
      DEPLOY_PORT: ${{ vars.DEPLOY_PORT }}
      DEPLOY_USER: ${{ vars.DEPLOY_USER }}
      DEPLOY_PATH: ${{ vars.DEPLOY_PATH }}
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: BUILD_VERSION="$(git rev-parse --short HEAD)" sh scripts/build.sh
      - name: Instalar rsync e ssh
        run: |
          if ! command -v rsync > /dev/null; then
            apt-get update
            apt-get install -y rsync openssh-client
          fi
      - name: Preparar o SSH
        run: |
          mkdir -p ~/.ssh
          printf '%s\n' "$SSH_KEY" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts
      - name: Enviar os arquivos
        run: |
          rsync -az --delete \
            -e "ssh -p ${DEPLOY_PORT:-22}" \
            dist/ "${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/"

Enquanto a variável DEPLOY_ENABLED não existir (ou for diferente de true), o job aparece como ignorado a cada push na deploy e nada é enviado. O arquivo já está pronto e versionado, então ligar o deploy depois não exige mexer no código.

Como ligar o deploy no futuro (só documentação)

  1. Gere uma chave exclusiva para o deploy, sem senha: ssh-keygen -t ed25519 -f deploy_key -C "forgejo-deploy".
  2. Coloque o conteúdo de deploy_key.pub no ~/.ssh/authorized_keys do usuário de deploy no servidor.
  3. Pegue a impressão digital do servidor com ssh-keyscan -p 22 seu-servidor.com.br.
  4. Em Configurações do repositório → Actions → Segredos, crie DEPLOY_SSH_KEY (conteúdo da chave privada) e DEPLOY_KNOWN_HOSTS (saída do ssh-keyscan).
  5. Em Actions → Variáveis, crie DEPLOY_HOST, DEPLOY_PORT, DEPLOY_USER e DEPLOY_PATH (pasta do site no servidor).
  6. Por último, crie a variável DEPLOY_ENABLED com o valor true.
  7. Apague os dois arquivos da chave da sua máquina: deploy_key e deploy_key.pub.

Para desligar de novo, basta apagar a variável DEPLOY_ENABLED ou mudar o valor para false.

Fluxo do dia a dia

1. Criar uma feature

git switch develop
git pull
git switch -c feature/secao-contato

Trabalhe, faça commits pequenos e frequentes e envie a branch:

git add .
git commit -m "feat: adiciona seção de contato"
git push -u origin feature/secao-contato

O CI roda lint, testes e build. Abra um pull request de feature/secao-contato para develop, espere o CI ficar verde e faça o merge (pode usar a opção de merge commit, que mantém a história do Git Flow). Depois limpe:

git switch develop
git pull
git branch -d feature/secao-contato

Se a branch remota não foi apagada pelo Forgejo, use git push origin --delete feature/secao-contato.

2. Lançar uma versão (release)

git switch develop
git pull
git switch -c release/1.0.0

Atualize o CHANGELOG.md e faça o commit:

git add CHANGELOG.md
git commit -m "chore: prepara release 1.0.0"
git push -u origin release/1.0.0

Com o CI verde, leve a release para a main, crie a tag e envie tudo:

git switch main
git pull
git merge --no-ff release/1.0.0 -m "chore: release 1.0.0"
git tag -a v1.0.0 -m "Versão 1.0.0"
git push origin main v1.0.0

O push da tag v1.0.0 dispara o pipeline completo e publica o pacote meu-site 1.0.0 no registry. Agora traga as mudanças de volta para a develop e limpe a branch:

git switch develop
git merge --no-ff release/1.0.0
git push origin develop
git branch -d release/1.0.0
git push origin --delete release/1.0.0

Se a main estiver protegida contra push direto, faça o merge da release por pull request e crie a tag depois, já com a main atualizada: git switch main, git pull, git tag -a v1.0.0 -m "Versão 1.0.0", git push origin v1.0.0.

3. Corrigir algo urgente (hotfix)

git switch main
git pull
git switch -c hotfix/1.0.1

Corrija, faça o commit (fix: corrige link quebrado do rodapé) e depois:

git switch main
git merge --no-ff hotfix/1.0.1 -m "chore: hotfix 1.0.1"
git tag -a v1.0.1 -m "Versão 1.0.1"
git push origin main v1.0.1

git switch develop
git merge --no-ff hotfix/1.0.1
git push origin develop
git branch -d hotfix/1.0.1

4. Colocar no ar (branch deploy)

git switch deploy
git pull
git merge --ff-only main
git push origin deploy

O --ff-only garante que a deploy seja sempre uma cópia exata da main, sem commits próprios. Se o merge recusar, algo foi escrito direto na deploy e precisa ser investigado antes de continuar. Com o deploy desativado, esse push só gera a execução "ignorada" no Forgejo.

Comandos que salvam o dia

Comando Para que serve
git status Ver o que mudou e em qual branch você está
git log --oneline --graph --decorate --all Ver o histórico e as branches em forma de árvore
git diff e git diff --staged Ver o que mudou antes de adicionar e antes de commitar
git restore arquivo Descartar alterações ainda não commitadas de um arquivo
git restore --staged arquivo Tirar um arquivo da área de preparo (git add)
git commit --amend Corrigir o último commit, só se ele ainda não foi enviado
git stash e git stash pop Guardar o trabalho pela metade e recuperar depois
git switch - Voltar para a branch anterior
git tag -n Listar as tags com suas mensagens
git reflog Achar commits "perdidos" depois de um erro

Proteger as branches

Em Configurações do repositório → Branches, crie regras de proteção:

  • main e develop: exigir pull request e exigir que o CI esteja verde antes do merge.
  • deploy: permitir push apenas para você (ou para quem pode publicar), já que essa branch é a que vai para o servidor.
  • Para a main, se você prefere fazer o merge da release pela linha de comando, inclua o seu usuário na lista de quem pode dar push.

Problemas comuns

Sintoma Causa e solução
Permission denied (publickey) A chave pública não está cadastrada no Forgejo, ou o SSH está usando outra chave. Teste com ssh -T Este endereço de email está sendo protegido de spambots. Você precisa do JavaScript ativado para vê-lo..
rejected ... non-fast-forward Alguém (ou você, em outra máquina) enviou commits antes. Rode git pull, resolva conflitos se houver e envie de novo.
A tag não dispara o CI O git push comum não envia tags. Use git push origin v1.0.0 (ou inclua a tag no push da main).
O workflow não aparece no Forgejo O arquivo precisa estar em .forgejo/workflows/ com extensão .yml, e o Actions precisa estar ativo no repositório.
O job fica eternamente em "Aguardando" O runs-on não bate com os labels do runner (ubuntu-latest ou debian), ou o runner está offline.
bad interpreter: ^M ao rodar um script O arquivo foi salvo com CRLF no Windows. Confira o .gitattributes e regrave o script com final de linha LF.
Erro 401 ou 403 ao publicar o pacote O segredo PACKAGE_TOKEN está errado, sem permissão de escrita em package, ou o usuário não é membro da organização.
Erro 409 ao publicar o pacote Essa versão já existe no registry. Corrija e lance uma nova versão (por exemplo v1.0.1).
O job de deploy aparece como "ignorado" É o esperado enquanto DEPLOY_ENABLED não for true.
npx ou node não encontrado no lint Confira se o passo actions/setup-node vem antes do lint.

Segurança

  • Nunca versione tokens, chaves privadas ou arquivos .env. Eles ficam somente em Segredos do Forgejo. Se algo vazar, revogue e gere outro.
  • Use uma chave SSH dedicada ao deploy, diferente da sua chave pessoal, e apague a cópia local depois de cadastrá-la.
  • Guarde o known_hosts do servidor como segredo, em vez de aceitar qualquer host automaticamente.
  • Proteja main, develop e deploy para que nada chegue a elas sem passar pelo CI.
  • Não use git push --force em branches compartilhadas.

Resumo do ciclo

  1. feature/* sai da develop e volta para ela por pull request.
  2. release/* sai da develop, vai para a main com uma tag vX.Y.Z e o pacote é publicado.
  3. A main avança a deploy com --ff-only e, quando o deploy estiver ligado, o site é atualizado.
  4. hotfix/* sai da main e volta para a main e para a develop.

<< voltar para Artigos