Compare commits

..

7 Commits

Author SHA1 Message Date
26a68e1823 fix: restaurar deploy-staging via workflow_run (Gitea ≥1.25)
All checks were successful
CI / static (push) Successful in 2m38s
CI / unit (push) Successful in 3m40s
CI / feature (push) Successful in 2m55s
CI / container (push) Successful in 1m11s
CI / browser (push) Successful in 4m22s
Remove job/reusable workflow do CI. Staging volta a ser workflow
separado após CI; 1.24 não implementava o trigger.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 16:01:56 -03:00
ac08b5f064 fix: staging deploy via needs no CI, não workflow_run
Some checks failed
CI / static (push) Successful in 2m35s
CI / unit (push) Successful in 3m36s
CI / feature (push) Successful in 2m31s
CI / container (push) Successful in 1m0s
CI / browser (push) Successful in 4m11s
CI / deploy-staging (push) Failing after 6m19s
workflow_run nunca disparou no Gitea 1.24 com CI multi-job.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 15:33:29 -03:00
91c0d412fa fix: nomes únicos para containers CI no docker.sock compartilhado
All checks were successful
CI / static (push) Successful in 2m14s
CI / unit (push) Successful in 3m50s
CI / feature (push) Successful in 2m48s
CI / container (push) Successful in 1m3s
CI / browser (push) Successful in 3m54s
Cancel/retry deixava amare-health órfão → Conflict no job container.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 15:21:38 -03:00
dd82ca2dca fix: paralelizar CI no act_runner (rede por job, sem :8000 no host)
Some checks failed
CI / static (push) Successful in 2m48s
CI / unit (push) Successful in 3m54s
CI / feature (push) Successful in 2m53s
CI / container (push) Failing after 55s
CI / browser (push) Successful in 4m18s
capacity>1 exige rede isolada por job e containers aninhados via DNS,
sem publish de host ports que colidem entre jobs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 15:12:07 -03:00
87a019109d fix: CI Postgres via hostname, sem bind :5432 no host
Some checks failed
CI / container (push) Waiting to run
CI / static (push) Successful in 1m24s
CI / unit (push) Failing after 1m31s
CI / feature (push) Failing after 1m14s
CI / browser (push) Failing after 2m10s
act_runner em VPS compartilhada falhava com port already
allocated; jobs usam service postgres na rede do job.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 15:02:24 -03:00
1468a23145 fix: remover actions/cache no Gitea Actions
Some checks failed
CI / static (push) Successful in 1m25s
CI / unit (push) Failing after 1s
CI / feature (push) Failing after 1s
CI / browser (push) Failing after 0s
CI / container (push) Failing after 2m38s
Job containers nao alcancam o cache do act_runner
(ETIMEDOUT ~5m). Tira cache steps e Buildx type=gha.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 14:57:12 -03:00
f730e766f5 fix: secrets de registry sem prefixo GITEA_
Some checks failed
CI / unit (push) Waiting to run
CI / static (push) Successful in 6m23s
CI / feature (push) Failing after 0s
CI / browser (push) Failing after 0s
CI / container (push) Has been cancelled
Gitea rejeita nomes de secret reservados GITEA_*; workflows
e runbook passam a REGISTRY_PAT / REGISTRY_USER.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 14:44:04 -03:00
4 changed files with 82 additions and 83 deletions

View File

@@ -18,7 +18,9 @@ env:
BCRYPT_ROUNDS: 4
CACHE_STORE: database
DB_CONNECTION: pgsql
DB_HOST: 127.0.0.1
# Service hostname on the per-job network (act_runner with empty
# container.network). Do not publish host :5432/:8000 — parallel jobs collide.
DB_HOST: postgres
DB_PORT: 5432
DB_DATABASE: amare_test
DB_USERNAME: amare
@@ -40,12 +42,6 @@ jobs:
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
coverage: none
- uses: actions/cache@v5
with:
path: ~/.composer/cache/files
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
restore-keys: composer-${{ runner.os }}-
- run: composer validate --strict
- run: composer install --no-interaction --prefer-dist
- run: composer pint:check
@@ -74,8 +70,6 @@ jobs:
POSTGRES_DB: amare_test
POSTGRES_USER: amare
POSTGRES_PASSWORD: secret
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U amare -d amare_test"
--health-interval 5s
@@ -91,18 +85,6 @@ jobs:
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
coverage: pcov
- uses: actions/cache@v5
with:
path: ~/.composer/cache/files
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
restore-keys: composer-${{ runner.os }}-
- uses: actions/cache@v5
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: npm-${{ runner.os }}-
- run: composer install --no-interaction --prefer-dist
- run: npm ci
- run: npm run build
@@ -126,8 +108,6 @@ jobs:
POSTGRES_DB: amare_test
POSTGRES_USER: amare
POSTGRES_PASSWORD: secret
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U amare -d amare_test"
--health-interval 5s
@@ -143,18 +123,6 @@ jobs:
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
coverage: none
- uses: actions/cache@v5
with:
path: ~/.composer/cache/files
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
restore-keys: composer-${{ runner.os }}-
- uses: actions/cache@v5
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: npm-${{ runner.os }}-
- run: composer install --no-interaction --prefer-dist
- run: npm ci
- run: npm run build
@@ -171,8 +139,6 @@ jobs:
POSTGRES_DB: amare_test
POSTGRES_USER: amare
POSTGRES_PASSWORD: secret
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U amare -d amare_test"
--health-interval 5s
@@ -188,18 +154,6 @@ jobs:
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
coverage: none
- uses: actions/cache@v5
with:
path: ~/.composer/cache/files
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
restore-keys: composer-${{ runner.os }}-
- uses: actions/cache@v5
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: npm-${{ runner.os }}-
- run: composer install --no-interaction --prefer-dist
- run: npm ci
- run: npm run build
@@ -212,15 +166,28 @@ jobs:
- name: Run browser tests against FrankenPHP container
run: |
docker run -d --name amare-web \
# Join the per-job network (act_runner creates one when
# container.network is empty). No host -p: parallel jobs would
# collide on :8000/:5432; DNS names work on the job network.
# Container --name is global on the shared docker.sock host —
# include run id or leftovers from cancelled jobs Conflict.
JOB_CID="$(hostname)"
JOB_NET="$(docker inspect -f '{{range $k, $_ := .NetworkSettings.Networks}}{{println $k}}{{end}}' "$JOB_CID" | head -n1)"
test -n "$JOB_NET"
WEB_NAME="amare-web-${GITHUB_RUN_ID:-$$}"
docker rm -f "$WEB_NAME" 2>/dev/null || true
docker run -d --name "$WEB_NAME" \
--network "$JOB_NET" \
--network-alias amare-web \
-e APP_ENV=testing \
-e APP_KEY="${APP_KEY}" \
-e APP_URL=http://127.0.0.1:8000 \
-e APP_URL=http://amare-web:8000 \
-e APP_LOCALE=pt_BR \
-e APP_FALLBACK_LOCALE=pt_BR \
-e APP_TIMEZONE=America/Sao_Paulo \
-e DB_CONNECTION=pgsql \
-e DB_HOST=host.docker.internal \
-e DB_HOST=postgres \
-e DB_PORT=5432 \
-e DB_DATABASE=amare_test \
-e DB_USERNAME=amare \
@@ -228,26 +195,28 @@ jobs:
-e SESSION_DRIVER=database \
-e CACHE_STORE=database \
-e QUEUE_CONNECTION=database \
--add-host=host.docker.internal:host-gateway \
-v "${GITHUB_WORKSPACE}/storage/app/public:/app/storage/app/public" \
-p 8000:8000 \
amare-app:ci
cleanup() { docker rm -f "$WEB_NAME" >/dev/null 2>&1 || true; }
trap cleanup EXIT
for i in $(seq 1 30); do
if curl -fsS http://127.0.0.1:8000/up; then
if curl -fsS http://amare-web:8000/up; then
break
fi
sleep 2
done
curl -fsS http://127.0.0.1:8000/up
./vendor/bin/pest --testsuite=Browser
curl -fsS http://amare-web:8000/up
APP_URL=http://amare-web:8000 ./vendor/bin/pest --testsuite=Browser
- name: Collect failure diagnostics
if: failure()
run: |
mkdir -p artifacts/browser
docker logs amare-web > artifacts/browser/container.log 2>&1 || true
WEB_NAME="amare-web-${GITHUB_RUN_ID:-$$}"
docker logs "$WEB_NAME" > artifacts/browser/container.log 2>&1 || true
cp -R storage/logs artifacts/browser/app-logs 2>/dev/null || true
- name: Upload browser failure artifacts
@@ -269,10 +238,21 @@ jobs:
- name: Verify container healthcheck and storage link
run: |
docker run -d --name amare-health \
# Same per-job network as the step container — no host :8000
# publish (collides when capacity > 1). Unique --name: docker.sock
# is shared across jobs; leftovers from cancelled runs Conflict.
JOB_CID="$(hostname)"
JOB_NET="$(docker inspect -f '{{range $k, $_ := .NetworkSettings.Networks}}{{println $k}}{{end}}' "$JOB_CID" | head -n1)"
test -n "$JOB_NET"
HEALTH_NAME="amare-health-${GITHUB_RUN_ID:-$$}"
docker rm -f "$HEALTH_NAME" 2>/dev/null || true
docker run -d --name "$HEALTH_NAME" \
--network "$JOB_NET" \
--network-alias amare-health \
-e APP_ENV=production \
-e APP_KEY="${{ env.APP_KEY }}" \
-e APP_URL=http://127.0.0.1:8000 \
-e APP_URL=http://amare-health:8000 \
-e APP_DEBUG=false \
-e DB_CONNECTION=pgsql \
-e DB_HOST=127.0.0.1 \
@@ -280,16 +260,18 @@ jobs:
-e DB_DATABASE=amare \
-e DB_USERNAME=amare \
-e DB_PASSWORD=secret \
-p 8000:8000 \
amare-app:ci
cleanup() { docker rm -f "$HEALTH_NAME" >/dev/null 2>&1 || true; }
trap cleanup EXIT
for i in $(seq 1 30); do
if curl -fsS http://127.0.0.1:8000/up; then
docker exec amare-health test -L /app/public/storage
if curl -fsS http://amare-health:8000/up; then
docker exec "$HEALTH_NAME" test -L /app/public/storage
exit 0
fi
sleep 2
done
docker logs amare-health
docker logs "$HEALTH_NAME"
exit 1

View File

@@ -1,10 +1,16 @@
name: Deploy staging
# Requires Gitea >= 1.25 for workflow_run (1.24.x has no trigger match).
on:
workflow_run:
workflows: [CI]
types: [completed]
branches: [main]
workflows:
- CI
- ci.yml
types:
- completed
branches:
- main
workflow_dispatch:
permissions:
contents: read
@@ -22,20 +28,21 @@ jobs:
deploy:
name: publish-and-deploy-staging
if: >-
gitea.event.workflow_run.conclusion == 'success' &&
gitea.event.workflow_run.event == 'push' &&
gitea.event.workflow_run.head_branch == 'main'
gitea.event_name == 'workflow_dispatch' ||
(gitea.event.workflow_run.conclusion == 'success' &&
gitea.event.workflow_run.event == 'push' &&
gitea.event.workflow_run.head_branch == 'main')
runs-on: ubuntu-latest
steps:
- name: Checkout deployed SHA
uses: actions/checkout@v4
with:
ref: ${{ gitea.event.workflow_run.head_sha }}
ref: ${{ gitea.event.workflow_run.head_sha || gitea.sha }}
- name: Set image metadata
id: meta
run: |
SHA="${{ gitea.event.workflow_run.head_sha }}"
SHA="${{ gitea.event.workflow_run.head_sha || gitea.sha }}"
SHORT_SHA="${SHA:0:7}"
IMAGE="${REGISTRY}/${IMAGE_NAME}"
IMAGE="$(echo "$IMAGE" | tr '[:upper:]' '[:lower:]')"
@@ -49,8 +56,8 @@ jobs:
registry: ${{ env.REGISTRY }}
# Gitea's GITEA_TOKEN cannot push OCI packages (gitea#23642); a PAT
# with read:package/write:package scopes is required instead.
username: ${{ secrets.GITEA_REGISTRY_USER }}
password: ${{ secrets.GITEA_PAT }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PAT }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -63,8 +70,6 @@ jobs:
tags: |
${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.sha }}
${{ steps.meta.outputs.image }}:staging
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Deploy staging on Dokploy
env:

View File

@@ -56,8 +56,8 @@ jobs:
registry: ${{ env.REGISTRY }}
# Gitea's GITEA_TOKEN cannot push OCI packages (gitea#23642); a PAT
# with read:package/write:package scopes is required instead.
username: ${{ secrets.GITEA_REGISTRY_USER }}
password: ${{ secrets.GITEA_PAT }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PAT }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

View File

@@ -33,7 +33,7 @@ Promote (manual) → retag same digest as :production (no rebuild)
5. Two R2 buckets (or prefixes) and Resend credentials for each environment as needed.
6. Domains (or temporary Dokploy/traefik.me hosts) pointing at the VPS with TLS.
Note: Gitea's `GITEA_TOKEN` cannot push OCI packages ([gitea#23642](https://github.com/go-gitea/gitea/issues/23642)); registry authentication in the workflows uses a PAT (`GITEA_PAT`), not the token.
Note: Gitea's `GITEA_TOKEN` cannot push OCI packages ([gitea#23642](https://github.com/go-gitea/gitea/issues/23642)); registry authentication in the workflows uses a PAT (`REGISTRY_PAT`), not the token. Secret names must not use the reserved `GITEA_` prefix (Gitea rejects them as invalid).
## Create Compose stacks
@@ -135,26 +135,38 @@ Repository secrets (Gitea → Settings → Actions → Secrets) used by workflow
| `DOKPLOY_PRODUCTION_COMPOSE_ID` | Production **Compose** service id (not an Application id) |
| `STAGING_URL` | Public origin for staging smoke (e.g. `https://staging.example.com`) |
| `PRODUCTION_URL` | Public origin for production smoke |
| `GITEA_PAT` | Personal Access Token with `read:package` + `write:package` scopes — used to push images to `git.hellomanoel.com` |
| `GITEA_REGISTRY_USER` | Gitea username that owns `GITEA_PAT` (`manoel-freitas`) |
| `REGISTRY_PAT` | Personal Access Token with `read:package` + `write:package` scopes — used to push images to `git.hellomanoel.com` (do not name secrets `GITEA_*`; that prefix is reserved) |
| `REGISTRY_USER` | Gitea username that owns `REGISTRY_PAT` (`manoel-freitas`) |
HTTP 404 from `compose.deploy` usually means the compose id is wrong (Application id instead of Compose) or `DOKPLOY_URL` still includes `/api`.
The automatic `GITEA_TOKEN` runs workflows but **cannot push OCI packages** ([gitea#23642](https://github.com/go-gitea/gitea/issues/23642)); registry authentication therefore uses `GITEA_PAT` + `GITEA_REGISTRY_USER`. No Laravel/`APP_KEY`/DB/R2/Resend secrets belong in Gitea for this pipeline.
The automatic `GITEA_TOKEN` runs workflows but **cannot push OCI packages** ([gitea#23642](https://github.com/go-gitea/gitea/issues/23642)); registry authentication therefore uses `REGISTRY_PAT` + `REGISTRY_USER`. No Laravel/`APP_KEY`/DB/R2/Resend secrets belong in Gitea for this pipeline.
## Workflows
Workflows live in [`.gitea/workflows/`](../../.gitea/workflows/).
`actions/cache` and Docker Buildx `type=gha` are **not** used: the act_runner cache server is not reachable from job containers by default (`getCacheEntry` ETIMEDOUT). Re-enable only after configuring a reachable `cache.host`/`external_server` on the runner.
CI Postgres services must **not** publish host port `5432` (use service hostname `postgres` on the job network). Publishing `5432:5432` on a shared VPS runner fails with `Bind for 0.0.0.0:5432 failed: port is already allocated` when another job/orphan still holds the port.
Parallel CI jobs need the act_runner `config.yaml` to keep `container.network` **empty** (per-job Docker network + service DNS) and `runner.capacity` ≥ 2. Setting `network: bridge` puts every job on the default bridge and makes parallel Postgres collide. Nested app containers (browser/container jobs) must join that job network by name and must **not** publish host `:8000`. On the current 1 vCPU / ~4 GiB VPS, `capacity: 2` is the safe ceiling.
### Staging (automatic)
[`.gitea/workflows/deploy-staging.yml`](../../.gitea/workflows/deploy-staging.yml)
[`.gitea/workflows/deploy-staging.yml`](../../.gitea/workflows/deploy-staging.yml) — separate workflow, triggered by `workflow_run` when **CI** completes on `main`.
Requires **Gitea ≥ 1.25** (`workflow_run` is not implemented as an Actions trigger in 1.24.x). Match both workflow display name `CI` and file id `ci.yml`.
1. Waits for workflow `CI` success (`workflow_run`) on push to `main`.
2. Builds once; pushes `:<full-sha>` and `:staging` to the Gitea registry.
3. Calls Dokploy `compose.deploy` and polls until done.
4. Runs [`scripts/deploy/smoke.sh`](../../scripts/deploy/smoke.sh) against `STAGING_URL`.
Manual re-deploy: **Actions → Deploy staging → Run workflow** (`workflow_dispatch`).
`DOKPLOY_API_KEY` must be the **plaintext** key from Dokploy → Profile → API (starts like `amare…`). Do not paste the hashed `apikey.key` column from Postgres — that yields HTTP 401.
### Production (manual)
[`.gitea/workflows/promote-production.yml`](../../.gitea/workflows/promote-production.yml)