Files
dtf-system/.gitea/workflows/deploy.yml
Cauê Faleiros c18b9e5b87
All checks were successful
Build and deploy / Validate source (push) Successful in 1m45s
Build and deploy / Integration suite on a real stack (push) Successful in 4m48s
Build and deploy / Secret scan and release gate (push) Successful in 11s
Build and deploy / Publish images and notify Portainer (push) Has been skipped
feat: generate print files, collect delivery addresses, add provider adapters
Week 2 work that did not need client inputs.

Print files (1.4): each paid item gets a PDF the width of the film and the
length of the approved layout, with every copy at its reviewed position,
rotation and mirror. Sources are embedded once at original resolution; JPEG
bytes pass through and PNG alpha becomes a soft mask. Artwork the generator
cannot reproduce goes to hand preparation with the reason. The worker renders
outside any transaction, and the operator approves the generated file as the
final one through the existing review.

Delivery address (3.8): required for any non-pickup quote, bound to the
quoted CEP, carried into the order snapshot, the Kanban card and Tiny.

Kanban (1.5): print-file status per item, and a panel of payment events that
need a person (money without an order, refunds after an order) until an
operator records the resolution.

Mercado Pago and Tiny (1.1, 1.3): adapters written from the public API
documentation and tested against fake transports only. Selectable for
sandbox testing with their credentials; the production preflight still
blocks release. Adds payment intents and a PIX step on the Site.

MinIO: Docker Hub and quay.io now refuse anonymous pulls, so local and CI
storage use Chainguard's MinIO build, pinned by digest.

Verified with the full CI integration sequence on a fresh local build,
including the new print_file_test and both browser suites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 11:56:46 -03:00

242 lines
10 KiB
YAML

name: Build and deploy
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
jobs:
validate:
name: Validate source
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: Run fast regression checks
run: |
python3 -m py_compile app/*.py app/**/*.py ops/*.py deploy/*.py
python3 -m unittest \
tests.test_dependency_lock \
tests.test_staging_readiness \
deploy.test_production_preflight \
tests.test_pricing \
tests.test_secrets -v
sh -n infra/lock_dependencies.sh
integration:
name: Integration suite on a real stack
needs: validate
runs-on: ubuntu-latest
timeout-minutes: 45
env:
# The runner shares the host's Docker daemon, so every published port is
# taken on the machine itself. Known occupants of that host:
# 8000, 9443 Portainer (the Edge tunnel and its UI)
# 18080/18081 the production dtf-cloud stack (docker-compose.yml defaults)
# 9000/9001 MinIO defaults elsewhere
# This block avoids all of them. Ephemeral ports are not an option: the
# published port is baked into PUBLIC_ORIGIN, ALLOWED_ORIGINS and the CSP
# when the containers start, so it has to be known beforehand.
SITE_PORT: "28080"
KANBAN_PORT: "28081"
API_PORT: "28000"
STORAGE_PORT: "29000"
STORAGE_CONSOLE_PORT: "29001"
# Presigned URLs are signed against this endpoint, so it must be reachable
# by whoever follows them. The suites run inside the network, so it has to
# be the service name, not a published port on the host.
S3_PUBLIC_ENDPOINT: http://storage:9000
PUBLIC_ORIGIN: http://site
ALLOWED_HOSTS: localhost,127.0.0.1,site,kanban
ALLOWED_ORIGINS: http://site,http://kanban,http://localhost:28080,http://localhost:28081
COMPOSE: docker compose -f compose.local.yaml
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
# py_compile cannot see an unresolved name, and the four unit tests above
# never start the application. A missing import in local/auth.py therefore
# reached production and returned 500 on every session, login and
# registration. These suites exercise the running stack and would have
# failed on it immediately.
- name: Start the stack
run: |
$COMPOSE up --build -d --wait --wait-timeout 600
$COMPOSE ps
# Run inside the stack's own network. The runner is itself a container, so
# ports published on the host's loopback are in a different namespace and
# unreachable from here. SITE_HOST_HEADER keeps the Host the gateway and
# TrustedHostMiddleware expect, so the configuration under test is the same
# one a developer exercises on localhost.
- name: API and workflow regressions
run: |
for suite in smoke_test workflow_test security_test scanning_test payment_test quote_pagination_test print_file_test; do
echo "--- $suite"
$COMPOSE exec -T \
-e SITE_BASE_URL=http://site \
-e SITE_HOST_HEADER=localhost \
api python -m "tests.$suite"
done
# Need Pillow and httpx, which only the application image has. The raster
# check needs PyMuPDF as well and skips here; run it locally when changing
# the generator's geometry. The provider suites use a fake transport: they
# prove the documented contract, not the integration.
- name: Print-file geometry and provider adapters
run: $COMPOSE exec -T api python -m unittest tests.test_printfile tests.test_mercadopago tests.test_tiny -v
- name: Runtime and retention regressions
run: |
$COMPOSE exec -T api python -m tests.retention_test
$COMPOSE exec -T api python -m tests.runtime_security_test
# Run Chrome on the Compose network. It must resolve the same storage:9000
# hostname used in presigned URLs, and absence of Chrome must fail CI.
- name: Browser regressions
run: |
$COMPOSE build browser-tests
$COMPOSE run --rm --no-deps browser-tests sh -ec \
'node tests/artwork_browser_test.mjs && node tests/browser_test.mjs'
- name: Diagnostics on failure
if: failure()
run: |
$COMPOSE ps || true
$COMPOSE logs --tail 200 api worker site kanban || true
- name: Tear down
if: always()
run: $COMPOSE down -v || true
scan:
name: Secret scan and release gate
needs: validate
runs-on: ubuntu-latest
timeout-minutes: 30
env:
TRIVY_IMAGE: ${{ vars.TRIVY_IMAGE }}
ENFORCE_PRODUCTION_PREFLIGHT: ${{ vars.ENFORCE_PRODUCTION_PREFLIGHT }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
# Blocking. A credential committed by accident must never reach the
# registry or the deployed stack, and the repository is clean today, so
# this gate costs nothing until it is actually needed.
- name: Secret scan
run: |
image="${TRIVY_IMAGE:-aquasec/trivy:0.58.1}"
docker run --rm -v "$PWD:/src:ro" "$image" \
fs --scanners secret --exit-code 1 --severity HIGH,CRITICAL \
--no-progress /src
# Keep push feedback advisory while the provider adapters are fake.
# The manual release job enforces the source preflight unconditionally.
# ENFORCE_PRODUCTION_PREFLIGHT can make push checks fail on blockers too.
- name: Production source preflight
run: |
set +e
python3 deploy/production_preflight.py --source-only
verdict=$?
set -e
if [ "$verdict" -eq 0 ]; then
echo "Source preflight passes."
exit 0
fi
if [ "${ENFORCE_PRODUCTION_PREFLIGHT:-false}" = "true" ]; then
echo "::error::Source preflight blocked the release."
exit "$verdict"
fi
echo "::warning::Source preflight reports blockers (advisory; set ENFORCE_PRODUCTION_PREFLIGHT=true to gate)."
publish-and-deploy:
name: Publish images and notify Portainer
needs: [validate, integration, scan]
if: gitea.event_name == 'workflow_dispatch' && gitea.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 45
env:
TRIVY_IMAGE: ${{ vars.TRIVY_IMAGE }}
PYTHON_BASE_IMAGE: ${{ vars.PYTHON_BASE_IMAGE }}
NGINX_BASE_IMAGE: ${{ vars.NGINX_BASE_IMAGE }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: Require production readiness
env:
PORTAINER_WEBHOOK: ${{ secrets.PORTAINER_WEBHOOK }}
run: |
python3 deploy/production_preflight.py --source-only
test -n "$PORTAINER_WEBHOOK"
- name: Sign in to the Gitea Container Registry
env:
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
test -n "$REGISTRY_USERNAME"
test -n "$REGISTRY_TOKEN"
echo "$REGISTRY_TOKEN" | docker login gitea.blyzer.com.br \
--username "$REGISTRY_USERNAME" --password-stdin
- name: Build API
run: |
image="gitea.blyzer.com.br/blyzer/dtf-api"
# The Dockerfiles pin digests themselves; these variables let a base be
# moved forward without editing the repository. --pull is intentionally
# absent: a digest already names one immutable image.
set --
[ -n "$PYTHON_BASE_IMAGE" ] && set -- --build-arg PYTHON_BASE_IMAGE="$PYTHON_BASE_IMAGE"
docker build --file deploy/Dockerfile.api "$@" \
--build-arg VCS_REF="${{ gitea.sha }}" \
--tag "$image:latest" --tag "$image:${{ gitea.sha }}" .
- name: Build web
run: |
image="gitea.blyzer.com.br/blyzer/dtf-web"
set --
[ -n "$PYTHON_BASE_IMAGE" ] && set -- --build-arg PYTHON_BASE_IMAGE="$PYTHON_BASE_IMAGE"
[ -n "$NGINX_BASE_IMAGE" ] && set -- "$@" --build-arg NGINX_BASE_IMAGE="$NGINX_BASE_IMAGE"
docker build --file deploy/Dockerfile.web "$@" \
--build-arg VCS_REF="${{ gitea.sha }}" \
--tag "$image:latest" --tag "$image:${{ gitea.sha }}" .
# CRITICAL blocks, HIGH is reported. Both images carry zero CRITICAL after
# the base pinning and OS upgrades, so this gate holds the line already
# reached. The remaining HIGH findings have no upstream fix, so failing on
# them would stop releases without making anything safer.
- name: Image vulnerabilities
run: |
image="${TRIVY_IMAGE:-aquasec/trivy:0.58.1}"
failed=0
for target in \
"gitea.blyzer.com.br/blyzer/dtf-api:${{ gitea.sha }}" \
"gitea.blyzer.com.br/blyzer/dtf-web:${{ gitea.sha }}"; do
echo "--- $target (HIGH, reported)"
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock "$image" \
image --image-src docker --scanners vuln --severity HIGH --no-progress \
--format table --exit-code 0 "$target" ||
echo "::warning::Could not scan $target for HIGH findings"
echo "--- $target (CRITICAL, blocking)"
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock "$image" \
image --image-src docker --scanners vuln --severity CRITICAL --no-progress \
--format table --exit-code 1 "$target" || failed=1
done
if [ "$failed" -ne 0 ]; then
echo "::error::A CRITICAL vulnerability was found in a release image."
exit 1
fi
- name: Publish validated images
run: |
for name in dtf-api dtf-web; do
image="gitea.blyzer.com.br/blyzer/$name"
docker push "$image:${{ gitea.sha }}"
docker push "$image:latest"
done
- name: Trigger Portainer redeployment
env:
PORTAINER_WEBHOOK: ${{ secrets.PORTAINER_WEBHOOK }}
run: |
curl --fail --silent --show-error --max-time 30 --request POST "$PORTAINER_WEBHOOK"