ci: publish images from every green push to main
All checks were successful
Build and deploy / Validate source (push) Successful in 5s
Build and deploy / Integration suite on a real stack (push) Successful in 2m5s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images (push) Successful in 1m54s

The release job enforced the production source preflight, which blocks while
the payment and messaging adapters are fake. They still are, by design, so
since 2026-09-23 no release could succeed and production kept running older
images while main moved on.

Pushes to main that pass validation, the integration suite and the scans now
build, scan and publish the images. Nothing is deployed automatically:
production changes when the stack is pulled and redeployed in Portainer. A
manual run also calls the Portainer webhook when one is configured. The
preflight stays in the scan job, advisory unless ENFORCE_PRODUCTION_PREFLIGHT
is true, in which case a blocked preflight stops publishing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cauê Faleiros
2026-09-24 14:30:43 -03:00
parent 4c01e932c3
commit 641dc6053b
4 changed files with 44 additions and 31 deletions

View File

@@ -5,21 +5,24 @@ from .production_preflight import config_errors, source_errors
class ReleaseWorkflowTests(unittest.TestCase):
def test_main_push_cannot_publish_and_manual_release_is_gated(self):
def test_only_checked_main_publishes_and_pushes_never_deploy(self):
workflow = (Path(__file__).resolve().parents[1] /
'.gitea/workflows/deploy.yml').read_text()
release = workflow.split(' publish-and-deploy:\n', 1)[1]
self.assertIn("if: gitea.event_name == 'workflow_dispatch' && "
"gitea.ref == 'refs/heads/main'", release)
preflight = release.index('python3 deploy/production_preflight.py --source-only')
webhook = release.index('test -n "$PORTAINER_WEBHOOK"')
checks, release = workflow.split(' publish-and-deploy:\n', 1)
# Publishing waits for every check, and only ever happens from main.
self.assertIn('needs: [validate, integration, scan]', release)
self.assertIn("if: gitea.ref == 'refs/heads/main' && ", release)
# The source preflight lives in the scan job and can be made blocking.
self.assertIn('python3 deploy/production_preflight.py --source-only', checks)
self.assertIn('ENFORCE_PRODUCTION_PREFLIGHT', checks)
# Images are scanned before they are pushed, and a push to main only
# publishes: redeployment is Portainer's (or a manual run's) decision.
scan = release.index('- name: Image vulnerabilities')
publish = release.index('- name: Publish validated images')
redeploy = release.index('- name: Trigger Portainer redeployment')
self.assertLess(preflight, scan)
self.assertLess(webhook, scan)
self.assertLess(scan, publish)
self.assertLess(publish, redeploy)
self.assertIn("if: gitea.event_name == 'workflow_dispatch'", release[redeploy:])
def valid_config():