Skip to content

Consider migrating ansible/deploy.yml to Fabric (Python) #111

Description

@ineedjet

Context

ansible/deploy.yml doesn't actually use Ansible idiomatically. It's a
strictly sequential procedure — download bundle, extract, pull+merge app
packages, pull+decrypt+merge env packages, symlink switch, run
./deploy.sh, prune old releases — not a converging desired-state
playbook. "Recreate release directory" even does state: absent then
recreates unconditionally on every run, the opposite of idempotent
convergence. This matches #107's framing exactly: flightdeck is a
Capistrano-style deployment system (versioned releases, current symlink,
retained history), not a Nomad/Kubernetes-style reconciler. Capistrano's
own model — SSH in, run a sequence of tasks, switch a symlink — is what
Fabric provides for Python, the same way Capistrano provides it for Ruby.

Concrete friction with Ansible specifically

  • No shared-function mechanism across tasks. Each shell: task is an
    independent subprocess — there's no way to define a bash function in one
    task and reuse it in another without a checked-in script file. This is
    the direct cause of Deduplicate release-ref resolution logic in ansible/deploy.yml #103: four near-identical ~30-line "parse
    owner/repo@tag[:asset], resolve @latest via gh release view,
    download" blocks, hand-copied because Ansible has no better answer short
    of extracting a separate shell script. In Fabric this is just an
    importable Python function — the duplication wouldn't exist to begin
    with, and Deduplicate release-ref resolution logic in ansible/deploy.yml #103 would become moot rather than needing its own fix.
  • Zero unit test coverage of the deploy logic itself.encrypt-env
    and load-yaml-matrix are both Python, both have real unit tests
    (unittest, PyYAML). ansible/deploy.yml is YAML + Jinja + embedded
    bash — the only verification it gets is --syntax-check and manually
    tracing a generated matrix through by hand (done for feat: deploy rybbit for rubykatzen.com through flightdeck itself #102/feat: move apps to targets, support env_refs as a list #110, but
    that's not the same as testing the ref-parsing/collision-detection logic
    itself). A Fabric-based deploy script would be ordinary Python, testable
    the same way the rest of this repo's automation already is.
  • The inventory is ceremony, not a real feature.deploy-shared.yml
    builds a throwaway JSON inventory via jq on every run purely to
    satisfy Ansible's -i requirement — nothing here uses Ansible's actual
    host-grouping/inventory capabilities.
  • Real, recurring cost for capabilities we don't use.ansible-core
    is installed fresh via unpinned pip install on every single deploy run
    (deploy-shared.yml's "Install ansible-core" step) — paying setup cost
    for idempotency/convergence/templating machinery the playbook doesn't
    actually lean on.

Refined direction: push-based deploy, decided during #114's design

Discussion while designing #114 (per-app vault delivery) converged on a
concrete shape for what a Fabric-based ansible/deploy.yml replacement
should actually look like — this is now the leading design, not just "port
the playbook to Python":

  • All ref-resolution/download logic moves to the CI runner. Parsing
    owner/repo@tag[:asset], resolving @latest via the GitHub API,
    downloading the resolved asset — all of it runs as ordinary Python on
    the GitHub Actions runner, written once, reused for the machinery
    bundle, every app bundle, and every encrypted vault asset. The server
    never calls gh itself and doesn't need it installed — a real
    prerequisite reduction from what README currently documents.
  • Fabric pushes files to the server instead of the server pulling
    them.
    Downloaded app bundles and still-encrypted vault blobs are
    transferred to the server directly (SSH/SCP), rather than the server
    reaching out to GitHub itself.
  • Decryption stays strictly server-side. The server's private age key
    never leaves it, same as today. The server's only vault-related
    capability is running sops decrypt on a single file it's given — a
    generic, dumb, reusable primitive with no knowledge of refs, targets, or
    merging.
  • No composition/merge logic lives on the server at all — but collision
    detection still happens, in CI, before anything is pushed.
    SOPS's
    dotenv output format only encrypts values; key names stay in
    cleartext (APPS_DOMAIN=ENC[...]). So when an app's env_refs has more
    than one entry, Fabric downloads each .sops.env asset and compares key
    names across them directly — no decryption needed for this check at
    all. Any duplicate key fails the build right there, before anything
    touches the server. Only once that check passes does Fabric push the
    files and tell the server to decrypt each one and plain cat them
    together into apps/{app}/.env. The server still never understands
    composition; it just never sees a colliding set in the first place.
  • Update, superseding the below: this went further than "almost
    nothing" — the session that implemented this also decided flightdeck
    will have no manual administration flow at all, ever (no server SSH
    console access, no local quick-start either). With that decided,
    decryption and config-template rendering moved to the CI runner too
    (see Consider decrypting vaults on the CI runner instead of server-side #116), and literally nothing server-side survives:
    up.sh/down.sh/restart.sh/deploy.sh/generate-env.sh/lib.sh
    were all deleted outright, no replacements. The target host's only
    dependencies are Docker and Docker Compose. deploy/deploy.py issues
    docker compose pull/up directly per app over SSH; there is no
    server-side script layer of any kind left to describe.

Considerations against

  • Ansible's SSH connection handling, privilege escalation (become), and
    retry semantics are battle-tested. Re-implementing that in Fabric means
    re-verifying connection and privilege-escalation behavior against real
    hosts, not just a drop-in rewrite.
  • Ansible is more broadly known in ops contexts than Fabric — a real
    discoverability/onboarding tradeoff, not just a technical one.
  • This touches the most safety-critical path in the repo (the actual
    production deploy mechanism) — needs a careful verification plan, not a
    quick swap, and shouldn't block anything currently in flight (Normalize the hawkeye/rybbit deploy: configure prerequisites and get the first real deployment running #106's
    first real hawkeye deployment).

Proposal

Implemented, going further than originally scoped here (see the "Update"
note above) — ansible/ is deleted, deploy/deploy.py is the entire
deploy mechanism, verified via real unittest coverage of every module
plus a matrix trace-through of the updated targets//vaults/ schema.
No live deploy to hawkeye yet (still gated on infra prep), same
verification-rigor bar as originally proposed here.

Related

#107 (Capistrano-style framing this migration leans into), #103 (the
concrete duplication problem this resolves at the root instead of
patching — becomes moot once ref-resolution is a Python function), #114
(per-app vault delivery is the concrete feature this push-based model was
designed to carry).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , '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" + '
      Consider migrating ansible/deploy.yml to Fabric (Python) · Issue #111 · rubykatzen/flightdeck · GitHub
      Skip to content

      Consider migrating ansible/deploy.yml to Fabric (Python) #111

      Description

      @ineedjet

      Context

      ansible/deploy.yml doesn't actually use Ansible idiomatically. It's a
      strictly sequential procedure — download bundle, extract, pull+merge app
      packages, pull+decrypt+merge env packages, symlink switch, run
      ./deploy.sh, prune old releases — not a converging desired-state
      playbook. "Recreate release directory" even does state: absent then
      recreates unconditionally on every run, the opposite of idempotent
      convergence. This matches #107's framing exactly: flightdeck is a
      Capistrano-style deployment system (versioned releases, current symlink,
      retained history), not a Nomad/Kubernetes-style reconciler. Capistrano's
      own model — SSH in, run a sequence of tasks, switch a symlink — is what
      Fabric provides for Python, the same way Capistrano provides it for Ruby.

      Concrete friction with Ansible specifically

      • No shared-function mechanism across tasks. Each shell: task is an
        independent subprocess — there's no way to define a bash function in one
        task and reuse it in another without a checked-in script file. This is
        the direct cause of Deduplicate release-ref resolution logic in ansible/deploy.yml #103: four near-identical ~30-line "parse
        owner/repo@tag[:asset], resolve @latest via gh release view,
        download" blocks, hand-copied because Ansible has no better answer short
        of extracting a separate shell script. In Fabric this is just an
        importable Python function — the duplication wouldn't exist to begin
        with, and Deduplicate release-ref resolution logic in ansible/deploy.yml #103 would become moot rather than needing its own fix.
      • Zero unit test coverage of the deploy logic itself.encrypt-env
        and load-yaml-matrix are both Python, both have real unit tests
        (unittest, PyYAML). ansible/deploy.yml is YAML + Jinja + embedded
        bash — the only verification it gets is --syntax-check and manually
        tracing a generated matrix through by hand (done for feat: deploy rybbit for rubykatzen.com through flightdeck itself #102/feat: move apps to targets, support env_refs as a list #110, but
        that's not the same as testing the ref-parsing/collision-detection logic
        itself). A Fabric-based deploy script would be ordinary Python, testable
        the same way the rest of this repo's automation already is.
      • The inventory is ceremony, not a real feature.deploy-shared.yml
        builds a throwaway JSON inventory via jq on every run purely to
        satisfy Ansible's -i requirement — nothing here uses Ansible's actual
        host-grouping/inventory capabilities.
      • Real, recurring cost for capabilities we don't use.ansible-core
        is installed fresh via unpinned pip install on every single deploy run
        (deploy-shared.yml's "Install ansible-core" step) — paying setup cost
        for idempotency/convergence/templating machinery the playbook doesn't
        actually lean on.

      Refined direction: push-based deploy, decided during #114's design

      Discussion while designing #114 (per-app vault delivery) converged on a
      concrete shape for what a Fabric-based ansible/deploy.yml replacement
      should actually look like — this is now the leading design, not just "port
      the playbook to Python":

      • All ref-resolution/download logic moves to the CI runner. Parsing
        owner/repo@tag[:asset], resolving @latest via the GitHub API,
        downloading the resolved asset — all of it runs as ordinary Python on
        the GitHub Actions runner, written once, reused for the machinery
        bundle, every app bundle, and every encrypted vault asset. The server
        never calls gh itself and doesn't need it installed — a real
        prerequisite reduction from what README currently documents.
      • Fabric pushes files to the server instead of the server pulling
        them.
        Downloaded app bundles and still-encrypted vault blobs are
        transferred to the server directly (SSH/SCP), rather than the server
        reaching out to GitHub itself.
      • Decryption stays strictly server-side. The server's private age key
        never leaves it, same as today. The server's only vault-related
        capability is running sops decrypt on a single file it's given — a
        generic, dumb, reusable primitive with no knowledge of refs, targets, or
        merging.
      • No composition/merge logic lives on the server at all — but collision
        detection still happens, in CI, before anything is pushed.
        SOPS's
        dotenv output format only encrypts values; key names stay in
        cleartext (APPS_DOMAIN=ENC[...]). So when an app's env_refs has more
        than one entry, Fabric downloads each .sops.env asset and compares key
        names across them directly — no decryption needed for this check at
        all. Any duplicate key fails the build right there, before anything
        touches the server. Only once that check passes does Fabric push the
        files and tell the server to decrypt each one and plain cat them
        together into apps/{app}/.env. The server still never understands
        composition; it just never sees a colliding set in the first place.
      • Update, superseding the below: this went further than "almost
        nothing" — the session that implemented this also decided flightdeck
        will have no manual administration flow at all, ever (no server SSH
        console access, no local quick-start either). With that decided,
        decryption and config-template rendering moved to the CI runner too
        (see Consider decrypting vaults on the CI runner instead of server-side #116), and literally nothing server-side survives:
        up.sh/down.sh/restart.sh/deploy.sh/generate-env.sh/lib.sh
        were all deleted outright, no replacements. The target host's only
        dependencies are Docker and Docker Compose. deploy/deploy.py issues
        docker compose pull/up directly per app over SSH; there is no
        server-side script layer of any kind left to describe.

      Considerations against

      • Ansible's SSH connection handling, privilege escalation (become), and
        retry semantics are battle-tested. Re-implementing that in Fabric means
        re-verifying connection and privilege-escalation behavior against real
        hosts, not just a drop-in rewrite.
      • Ansible is more broadly known in ops contexts than Fabric — a real
        discoverability/onboarding tradeoff, not just a technical one.
      • This touches the most safety-critical path in the repo (the actual
        production deploy mechanism) — needs a careful verification plan, not a
        quick swap, and shouldn't block anything currently in flight (Normalize the hawkeye/rybbit deploy: configure prerequisites and get the first real deployment running #106's
        first real hawkeye deployment).

      Proposal

      Implemented, going further than originally scoped here (see the "Update"
      note above) — ansible/ is deleted, deploy/deploy.py is the entire
      deploy mechanism, verified via real unittest coverage of every module
      plus a matrix trace-through of the updated targets//vaults/ schema.
      No live deploy to hawkeye yet (still gated on infra prep), same
      verification-rigor bar as originally proposed here.

      Related

      #107 (Capistrano-style framing this migration leans into), #103 (the
      concrete duplication problem this resolves at the root instead of
      patching — becomes moot once ref-resolution is a Python function), #114
      (per-app vault delivery is the concrete feature this push-based model was
      designed to carry).

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , '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('^' + ".*" + ' Consider migrating ansible/deploy.yml to Fabric (Python) · Issue #111 · rubykatzen/flightdeck · GitHub
          Skip to content

          Consider migrating ansible/deploy.yml to Fabric (Python) #111

          Description

          @ineedjet

          Context

          ansible/deploy.yml doesn't actually use Ansible idiomatically. It's a
          strictly sequential procedure — download bundle, extract, pull+merge app
          packages, pull+decrypt+merge env packages, symlink switch, run
          ./deploy.sh, prune old releases — not a converging desired-state
          playbook. "Recreate release directory" even does state: absent then
          recreates unconditionally on every run, the opposite of idempotent
          convergence. This matches #107's framing exactly: flightdeck is a
          Capistrano-style deployment system (versioned releases, current symlink,
          retained history), not a Nomad/Kubernetes-style reconciler. Capistrano's
          own model — SSH in, run a sequence of tasks, switch a symlink — is what
          Fabric provides for Python, the same way Capistrano provides it for Ruby.

          Concrete friction with Ansible specifically

          • No shared-function mechanism across tasks. Each shell: task is an
            independent subprocess — there's no way to define a bash function in one
            task and reuse it in another without a checked-in script file. This is
            the direct cause of Deduplicate release-ref resolution logic in ansible/deploy.yml #103: four near-identical ~30-line "parse
            owner/repo@tag[:asset], resolve @latest via gh release view,
            download" blocks, hand-copied because Ansible has no better answer short
            of extracting a separate shell script. In Fabric this is just an
            importable Python function — the duplication wouldn't exist to begin
            with, and Deduplicate release-ref resolution logic in ansible/deploy.yml #103 would become moot rather than needing its own fix.
          • Zero unit test coverage of the deploy logic itself.encrypt-env
            and load-yaml-matrix are both Python, both have real unit tests
            (unittest, PyYAML). ansible/deploy.yml is YAML + Jinja + embedded
            bash — the only verification it gets is --syntax-check and manually
            tracing a generated matrix through by hand (done for feat: deploy rybbit for rubykatzen.com through flightdeck itself #102/feat: move apps to targets, support env_refs as a list #110, but
            that's not the same as testing the ref-parsing/collision-detection logic
            itself). A Fabric-based deploy script would be ordinary Python, testable
            the same way the rest of this repo's automation already is.
          • The inventory is ceremony, not a real feature.deploy-shared.yml
            builds a throwaway JSON inventory via jq on every run purely to
            satisfy Ansible's -i requirement — nothing here uses Ansible's actual
            host-grouping/inventory capabilities.
          • Real, recurring cost for capabilities we don't use.ansible-core
            is installed fresh via unpinned pip install on every single deploy run
            (deploy-shared.yml's "Install ansible-core" step) — paying setup cost
            for idempotency/convergence/templating machinery the playbook doesn't
            actually lean on.

          Refined direction: push-based deploy, decided during #114's design

          Discussion while designing #114 (per-app vault delivery) converged on a
          concrete shape for what a Fabric-based ansible/deploy.yml replacement
          should actually look like — this is now the leading design, not just "port
          the playbook to Python":

          • All ref-resolution/download logic moves to the CI runner. Parsing
            owner/repo@tag[:asset], resolving @latest via the GitHub API,
            downloading the resolved asset — all of it runs as ordinary Python on
            the GitHub Actions runner, written once, reused for the machinery
            bundle, every app bundle, and every encrypted vault asset. The server
            never calls gh itself and doesn't need it installed — a real
            prerequisite reduction from what README currently documents.
          • Fabric pushes files to the server instead of the server pulling
            them.
            Downloaded app bundles and still-encrypted vault blobs are
            transferred to the server directly (SSH/SCP), rather than the server
            reaching out to GitHub itself.
          • Decryption stays strictly server-side. The server's private age key
            never leaves it, same as today. The server's only vault-related
            capability is running sops decrypt on a single file it's given — a
            generic, dumb, reusable primitive with no knowledge of refs, targets, or
            merging.
          • No composition/merge logic lives on the server at all — but collision
            detection still happens, in CI, before anything is pushed.
            SOPS's
            dotenv output format only encrypts values; key names stay in
            cleartext (APPS_DOMAIN=ENC[...]). So when an app's env_refs has more
            than one entry, Fabric downloads each .sops.env asset and compares key
            names across them directly — no decryption needed for this check at
            all. Any duplicate key fails the build right there, before anything
            touches the server. Only once that check passes does Fabric push the
            files and tell the server to decrypt each one and plain cat them
            together into apps/{app}/.env. The server still never understands
            composition; it just never sees a colliding set in the first place.
          • Update, superseding the below: this went further than "almost
            nothing" — the session that implemented this also decided flightdeck
            will have no manual administration flow at all, ever (no server SSH
            console access, no local quick-start either). With that decided,
            decryption and config-template rendering moved to the CI runner too
            (see Consider decrypting vaults on the CI runner instead of server-side #116), and literally nothing server-side survives:
            up.sh/down.sh/restart.sh/deploy.sh/generate-env.sh/lib.sh
            were all deleted outright, no replacements. The target host's only
            dependencies are Docker and Docker Compose. deploy/deploy.py issues
            docker compose pull/up directly per app over SSH; there is no
            server-side script layer of any kind left to describe.

          Considerations against

          • Ansible's SSH connection handling, privilege escalation (become), and
            retry semantics are battle-tested. Re-implementing that in Fabric means
            re-verifying connection and privilege-escalation behavior against real
            hosts, not just a drop-in rewrite.
          • Ansible is more broadly known in ops contexts than Fabric — a real
            discoverability/onboarding tradeoff, not just a technical one.
          • This touches the most safety-critical path in the repo (the actual
            production deploy mechanism) — needs a careful verification plan, not a
            quick swap, and shouldn't block anything currently in flight (Normalize the hawkeye/rybbit deploy: configure prerequisites and get the first real deployment running #106's
            first real hawkeye deployment).

          Proposal

          Implemented, going further than originally scoped here (see the "Update"
          note above) — ansible/ is deleted, deploy/deploy.py is the entire
          deploy mechanism, verified via real unittest coverage of every module
          plus a matrix trace-through of the updated targets//vaults/ schema.
          No live deploy to hawkeye yet (still gated on infra prep), same
          verification-rigor bar as originally proposed here.

          Related

          #107 (Capistrano-style framing this migration leans into), #103 (the
          concrete duplication problem this resolves at the root instead of
          patching — becomes moot once ref-resolution is a Python function), #114
          (per-app vault delivery is the concrete feature this push-based model was
          designed to carry).

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , '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('^' + ".*" + ' Consider migrating ansible/deploy.yml to Fabric (Python) · Issue #111 · rubykatzen/flightdeck · GitHub
              Skip to content

              Consider migrating ansible/deploy.yml to Fabric (Python) #111

              Description

              @ineedjet

              Context

              ansible/deploy.yml doesn't actually use Ansible idiomatically. It's a
              strictly sequential procedure — download bundle, extract, pull+merge app
              packages, pull+decrypt+merge env packages, symlink switch, run
              ./deploy.sh, prune old releases — not a converging desired-state
              playbook. "Recreate release directory" even does state: absent then
              recreates unconditionally on every run, the opposite of idempotent
              convergence. This matches #107's framing exactly: flightdeck is a
              Capistrano-style deployment system (versioned releases, current symlink,
              retained history), not a Nomad/Kubernetes-style reconciler. Capistrano's
              own model — SSH in, run a sequence of tasks, switch a symlink — is what
              Fabric provides for Python, the same way Capistrano provides it for Ruby.

              Concrete friction with Ansible specifically

              • No shared-function mechanism across tasks. Each shell: task is an
                independent subprocess — there's no way to define a bash function in one
                task and reuse it in another without a checked-in script file. This is
                the direct cause of Deduplicate release-ref resolution logic in ansible/deploy.yml #103: four near-identical ~30-line "parse
                owner/repo@tag[:asset], resolve @latest via gh release view,
                download" blocks, hand-copied because Ansible has no better answer short
                of extracting a separate shell script. In Fabric this is just an
                importable Python function — the duplication wouldn't exist to begin
                with, and Deduplicate release-ref resolution logic in ansible/deploy.yml #103 would become moot rather than needing its own fix.
              • Zero unit test coverage of the deploy logic itself.encrypt-env
                and load-yaml-matrix are both Python, both have real unit tests
                (unittest, PyYAML). ansible/deploy.yml is YAML + Jinja + embedded
                bash — the only verification it gets is --syntax-check and manually
                tracing a generated matrix through by hand (done for feat: deploy rybbit for rubykatzen.com through flightdeck itself #102/feat: move apps to targets, support env_refs as a list #110, but
                that's not the same as testing the ref-parsing/collision-detection logic
                itself). A Fabric-based deploy script would be ordinary Python, testable
                the same way the rest of this repo's automation already is.
              • The inventory is ceremony, not a real feature.deploy-shared.yml
                builds a throwaway JSON inventory via jq on every run purely to
                satisfy Ansible's -i requirement — nothing here uses Ansible's actual
                host-grouping/inventory capabilities.
              • Real, recurring cost for capabilities we don't use.ansible-core
                is installed fresh via unpinned pip install on every single deploy run
                (deploy-shared.yml's "Install ansible-core" step) — paying setup cost
                for idempotency/convergence/templating machinery the playbook doesn't
                actually lean on.

              Refined direction: push-based deploy, decided during #114's design

              Discussion while designing #114 (per-app vault delivery) converged on a
              concrete shape for what a Fabric-based ansible/deploy.yml replacement
              should actually look like — this is now the leading design, not just "port
              the playbook to Python":

              • All ref-resolution/download logic moves to the CI runner. Parsing
                owner/repo@tag[:asset], resolving @latest via the GitHub API,
                downloading the resolved asset — all of it runs as ordinary Python on
                the GitHub Actions runner, written once, reused for the machinery
                bundle, every app bundle, and every encrypted vault asset. The server
                never calls gh itself and doesn't need it installed — a real
                prerequisite reduction from what README currently documents.
              • Fabric pushes files to the server instead of the server pulling
                them.
                Downloaded app bundles and still-encrypted vault blobs are
                transferred to the server directly (SSH/SCP), rather than the server
                reaching out to GitHub itself.
              • Decryption stays strictly server-side. The server's private age key
                never leaves it, same as today. The server's only vault-related
                capability is running sops decrypt on a single file it's given — a
                generic, dumb, reusable primitive with no knowledge of refs, targets, or
                merging.
              • No composition/merge logic lives on the server at all — but collision
                detection still happens, in CI, before anything is pushed.
                SOPS's
                dotenv output format only encrypts values; key names stay in
                cleartext (APPS_DOMAIN=ENC[...]). So when an app's env_refs has more
                than one entry, Fabric downloads each .sops.env asset and compares key
                names across them directly — no decryption needed for this check at
                all. Any duplicate key fails the build right there, before anything
                touches the server. Only once that check passes does Fabric push the
                files and tell the server to decrypt each one and plain cat them
                together into apps/{app}/.env. The server still never understands
                composition; it just never sees a colliding set in the first place.
              • Update, superseding the below: this went further than "almost
                nothing" — the session that implemented this also decided flightdeck
                will have no manual administration flow at all, ever (no server SSH
                console access, no local quick-start either). With that decided,
                decryption and config-template rendering moved to the CI runner too
                (see Consider decrypting vaults on the CI runner instead of server-side #116), and literally nothing server-side survives:
                up.sh/down.sh/restart.sh/deploy.sh/generate-env.sh/lib.sh
                were all deleted outright, no replacements. The target host's only
                dependencies are Docker and Docker Compose. deploy/deploy.py issues
                docker compose pull/up directly per app over SSH; there is no
                server-side script layer of any kind left to describe.

              Considerations against

              • Ansible's SSH connection handling, privilege escalation (become), and
                retry semantics are battle-tested. Re-implementing that in Fabric means
                re-verifying connection and privilege-escalation behavior against real
                hosts, not just a drop-in rewrite.
              • Ansible is more broadly known in ops contexts than Fabric — a real
                discoverability/onboarding tradeoff, not just a technical one.
              • This touches the most safety-critical path in the repo (the actual
                production deploy mechanism) — needs a careful verification plan, not a
                quick swap, and shouldn't block anything currently in flight (Normalize the hawkeye/rybbit deploy: configure prerequisites and get the first real deployment running #106's
                first real hawkeye deployment).

              Proposal

              Implemented, going further than originally scoped here (see the "Update"
              note above) — ansible/ is deleted, deploy/deploy.py is the entire
              deploy mechanism, verified via real unittest coverage of every module
              plus a matrix trace-through of the updated targets//vaults/ schema.
              No live deploy to hawkeye yet (still gated on infra prep), same
              verification-rigor bar as originally proposed here.

              Related

              #107 (Capistrano-style framing this migration leans into), #103 (the
              concrete duplication problem this resolves at the root instead of
              patching — becomes moot once ref-resolution is a Python function), #114
              (per-app vault delivery is the concrete feature this push-based model was
              designed to carry).

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , '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" + ' Consider migrating ansible/deploy.yml to Fabric (Python) · Issue #111 · rubykatzen/flightdeck · GitHub
                  Skip to content

                  Consider migrating ansible/deploy.yml to Fabric (Python) #111

                  Description

                  @ineedjet

                  Context

                  ansible/deploy.yml doesn't actually use Ansible idiomatically. It's a
                  strictly sequential procedure — download bundle, extract, pull+merge app
                  packages, pull+decrypt+merge env packages, symlink switch, run
                  ./deploy.sh, prune old releases — not a converging desired-state
                  playbook. "Recreate release directory" even does state: absent then
                  recreates unconditionally on every run, the opposite of idempotent
                  convergence. This matches #107's framing exactly: flightdeck is a
                  Capistrano-style deployment system (versioned releases, current symlink,
                  retained history), not a Nomad/Kubernetes-style reconciler. Capistrano's
                  own model — SSH in, run a sequence of tasks, switch a symlink — is what
                  Fabric provides for Python, the same way Capistrano provides it for Ruby.

                  Concrete friction with Ansible specifically

                  • No shared-function mechanism across tasks. Each shell: task is an
                    independent subprocess — there's no way to define a bash function in one
                    task and reuse it in another without a checked-in script file. This is
                    the direct cause of Deduplicate release-ref resolution logic in ansible/deploy.yml #103: four near-identical ~30-line "parse
                    owner/repo@tag[:asset], resolve @latest via gh release view,
                    download" blocks, hand-copied because Ansible has no better answer short
                    of extracting a separate shell script. In Fabric this is just an
                    importable Python function — the duplication wouldn't exist to begin
                    with, and Deduplicate release-ref resolution logic in ansible/deploy.yml #103 would become moot rather than needing its own fix.
                  • Zero unit test coverage of the deploy logic itself.encrypt-env
                    and load-yaml-matrix are both Python, both have real unit tests
                    (unittest, PyYAML). ansible/deploy.yml is YAML + Jinja + embedded
                    bash — the only verification it gets is --syntax-check and manually
                    tracing a generated matrix through by hand (done for feat: deploy rybbit for rubykatzen.com through flightdeck itself #102/feat: move apps to targets, support env_refs as a list #110, but
                    that's not the same as testing the ref-parsing/collision-detection logic
                    itself). A Fabric-based deploy script would be ordinary Python, testable
                    the same way the rest of this repo's automation already is.
                  • The inventory is ceremony, not a real feature.deploy-shared.yml
                    builds a throwaway JSON inventory via jq on every run purely to
                    satisfy Ansible's -i requirement — nothing here uses Ansible's actual
                    host-grouping/inventory capabilities.
                  • Real, recurring cost for capabilities we don't use.ansible-core
                    is installed fresh via unpinned pip install on every single deploy run
                    (deploy-shared.yml's "Install ansible-core" step) — paying setup cost
                    for idempotency/convergence/templating machinery the playbook doesn't
                    actually lean on.

                  Refined direction: push-based deploy, decided during #114's design

                  Discussion while designing #114 (per-app vault delivery) converged on a
                  concrete shape for what a Fabric-based ansible/deploy.yml replacement
                  should actually look like — this is now the leading design, not just "port
                  the playbook to Python":

                  • All ref-resolution/download logic moves to the CI runner. Parsing
                    owner/repo@tag[:asset], resolving @latest via the GitHub API,
                    downloading the resolved asset — all of it runs as ordinary Python on
                    the GitHub Actions runner, written once, reused for the machinery
                    bundle, every app bundle, and every encrypted vault asset. The server
                    never calls gh itself and doesn't need it installed — a real
                    prerequisite reduction from what README currently documents.
                  • Fabric pushes files to the server instead of the server pulling
                    them.
                    Downloaded app bundles and still-encrypted vault blobs are
                    transferred to the server directly (SSH/SCP), rather than the server
                    reaching out to GitHub itself.
                  • Decryption stays strictly server-side. The server's private age key
                    never leaves it, same as today. The server's only vault-related
                    capability is running sops decrypt on a single file it's given — a
                    generic, dumb, reusable primitive with no knowledge of refs, targets, or
                    merging.
                  • No composition/merge logic lives on the server at all — but collision
                    detection still happens, in CI, before anything is pushed.
                    SOPS's
                    dotenv output format only encrypts values; key names stay in
                    cleartext (APPS_DOMAIN=ENC[...]). So when an app's env_refs has more
                    than one entry, Fabric downloads each .sops.env asset and compares key
                    names across them directly — no decryption needed for this check at
                    all. Any duplicate key fails the build right there, before anything
                    touches the server. Only once that check passes does Fabric push the
                    files and tell the server to decrypt each one and plain cat them
                    together into apps/{app}/.env. The server still never understands
                    composition; it just never sees a colliding set in the first place.
                  • Update, superseding the below: this went further than "almost
                    nothing" — the session that implemented this also decided flightdeck
                    will have no manual administration flow at all, ever (no server SSH
                    console access, no local quick-start either). With that decided,
                    decryption and config-template rendering moved to the CI runner too
                    (see Consider decrypting vaults on the CI runner instead of server-side #116), and literally nothing server-side survives:
                    up.sh/down.sh/restart.sh/deploy.sh/generate-env.sh/lib.sh
                    were all deleted outright, no replacements. The target host's only
                    dependencies are Docker and Docker Compose. deploy/deploy.py issues
                    docker compose pull/up directly per app over SSH; there is no
                    server-side script layer of any kind left to describe.

                  Considerations against

                  • Ansible's SSH connection handling, privilege escalation (become), and
                    retry semantics are battle-tested. Re-implementing that in Fabric means
                    re-verifying connection and privilege-escalation behavior against real
                    hosts, not just a drop-in rewrite.
                  • Ansible is more broadly known in ops contexts than Fabric — a real
                    discoverability/onboarding tradeoff, not just a technical one.
                  • This touches the most safety-critical path in the repo (the actual
                    production deploy mechanism) — needs a careful verification plan, not a
                    quick swap, and shouldn't block anything currently in flight (Normalize the hawkeye/rybbit deploy: configure prerequisites and get the first real deployment running #106's
                    first real hawkeye deployment).

                  Proposal

                  Implemented, going further than originally scoped here (see the "Update"
                  note above) — ansible/ is deleted, deploy/deploy.py is the entire
                  deploy mechanism, verified via real unittest coverage of every module
                  plus a matrix trace-through of the updated targets//vaults/ schema.
                  No live deploy to hawkeye yet (still gated on infra prep), same
                  verification-rigor bar as originally proposed here.

                  Related

                  #107 (Capistrano-style framing this migration leans into), #103 (the
                  concrete duplication problem this resolves at the root instead of
                  patching — becomes moot once ref-resolution is a Python function), #114
                  (per-app vault delivery is the concrete feature this push-based model was
                  designed to carry).

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , '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('^' + ".*" + ' Consider migrating ansible/deploy.yml to Fabric (Python) · Issue #111 · rubykatzen/flightdeck · GitHub
                      Skip to content

                      Consider migrating ansible/deploy.yml to Fabric (Python) #111

                      Description

                      @ineedjet

                      Context

                      ansible/deploy.yml doesn't actually use Ansible idiomatically. It's a
                      strictly sequential procedure — download bundle, extract, pull+merge app
                      packages, pull+decrypt+merge env packages, symlink switch, run
                      ./deploy.sh, prune old releases — not a converging desired-state
                      playbook. "Recreate release directory" even does state: absent then
                      recreates unconditionally on every run, the opposite of idempotent
                      convergence. This matches #107's framing exactly: flightdeck is a
                      Capistrano-style deployment system (versioned releases, current symlink,
                      retained history), not a Nomad/Kubernetes-style reconciler. Capistrano's
                      own model — SSH in, run a sequence of tasks, switch a symlink — is what
                      Fabric provides for Python, the same way Capistrano provides it for Ruby.

                      Concrete friction with Ansible specifically

                      • No shared-function mechanism across tasks. Each shell: task is an
                        independent subprocess — there's no way to define a bash function in one
                        task and reuse it in another without a checked-in script file. This is
                        the direct cause of Deduplicate release-ref resolution logic in ansible/deploy.yml #103: four near-identical ~30-line "parse
                        owner/repo@tag[:asset], resolve @latest via gh release view,
                        download" blocks, hand-copied because Ansible has no better answer short
                        of extracting a separate shell script. In Fabric this is just an
                        importable Python function — the duplication wouldn't exist to begin
                        with, and Deduplicate release-ref resolution logic in ansible/deploy.yml #103 would become moot rather than needing its own fix.
                      • Zero unit test coverage of the deploy logic itself.encrypt-env
                        and load-yaml-matrix are both Python, both have real unit tests
                        (unittest, PyYAML). ansible/deploy.yml is YAML + Jinja + embedded
                        bash — the only verification it gets is --syntax-check and manually
                        tracing a generated matrix through by hand (done for feat: deploy rybbit for rubykatzen.com through flightdeck itself #102/feat: move apps to targets, support env_refs as a list #110, but
                        that's not the same as testing the ref-parsing/collision-detection logic
                        itself). A Fabric-based deploy script would be ordinary Python, testable
                        the same way the rest of this repo's automation already is.
                      • The inventory is ceremony, not a real feature.deploy-shared.yml
                        builds a throwaway JSON inventory via jq on every run purely to
                        satisfy Ansible's -i requirement — nothing here uses Ansible's actual
                        host-grouping/inventory capabilities.
                      • Real, recurring cost for capabilities we don't use.ansible-core
                        is installed fresh via unpinned pip install on every single deploy run
                        (deploy-shared.yml's "Install ansible-core" step) — paying setup cost
                        for idempotency/convergence/templating machinery the playbook doesn't
                        actually lean on.

                      Refined direction: push-based deploy, decided during #114's design

                      Discussion while designing #114 (per-app vault delivery) converged on a
                      concrete shape for what a Fabric-based ansible/deploy.yml replacement
                      should actually look like — this is now the leading design, not just "port
                      the playbook to Python":

                      • All ref-resolution/download logic moves to the CI runner. Parsing
                        owner/repo@tag[:asset], resolving @latest via the GitHub API,
                        downloading the resolved asset — all of it runs as ordinary Python on
                        the GitHub Actions runner, written once, reused for the machinery
                        bundle, every app bundle, and every encrypted vault asset. The server
                        never calls gh itself and doesn't need it installed — a real
                        prerequisite reduction from what README currently documents.
                      • Fabric pushes files to the server instead of the server pulling
                        them.
                        Downloaded app bundles and still-encrypted vault blobs are
                        transferred to the server directly (SSH/SCP), rather than the server
                        reaching out to GitHub itself.
                      • Decryption stays strictly server-side. The server's private age key
                        never leaves it, same as today. The server's only vault-related
                        capability is running sops decrypt on a single file it's given — a
                        generic, dumb, reusable primitive with no knowledge of refs, targets, or
                        merging.
                      • No composition/merge logic lives on the server at all — but collision
                        detection still happens, in CI, before anything is pushed.
                        SOPS's
                        dotenv output format only encrypts values; key names stay in
                        cleartext (APPS_DOMAIN=ENC[...]). So when an app's env_refs has more
                        than one entry, Fabric downloads each .sops.env asset and compares key
                        names across them directly — no decryption needed for this check at
                        all. Any duplicate key fails the build right there, before anything
                        touches the server. Only once that check passes does Fabric push the
                        files and tell the server to decrypt each one and plain cat them
                        together into apps/{app}/.env. The server still never understands
                        composition; it just never sees a colliding set in the first place.
                      • Update, superseding the below: this went further than "almost
                        nothing" — the session that implemented this also decided flightdeck
                        will have no manual administration flow at all, ever (no server SSH
                        console access, no local quick-start either). With that decided,
                        decryption and config-template rendering moved to the CI runner too
                        (see Consider decrypting vaults on the CI runner instead of server-side #116), and literally nothing server-side survives:
                        up.sh/down.sh/restart.sh/deploy.sh/generate-env.sh/lib.sh
                        were all deleted outright, no replacements. The target host's only
                        dependencies are Docker and Docker Compose. deploy/deploy.py issues
                        docker compose pull/up directly per app over SSH; there is no
                        server-side script layer of any kind left to describe.

                      Considerations against

                      • Ansible's SSH connection handling, privilege escalation (become), and
                        retry semantics are battle-tested. Re-implementing that in Fabric means
                        re-verifying connection and privilege-escalation behavior against real
                        hosts, not just a drop-in rewrite.
                      • Ansible is more broadly known in ops contexts than Fabric — a real
                        discoverability/onboarding tradeoff, not just a technical one.
                      • This touches the most safety-critical path in the repo (the actual
                        production deploy mechanism) — needs a careful verification plan, not a
                        quick swap, and shouldn't block anything currently in flight (Normalize the hawkeye/rybbit deploy: configure prerequisites and get the first real deployment running #106's
                        first real hawkeye deployment).

                      Proposal

                      Implemented, going further than originally scoped here (see the "Update"
                      note above) — ansible/ is deleted, deploy/deploy.py is the entire
                      deploy mechanism, verified via real unittest coverage of every module
                      plus a matrix trace-through of the updated targets//vaults/ schema.
                      No live deploy to hawkeye yet (still gated on infra prep), same
                      verification-rigor bar as originally proposed here.

                      Related

                      #107 (Capistrano-style framing this migration leans into), #103 (the
                      concrete duplication problem this resolves at the root instead of
                      patching — becomes moot once ref-resolution is a Python function), #114
                      (per-app vault delivery is the concrete feature this push-based model was
                      designed to carry).

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , '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('^' + ".*" + ' Consider migrating ansible/deploy.yml to Fabric (Python) · Issue #111 · rubykatzen/flightdeck · GitHub
                          Skip to content

                          Consider migrating ansible/deploy.yml to Fabric (Python) #111

                          Description

                          @ineedjet

                          Context

                          ansible/deploy.yml doesn't actually use Ansible idiomatically. It's a
                          strictly sequential procedure — download bundle, extract, pull+merge app
                          packages, pull+decrypt+merge env packages, symlink switch, run
                          ./deploy.sh, prune old releases — not a converging desired-state
                          playbook. "Recreate release directory" even does state: absent then
                          recreates unconditionally on every run, the opposite of idempotent
                          convergence. This matches #107's framing exactly: flightdeck is a
                          Capistrano-style deployment system (versioned releases, current symlink,
                          retained history), not a Nomad/Kubernetes-style reconciler. Capistrano's
                          own model — SSH in, run a sequence of tasks, switch a symlink — is what
                          Fabric provides for Python, the same way Capistrano provides it for Ruby.

                          Concrete friction with Ansible specifically

                          • No shared-function mechanism across tasks. Each shell: task is an
                            independent subprocess — there's no way to define a bash function in one
                            task and reuse it in another without a checked-in script file. This is
                            the direct cause of Deduplicate release-ref resolution logic in ansible/deploy.yml #103: four near-identical ~30-line "parse
                            owner/repo@tag[:asset], resolve @latest via gh release view,
                            download" blocks, hand-copied because Ansible has no better answer short
                            of extracting a separate shell script. In Fabric this is just an
                            importable Python function — the duplication wouldn't exist to begin
                            with, and Deduplicate release-ref resolution logic in ansible/deploy.yml #103 would become moot rather than needing its own fix.
                          • Zero unit test coverage of the deploy logic itself.encrypt-env
                            and load-yaml-matrix are both Python, both have real unit tests
                            (unittest, PyYAML). ansible/deploy.yml is YAML + Jinja + embedded
                            bash — the only verification it gets is --syntax-check and manually
                            tracing a generated matrix through by hand (done for feat: deploy rybbit for rubykatzen.com through flightdeck itself #102/feat: move apps to targets, support env_refs as a list #110, but
                            that's not the same as testing the ref-parsing/collision-detection logic
                            itself). A Fabric-based deploy script would be ordinary Python, testable
                            the same way the rest of this repo's automation already is.
                          • The inventory is ceremony, not a real feature.deploy-shared.yml
                            builds a throwaway JSON inventory via jq on every run purely to
                            satisfy Ansible's -i requirement — nothing here uses Ansible's actual
                            host-grouping/inventory capabilities.
                          • Real, recurring cost for capabilities we don't use.ansible-core
                            is installed fresh via unpinned pip install on every single deploy run
                            (deploy-shared.yml's "Install ansible-core" step) — paying setup cost
                            for idempotency/convergence/templating machinery the playbook doesn't
                            actually lean on.

                          Refined direction: push-based deploy, decided during #114's design

                          Discussion while designing #114 (per-app vault delivery) converged on a
                          concrete shape for what a Fabric-based ansible/deploy.yml replacement
                          should actually look like — this is now the leading design, not just "port
                          the playbook to Python":

                          • All ref-resolution/download logic moves to the CI runner. Parsing
                            owner/repo@tag[:asset], resolving @latest via the GitHub API,
                            downloading the resolved asset — all of it runs as ordinary Python on
                            the GitHub Actions runner, written once, reused for the machinery
                            bundle, every app bundle, and every encrypted vault asset. The server
                            never calls gh itself and doesn't need it installed — a real
                            prerequisite reduction from what README currently documents.
                          • Fabric pushes files to the server instead of the server pulling
                            them.
                            Downloaded app bundles and still-encrypted vault blobs are
                            transferred to the server directly (SSH/SCP), rather than the server
                            reaching out to GitHub itself.
                          • Decryption stays strictly server-side. The server's private age key
                            never leaves it, same as today. The server's only vault-related
                            capability is running sops decrypt on a single file it's given — a
                            generic, dumb, reusable primitive with no knowledge of refs, targets, or
                            merging.
                          • No composition/merge logic lives on the server at all — but collision
                            detection still happens, in CI, before anything is pushed.
                            SOPS's
                            dotenv output format only encrypts values; key names stay in
                            cleartext (APPS_DOMAIN=ENC[...]). So when an app's env_refs has more
                            than one entry, Fabric downloads each .sops.env asset and compares key
                            names across them directly — no decryption needed for this check at
                            all. Any duplicate key fails the build right there, before anything
                            touches the server. Only once that check passes does Fabric push the
                            files and tell the server to decrypt each one and plain cat them
                            together into apps/{app}/.env. The server still never understands
                            composition; it just never sees a colliding set in the first place.
                          • Update, superseding the below: this went further than "almost
                            nothing" — the session that implemented this also decided flightdeck
                            will have no manual administration flow at all, ever (no server SSH
                            console access, no local quick-start either). With that decided,
                            decryption and config-template rendering moved to the CI runner too
                            (see Consider decrypting vaults on the CI runner instead of server-side #116), and literally nothing server-side survives:
                            up.sh/down.sh/restart.sh/deploy.sh/generate-env.sh/lib.sh
                            were all deleted outright, no replacements. The target host's only
                            dependencies are Docker and Docker Compose. deploy/deploy.py issues
                            docker compose pull/up directly per app over SSH; there is no
                            server-side script layer of any kind left to describe.

                          Considerations against

                          • Ansible's SSH connection handling, privilege escalation (become), and
                            retry semantics are battle-tested. Re-implementing that in Fabric means
                            re-verifying connection and privilege-escalation behavior against real
                            hosts, not just a drop-in rewrite.
                          • Ansible is more broadly known in ops contexts than Fabric — a real
                            discoverability/onboarding tradeoff, not just a technical one.
                          • This touches the most safety-critical path in the repo (the actual
                            production deploy mechanism) — needs a careful verification plan, not a
                            quick swap, and shouldn't block anything currently in flight (Normalize the hawkeye/rybbit deploy: configure prerequisites and get the first real deployment running #106's
                            first real hawkeye deployment).

                          Proposal

                          Implemented, going further than originally scoped here (see the "Update"
                          note above) — ansible/ is deleted, deploy/deploy.py is the entire
                          deploy mechanism, verified via real unittest coverage of every module
                          plus a matrix trace-through of the updated targets//vaults/ schema.
                          No live deploy to hawkeye yet (still gated on infra prep), same
                          verification-rigor bar as originally proposed here.

                          Related

                          #107 (Capistrano-style framing this migration leans into), #103 (the
                          concrete duplication problem this resolves at the root instead of
                          patching — becomes moot once ref-resolution is a Python function), #114
                          (per-app vault delivery is the concrete feature this push-based model was
                          designed to carry).

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , '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); } })(); })(); Consider migrating ansible/deploy.yml to Fabric (Python) · Issue #111 · rubykatzen/flightdeck · GitHub
                              Skip to content

                              Consider migrating ansible/deploy.yml to Fabric (Python) #111

                              Description

                              @ineedjet

                              Context

                              ansible/deploy.yml doesn't actually use Ansible idiomatically. It's a
                              strictly sequential procedure — download bundle, extract, pull+merge app
                              packages, pull+decrypt+merge env packages, symlink switch, run
                              ./deploy.sh, prune old releases — not a converging desired-state
                              playbook. "Recreate release directory" even does state: absent then
                              recreates unconditionally on every run, the opposite of idempotent
                              convergence. This matches #107's framing exactly: flightdeck is a
                              Capistrano-style deployment system (versioned releases, current symlink,
                              retained history), not a Nomad/Kubernetes-style reconciler. Capistrano's
                              own model — SSH in, run a sequence of tasks, switch a symlink — is what
                              Fabric provides for Python, the same way Capistrano provides it for Ruby.

                              Concrete friction with Ansible specifically

                              • No shared-function mechanism across tasks. Each shell: task is an
                                independent subprocess — there's no way to define a bash function in one
                                task and reuse it in another without a checked-in script file. This is
                                the direct cause of Deduplicate release-ref resolution logic in ansible/deploy.yml #103: four near-identical ~30-line "parse
                                owner/repo@tag[:asset], resolve @latest via gh release view,
                                download" blocks, hand-copied because Ansible has no better answer short
                                of extracting a separate shell script. In Fabric this is just an
                                importable Python function — the duplication wouldn't exist to begin
                                with, and Deduplicate release-ref resolution logic in ansible/deploy.yml #103 would become moot rather than needing its own fix.
                              • Zero unit test coverage of the deploy logic itself.encrypt-env
                                and load-yaml-matrix are both Python, both have real unit tests
                                (unittest, PyYAML). ansible/deploy.yml is YAML + Jinja + embedded
                                bash — the only verification it gets is --syntax-check and manually
                                tracing a generated matrix through by hand (done for feat: deploy rybbit for rubykatzen.com through flightdeck itself #102/feat: move apps to targets, support env_refs as a list #110, but
                                that's not the same as testing the ref-parsing/collision-detection logic
                                itself). A Fabric-based deploy script would be ordinary Python, testable
                                the same way the rest of this repo's automation already is.
                              • The inventory is ceremony, not a real feature.deploy-shared.yml
                                builds a throwaway JSON inventory via jq on every run purely to
                                satisfy Ansible's -i requirement — nothing here uses Ansible's actual
                                host-grouping/inventory capabilities.
                              • Real, recurring cost for capabilities we don't use.ansible-core
                                is installed fresh via unpinned pip install on every single deploy run
                                (deploy-shared.yml's "Install ansible-core" step) — paying setup cost
                                for idempotency/convergence/templating machinery the playbook doesn't
                                actually lean on.

                              Refined direction: push-based deploy, decided during #114's design

                              Discussion while designing #114 (per-app vault delivery) converged on a
                              concrete shape for what a Fabric-based ansible/deploy.yml replacement
                              should actually look like — this is now the leading design, not just "port
                              the playbook to Python":

                              • All ref-resolution/download logic moves to the CI runner. Parsing
                                owner/repo@tag[:asset], resolving @latest via the GitHub API,
                                downloading the resolved asset — all of it runs as ordinary Python on
                                the GitHub Actions runner, written once, reused for the machinery
                                bundle, every app bundle, and every encrypted vault asset. The server
                                never calls gh itself and doesn't need it installed — a real
                                prerequisite reduction from what README currently documents.
                              • Fabric pushes files to the server instead of the server pulling
                                them.
                                Downloaded app bundles and still-encrypted vault blobs are
                                transferred to the server directly (SSH/SCP), rather than the server
                                reaching out to GitHub itself.
                              • Decryption stays strictly server-side. The server's private age key
                                never leaves it, same as today. The server's only vault-related
                                capability is running sops decrypt on a single file it's given — a
                                generic, dumb, reusable primitive with no knowledge of refs, targets, or
                                merging.
                              • No composition/merge logic lives on the server at all — but collision
                                detection still happens, in CI, before anything is pushed.
                                SOPS's
                                dotenv output format only encrypts values; key names stay in
                                cleartext (APPS_DOMAIN=ENC[...]). So when an app's env_refs has more
                                than one entry, Fabric downloads each .sops.env asset and compares key
                                names across them directly — no decryption needed for this check at
                                all. Any duplicate key fails the build right there, before anything
                                touches the server. Only once that check passes does Fabric push the
                                files and tell the server to decrypt each one and plain cat them
                                together into apps/{app}/.env. The server still never understands
                                composition; it just never sees a colliding set in the first place.
                              • Update, superseding the below: this went further than "almost
                                nothing" — the session that implemented this also decided flightdeck
                                will have no manual administration flow at all, ever (no server SSH
                                console access, no local quick-start either). With that decided,
                                decryption and config-template rendering moved to the CI runner too
                                (see Consider decrypting vaults on the CI runner instead of server-side #116), and literally nothing server-side survives:
                                up.sh/down.sh/restart.sh/deploy.sh/generate-env.sh/lib.sh
                                were all deleted outright, no replacements. The target host's only
                                dependencies are Docker and Docker Compose. deploy/deploy.py issues
                                docker compose pull/up directly per app over SSH; there is no
                                server-side script layer of any kind left to describe.

                              Considerations against

                              • Ansible's SSH connection handling, privilege escalation (become), and
                                retry semantics are battle-tested. Re-implementing that in Fabric means
                                re-verifying connection and privilege-escalation behavior against real
                                hosts, not just a drop-in rewrite.
                              • Ansible is more broadly known in ops contexts than Fabric — a real
                                discoverability/onboarding tradeoff, not just a technical one.
                              • This touches the most safety-critical path in the repo (the actual
                                production deploy mechanism) — needs a careful verification plan, not a
                                quick swap, and shouldn't block anything currently in flight (Normalize the hawkeye/rybbit deploy: configure prerequisites and get the first real deployment running #106's
                                first real hawkeye deployment).

                              Proposal

                              Implemented, going further than originally scoped here (see the "Update"
                              note above) — ansible/ is deleted, deploy/deploy.py is the entire
                              deploy mechanism, verified via real unittest coverage of every module
                              plus a matrix trace-through of the updated targets//vaults/ schema.
                              No live deploy to hawkeye yet (still gated on infra prep), same
                              verification-rigor bar as originally proposed here.

                              Related

                              #107 (Capistrano-style framing this migration leans into), #103 (the
                              concrete duplication problem this resolves at the root instead of
                              patching — becomes moot once ref-resolution is a Python function), #114
                              (per-app vault delivery is the concrete feature this push-based model was
                              designed to carry).

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions