diff --git a/.github/workflows/governance-sentinel.yml b/.github/workflows/governance-sentinel.yml new file mode 100644 index 0000000..26c03ab --- /dev/null +++ b/.github/workflows/governance-sentinel.yml @@ -0,0 +1,72 @@ +name: governance-sentinel + +# Hardening Q6 — alert if the default-branch protection ruleset (hardening Q2) +# is removed. Covers loss of force-push + deletion protection on the default +# branch. A failed scheduled run also emails the owner, so the check failing +# is itself a signal. +# +# NOT covered here (GitHub Free + this repo token has no admin:org): org +# member-change and audit-log alerting — those need an org-level webhook or the +# audit-log API (GitHub Team/Enterprise) or a PAT with admin:org. See +# dev/reference/github-org-operations.md §Q6 for the documented remainder. + +on: + schedule: + - cron: '0 6 * * 1' # Mondays 06:00 UTC + workflow_dispatch: {} + +permissions: + contents: read + administration: read + issues: write + +jobs: + check-branch-protection: + runs-on: ubuntu-latest + steps: + - name: Verify default-branch protection ruleset is active + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + set -euo pipefail + echo "Checking rulesets on $REPO (default branch: $DEFAULT_BRANCH)" + + # Fail SAFE/QUIET on a token-permission problem: warn, do not alarm. + if ! rulesets=$(gh api "repos/$REPO/rulesets" 2>/tmp/err); then + echo "::warning::Could not read rulesets (token permissions?). Skipping check." + cat /tmp/err || true + exit 0 + fi + + ok=0 + for id in $(echo "$rulesets" | jq -r '.[] | select(.enforcement=="active" and .target=="branch") | .id'); do + rules=$(gh api "repos/$REPO/rulesets/$id" --jq '[.rules[].type] | sort | join(",")') + echo "active branch ruleset $id -> rules: $rules" + if echo "$rules" | grep -q "non_fast_forward" && echo "$rules" | grep -q "deletion"; then + ok=1 + fi + done + + if [ "$ok" -eq 1 ]; then + echo "OK: default-branch force-push + deletion protection is active." + exit 0 + fi + + echo "::error::Default-branch protection ruleset MISSING or incomplete on $REPO" + title="⚠️ Governance drift: default-branch protection missing on $REPO" + existing=$(gh issue list --repo "$REPO" --state open --search "$title in:title" --json number --jq '.[0].number // empty') + if [ -z "$existing" ]; then + gh issue create --repo "$REPO" --title "$title" --body \ + "The scheduled **governance-sentinel** workflow detected that the default-branch protection ruleset (force-push + deletion block, hardening Q2) is no longer active on \`$REPO\`. + + **Expected:** an active \`branch\`-target ruleset carrying \`non_fast_forward\` + \`deletion\` rules on \`$DEFAULT_BRANCH\`. + + **Action:** re-apply per \`dev/reference/github-org-operations.md\` §Q2, or close this issue if the change was intentional. + + _Filed automatically by \`.github/workflows/governance-sentinel.yml\`._" + else + echo "Alert issue already open: #$existing" + fi + exit 1