Files
dtf-system/.gitea/workflows/deploy.yml
Cauê Faleiros 7cab210674 ci: move the integration stack clear of the ports that host already uses
8000 was Portainer's Edge tunnel, not a stray process. The first attempt at a
fix picked 18080/18081, which are the production dtf-cloud stack's own defaults
in docker-compose.yml: it would have passed only while that stack was down and
collided again the moment it came back.

Use 28080/28081/28000/29000/29001, clear of Portainer (8000, 9443), both
production stack definitions (18080/18081 and 8080/8081) and the usual MinIO
ports. The occupants are listed in the workflow so the next person choosing a
port can see what is taken.

Ephemeral ports would remove the guesswork but do not work here: the published
port is baked into PUBLIC_ORIGIN, ALLOWED_ORIGINS and the CSP when the
containers start, so it has to be known before they run.

Full suite verified on the new block, including the browser end-to-end.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 13:04:56 -03:00

233 lines
9.6 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 local/*.py deploy/*.py
python3 -m unittest \
local.test_dependency_lock \
local.test_staging_readiness \
deploy.test_production_preflight \
local.test_pricing \
local.test_secrets -v
sh -n local/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)
# 8080/8081 deploy/stack.yaml 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"
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
- name: API and workflow regressions
run: |
python3 -m local.smoke_test
python3 -m local.workflow_test
python3 -m local.security_test
python3 -m local.scanning_test
- name: Runtime and retention regressions
run: |
$COMPOSE exec -T api python -m local.retention_test
$COMPOSE exec -T api python -m local.runtime_security_test
# These need a real Chrome. They are the only coverage for the artwork
# editor and the full customer journey, so install google-chrome-stable
# (or set CHROME_BIN) on the runner to make them gate deployments. The
# suites above stay hard gates either way.
- name: Browser regressions
run: |
for candidate in "$CHROME_BIN" /usr/bin/google-chrome-stable \
/usr/bin/google-chrome /usr/bin/chromium /usr/bin/chromium-browser; do
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
export CHROME_BIN="$candidate"
break
fi
done
if [ ! -x "${CHROME_BIN:-}" ]; then
echo "::warning::No Chrome on this runner; browser regressions were NOT run."
echo "Install google-chrome-stable or set CHROME_BIN to gate on them."
exit 0
fi
echo "Using $CHROME_BIN"
node local/artwork_browser_test.mjs
node local/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
# PORTAINER.md described this as blocking publication. It never ran at
# all, and turning it on unconditionally would block every deploy: the
# source preflight refuses a release while the payment and messaging
# adapters are fake, which is the deliberate state the stack runs in
# today. So its verdict is always printed, and enforcement is opt-in.
# Set the repository variable ENFORCE_PRODUCTION_PREFLIGHT to "true" once
# real adapters land, and this becomes the gate the documentation claims.
- 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 == 'push' && 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: 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 and publish 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 }}" .
docker push "$image:latest"
docker push "$image:${{ gitea.sha }}"
- name: Build and publish 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 }}" .
docker push "$image:latest"
docker push "$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 --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 --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 published image."
exit 1
fi
- name: Trigger Portainer redeployment
env:
PORTAINER_WEBHOOK: ${{ secrets.PORTAINER_WEBHOOK }}
run: |
if [ -z "$PORTAINER_WEBHOOK" ]; then
echo "PORTAINER_WEBHOOK is not configured; images were published but deployment was skipped."
exit 0
fi
curl --fail --silent --show-error --max-time 30 --request POST "$PORTAINER_WEBHOOK"