From 8305314bbec90c8760b85a50030e16d03c22cf42 Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Wed, 4 Mar 2026 01:06:13 -0600 Subject: [PATCH] Normalize required check contexts and harden governance scripts --- .../10-require-chittyconnect-sync-sync.json | 2 +- .../10-require-codeql-analyze.json | 2 +- .../10-require-pr-validation-validate.json | 2 +- .../10-require-chittyconnect-sync-sync.json | 2 +- ...-chittyos-compliance-check-compliance.json | 2 +- .../10-require-chittyconnect-sync-sync.json | 2 +- .../10-require-ci-lint-and-format.json | 2 +- .../10-require-ci-test-and-build.json | 2 +- .../orgs/chittycorp/10-require-ci-test.json | 2 +- ...0-require-compliance-check-compliance.json | 2 +- .../10-require-chittyconnect-sync-sync.json | 2 +- .../chittyos/10-require-ci-lint-and-test.json | 2 +- .../orgs/chittyos/10-require-ci-test.json | 2 +- .../chittyos/10-require-codeql-analyze.json | 2 +- ...0-require-compliance-check-compliance.json | 2 +- .../furnished-condos/10-require-ci-build.json | 2 +- scripts/apply-repo-settings.sh | 8 +- scripts/audit-org-webhooks.sh | 42 ++++++- scripts/discover-org-checks.sh | 2 + scripts/generate-org-rulesets.py | 103 ++++++++++++++---- 20 files changed, 147 insertions(+), 40 deletions(-) diff --git a/compliance/rulesets/orgs/chicagoapps/10-require-chittyconnect-sync-sync.json b/compliance/rulesets/orgs/chicagoapps/10-require-chittyconnect-sync-sync.json index 43d9a9b..ac1a3d2 100644 --- a/compliance/rulesets/orgs/chicagoapps/10-require-chittyconnect-sync-sync.json +++ b/compliance/rulesets/orgs/chicagoapps/10-require-chittyconnect-sync-sync.json @@ -28,7 +28,7 @@ "strict_required_status_checks_policy": true, "required_status_checks": [ { - "context": "ChittyConnect Sync / sync" + "context": "sync" } ] } diff --git a/compliance/rulesets/orgs/chicagoapps/10-require-codeql-analyze.json b/compliance/rulesets/orgs/chicagoapps/10-require-codeql-analyze.json index 2535220..c04eef1 100644 --- a/compliance/rulesets/orgs/chicagoapps/10-require-codeql-analyze.json +++ b/compliance/rulesets/orgs/chicagoapps/10-require-codeql-analyze.json @@ -23,7 +23,7 @@ "strict_required_status_checks_policy": true, "required_status_checks": [ { - "context": "CodeQL / Analyze" + "context": "CodeQL" } ] } diff --git a/compliance/rulesets/orgs/chicagoapps/10-require-pr-validation-validate.json b/compliance/rulesets/orgs/chicagoapps/10-require-pr-validation-validate.json index 0f28bb0..fd23014 100644 --- a/compliance/rulesets/orgs/chicagoapps/10-require-pr-validation-validate.json +++ b/compliance/rulesets/orgs/chicagoapps/10-require-pr-validation-validate.json @@ -23,7 +23,7 @@ "strict_required_status_checks_policy": true, "required_status_checks": [ { - "context": "PR Validation / validate" + "context": "validate" } ] } diff --git a/compliance/rulesets/orgs/chittyapps/10-require-chittyconnect-sync-sync.json b/compliance/rulesets/orgs/chittyapps/10-require-chittyconnect-sync-sync.json index 0e6e80e..857b345 100644 --- a/compliance/rulesets/orgs/chittyapps/10-require-chittyconnect-sync-sync.json +++ b/compliance/rulesets/orgs/chittyapps/10-require-chittyconnect-sync-sync.json @@ -27,7 +27,7 @@ "strict_required_status_checks_policy": true, "required_status_checks": [ { - "context": "ChittyConnect Sync / sync" + "context": "sync" } ] } diff --git a/compliance/rulesets/orgs/chittyapps/10-require-chittyos-compliance-check-compliance.json b/compliance/rulesets/orgs/chittyapps/10-require-chittyos-compliance-check-compliance.json index 3a26aa3..98cc7c8 100644 --- a/compliance/rulesets/orgs/chittyapps/10-require-chittyos-compliance-check-compliance.json +++ b/compliance/rulesets/orgs/chittyapps/10-require-chittyos-compliance-check-compliance.json @@ -23,7 +23,7 @@ "strict_required_status_checks_policy": true, "required_status_checks": [ { - "context": "ChittyOS Compliance Check / compliance" + "context": "compliance" } ] } diff --git a/compliance/rulesets/orgs/chittycorp/10-require-chittyconnect-sync-sync.json b/compliance/rulesets/orgs/chittycorp/10-require-chittyconnect-sync-sync.json index f39e62b..f45d418 100644 --- a/compliance/rulesets/orgs/chittycorp/10-require-chittyconnect-sync-sync.json +++ b/compliance/rulesets/orgs/chittycorp/10-require-chittyconnect-sync-sync.json @@ -24,7 +24,7 @@ "strict_required_status_checks_policy": true, "required_status_checks": [ { - "context": "ChittyConnect Sync / sync" + "context": "sync" } ] } diff --git a/compliance/rulesets/orgs/chittycorp/10-require-ci-lint-and-format.json b/compliance/rulesets/orgs/chittycorp/10-require-ci-lint-and-format.json index 57e9ee4..efb1b53 100644 --- a/compliance/rulesets/orgs/chittycorp/10-require-ci-lint-and-format.json +++ b/compliance/rulesets/orgs/chittycorp/10-require-ci-lint-and-format.json @@ -23,7 +23,7 @@ "strict_required_status_checks_policy": true, "required_status_checks": [ { - "context": "CI / lint-and-format" + "context": "lint-and-format" } ] } diff --git a/compliance/rulesets/orgs/chittycorp/10-require-ci-test-and-build.json b/compliance/rulesets/orgs/chittycorp/10-require-ci-test-and-build.json index 240e41d..b87d651 100644 --- a/compliance/rulesets/orgs/chittycorp/10-require-ci-test-and-build.json +++ b/compliance/rulesets/orgs/chittycorp/10-require-ci-test-and-build.json @@ -23,7 +23,7 @@ "strict_required_status_checks_policy": true, "required_status_checks": [ { - "context": "CI / test-and-build" + "context": "test-and-build" } ] } diff --git a/compliance/rulesets/orgs/chittycorp/10-require-ci-test.json b/compliance/rulesets/orgs/chittycorp/10-require-ci-test.json index c006676..8568963 100644 --- a/compliance/rulesets/orgs/chittycorp/10-require-ci-test.json +++ b/compliance/rulesets/orgs/chittycorp/10-require-ci-test.json @@ -23,7 +23,7 @@ "strict_required_status_checks_policy": true, "required_status_checks": [ { - "context": "CI / test" + "context": "test" } ] } diff --git a/compliance/rulesets/orgs/chittycorp/10-require-compliance-check-compliance.json b/compliance/rulesets/orgs/chittycorp/10-require-compliance-check-compliance.json index b4de320..7d1d7d8 100644 --- a/compliance/rulesets/orgs/chittycorp/10-require-compliance-check-compliance.json +++ b/compliance/rulesets/orgs/chittycorp/10-require-compliance-check-compliance.json @@ -23,7 +23,7 @@ "strict_required_status_checks_policy": true, "required_status_checks": [ { - "context": "Compliance Check / compliance" + "context": "compliance" } ] } diff --git a/compliance/rulesets/orgs/chittyos/10-require-chittyconnect-sync-sync.json b/compliance/rulesets/orgs/chittyos/10-require-chittyconnect-sync-sync.json index 274781a..833a93f 100644 --- a/compliance/rulesets/orgs/chittyos/10-require-chittyconnect-sync-sync.json +++ b/compliance/rulesets/orgs/chittyos/10-require-chittyconnect-sync-sync.json @@ -37,7 +37,7 @@ "strict_required_status_checks_policy": true, "required_status_checks": [ { - "context": "ChittyConnect Sync / sync" + "context": "sync" } ] } diff --git a/compliance/rulesets/orgs/chittyos/10-require-ci-lint-and-test.json b/compliance/rulesets/orgs/chittyos/10-require-ci-lint-and-test.json index 8043999..70fed05 100644 --- a/compliance/rulesets/orgs/chittyos/10-require-ci-lint-and-test.json +++ b/compliance/rulesets/orgs/chittyos/10-require-ci-lint-and-test.json @@ -23,7 +23,7 @@ "strict_required_status_checks_policy": true, "required_status_checks": [ { - "context": "CI / lint-and-test" + "context": "lint-and-test" } ] } diff --git a/compliance/rulesets/orgs/chittyos/10-require-ci-test.json b/compliance/rulesets/orgs/chittyos/10-require-ci-test.json index 1d1c1cb..c2c8dc3 100644 --- a/compliance/rulesets/orgs/chittyos/10-require-ci-test.json +++ b/compliance/rulesets/orgs/chittyos/10-require-ci-test.json @@ -30,7 +30,7 @@ "strict_required_status_checks_policy": true, "required_status_checks": [ { - "context": "CI / test" + "context": "test" } ] } diff --git a/compliance/rulesets/orgs/chittyos/10-require-codeql-analyze.json b/compliance/rulesets/orgs/chittyos/10-require-codeql-analyze.json index e8161ad..9287d6e 100644 --- a/compliance/rulesets/orgs/chittyos/10-require-codeql-analyze.json +++ b/compliance/rulesets/orgs/chittyos/10-require-codeql-analyze.json @@ -23,7 +23,7 @@ "strict_required_status_checks_policy": true, "required_status_checks": [ { - "context": "CodeQL / Analyze" + "context": "CodeQL" } ] } diff --git a/compliance/rulesets/orgs/chittyos/10-require-compliance-check-compliance.json b/compliance/rulesets/orgs/chittyos/10-require-compliance-check-compliance.json index aa02eb7..b368e56 100644 --- a/compliance/rulesets/orgs/chittyos/10-require-compliance-check-compliance.json +++ b/compliance/rulesets/orgs/chittyos/10-require-compliance-check-compliance.json @@ -32,7 +32,7 @@ "strict_required_status_checks_policy": true, "required_status_checks": [ { - "context": "Compliance Check / compliance" + "context": "compliance" } ] } diff --git a/compliance/rulesets/orgs/furnished-condos/10-require-ci-build.json b/compliance/rulesets/orgs/furnished-condos/10-require-ci-build.json index 063f0f4..16625bf 100644 --- a/compliance/rulesets/orgs/furnished-condos/10-require-ci-build.json +++ b/compliance/rulesets/orgs/furnished-condos/10-require-ci-build.json @@ -23,7 +23,7 @@ "strict_required_status_checks_policy": true, "required_status_checks": [ { - "context": "CI / build" + "context": "build" } ] } diff --git a/scripts/apply-repo-settings.sh b/scripts/apply-repo-settings.sh index 87ad08c..1d16b08 100755 --- a/scripts/apply-repo-settings.sh +++ b/scripts/apply-repo-settings.sh @@ -33,13 +33,17 @@ apply_repo_settings() { return 0 fi - if gh api "repos/$org/$repo" --method PATCH --input <(printf '%s' "$payload") >/dev/null 2>/tmp/repo_patch_err.log; then + local errfile + errfile="$(mktemp)" + if gh api "repos/$org/$repo" --method PATCH --input <(printf '%s' "$payload") >/dev/null 2>"$errfile"; then + rm -f "$errfile" echo "patched $org/$repo" return 0 fi echo "failed $org/$repo" - sed -n '1,2p' /tmp/repo_patch_err.log + sed -n '1,2p' "$errfile" + rm -f "$errfile" return 1 } diff --git a/scripts/audit-org-webhooks.sh b/scripts/audit-org-webhooks.sh index 6ab668b..4f558ff 100755 --- a/scripts/audit-org-webhooks.sh +++ b/scripts/audit-org-webhooks.sh @@ -59,8 +59,18 @@ if ! gh auth status >/dev/null 2>&1; then exit 1 fi -if ! gh auth status -t 2>/dev/null | grep -q 'admin:org_hook'; then - echo "Missing admin:org_hook scope. Run: gh auth refresh -h github.com -s admin:org_hook" +org_probe="${orgs[0]}" +probe_status="$(gh api -X GET -H 'Accept: application/vnd.github+json' "/orgs/$org_probe/hooks" -i 2>/dev/null | awk 'NR==1 {print $2}')" +if [ -z "$probe_status" ]; then + echo "Failed to verify GitHub token permissions for org '$org_probe'" + exit 1 +fi +if [ "$probe_status" = "403" ]; then + echo "Token lacks permission to list org hooks for '$org_probe'" + exit 1 +fi +if [ "$probe_status" != "200" ]; then + echo "Unexpected HTTP status '$probe_status' when verifying org hook access for '$org_probe'" exit 1 fi @@ -115,9 +125,19 @@ for org in "${orgs[@]}"; do needs_apply_reasons=() - # Validate event scope unless wildcard is used intentionally. + # Validate event scope against the required event set. if [ "$events_joined" = "*" ]; then needs_apply_reasons+=("wildcard-events") + else + sorted_events_joined="$( + tr ',' '\n' <<<"$events_joined" | sed '/^$/d' | sort -u | tr '\n' ',' | sed 's/,$//' + )" + sorted_required_events="$( + tr ',' '\n' <<<"$WEBHOOK_EVENTS_CSV" | sed '/^$/d' | sort -u | tr '\n' ',' | sed 's/,$//' + )" + if [ "$sorted_events_joined" != "$sorted_required_events" ]; then + needs_apply_reasons+=("events-mismatch") + fi fi if [ "$APPLY" -eq 1 ]; then @@ -134,6 +154,22 @@ for org in "${orgs[@]}"; do hooks_json="$(gh api "/orgs/$org/hooks")" hook_obj="$(jq --argjson id "$hook_id" '.[] | select(.id == $id)' <<<"$hooks_json")" events_joined="$(jq -r '.events | join(",")' <<<"$hook_obj")" + + # Re-evaluate event scope after apply mode rewrites events. + needs_apply_reasons=() + if [ "$events_joined" = "*" ]; then + needs_apply_reasons+=("wildcard-events") + else + sorted_events_joined="$( + tr ',' '\n' <<<"$events_joined" | sed '/^$/d' | sort -u | tr '\n' ',' | sed 's/,$//' + )" + sorted_required_events="$( + tr ',' '\n' <<<"$WEBHOOK_EVENTS_CSV" | sed '/^$/d' | sort -u | tr '\n' ',' | sed 's/,$//' + )" + if [ "$sorted_events_joined" != "$sorted_required_events" ]; then + needs_apply_reasons+=("events-mismatch") + fi + fi fi # Delivery-level verification (signature header + response code) diff --git a/scripts/discover-org-checks.sh b/scripts/discover-org-checks.sh index 595c012..6a4557b 100755 --- a/scripts/discover-org-checks.sh +++ b/scripts/discover-org-checks.sh @@ -61,6 +61,8 @@ discover_org() { j = cfg.is_a?(Hash) ? cfg : {} jname = j["name"].to_s jname = jid.to_s if jname.nil? || jname.empty? + # Include both raw check-run name and workflow/job for compatibility. + puts "- #{jname}" puts "- #{wname} / #{jname}" end ' 2>/dev/null || true) diff --git a/scripts/generate-org-rulesets.py b/scripts/generate-org-rulesets.py index e18ce9c..ad3bd74 100755 --- a/scripts/generate-org-rulesets.py +++ b/scripts/generate-org-rulesets.py @@ -19,20 +19,81 @@ } candidate_checks = [ - 'ChittyConnect Sync / sync', - 'Compliance Check / compliance', - 'CI / test', - 'CI / build', - 'CI / lint-and-test', - 'CI / lint-and-format', - 'CI / test-and-build', - 'CodeQL / Analyze', - 'PR Validation / validate', - 'ChittyOS Compliance Check / compliance', + { + 'name': 'ChittyConnect Sync / sync', + 'context': 'sync', + 'aliases': ['sync', 'ChittyConnect Sync / sync'], + }, + { + 'name': 'Compliance Check / compliance', + 'context': 'compliance', + 'aliases': ['compliance', 'Compliance Check / compliance'], + }, + { + 'name': 'CI / test', + 'context': 'test', + 'aliases': ['test', 'CI / test'], + }, + { + 'name': 'CI / build', + 'context': 'build', + 'aliases': ['build', 'CI / build'], + }, + { + 'name': 'CI / lint-and-test', + 'context': 'lint-and-test', + 'aliases': ['lint-and-test', 'CI / lint-and-test'], + }, + { + 'name': 'CI / lint-and-format', + 'context': 'lint-and-format', + 'aliases': ['lint-and-format', 'CI / lint-and-format'], + }, + { + 'name': 'CI / test-and-build', + 'context': 'test-and-build', + 'aliases': ['test-and-build', 'CI / test-and-build'], + }, + { + 'name': 'CodeQL / Analyze', + 'context': 'CodeQL', + 'aliases': ['CodeQL', 'CodeQL / Analyze', 'Analyze (javascript)'], + }, + { + 'name': 'PR Validation / validate', + 'context': 'validate', + 'aliases': ['validate', 'PR Validation / validate'], + }, + { + 'name': 'ChittyOS Compliance Check / compliance', + 'context': 'compliance', + 'aliases': ['compliance', 'ChittyOS Compliance Check / compliance'], + }, ] -REQUIRED_APPROVING_REVIEW_COUNT = int( - os.getenv('REQUIRED_APPROVING_REVIEW_COUNT', '0') -) + + +def parse_required_approving_review_count(): + raw = os.getenv('REQUIRED_APPROVING_REVIEW_COUNT', '0') + if raw == '': + return 0 + try: + value = int(raw) + except ValueError: + sys.stderr.write( + 'ERROR: REQUIRED_APPROVING_REVIEW_COUNT must be an integer between 0 and 6; ' + f'got {raw!r}.\n' + ) + sys.exit(1) + if value < 0 or value > 6: + sys.stderr.write( + 'ERROR: REQUIRED_APPROVING_REVIEW_COUNT must be between 0 and 6; ' + f'got {value}.\n' + ) + sys.exit(1) + return value + + +REQUIRED_APPROVING_REVIEW_COUNT = parse_required_approving_review_count() def parse_repo_checks(p: Path): @@ -75,9 +136,9 @@ def baseline_ruleset(org: str): } -def check_ruleset(check: str, repos: list[str]): +def check_ruleset(name: str, context: str, repos: list[str]): slug = ( - check.lower() + name.lower() .replace(' / ', '-') .replace('/', '-') .replace(' ', '-') @@ -88,7 +149,7 @@ def check_ruleset(check: str, repos: list[str]): while '--' in slug: slug = slug.replace('--', '-') payload = { - 'name': f'Require {check}', + 'name': f'Require {name}', 'target': 'branch', 'enforcement': 'active', 'conditions': { @@ -101,7 +162,7 @@ def check_ruleset(check: str, repos: list[str]): 'parameters': { 'strict_required_status_checks_policy': True, 'required_status_checks': [ - {'context': check} + {'context': context} ], }, } @@ -128,10 +189,14 @@ def main(): files = ['00-baseline.json'] for check in candidate_checks: - repos = [r for r, checks in repo_checks.items() if check in checks] + aliases = set(check['aliases']) + repos = [ + r for r, checks in repo_checks.items() + if aliases.intersection(checks) + ] if not repos: continue - slug, payload = check_ruleset(check, repos) + slug, payload = check_ruleset(check['name'], check['context'], repos) fname = f'10-require-{slug}.json' (org_dir / fname).write_text(json.dumps(payload, indent=2) + '\n') files.append(fname)