The live orchestration layer of ReleaseGate. It detects a container-image update,
stages the upgrade against the previous version, collects evidence, asks
ReleaseGateCore for a promote or hold
decision, and tears the staging environment down.
Production is deliberately outside this trust boundary: a promote decision is
sent as an HMAC-signed webhook to an operator-controlled deployer, and the stack
never mutates production itself.
The v1 workflow supports Diun image-update detection; a Postgres-backed
application and deployment-profile registry; isolated Dokploy staging projects;
baseline-to-candidate, upgrade-in-place health validation; GitHub release-note and
OSV evidence collection; a single-call Core decision with conservative hold
fallback; confidence gating and optional Windmill approval suspension; signed
promotion-intent webhooks; and teardown, run serialization, and an S3-backed
benchmark capture queue.
Golden-snapshot seeding, browser end-to-end checks, production mutation, post-deployment monitoring, and automatic rollback are not part of v1.
ReleaseGateStack/
├── releasegate_dokploy/ # installable Dokploy client and CLI
├── stack/
│ ├── core/ # Windmill and Diun Compose definitions
│ ├── webhook_receiver_example.py
│ └── windmill/ # generated flow/app plus source scripts
├── tests/ # network-free unit tests
└── .env.example
Generated Windmill JSON is committed because push.py consumes it. gen_flow.py,
capture/gen_capture.py, and gen_app.py are its source; CI regenerates them and
fails on drift.
Python 3.10+ (3.11–3.13 alongside the Bench or Core packages).
cd ReleaseGateStack
python -m venv .venv && . .venv/bin/activate
python -m pip install -e '.[dev,logs]'
cp .env.example .env
set -a; . ./.env; set +areleasegate-dokploy projects
releasegate-dokploy stage --app grafana --tag 12.4.4 \
--compose @grafana.compose.yaml --env @grafana.env
releasegate-dokploy logs-deploy <compose-id> --idle-timeout 30
releasegate-dokploy teardown <staging-project-id>teardown verifies the remote project name starts with staging- before deleting
that project's Compose stacks and project record. Each automatically named staging
project and Compose app carries a random ownership token, so a duplicate app/tag
run does not share cleanup targets. If Compose deletion fails, teardown raises and
retains the project record as a manual recovery target. The generic get/post
commands expose the wider Dokploy API and are privileged administrative operations.
from releasegate_dokploy import DokployClient
from releasegate_dokploy.staging import create_staging_compose, teardown_staging
with DokployClient() as client:
handle = create_staging_compose(
client, app="grafana", tag="12.4.4",
compose_file="services:\n app:\n image: grafana/grafana:12.4.4\n",
)
# Run staging checks and pass the evidence to ReleaseGateCore.
teardown_staging(client, handle)stack/core/ is a reference deployment, not an internet-ready production
configuration: it exposes Windmill directly over HTTP and mounts the Docker socket
into Diun. Put the services on a private network, terminate TLS in front of
Windmill, restrict Dokploy credentials, and review the Docker socket risk first.
PG_PASSWORD=test DOKPLOY_URL=https://dokploy.example.com DOKPLOY_API_KEY=test \
docker compose -f stack/core/windmill.compose.yaml config --quiet
DIUN_WEBHOOK_ENDPOINT=https://windmill.example.com/api/w/releasegate/jobs/run/f/f/releasegate/closed_loop \
docker compose -f stack/core/diun.compose.yaml config --quietstack/core/deploy_core.py --help documents the bootstrap arguments.
stack/windmill/ contains the scripts, flow, and dashboard for the v1
staging-only loop plus the Bench staging-capture queue.
wmill_mcp.py minimal JSON-RPC client for a workspace MCP endpoint
push.py publish config, libraries, steps, flows, and app
run_wait.py preview a script or poll a path-only flow run
gen_flow.py generate flow/closed_loop.json
gen_app.py generate app/ops_dashboard.json
seed_registry.py generate registry_seed.json from ReleaseGateBench/apps
lib/ Dokploy, decision, registry, and S3 libraries
steps/ staging-only closed-loop steps
capture/ S3-backed benchmark capture flow and steps
apps/s3.compose.yaml optional VersityGW S3 reference service
lib/sync_decide.py is a development tool and is not pushed. It vendors the
dependency-free prompt/parser from ReleaseGateCore so the live workflow and the
benchmarked core share the same logic.
Provision before pushing: a Windmill workspace named releasegate; a scoped
workspace MCP token; a Postgres database and Windmill postgresql resource at
f/releasegate/registry_db; a Dokploy URL and scoped API key; optionally
S3-compatible storage for the capture queue; optionally an LLM key and a signed
promotion-webhook receiver.
The reference control-plane Compose file includes Windmill's own Postgres, but creating the separate registry database and Windmill resource is an operator action.
Generate and verify the four artifacts first (see Development and validation), then:
cd ReleaseGateStack
set -a; . ./.env; set +a
python stack/windmill/push.py all # config | libs | steps | flow | capture | app | allWMILL_MCP_URL must be the full workspace MCP endpoint, including its token.
push.py config updates a variable only when its environment variable is
explicitly present; it does not blank existing secrets. Workspace paths involved:
variables under f/releasegate/, the operator-provisioned resource
f/releasegate/registry_db, libraries under f/releasegate/lib/, steps under
f/releasegate/steps/, flows f/releasegate/closed_loop and f/rg_staging/capture,
and dashboard f/releasegate/ops_dashboard.
registry_seed.json holds six application fixtures and their deployment profiles:
python stack/windmill/run_wait.py preview lib/registry.py \
"{\"rows_json\": $(jq -Rs . < stack/windmill/registry_seed.json)}"The seed sets each profile's baseline to the image tag in its benchmark Compose
fixture. Operators must update prod_version and prod_digest to match their real
production inventory before enabling autonomous detection.
detect → inventory → evidence
→ for each profile:
provision baseline → upgrade in place → decide → gate
→ optional approval suspension → signed intent webhook → teardown → log
No configured LLM key produces a conservative hold (absent and empty are treated
alike). A promote intent is dispatched only when both webhook URL and HMAC secret
are configured. A 2xx response acknowledges the intent; it is not proof that
deployment completed, and the flow does not advance the registry baseline on
acknowledgment — the external deployer or an operator must reconcile that profile's
prod_version and prod_digest.
The capture flow reads an S3 batch, processes items serially, persists each staging result, and writes a batch index. It shares libraries with the live flow but does not mutate production.
- The MCP URL contains a secret token. Do not print, commit, or log it.
- Windmill previews do not reproduce every dependency-lock behavior of a pushed flow. Run the real flow before relying on live automation.
- The failure handler never deletes projects by shared name prefix. If an iteration
fails before teardown it reports that cleanup is required; inspect that run,
identify its exact staging project ID, and run the guarded
releasegate-dokploy teardown <project-id>. runFlowByPathon the workspace MCP is path-only. Use a Windmill worker call or a scoped webhook when flow inputs are required.- Benchmark fixture credentials are deliberately weak and suitable only for isolated staging.
For a promote decision, 08_dispatch_promote.py sends compact JSON with
X-ReleaseGate-Signature: sha256=<HMAC-SHA256>, X-ReleaseGate-Timestamp, and
X-ReleaseGate-Delivery. Both PROMOTE_WEBHOOK_URL and a non-empty
PROMOTE_WEBHOOK_SECRET are required.
The reference receiver verifies signatures, timestamp skew, and nonce replay, but
it is an example rather than a production deployer. Its skew and nonce checks read
the ts and nonce inside the signed body, since the matching headers are not
covered by the HMAC.
python stack/webhook_receiver_example.py # local sign-and-verify self-test
RELEASEGATE_WEBHOOK_SECRET='replace-me' \
python stack/webhook_receiver_example.py serve 8099python -m pytest -q
python stack/windmill/lib/sync_decide.py --check
python stack/windmill/gen_flow.py
python stack/windmill/capture/gen_capture.py
python stack/windmill/gen_app.py
python stack/windmill/seed_registry.py ../ReleaseGateBench/appsAfter generation these four should have no diff: stack/windmill/flow/closed_loop.json,
stack/windmill/capture/flow/capture.json, stack/windmill/app/ops_dashboard.json,
stack/windmill/registry_seed.json.
All unit tests are offline. Live Dokploy and Windmill checks are intentionally separate because they create and remove real infrastructure.
- Never commit
.env, database passwords, API keys, cookies, or token-bearing MCP URLs. - Use a scoped credential for staging; the generic API client can perform destructive administrative actions.
- Staging Compose files can execute arbitrary container workloads. Review fixtures before running them on a shared host.
- A failed flow cannot safely infer which staging project it owns — tear down only
its exact project ID, never every
staging-*project. - Never mount a Docker socket into an LLM-controlled worker.