A row shows what a change costs, and a threshold turns a merge back to a tick
Decision record 0105
Amends 0014 (promise 3 gains the Infracost pricing API, opt in), 0053 (the preview may hand the plan’s JSON to the estimate), 0078 (one more network call the owner allowed) and 0095 (one more reason a stack set to on-merge waits). Built as slice 5.40.
Four of the tools Sluiceway is compared with sell cost estimation as a paid feature, all of them on Infracost (issue 228). It fits Sluiceway better than it fits them: the dashboard is read at the moment someone decides, and a row that says what a change costs is the one number a person wants before ticking. The owner decided on 2026-09-24: the Infracost pricing API call is allowed, opt in only, with the docs saying in one sentence what leaves the runner; only OpenTofu and Terraform stacks get a line, because Infracost reads nothing else; a failed estimate is a missing line and never a red scan; the cost is not part of the diff hash; and a stack set to on-merge whose change costs more than a threshold waits for a tick, with its row saying why.
Decision
cost.enabled, off, turns the estimate on for the repo, andstacks[].cost.enabledper entry. Nothing is sent anywhere and no CLI runs unless a repo sets it. The Infracost CLI is installed by the workflow, the way every tool is (0013), and Sluiceway runs it and never wraps it. Its key is the workflow’s business: the CLI reads it from the environment or its own file, and no Sluiceway code reads it by name (0014).INFRACOST_CURRENCYof the job environment picks the currency, as any setting of the tool’s own passes through.- The estimate is
infracost diffover the plan’s JSON the preview already made. The OpenTofu adapter’s preview holds the output oftofu show -json; with the estimate on it writes that JSON next to the plan file, in the plan’s own directory, and runsinfracost diff --path plan.json --format json --no-colorthere, with the preview’s time limit and the job’s environment plus two settings that keep the CLI to its pricing API:INFRACOST_SKIP_UPDATE_CHECK=trueandINFRACOST_ENABLE_CLOUD=false. No tool runs against the cloud again, the CLI reads nothing of the checkout, and the JSON goes with the plan’s directory (0053). Only a change is estimated: a stack in sync costs nothing more. The estimate is the plan’s prior state against its planned state, so the row shows what the change does to the bill, never the bill. - What leaves the runner is what the CLI sends its pricing API to price a plan: the resource types, regions and quantities of the change, as filters of one query per cost component, and the counts of its own run, never a value and never a credential. That is the one sentence the docs say, and the promise of 0014 gains it, opt in.
- Only the totals are read from the CLI’s output. Its JSON holds resource names, tag values and every cost component, so it never reaches the job log or anything Sluiceway writes; the currency and the monthly delta are read from it and the rest is dropped. Its stderr is the tool’s own words and goes to the stack’s group of the job log (0022).
- A failed estimate is a missing line, never a red scan. A CLI that is not there, one that ran out of time or exited with an error, output that is not its JSON, and output whose project says, as data, that the plan was not priced: each is a failed estimate with a reason of Sluiceway’s own, the row shows no cost line, the run carries the warning “Cost not estimated” with the reason, and the scan goes on. The last one matters: when the CLI cannot reach its pricing API it exits with 0 and prints every total as 0, which must never read as a change that costs nothing, so the adapter reads the project’s type and errors from the JSON and decides from them, never from the CLI’s words.
- The row’s cost line sits right under the first line of a pending row:
about **31.20 USD** more a month,less a monthfor a change that saves,about the same cost a monthfor one that changes nothing, always “about”, because an estimate is a price list against a plan. It names nothing, sodashboard.redactkeeps it and so does every level of the size budget. It is not on the marker, not in the summary, not on the preview page and not in the result file: the row is where a person decides, and each of the others is a surface of its own (docs/later.md). - The cost is not in the diff hash. The hash covers what changes and a tick approves that; the cost is derived from it, and a price that moved between the tick and the deploy stops nothing. Nothing new is compared by
apply. cost.thresholdturns a stack set to on-merge back to a tick. A change that costs more a month than the threshold, in the currency of the estimate, is not deployed by the merge; its row saysthis stack deploys on merge, and this change waits for a tick: it costs about **120.50 USD** more a month, above the threshold of 100.00 USD.A change at or under the threshold, and one that saves, go out. When the threshold is set and the estimate failed, nobody knows what the change costs, so the gate fails closed: the stack waits and its row saysits cost could not be estimated. A stack whose tool has no estimate is not gated, so a Pulumi stack set to on-merge is what it was. A threshold changes nothing for a stack on a tick: the person reads the line and ticks or not. The reason sits after a destroy and drift and before a scan no merge started, in the order of 0095: what a deploy would do, to the resources and then to the bill.stacks[].cost.thresholdsets a stack’s own.- A threshold without the estimate is refused where it is written, at the top level, in an entry, and across entries when another entry or the top level turns the estimate off for the stack. A threshold that gates nothing in silence is the kind of typo the config refuses loudly.
- The CLI is the open source 0.10 line, and no floor is checked. The 2.x line drops
diffandbreakdownfor ascanthat sends the code to Infracost’s own service behind a login, which is not the call the owner allowed. A 2.x binary answersdiffwith an exit code of 1 and a line that says it is no longer supported, which the row shows as a missing line and the job log explains. A version check that fails the job would make a missing tool a red scan, against the decision, so the docs name the line and the adapter checks nothing.
Considered
- A version floor, as every other tool has (0001). Rejected above: it would turn a missing or new CLI into a failed job where the decision says a missing line.
- The threshold gate letting a failed estimate through. Rejected: the threshold is a gate the repo asked for, and going out when the gate cannot be read would bypass it in silence. Failing closed is what every other gate of Sluiceway does.
- A
cost.currencykey. Left out: the CLI readsINFRACOST_CURRENCY, and a setting of the tool’s own is the workflow’s (0013). - The cost in the summary, the preview page, the result file and the notifications. Left out with the door open: the result file is a published shape, and each of the others is a decision of its own.
- A cost line for Pulumi, Helm and Kubernetes manifests. Not possible: Infracost reads Terraform plans and nothing else. The docs say plainly which stacks get a line.
Consequences
- The adapter interface gains
coston the preview options and on a preview that went well: what the change costs a month, or why no estimate came back. Only a scan asks;apply, the branch preview and the pull request preview never do. core/cost.tsholds the settings per stack, the failure reasons in Sluiceway’s words and the gate, pure, so a hosted version reuses them. The words of the line and the notes are the row renderer’s.- The fixtures of the estimate are recorded with the real CLI over real tofu plans of an AWS instance, against a fake pricing API the recorder starts on the machine with a made-up key and a price list of its own, so the numbers can be checked by hand and no recording asks Infracost’s service. The AWS provider plans without an account, and a state a scenario writes is the scenario’s input, as an edit of the example is.
- The build plan’s “Ask the owner before” line names the Infracost pricing API as allowed, opt in.
CONTEXT.mdgains Cost line and Cost threshold.