Skip to content

fix(gitops): the cell file never silently drops a composed module's workloads - #1150

Merged
antoinetoussaint-byte merged 1 commit into
mainfrom
fix/cell-file-never-drops-workloads
Oct 24, 2026
Merged

antoinetoussaint-byte merged 1 commit into
mainfrom
fix/cell-file-never-drops-workloads

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor

Defect

On 2026-10-24, in obin-ai/platform-obin with CLI 0.1.211, codefly deploy gitops render <module> --env blue rebuilt the whole cell file (deployments/environments/blue/cells/blue/cell.yaml) from whichever per-module render records (deployments/modules/<m>/.codefly-render.json) happened to be in the checkout. Those records are uncommitted render outputs. After a git clean of deployments/, rendering only saas and robin wrote a cell file with 4 insertions and 1026 deletions: 25 workloads of 9 other modules were gone. Publishing that file would have taken those modules out of admission, so every new pod would have been denied. Earlier the same day, a render where 6 modules failed produced a cell file missing 818 lines. Neither run warned.

Design

Classification: fix, made where the behaviour lives (the cell render and the publish). No hack.

  1. The render refuses to drop a module the workspace composes (refuseUnrenderedModules, pkg/gitops/cell.go). Suppose a module in workspace.Modules has no render record for the environment, either because there is none or because the one present was rendered for a different environment. Then deriveCell refuses. It names each such module and gives the command that renders it (codefly deploy gitops render <m> --env <env>, or render --all). The cell file is not written, and the copy on disk stays as it was. A single-module render still finishes and reports the refusal through the existing "Cell file not re-derived" warning. Rendering module by module converges, and render --all is unaffected.
    • Refuse, or keep the module's entries from the cell file on disk? It refuses. A namespace entry is not owned by its module alone. Its consumer lists come from other modules' declared dependencies, and its bindings and egress come from the environment. An entry carried over from an earlier render is therefore a claim about a composition that may no longer exist. A new consumer would silently miss an edge, and a dependency that was dropped would silently keep one. The file the entry would be copied from is also cleaned by the same git clean that removes the records. No digest is invented.
    • The same derivation feeds deploy dev. It already refused any namespace move (cellMovesBeyond), and it now gets the clearer reason.
  2. Publish holds the cell file to the base branch's file (holdCellToBase, pkg/gitops/cell_removals.go). plan and publish read two files for a hosted environment: the workspace's cell file, and the one at <gitops path>/cells/<env>/cell.yaml on the delivery base revision in the promotion clone. They refuse when the workspace's file removes anything the base admits: a namespace, a workload, an endpoint, or a consumer of an endpoint. The error names every removal and the module it concerns.
    • --allow-cell-removal <module> (repeatable, on plan/publish) lets through the removals that concern a named module. Use it, for example, when the workspace no longer composes a module. A consumer edge is covered by naming either the endpoint's module or the consumer's module.
    • The plan lists every removal it allowed in cellRemovals. That field is part of the plan ID.
    • A base with no cell file has nothing to compare against.
    • This implements the rule "a render is a claim about a cell; refuse a render that removes a consumer without saying so" (handbook proposal 2026-10-19-one-ceiling-one-proof, items 4 and 5).

Reused: deriveCell / CellPath / cellfile.Parse, the promotion clone and base revision that preparePublish already holds, gitCommandBytes / gitSaysAbsent, and the plan-ID hashing.

Not done, on purpose: publish still does not stage the cell file into the delivery repository. The cell file is committed alongside the rendered trees, as before. This check is what holds that claim to the base before a promotion. Rollback is not covered: it re-promotes a module tree and leaves the cell file alone.

Tests

  • TestCellFileRefusesToDropAComposedModuleWithNoRender: a full render writes the file. Deleting billing's render makes the next render refuse, naming billing and codefly deploy gitops render billing --env staging, and the file on disk stays byte-identical. Rendering billing again writes the file.
  • TestCellFileSkipsTreesRenderedForAnotherEnvironment (updated): a composed module whose only record is for another environment is refused by name instead of being skipped silently.
  • TestPublishRefusesACellFileThatRemovesWhatTheBaseAdmits:
    • A full render compared with a base holding the same file has no removals.
    • Dropping billing's namespace and its consumer edge is refused, naming both removals and --allow-cell-removal (billing).
    • Naming only shop covers the edge but not the namespace.
    • Naming billing allows both, and the plan lists them.
    • A base with no cell file is not compared.
  • TestCellFileInventoriesEveryRenderedWorkload and the rest of pkg/gitops, cmd/deploy and cmd (CLI reference) pass unchanged. make lint reports 0 issues.

Docs

  • docs/commands.md: the cell file section now covers the never-drop rule with its justification and the publish hold with --allow-cell-removal. The render section says a single render reports a composed module with no render as owed.
  • docs/cli-reference.md: regenerated for the new flag.

Not verified

  • Not run against platform-obin or a cell. The 1026- and 818-line incidents were not replayed. Both are covered by the fixture tests above.
  • holdCellToBase is unit-tested directly against a real git base revision. No full publish test goes through a hosted fixture with a pre-existing base cell file; the wiring in preparePublish is one call.
  • Whether workspace.Modules matches "every module delivered to this environment" in every composition: a module whose services an environment excludes entirely would now need a record. It is the same set render --all renders.

🤖 Generated with Claude Code

…orkloads

The cell file is regenerated whole from the per-module render records
(deployments/modules/<m>/.codefly-render.json), which are uncommitted render
outputs. After a clean of deployments/, rendering saas and robin wrote a cell
file with 4 insertions and 1026 deletions: 25 workloads of 9 other modules
gone, and nothing warned.

- A render refuses to write the cell file while a module the workspace
  composes has no render record for the environment (missing, or rendered for
  another environment), naming each module and the command that renders it.
  It does not carry the module's entries over from the file on disk: an entry
  is derived from the whole composition, so a carried-over entry is a claim
  about a composition that may no longer exist.
- plan/publish hold the workspace's cell file to the delivery base branch's
  and refuse one that removes a namespace, workload, endpoint or endpoint
  consumer the base admits, unless --allow-cell-removal names the module the
  removal concerns; the plan lists every removal it lets through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit 4d077bf Oct 24, 2026
14 checks passed
Sign up for free to 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.

1 participant