Uh oh!
There was an error while loading. Please reload this page.
feat: push-based deploy - target host needs only Docker + Compose - #115
Merged
Conversation
Moves all release-ref resolution/downloading and app-bundle merging to the CI runner as plain Python (deploy/), which then pushes the finished release and per-app encrypted vault files to each target host over SSH and runs a short remote command sequence. Decryption stays strictly server-side; env key collisions across an app's own env_refs are detected in CI from ciphertext, before anything is pushed. Targets now wire each app to its own env_refs directly (apps.<name>.env_refs instead of a flat apps list + target-level env_refs), and vaults split one-per-app so they can declare output env var names with no app-prefix convention. Implements #111 and #114.
There was a problem hiding this comment.
Pull request overview
This PR migrates Flightdeck’s automated deployment mechanism from an Ansible pull-based playbook to a push-based Python/Fabric deploy runner, while also switching secret delivery from one vault-per-target to one vault-per-app (enabling non-prefixed env var names inside app-specific .env files).
Changes:
- Replace
ansible/deploy.ymlwithdeploy/deploy.py+ helpers (resolve.py,collisions.py) and add unit tests for deploy/ref-resolution/collision detection. - Update target/vault manifest shapes to per-app
env_refs, and update docs/workflows to match the new deploy interface. - Add
FLIGHTDECK_SKIP_ENV_GENERATIONguards to avoid clobbering pushed per-app.envfiles, and update Traefik ports to non-prefixed names.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| vaults/hawkeye.yml | Removes the former single vault-per-target env manifest. |
| vaults/hawkeye-traefik.yml | Adds app-scoped vault manifest for Traefik (non-prefixed port vars). |
| vaults/hawkeye-rybbit.yml | Adds app-scoped vault manifest for Rybbit (shared APPS_* + secrets). |
| up.sh | Skips env generation when automated deploy has already pushed per-app .env. |
| targets/hawkeye.yml | Converts apps from a list into a mapping with per-app env_refs. |
| README.md | Updates documentation from Ansible to push-based deploy/deploy.py and new manifest shapes. |
| deploy/tests/test_resolve.py | Adds unit tests for parsing/resolving/downloading release refs. |
| deploy/tests/test_deploy.py | Adds unit tests for release building, env resolution, archiving, and remote command sequencing. |
| deploy/tests/test_collisions.py | Adds unit tests for ciphertext-based env key collision detection. |
| deploy/resolve.py | Implements shared ref parsing, @latest resolution, and asset download via gh. |
| deploy/requirements.txt | Adds Fabric dependency for runner-side SSH deploy. |
| deploy/deploy.py | Implements runner-side bundle merge + push deploy + per-app env decryption workflow. |
| deploy/collisions.py | Implements collision detection by reading dotenv ciphertext key names. |
| deploy.sh | Adds .env-optional behavior and skip-env-generation guard for automated deploy. |
| apps/traefik/docker-compose.yml | Switches Traefik port env vars to non-prefixed names (per-app vault model). |
| ansible/deploy.yml | Removes legacy Ansible deployment playbook. |
| ansible.cfg | Removes Ansible configuration now that Ansible is removed. |
| AGENTS.md | Updates repo guidance to reflect push-based deploy and per-app env delivery. |
| .github/workflows/release.yml | Removes legacy env-refs wiring; uses per-app apps mapping. |
| .github/workflows/deploy.yml | Removes legacy env-refs wiring; uses per-app apps mapping. |
| .github/workflows/deploy-shared.yml | Switches from Ansible execution to python3 deploy/deploy.py with Fabric deps. |
| .github/actions/build-bundle/action.yml | Removes bundling of ansible.cfg now that Ansible is removed. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
9
to
+11
| ports: | ||
| - "${TRAEFIK_HTTP_PORT}:80" | ||
| - "${TRAEFIK_HTTPS_PORT}:443" | ||
| - "${HTTP_PORT}:80" | ||
| - "${HTTPS_PORT}:443" |
Comment on lines
+17
to
+19
| if [ -z "${FLIGHTDECK_SKIP_ENV_GENERATION:-}" ]; then | ||
| "$(dirname "$0")/generate-env.sh" "${apps[@]}" | ||
| fi |
Comment on lines
+167
to
+175
| def validate_config(config): | ||
| if not config.get("hosts"): | ||
| raise DeployError("Config must set hosts to a non-empty list") | ||
| if not config.get("app_ref"): | ||
| raise DeployError("Config must set app_ref") | ||
| if not config.get("app_refs"): | ||
| raise DeployError("Config must set app_refs to a non-empty list") | ||
| if not config.get("apps"): | ||
| raise DeployError("Config must set apps to a non-empty object") |
Comment on lines
+103
to
+108
| remote_path = f"{vaults_path}/{app}-{index}.sops.env" | ||
| connection.put(str(local_path), remote=remote_path) | ||
| remote_sources.append(remote_path) | ||
| app_env_path = f"{release_path}/apps/{app}/.env" | ||
| decrypt_steps = " && ".join( |
.env is no longer bootstrapped from a template - up.sh's ensure_file step and the now-dead helper are gone, and docs are updated to match.
Decrypts vaults and renders config templates on the CI runner instead of the target host (closes#116), and removes the shell-script layer that existed only for a human console operator who no longer exists in this model - up.sh, down.sh, restart.sh, deploy.sh, generate-env.sh, and lib.sh are gone with no replacement, along with the now-empty machinery bundle (flightdeck.zip/app_ref) they were the entire payload of. deploy/deploy.py pushes a fully finished release - real .env, already- rendered config - and runs `docker compose pull/up` per app directly over SSH. The target host's only remaining dependencies are Docker and Docker Compose; no sops, no age key, no gh, no flightdeck scripts of any kind. Also fixes a real bug found along the way: the app-bundle merge only ever copied directories, silently dropping apps/common.yml, networks.yml, and postgres.yml from every deployed release tree (inherited unchanged from the original ansible/deploy.yml logic, never caught since nothing has deployed to hawkeye yet).
apps/postgres.yml, apps/redis.yml, apps/mongo.yml never existed as such - the catalog uses versioned filenames (postgres-17.yml/postgres-18.yml, redis-7.yml/redis-8.yml, mongodb-8.yml) and has grown to include several more shared templates (clickhouse, mysql, timescale, paradedb, pgvector, gotenberg) that weren't documented at all. Predates this session's other changes; caught during an accuracy pass.
CI's check-precommit step compares baseline's auto-detected linter set against .pre-commit-config.yaml's hook list and fails on any mismatch. Deleting the last .sh files dropped shellcheck from the auto-detected set; the static pre-commit config still listed it.
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 21, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ansible/deploy.ymlwithdeploy/deploy.py, a plain-Python push-based deploy tool with realunittestcoverage (50 tests acrossresolve.py,collisions.py,vault.py,render.py,deploy.py).sops, no age key, nogh, no flightdeck scripts of any kind.up.sh,down.sh,restart.sh,deploy.sh,generate-env.sh,lib.sh,logs.sh,backup.sh, plusansible/andansible.cfg. There is no manual administration flow anymore — no server console access, no local quick-start; every deploy goes throughtargets//vaults/manifests and GitHub Actions.targets/*.yml'sappsfield is a mapping from app name to that app's ownenv_refs, replacing the old flatappslist + target-levelenv_refs.vaults/*.ymlis one manifest per app (not per target), so a vault can declare its output env var names directly (HTTP_PORT, notTRAEFIK_HTTP_PORT) with no app-prefix convention.env_refsstill happens in CI from ciphertext (SOPS's dotenv format only encrypts values, so key names are readable without decryption) — before anything is decrypted or pushed.apps/common.yml/networks.yml/etc. from every deployed release tree (inherited unchanged from the original Ansible logic, never caught since nothing has deployed to hawkeye yet).apps/postgres.yml/redis.yml/mongo.ymlcatalog shape that hasn't existed for a while — the real files are versioned (postgres-17.yml/postgres-18.yml,redis-7.yml/redis-8.yml, etc.) and several shared templates (clickhouse, mysql, timescale, paradedb, pgvector, gotenberg) weren't documented at all.New GitHub Secret required
targets/hawkeye.ymlnow referencescredentials.secrets.sops_age_key: HAWKEYE_AGE_PRIVATE_KEY— the private age key content that used to live only on hawkeye itself now needs to exist as a GitHub Secret too. Not yet created; part of the infra prep happening separately.Known gap, not fixed here
apps/traefik/docker-compose.ymlcarries the Watchtower label, but hawkeye's target doesn't run awatchtowerapp — as implemented,docker compose upwould never actually run for traefik on the automated path. Tracked in #106, needs its own decision before the first real deploy.Test plan
deploy/unit tests (50, covering ref resolution, ciphertext collision detection, decryption, config-template rendering, release-bundle merging, and the full remote command sequence against a mocked SSH connection)load-yaml-matrix.pyagainst the updatedtargets//vaults/schemapre-commit run --all-filesclean across the whole repo; CI lint passing