name: Validate, publish and deploy on: pull_request: push: branches: [main] workflow_dispatch: jobs: validate: runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Validate source and deployment definitions 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 -v sh -n local/lock_dependencies.sh docker compose config --quiet docker compose -f compose.staging.yaml config --quiet set -a . deploy/portainer.env.example set +a docker stack config --compose-file deploy/stack.yaml >/dev/null publish-and-deploy: needs: validate if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 75 env: COMPOSE_PROJECT_NAME: dtf-release-${{ gitea.run_number }} REGISTRY_HOST: ${{ vars.REGISTRY_HOST }} REGISTRY_OWNER: ${{ vars.REGISTRY_OWNER }} PYTHON_BASE_IMAGE: ${{ vars.PYTHON_BASE_IMAGE }} NGINX_BASE_IMAGE: ${{ vars.NGINX_BASE_IMAGE }} TRIVY_IMAGE: ${{ vars.TRIVY_IMAGE }} steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Require production-capable application source run: python3 deploy/production_preflight.py --source-only - name: Validate immutable build inputs run: | for value in "$PYTHON_BASE_IMAGE" "$NGINX_BASE_IMAGE" "$TRIVY_IMAGE"; do echo "$value" | grep -Eq '^[a-z0-9][a-z0-9._:/-]*@sha256:[0-9a-f]{64}$' echo "$value" | grep -Ev '@sha256:0{64}$' >/dev/null done case "$REGISTRY_HOST/$REGISTRY_OWNER" in *[A-Z]*|*' '*|'/'*) exit 2;; esac - name: Run complete isolated regression suite run: | docker compose up --build -d --wait python3 -m local.security_test python3 -m local.scanning_test python3 -m local.smoke_test python3 -m local.workflow_test docker compose exec -T api python -m local.runtime_security_test docker compose exec -T api python -m local.retention_test node local/browser_test.mjs python3 -m local.backup create-and-verify - name: Build production images run: | api_sha="$REGISTRY_HOST/$REGISTRY_OWNER/dtf-api:${{ gitea.sha }}" api_latest="$REGISTRY_HOST/$REGISTRY_OWNER/dtf-api:latest" web_sha="$REGISTRY_HOST/$REGISTRY_OWNER/dtf-web:${{ gitea.sha }}" web_latest="$REGISTRY_HOST/$REGISTRY_OWNER/dtf-web:latest" docker build --pull --file deploy/Dockerfile.api \ --build-arg PYTHON_BASE_IMAGE="$PYTHON_BASE_IMAGE" \ --build-arg VCS_REF="${{ gitea.sha }}" \ --tag "$api_sha" --tag "$api_latest" . docker build --pull --file deploy/Dockerfile.web \ --build-arg PYTHON_BASE_IMAGE="$PYTHON_BASE_IMAGE" \ --build-arg NGINX_BASE_IMAGE="$NGINX_BASE_IMAGE" \ --build-arg VCS_REF="${{ gitea.sha }}" \ --tag "$web_sha" --tag "$web_latest" . - name: Block secret, configuration and image findings run: | api="$REGISTRY_HOST/$REGISTRY_OWNER/dtf-api:${{ gitea.sha }}" web="$REGISTRY_HOST/$REGISTRY_OWNER/dtf-web:${{ gitea.sha }}" docker run --rm --volume "$PWD:/src:ro" --workdir /src "$TRIVY_IMAGE" \ fs --scanners secret --exit-code 1 --severity HIGH,CRITICAL . docker run --rm --volume "$PWD:/src:ro" --workdir /src "$TRIVY_IMAGE" \ config --exit-code 1 --severity HIGH,CRITICAL deploy docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock:ro "$TRIVY_IMAGE" \ image --exit-code 1 --scanners vuln --severity HIGH,CRITICAL "$api" docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock:ro "$TRIVY_IMAGE" \ image --exit-code 1 --scanners vuln --severity HIGH,CRITICAL "$web" - name: Publish latest and rollback tags env: REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} run: | trap 'docker logout "$REGISTRY_HOST" >/dev/null 2>&1 || true' EXIT echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" --username "$REGISTRY_USERNAME" --password-stdin docker push "$REGISTRY_HOST/$REGISTRY_OWNER/dtf-api:${{ gitea.sha }}" docker push "$REGISTRY_HOST/$REGISTRY_OWNER/dtf-api:latest" docker push "$REGISTRY_HOST/$REGISTRY_OWNER/dtf-web:${{ gitea.sha }}" docker push "$REGISTRY_HOST/$REGISTRY_OWNER/dtf-web:latest" - name: Trigger the Portainer stack webhook env: PORTAINER_WEBHOOK: ${{ secrets.PORTAINER_WEBHOOK }} run: | test -n "$PORTAINER_WEBHOOK" curl --fail --silent --show-error --max-time 30 --request POST "$PORTAINER_WEBHOOK" - name: Stop isolated test stack if: always() run: docker compose down --volumes --remove-orphans