diff --git a/README.md b/README.md index e71e4f9..9505bca 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ drift — then act through delegated, gated Ops. gets you to a running graph without reading anything else first. Where Argo CD shows one cluster's tree, behold shows the mixed-substrate estate: -cloud drift on AWS, supply-chain drift on GitHub Actions, artifact presence on Helm — -each substrate's own kind of truth, in one picture. +cloud drift on AWS, supply-chain drift on GitHub Actions, artifact presence and +render provenance on Helm — each substrate's own kind of truth, in one picture. ``` chant source ──build/lint──▶ graph IR ──behold──▶ live graph + drift + (delegated) actions @@ -354,6 +354,38 @@ render and the graph loads with no tier selected — the default for any project that doesn't opt in. There's no other tier config surface (not `chant.config.ts`, not an env var behold guesses the name of). +### Deploy executors — `executor` + +The same file can designate which forge deploys an environment (#165): + +```json +{ + "executor": { + "prod": { "forge": "github", "workflow": "deploy-prod.yml" } + } +} +``` + +It names the committed **workflow**, not just the forge, because two +environments' generated pipelines carry identical job ids and a picker cannot +tell them apart. For a designated environment the Deploy button dispatches that +workflow through your own `gh` login and follows the run on the dial; a local +apply, a committed ApplyOp for that environment, and auto-sync are all refused +(`409 executor-forge`), and rollback is withheld. Any approval the workflow's +GitHub environment requires is granted on GitHub, by your identity there — the +dial links the run's page and offers nothing else, because behold holds no +identity that could clear a forge gate. A designation behold cannot honour (a +typo'd forge, a missing workflow, one without `workflow_dispatch`) disables +Deploy for that environment with the reason, and never falls back to running +it on your machine. The workflow is read from the `.github/workflows` of the +repository the project belongs to, so a project that is one directory of a +monorepo works as-is. + +A dispatched run's id is kept under `~/.behold/ci-runs/` — the operator's own +state, outside the project — so a behold restarted mid-deploy re-adopts the run +and keeps following it. A follow whose stream dies is reported as `lost`, never +as a verdict; `just e2e-ci-github` proves the whole contract against GitHub. + ### The hand-layout sidecar — `.behold/layout.json` dagre places your nodes; you can move them. Drag a card, resize a containment diff --git a/docs/src/content/docs/using/acting.mdx b/docs/src/content/docs/using/acting.mdx index 3c93dc2..b6ac156 100644 --- a/docs/src/content/docs/using/acting.mdx +++ b/docs/src/content/docs/using/acting.mdx @@ -26,6 +26,25 @@ A destructive apply pauses for approval. behold surfaces the gate and the **Appr **Auto-sync never approves a gate.** That is the invariant that makes an automatic loop acceptable at all. +## Deploying through a forge + +An environment can be designated to deploy through GitHub Actions rather than from the machine behold runs on. In the project's `.behold.json`: + +```json +{ "executor": { "prod": { "forge": "github", "workflow": "deploy-prod.yml" } } } +``` + +The designation names the committed workflow file, not only the forge: two environments' generated pipelines carry identical job ids, so nothing short of the file can tell prod's from staging's. For a designated environment: + +- **Deploy** dispatches that workflow through your own `gh` login (`gh workflow run`, on the current branch) and follows the run on the dial, job by job, to GitHub's own verdict. The run's page is linked from the dial. +- A local apply, a committed ApplyOp for that environment, and auto-sync are refused with `executor-forge`, and rollback is withheld. Nothing about that environment ever runs here. +- **Approval lives on the forge.** If the workflow's GitHub environment has a protection rule, the approval is granted there, by your GitHub identity. behold holds none that could, on purpose, so the only affordance it offers is the link. +- A designation behold cannot honour — a forge it has no trigger for, a workflow that is missing or lacks `workflow_dispatch` — disables Deploy for that environment and says why. It never falls back to a guess, and never to your laptop. + +Without a designation, the palette's dispatch picks the committed workflow named for the environment (`chant-components-`, as chant 0.54 names a generated pipeline). On older projects it matches by job ids, refuses a tie rather than letting directory order choose, and never picks a workflow named for another environment. + +A dispatched run's id is persisted under `~/.behold/ci-runs/`, the operator's own state. Restart behold mid-deploy and it re-adopts the run at boot; `POST /api/ci/readopt` does the same on request. A follow whose polls keep failing, or that outlives its deadline, is reported as **lost**, never as a verdict — the run itself may still be live at its page, which the lost line names. + ## Auto-sync Off by default. With `--poll` and `--auto-sync`, a detected drift triggers a committed Op: diff --git a/docs/src/content/docs/using/agents.mdx b/docs/src/content/docs/using/agents.mdx index a11b6c6..ec5f2c6 100644 --- a/docs/src/content/docs/using/agents.mdx +++ b/docs/src/content/docs/using/agents.mdx @@ -17,13 +17,13 @@ curl -s http://localhost:4600/api | jq 1. **observe** — `GET /api/graph` returns `{ ir, svg, meta }`. Every node in `ir.nodes` carries `id`, `kind`, `lexicon`, `attrs`, and `sourceLoc`. Drift status, when present, is `attrs._status`: `good` = managed, `warn` = foreign, `accent` = pending, `neutral` = unobserved, `runtime` = runtime child. 2. **focus** — narrow with query params: `?detail=0..3`, `?components=1`, `?logical=1`, `?lens=blast:&down=1`, `?lens=lexicon:aws`, plus the `env` / `stack` / `tier` / `target` axes the UI's Scope tab exposes. 3. **go deeper** — with an `?env=`: `/api/overlay` is the live entity overlay, `/api/diff` slices per-node observed state / drift / field ownership, `/api/reconcile` summarizes the pending change set. `/api/substrates` reports substrate readiness, and `/api/events` (SSE) pushes `changed` / `op` / `apply` / `pr` so you re-pull only when something moved. -4. **inspect** — a node's `sourceLoc.file` is the typed source that declared it. To change the estate, edit there: chant is the source of truth, not behold. +4. **inspect** — a node's `sourceLoc.file` is the typed source that declared it. To change the estate, edit there: chant is the source of truth, not behold. Provenance rides on the node too: `attrs._release` (the recorded deploy: run id, origin, commit, digest, actor, approver), `attrs._carve` (the Terraform address a carved entity came from), and on a Helm chart `attrs._renderDrift.identity` (whether the running release was deployed from this project's render). A directory that is not a chant project gets a structured `{code: "no-project", error, remedy}` from `/api/graph` — the same machine-readable shape as every other precondition failure (`lint`, `not-installed`, `tier`, `eval`), so an agent can branch on `code` instead of parsing prose. ## The act loop — delegated, never direct -behold performs no cloud write of its own. The mutating endpoints trigger Ops the project committed: `POST /api/apply?env=&component=`, `POST /api/ops/:name/run`, `POST /api/ops/:name/signal/:gate`, `POST /api/rollback?to=`. All are guarded (one write at a time), gated where the Op declares a gate, and locked out entirely in preview and static modes. For richer control — build, lint, lifecycle diff, op status — prefer chant's own MCP tools over shelling. +behold performs no cloud write of its own. The mutating endpoints trigger Ops the project committed: `POST /api/apply?env=&component=`, `POST /api/ops/:name/run`, `POST /api/ops/:name/signal/:gate`, `POST /api/rollback?to=`. All are guarded (one write at a time), gated where the Op declares a gate, and locked out entirely in preview and static modes. An environment `.behold.json` designates to a forge (`executor`) answers those with `409 executor-forge`; its one deploy is `POST /api/ci/dispatch?env=`, through the operator's `gh`, followed on the dial, with `GET /api/ci/run` the persisted record and `POST /api/ci/readopt` the re-follow after a restart. For richer control — build, lint, lifecycle diff, op status — prefer chant's own MCP tools over shelling. ## The contract diff --git a/docs/src/content/docs/using/carve.mdx b/docs/src/content/docs/using/carve.mdx index 7027ea5..8f0e7a6 100644 --- a/docs/src/content/docs/using/carve.mdx +++ b/docs/src/content/docs/using/carve.mdx @@ -88,6 +88,10 @@ curl -X POST localhost:4600/api/carve/bridge -H 'content-type: application/json' A file that isn't a peelability report gets a structured `{error, code: "carve-report", remedy}` — in the terminal before the server starts, and from the routes if it goes bad later. It never renders as a blank graph. +## The carved entity, on the chant side + +Since chant 0.54 a carve manifest records its emitted files relative to its own directory, and `chant carve status --from --json` is chant's own read over a project's manifests — behold uses it (the local walk stands in below that chant) and joins each manifest's emitted file to the chant entity that file declares. On the entity graph and the overlay that entity carries `carved:
` and the `carved → chant` mark; in the carve lens a graduated card takes on the kind, lexicon and declared attributes of the entity it became, while keeping its Terraform address as its id so the morph still glides. + ## What is not here yet -Today's `carve advise --json` reports per-resource boundary **counts**, not the edge lists behind them, so no boundary edges are drawn and the graph says so on its status line. chant#1636 is the ask to publish the lists; behold already draws them (labelled `inbound`/`outbound`, with the survivors that would need a data-source patch named in the inspect pane) the moment a report carries them. +Boundary edges draw from the lists a chant ≥ 0.52 report carries (chant#1636); a report from an older chant has counts only, and the graph says so on its status line. The cross-environment question — is prod running the render staging tested — now has digests on both sides but no two-environment surface to ask it on. diff --git a/docs/src/content/docs/using/reading-the-graph.mdx b/docs/src/content/docs/using/reading-the-graph.mdx index 3222bbb..1acbbf6 100644 --- a/docs/src/content/docs/using/reading-the-graph.mdx +++ b/docs/src/content/docs/using/reading-the-graph.mdx @@ -43,3 +43,16 @@ Foreign-owned is shown as a distinct state from drifted. A field the HPA legitim ## Cross-substrate edges An estate that spans providers keeps its edges: a managed cluster declared in your cloud lexicon and the workload running on it render connected, in the live view and not only in the source graph. + + +## What was deployed, from where + +Three cards carry provenance beside their status, so "is this what we meant to deploy" has an answer on the card rather than in a CI log. + +- **A component** that the release ledger recorded shows ` via ` — the commit and the id space of the run that deployed it — and the inspect pane's **release** section carries the run id, commit, digest, actor and approver. The run is a link only when the record itself carries an address (chant ≥ 0.54); a bare id from an older chant is stated as unlinked rather than guessed at, and a laptop run says it has nowhere to go. +- **A Helm chart** whose observed release records its render identity (chant ≥ 0.54) gets a **deployed from** row in the render-diff section: the release was deployed from this project's render byte for byte, from the same inputs, or from something else — a mismatch repaints the card the drift colour, since a release running someone else's render is bad news whatever its status. A release no ledger record names reads as unrecorded, never as a mismatch. +- **A chant entity carved out of Terraform** carries `carved:
` and a `carved → chant` mark on the entity graph and the overlay: the carve manifest names the file it emitted, `chant graph` names the file that declared the entity, and the join is a byte comparison. The inspect pane's **carved from** section gives the Terraform address, stage and estate. + +## Ops that touch the estate + +On the ops lens, a step whose contract declares which of its arguments identify an estate entity (`httpCheck` and its `url`, chant ≥ 0.54) links to the card it touches: the card is drawn beside the track in an `estate` box with a dashed edge from the step, and the pane lists the value on both ends. The join is exact — a node id, or a declared attribute value equal to the step's — and a value nothing carries stays on the step as unresolved, stated rather than guessed. The status line counts how many references linked.