Skip to content

feat: push-based deploy - target host needs only Docker + Compose - #115

Merged
ineedjet merged 5 commits into
mainfrom
feat/fabric-push-deploy
Aug 21, 2026
Merged

feat: push-based deploy - target host needs only Docker + Compose#115
ineedjet merged 5 commits into
mainfrom
feat/fabric-push-deploy

Conversation

@ineedjet

@ineedjetineedjet commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Replaces ansible/deploy.yml with deploy/deploy.py, a plain-Python push-based deploy tool with real unittest coverage (50 tests across resolve.py, collisions.py, vault.py, render.py, deploy.py).
  • Everything — ref resolution, download, decryption, config-template rendering — now runs on the GitHub Actions runner. The target host needs only Docker and Docker Compose: no sops, no age key, no gh, no flightdeck scripts of any kind.
  • Deleted outright, no replacement: up.sh, down.sh, restart.sh, deploy.sh, generate-env.sh, lib.sh, logs.sh, backup.sh, plus ansible/ and ansible.cfg. There is no manual administration flow anymore — no server console access, no local quick-start; every deploy goes through targets//vaults/ manifests and GitHub Actions.
  • targets/*.yml's apps field is a mapping from app name to that app's own env_refs, replacing the old flat apps list + target-level env_refs. vaults/*.yml is one manifest per app (not per target), so a vault can declare its output env var names directly (HTTP_PORT, not TRAEFIK_HTTP_PORT) with no app-prefix convention.
  • Collision detection across an app's own env_refs still 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.
  • Fixed a real bug found along the way: the app-bundle merge only ever copied directories, silently dropping 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).
  • Fixed stale docs describing an apps/postgres.yml/redis.yml/mongo.yml catalog 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.yml now references credentials.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.yml carries the Watchtower label, but hawkeye's target doesn't run a watchtower app — as implemented, docker compose up would 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)
  • Matrix trace via load-yaml-matrix.py against the updated targets//vaults/ schema
  • pre-commit run --all-files clean across the whole repo; CI lint passing
  • Live deploy to hawkeye — intentionally deferred, gated on infra prep (including the new secret above) happening separately

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.
@ineedjet
ineedjet requested a lite review from CopilotAugust 21, 2026 00:14

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yml with deploy/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_GENERATION guards to avoid clobbering pushed per-app .env files, 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
FileDescription
vaults/hawkeye.ymlRemoves the former single vault-per-target env manifest.
vaults/hawkeye-traefik.ymlAdds app-scoped vault manifest for Traefik (non-prefixed port vars).
vaults/hawkeye-rybbit.ymlAdds app-scoped vault manifest for Rybbit (shared APPS_* + secrets).
up.shSkips env generation when automated deploy has already pushed per-app .env.
targets/hawkeye.ymlConverts apps from a list into a mapping with per-app env_refs.
README.mdUpdates documentation from Ansible to push-based deploy/deploy.py and new manifest shapes.
deploy/tests/test_resolve.pyAdds unit tests for parsing/resolving/downloading release refs.
deploy/tests/test_deploy.pyAdds unit tests for release building, env resolution, archiving, and remote command sequencing.
deploy/tests/test_collisions.pyAdds unit tests for ciphertext-based env key collision detection.
deploy/resolve.pyImplements shared ref parsing, @latest resolution, and asset download via gh.
deploy/requirements.txtAdds Fabric dependency for runner-side SSH deploy.
deploy/deploy.pyImplements runner-side bundle merge + push deploy + per-app env decryption workflow.
deploy/collisions.pyImplements collision detection by reading dotenv ciphertext key names.
deploy.shAdds .env-optional behavior and skip-env-generation guard for automated deploy.
apps/traefik/docker-compose.ymlSwitches Traefik port env vars to non-prefixed names (per-app vault model).
ansible/deploy.ymlRemoves legacy Ansible deployment playbook.
ansible.cfgRemoves Ansible configuration now that Ansible is removed.
AGENTS.mdUpdates repo guidance to reflect push-based deploy and per-app env delivery.
.github/workflows/release.ymlRemoves legacy env-refs wiring; uses per-app apps mapping.
.github/workflows/deploy.ymlRemoves legacy env-refs wiring; uses per-app apps mapping.
.github/workflows/deploy-shared.ymlSwitches from Ansible execution to python3 deploy/deploy.py with Fabric deps.
.github/actions/build-bundle/action.ymlRemoves 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 threaddeploy.sh Outdated
Comment on lines +17 to +19
if [ -z "${FLIGHTDECK_SKIP_ENV_GENERATION:-}" ]; then
"$(dirname "$0")/generate-env.sh" "${apps[@]}"
fi
Comment threaddeploy/deploy.py
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 threaddeploy/deploy.py Outdated
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ineedjet
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat: push-based deploy - target host needs only Docker + Compose by ineedjet · Pull Request #115 · rubykatzen/flightdeck · GitHub
Skip to content

feat: push-based deploy - target host needs only Docker + Compose - #115

Merged
ineedjet merged 5 commits into
mainfrom
feat/fabric-push-deploy
Aug 21, 2026
Merged

feat: push-based deploy - target host needs only Docker + Compose#115
ineedjet merged 5 commits into
mainfrom
feat/fabric-push-deploy

Conversation

@ineedjet

@ineedjetineedjet commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Replaces ansible/deploy.yml with deploy/deploy.py, a plain-Python push-based deploy tool with real unittest coverage (50 tests across resolve.py, collisions.py, vault.py, render.py, deploy.py).
  • Everything — ref resolution, download, decryption, config-template rendering — now runs on the GitHub Actions runner. The target host needs only Docker and Docker Compose: no sops, no age key, no gh, no flightdeck scripts of any kind.
  • Deleted outright, no replacement: up.sh, down.sh, restart.sh, deploy.sh, generate-env.sh, lib.sh, logs.sh, backup.sh, plus ansible/ and ansible.cfg. There is no manual administration flow anymore — no server console access, no local quick-start; every deploy goes through targets//vaults/ manifests and GitHub Actions.
  • targets/*.yml's apps field is a mapping from app name to that app's own env_refs, replacing the old flat apps list + target-level env_refs. vaults/*.yml is one manifest per app (not per target), so a vault can declare its output env var names directly (HTTP_PORT, not TRAEFIK_HTTP_PORT) with no app-prefix convention.
  • Collision detection across an app's own env_refs still 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.
  • Fixed a real bug found along the way: the app-bundle merge only ever copied directories, silently dropping 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).
  • Fixed stale docs describing an apps/postgres.yml/redis.yml/mongo.yml catalog 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.yml now references credentials.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.yml carries the Watchtower label, but hawkeye's target doesn't run a watchtower app — as implemented, docker compose up would 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)
  • Matrix trace via load-yaml-matrix.py against the updated targets//vaults/ schema
  • pre-commit run --all-files clean across the whole repo; CI lint passing
  • Live deploy to hawkeye — intentionally deferred, gated on infra prep (including the new secret above) happening separately

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.
@ineedjet
ineedjet requested a lite review from CopilotAugust 21, 2026 00:14

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yml with deploy/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_GENERATION guards to avoid clobbering pushed per-app .env files, 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
FileDescription
vaults/hawkeye.ymlRemoves the former single vault-per-target env manifest.
vaults/hawkeye-traefik.ymlAdds app-scoped vault manifest for Traefik (non-prefixed port vars).
vaults/hawkeye-rybbit.ymlAdds app-scoped vault manifest for Rybbit (shared APPS_* + secrets).
up.shSkips env generation when automated deploy has already pushed per-app .env.
targets/hawkeye.ymlConverts apps from a list into a mapping with per-app env_refs.
README.mdUpdates documentation from Ansible to push-based deploy/deploy.py and new manifest shapes.
deploy/tests/test_resolve.pyAdds unit tests for parsing/resolving/downloading release refs.
deploy/tests/test_deploy.pyAdds unit tests for release building, env resolution, archiving, and remote command sequencing.
deploy/tests/test_collisions.pyAdds unit tests for ciphertext-based env key collision detection.
deploy/resolve.pyImplements shared ref parsing, @latest resolution, and asset download via gh.
deploy/requirements.txtAdds Fabric dependency for runner-side SSH deploy.
deploy/deploy.pyImplements runner-side bundle merge + push deploy + per-app env decryption workflow.
deploy/collisions.pyImplements collision detection by reading dotenv ciphertext key names.
deploy.shAdds .env-optional behavior and skip-env-generation guard for automated deploy.
apps/traefik/docker-compose.ymlSwitches Traefik port env vars to non-prefixed names (per-app vault model).
ansible/deploy.ymlRemoves legacy Ansible deployment playbook.
ansible.cfgRemoves Ansible configuration now that Ansible is removed.
AGENTS.mdUpdates repo guidance to reflect push-based deploy and per-app env delivery.
.github/workflows/release.ymlRemoves legacy env-refs wiring; uses per-app apps mapping.
.github/workflows/deploy.ymlRemoves legacy env-refs wiring; uses per-app apps mapping.
.github/workflows/deploy-shared.ymlSwitches from Ansible execution to python3 deploy/deploy.py with Fabric deps.
.github/actions/build-bundle/action.ymlRemoves 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 threaddeploy.sh Outdated
Comment on lines +17 to +19
if [ -z "${FLIGHTDECK_SKIP_ENV_GENERATION:-}" ]; then
"$(dirname "$0")/generate-env.sh" "${apps[@]}"
fi
Comment threaddeploy/deploy.py
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 threaddeploy/deploy.py Outdated
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ineedjet
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: push-based deploy - target host needs only Docker + Compose by ineedjet · Pull Request #115 · rubykatzen/flightdeck · GitHub
Skip to content

feat: push-based deploy - target host needs only Docker + Compose - #115

Merged
ineedjet merged 5 commits into
mainfrom
feat/fabric-push-deploy
Aug 21, 2026
Merged

feat: push-based deploy - target host needs only Docker + Compose#115
ineedjet merged 5 commits into
mainfrom
feat/fabric-push-deploy

Conversation

@ineedjet

@ineedjetineedjet commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Replaces ansible/deploy.yml with deploy/deploy.py, a plain-Python push-based deploy tool with real unittest coverage (50 tests across resolve.py, collisions.py, vault.py, render.py, deploy.py).
  • Everything — ref resolution, download, decryption, config-template rendering — now runs on the GitHub Actions runner. The target host needs only Docker and Docker Compose: no sops, no age key, no gh, no flightdeck scripts of any kind.
  • Deleted outright, no replacement: up.sh, down.sh, restart.sh, deploy.sh, generate-env.sh, lib.sh, logs.sh, backup.sh, plus ansible/ and ansible.cfg. There is no manual administration flow anymore — no server console access, no local quick-start; every deploy goes through targets//vaults/ manifests and GitHub Actions.
  • targets/*.yml's apps field is a mapping from app name to that app's own env_refs, replacing the old flat apps list + target-level env_refs. vaults/*.yml is one manifest per app (not per target), so a vault can declare its output env var names directly (HTTP_PORT, not TRAEFIK_HTTP_PORT) with no app-prefix convention.
  • Collision detection across an app's own env_refs still 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.
  • Fixed a real bug found along the way: the app-bundle merge only ever copied directories, silently dropping 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).
  • Fixed stale docs describing an apps/postgres.yml/redis.yml/mongo.yml catalog 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.yml now references credentials.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.yml carries the Watchtower label, but hawkeye's target doesn't run a watchtower app — as implemented, docker compose up would 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)
  • Matrix trace via load-yaml-matrix.py against the updated targets//vaults/ schema
  • pre-commit run --all-files clean across the whole repo; CI lint passing
  • Live deploy to hawkeye — intentionally deferred, gated on infra prep (including the new secret above) happening separately

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.
@ineedjet
ineedjet requested a lite review from CopilotAugust 21, 2026 00:14

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yml with deploy/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_GENERATION guards to avoid clobbering pushed per-app .env files, 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
FileDescription
vaults/hawkeye.ymlRemoves the former single vault-per-target env manifest.
vaults/hawkeye-traefik.ymlAdds app-scoped vault manifest for Traefik (non-prefixed port vars).
vaults/hawkeye-rybbit.ymlAdds app-scoped vault manifest for Rybbit (shared APPS_* + secrets).
up.shSkips env generation when automated deploy has already pushed per-app .env.
targets/hawkeye.ymlConverts apps from a list into a mapping with per-app env_refs.
README.mdUpdates documentation from Ansible to push-based deploy/deploy.py and new manifest shapes.
deploy/tests/test_resolve.pyAdds unit tests for parsing/resolving/downloading release refs.
deploy/tests/test_deploy.pyAdds unit tests for release building, env resolution, archiving, and remote command sequencing.
deploy/tests/test_collisions.pyAdds unit tests for ciphertext-based env key collision detection.
deploy/resolve.pyImplements shared ref parsing, @latest resolution, and asset download via gh.
deploy/requirements.txtAdds Fabric dependency for runner-side SSH deploy.
deploy/deploy.pyImplements runner-side bundle merge + push deploy + per-app env decryption workflow.
deploy/collisions.pyImplements collision detection by reading dotenv ciphertext key names.
deploy.shAdds .env-optional behavior and skip-env-generation guard for automated deploy.
apps/traefik/docker-compose.ymlSwitches Traefik port env vars to non-prefixed names (per-app vault model).
ansible/deploy.ymlRemoves legacy Ansible deployment playbook.
ansible.cfgRemoves Ansible configuration now that Ansible is removed.
AGENTS.mdUpdates repo guidance to reflect push-based deploy and per-app env delivery.
.github/workflows/release.ymlRemoves legacy env-refs wiring; uses per-app apps mapping.
.github/workflows/deploy.ymlRemoves legacy env-refs wiring; uses per-app apps mapping.
.github/workflows/deploy-shared.ymlSwitches from Ansible execution to python3 deploy/deploy.py with Fabric deps.
.github/actions/build-bundle/action.ymlRemoves 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 threaddeploy.sh Outdated
Comment on lines +17 to +19
if [ -z "${FLIGHTDECK_SKIP_ENV_GENERATION:-}" ]; then
"$(dirname "$0")/generate-env.sh" "${apps[@]}"
fi
Comment threaddeploy/deploy.py
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 threaddeploy/deploy.py Outdated
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ineedjet
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: push-based deploy - target host needs only Docker + Compose by ineedjet · Pull Request #115 · rubykatzen/flightdeck · GitHub
Skip to content

feat: push-based deploy - target host needs only Docker + Compose - #115

Merged
ineedjet merged 5 commits into
mainfrom
feat/fabric-push-deploy
Aug 21, 2026
Merged

feat: push-based deploy - target host needs only Docker + Compose#115
ineedjet merged 5 commits into
mainfrom
feat/fabric-push-deploy

Conversation

@ineedjet

@ineedjetineedjet commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Replaces ansible/deploy.yml with deploy/deploy.py, a plain-Python push-based deploy tool with real unittest coverage (50 tests across resolve.py, collisions.py, vault.py, render.py, deploy.py).
  • Everything — ref resolution, download, decryption, config-template rendering — now runs on the GitHub Actions runner. The target host needs only Docker and Docker Compose: no sops, no age key, no gh, no flightdeck scripts of any kind.
  • Deleted outright, no replacement: up.sh, down.sh, restart.sh, deploy.sh, generate-env.sh, lib.sh, logs.sh, backup.sh, plus ansible/ and ansible.cfg. There is no manual administration flow anymore — no server console access, no local quick-start; every deploy goes through targets//vaults/ manifests and GitHub Actions.
  • targets/*.yml's apps field is a mapping from app name to that app's own env_refs, replacing the old flat apps list + target-level env_refs. vaults/*.yml is one manifest per app (not per target), so a vault can declare its output env var names directly (HTTP_PORT, not TRAEFIK_HTTP_PORT) with no app-prefix convention.
  • Collision detection across an app's own env_refs still 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.
  • Fixed a real bug found along the way: the app-bundle merge only ever copied directories, silently dropping 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).
  • Fixed stale docs describing an apps/postgres.yml/redis.yml/mongo.yml catalog 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.yml now references credentials.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.yml carries the Watchtower label, but hawkeye's target doesn't run a watchtower app — as implemented, docker compose up would 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)
  • Matrix trace via load-yaml-matrix.py against the updated targets//vaults/ schema
  • pre-commit run --all-files clean across the whole repo; CI lint passing
  • Live deploy to hawkeye — intentionally deferred, gated on infra prep (including the new secret above) happening separately

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.
@ineedjet
ineedjet requested a lite review from CopilotAugust 21, 2026 00:14

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yml with deploy/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_GENERATION guards to avoid clobbering pushed per-app .env files, 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
FileDescription
vaults/hawkeye.ymlRemoves the former single vault-per-target env manifest.
vaults/hawkeye-traefik.ymlAdds app-scoped vault manifest for Traefik (non-prefixed port vars).
vaults/hawkeye-rybbit.ymlAdds app-scoped vault manifest for Rybbit (shared APPS_* + secrets).
up.shSkips env generation when automated deploy has already pushed per-app .env.
targets/hawkeye.ymlConverts apps from a list into a mapping with per-app env_refs.
README.mdUpdates documentation from Ansible to push-based deploy/deploy.py and new manifest shapes.
deploy/tests/test_resolve.pyAdds unit tests for parsing/resolving/downloading release refs.
deploy/tests/test_deploy.pyAdds unit tests for release building, env resolution, archiving, and remote command sequencing.
deploy/tests/test_collisions.pyAdds unit tests for ciphertext-based env key collision detection.
deploy/resolve.pyImplements shared ref parsing, @latest resolution, and asset download via gh.
deploy/requirements.txtAdds Fabric dependency for runner-side SSH deploy.
deploy/deploy.pyImplements runner-side bundle merge + push deploy + per-app env decryption workflow.
deploy/collisions.pyImplements collision detection by reading dotenv ciphertext key names.
deploy.shAdds .env-optional behavior and skip-env-generation guard for automated deploy.
apps/traefik/docker-compose.ymlSwitches Traefik port env vars to non-prefixed names (per-app vault model).
ansible/deploy.ymlRemoves legacy Ansible deployment playbook.
ansible.cfgRemoves Ansible configuration now that Ansible is removed.
AGENTS.mdUpdates repo guidance to reflect push-based deploy and per-app env delivery.
.github/workflows/release.ymlRemoves legacy env-refs wiring; uses per-app apps mapping.
.github/workflows/deploy.ymlRemoves legacy env-refs wiring; uses per-app apps mapping.
.github/workflows/deploy-shared.ymlSwitches from Ansible execution to python3 deploy/deploy.py with Fabric deps.
.github/actions/build-bundle/action.ymlRemoves 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 threaddeploy.sh Outdated
Comment on lines +17 to +19
if [ -z "${FLIGHTDECK_SKIP_ENV_GENERATION:-}" ]; then
"$(dirname "$0")/generate-env.sh" "${apps[@]}"
fi
Comment threaddeploy/deploy.py
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 threaddeploy/deploy.py Outdated
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ineedjet
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat: push-based deploy - target host needs only Docker + Compose by ineedjet · Pull Request #115 · rubykatzen/flightdeck · GitHub
Skip to content

feat: push-based deploy - target host needs only Docker + Compose - #115

Merged
ineedjet merged 5 commits into
mainfrom
feat/fabric-push-deploy
Aug 21, 2026
Merged

feat: push-based deploy - target host needs only Docker + Compose#115
ineedjet merged 5 commits into
mainfrom
feat/fabric-push-deploy

Conversation

@ineedjet

@ineedjetineedjet commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Replaces ansible/deploy.yml with deploy/deploy.py, a plain-Python push-based deploy tool with real unittest coverage (50 tests across resolve.py, collisions.py, vault.py, render.py, deploy.py).
  • Everything — ref resolution, download, decryption, config-template rendering — now runs on the GitHub Actions runner. The target host needs only Docker and Docker Compose: no sops, no age key, no gh, no flightdeck scripts of any kind.
  • Deleted outright, no replacement: up.sh, down.sh, restart.sh, deploy.sh, generate-env.sh, lib.sh, logs.sh, backup.sh, plus ansible/ and ansible.cfg. There is no manual administration flow anymore — no server console access, no local quick-start; every deploy goes through targets//vaults/ manifests and GitHub Actions.
  • targets/*.yml's apps field is a mapping from app name to that app's own env_refs, replacing the old flat apps list + target-level env_refs. vaults/*.yml is one manifest per app (not per target), so a vault can declare its output env var names directly (HTTP_PORT, not TRAEFIK_HTTP_PORT) with no app-prefix convention.
  • Collision detection across an app's own env_refs still 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.
  • Fixed a real bug found along the way: the app-bundle merge only ever copied directories, silently dropping 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).
  • Fixed stale docs describing an apps/postgres.yml/redis.yml/mongo.yml catalog 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.yml now references credentials.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.yml carries the Watchtower label, but hawkeye's target doesn't run a watchtower app — as implemented, docker compose up would 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)
  • Matrix trace via load-yaml-matrix.py against the updated targets//vaults/ schema
  • pre-commit run --all-files clean across the whole repo; CI lint passing
  • Live deploy to hawkeye — intentionally deferred, gated on infra prep (including the new secret above) happening separately

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.
@ineedjet
ineedjet requested a lite review from CopilotAugust 21, 2026 00:14

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yml with deploy/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_GENERATION guards to avoid clobbering pushed per-app .env files, 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
FileDescription
vaults/hawkeye.ymlRemoves the former single vault-per-target env manifest.
vaults/hawkeye-traefik.ymlAdds app-scoped vault manifest for Traefik (non-prefixed port vars).
vaults/hawkeye-rybbit.ymlAdds app-scoped vault manifest for Rybbit (shared APPS_* + secrets).
up.shSkips env generation when automated deploy has already pushed per-app .env.
targets/hawkeye.ymlConverts apps from a list into a mapping with per-app env_refs.
README.mdUpdates documentation from Ansible to push-based deploy/deploy.py and new manifest shapes.
deploy/tests/test_resolve.pyAdds unit tests for parsing/resolving/downloading release refs.
deploy/tests/test_deploy.pyAdds unit tests for release building, env resolution, archiving, and remote command sequencing.
deploy/tests/test_collisions.pyAdds unit tests for ciphertext-based env key collision detection.
deploy/resolve.pyImplements shared ref parsing, @latest resolution, and asset download via gh.
deploy/requirements.txtAdds Fabric dependency for runner-side SSH deploy.
deploy/deploy.pyImplements runner-side bundle merge + push deploy + per-app env decryption workflow.
deploy/collisions.pyImplements collision detection by reading dotenv ciphertext key names.
deploy.shAdds .env-optional behavior and skip-env-generation guard for automated deploy.
apps/traefik/docker-compose.ymlSwitches Traefik port env vars to non-prefixed names (per-app vault model).
ansible/deploy.ymlRemoves legacy Ansible deployment playbook.
ansible.cfgRemoves Ansible configuration now that Ansible is removed.
AGENTS.mdUpdates repo guidance to reflect push-based deploy and per-app env delivery.
.github/workflows/release.ymlRemoves legacy env-refs wiring; uses per-app apps mapping.
.github/workflows/deploy.ymlRemoves legacy env-refs wiring; uses per-app apps mapping.
.github/workflows/deploy-shared.ymlSwitches from Ansible execution to python3 deploy/deploy.py with Fabric deps.
.github/actions/build-bundle/action.ymlRemoves 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 threaddeploy.sh Outdated
Comment on lines +17 to +19
if [ -z "${FLIGHTDECK_SKIP_ENV_GENERATION:-}" ]; then
"$(dirname "$0")/generate-env.sh" "${apps[@]}"
fi
Comment threaddeploy/deploy.py
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 threaddeploy/deploy.py Outdated
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ineedjet
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: push-based deploy - target host needs only Docker + Compose by ineedjet · Pull Request #115 · rubykatzen/flightdeck · GitHub
Skip to content

feat: push-based deploy - target host needs only Docker + Compose - #115

Merged
ineedjet merged 5 commits into
mainfrom
feat/fabric-push-deploy
Aug 21, 2026
Merged

feat: push-based deploy - target host needs only Docker + Compose#115
ineedjet merged 5 commits into
mainfrom
feat/fabric-push-deploy

Conversation

@ineedjet

@ineedjetineedjet commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Replaces ansible/deploy.yml with deploy/deploy.py, a plain-Python push-based deploy tool with real unittest coverage (50 tests across resolve.py, collisions.py, vault.py, render.py, deploy.py).
  • Everything — ref resolution, download, decryption, config-template rendering — now runs on the GitHub Actions runner. The target host needs only Docker and Docker Compose: no sops, no age key, no gh, no flightdeck scripts of any kind.
  • Deleted outright, no replacement: up.sh, down.sh, restart.sh, deploy.sh, generate-env.sh, lib.sh, logs.sh, backup.sh, plus ansible/ and ansible.cfg. There is no manual administration flow anymore — no server console access, no local quick-start; every deploy goes through targets//vaults/ manifests and GitHub Actions.
  • targets/*.yml's apps field is a mapping from app name to that app's own env_refs, replacing the old flat apps list + target-level env_refs. vaults/*.yml is one manifest per app (not per target), so a vault can declare its output env var names directly (HTTP_PORT, not TRAEFIK_HTTP_PORT) with no app-prefix convention.
  • Collision detection across an app's own env_refs still 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.
  • Fixed a real bug found along the way: the app-bundle merge only ever copied directories, silently dropping 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).
  • Fixed stale docs describing an apps/postgres.yml/redis.yml/mongo.yml catalog 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.yml now references credentials.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.yml carries the Watchtower label, but hawkeye's target doesn't run a watchtower app — as implemented, docker compose up would 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)
  • Matrix trace via load-yaml-matrix.py against the updated targets//vaults/ schema
  • pre-commit run --all-files clean across the whole repo; CI lint passing
  • Live deploy to hawkeye — intentionally deferred, gated on infra prep (including the new secret above) happening separately

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.
@ineedjet
ineedjet requested a lite review from CopilotAugust 21, 2026 00:14

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yml with deploy/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_GENERATION guards to avoid clobbering pushed per-app .env files, 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
FileDescription
vaults/hawkeye.ymlRemoves the former single vault-per-target env manifest.
vaults/hawkeye-traefik.ymlAdds app-scoped vault manifest for Traefik (non-prefixed port vars).
vaults/hawkeye-rybbit.ymlAdds app-scoped vault manifest for Rybbit (shared APPS_* + secrets).
up.shSkips env generation when automated deploy has already pushed per-app .env.
targets/hawkeye.ymlConverts apps from a list into a mapping with per-app env_refs.
README.mdUpdates documentation from Ansible to push-based deploy/deploy.py and new manifest shapes.
deploy/tests/test_resolve.pyAdds unit tests for parsing/resolving/downloading release refs.
deploy/tests/test_deploy.pyAdds unit tests for release building, env resolution, archiving, and remote command sequencing.
deploy/tests/test_collisions.pyAdds unit tests for ciphertext-based env key collision detection.
deploy/resolve.pyImplements shared ref parsing, @latest resolution, and asset download via gh.
deploy/requirements.txtAdds Fabric dependency for runner-side SSH deploy.
deploy/deploy.pyImplements runner-side bundle merge + push deploy + per-app env decryption workflow.
deploy/collisions.pyImplements collision detection by reading dotenv ciphertext key names.
deploy.shAdds .env-optional behavior and skip-env-generation guard for automated deploy.
apps/traefik/docker-compose.ymlSwitches Traefik port env vars to non-prefixed names (per-app vault model).
ansible/deploy.ymlRemoves legacy Ansible deployment playbook.
ansible.cfgRemoves Ansible configuration now that Ansible is removed.
AGENTS.mdUpdates repo guidance to reflect push-based deploy and per-app env delivery.
.github/workflows/release.ymlRemoves legacy env-refs wiring; uses per-app apps mapping.
.github/workflows/deploy.ymlRemoves legacy env-refs wiring; uses per-app apps mapping.
.github/workflows/deploy-shared.ymlSwitches from Ansible execution to python3 deploy/deploy.py with Fabric deps.
.github/actions/build-bundle/action.ymlRemoves 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 threaddeploy.sh Outdated
Comment on lines +17 to +19
if [ -z "${FLIGHTDECK_SKIP_ENV_GENERATION:-}" ]; then
"$(dirname "$0")/generate-env.sh" "${apps[@]}"
fi
Comment threaddeploy/deploy.py
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 threaddeploy/deploy.py Outdated
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ineedjet
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: push-based deploy - target host needs only Docker + Compose by ineedjet · Pull Request #115 · rubykatzen/flightdeck · GitHub
Skip to content

feat: push-based deploy - target host needs only Docker + Compose - #115

Merged
ineedjet merged 5 commits into
mainfrom
feat/fabric-push-deploy
Aug 21, 2026
Merged

feat: push-based deploy - target host needs only Docker + Compose#115
ineedjet merged 5 commits into
mainfrom
feat/fabric-push-deploy

Conversation

@ineedjet

@ineedjetineedjet commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Replaces ansible/deploy.yml with deploy/deploy.py, a plain-Python push-based deploy tool with real unittest coverage (50 tests across resolve.py, collisions.py, vault.py, render.py, deploy.py).
  • Everything — ref resolution, download, decryption, config-template rendering — now runs on the GitHub Actions runner. The target host needs only Docker and Docker Compose: no sops, no age key, no gh, no flightdeck scripts of any kind.
  • Deleted outright, no replacement: up.sh, down.sh, restart.sh, deploy.sh, generate-env.sh, lib.sh, logs.sh, backup.sh, plus ansible/ and ansible.cfg. There is no manual administration flow anymore — no server console access, no local quick-start; every deploy goes through targets//vaults/ manifests and GitHub Actions.
  • targets/*.yml's apps field is a mapping from app name to that app's own env_refs, replacing the old flat apps list + target-level env_refs. vaults/*.yml is one manifest per app (not per target), so a vault can declare its output env var names directly (HTTP_PORT, not TRAEFIK_HTTP_PORT) with no app-prefix convention.
  • Collision detection across an app's own env_refs still 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.
  • Fixed a real bug found along the way: the app-bundle merge only ever copied directories, silently dropping 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).
  • Fixed stale docs describing an apps/postgres.yml/redis.yml/mongo.yml catalog 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.yml now references credentials.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.yml carries the Watchtower label, but hawkeye's target doesn't run a watchtower app — as implemented, docker compose up would 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)
  • Matrix trace via load-yaml-matrix.py against the updated targets//vaults/ schema
  • pre-commit run --all-files clean across the whole repo; CI lint passing
  • Live deploy to hawkeye — intentionally deferred, gated on infra prep (including the new secret above) happening separately

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.
@ineedjet
ineedjet requested a lite review from CopilotAugust 21, 2026 00:14

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yml with deploy/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_GENERATION guards to avoid clobbering pushed per-app .env files, 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
FileDescription
vaults/hawkeye.ymlRemoves the former single vault-per-target env manifest.
vaults/hawkeye-traefik.ymlAdds app-scoped vault manifest for Traefik (non-prefixed port vars).
vaults/hawkeye-rybbit.ymlAdds app-scoped vault manifest for Rybbit (shared APPS_* + secrets).
up.shSkips env generation when automated deploy has already pushed per-app .env.
targets/hawkeye.ymlConverts apps from a list into a mapping with per-app env_refs.
README.mdUpdates documentation from Ansible to push-based deploy/deploy.py and new manifest shapes.
deploy/tests/test_resolve.pyAdds unit tests for parsing/resolving/downloading release refs.
deploy/tests/test_deploy.pyAdds unit tests for release building, env resolution, archiving, and remote command sequencing.
deploy/tests/test_collisions.pyAdds unit tests for ciphertext-based env key collision detection.
deploy/resolve.pyImplements shared ref parsing, @latest resolution, and asset download via gh.
deploy/requirements.txtAdds Fabric dependency for runner-side SSH deploy.
deploy/deploy.pyImplements runner-side bundle merge + push deploy + per-app env decryption workflow.
deploy/collisions.pyImplements collision detection by reading dotenv ciphertext key names.
deploy.shAdds .env-optional behavior and skip-env-generation guard for automated deploy.
apps/traefik/docker-compose.ymlSwitches Traefik port env vars to non-prefixed names (per-app vault model).
ansible/deploy.ymlRemoves legacy Ansible deployment playbook.
ansible.cfgRemoves Ansible configuration now that Ansible is removed.
AGENTS.mdUpdates repo guidance to reflect push-based deploy and per-app env delivery.
.github/workflows/release.ymlRemoves legacy env-refs wiring; uses per-app apps mapping.
.github/workflows/deploy.ymlRemoves legacy env-refs wiring; uses per-app apps mapping.
.github/workflows/deploy-shared.ymlSwitches from Ansible execution to python3 deploy/deploy.py with Fabric deps.
.github/actions/build-bundle/action.ymlRemoves 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 threaddeploy.sh Outdated
Comment on lines +17 to +19
if [ -z "${FLIGHTDECK_SKIP_ENV_GENERATION:-}" ]; then
"$(dirname "$0")/generate-env.sh" "${apps[@]}"
fi
Comment threaddeploy/deploy.py
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 threaddeploy/deploy.py Outdated
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ineedjet
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat: push-based deploy - target host needs only Docker + Compose by ineedjet · Pull Request #115 · rubykatzen/flightdeck · GitHub
Skip to content

feat: push-based deploy - target host needs only Docker + Compose - #115

Merged
ineedjet merged 5 commits into
mainfrom
feat/fabric-push-deploy
Aug 21, 2026
Merged

feat: push-based deploy - target host needs only Docker + Compose#115
ineedjet merged 5 commits into
mainfrom
feat/fabric-push-deploy

Conversation

@ineedjet

@ineedjetineedjet commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Replaces ansible/deploy.yml with deploy/deploy.py, a plain-Python push-based deploy tool with real unittest coverage (50 tests across resolve.py, collisions.py, vault.py, render.py, deploy.py).
  • Everything — ref resolution, download, decryption, config-template rendering — now runs on the GitHub Actions runner. The target host needs only Docker and Docker Compose: no sops, no age key, no gh, no flightdeck scripts of any kind.
  • Deleted outright, no replacement: up.sh, down.sh, restart.sh, deploy.sh, generate-env.sh, lib.sh, logs.sh, backup.sh, plus ansible/ and ansible.cfg. There is no manual administration flow anymore — no server console access, no local quick-start; every deploy goes through targets//vaults/ manifests and GitHub Actions.
  • targets/*.yml's apps field is a mapping from app name to that app's own env_refs, replacing the old flat apps list + target-level env_refs. vaults/*.yml is one manifest per app (not per target), so a vault can declare its output env var names directly (HTTP_PORT, not TRAEFIK_HTTP_PORT) with no app-prefix convention.
  • Collision detection across an app's own env_refs still 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.
  • Fixed a real bug found along the way: the app-bundle merge only ever copied directories, silently dropping 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).
  • Fixed stale docs describing an apps/postgres.yml/redis.yml/mongo.yml catalog 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.yml now references credentials.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.yml carries the Watchtower label, but hawkeye's target doesn't run a watchtower app — as implemented, docker compose up would 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)
  • Matrix trace via load-yaml-matrix.py against the updated targets//vaults/ schema
  • pre-commit run --all-files clean across the whole repo; CI lint passing
  • Live deploy to hawkeye — intentionally deferred, gated on infra prep (including the new secret above) happening separately

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.
@ineedjet
ineedjet requested a lite review from CopilotAugust 21, 2026 00:14

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yml with deploy/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_GENERATION guards to avoid clobbering pushed per-app .env files, 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
FileDescription
vaults/hawkeye.ymlRemoves the former single vault-per-target env manifest.
vaults/hawkeye-traefik.ymlAdds app-scoped vault manifest for Traefik (non-prefixed port vars).
vaults/hawkeye-rybbit.ymlAdds app-scoped vault manifest for Rybbit (shared APPS_* + secrets).
up.shSkips env generation when automated deploy has already pushed per-app .env.
targets/hawkeye.ymlConverts apps from a list into a mapping with per-app env_refs.
README.mdUpdates documentation from Ansible to push-based deploy/deploy.py and new manifest shapes.
deploy/tests/test_resolve.pyAdds unit tests for parsing/resolving/downloading release refs.
deploy/tests/test_deploy.pyAdds unit tests for release building, env resolution, archiving, and remote command sequencing.
deploy/tests/test_collisions.pyAdds unit tests for ciphertext-based env key collision detection.
deploy/resolve.pyImplements shared ref parsing, @latest resolution, and asset download via gh.
deploy/requirements.txtAdds Fabric dependency for runner-side SSH deploy.
deploy/deploy.pyImplements runner-side bundle merge + push deploy + per-app env decryption workflow.
deploy/collisions.pyImplements collision detection by reading dotenv ciphertext key names.
deploy.shAdds .env-optional behavior and skip-env-generation guard for automated deploy.
apps/traefik/docker-compose.ymlSwitches Traefik port env vars to non-prefixed names (per-app vault model).
ansible/deploy.ymlRemoves legacy Ansible deployment playbook.
ansible.cfgRemoves Ansible configuration now that Ansible is removed.
AGENTS.mdUpdates repo guidance to reflect push-based deploy and per-app env delivery.
.github/workflows/release.ymlRemoves legacy env-refs wiring; uses per-app apps mapping.
.github/workflows/deploy.ymlRemoves legacy env-refs wiring; uses per-app apps mapping.
.github/workflows/deploy-shared.ymlSwitches from Ansible execution to python3 deploy/deploy.py with Fabric deps.
.github/actions/build-bundle/action.ymlRemoves 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 threaddeploy.sh Outdated
Comment on lines +17 to +19
if [ -z "${FLIGHTDECK_SKIP_ENV_GENERATION:-}" ]; then
"$(dirname "$0")/generate-env.sh" "${apps[@]}"
fi
Comment threaddeploy/deploy.py
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 threaddeploy/deploy.py Outdated
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ineedjet