Skip to content

Commit 2e8cca9

Browse files
committed
chore(governance): add governance gates
1 parent 8d7a35f commit 2e8cca9

1 file changed

Lines changed: 111 additions & 26 deletions

File tree

.github/workflows/governance.yml

Lines changed: 111 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)