@@ -30,25 +30,65 @@ jobs:
3030 with :
3131 fetch-depth : 0
3232
33- - name : Fetch Governance Rules
33+ - name : Fetch Governance Rules + Repo Requirements
3434 run : |
3535 # Public distribution repo (rules are signed; safe to publish)
3636 curl -sL "https://raw.githubusercontent.com/chittyfoundation/.github/main/governance/rules.json" -o rules.json
3737 curl -sL "https://raw.githubusercontent.com/chittyfoundation/.github/main/governance/CANON_PUBLIC_KEY" -o CANON_PUBLIC_KEY
38+ curl -sL "https://raw.githubusercontent.com/chittyfoundation/.github/main/governance/repo_requirements.json" -o repo_requirements.json
3839
39- - name : Verify Rules Signature
40+ - name : Determine Repo Tier
41+ id : tier
42+ shell : bash
4043 run : |
44+ set -euo pipefail
45+ FULL_REPO="${GITHUB_REPOSITORY}"
46+ tier=$(jq -r --arg repo "$FULL_REPO" '
47+ def glob_to_re($g):
48+ "^" + ($g
49+ | gsub("\\."; "\\\\.")
50+ | gsub("\\*\\*"; ".*")
51+ | gsub("\\*"; "[^/]*")
52+ ) + "$";
53+ .repos
54+ | map(. + { re: (glob_to_re(.repo)) })
55+ | map(select($repo | test(.re)))
56+ | .[0].tier // 99
57+ ' repo_requirements.json 2>/dev/null || echo "99")
58+ echo "tier=$tier" >> "$GITHUB_OUTPUT"
59+ if [[ "$tier" -le 1 ]]; then
60+ echo "::notice::Repo tier=${tier} (strict governance)"
61+ else
62+ echo "::notice::Repo tier=${tier} (bootstrap governance allowed)"
63+ fi
64+
65+ - name : Verify Rules Signature (Phased)
66+ shell : bash
67+ run : |
68+ set -euo pipefail
69+ tier='${{ steps.tier.outputs.tier }}'
4170 SIGNATURE=$(jq -r '.signature.signature // empty' rules.json)
71+
4272 if [[ -z "$SIGNATURE" ]]; then
43- echo "::error::Governance rules are NOT SIGNED. Human must run sign-rules.sh and publish CANON_PUBLIC_KEY + signed rules.json."
44- exit 1
73+ if [[ "$tier" -le 1 ]]; then
74+ echo "::error::Governance rules are NOT SIGNED. Tier ${tier} requires signed rules. Human must run sign-rules.sh and publish CANON_PUBLIC_KEY + signed rules.json."
75+ exit 1
76+ fi
77+ echo "::warning::Governance rules are NOT SIGNED. Continuing in bootstrap mode (Tier ${tier})."
78+ exit 0
4579 fi
4680
4781 CANONICAL=$(jq -cS 'del(.signature.signature) | del(.signature.signed_at)' rules.json)
4882 echo -n "$CANONICAL" > canonical.json
4983 echo "$SIGNATURE" | base64 -d > signature.bin
5084
51- openssl pkeyutl -verify -pubin -inkey CANON_PUBLIC_KEY -sigfile signature.bin -in canonical.json
85+ if openssl pkeyutl -verify -pubin -inkey CANON_PUBLIC_KEY -sigfile signature.bin -in canonical.json; then
86+ echo "::notice::Governance rules signature VERIFIED"
87+ exit 0
88+ fi
89+
90+ echo "::error::Governance rules signature INVALID. Rules may have been tampered with."
91+ exit 1
5292
5393 - name : Require CODEOWNERS
5494 run : |
@@ -59,13 +99,27 @@ jobs:
5999 exit 1
60100
61101 - name : Check Protected File Changes
102+ shell : bash
62103 run : |
104+ set -euo pipefail
63105 CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }})
64106 echo "Changed files:"
65107 echo "$CHANGED_FILES"
66108 echo ""
67109
68- PROTECTED_PATTERNS=$(jq -r '.rules.protected_files.files[]' rules.json)
110+ # If rules are unsigned, fall back to a minimal bootstrap set.
111+ SIGNATURE=$(jq -r '.signature.signature // empty' rules.json)
112+ if [[ -z "$SIGNATURE" ]]; then
113+ PROTECTED_PATTERNS=$(cat <<'EOF'
114+ .github/workflows/**
115+ .github/CODEOWNERS
116+ CODEOWNERS
117+ governance/**
118+ EOF
119+ )
120+ else
121+ PROTECTED_PATTERNS=$(jq -r '.rules.protected_files.files[]' rules.json)
122+ fi
69123 VIOLATIONS=""
70124
71125 for pattern in $PROTECTED_PATTERNS; do
@@ -118,8 +172,33 @@ jobs:
118172 curl -sL "$OWNERS_URL" -o owners.json || fail "Unable to fetch ownership registry: $OWNERS_URL"
119173 curl -sL "$REQS_URL" -o repo_requirements.json || fail "Unable to fetch repo requirements: $REQS_URL"
120174
175+ # Determine tier (unknown repos default to tier 99)
176+ FULL_REPO="${GITHUB_REPOSITORY}"
177+ REQUIREMENT=$(jq -c --arg repo "$FULL_REPO" '
178+ def glob_to_re($g):
179+ "^" + ($g
180+ | gsub("\\."; "\\\\.")
181+ | gsub("\\*\\*"; ".*")
182+ | gsub("\\*"; "[^/]*")
183+ ) + "$";
184+ .repos
185+ | map(. + { re: (glob_to_re(.repo)) })
186+ | map(select($repo | test(.re)))
187+ | .[0] // empty
188+ ' repo_requirements.json)
189+
190+ tier=99
191+ if [[ -n "$REQUIREMENT" ]]; then
192+ tier=$(echo "$REQUIREMENT" | jq -r '.tier // 99')
193+ fi
194+
195+ # Tiered enforcement: hard-fail Tier 0/1 first; warn elsewhere (except secrets/supply-chain hard fails).
121196 if [[ ! -f "SECURITY.md" && ! -f ".github/SECURITY.md" ]]; then
122- fail "Missing SECURITY.md"
197+ if [[ "$tier" -le 1 ]]; then
198+ fail "Missing SECURITY.md (Tier ${tier})"
199+ else
200+ warn "Missing SECURITY.md (Tier ${tier})"
201+ fi
123202 fi
124203 if git ls-files --error-unmatch .env >/dev/null 2>&1; then
125204 fail "Tracked .env detected"
@@ -136,31 +215,29 @@ jobs:
136215 [[ -f "pnpm-lock.yaml" ]] && LOCKS=$((LOCKS+1))
137216 [[ -f "yarn.lock" ]] && LOCKS=$((LOCKS+1))
138217 if [[ "$LOCKS" -eq 0 ]]; then
139- fail "package.json present but no lockfile found"
218+ if [[ "$tier" -le 1 ]]; then
219+ fail "package.json present but no lockfile found (Tier ${tier})"
220+ else
221+ warn "package.json present but no lockfile found (Tier ${tier})"
222+ fi
140223 fi
141224 fi
142225
143- # Eligibility gating (warn-only until registry is populated )
226+ # Eligibility gating (warn-only for Tier 2+; fail for Tier 0/1 )
144227 OWNERS_FILE="CODEOWNERS"
145228 [[ -f ".github/CODEOWNERS" ]] && OWNERS_FILE=".github/CODEOWNERS"
229+ if [[ ! -f "$OWNERS_FILE" ]]; then
230+ if [[ "$tier" -le 1 ]]; then
231+ fail "Missing CODEOWNERS (Tier ${tier})"
232+ else
233+ warn "Missing CODEOWNERS (Tier ${tier})"
234+ exit 0
235+ fi
236+ fi
146237 PRINCIPALS=$(sed 's/#.*$//' "$OWNERS_FILE" | tr '\t' ' ' | tr ' ' '\n' | grep '^@' | sort -u || true)
147238 [[ -n "$PRINCIPALS" ]] || exit 0
148239
149- FULL_REPO="${GITHUB_REPOSITORY}"
150- REQUIREMENT=$(jq -c --arg repo "$FULL_REPO" '
151- def glob_to_re($g):
152- "^" + ($g
153- | gsub("\\."; "\\\\.")
154- | gsub("\\*\\*"; ".*")
155- | gsub("\\*"; "[^/]*")
156- ) + "$";
157- .repos
158- | map(. + { re: (glob_to_re(.repo)) })
159- | map(select($repo | test(.re)))
160- | .[0] // empty
161- ' repo_requirements.json)
162-
163- [[ -n "$REQUIREMENT" ]] || exit 0
240+ [[ -n "$REQUIREMENT" ]] || { warn "No repo_requirements entry for ${FULL_REPO}; skipping eligibility gating"; exit 0; }
164241
165242 REQ_TY=$(echo "$REQUIREMENT" | jq -r '.requires.ty[]?' 2>/dev/null || true)
166243 REQ_VY=$(echo "$REQUIREMENT" | jq -r '.requires.vy[]?' 2>/dev/null || true)
@@ -195,7 +272,11 @@ jobs:
195272 [[ -z "$principal" ]] && continue
196273 row=$(jq -c --arg gh "$principal" '.principals[]? | select(.github==$gh)' owners.json || true)
197274 if [[ -z "$row" ]]; then
198- warn "CODEOWNERS principal missing from registry: $principal"
275+ if [[ "$tier" -le 1 ]]; then
276+ fail "CODEOWNERS principal missing from registry: $principal (Tier ${tier})"
277+ else
278+ warn "CODEOWNERS principal missing from registry: $principal (Tier ${tier})"
279+ fi
199280 continue
200281 fi
201282 ok="true"
@@ -218,6 +299,10 @@ jobs:
218299 fi
219300 done
220301 if [[ "$ok" != "true" ]]; then
221- warn "CODEOWNERS principal fails TY/VY/RY eligibility: $principal"
302+ if [[ "$tier" -le 1 ]]; then
303+ fail "CODEOWNERS principal fails TY/VY/RY eligibility: $principal (Tier ${tier})"
304+ else
305+ warn "CODEOWNERS principal fails TY/VY/RY eligibility: $principal (Tier ${tier})"
306+ fi
222307 fi
223308 done <<< "$PRINCIPALS"
0 commit comments