deploy/stack.yaml passes DATABASE_URL_FILE, AWS_ACCESS_KEY_ID_FILE, OPERATOR_PASSWORD_FILE and the provider tokens as Swarm secret paths, but the runtime only ever read the plain names. That stack could not start: the database URL and R2 credentials were absent, and operator login raised KeyError, so it returned 500 instead of the intended 503. local/secrets.py resolves every <NAME>_FILE into <NAME> before configuration is read, from the API, worker and bootstrap entrypoints. It fails closed on an unreadable or empty secret and on a name supplied both directly and as a file, because starting with a credential nobody intended is worse than not starting. Only one trailing newline is stripped, so a generated password keeps any whitespace that belongs to it, and no value reaches an error message. The stack also passed OPERATOR_USER while the Kanban authenticates by email; it now passes OPERATOR_EMAIL, matching the runtime. The release gate checked this by searching local/secrets.py for the literal "DATABASE_URL_FILE", which would pass for any file containing that string. It now loads the module and makes it resolve every secret the stack declares, and asserts it fails closed on a missing one. Four marker strings that stopped matching when R2 support landed are removed rather than left to rot; the two that still describe real blockers stay, so the gate continues to refuse a release while payment and messaging adapters are fake. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
139 lines
5.0 KiB
YAML
139 lines
5.0 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:
|
|
SITE_PORT: "8080"
|
|
KANBAN_PORT: "8081"
|
|
API_PORT: "8000"
|
|
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
|
|
|
|
publish-and-deploy:
|
|
name: Publish images and notify Portainer
|
|
needs: [validate, integration]
|
|
if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
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"
|
|
docker build --pull --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"
|
|
docker build --pull --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 }}"
|
|
- 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"
|