diff --git a/lexicons/aws/docs/pages/policy-validation.mdx b/lexicons/aws/docs/pages/policy-validation.mdx index 1b21ddbe8..b0fa0a655 100644 --- a/lexicons/aws/docs/pages/policy-validation.mdx +++ b/lexicons/aws/docs/pages/policy-validation.mdx @@ -53,7 +53,7 @@ export default Op({ // (`chant build src --lexicon aws -o template.json`). phase("Build", [build(".")]), // Runs `cfn-guard validate -r rules.guard -d template.json`. - // A violation fails the workflow here, so nothing is applied. + // A violation fails the run here, so nothing is applied. phase("Policy", [guardValidate("rules.guard")]), phase("Apply", [awsApply("template.json", { stackName: "prod" })]), ], diff --git a/lexicons/aws/docs/src/content/docs/policy-validation.mdx b/lexicons/aws/docs/src/content/docs/policy-validation.mdx index db6ad6c16..784277276 100644 --- a/lexicons/aws/docs/src/content/docs/policy-validation.mdx +++ b/lexicons/aws/docs/src/content/docs/policy-validation.mdx @@ -57,7 +57,7 @@ export default Op({ // (`chant build src --lexicon aws -o template.json`). phase("Build", [build(".")]), // Runs `cfn-guard validate -r rules.guard -d template.json`. - // A violation fails the workflow here, so nothing is applied. + // A violation fails the run here, so nothing is applied. phase("Policy", [guardValidate("rules.guard")]), phase("Apply", [awsApply("template.json", { stackName: "prod" })]), ], diff --git a/lexicons/aws/src/agentcore/trace-fetch.ts b/lexicons/aws/src/agentcore/trace-fetch.ts index fc6b19dd5..0a72d72dd 100644 --- a/lexicons/aws/src/agentcore/trace-fetch.ts +++ b/lexicons/aws/src/agentcore/trace-fetch.ts @@ -6,10 +6,11 @@ * lexicon contributes `dogwoodReplay`: a plain exported async function taking * one args object, re-exported from `src/op/activities/index.ts`, resolved **by * name** by core's activity registry when a project lists the `aws` lexicon. - * No Temporal import beneath it, so the local executor runs it unchanged and a - * Temporal worker registers the same function. Transport is injectable through - * the same `AwsReadHttp` seam `src/api/read-client.ts` already uses, so tests - * never touch the network and `endpoint` retargets the whole thing. + * It imports no runtime of its own, so the local executor + * (`packages/core/src/op/local-executor.ts`) calls it as-is. Transport is + * injectable through the same `AwsReadHttp` seam `src/api/read-client.ts` + * already uses, so tests never touch the network and `endpoint` retargets the + * whole thing. * * The output is text. The cedar lexicon's `PolicyReplayOp` reads a trace from * `tracePath`, so `outPath` here is the handoff — and it is the *only* handoff. diff --git a/lexicons/aws/src/op/activities/floci.ts b/lexicons/aws/src/op/activities/floci.ts index 4206b1fd4..f66130fc4 100644 --- a/lexicons/aws/src/op/activities/floci.ts +++ b/lexicons/aws/src/op/activities/floci.ts @@ -101,8 +101,9 @@ export function flociRunCommand(args: FlociUpArgs = {}): string { * Idempotent: reuses a running container of the same name. Waits for the health * endpoint to report `readyService`, then sets `AWS_ENDPOINT_URL` + test creds in * the process environment so a following `nativeApply`/`cfn-deploy` targets the - * emulator. Env injection assumes the in-process local executor; under a - * distributed Temporal worker, pass the endpoint explicitly instead. + * emulator. Env injection works because the local executor runs every step of a + * run in this process; a step that runs anywhere else needs the endpoint passed + * to it explicitly. */ export async function flociUp(args: FlociUpArgs = {}, signal?: AbortSignal): Promise<{ endpoint: string }> { const region = args.region ?? DEFAULT_REGION; diff --git a/lexicons/cedar/docs/pages/dogwood-replay.mdx b/lexicons/cedar/docs/pages/dogwood-replay.mdx index 0fbc4223b..c51ebdac8 100644 --- a/lexicons/cedar/docs/pages/dogwood-replay.mdx +++ b/lexicons/cedar/docs/pages/dogwood-replay.mdx @@ -67,11 +67,22 @@ markdown, and the other two hand back a title and body for whatever opens them division `workflowSupplyChainAudit` draws. `failOnDivergence` defaults to false: an observe-dial Op reports, and a red run is the caller's decision. -The composite ships from cedar, not from temporal, because it hands back an Op -and nothing else. It imports `@intentius/chant/op` and carries no dependency on -the temporal lexicon. A project that wants it scheduled pairs it with a -`TemporalSchedule` of its own — two lines, project-side, rather than a config -flag that would drag the dependency in for everyone. +The composite ships from cedar because it hands back an Op and nothing else. It +imports `@intentius/chant/op` and depends on no other lexicon. + +A project that wants it scheduled sets `schedule` on the composite (#2120). The +string lands on the Op as `schedule: { cron, overlap: "skip" }`, which is +runtime-neutral data each reader interprets: `chant operator` ticks it locally, +the github/gitlab/forgejo lexicons render it as a CI cron, and the one-shot +executor behind `chant run policy-replay` ignores it. + +```typescript +export const { op } = PolicyReplayOp({ + name: "policy-replay", + schedule: "0 6 * * *", + // … +}); +``` ## Typed traces diff --git a/lexicons/cedar/docs/src/content/docs/dogwood-replay.mdx b/lexicons/cedar/docs/src/content/docs/dogwood-replay.mdx index 018040693..8787ad31d 100644 --- a/lexicons/cedar/docs/src/content/docs/dogwood-replay.mdx +++ b/lexicons/cedar/docs/src/content/docs/dogwood-replay.mdx @@ -70,11 +70,22 @@ markdown, and the other two hand back a title and body for whatever opens them division `workflowSupplyChainAudit` draws. `failOnDivergence` defaults to false: an observe-dial Op reports, and a red run is the caller's decision. -The composite ships from cedar, not from temporal, because it hands back an Op -and nothing else. It imports `@intentius/chant/op` and carries no dependency on -the temporal lexicon. A project that wants it scheduled pairs it with a -`TemporalSchedule` of its own — two lines, project-side, rather than a config -flag that would drag the dependency in for everyone. +The composite ships from cedar because it hands back an Op and nothing else. It +imports `@intentius/chant/op` and depends on no other lexicon. + +A project that wants it scheduled sets `schedule` on the composite (#2120). The +string lands on the Op as `schedule: { cron, overlap: "skip" }`, which is +runtime-neutral data each reader interprets: `chant operator` ticks it locally, +the github/gitlab/forgejo lexicons render it as a CI cron, and the one-shot +executor behind `chant run policy-replay` ignores it. + +```typescript +export const { op } = PolicyReplayOp({ + name: "policy-replay", + schedule: "0 6 * * *", + // … +}); +``` ## Typed traces diff --git a/lexicons/cedar/src/dogwood/replay-activity.ts b/lexicons/cedar/src/dogwood/replay-activity.ts index 3a4a2ce5f..ee2b7d3f5 100644 --- a/lexicons/cedar/src/dogwood/replay-activity.ts +++ b/lexicons/cedar/src/dogwood/replay-activity.ts @@ -5,9 +5,8 @@ * Contributed the way the fly lexicon contributes `flyApply`: a plain exported * async function taking one args object, re-exported from * `src/op/activities/index.ts`, resolved **by name** by core's activity - * registry when a project lists the `cedar` lexicon. There is no Temporal - * import here and no Temporal dependency in the package — the local executor - * runs it as-is, and a Temporal worker registers the same function. + * registry when a project lists the `cedar` lexicon. It imports no runtime of + * its own, so the local executor calls it as-is. * * What it does: takes a policy bundle (inline text or paths), an event trace * (typed events, inline text, or a path) and a set of expectations, runs diff --git a/lexicons/cedar/src/index.ts b/lexicons/cedar/src/index.ts index 8dd3c2c6d..bcb0982e4 100644 --- a/lexicons/cedar/src/index.ts +++ b/lexicons/cedar/src/index.ts @@ -115,8 +115,8 @@ export { DOGWOOD_UPSTREAM } from "./dogwood/upstream"; // The replay Op composite and its typed step builders (#1661). Flat, like // fly's `flyDeploy`: an Op factory is what a project's `ops/*.op.ts` names, -// and it carries no dependency on the temporal lexicon — see -// ./dogwood/replay-op.ts for why the composite ships from cedar. +// and it depends on no lexicon but this one — see ./dogwood/replay-op.ts for +// why the composite ships from cedar. export { DEFAULT_REPLAY_REPORT_PATH, PolicyReplayOp, diff --git a/lexicons/cedar/src/op/activities/index.ts b/lexicons/cedar/src/op/activities/index.ts index ec2e3e0e7..903e5378a 100644 --- a/lexicons/cedar/src/op/activities/index.ts +++ b/lexicons/cedar/src/op/activities/index.ts @@ -10,8 +10,8 @@ * being registered as activities nobody would ever name in a step. * * Contributed the `flyApply` way: a plain async function taking one args - * object, with no Temporal import anywhere beneath it, so the local executor - * runs it unchanged and a Temporal worker registers the same function. + * object, depending on no runtime beyond node, so the local executor + * (`packages/core/src/op/local-executor.ts`) calls it directly. */ export { dogwoodReplay, dogwoodReplayReport } from "../../dogwood/replay-activity"; diff --git a/lexicons/cedar/src/skills/chant-cedar-dogwood.md b/lexicons/cedar/src/skills/chant-cedar-dogwood.md index 57f1bc388..995841d61 100644 --- a/lexicons/cedar/src/skills/chant-cedar-dogwood.md +++ b/lexicons/cedar/src/skills/chant-cedar-dogwood.md @@ -282,8 +282,11 @@ Three phases — Artifacts (`chantBuild`, skippable with `buildScript: false`), Replay (`dogwoodReplay`, writes `dist/dogwood-replay.json`), Report (`dogwoodReplayReport`, acts on `report | issue | pull-request`). `failOnDivergence` defaults to false: an observe-dial Op reports. The composite -ships from cedar and carries no dependency on the temporal lexicon; a scheduled -form is a project-side `TemporalSchedule` pairing. +ships from cedar; it imports `@intentius/chant/op` and nothing else. Cadence is +a `schedule` on the Op itself (#2120): pass `schedule: "0 6 * * *"` and it lands +as `schedule: { cron, overlap: "skip" }`, which `chant operator` ticks locally +and the github/gitlab/forgejo lexicons render as a CI cron. The one-shot +executor behind `chant run` ignores it. Build traces with `dogwood.traceEvent()` rather than by hand. Two traps it exists to close, and both are worth naming whenever a user assembles a fixture: diff --git a/lexicons/fountain/docs/pages/composites.mdx b/lexicons/fountain/docs/pages/composites.mdx index 9b562cabc..95c78dbdb 100644 --- a/lexicons/fountain/docs/pages/composites.mdx +++ b/lexicons/fountain/docs/pages/composites.mdx @@ -68,4 +68,4 @@ Three things are refused at construction rather than at apply: - An op whose `schedule.overlap` is anything but `skip`. A fountain schedule that fires while the teammate is busy is dropped with `teammate was busy`, so any other policy would be a promise the server does not keep. - A webhook url FTN022 would reject: plaintext http, or a loopback, link-local or RFC1918 target. Refusing here rather than at synth keeps the author from reading a lint error about a resource they never typed. -`chant build` emits all four to the manifest in dependency order — Environment, Vault, Agent, Teammate, Schedule, Webhook — and `fountainApply` reconciles them. The first three go through fountain's bulk `POST /api/apply`; the other three, which bulk apply does not carry yet ([fountain#1636](https://github.com/BinaryBourbon/fountain/issues/1636)), go through their own routes afterwards, matched by name and by url, so a second apply of an unchanged manifest makes no writes at all. +`chant build` emits all six to the manifest in dependency order: Environment, Vault, Agent, Teammate, Schedule, Webhook. `fountainApply` reconciles them. The first three go through fountain's bulk `POST /api/apply`, which reports each resource as `created` or `updated` and nothing else, so it cannot tell you an Environment was already right. The other three, which bulk apply does not carry yet ([fountain#1636](https://github.com/BinaryBourbon/fountain/issues/1636)), go through their own routes afterwards, matched by name and by url, reading live state and comparing before they write, so a second apply of an unchanged manifest makes no Teammate, Schedule or Webhook writes. diff --git a/lexicons/fountain/docs/pages/steward.mdx b/lexicons/fountain/docs/pages/steward.mdx index 551e6e04c..44b6df995 100644 --- a/lexicons/fountain/docs/pages/steward.mdx +++ b/lexicons/fountain/docs/pages/steward.mdx @@ -10,7 +10,7 @@ The teammate's thread is that environment's operational history. Each turn is on ## The mesh -chant needs six things from a place to run ops. fountain already has a noun for each: +chant needs seven things from a place to run ops. fountain already has a noun for each: | chant need | fountain noun | |---|---| @@ -100,6 +100,6 @@ The composite refuses three things at construction, so the failure arrives while ## What `chant build` emits -All six kinds, in dependency order: Environment, Vault, Agent, Teammate, Schedule, Webhook. `fountainApply` sends the first three through fountain's bulk `POST /api/apply` and the other three through their own routes afterwards, matched by name and by url, so a second apply of an unchanged manifest makes no writes. +All six kinds, in dependency order: Environment, Vault, Agent, Teammate, Schedule, Webhook. `fountainApply` sends the first three through fountain's bulk `POST /api/apply` and the other three through their own routes afterwards, matched by name and by url. Bulk apply reports each resource as `created` or `updated` and nothing else, so an Environment that changed in no way still comes back `updated`. The per-route reconcilers read what is live and compare before they write, so a second apply of an unchanged manifest makes no Teammate, Schedule or Webhook writes. See [Composites](/chant/lexicons/fountain/composites/) for the constructor's full option list, [Runtime](/chant/lexicons/fountain/runtime/) for running an op on the steward, and [ACP](/chant/lexicons/fountain/acp/) for what the sandbox is actually speaking. diff --git a/lexicons/fountain/docs/src/content/docs/composites.mdx b/lexicons/fountain/docs/src/content/docs/composites.mdx index e5dee21ad..53a21e587 100644 --- a/lexicons/fountain/docs/src/content/docs/composites.mdx +++ b/lexicons/fountain/docs/src/content/docs/composites.mdx @@ -72,4 +72,4 @@ Three things are refused at construction rather than at apply: - An op whose `schedule.overlap` is anything but `skip`. A fountain schedule that fires while the teammate is busy is dropped with `teammate was busy`, so any other policy would be a promise the server does not keep. - A webhook url FTN022 would reject: plaintext http, or a loopback, link-local or RFC1918 target. Refusing here rather than at synth keeps the author from reading a lint error about a resource they never typed. -`chant build` emits all four to the manifest in dependency order — Environment, Vault, Agent, Teammate, Schedule, Webhook — and `fountainApply` reconciles them. The first three go through fountain's bulk `POST /api/apply`; the other three, which bulk apply does not carry yet ([fountain#1636](https://github.com/BinaryBourbon/fountain/issues/1636)), go through their own routes afterwards, matched by name and by url, so a second apply of an unchanged manifest makes no writes at all. +`chant build` emits all six to the manifest in dependency order: Environment, Vault, Agent, Teammate, Schedule, Webhook. `fountainApply` reconciles them. The first three go through fountain's bulk `POST /api/apply`, which reports each resource as `created` or `updated` and nothing else, so it cannot tell you an Environment was already right. The other three, which bulk apply does not carry yet ([fountain#1636](https://github.com/BinaryBourbon/fountain/issues/1636)), go through their own routes afterwards, matched by name and by url, reading live state and comparing before they write, so a second apply of an unchanged manifest makes no Teammate, Schedule or Webhook writes. diff --git a/lexicons/fountain/docs/src/content/docs/steward.mdx b/lexicons/fountain/docs/src/content/docs/steward.mdx index 5d07a45c0..36a1b3876 100644 --- a/lexicons/fountain/docs/src/content/docs/steward.mdx +++ b/lexicons/fountain/docs/src/content/docs/steward.mdx @@ -14,7 +14,7 @@ The teammate's thread is that environment's operational history. Each turn is on ## The mesh -chant needs six things from a place to run ops. fountain already has a noun for each: +chant needs seven things from a place to run ops. fountain already has a noun for each: | chant need | fountain noun | |---|---| @@ -104,6 +104,6 @@ The composite refuses three things at construction, so the failure arrives while ## What `chant build` emits -All six kinds, in dependency order: Environment, Vault, Agent, Teammate, Schedule, Webhook. `fountainApply` sends the first three through fountain's bulk `POST /api/apply` and the other three through their own routes afterwards, matched by name and by url, so a second apply of an unchanged manifest makes no writes. +All six kinds, in dependency order: Environment, Vault, Agent, Teammate, Schedule, Webhook. `fountainApply` sends the first three through fountain's bulk `POST /api/apply` and the other three through their own routes afterwards, matched by name and by url. Bulk apply reports each resource as `created` or `updated` and nothing else, so an Environment that changed in no way still comes back `updated`. The per-route reconcilers read what is live and compare before they write, so a second apply of an unchanged manifest makes no Teammate, Schedule or Webhook writes. See [Composites](/chant/lexicons/fountain/composites/) for the constructor's full option list, [Runtime](/chant/lexicons/fountain/runtime/) for running an op on the steward, and [ACP](/chant/lexicons/fountain/acp/) for what the sandbox is actually speaking. diff --git a/lexicons/fountain/src/deep-observe.ts b/lexicons/fountain/src/deep-observe.ts index b2c7b3f6f..0f0b5bddb 100644 --- a/lexicons/fountain/src/deep-observe.ts +++ b/lexicons/fountain/src/deep-observe.ts @@ -26,8 +26,8 @@ * * fountain's JSON views name their fields the same way the request schema does * (`networking_type`, `env_vars`, `skills`), so the live tree and the declared - * tree already speak one vocabulary — the AWS situation, not temporal's. The - * payload is therefore forwarded as-is and the noise rules + * tree already speak one vocabulary and nothing has to be renamed on the way in. + * The payload is therefore forwarded as-is and the noise rules * (./deep-observe-hooks.ts) do the rest. A field fountain adds in a later * release surfaces as unclaimed until the table names it (#2160: source * never set it, so it is not drift and is never proposed for update), which is diff --git a/lexicons/fountain/src/skills/chant-fountain-ops.md b/lexicons/fountain/src/skills/chant-fountain-ops.md index b1a27e759..6421f6b7f 100644 --- a/lexicons/fountain/src/skills/chant-fountain-ops.md +++ b/lexicons/fountain/src/skills/chant-fountain-ops.md @@ -75,8 +75,10 @@ chant run # or call fountainApply directly `fountainApply` sends Environment, Vault and Agent through fountain's bulk `POST /api/apply`, then Teammate, Schedule and Webhook through their own routes, -matched by name and by url. A second apply of an unchanged manifest writes -nothing. +matched by name and by url. Bulk apply reports each resource as `created` or +`updated` and nothing else, so it never says a resource was already right. The +per-route reconcilers compare live state before they write, so a second apply of +an unchanged manifest makes no Teammate, Schedule or Webhook writes. Endpoint and token come from `fountain.profiles` in `chant.config.ts`, falling back to `FOUNTAIN_ENDPOINT` / `FOUNTAIN_TOKEN`. A profile's `token` is always diff --git a/lexicons/github/docs/pages/lint-rules.mdx b/lexicons/github/docs/pages/lint-rules.mdx index 479fa46a6..b3bf317ac 100644 --- a/lexicons/github/docs/pages/lint-rules.mdx +++ b/lexicons/github/docs/pages/lint-rules.mdx @@ -188,7 +188,7 @@ Flags a workflow file with no top-level `on:` key. Without a trigger the workflo GHA029 onward are a CI/CD supply-chain security pass: pin & vet external references, enforce least-privilege token scopes, guard trust boundaries against untrusted input, contain secrets, reject unsound expressions, and keep artifacts/caches honest. They run statically on the emitted YAML — everything answerable without leaving the build. -The checks that need a *moving external truth* — whether a pinned SHA still maps to a real upstream tag, whether a ref still exists, whether a new advisory now covers an action in use — can't be deterministic, so they live in the operational layer instead. Schedule the [`WorkflowAuditOp`](/chant/guide/ops/#audit-supply-chain-drift) (temporal lexicon) for that live, always-fresh half; it reads the same emitted workflow references and reports drift via `report | issue | pull-request`. +The checks that need a *moving external truth* — whether a pinned SHA still maps to a real upstream tag, whether a ref still exists, whether a new advisory now covers an action in use — can't be deterministic, so they live in the operational layer instead. Schedule the [`WorkflowAuditOp`](/chant/guide/ops/#audit-supply-chain-drift), which ships from core's `@intentius/chant/op`, for that live, always-fresh half; it reads the same emitted workflow references and reports drift via `report | issue | pull-request`. ### GHA029 — Action or reusable workflow not pinned to a commit SHA diff --git a/lexicons/github/docs/src/content/docs/lint-rules.mdx b/lexicons/github/docs/src/content/docs/lint-rules.mdx index f5ab4ea83..2b8350f80 100644 --- a/lexicons/github/docs/src/content/docs/lint-rules.mdx +++ b/lexicons/github/docs/src/content/docs/lint-rules.mdx @@ -217,7 +217,7 @@ Flags a workflow file with no top-level `on:` key. Without a trigger the workflo GHA029 onward are a CI/CD supply-chain security pass: pin & vet external references, enforce least-privilege token scopes, guard trust boundaries against untrusted input, contain secrets, reject unsound expressions, and keep artifacts/caches honest. They run statically on the emitted YAML — everything answerable without leaving the build. -The checks that need a *moving external truth* — whether a pinned SHA still maps to a real upstream tag, whether a ref still exists, whether a new advisory now covers an action in use — can't be deterministic, so they live in the operational layer instead. Schedule the [`WorkflowAuditOp`](/chant/guide/ops/#audit-supply-chain-drift) (temporal lexicon) for that live, always-fresh half; it reads the same emitted workflow references and reports drift via `report | issue | pull-request`. +The checks that need a *moving external truth* — whether a pinned SHA still maps to a real upstream tag, whether a ref still exists, whether a new advisory now covers an action in use — can't be deterministic, so they live in the operational layer instead. Schedule the [`WorkflowAuditOp`](/chant/guide/ops/#audit-supply-chain-drift), which ships from core's `@intentius/chant/op`, for that live, always-fresh half; it reads the same emitted workflow references and reports drift via `report | issue | pull-request`. ### GHA029 — Action or reusable workflow not pinned to a commit SHA diff --git a/lexicons/github/src/components/generate-op-pipeline.ts b/lexicons/github/src/components/generate-op-pipeline.ts index aef2af8bf..f0df5822c 100644 --- a/lexicons/github/src/components/generate-op-pipeline.ts +++ b/lexicons/github/src/components/generate-op-pipeline.ts @@ -4,10 +4,11 @@ * The Op counterpart to `./generate-pipeline.ts` (#891): that module * synthesizes a `workflow_dispatch`-triggered pipeline from a deploy-time * component graph, this one synthesizes a cron-triggered workflow per - * stateless Op — the CI-native alternative to a Temporal `TemporalSchedule` - * for downstream projects that don't run Temporal (`WorkflowAuditOp`, - * `PipelineAuditOp`, `ReconcileOp`, … all accept an optional `schedule` - * precisely for this). + * stateless Op. An Op's cadence is an `OpSchedule` on the Op itself + * (`packages/core/src/op/types.ts`), runtime-neutral data each reader + * interprets; this module is the reader that turns it into a cron a GitHub + * runner fires (`WorkflowAuditOp`, `PipelineAuditOp`, `ReconcileOp`, … all + * accept an optional `schedule` precisely for this). * * GitHub Actions' `on.schedule` is workflow-scoped, not job-scoped, so unlike * the component generator (one combined pipeline for the whole graph) this diff --git a/lexicons/gitlab/docs/pages/lint-rules.mdx b/lexicons/gitlab/docs/pages/lint-rules.mdx index 457a33542..23b28b780 100644 --- a/lexicons/gitlab/docs/pages/lint-rules.mdx +++ b/lexicons/gitlab/docs/pages/lint-rules.mdx @@ -166,7 +166,7 @@ Detects `needs:` entries already implied by stage ordering. Not incorrect, but r WGL029 onward are a CI/CD supply-chain security pass, the GitLab counterpart to the github lexicon's GHA029–058: pin & vet includes/components/images, scope `CI_JOB_TOKEN` and OIDC, guard trust boundaries against untrusted CI input, mask/protect/scope secrets, reject unsound `rules:` expressions, and keep artifacts/caches honest. They run statically on the emitted `.gitlab-ci.yml`. -The checks that need a *moving external truth* — whether a pinned component/include ref still resolves, whether an upstream was archived or moved, whether a new advisory covers a component in use — live in the operational layer instead. Schedule the [`PipelineAuditOp`](/chant/guide/ops/#audit-supply-chain-drift) (temporal lexicon) for that live half; it reads the emitted `include:` / `component:` / `image:` references and reports drift via `report | issue | merge-request`. +The checks that need a *moving external truth* — whether a pinned component/include ref still resolves, whether an upstream was archived or moved, whether a new advisory covers a component in use — live in the operational layer instead. Schedule the [`PipelineAuditOp`](/chant/guide/ops/#audit-supply-chain-drift), which ships from core's `@intentius/chant/op`, for that live half; it reads the emitted `include:` / `component:` / `image:` references and reports drift via `report | issue | merge-request`. ### WGL029 — Unpinned include:project / component diff --git a/lexicons/gitlab/docs/src/content/docs/lint-rules.mdx b/lexicons/gitlab/docs/src/content/docs/lint-rules.mdx index d125a5c26..a9eb4a815 100644 --- a/lexicons/gitlab/docs/src/content/docs/lint-rules.mdx +++ b/lexicons/gitlab/docs/src/content/docs/lint-rules.mdx @@ -246,7 +246,7 @@ Detects `needs:` entries already implied by stage ordering. Not incorrect, but r WGL029 onward are a CI/CD supply-chain security pass, the GitLab counterpart to the github lexicon's GHA029–058: pin & vet includes/components/images, scope `CI_JOB_TOKEN` and OIDC, guard trust boundaries against untrusted CI input, mask/protect/scope secrets, reject unsound `rules:` expressions, and keep artifacts/caches honest. They run statically on the emitted `.gitlab-ci.yml`. -The checks that need a *moving external truth* — whether a pinned component/include ref still resolves, whether an upstream was archived or moved, whether a new advisory covers a component in use — live in the operational layer instead. Schedule the [`PipelineAuditOp`](/chant/guide/ops/#audit-supply-chain-drift) (temporal lexicon) for that live half; it reads the emitted `include:` / `component:` / `image:` references and reports drift via `report | issue | merge-request`. +The checks that need a *moving external truth* — whether a pinned component/include ref still resolves, whether an upstream was archived or moved, whether a new advisory covers a component in use — live in the operational layer instead. Schedule the [`PipelineAuditOp`](/chant/guide/ops/#audit-supply-chain-drift), which ships from core's `@intentius/chant/op`, for that live half; it reads the emitted `include:` / `component:` / `image:` references and reports drift via `report | issue | merge-request`. ### WGL029 — Unpinned include:project / component diff --git a/lexicons/gitlab/src/components/generate-op-pipeline.ts b/lexicons/gitlab/src/components/generate-op-pipeline.ts index f6ad4684f..81c727a2c 100644 --- a/lexicons/gitlab/src/components/generate-op-pipeline.ts +++ b/lexicons/gitlab/src/components/generate-op-pipeline.ts @@ -3,10 +3,12 @@ * * The Op counterpart to `./generate-pipeline.ts` (#563): that module * synthesizes a deploy-time component graph as one `.gitlab-ci.yml`; this one - * synthesizes a cron-triggered job per stateless Op — the CI-native - * alternative to a Temporal `TemporalSchedule` for downstream projects that - * don't run Temporal (`WorkflowAuditOp`/`PipelineAuditOp`/`ReconcileOp` all - * accept an optional `schedule` precisely for this). + * synthesizes a cron-triggered job per stateless Op. An Op's cadence is an + * `OpSchedule` on the Op itself (`packages/core/src/op/types.ts`), + * runtime-neutral data each reader interprets; this module is the reader that + * turns it into a GitLab pipeline schedule (`WorkflowAuditOp`/ + * `PipelineAuditOp`/`ReconcileOp` all accept an optional `schedule` precisely + * for this). * * Unlike GitHub Actions' per-workflow `on.schedule`, GitLab has no in-file * cron at all — a schedule is a project-level object (Settings → CI/CD → diff --git a/lexicons/gitlab/src/op/activities/gitlab.ts b/lexicons/gitlab/src/op/activities/gitlab.ts index 0a68976a6..45e0ad535 100644 --- a/lexicons/gitlab/src/op/activities/gitlab.ts +++ b/lexicons/gitlab/src/op/activities/gitlab.ts @@ -16,7 +16,7 @@ export interface GitlabPipelineArgs { /** * Trigger a GitLab CI pipeline and wait for it to complete successfully. * Requires `glab` CLI authenticated in the environment. - * Uses longInfra profile — 20m timeout, heartbeat every poll. + * Uses the longInfra profile: 20m timeout, three attempts backing off from 30s. */ export async function gitlabPipeline(args: GitlabPipelineArgs, signal?: AbortSignal): Promise { const ref = args.ref ?? "HEAD"; diff --git a/lexicons/gitlab/src/op/activities/index.ts b/lexicons/gitlab/src/op/activities/index.ts index 85c4875f3..6081c9587 100644 --- a/lexicons/gitlab/src/op/activities/index.ts +++ b/lexicons/gitlab/src/op/activities/index.ts @@ -1,10 +1,11 @@ /** * gitlab Op activities — resolved by the core activity registry when a project's * `chant.config.ts` lists the `gitlab` lexicon. `gitlabPipeline` triggers a - * pipeline over the GitLab CLI with heartbeat/retry semantics; relocated from the - * temporal lexicon (#809) so gitlab's imperative activity lives with its product. - * The `gitlabPipeline` step builder stays in core, re-exported from the temporal - * Op-authoring barrel like the other core builders. + * pipeline over the GitLab CLI and polls it to completion under the step's + * profile; relocated from the hosting lexicon (#809) so gitlab's imperative + * activity lives with its product. The `gitlabPipeline` step builder stays in + * core and reaches authors through `@intentius/chant/op` like the other core + * builders. */ export { gitlabPipeline } from "./gitlab"; export type { GitlabPipelineArgs } from "./gitlab"; diff --git a/lexicons/helm/src/op/activities/helm.ts b/lexicons/helm/src/op/activities/helm.ts index 9d81c06e0..24963ba32 100644 --- a/lexicons/helm/src/op/activities/helm.ts +++ b/lexicons/helm/src/op/activities/helm.ts @@ -394,7 +394,7 @@ async function recordHelmRelease( * finished deploy into a failed activity — the record is observability, not * part of the deploy itself. * - * Uses longInfra profile — 20m timeout, heartbeat every 15s. + * Uses the longInfra profile: 20m timeout, three attempts backing off from 30s. */ export async function helmInstall( args: HelmInstallArgs, diff --git a/lexicons/helm/src/op/activities/index.ts b/lexicons/helm/src/op/activities/index.ts index dc1b7d3a8..00f5f672f 100644 --- a/lexicons/helm/src/op/activities/index.ts +++ b/lexicons/helm/src/op/activities/index.ts @@ -1,10 +1,10 @@ /** * helm Op activities — resolved by the core activity registry when a project's * `chant.config.ts` lists the `helm` lexicon. `helmInstall` shells out to the - * helm CLI with heartbeat/retry semantics; relocated from the temporal lexicon - * (#809) so helm's imperative activity lives with its product, not in temporal. - * The `helmInstall` step builder stays in core (@intentius/chant/op), re-exported - * from the temporal Op-authoring barrel like the other core builders. + * helm CLI, retried under the step's profile; relocated from the hosting + * lexicon (#809) so helm's imperative activity lives with its product. The + * `helmInstall` step builder stays in core and reaches authors through + * `@intentius/chant/op` like the other core builders. */ export { helmInstall, diff --git a/lexicons/k3d/src/op/activities/index.ts b/lexicons/k3d/src/op/activities/index.ts index 1a66e3605..930c49081 100644 --- a/lexicons/k3d/src/op/activities/index.ts +++ b/lexicons/k3d/src/op/activities/index.ts @@ -7,10 +7,12 @@ * context are left alone unless explicitly requested, and `k3dUp` returns * `{ context, kubeconfigPath? }` so later steps know what to talk to. * - * The step builders (k3dUp, k3dDown) stay in core, re-exported from the - * temporal Op-authoring barrel like the other core builders. The activities are + * The step builders (k3dUp, k3dDown) stay in core and reach authors through + * `@intentius/chant/op` like the other core builders. The activities are * dependency-light — they shell out to the k3d CLI and do not import the k3d - * declarable surface — so a Temporal worker loads them cheaply. + * declarable surface — so `loadActivities` + * (`packages/core/src/op/activity-registry.ts`), which imports this module at + * run time, pulls in nothing expensive. */ export { k3dUp, diff --git a/lexicons/k3d/src/op/activities/k3d.ts b/lexicons/k3d/src/op/activities/k3d.ts index 35d5644f4..f0c538176 100644 --- a/lexicons/k3d/src/op/activities/k3d.ts +++ b/lexicons/k3d/src/op/activities/k3d.ts @@ -137,8 +137,9 @@ async function resolveConnection(args: K3dUpArgs, signal?: AbortSignal): Promise /** * Create a local k3d cluster (vanilla Kubernetes in Docker). Idempotent: if a - * cluster of the same name already exists it is left as-is. Uses longInfra - * profile — 20m timeout, heartbeat every 15s (creation may pull the k3s image). + * cluster of the same name already exists it is left as-is. Uses the longInfra + * profile: a 20m timeout, wide enough that creation can pull the k3s image, and + * three attempts backing off from 30s. * * Unlike the upstream CLI, this does NOT touch the caller's default kubeconfig * or current context unless asked (see {@link K3dUpArgs.updateDefaultKubeconfig}). diff --git a/lexicons/k3s/src/op/activities/index.ts b/lexicons/k3s/src/op/activities/index.ts index 7a996d738..4d6b76ace 100644 --- a/lexicons/k3s/src/op/activities/index.ts +++ b/lexicons/k3s/src/op/activities/index.ts @@ -11,11 +11,12 @@ * as a value — only `tokenFile`, a path — see the token-boundary note on * {@link k3sInstall} in ./k3s. * - * The step builders (k3sInstall, k3sUninstall) live in core, re-exported from - * the temporal Op-authoring barrel like k3dUp/k3dDown. The activities here are + * The step builders (k3sInstall, k3sUninstall) live in core and reach authors + * through `@intentius/chant/op` like k3dUp/k3dDown. The activities here are * dependency-light — they shell out to the k3s installer/uninstall scripts and - * only pull in the lexicon's version pin, not its declarable surface — so a - * Temporal worker loads them cheaply. + * only pull in the lexicon's version pin, not its declarable surface — so + * `loadActivities` (`packages/core/src/op/activity-registry.ts`), which imports + * this module at run time, pulls in nothing expensive. */ export { k3sInstall, diff --git a/lexicons/k3s/src/op/activities/k3s.ts b/lexicons/k3s/src/op/activities/k3s.ts index 6d7155fdd..69266612b 100644 --- a/lexicons/k3s/src/op/activities/k3s.ts +++ b/lexicons/k3s/src/op/activities/k3s.ts @@ -111,9 +111,9 @@ export function k3sUninstallCommand(args: K3sUninstallArgs): string { /** * Run the pinned k3s installer against a reachable host. Idempotent on an * already-installed matching version: if `k3s --version` already reports the - * target version, the install is skipped. Uses longInfra profile — 20m - * timeout, heartbeat every 15s (the installer downloads and starts the - * k3s binary). + * target version, the install is skipped. Uses the longInfra profile: a 20m + * timeout, wide enough for the installer to download and start the k3s binary, + * and three attempts backing off from 30s. * * Bounded exactly as `k3dUp`/`k3dDown` were (chant#1410, epic #1598): this * drives the case where the host is reachable from where the Op runs. It diff --git a/lexicons/k8s/docs/pages/api-client.mdx b/lexicons/k8s/docs/pages/api-client.mdx index 686cbc72f..c112aedbd 100644 --- a/lexicons/k8s/docs/pages/api-client.mdx +++ b/lexicons/k8s/docs/pages/api-client.mdx @@ -18,7 +18,7 @@ Everything chant does against a live cluster — `chant lifecycle diff --live`, **Typed failures.** The API server sends a `Status` object with a numeric code and a `reason` enum. Chant reads those fields instead of matching English on stderr, so `403 Forbidden` becomes "not observed, no credentials" and `404 NotFound` becomes a genuine absence, by construction rather than by regex. -**No `kubectl` in the image.** A Temporal worker running `kubectlApply` or `waitForReady` needs no `kubectl` binary. (The activity is still called `kubectlApply`: workers register activities by name, and renaming it would break every registered workflow.) +**No `kubectl` in the image.** A process running `kubectlApply` or `waitForReady` needs no `kubectl` binary. (The activity is still called `kubectlApply`: core's activity registry keys the activity map by export name and an Op step resolves its `fn` string against it, so renaming the export would break every Op that names the step.) ## Installation @@ -79,7 +79,7 @@ The allowlist matches the command *name*, not its full path: `aws`, `/usr/local/ `kubectlApply` performs a **server-side apply**, rather than the client-side three-way merge `kubectl apply` does by default. Server-side apply is where Kubernetes itself has gone, it removes the `last-applied-configuration` annotation from the story, and it is what makes field ownership something the API server tracks rather than something chant has to infer. -`ApplyOp({ target: "kubectl" })` goes through the same path. The kubectl branch of `nativeApply` used to shell `kubectl apply -f`; it now dispatches into this lexicon, where the apply is server-side and the prune runs against the typed client. The dispatcher itself stays in the Temporal lexicon, because "which mechanism applies this target" is not Kubernetes knowledge — but applying to Kubernetes is. +`ApplyOp({ target: "kubectl" })` goes through the same path. The kubectl branch of `nativeApply` used to shell `kubectl apply -f`; it now dispatches into this lexicon, where the apply is server-side and the prune runs against the typed client. `nativeApply`, the dispatcher itself, stays in core, because "which mechanism applies this target" is not Kubernetes knowledge; applying to Kubernetes is. ### chant's field manager diff --git a/lexicons/k8s/docs/pages/argo-composites.mdx b/lexicons/k8s/docs/pages/argo-composites.mdx index 6f934842f..84b4c5a44 100644 --- a/lexicons/k8s/docs/pages/argo-composites.mdx +++ b/lexicons/k8s/docs/pages/argo-composites.mdx @@ -1,6 +1,6 @@ --- title: "Argo CD Composites" -description: "Argo CD support in the k8s lexicon — the argoproj.io CRDs, ArgoAppFor / ArgoAppSetForRegions composites, cluster registration, the ARGO00x rules, and the Argo-vs-Temporal split." +description: "Argo CD support in the k8s lexicon — the argoproj.io CRDs, ArgoAppFor / ArgoAppSetForRegions composites, cluster registration, the ARGO00x rules, and how a deploy splits between Argo, an Op and CI." diataxis: reference --- @@ -12,14 +12,14 @@ Argo CD's `Application`, `ApplicationSet`, and `AppProject` are Kubernetes CRDs The k8s lexicon stays **runtime-agnostic** — it only emits manifests. Argo is **opt-in** via the composites below; nothing here is implied unless you reach for it. -## The three-layer model: Argo vs Temporal vs CI +## The three-layer model: Argo, an Op and CI Chant authors typed infra into manifests. From there, who applies them? | Layer | Owns | Reach for it when | |---|---|---| | **Argo CD** | Continuously reconciling declarative manifests | the desired state lives in git and *converges* — Deployments, CRs, Helm releases | -| **Temporal** | Procedural steps with ordering, signals, human gates, one-shot RPCs | the step is a *procedure*, not a state — DNS delegation, cert generation, `db init` | +| **A chant Op** | Procedural steps with ordering, human gates, one-shot RPCs | the step is a *procedure*, not a state — DNS delegation, cert generation, `db init` | | **CI** | One-shot, fire-and-forget apply | a simple pipeline with no reconciliation or long-running orchestration need | Rule of thumb: **if it's declarative and converges, let Argo reconcile it; if it's a procedure with ordering or gates, run it as a chant Op.** Prefer Argo CD over Argo *Workflows* — the procedural layer stays an Op. See the [CockroachDB multi-region tutorial](/chant/tutorials/cockroachdb-multi-region/) for a deployment that uses both. diff --git a/lexicons/k8s/docs/pages/crd-classes.mdx b/lexicons/k8s/docs/pages/crd-classes.mdx index 0323f42ce..a68dd7a69 100644 --- a/lexicons/k8s/docs/pages/crd-classes.mdx +++ b/lexicons/k8s/docs/pages/crd-classes.mdx @@ -421,7 +421,7 @@ export const apps = new Kustomization({ ``` The `FluxGitSource` and `FluxAppFor` composites collapse the `GitRepository` + `Kustomization` pair above into two calls with estate-tested defaults, and the FLUX001–003 [lint rules](/chant/lexicons/k8s/lint-rules/#flux) validate the source pin, `sourceRef`, and `dependsOn` edges. diff --git a/lexicons/k8s/docs/pages/flux-composites.mdx b/lexicons/k8s/docs/pages/flux-composites.mdx index 8bf1a75bf..de01d438d 100644 --- a/lexicons/k8s/docs/pages/flux-composites.mdx +++ b/lexicons/k8s/docs/pages/flux-composites.mdx @@ -91,5 +91,5 @@ It applies the Flux CRs through the same server-side apply `kubectl-apply` uses ## See also - [flux-apps tutorial](/chant/tutorials/flux-apps/) — a self-hosted on-ramp: k3s, Traefik `IngressRoute`, cert-manager, three Kustomizations with `dependsOn`. -- [Argo CD Composites](../argo-composites/) — the Argo counterpart, and the three-layer Argo-vs-Temporal split. +- [Argo CD Composites](../argo-composites/) — the Argo counterpart, and the three-layer split between Argo, an Op and CI. - The `chant-k8s-flux` [skill](../skills/) — agent guidance for these patterns. diff --git a/lexicons/k8s/docs/src/content/docs/api-client.mdx b/lexicons/k8s/docs/src/content/docs/api-client.mdx index 94a1d7852..6998212fb 100644 --- a/lexicons/k8s/docs/src/content/docs/api-client.mdx +++ b/lexicons/k8s/docs/src/content/docs/api-client.mdx @@ -20,7 +20,7 @@ Everything chant does against a live cluster — `chant lifecycle diff --live`, **Typed failures.** The API server sends a `Status` object with a numeric code and a `reason` enum. Chant reads those fields instead of matching English on stderr, so `403 Forbidden` becomes "not observed, no credentials" and `404 NotFound` becomes a genuine absence, by construction rather than by regex. -**No `kubectl` in the image.** A Temporal worker running `kubectlApply` or `waitForReady` needs no `kubectl` binary. (The activity is still called `kubectlApply`: workers register activities by name, and renaming it would break every registered workflow.) +**No `kubectl` in the image.** A process running `kubectlApply` or `waitForReady` needs no `kubectl` binary. (The activity is still called `kubectlApply`: core's activity registry keys the activity map by export name and an Op step resolves its `fn` string against it, so renaming the export would break every Op that names the step.) ## Installation @@ -81,7 +81,7 @@ The allowlist matches the command *name*, not its full path: `aws`, `/usr/local/ `kubectlApply` performs a **server-side apply**, rather than the client-side three-way merge `kubectl apply` does by default. Server-side apply is where Kubernetes itself has gone, it removes the `last-applied-configuration` annotation from the story, and it is what makes field ownership something the API server tracks rather than something chant has to infer. -`ApplyOp({ target: "kubectl" })` goes through the same path. The kubectl branch of `nativeApply` used to shell `kubectl apply -f`; it now dispatches into this lexicon, where the apply is server-side and the prune runs against the typed client. The dispatcher itself stays in the Temporal lexicon, because "which mechanism applies this target" is not Kubernetes knowledge — but applying to Kubernetes is. +`ApplyOp({ target: "kubectl" })` goes through the same path. The kubectl branch of `nativeApply` used to shell `kubectl apply -f`; it now dispatches into this lexicon, where the apply is server-side and the prune runs against the typed client. `nativeApply`, the dispatcher itself, stays in core, because "which mechanism applies this target" is not Kubernetes knowledge; applying to Kubernetes is. ### chant's field manager diff --git a/lexicons/k8s/docs/src/content/docs/argo-composites.mdx b/lexicons/k8s/docs/src/content/docs/argo-composites.mdx index 482b7940d..1ad92ae11 100644 --- a/lexicons/k8s/docs/src/content/docs/argo-composites.mdx +++ b/lexicons/k8s/docs/src/content/docs/argo-composites.mdx @@ -1,6 +1,6 @@ --- title: "Argo CD Composites" -description: "Argo CD support in the k8s lexicon — the argoproj.io CRDs, ArgoAppFor / ArgoAppSetForRegions composites, cluster registration, the ARGO00x rules, and the Argo-vs-Temporal split." +description: "Argo CD support in the k8s lexicon — the argoproj.io CRDs, ArgoAppFor / ArgoAppSetForRegions composites, cluster registration, the ARGO00x rules, and how a deploy splits between Argo, an Op and CI." diataxis: reference --- @@ -16,14 +16,14 @@ Argo CD's `Application`, `ApplicationSet`, and `AppProject` are Kubernetes CRDs The k8s lexicon stays **runtime-agnostic** — it only emits manifests. Argo is **opt-in** via the composites below; nothing here is implied unless you reach for it. -## The three-layer model: Argo vs Temporal vs CI +## The three-layer model: Argo, an Op and CI Chant authors typed infra into manifests. From there, who applies them? | Layer | Owns | Reach for it when | |---|---|---| | **Argo CD** | Continuously reconciling declarative manifests | the desired state lives in git and *converges* — Deployments, CRs, Helm releases | -| **Temporal** | Procedural steps with ordering, signals, human gates, one-shot RPCs | the step is a *procedure*, not a state — DNS delegation, cert generation, `db init` | +| **A chant Op** | Procedural steps with ordering, human gates, one-shot RPCs | the step is a *procedure*, not a state — DNS delegation, cert generation, `db init` | | **CI** | One-shot, fire-and-forget apply | a simple pipeline with no reconciliation or long-running orchestration need | Rule of thumb: **if it's declarative and converges, let Argo reconcile it; if it's a procedure with ordering or gates, run it as a chant Op.** Prefer Argo CD over Argo *Workflows* — the procedural layer stays an Op. See the [CockroachDB multi-region tutorial](/chant/tutorials/cockroachdb-multi-region/) for a deployment that uses both. diff --git a/lexicons/k8s/docs/src/content/docs/crd-classes.mdx b/lexicons/k8s/docs/src/content/docs/crd-classes.mdx index f2cdec04d..764224330 100644 --- a/lexicons/k8s/docs/src/content/docs/crd-classes.mdx +++ b/lexicons/k8s/docs/src/content/docs/crd-classes.mdx @@ -425,7 +425,7 @@ export const apps = new Kustomization({ ``` The `FluxGitSource` and `FluxAppFor` composites collapse the `GitRepository` + `Kustomization` pair above into two calls with estate-tested defaults, and the FLUX001–003 [lint rules](/chant/lexicons/k8s/lint-rules/#flux) validate the source pin, `sourceRef`, and `dependsOn` edges. @@ -473,3 +473,22 @@ Guidelines: - **One URL per CRD**, or a single multi-doc bundle URL (the parser uses `loadAll`, as with cert-manager). - The group's first segment becomes the namespace. Add a `GROUP_NAMESPACE_OVERRIDES` entry in `crd/parser.ts` if the default mapping reads poorly. - Document the produced classes and the operator install command in a comment block next to the entry — match the existing entries. + +## A kind with no generated class: `k8sManifest` + +`k8sManifest` declares an object from its manifest, for a kind the lexicon ships no class for and you do not want to generate one for: + +```typescript +import { k8sManifest } from "@intentius/chant-lexicon-k8s"; + +export const widget = k8sManifest({ + apiVersion: "acme.io/v1", + kind: "Widget", + metadata: { name: "demo", namespace: "web" }, + spec: { size: 3 }, +}); +``` + +The props **are** the manifest: the serializer emits the document as written, plus the default-label and ownership merge every discovered resource gets. `apiVersion` and `kind` are required — they are what makes the object addressable — and they resolve the entity type through the same group rule a generated class uses, so `acme.io/v1 Widget` is `K8s::Acme::Widget` either way and `lifecycle diff --live` reads it through the same operation surface. + +What you give up is the whole point of a generated class: no typed constructor, no spec validation at lint time, no LSP hover. Prefer generating the CRD. Two callers use this deliberately — a [kustomize build root](/chant/lexicons/k8s/importing-yaml/), whose output is already finished documents, and `chant carve emit` adopting a Terraform `kubernetes_manifest`, whose kind is only known once the body is read. diff --git a/lexicons/k8s/docs/src/content/docs/flux-composites.mdx b/lexicons/k8s/docs/src/content/docs/flux-composites.mdx index f61214fb3..cf0e09bfe 100644 --- a/lexicons/k8s/docs/src/content/docs/flux-composites.mdx +++ b/lexicons/k8s/docs/src/content/docs/flux-composites.mdx @@ -95,5 +95,5 @@ It applies the Flux CRs through the same server-side apply `kubectl-apply` uses ## See also - [flux-apps tutorial](/chant/tutorials/flux-apps/) — a self-hosted on-ramp: k3s, Traefik `IngressRoute`, cert-manager, three Kustomizations with `dependsOn`. -- [Argo CD Composites](../argo-composites/) — the Argo counterpart, and the three-layer Argo-vs-Temporal split. +- [Argo CD Composites](../argo-composites/) — the Argo counterpart, and the three-layer split between Argo, an Op and CI. - The `chant-k8s-flux` [skill](../skills/) — agent guidance for these patterns. diff --git a/lexicons/k8s/src/api/sweep-types.ts b/lexicons/k8s/src/api/sweep-types.ts index 57a9702aa..b5ec8cacf 100644 --- a/lexicons/k8s/src/api/sweep-types.ts +++ b/lexicons/k8s/src/api/sweep-types.ts @@ -10,8 +10,7 @@ * * It lives in its own module, with no imports of its own, because both * consumers reach it from different directions — `../export-resources.ts` - * pulls in the whole import parser, and a Temporal worker loading the apply - * activity should not. + * pulls in the whole import parser, and the apply activity should not. */ export const DEFAULT_IMPORT_TYPES: readonly string[] = [ "K8s::Apps::Deployment", diff --git a/lexicons/k8s/src/config.ts b/lexicons/k8s/src/config.ts index 4e0a53cc2..fc3fafe94 100644 --- a/lexicons/k8s/src/config.ts +++ b/lexicons/k8s/src/config.ts @@ -2,8 +2,8 @@ * K8s environment → cluster binding — chant #1100. * * Every cloud lexicon binds an environment to a scope: AWS resolves `` - * to a CloudFormation stack, Azure treats `` as the resource group, - * Temporal looks up `temporal.profiles.`. Before this, k8s bound + * to a CloudFormation stack, Azure treats `` as the resource group. + * Before this, k8s bound * nothing — `describeResources` shelled out to `kubectl get` with no * `--context`, so `chant lifecycle diff prod --live` read whichever cluster * `kubectl config current-context` happened to point at. diff --git a/lexicons/k8s/src/deep-observe-hooks.ts b/lexicons/k8s/src/deep-observe-hooks.ts index dc811e4c9..b913d2957 100644 --- a/lexicons/k8s/src/deep-observe-hooks.ts +++ b/lexicons/k8s/src/deep-observe-hooks.ts @@ -61,7 +61,8 @@ import { GENERATED_ONCE_LABEL_KEY } from "./secret-labels"; * Kubernetes-defaulted fields, per entity type, as index-erased property * paths. Subtracted only where source never declared the property * (`side === "live" && counterpart === "absent"`) — cdk-real-drift's default - * subtraction, same convention as AWS/Azure/Temporal's tables. + * subtraction, same convention as the AWS and Azure tables + * (`AWS_SERVICE_DEFAULTS`, `AZURE_SERVICE_DEFAULTS`). * * Sparse and evidence-based rather than derived from the generated schema: * the k8s OpenAPI spec this lexicon's codegen consumes @@ -70,9 +71,8 @@ import { GENERATED_ONCE_LABEL_KEY } from "./secret-labels"; * actually expressible today. Widening this table is additive and needs no * contract change. * - * `spec.strategy` is listed whole, not as `spec.strategy.type`, for the same - * reason Temporal's `TEMPORAL_SCHEDULE_DEFAULTS` lists `state` whole: pruning - * only the leaf would still recurse into the object, and a nested default the + * `spec.strategy` is listed whole rather than as `spec.strategy.type`, because + * pruning only the leaf would still recurse into the object, and a nested default the * table does not separately name (`rollingUpdate.maxSurge`/`maxUnavailable`, * both `"25%"` when `spec.strategy` is omitted entirely) would leave behind an * empty `strategy: {}` — a value distinct from no `strategy` key at all, and diff --git a/lexicons/k8s/src/deep-observe.ts b/lexicons/k8s/src/deep-observe.ts index 29a6c6fa3..e5c112705 100644 --- a/lexicons/k8s/src/deep-observe.ts +++ b/lexicons/k8s/src/deep-observe.ts @@ -9,10 +9,10 @@ * * ## What managedFields decides, and what it does not * - * AWS, Azure and Temporal's rows all prune by a **static, entityType-keyed** - * table: an ARN always looks like an ARN, `provisioningState` is always - * server-populated, a namespace's retention default is always the same - * value. None of that needs the specific live object in hand — it is exactly + * The AWS and Azure rows both prune by a **static, entityType-keyed** table + * (`AWS_SERVICE_DEFAULTS`, `AZURE_SERVICE_DEFAULTS`): an ARN always looks like + * an ARN, `provisioningState` is always server-populated. Neither needs the + * specific live object in hand — it is exactly * what `./deep-observe-hooks.ts`'s `k8sDeepNormalizationHooks` is, and it * covers Kubernetes' *equivalent* static noise (`status`, * `metadata.{uid,resourceVersion,generation,creationTimestamp}`, a handful of @@ -121,7 +121,7 @@ import { k8sDeepNormalizationHooks } from "./deep-observe-hooks"; // Re-exported so a dynamic importer of this module (plugin.ts's // `observeResourcesDeep`, a test) can get the reader and its hooks from one -// place, the same shape AWS/Azure/Temporal's single deep-observe.ts offers. +// place, the same shape the AWS and Azure deep-observe.ts modules offer. // `plugin.ts`'s own `deepNormalizationHooks` field imports the hooks // separately, directly from `./deep-observe-hooks` — that file has no // dependency on `@intentius/chant-k8s-client`, so it is safe to import diff --git a/lexicons/k8s/src/export-resources.ts b/lexicons/k8s/src/export-resources.ts index a9102a486..0f5093f0a 100644 --- a/lexicons/k8s/src/export-resources.ts +++ b/lexicons/k8s/src/export-resources.ts @@ -28,7 +28,7 @@ import { DEFAULT_IMPORT_TYPES } from "./api/sweep-types"; * reachable by naming it with `--selector type=...`. Defined in * `./api/sweep-types.ts` and re-exported here, its original home — chant * #1075's ownership-scoped prune needs the same list without pulling the - * import parser into a Temporal worker. + * import parser in behind the apply activity. */ export { DEFAULT_IMPORT_TYPES }; diff --git a/lexicons/k8s/src/op/activities/argo.test.ts b/lexicons/k8s/src/op/activities/argo.test.ts index d7e40889b..799cd7734 100644 --- a/lexicons/k8s/src/op/activities/argo.test.ts +++ b/lexicons/k8s/src/op/activities/argo.test.ts @@ -5,8 +5,8 @@ import { type ArgoAppStatus, type ArgoStatusFetcher, } from "./argo"; -// Activity profiles live centrally in the temporal lexicon (loadProfiles reads -// them there); argoSync marks ArgoSyncFailedError non-retryable for this activity. +// Activity profiles live centrally in core (loadProfiles serves this table); +// argoSync marks ArgoSyncFailedError non-retryable for this activity. import { ACTIVITY_PROFILES } from "@intentius/chant/op/activity-profiles"; /** A fetcher that returns a scripted sequence of statuses, repeating the last. */ @@ -66,7 +66,7 @@ describe("waitForArgoSync", () => { }); describe("argoSync profile", () => { - test("is exported with a long timeout and 60s heartbeat", () => { + test("is exported with a long timeout", () => { const p = ACTIVITY_PROFILES.argoSync; expect(p.timeout).toBe("30m"); }); diff --git a/lexicons/k8s/src/op/activities/argo.ts b/lexicons/k8s/src/op/activities/argo.ts index cad24e64e..ada70a34f 100644 --- a/lexicons/k8s/src/op/activities/argo.ts +++ b/lexicons/k8s/src/op/activities/argo.ts @@ -16,8 +16,8 @@ const execAsync = promisify(exec); * `ResourceFetcher` for `waitForReady`. * * It stays **dependency-light** — primitives-only signature (app name / - * namespace / server), no generated Argo CRD types — so a Temporal worker loads - * it cheaply. + * namespace / server), no generated Argo CRD types — so importing the lexicon's + * activity module at run time stays cheap. */ export interface WaitForArgoSyncArgs { @@ -36,7 +36,7 @@ export interface WaitForArgoSyncArgs { insecure?: boolean; /** kubectl context (used when `server` is not set). */ context?: string; - /** Poll interval in ms (default 15000). Heartbeats every poll. */ + /** Poll interval in ms (default 15000). */ intervalMs?: number; } @@ -113,7 +113,7 @@ export const defaultArgoStatusFetcher: ArgoStatusFetcher = (args, signal) => * `ArgoSyncFailedError` if it reaches a terminal unhealthy state (Degraded / * Missing). * - * Delegates the poll loop, heartbeat, and ready/terminal evaluation to the + * Delegates the poll loop and the ready/terminal evaluation to the * generic `waitForReady` using the shared `argoproj.io/Application` readiness * spec. The Argo `ArgoStatusFetcher` is adapted into a `ResourceFetcher` that * shapes `{health, sync}` into the `status.health.status` / `status.sync.status` diff --git a/lexicons/k8s/src/op/activities/index.ts b/lexicons/k8s/src/op/activities/index.ts index b95ab834c..a5be7e36e 100644 --- a/lexicons/k8s/src/op/activities/index.ts +++ b/lexicons/k8s/src/op/activities/index.ts @@ -1,21 +1,23 @@ /** * k8s Op activities — resolved by the core activity registry when a project's - * `chant.config.ts` lists the `k8s` lexicon. Relocated from the temporal lexicon + * `chant.config.ts` lists the `k8s` lexicon. Relocated from the hosting lexicon * (#809) so Kubernetes-facing imperative activities live with their product: * - kubectlApply — server-side apply a rendered manifest (and, since chant * #1075, prune chant-owned objects it no longer declares; `applyManifest` - * is the same work with a report of what it did, which is what the - * Temporal lexicon's `nativeApply` dispatcher calls for a kubectl target) + * is the same work with a report of what it did, which is what core's + * `nativeApply` dispatcher calls for a kubectl target) * - waitForArgoSync — block until an Argo CD Application is Healthy && Synced * * k3dUp / k3dDown moved again, to the k3d lexicon (chant #1410) — a lexicon * owns its own product's activities, and k3d is its own product now. Projects * using them list `k3d` in `lexicons`; loadActivities(["k3d"]) provides them. * - * The step builders (kubectlApply, k3dUp, k3dDown) stay in core, re-exported from - * the temporal Op-authoring barrel like the other core builders. Each activity is - * dependency-light — it shells out to a CLI and does not import the k8s declarable - * surface — so a Temporal worker loads it cheaply. + * The step builders (kubectlApply, k3dUp, k3dDown) stay in core and reach + * authors through `@intentius/chant/op` like the other core builders. Each + * activity is dependency-light — it shells out to a CLI and does not import the + * k8s declarable surface — so `loadActivities` + * (`packages/core/src/op/activity-registry.ts`), which imports this module at + * run time, pulls in nothing expensive. */ export { kubectlApply, applyManifest, readManifestDocuments } from "./kubectl"; export type { KubectlApplyArgs, ApplyManifestResult, AppliedRef, ApplyDeleteMode } from "./kubectl"; diff --git a/lexicons/k8s/src/op/activities/kubectl.test.ts b/lexicons/k8s/src/op/activities/kubectl.test.ts index 44957df8e..80c74a49a 100644 --- a/lexicons/k8s/src/op/activities/kubectl.test.ts +++ b/lexicons/k8s/src/op/activities/kubectl.test.ts @@ -1,10 +1,11 @@ /** * `kubectlApply` over the typed API client (chant #1074, #1075). * - * The activity contract is what Temporal workers register, so the shape of the - * arguments and the `Promise` return are asserted alongside the new - * behavior. Nothing here spawns a process or reads an ambient kubeconfig, - * which is the acceptance criterion: a worker image needs no `kubectl` binary. + * The activity contract is what an Op step names and core's registry resolves, + * so the shape of the arguments and the `Promise` return are asserted + * alongside the new behavior. Nothing here spawns a process or reads an ambient + * kubeconfig, which is the acceptance criterion: the image a run happens in + * needs no `kubectl` binary. * * chant #1075 adds two things to assert: that the field manager is derived * from the project's `ownership.stack` rather than hardcoded, and that the diff --git a/lexicons/k8s/src/op/activities/kubectl.ts b/lexicons/k8s/src/op/activities/kubectl.ts index f1268834c..720c469a2 100644 --- a/lexicons/k8s/src/op/activities/kubectl.ts +++ b/lexicons/k8s/src/op/activities/kubectl.ts @@ -3,9 +3,9 @@ * * chant #1074 moved this off `kubectl apply -f`. The activity contract is * unchanged (a manifest path, an optional context, `Promise`, the - * `longInfra` profile's 15s heartbeat) because Temporal workers register it by - * that signature; what changed is underneath. The name is kept for the same - * reason. + * `longInfra` profile) because an Op step names the activity by its export + * name and core's registry resolves it there; what changed is underneath. The + * name is kept for the same reason. * * chant #1075 finished the job on two axes: * @@ -58,7 +58,7 @@ export type ApplyDeleteMode = "never" | "owned-only" | "gated"; export interface KubectlApplyArgs { /** * Path to a manifest file, or a directory of them. With `documents` given, - * this becomes only the human-facing label the heartbeats and logs carry + * this becomes only the human-facing label the step's log lines carry * (e.g. `kustomize:`), and nothing is read from disk. */ manifest: string; @@ -377,7 +377,7 @@ export async function applyManifest( /** * Apply every document in `args.manifest`. - * Uses longInfra profile — 20m timeout, heartbeat every 15s. + * Uses the longInfra profile: 20m timeout, three attempts backing off from 30s. */ export async function kubectlApply( args: KubectlApplyArgs, diff --git a/lexicons/k8s/src/op/activities/wait-for-ready.ts b/lexicons/k8s/src/op/activities/wait-for-ready.ts index d860a8235..b4cc7aeb8 100644 --- a/lexicons/k8s/src/op/activities/wait-for-ready.ts +++ b/lexicons/k8s/src/op/activities/wait-for-ready.ts @@ -6,8 +6,8 @@ import { defaultK8sConnector, type K8sConnector } from "../../api/connect"; * ready, driven by a data-only **readiness spec** rather than per-CRD code. * * Like `waitForArgoSync`, this activity is intentionally **dependency-light**: - * its signature is primitives + a plain readiness spec, so a Temporal worker - * loads it without importing the generated CRD declarable surface. It reads the + * its signature is primitives + a plain readiness spec, so the activity module + * loads without pulling in the generated CRD declarable surface. It reads the * resource and evaluates the spec's predicates. It generalizes the bespoke * `waitForArgoSync` / `waitForStack` waits — see #365. * @@ -271,7 +271,7 @@ export interface WaitForReadyArgs { group?: string; /** Explicit readiness spec — wins over the registry/default. */ spec?: ReadinessSpec; - /** Poll interval in ms (default 15000). Heartbeats every poll. */ + /** Poll interval in ms (default 15000). */ intervalMs?: number; } @@ -335,8 +335,9 @@ export const defaultResourceFetcher: ResourceFetcher = (args, signal) => apiReso /** * Poll until the resource satisfies its readiness spec. Throws - * `ReadinessFailedError` on a terminal state. Heartbeats every poll so the - * `k8sWait` profile's 60s heartbeat timeout never trips. + * `ReadinessFailedError` on a terminal state, which the `k8sWait` profile lists + * as non-retryable, so a resource that will never become ready fails on the + * first attempt instead of burning the profile's 15m timeout three times. * * @param fetcher injectable reader (defaults to kubectl). Tests pass a fake to * drive not-ready → ready / terminal transitions. diff --git a/lexicons/k8s/src/plugin.ts b/lexicons/k8s/src/plugin.ts index 6d7aea2a6..6edffa9e9 100644 --- a/lexicons/k8s/src/plugin.ts +++ b/lexicons/k8s/src/plugin.ts @@ -632,7 +632,7 @@ const { deployment, service, serviceMonitor, prometheusRule } = MonitoredService { file: "chant-k8s-argo.md", name: "chant-k8s-argo", - description: "Argo CD composites — ArgoAppFor, ArgoAppSetForRegions, AppProject scoping, cluster registration, and the Argo-vs-Temporal split", + description: "Argo CD composites — ArgoAppFor, ArgoAppSetForRegions, AppProject scoping, cluster registration, and how a deploy splits between Argo and a chant Op", triggers: [ { type: "context", value: "argo" }, { type: "context", value: "argo cd" }, diff --git a/lexicons/k8s/src/skills/chant-k8s-argo.md b/lexicons/k8s/src/skills/chant-k8s-argo.md index e368cfc3a..049843d66 100644 --- a/lexicons/k8s/src/skills/chant-k8s-argo.md +++ b/lexicons/k8s/src/skills/chant-k8s-argo.md @@ -1,6 +1,6 @@ --- skill: chant-k8s-argo -description: Argo CD composites for GitOps reconciliation — ArgoAppFor, ArgoAppSetForRegions, AppProject scoping, cluster registration, and the Argo-vs-Temporal split +description: Argo CD composites for GitOps reconciliation — ArgoAppFor, ArgoAppSetForRegions, AppProject scoping, cluster registration, and how a deploy splits between Argo and a chant Op user-invocable: true --- @@ -14,9 +14,9 @@ Chant authors typed infrastructure into manifests. Argo CD continuously reconcil |---|---|---| | **Chant** | Authoring typed infra → manifests | the lexicons | | **Argo CD** | Continuously reconciling declarative manifests (the apply layer) | `ArgoAppFor` / `ArgoAppSetForRegions` | -| **Temporal** | Procedural steps Argo can't express — ordering, signals, human gates, one-shot RPCs | the temporal lexicon + `waitForArgoSync` | +| **A chant Op** | Procedural steps Argo can't express: ordering, human gates, one-shot RPCs | an `Op` in the project, plus this lexicon's `waitForArgoSync` | -Rule of thumb: **if it's declarative and converges, let Argo reconcile it. If it's a procedure with ordering, gates, or out-of-band steps, orchestrate it in Temporal.** Prefer Argo CD over Argo Workflows — the procedural layer stays Temporal. +Rule of thumb: **if it's declarative and converges, let Argo reconcile it. If it's a procedure with ordering, gates, or out-of-band steps, write it as a chant Op and run it from CI or a steward.** Prefer Argo CD over Argo Workflows; the procedural layer stays an Op. ## Prerequisites @@ -138,9 +138,9 @@ Produces a `Secret` labelled `argocd.argoproj.io/secret-type: cluster`. After th --- -## The Argo-vs-Temporal split +## Splitting a deploy between Argo and an Op -When a deploy has both declarative and procedural parts, let each layer own what it's good at. Example — the multi-region CockroachDB deploy: +When a deploy has both declarative and procedural parts, let each layer own what it's good at. Example, the multi-region CockroachDB deploy: | Step | Owner | Why | |---|---|---| @@ -148,16 +148,30 @@ When a deploy has both declarative and procedural parts, let each layer own what | Install ESO / operators (Helm) | **Argo** | Declarative Helm source | | Apply per-cluster K8s manifests | **Argo** (`ApplicationSet`) | One App per workload cluster | | Wait for workloads Healthy | **Argo** (`Health=Healthy`) | Subsumed by Application health | -| Wait for DNS delegation | **Temporal** | Signal/update/auto-poll race — out of band | -| Generate + push TLS certs | **Temporal** | One-shot procedure, secrets not in git | -| `cockroach init`, configure regions | **Temporal** | Ordered one-shot RPCs | - -From a Temporal workflow, gate procedural steps on Argo finishing a declarative apply with the `waitForArgoSync` activity (temporal lexicon, `argoSync` profile): +| Wait for DNS delegation | **an Op** | Out of band, and a human confirms it | +| Generate + push TLS certs | **an Op** | One-shot procedure, secrets not in git | +| `cockroach init`, configure regions | **an Op** | Ordered one-shot RPCs | + +Argo owns the sync. The Op owns the ordering and the gates: its phases run in +sequence in one process (`packages/core/src/op/local-executor.ts`), and a `gate` +step reads the gate ledger, so a run that reaches a gate nobody has approved +records the pending fact, ends with status `gated` and exits 3. Someone runs +`chant approve `, the next run reads the resolution and walks +through. CI is what runs the Op, on whatever cadence the Op's `schedule` +names. + +To make a step wait on Argo, use this lexicon's `waitForArgoSync` activity. It is +exported from `lexicons/k8s/src/op/activities/index.ts`, and the core activity +registry resolves it by export name once `k8s` is in the project's `lexicons`. +Give the step core's `argoSync` profile +(`packages/core/src/op/activity-profiles.ts`): a 30m timeout, five attempts +backing off from 10s, and `ArgoSyncFailedError` marked non-retryable so a +terminally unhealthy Application fails fast instead of polling to the cap. ```typescript -// In a Temporal Op workflow: -await waitForArgoSync({ appName: "east-crdb", namespace: "argocd" }); -// ...now run the procedural steps that depend on the workloads being Healthy. +// In an Op phase: +activity("waitForArgoSync", { appName: "east-crdb", namespace: "argocd" }, "argoSync"), +// Later steps in the phase run once the workloads are Healthy. ``` `waitForArgoSync` is dependency-free — it polls the Application's status (`health=Healthy && sync=Synced`) and never imports the Argo CRD types. diff --git a/lexicons/terraform/docs/pages/ops.mdx b/lexicons/terraform/docs/pages/ops.mdx index 4e410da1e..42bacbc03 100644 --- a/lexicons/terraform/docs/pages/ops.mdx +++ b/lexicons/terraform/docs/pages/ops.mdx @@ -46,7 +46,7 @@ export const { op } = TerraformApplyOp({ | Option | Default | Notes | |---|---|---| -| `name` | — required | Op name (kebab-case). Also the default task queue and gate signal suffix. | +| `name` | — required | Op name (kebab-case). `signalName` defaults to `approve-`. | | `root` | — required | Key into `terraform.roots`. | | `planFile` | `chant.tfplan` | Written by Plan, consumed by Apply — relative to the root dir. | | `gate` | `"on-destroy"` | `"on-destroy"` and `"always"` build the identical four-phase shape (`GateStep` carries no condition to branch a plan's destroy count at build time — see below); `"never"` drops the Gate phase entirely. | @@ -56,7 +56,6 @@ export const { op } = TerraformApplyOp({ | `upgrade` | `false` | `-upgrade` on the Init step. | | `cwd` | the process's cwd | Directory each step starts its `chant.config.*` search from. | | `compensate` | unset | Saga-style rollback on a failed apply — see below. | -| `taskQueue` | `name` | Override the task queue. | ### The gate @@ -173,7 +172,7 @@ export const { op } = TerraformAdoptOp({ name: "estate-adopt", root: "estate" }) | Option | Default | Notes | |---|---|---| -| `name` | — required | Op name (kebab-case). Also the default task queue and gate signal suffix. | +| `name` | — required | Op name (kebab-case). `signalName` defaults to `approve-`. | | `root` | — required | Key into `terraform.roots`. Must be a live root. | | `estate` | auto-detected | The estate whose markers to look for; normally the root's own `live` block or sidecar answers this. | | `signalName` | `approve-` | Gate signal name. | @@ -181,7 +180,6 @@ export const { op } = TerraformAdoptOp({ name: "estate-adopt", root: "estate" }) | `gateDescription` | a generated description naming the ledger | Override text shown to the approver. | | `cwd` | the process's cwd | Directory each step starts its `chant.config.*` search from. | | `compensate` | unset | Refused without a command. See below. | -| `taskQueue` | `name` | Override the task queue. | ### The adopt mechanism diff --git a/lexicons/terraform/docs/src/content/docs/ops.mdx b/lexicons/terraform/docs/src/content/docs/ops.mdx index 6bec44eea..ae60f2830 100644 --- a/lexicons/terraform/docs/src/content/docs/ops.mdx +++ b/lexicons/terraform/docs/src/content/docs/ops.mdx @@ -50,7 +50,7 @@ export const { op } = TerraformApplyOp({ | Option | Default | Notes | |---|---|---| -| `name` | — required | Op name (kebab-case). Also the default task queue and gate signal suffix. | +| `name` | — required | Op name (kebab-case). `signalName` defaults to `approve-`. | | `root` | — required | Key into `terraform.roots`. | | `planFile` | `chant.tfplan` | Written by Plan, consumed by Apply — relative to the root dir. | | `gate` | `"on-destroy"` | `"on-destroy"` and `"always"` build the identical four-phase shape (`GateStep` carries no condition to branch a plan's destroy count at build time — see below); `"never"` drops the Gate phase entirely. | @@ -60,7 +60,6 @@ export const { op } = TerraformApplyOp({ | `upgrade` | `false` | `-upgrade` on the Init step. | | `cwd` | the process's cwd | Directory each step starts its `chant.config.*` search from. | | `compensate` | unset | Saga-style rollback on a failed apply — see below. | -| `taskQueue` | `name` | Override the task queue. | ### The gate @@ -177,7 +176,7 @@ export const { op } = TerraformAdoptOp({ name: "estate-adopt", root: "estate" }) | Option | Default | Notes | |---|---|---| -| `name` | — required | Op name (kebab-case). Also the default task queue and gate signal suffix. | +| `name` | — required | Op name (kebab-case). `signalName` defaults to `approve-`. | | `root` | — required | Key into `terraform.roots`. Must be a live root. | | `estate` | auto-detected | The estate whose markers to look for; normally the root's own `live` block or sidecar answers this. | | `signalName` | `approve-` | Gate signal name. | @@ -185,7 +184,6 @@ export const { op } = TerraformAdoptOp({ name: "estate-adopt", root: "estate" }) | `gateDescription` | a generated description naming the ledger | Override text shown to the approver. | | `cwd` | the process's cwd | Directory each step starts its `chant.config.*` search from. | | `compensate` | unset | Refused without a command. See below. | -| `taskQueue` | `name` | Override the task queue. | ### The adopt mechanism diff --git a/lexicons/terraform/src/composites/terraform-adopt-op.ts b/lexicons/terraform/src/composites/terraform-adopt-op.ts index 84fa8f939..08e9e4a15 100644 --- a/lexicons/terraform/src/composites/terraform-adopt-op.ts +++ b/lexicons/terraform/src/composites/terraform-adopt-op.ts @@ -89,7 +89,7 @@ import { } from "../op/builders"; export interface TerraformAdoptOpConfig { - /** Op name (kebab-case). Also the default task queue and gate signal suffix. */ + /** Op name (kebab-case). `signalName` defaults to `approve-`. */ name: string; /** Key into the project's `terraform.roots`. Must be a live root: choudoufu, with a declared estate. */ root: string; diff --git a/lexicons/terraform/src/composites/terraform-apply-op.ts b/lexicons/terraform/src/composites/terraform-apply-op.ts index fbd7e1aea..e18ba30f9 100644 --- a/lexicons/terraform/src/composites/terraform-apply-op.ts +++ b/lexicons/terraform/src/composites/terraform-apply-op.ts @@ -96,7 +96,7 @@ import { export type TerraformGateMode = "on-destroy" | "always" | "never"; export interface TerraformApplyOpConfig { - /** Op name (kebab-case). Also the default task queue and gate signal suffix. */ + /** Op name (kebab-case). `signalName` defaults to `approve-`. */ name: string; /** Key into the project's `terraform.roots`. The root carries dir, workspace, var files and backend config. */ root: string; diff --git a/lexicons/terraform/src/lsp/completions.test.ts b/lexicons/terraform/src/lsp/completions.test.ts index 73453938f..5677d518e 100644 --- a/lexicons/terraform/src/lsp/completions.test.ts +++ b/lexicons/terraform/src/lsp/completions.test.ts @@ -91,7 +91,8 @@ describe("LSP completions", () => { const labels = items.map((i) => i.label); expect(labels).toContain("gate"); expect(labels).toContain("compensate"); - expect(labels).toContain("taskQueue"); + expect(labels).toContain("gateTimeout"); + expect(labels).not.toContain("taskQueue"); }); it("completes a builder's own opts keys inside its second argument", () => { diff --git a/lexicons/terraform/src/lsp/option-keys.ts b/lexicons/terraform/src/lsp/option-keys.ts index d6c868ede..d74a7498a 100644 --- a/lexicons/terraform/src/lsp/option-keys.ts +++ b/lexicons/terraform/src/lsp/option-keys.ts @@ -30,7 +30,7 @@ export const ROOT_ENTRY_KEYS: OptionKey[] = [ /** `TerraformApplyOpConfig`'s keys (`TerraformApplyOp({ })`). */ export const APPLY_OP_KEYS: OptionKey[] = [ - { key: "name", detail: "Op name (kebab-case). Also the default task queue and gate signal suffix." }, + { key: "name", detail: "Op name (kebab-case). `signalName` defaults to `approve-`." }, { key: "root", detail: "Key into the project's `terraform.roots`." }, { key: "planFile", detail: "Plan file written by Plan and consumed by Apply. Default: `chant.tfplan`." }, { key: "gate", detail: '"on-destroy" | "always" | "never" — when to emit the approval gate. Default: "on-destroy".' }, @@ -40,7 +40,6 @@ export const APPLY_OP_KEYS: OptionKey[] = [ { key: "upgrade", detail: "`-upgrade` on the Init step: re-resolve provider and module versions." }, { key: "cwd", detail: "Directory each step starts the `chant.config.*` search from." }, { key: "compensate", detail: "Saga-style rollback on a failed apply: `true` with no command throws; supply `{ command }`." }, - { key: "taskQueue", detail: "Override the task queue. Defaults to `name`." }, ]; /** `compensate`'s own shape when it is an object: `compensate: { }`. */