# CI/CD — Sucesu V2

Pipeline GitLab CI/CD do portal Sucesu V2. Deploy é **SSH bare-metal** (sem Docker nos
ambientes de homologação e produção): o runner faz `git pull` + `artisan` + `composer`
no servidor de destino. O `vendor/` é **commitado**, portanto não há `composer install`
no deploy.

## Stages

| Stage    | Job              | Gatilho                                  | Ambiente |
|----------|------------------|------------------------------------------|----------|
| version  | `bump-version`   | push/merge em `master`                    | —        |
| deploy   | `deploy_homolog` | push/merge em `master`                    | Homologação |
| deploy   | `deploy_production` | push/merge em `producao`              | Produção |

- **Homologação** = branch `master`.
- **Produção** = branch `producao` (deploy automático no merge).

## O que cada job faz

### bump-version (master)
Incrementa o patch do arquivo `VERSION` (`vMAJOR.MINOR.PATCH - DD/MM/YYYY HH:MM`),
commita com `[skip ci]` e pusha. `allow_failure: true` — não bloqueia o pipeline.

### deploy_homolog / deploy_production
O script remoto vive em `ci/deploy.sh` (bash puro, sem indentação YAML) e é executado
no servidor via `ssh host 'bash -s' < ci/deploy.sh`, com `DEPLOY_DIR` e `DEPLOY_BRANCH`
passados como variáveis de ambiente. Passos:

1. **Atualiza o código** (`git fetch` + `git reset --hard origin/<branch>`), com log de commits
   (o `ci/backup.sh` é puxado neste passo, garantindo que existe antes de ser chamado).
2. **Backup pre-deploy** (`ci/backup.sh`): dump nativo do PostgreSQL (`pg_dump -Fc`,
   validado com `pg_restore --list`) em `backup/db/` e zip do `storage/app/public`
   em `backup/storage/` (estilo `scripts/backup_*.sh`, porém sem Docker). Falha de DB
   **aborta o deploy**; falha de storage é apenas warn (deploy continua).
3. `php artisan migrate --force` (apenas migrações — **sem seeds destrutivos**).
4. `php artisan package:discover` + `php artisan storage:link` (idempotentes).
5. `composer prod-optimize` — **último comando** (limpa e regeira `config:cache`,
   `route:cache`, `view:clear`, `v2:clear-page-cache`, `clear-compiled`).

Na **produção**, o site entra em manutenção (`php artisan down`) durante a migração,
com `trap` garantindo `php artisan up` ao final (mesmo em falha).

## Variáveis de CI (GitLab → Settings → CI/CD → Variables)

Toda a configuração de deploy vive em **UMA única variável** JSON,
`SUCESU_DEPLOY_CONFIG`, mais o `GIT_PUSH_TOKEN` (usado pelo `bump-version`):

| Variável               | Uso                                              |
|------------------------|--------------------------------------------------|
| `SUCESU_DEPLOY_CONFIG` | JSON com os dados de `homolog` e `producao`      |
| `GIT_PUSH_TOKEN`       | Token para o `bump-version` (opcional)           |

> **Tipo da variável:** use o tipo **File** no GitLab (não "Variable"). Assim o
> conteúdo do `.json` é gravado num arquivo temporário no runner e `$SUCESU_DEPLOY_CONFIG`
> passa a ser o **caminho desse arquivo** — evita caracteres não permitidos em variáveis
> de texto e o escape manual de `\n`. O `before_script` detecta se o valor é um arquivo
> (`os.path.isfile`) ou uma string JSON crua, e trata dos dois casos.
>
> **Não marque como "Mask variable".** O GitLab proíbe masking quando o valor contém
> whitespace (o JSON tem espaços/quebras) — daí o erro *"cannot contain whitespace
> characters"*. Variáveis do tipo **File** não são mascaráveis e não precisam ser: o
> conteúdo vai para um arquivo no runner e **não é impresso nos logs** (o deploy never
> faz `echo` da chave — ela só é gravada em `~/.ssh/sucesu_cicd` e usada via `ssh -i`).
> Marque como **Protected** (visível só em branches protegidas) se quiser restringir acesso.

Formato do JSON (o `ssh_key` deve vir com `\n` escapado):

```json
{
  "homolog": {
    "host": "hmg.exemplo.br",
    "port": 22,
    "user": "deploy",
    "dir":  "/var/www/sucesu",
    "ssh_key": "-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----\n"
  },
  "producao": {
    "host": "www.exemplo.br",
    "port": 22,
    "user": "deploy",
    "dir":  "/var/www/sucesu",
    "ssh_key": "-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----\n"
  }
}
```

O job seleciona o ambiente via `DEPLOY_ENV` (`homolog` no job de homolog,
`producao` no job de produção) e o `before_script` faz o parse com `python3`:
extrai `host`/`port`/`user`/`dir` para variáveis de shell e escreve `ssh_key`
em `~/.ssh/id_ed25519` (`chmod 600`). O host é fixado em `known_hosts` via
`ssh-keyscan` (`StrictHostKeyChecking=yes`, `BatchMode=yes`).

> **Observação de segurança:** variáveis do tipo JSON longas/multilinha **não podem
> ser mascaradas** pelo GitLab. Marque `SUCESU_DEPLOY_CONFIG` como **Protected** (visível
> só em branches protegidas) e restrinja o acesso em *Settings → CI/CD → Variables*.
> O segredo SSH nunca é hardcoded no `.gitlab-ci.yml`.

## Como registrar a chave SSH (ex.: cPanel)

1. Gere o par no servidor de origem (ou no runner) — RSA 4096 ou ed25519:
   ```bash
   ssh-keygen -t rsa -b 4096 -f ~/.ssh/sucesu_cicd -N ""
   ```
2. Adicione a **pública** (`~/.ssh/sucesu_cicd.pub`) ao `~/.ssh/authorized_keys`
   do **usuário de destino** em cada servidor (homolog e produção):
   ```bash
   cat ~/.ssh/sucesu_cicd.pub >> ~/.ssh/authorized_keys
   ```
3. Copie o conteúdo da **privada** (`~/.ssh/sucesu_cicd`) e converta para uma
   string JSON de linha única (com `\n` escapado). No cPanel/terminal:
   ```bash
   # opcao A (jq, se disponivel):
   jq -Rs . < ~/.ssh/sucesu_cicd
   # opcao B (awk):
   awk '{printf "%s\\n", $0}' ~/.ssh/sucesu_cicd
   ```
4. Cole o resultado no campo `ssh_key` do respectivo ambiente em
   `SUCESU_DEPLOY_CONFIG`. O tipo da chave (RSA/ed25519) não importa — o runner
   apenas grava o conteúdo em `~/.ssh/sucesu_cicd` e usa `ssh -i`.

O arquivo de exemplo está em `ci/gitlab_deploy_variables_example.json`.

## Segurança

- Chaves SSH separadas por ambiente e armazenadas como variáveis masked.
- `known_hosts` populado por `ssh-keyscan` → sem `StrictHostKeyChecking=no`.
- `set -euo pipefail` no script remoto (falha rápido).
- `git reset --hard` apenas em arquivos versionados; **sem `git clean`** → uploads e
  `.env` do servidor são preservados.

## Adicionar um novo servidor de deploy

1. Cadastre as 5 variáveis (`SSH_PRIVATE_KEY_*`, `*_HOST`, `*_PORT`, `*_USER`, `*_DIR`).
2. Copie o job `deploy_homolog`/`deploy_production` ajustando o `extends: .deploy`, as
   variáveis e a `rules` (branch de gatilho).

## Observações

- `composer prod-optimize` assume que não há `env()` fora de `config/*.php` (verificado).
- O ambiente local de desenvolvimento continua via `docker compose up -d`; o Docker **não**
  participa do deploy de homolog/produção.

## Pré-requisitos do servidor (bare-metal)

O deploy roda comandos nativos — **sem Docker**. O servidor de destino precisa de:

| Binário     | Uso no deploy                         |
|-------------|---------------------------------------|
| `git`       | `fetch` + `reset --hard`              |
| `php`       | `artisan migrate/down/up/...`         |
| `composer`  | `prod-optimize` (vendor é commitado)  |
| `pg_dump`   | dump de backup do PostgreSQL          |
| `pg_restore` | validação do dump (`--list`)         |
| `zip`       | zip do `storage/app/public`           |

Além disso, o `.env` deve existir em `DEPLOY_DIR` com `DB_DATABASE`, `DB_USERNAME` e
`DB_PASSWORD` (e `DB_HOST`/`DB_PORT` corretos para alcançar o PostgreSQL). O `ci/deploy.sh`
valida tudo isso no **pre-flight** e aborta cedo com mensagem clara se faltar algo.

Pós-deploy, um health-check opcional (`curl $APP_URL/`) confirma que o portal responde;
se o `curl` não estiver instalado, o passo é pulado com aviso.
