Resumo
Esta pipeline automatiza a publicação da versão estática do projeto: o GitHub valida o código, gera a pasta final do site e envia os arquivos prontos para o servidor. No ambiente de produção, o conteúdo é publicado diretamente na pasta www da aplicação, com uma cópia temporária da versão anterior durante a troca. Isso deixa o processo mais leve, fácil de repetir e simples de operar no dia a dia.
Vantagens:
- o deploy fica mais previsível e reproduzível
- o build roda fora do servidor, poupando recursos da hospedagem
- a publicação do projeto fica simples no dia a dia com
git pushouRun workflow - o servidor recebe apenas os arquivos finais do build
- a manutenção do ambiente fica mais enxuta no servidor
- o pipeline evita armazenar artifacts intermediários no GitHub Actions
- se a publicação falhar, o workflow tenta restaurar automaticamente a cópia temporária anterior de
www
Use este pipeline se o projeto gera uma pasta final com index.html e arquivos estáticos, normalmente em dist. O servidor apenas entrega arquivos; ele não mantém um processo Node rodando. Se sua aplicação precisa de Node.js, Express, API, webhook, SSR ou processo em background, use o pipeline Node/PM2.
Antes de começar, tenha em mãos:
- acesso ao repositório no GitHub
- permissão para criar secrets no GitHub
- acesso ao painel da hospedagem
- usuário SSH da aplicação
- domínio ou subdomínio definido
- variáveis de ambiente usadas no build, se existirem
- branch principal confirmada, normalmente
main
Importante: a pasta www é limpa a cada deploy antes de receber os novos arquivos. Não mantenha uploads, arquivos manuais ou conteúdos importantes dentro dela.
Pipeline
git push na main
> job CI instala Node 22 no runner
> runner restaura cache do npm
> runner instala dependências com npm ci
> runner executa lint (se existir)
> runner executa testes (se existirem)
> job CD é liberado apenas se o job CI passar
> job CD instala Node 22 no runner
> runner restaura cache do npm
> runner instala dependências com npm ci
> runner busca o ~/.env do servidor somente se ele existir
> runner executa o build estático
> runner valida dist/index.html
> runner compacta o conteúdo de dist
> runner envia o arquivo compactado por SCP
> servidor resolve a pasta *.configr.cloud/www de destino
> servidor valida o arquivo compactado enviado
> servidor cria uma cópia temporária da pasta www atual
> servidor limpa a pasta de publicação atual
> servidor extrai o novo build na pasta www
> servidor remove o arquivo compactado
> servidor valida index.html no destino final
> se a validação falhar, servidor restaura a cópia temporária anterior de www
> servidor remove o backup temporário
Workflow do GitHub Actions
No projeto local:
mkdir -p .github/workflows
nano .github/workflows/ci-cd-static-pipeline.yml
Cole o conteúdo abaixo:
name: CI/CD Static Pipeline
on:
push:
branches:
- main
workflow_dispatch:
concurrency:
group: deploy-static-production
cancel-in-progress: true
jobs:
ci:
name: CI - Quality Checks
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run lint
run: npm run lint --if-present
- name: Run tests
run: npm run test --if-present
cd:
name: CD - Build and Deploy Files
needs: ci
runs-on: ubuntu-latest
env:
REMOTE_ARCHIVE_PATH: /home/${{ secrets.SERVER_USER }}/dist.tar.gz
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Configure SSH
env:
SSH_KEY: ${{ secrets.SERVER_SSH_KEY }}
run: |
if [ -z "$SSH_KEY" ]; then
echo "SERVER_SSH_KEY is empty or was not configured in GitHub Secrets."
exit 1
fi
mkdir -p ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/server_key
chmod 600 ~/.ssh/server_key
if ! ssh-keyscan -H "${{ secrets.SERVER_HOST }}" >> ~/.ssh/known_hosts; then
echo "Could not add SERVER_HOST to known_hosts. Check SERVER_HOST in GitHub Secrets."
exit 1
fi
- name: Validate SSH connection
run: |
if ! ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "echo 'SSH connection validated.'"; then
echo "Could not connect to the server using SSH. Check SERVER_HOST, SERVER_USER, SERVER_SSH_KEY, and the authorized public key."
exit 1
fi
- name: Fetch optional build environment from server
run: |
if ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "test -f ~/.env"; then
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "cat ~/.env" > .server.env
echo "Remote ~/.env found and loaded for the static build."
else
: > .server.env
echo "Remote ~/.env was not found. Continuing static build without server environment variables."
fi
- name: Build static site
run: |
if [ -s .server.env ]; then
set -a
. ./.server.env
set +a
fi
if ! npm run build; then
echo "Static build failed. Check the build script and required build-time variables."
exit 1
fi
- name: Validate build output
run: |
if [ ! -d dist ]; then
echo "Build output directory 'dist' was not found."
exit 1
fi
if [ ! -f dist/index.html ]; then
echo "Static build did not generate dist/index.html."
exit 1
fi
- name: Create deployment archive
run: |
if ! tar -czf dist.tar.gz -C dist .; then
echo "Could not create dist.tar.gz from the dist directory."
exit 1
fi
- name: Upload archive to server via SCP
run: |
if ! scp -i ~/.ssh/server_key dist.tar.gz "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:${REMOTE_ARCHIVE_PATH}"; then
echo "Could not upload dist.tar.gz to the server. Check SSH credentials and available disk space."
exit 1
fi
- name: Resolve target directory on server
id: resolve_target_dir
run: |
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
set -e
TARGET_DIR=\"\$(find \"\$HOME\" -maxdepth 1 \( -type l -o -type d \) -name '*.configr.cloud' 2>/dev/null | sort | while IFS= read -r candidate; do
[ -d \"\$candidate/www\" ] && printf '%s\n' \"\$candidate/www\"
done | head -n 1)\"
if [ -z \"\$TARGET_DIR\" ]; then
echo 'Could not find a target *.configr.cloud/www directory on the server.'
echo 'Create an HTML application in the hosting panel before running this workflow.'
exit 1
fi
printf '%s' \"\$TARGET_DIR\"
" > .target-dir
test -s .target-dir
TARGET_DIR="$(tr -d '\r' < .target-dir)"
BACKUP_DIR="$(dirname "$TARGET_DIR")/www-backup-tmp"
printf 'target_dir=%s\n' "$TARGET_DIR" >> "$GITHUB_OUTPUT"
printf 'backup_dir=%s\n' "$BACKUP_DIR" >> "$GITHUB_OUTPUT"
- name: Show resolved target directory
run: |
echo "Target directory: ${{ steps.resolve_target_dir.outputs.target_dir }}"
test -n "${{ steps.resolve_target_dir.outputs.target_dir }}"
- name: Validate deployment archive on server
run: |
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
if [ ! -f \"${REMOTE_ARCHIVE_PATH}\" ]; then
echo 'Deployment archive was not found on the server after upload.'
exit 1
fi
"
- name: Publish static files on server
run: |
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
set -e
ARCHIVE_PATH=\"${REMOTE_ARCHIVE_PATH}\"
TARGET_DIR=\"${{ steps.resolve_target_dir.outputs.target_dir }}\"
BACKUP_DIR=\"${{ steps.resolve_target_dir.outputs.backup_dir }}\"
restore_previous() {
if [ -d \"\$BACKUP_DIR\" ]; then
find \"\$TARGET_DIR\" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
cp -a \"\$BACKUP_DIR/.\" \"\$TARGET_DIR/\" 2>/dev/null || true
fi
}
mkdir -p \"\$TARGET_DIR\"
rm -rf -- \"\$BACKUP_DIR\"
mkdir -p \"\$BACKUP_DIR\"
cp -a \"\$TARGET_DIR/.\" \"\$BACKUP_DIR/\" 2>/dev/null || true
if ! find \"\$TARGET_DIR\" -mindepth 1 -maxdepth 1 -exec rm -rf {} +; then
echo 'Could not clean the target www directory before publishing.'
restore_previous
exit 1
fi
if ! tar -xzf \"\$ARCHIVE_PATH\" -C \"\$TARGET_DIR\"; then
echo 'Could not extract the static archive into the target www directory.'
restore_previous
exit 1
fi
rm -f \"\$ARCHIVE_PATH\"
"
- name: Validate published static site
run: |
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
set -e
TARGET_DIR=\"${{ steps.resolve_target_dir.outputs.target_dir }}\"
if [ ! -f \"\$TARGET_DIR/index.html\" ]; then
echo 'Published site validation failed: index.html was not found in the target directory.'
exit 1
fi
"
- name: Restore previous static files on failure
if: failure()
run: |
TARGET_DIR="${{ steps.resolve_target_dir.outputs.target_dir }}"
BACKUP_DIR="${{ steps.resolve_target_dir.outputs.backup_dir }}"
if [ -z "$TARGET_DIR" ] || [ -z "$BACKUP_DIR" ] || [ ! -f ~/.ssh/server_key ]; then
echo "Target directory, backup directory, or SSH key is not available. Skipping static rollback."
exit 0
fi
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
set -e
if [ ! -d \"$BACKUP_DIR\" ]; then
echo 'No previous static backup was found. Skipping rollback.'
exit 0
fi
find \"$TARGET_DIR\" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
cp -a \"$BACKUP_DIR/.\" \"$TARGET_DIR/\" 2>/dev/null || true
echo 'Previous static files were restored after deployment failure.'
"
- name: Remove uploaded archive
if: always()
run: |
if [ ! -f ~/.ssh/server_key ]; then
echo "SSH key was not configured. Skipping remote archive cleanup."
exit 0
fi
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
rm -f -- \"${REMOTE_ARCHIVE_PATH}\"
"
- name: Remove temporary static backup
if: always()
run: |
BACKUP_DIR="${{ steps.resolve_target_dir.outputs.backup_dir }}"
if [ -z "$BACKUP_DIR" ] || [ ! -f ~/.ssh/server_key ]; then
echo "Backup directory or SSH key is not available. Skipping static backup cleanup."
exit 0
fi
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
rm -rf -- \"$BACKUP_DIR\"
"
- name: Show published files
run: |
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
TARGET_DIR=\"${{ steps.resolve_target_dir.outputs.target_dir }}\"
ls -la \"\$TARGET_DIR\"
"
No nano, salve e saia com: Ctrl + O, Enter, Ctrl + X.
Depois:
git add .github/workflows/ci-cd-static-pipeline.yml ci-cd-static-pipeline.md
git commit -m "Adiciona fluxo de CI/CD para deploy estático"
git push
Passo a passo do primeiro deploy
0. Criar a aplicação e preparar o acesso SSH
Na hospedagem, crie uma aplicação do tipo HTML.
Antes de configurar as chaves do GitHub Actions, defina também uma senha de acesso manual ao servidor. Isso permite entrar por SSH para validar o caminho publicado, conferir arquivos em www e executar os comandos operacionais do processo.
Na hospedagem, acesse:
Painel da aplicação > Acessos > Senha SFTP/SSH > Alterar Senha
Nesta etapa:
- defina a senha do usuário da aplicação para o acesso manual via
SSH - confirme que
Usar SSHestá ativado
Depois, teste o acesso manual pela sua máquina local:
ssh <usuario>@<host>
Na sua máquina local, dentro da pasta do projeto, valide se ele realmente gera uma saída estática:
npm ci
npm run build
test -f dist/index.html
Se não existir dist/index.html, ajuste o build do projeto ou use outro pipeline.
1. Gerar a chave SSH do GitHub Actions
Na sua máquina local ou em uma máquina segura:
ssh-keygen -t ed25519 -C "github-actions-deploy" -f github_actions_deploy
Quando o terminal pedir a passphrase, deixe em branco e pressione Enter nas duas perguntas. Essa chave deve ser criada sem passphrase para que o GitHub Actions consiga usá-la sem prompt interativo.
Esse comando vai gerar:
github_actions_deploy: chave privadagithub_actions_deploy.pub: chave pública
Copie e cole sempre o conteúdo diretamente do arquivo original, sem alterar quebras de linha, sem adicionar aspas e sem editar manualmente o texto da chave.
Se quiser visualizar e copiar os conteúdos logo em seguida:
cat github_actions_deploy
cat github_actions_deploy.pub
Exemplo visual do que significa copiar a chave completa:
Chave privada:
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
Chave pública:
ssh-ed25519 ... github-actions-deploy
A cópia deve incluir tudo, inclusive as linhas de início e fim da chave privada.
2. Configurar as chaves e secrets
Copie o conteúdo completo de github_actions_deploy.pub para o servidor ou para a interface da hospedagem. Esta é a chave pública.
Opção 1:
Na hospedagem, acesse:
Painel da aplicação > Acessos > Senha SFTP/SSH > Alterar Senha > Chaves Autorizadas
Cole o conteúdo completo do arquivo github_actions_deploy.pub no campo Chaves Autorizadas, do início ao fim e sem modificar a estrutura da chave. Mantenha Usar SSH ativado e salve em Atualizar.
Opção 2:
No servidor:
mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys
Cole dentro desse arquivo o conteúdo completo de github_actions_deploy.pub, do início ao fim, exatamente como ele foi gerado.
No nano, salve e saia com: Ctrl + O, Enter, Ctrl + X.
Em seguida:
chmod 600 ~/.ssh/authorized_keys
No GitHub
Copie o conteúdo completo de github_actions_deploy para o secret SERVER_SSH_KEY no GitHub. Esta é a chave privada.
Depois, no repositório, acesse Settings > Secrets and variables > Actions > New repository secret.
Criar:
SERVER_HOSTValor:<ip-ou-host-do-servidor>SERVER_USERValor:<usuario>SERVER_SSH_KEYValor: cole a chave privada completa do arquivogithub_actions_deploy, do início ao fim e sem modificar a estrutura da chave
3. Criar o arquivo ~/.env no servidor, se necessário
Conteúdo opcional: crie ~/.env apenas se o build estático depender de variáveis de ambiente; se não depender, pule esta etapa.
No servidor:
ssh <usuario>@<ip-ou-host-do-servidor>
nano ~/.env
Se precisar dele, cole o conteúdo como no exemplo abaixo:
SITE_URL=https://<projeto.seudominio.com.br>
No nano, salve e saia com: Ctrl + O, Enter, Ctrl + X.
Se você criar ou alterar o ~/.env do servidor, basta disparar um novo deploy para gerar e publicar uma nova versão estática com esses valores.
Etapas de apoio
▸ Checklist antes de rodar o workflow
Antes de fazer o primeiro git push ou clicar em Run workflow, confira:
- workflow criado em
.github/workflows/ci-cd-static-pipeline.yml - aplicação criada como
HTMLna hospedagem - acesso manual por
SSHtestado - chave pública autorizada no servidor
- secrets
SERVER_HOST,SERVER_USEReSERVER_SSH_KEYcriados no GitHub - arquivo
~/.envcriado no servidor, apenas se o build precisar de variáveis npm run buildfuncionando localmente- arquivo
dist/index.htmlgerado localmente - branch principal confirmada, normalmente
main
▸ Como o deploy funciona no dia a dia
Deploy automático
O deploy automático acontece sempre que você envia código para a branch main.
Use normalmente:
git add .
git commit -m "Atualizar projeto"
git push
As etapas de lint e test rodam com --if-present, então o workflow continua compatível com projetos que ainda não possuem esses scripts.
Deploy manual
Se quiser rodar o mesmo processo sem fazer um novo commit, use o disparo manual do workflow no GitHub:
GitHub > Actions > CI/CD Static Pipeline > Run workflow
Isso executa exatamente a mesma esteira do deploy automático, reaproveitando o commit atual da main.
▸ Comandos úteis de monitoramento
Se quiser inspecionar os arquivos publicados diretamente:
ls -la ~/*.configr.cloud/www
Para validar a publicação estática no servidor:
curl https://<projeto.seudominio.com.br>
▸ Como saber que deu certo
Use os comandos da etapa anterior e confira estes resultados:
ls -la ~/*.configr.cloud/www: mostra o novoindex.htmle os arquivos publicadoscurl https://<projeto.seudominio.com.br>: retorna o HTML do site sem erro- GitHub Actions: o workflow terminou verde
- navegador: o domínio abre; se ainda mostrar conteúdo antigo, limpe o cache ou aguarde o cache/CDN da hospedagem
▸ Erros comuns
Use a mensagem do log do GitHub Actions como ponto de partida:
SERVER_SSH_KEY is empty or was not configured in GitHub Secrets.: o secretSERVER_SSH_KEYnão foi criado ou está vazioCould not add SERVER_HOST to known_hosts.:SERVER_HOSTestá vazio, incorreto ou inacessível pelo runnerCould not connect to the server using SSH.: reviseSERVER_HOST,SERVER_USER,SERVER_SSH_KEYe a chave pública autorizada no servidorLoad key ... error in libcrypto: chave privada colada de forma incompleta ou com estrutura alterada no secretSERVER_SSH_KEYPermission denied (publickey,password): chave pública não autorizada no servidor ouSERVER_USER/SERVER_HOSTincorretoRemote ~/.env was not found. Continuing static build without server environment variables.: comportamento normal quando o build estático não precisa de variáveis remotasStatic build failed.:npm run buildfalhou; confira scripts, dependências e variáveis exigidas pelo buildBuild output directory 'dist' was not found.: o build não gerou a pastadistStatic build did not generate dist/index.html.: o projeto não gerou uma saída estática compatívelCould not upload dist.tar.gz to the server.: falha de SSH, permissão ou espaço em disco no servidorCould not find a target *.configr.cloud/www directory: aplicação HTML não foi criada corretamente ou a pastawwwnão existeCould not clean the target www directory before publishing.: o usuário não tem permissão para limpar a pasta de publicaçãoPublished site validation failed: index.html was not found: a extração falhou ou o pacote enviado não contémindex.htmlPrevious static files were restored after deployment failure.: o workflow restaurou automaticamente a cópia temporária anterior dewww- domínio não atualizou: cache do navegador, cache da hospedagem ou arquivos antigos fora da pasta
www