From d06eaa7135155e9016f8f2bf407103456ec3a4a6 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Mon, 5 Oct 2026 11:17:23 +0200 Subject: [PATCH 1/2] feat(rulesets): require World-owned regression gate --- ...ire-world-at-ruin-trusted-regressions.yaml | 22 +++++++++++-------- tests/world-at-ruin-regression-ruleset.sh | 5 ++++- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/deploy/organization-rulesets/require-world-at-ruin-trusted-regressions.yaml b/deploy/organization-rulesets/require-world-at-ruin-trusted-regressions.yaml index 86a6c640..797a0b84 100644 --- a/deploy/organization-rulesets/require-world-at-ruin-trusted-regressions.yaml +++ b/deploy/organization-rulesets/require-world-at-ruin-trusted-regressions.yaml @@ -1,13 +1,14 @@ -# Require the centrally hosted trusted-regression workflow on World at Ruin's -# default branch. The organization scope is required for GitHub ruleset -# workflows; repositoryId narrows this rule to the one product repository. +# Require both the established catalogue workflow and World at Ruin's +# product-owned replacement on the product's default branch. The coexistence +# window proves the replacement before the established source is retired. +# The organization scope is required for GitHub ruleset workflows; +# repositoryId narrows this rule to the one product repository. # -# The workflow lives in devantler-tech/.github, outside the candidate product -# checkout. provider-upjet-github v0.20.0 exposes a branch/tag ref but not -# GitHub's workflow SHA selector, so refs/heads/main is the strongest -# declarative source binding the deployed provider can express. Changes to the -# trusted controller therefore go through the canonical catalogue's reviewed -# main branch and its own merge gates. +# The established workflow lives in devantler-tech/.github. The replacement +# lives in World at Ruin, but GitHub still selects its reviewed main definition +# rather than candidate bytes. provider-upjet-github v0.20.0 exposes a +# branch/tag ref but not GitHub's workflow SHA selector, so refs/heads/main is +# the strongest declarative source binding the deployed provider can express. # # This is net-new and managed Observe + Create + Update + LateInitialize, never # Delete. No bypassActors are declared. @@ -36,6 +37,9 @@ spec: - repositoryId: 933213756 path: .github/workflows/world-at-ruin-required-regressions.yaml ref: refs/heads/main + - repositoryId: 1303188705 + path: .github/workflows/trusted-regressions.yaml + ref: refs/heads/main providerConfigRef: kind: ProviderConfig name: default diff --git a/tests/world-at-ruin-regression-ruleset.sh b/tests/world-at-ruin-regression-ruleset.sh index 20d5d0a4..21e8209c 100755 --- a/tests/world-at-ruin-regression-ruleset.sh +++ b/tests/world-at-ruin-regression-ruleset.sh @@ -52,10 +52,13 @@ assert_json "target exclusions" '[]' '.spec.forProvider.conditions[0].refName[0] assert_value "bypass actor count" "0" '(.spec.forProvider.bypassActors // []) | length' assert_value "rule count" "1" '.spec.forProvider.rules | length' assert_value "required workflow block count" "1" '.spec.forProvider.rules[0].requiredWorkflows | length' -assert_value "required workflow count" "1" '.spec.forProvider.rules[0].requiredWorkflows[0].requiredWorkflow | length' +assert_value "required workflow count" "2" '.spec.forProvider.rules[0].requiredWorkflows[0].requiredWorkflow | length' assert_value "source repository" "933213756" '.spec.forProvider.rules[0].requiredWorkflows[0].requiredWorkflow[0].repositoryId' assert_value "source path" ".github/workflows/world-at-ruin-required-regressions.yaml" '.spec.forProvider.rules[0].requiredWorkflows[0].requiredWorkflow[0].path' assert_value "source ref" "refs/heads/main" '.spec.forProvider.rules[0].requiredWorkflows[0].requiredWorkflow[0].ref' +assert_value "product source repository" "1303188705" '.spec.forProvider.rules[0].requiredWorkflows[0].requiredWorkflow[1].repositoryId' +assert_value "product source path" ".github/workflows/trusted-regressions.yaml" '.spec.forProvider.rules[0].requiredWorkflows[0].requiredWorkflow[1].path' +assert_value "product source ref" "refs/heads/main" '.spec.forProvider.rules[0].requiredWorkflows[0].requiredWorkflow[1].ref' inventory="${repo_root}/deploy/organization-rulesets/README.md" grep -Fq 'The 10 imported org rulesets' "${inventory}" || From 340fcb77e61f888302cce5b24f6237ff657bb5b7 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Mon, 5 Oct 2026 11:17:23 +0200 Subject: [PATCH 2/2] feat(rulesets): require World-owned regression gate --- deploy/organization-rulesets/README.md | 10 +++-- .../organization-rulesets/kustomization.yaml | 1 + ...ire-world-at-ruin-product-regressions.yaml | 39 +++++++++++++++++++ tests/world-at-ruin-regression-ruleset.sh | 27 +++++++++++-- 4 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 deploy/organization-rulesets/require-world-at-ruin-product-regressions.yaml diff --git a/deploy/organization-rulesets/README.md b/deploy/organization-rulesets/README.md index f4dfbd79..997a6d60 100644 --- a/deploy/organization-rulesets/README.md +++ b/deploy/organization-rulesets/README.md @@ -41,6 +41,7 @@ verb — e.g. `require-pull-request.yaml`). Repo-scoped rulesets live next door | `require-signed-commits.yaml` | **Require signed commits** (existing, retired) | Observe + Update — retain the disabled record; never create or delete | | `protect-release-tags.yaml` | **Protect release tags** (net-new) | Managed (Create) — block tag delete + force-move + require `v` | | `require-world-at-ruin-trusted-regressions.yaml` | **Require workflow - World at Ruin trusted regressions** (net-new) | Managed (Create) — target only World at Ruin and require the canonical catalogue's trusted regression workflow | +| `require-world-at-ruin-product-regressions.yaml` | **Require workflow - World at Ruin product regressions** (net-new) | Managed (Create) — target only World at Ruin and require its product-owned trusted regression workflow from reviewed `main` | | `require-monorepo-ci-aggregate-contract.yaml` | **Require workflow - Monorepo CI aggregate contract** (net-new) | Managed (Create) — target only monorepo and require the aggregate-execution control from its reviewed `main` | | `require-dotgithub-deploy-guards.yaml` | **Require workflow - .github deploy guards** (net-new) | Managed (Create) — target only this repository and run the `deploy/` release-contract and deletion validators from its reviewed `main` | | (in `../repository-rulesets/`) `require-merge-queue-on-platform.yaml` | `platform` "Require merge queue" | Observe + Update (managed import, full ruleset backfilled) | @@ -112,10 +113,11 @@ gates; a team audit cannot clear the latter two. that file's header for the team-vs-enterprise tier caveat on the name-pattern rule and its fallback. - **Required-workflow source pins** — v0.20.0 exposes the source repository, path and a - branch/tag `ref`, but not GitHub's immutable workflow `sha` selector. The World at Ruin - rule therefore binds the external trusted source to `devantler-tech/.github` on - `refs/heads/main`; Actions review and merge gates own source changes until the provider - exposes `sha`. + branch/tag `ref`, but not GitHub's immutable workflow `sha` selector. The two World at + Ruin rules bind the established external source in `devantler-tech/.github` and the + product-owned replacement in `devantler-tech/world-at-ruin` independently to + `refs/heads/main`. Their separate rulesets preserve replacement enforcement while the + established rule is later disabled and retired. - **Actions policies** — the 2026-06-18 [workflow execution protections](https://github.blog/changelog/2026-06-18-control-who-and-what-triggers-github-actions-workflows/) (actor + event allow-lists controlling who/what triggers workflows, delivered as org diff --git a/deploy/organization-rulesets/kustomization.yaml b/deploy/organization-rulesets/kustomization.yaml index 954a5b62..2a837d38 100644 --- a/deploy/organization-rulesets/kustomization.yaml +++ b/deploy/organization-rulesets/kustomization.yaml @@ -21,5 +21,6 @@ resources: # Net-new, managed (Create). - protect-release-tags.yaml - require-world-at-ruin-trusted-regressions.yaml + - require-world-at-ruin-product-regressions.yaml - require-monorepo-ci-aggregate-contract.yaml - require-dotgithub-deploy-guards.yaml diff --git a/deploy/organization-rulesets/require-world-at-ruin-product-regressions.yaml b/deploy/organization-rulesets/require-world-at-ruin-product-regressions.yaml new file mode 100644 index 00000000..2a73fac1 --- /dev/null +++ b/deploy/organization-rulesets/require-world-at-ruin-product-regressions.yaml @@ -0,0 +1,39 @@ +# Require World at Ruin's product-owned regression workflow on the product's +# default branch. This rule is separate from the established external rule so +# the replacement remains active while that rule is disabled and retired. +# +# GitHub selects the workflow from World at Ruin's reviewed main branch rather +# than from candidate bytes. provider-upjet-github v0.20.0 exposes a branch/tag +# ref but not GitHub's workflow SHA selector, so refs/heads/main is the strongest +# declarative source binding the deployed provider can express. +# +# This is net-new and managed Observe + Create + Update + LateInitialize, never +# Delete. No bypassActors are declared. +apiVersion: enterprise.github.m.upbound.io/v1alpha1 +kind: OrganizationRuleset +metadata: + name: require-world-at-ruin-product-regressions +spec: + managementPolicies: + - Observe + - Create + - Update + - LateInitialize + forProvider: + name: Require workflow - World at Ruin product regressions + target: branch + enforcement: active + conditions: + - refName: + - include: ["~DEFAULT_BRANCH"] + exclude: [] + repositoryId: [1303188705] + rules: + - requiredWorkflows: + - requiredWorkflow: + - repositoryId: 1303188705 + path: .github/workflows/trusted-regressions.yaml + ref: refs/heads/main + providerConfigRef: + kind: ProviderConfig + name: default diff --git a/tests/world-at-ruin-regression-ruleset.sh b/tests/world-at-ruin-regression-ruleset.sh index 20d5d0a4..fe02d0df 100755 --- a/tests/world-at-ruin-regression-ruleset.sh +++ b/tests/world-at-ruin-regression-ruleset.sh @@ -57,17 +57,36 @@ assert_value "source repository" "933213756" '.spec.forProvider.rules[0].require assert_value "source path" ".github/workflows/world-at-ruin-required-regressions.yaml" '.spec.forProvider.rules[0].requiredWorkflows[0].requiredWorkflow[0].path' assert_value "source ref" "refs/heads/main" '.spec.forProvider.rules[0].requiredWorkflows[0].requiredWorkflow[0].ref' +selector='select(.kind == "OrganizationRuleset" and .metadata.name == "require-world-at-ruin-product-regressions")' +count="$(yq -N "${selector} | .metadata.name" "${render}" | grep -c . || true)" +[[ "${count}" == "1" ]] || fail "expected exactly one rendered product-regression ruleset, got ${count}" + +assert_value "product ruleset name" "Require workflow - World at Ruin product regressions" '.spec.forProvider.name' +assert_value "product ruleset target" "branch" '.spec.forProvider.target' +assert_value "product ruleset enforcement" "active" '.spec.forProvider.enforcement' +assert_json "product management policy" '["Observe","Create","Update","LateInitialize"]' '.spec.managementPolicies' +assert_json "product target repository" '[1303188705]' '.spec.forProvider.conditions[0].repositoryId' +assert_json "product target branch" '["~DEFAULT_BRANCH"]' '.spec.forProvider.conditions[0].refName[0].include' +assert_json "product target exclusions" '[]' '.spec.forProvider.conditions[0].refName[0].exclude' +assert_value "product bypass actor count" "0" '(.spec.forProvider.bypassActors // []) | length' +assert_value "product rule count" "1" '.spec.forProvider.rules | length' +assert_value "product required workflow block count" "1" '.spec.forProvider.rules[0].requiredWorkflows | length' +assert_value "product required workflow count" "1" '.spec.forProvider.rules[0].requiredWorkflows[0].requiredWorkflow | length' +assert_value "product source repository" "1303188705" '.spec.forProvider.rules[0].requiredWorkflows[0].requiredWorkflow[0].repositoryId' +assert_value "product source path" ".github/workflows/trusted-regressions.yaml" '.spec.forProvider.rules[0].requiredWorkflows[0].requiredWorkflow[0].path' +assert_value "product source ref" "refs/heads/main" '.spec.forProvider.rules[0].requiredWorkflows[0].requiredWorkflow[0].ref' + inventory="${repo_root}/deploy/organization-rulesets/README.md" grep -Fq 'The 10 imported org rulesets' "${inventory}" || fail "organization ruleset inventory must account for 10 imported rulesets" # The backticks are literal Markdown table cell delimiters, not command substitution. # shellcheck disable=SC2016 managed_rows="$(grep -c '^| `[a-z-]*\.yaml` | .*(net-new) | Managed (Create)' "${inventory}" || true)" -[[ "${managed_rows}" == "4" ]] || - fail "organization ruleset inventory must list 4 managed rulesets, got ${managed_rows}" +[[ "${managed_rows}" == "5" ]] || + fail "organization ruleset inventory must list 5 managed rulesets, got ${managed_rows}" managed_rendered="$(yq -N 'select(.kind == "OrganizationRuleset" and (.spec.managementPolicies | contains(["Create"]))) | .metadata.name' "${render}" | grep -c . || true)" -[[ "${managed_rendered}" == "4" ]] || - fail "expected 4 rendered managed (Create) organization rulesets, got ${managed_rendered}" +[[ "${managed_rendered}" == "5" ]] || + fail "expected 5 rendered managed (Create) organization rulesets, got ${managed_rendered}" # Schema inspection is not a live census. Keep rendered ownership checks above, # and require the capability inventory to preserve that evidence boundary. if ! grep -Fq 'Schema support determines what can be declared; it does not prove adoption,' "${inventory}" ||