Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changes/pr-pipeline/cost-hold-iac-paths-9241a3c7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
bump: minor
---

### Changed

- The cost hold section names the infrastructure-as-code files the merge gate now holds:
- Pulumi project and stack files in their `.yml` and `.json` forms.
- `*.tf.json` and `*.tfvars.json`.
- The ARM template names `azuredeploy.json`, `azuredeploy.*.json`, `*.azuredeploy.json` and `*.azuredeploy.*.json`.
- Any file under a directory that holds a Pulumi project file.

It also says why a `secrets` reference moved within one file is accepted: the move adds no
charge, and where it moved is the review lanes' call.
15 changes: 15 additions & 0 deletions .changes/source-control/cost-hold-iac-paths-9241a3c7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
bump: patch
---

### Changed

- The `cost-review-required` hold now covers more infrastructure-as-code files:
- Pulumi project and stack files in their `.yml` and `.json` forms (`Pulumi.yml`, `Pulumi.json`,
`Pulumi.*.yml`, `Pulumi.*.json`).
- Terraform's JSON files (`*.tf.json`, `*.tfvars.json`).
- The ARM template names `azuredeploy.json`, `azuredeploy.*.json`, `*.azuredeploy.json` and `*.azuredeploy.*.json`.

A directory holding any Pulumi project file at the base now counts as a Pulumi project, and
`merge-holds.mjs` carries the same paths. A `secrets` reference moved within one file still
passes, because it adds no charge.
10 changes: 10 additions & 0 deletions .github/scripts/pr-merge/merge-holds.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,21 @@ export function personWrote(pull) {
export const COST_PATHS = Object.freeze([
// BEGIN hold-floor COST_PATHS: generated by scripts/sync-hold-floor.py
"**/Pulumi.yaml",
"**/Pulumi.yml",
"**/Pulumi.json",
"**/Pulumi.*.yaml",
"**/Pulumi.*.yml",
"**/Pulumi.*.json",
"**/*.tf",
"**/*.tf.json",
"**/*.tfvars",
"**/*.tfvars.json",
"**/*.bicep",
"**/*.bicepparam",
"**/azuredeploy.json",
"**/azuredeploy.*.json",
"**/*.azuredeploy.json",
"**/*.azuredeploy.*.json",
// END hold-floor COST_PATHS
]);

Expand Down
25 changes: 16 additions & 9 deletions docs/conventions/pr-pipeline/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1262,9 +1262,15 @@ the rules that only tighten in
`cost-review-required` in both lanes, whatever `high-path-floor` says and whatever an adopted
author's own paths leave off the floor, and waits for a human like any other HIGH reason. It
catches:
- any changed `Pulumi.yaml`, `Pulumi.*.yaml`, `*.tf`, `*.tfvars`, `*.bicep` or `*.bicepparam`
file, and any file under a directory that holds a `Pulumi.yaml` at the base. A base tree the gate cannot
list whole holds every file;
- any changed Pulumi project or stack file (`Pulumi.yaml`, `Pulumi.*.yaml` and their `.yml`
and `.json` forms, which
[Pulumi reads too](https://github.com/pulumi/pulumi/blob/master/sdk/go/common/encoding/marshal.go)),
`*.tf`, `*.tf.json`, `*.tfvars`, `*.tfvars.json`, `*.bicep` or `*.bicepparam` file, any ARM
template under the names
[Azure's quickstart samples use](https://github.com/Azure/azure-quickstart-templates/blob/master/1-CONTRIBUTION-GUIDE/README.md)
(`azuredeploy.json`, `azuredeploy.*.json`, `*.azuredeploy.json`, `*.azuredeploy.*.json`), and
any file under a directory that holds a Pulumi project file at the base. A base tree the gate
cannot list whole holds every file;
- a file renamed or copied into `.github/workflows/` or a `pr-pipeline.yaml` path from outside;
- a runner value the workflow's head text gains over its base text, or an added inline
`runs-on:` value: on a public repository only one that is not a free literal (GitHub's
Expand Down Expand Up @@ -1325,11 +1331,12 @@ the rules that only tighten in
credential, an agent or a write token can reach, not only spend, so a person clears them and no
cost decision does.

Accepted: a `secrets` reference moved within one file is net zero and does not hold. Other new
triggers, a removed `concurrency` or job `if:` guard, matrix growth and `max-parallel`, a
non-Claude model action, and paths outside the ones above (a workflow in a subdirectory of
`.github/workflows/`, `Pulumi.yml`, ARM templates, `*.tf.json`) are left to the review lanes'
cost hold, which holds for a real charge (`pr-review-hosted.yml`).
Accepted: a `secrets` reference moved within one file is net zero, so `secret-exposure-review`
does not see it, and it adds no charge, so the cost hold does not either; where it moved is the
review lanes' call. Other new triggers, a removed `concurrency` or job `if:` guard, matrix growth
and `max-parallel`, a non-Claude model action, and paths outside the ones above (a workflow in
a subdirectory of `.github/workflows/`, an ARM template under another name) are left to the
review lanes' cost hold, which holds for a real charge (`pr-review-hosted.yml`).

The hold clears only when the pull request body links an owner decision that covers it: a
comment on the pull request's linked issue (its closing issue, or its `Refs:` issue as
Expand Down Expand Up @@ -1852,7 +1859,7 @@ approval lifts no aliasing path, symlink, submodule or mode change (`high-path:*
trust-gate, manifest, file-cap or review-lane hold, and a review list or trusted-actor list that
does not read lifts nothing.
Both also hold a pull request that changes one of the merge gate's cost paths
(`babysit_risk.COST_PATHS`: Pulumi, Terraform and Bicep files). An
(`babysit_risk.COST_PATHS`: Pulumi, Terraform, Bicep and ARM template files). An
approval does not lift that hold. Only the owner's cost decision does, read as the gate's
`cost_decision` reads it: an unedited comment by a person on the trusted-actor list whose first
line opens `**Decision recorded.** Cost:`, on the pull request or on its one closing issue after
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5238,8 +5238,9 @@ def merged_since_floor(

def pulumi_dirs_at(repo: str, pr: Mapping[str, Any]) -> tuple[str, ...] | None:
"""`babysit_risk.RiskInput.pulumi_dirs`: each directory holding a
`Pulumi.yaml` at the base ("" for the root); None when the base tree
cannot be listed whole."""
Pulumi project file (`Pulumi.yaml`, `Pulumi.yml` or `Pulumi.json`) at
the base ("" for the root); None when the base tree cannot be listed
whole."""
owner, name = _split_repo(repo)
base = str(json_object(pr.get("base")).get("sha"))
try:
Expand All @@ -5257,7 +5258,8 @@ def pulumi_dirs_at(repo: str, pr: Mapping[str, Any]) -> tuple[str, ...] | None:
sorted(
path.rpartition("/")[0]
for path in modes
if path.rpartition("/")[2].casefold() == "pulumi.yaml"
if path.rpartition("/")[2].casefold()
in ("pulumi.yaml", "pulumi.yml", "pulumi.json")
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
README.md, "When work stops for the owner"). A change that could start one
(billed infrastructure or billed runner minutes) rates HIGH as
`cost-review-required` in every lane, floor or not and whatever `floor_exempt`
says: any `COST_PATHS` file (Pulumi, Terraform, Bicep) or file
says: any `COST_PATHS` file (Pulumi, Terraform, Bicep, ARM templates) or file
under a `RiskInput.pulumi_dirs` directory, and every file when `pulumi_dirs` is
None (the base tree was not listed); a file renamed or copied into a workflow
or `pr-pipeline.yaml` path from outside one; a workflow or pipeline file with
Expand Down Expand Up @@ -219,8 +219,9 @@ class RiskInput:
# The owner decision the caller verified for a `cost-review-required`
# change (its comment URL); None keeps the hold.
cost_decision: str | None = None
# Directories holding a Pulumi.yaml at the base ("" for the root): any
# change under one is infrastructure as code.
# Directories holding a Pulumi project file (Pulumi.yaml, .yml or .json)
# at the base ("" for the root): any change under one is infrastructure
# as code.
pulumi_dirs: tuple[str, ...] | None = ()
# Paths only a trusted author's own commits touched: the path floor and
# the manifest review leave them out (owner decision 2026-10-22).
Expand Down Expand Up @@ -971,13 +972,26 @@ def _trust_gate_reasons(files: list[_File], raw_files: list[Any]) -> list[Reason
SECRET_EXPOSURE_CODE = "secret-exposure-review"
AGENT_SURFACE_CODE = "agent-surface-review"
# Infrastructure as code: any change can add or resize a billed resource.
# Pulumi reads a project or stack file in JSON or either YAML extension;
# Terraform's JSON forms and the ARM template names Azure's quickstart
# samples use count with the native ones.
COST_PATHS: tuple[str, ...] = (
"**/Pulumi.yaml",
"**/Pulumi.yml",
"**/Pulumi.json",
"**/Pulumi.*.yaml",
"**/Pulumi.*.yml",
"**/Pulumi.*.json",
"**/*.tf",
"**/*.tf.json",
"**/*.tfvars",
"**/*.tfvars.json",
"**/*.bicep",
"**/*.bicepparam",
"**/azuredeploy.json",
"**/azuredeploy.*.json",
"**/*.azuredeploy.json",
"**/*.azuredeploy.*.json",
)
_WORKFLOWS = (".github/workflows/*.yml", ".github/workflows/*.yaml")
_PIPELINE_CONFIG = "**/pr-pipeline.yaml"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -949,10 +949,15 @@ def test_each_directory_holding_a_pulumi_yaml(self) -> None:
tree = {
"Pulumi.yaml": "100644",
"infra/x/pulumi.YAML": "100644",
"stack/Pulumi.yml": "100644",
"web/Pulumi.json": "100644",
"a.md": "100644",
"b/Pulumi.prod.yml": "100644",
}
with mock.patch.object(policy, "_tree_modes", return_value=tree):
self.assertEqual(policy.pulumi_dirs_at(REPO, self.PR), ("", "infra/x"))
self.assertEqual(
policy.pulumi_dirs_at(REPO, self.PR), ("", "infra/x", "stack", "web")
)

def test_an_unlisted_tree_is_unknown(self) -> None:
with mock.patch.object(policy, "_tree_modes", return_value=None):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,34 @@ class CostHold(_Finding):
def test_infrastructure_as_code_holds_floor_or_not(self):
for path in (
"infra/Pulumi.yaml",
"infra/Pulumi.yml",
"infra/Pulumi.json",
"infra/Pulumi.prod.yaml",
"infra/Pulumi.prod.yml",
"infra/Pulumi.prod.json",
"infra/main.tf",
"infra/main.tf.json",
"infra/prod.tfvars",
"infra/terraform.tfvars.json",
"infra/prod.auto.tfvars.json",
"infra/main.bicep",
"infra/main.bicepparam",
"deploy/azuredeploy.json",
"deploy/azuredeploy.parameters.json",
"deploy/azuredeploy.parameters.us.json",
"deploy/prereq.azuredeploy.json",
"deploy/prereqs/prereq.azuredeploy.parameters.json",
):
with self.subTest(path=path):
result = _cost([_file(path)])
self.assertEqual(result.score, Risk.HIGH)
self.assertIn(COST_CODE, _codes(result))

def test_names_near_an_infrastructure_file_do_not_hold(self):
for path in ("infra/main.json", "infra/tf.json", "deploy/azuredeploy.md"):
with self.subTest(path=path):
self.assertNotIn(COST_CODE, _codes(_cost([_file(path)])))

def test_a_cited_owner_decision_clears_it(self):
result = _cost([_file("infra/Pulumi.yaml")], cost_decision=DECISION)
self.assertEqual(result.score, Risk.LOW)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,21 @@ def python_closure(js_files: list[str]) -> list[str]:
"plugins/multi-agent/reference/defaults.yml",
"Pulumi.yaml",
"infra/Pulumi.yaml",
"infra/Pulumi.yml",
"infra/Pulumi.json",
"infra/pulumi.PROD.yaml",
"infra/Pulumi.prod.yml",
"infra/Pulumi.prod.json",
"infra/Pulumi.md",
"main.tf",
"infra/main.tf.json",
"infra/terraform.tfvars.json",
"infra/main.json",
"deploy/azuredeploy.json",
"deploy/AzureDeploy.Parameters.json",
"deploy/prereq.azuredeploy.json",
"deploy/prereqs/prereq.azuredeploy.parameters.json",
"deploy/azuredeploy.md",
".infra/net/main.TF",
"a.tf/inside.txt",
".tf",
Expand Down
Loading