Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 0 additions & 37 deletions .github/rulesets/code-quality.json

This file was deleted.

36 changes: 0 additions & 36 deletions .github/rulesets/pr-quality.json

This file was deleted.

12 changes: 5 additions & 7 deletions docs/bootstrap/new-repo-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,13 @@ $ DRY_RUN=true GITHUB_ACTOR=octocat bash scripts/bootstrap-new-repo.sh petry-pro
[bootstrap] (2/5) repo settings + security/GHAS + push protection
[dry-run] would patch security_and_analysis on petry-projects/acme-service: secret_scanning secret_scanning_push_protection secret_scanning_ai_detection secret_scanning_non_provider_patterns dependabot_security_updates
[dry-run] would disable auto-trigger for apps 1236702 347564 on petry-projects/acme-service
[bootstrap] (3/5) sanctioned rulesets (pr-quality + code-quality + …)
[apply-rulesets] repo=petry-projects/acme-service dir=.../.github/rulesets dry_run=true
[bootstrap] (3/5) sanctioned fleet rulesets (pr-quality + code-quality)
[apply-rulesets] repo=petry-projects/acme-service dir=<materialized from petry-projects/.github> dry_run=true
create ruleset 'code-quality' on petry-projects/acme-service
[dry-run] POST repos/petry-projects/acme-service/rulesets
create ruleset 'pr-quality' on petry-projects/acme-service
[dry-run] POST repos/petry-projects/acme-service/rulesets
create ruleset 'release-channel-tags' on petry-projects/acme-service
[dry-run] POST repos/petry-projects/acme-service/rulesets
[apply-rulesets] done (3 ruleset(s))
[apply-rulesets] done (2 ruleset(s))
[bootstrap] (4/5) standard label set
[dry-run] would ensure label 'needs-human-review' on petry-projects/acme-service
[dry-run] would ensure label 'ack-test-deletion' on petry-projects/acme-service
Expand Down Expand Up @@ -92,8 +90,8 @@ caller stub — and asserts no drift before writing:
| Repo settings + security/GHAS | `apply-repo-settings.sh` | `security_and_analysis` patch intent |
| Secret-scanning push protection | `lib/push-protection.sh` | `secret_scanning_push_protection` in the patch set |
| Check-suite auto-trigger (Claude/CodeRabbit) | `apply-repo-settings.sh` | `would disable auto-trigger for apps 1236702 347564` |
| `pr-quality` ruleset + bypass actors | `.github/rulesets/pr-quality.json` | created; bypass = OrganizationAdmin + Integration (`bypass_mode: always`) |
| `code-quality` ruleset + required checks + bypass | `.github/rulesets/code-quality.json` | created; required checks SonarCloud, CodeQL, agent-shield, dependency-audit; same bypass actors |
| `pr-quality` ruleset + bypass actors | `petry-projects/.github` → `standards/rulesets/pr-quality.json` | created; bypass = OrganizationAdmin + Integration (`bypass_mode: always`) |
| `code-quality` ruleset + required checks + bypass | `petry-projects/.github` → `standards/rulesets/code-quality.json` | created; required checks SonarCloud, CodeQL, agent-shield, dependency-audit; same bypass actors |
| Required status checks | carried in the ruleset JSONs | not wired by bootstrap — live in `code-quality.json` |
| Standard labels | `bootstrap-new-repo.sh` `BOOTSTRAP_LABELS` | needs-human-review, ack-test-deletion, dependencies, automerge |
| CODEOWNERS team | new repo's `.github/CODEOWNERS` | first owner verified = `@petry-projects/org-leads` |
Expand Down
87 changes: 75 additions & 12 deletions scripts/apply-rulesets.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
# apply-rulesets.sh — codified, idempotent application of repository rulesets from
# .github/rulesets/*.json (initiative #495, issue #868). Makes the previously
# live-only rulesets — notably `release-channel-tags`, which protects the moving
# channel tags `pr-review/**` + `dev-lead/**` (and therefore the ring channels
# next/ring0/ring1 via the `**` glob) — reproducible and version-controlled.
# apply-rulesets.sh — codified, idempotent application of repository rulesets
# (initiative #495, issue #868).
#
# The org-wide compliance rulesets — `code-quality` and `pr-quality` — are OWNED by
# petry-projects/.github and sourced from its standards/rulesets/*.json (relocated
# there under petry-projects/.github#575; the repo boundary is codified in #576).
# The one ruleset that stays LOCAL to this repo is `release-channel-tags`, which
# protects .github-private's own moving channel tags `pr-review/**` + `dev-lead/**`
# (and therefore the ring channels next/ring0/ring1 via the `**` glob).
#
# By default this materializes the fleet rulesets (code-quality, pr-quality) from
# petry-projects/.github and applies them to the target repo. To apply the
# repo-local `release-channel-tags`, point RULESETS_DIR at this repo's own dir:
# RULESETS_DIR=.github/rulesets RULESETS_REPO=petry-projects/.github-private \
# bash scripts/apply-rulesets.sh release-channel-tags
#
# For each ruleset JSON, this finds the existing ruleset by name on the target repo
# and PUTs an update, or POSTs a create if absent. Re-running is a no-op-shaped
Expand All @@ -15,23 +25,63 @@ set -euo pipefail
# RULESETS_REPO=owner/repo bash scripts/apply-rulesets.sh
#
# Env:
# RULESETS_REPO target repo (default: petry-projects/.github-private — where the
# pr-review/dev-lead release tags live)
# RULESETS_DIR directory of ruleset JSONs (default: .github/rulesets)
# GH_TOKEN token with admin:org / repo admin to read+write rulesets
# DRY_RUN "true" → print intent, make no write calls
# RULESETS_REPO target repo to apply rulesets TO (default: petry-projects/.github-private)
# RULESETS_DIR explicit directory of ruleset JSONs. When set, it is used
# as-is — this is how the repo-local `release-channel-tags` is
# applied from this repo's own .github/rulesets. When UNSET,
# the fleet rulesets are materialized from STANDARDS_REPO.
# FLEET_RULESETS_DIR local checkout of standards/rulesets/ (at the petry-projects/.github
# repo root) to source the fleet rulesets from, skipping the network
# fetch (offline/CI/pin).
# STANDARDS_REPO repo owning the fleet rulesets (default: petry-projects/.github).
# GH_TOKEN token with admin:org / repo admin to read+write rulesets
# DRY_RUN "true" → print intent, make no write calls
#
# Bypass model (release-channel-tags): OrganizationAdmin + the automation Integration
# app may move/delete channel tags; agents running as GITHUB_TOKEN cannot. The
# canary-rollout promotion workflow (#501) moves tags via GH_PAT_WORKFLOWS (owned by
# an org admin); the long-term hardening is a dedicated GitHub App whose id is added
# to bypass_actors here so the bypass scopes to the workflow identity.

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
RULESETS_REPO="${RULESETS_REPO:-petry-projects/.github-private}"
RULESETS_DIR="${RULESETS_DIR:-$(cd "${SCRIPT_DIR}/.." && pwd)/.github/rulesets}"
RULESETS_DIR="${RULESETS_DIR:-}"
FLEET_RULESETS_DIR="${FLEET_RULESETS_DIR:-}"
STANDARDS_REPO="${STANDARDS_REPO:-petry-projects/.github}"
DRY_RUN="${DRY_RUN:-false}"

# The org-wide fleet rulesets, owned by petry-projects/.github (standards/rulesets/).
FLEET_RULESETS=(code-quality pr-quality)

# _cleanup_fleet_tmpdir / _materialize_fleet_dir — when RULESETS_DIR is unset, place
# the fleet ruleset JSONs into a directory and assign it to the global RULESETS_DIR.
# Uses FLEET_RULESETS_DIR verbatim when provided (offline/CI/local checkout), else
# fetches each fleet ruleset from ${STANDARDS_REPO} via the contents API into a temp
# dir (registered for cleanup on exit). Mirrors seed-repo-template.sh's fetch model.
_FLEET_TMPDIR=""
_FLEET_MODE=false
_cleanup_fleet_tmpdir() { [ -n "$_FLEET_TMPDIR" ] && rm -rf "$_FLEET_TMPDIR"; return 0; }

_materialize_fleet_dir() {
if [ -n "$FLEET_RULESETS_DIR" ]; then
[ -d "$FLEET_RULESETS_DIR" ] \
|| { echo "::error::FLEET_RULESETS_DIR not found: $FLEET_RULESETS_DIR" >&2; return 1; }
RULESETS_DIR="$FLEET_RULESETS_DIR"
_FLEET_MODE=true
return 0
fi
_FLEET_TMPDIR="$(mktemp -d)" || return 1
_FLEET_MODE=true
trap _cleanup_fleet_tmpdir EXIT
local name
for name in "${FLEET_RULESETS[@]}"; do
gh api "repos/${STANDARDS_REPO}/contents/standards/rulesets/${name}.json" --jq '.content' 2>/dev/null \
| base64 -d 2>/dev/null > "${_FLEET_TMPDIR}/${name}.json" || true
[ -s "${_FLEET_TMPDIR}/${name}.json" ] \
|| { echo "::error::could not fetch standards/rulesets/${name}.json from ${STANDARDS_REPO}" >&2; return 1; }
done
RULESETS_DIR="$_FLEET_TMPDIR"
}

# ruleset_id_by_name <repo> <name> — echo the id of an existing ruleset, or empty.
ruleset_id_by_name() {
local repo="$1" name="$2"
Expand Down Expand Up @@ -77,6 +127,13 @@ main() {
esac
done

# Resolve the source directory: an explicit RULESETS_DIR (e.g. this repo's own
# .github/rulesets for release-channel-tags) wins; otherwise materialize the fleet
# rulesets owned by petry-projects/.github.
if [ -z "$RULESETS_DIR" ]; then
_materialize_fleet_dir || return 1
fi
Comment thread
don-petry marked this conversation as resolved.

[ -d "$RULESETS_DIR" ] || { echo "::error::rulesets dir not found: $RULESETS_DIR" >&2; return 1; }
echo "[apply-rulesets] repo=${repo} dir=${RULESETS_DIR} dry_run=${DRY_RUN}"

Expand All @@ -87,6 +144,12 @@ main() {
[ -f "${RULESETS_DIR}/${n}.json" ] && files+=("${RULESETS_DIR}/${n}.json") \
|| { echo "::error::no ruleset file ${n}.json" >&2; return 1; }
done
elif [ "$_FLEET_MODE" = "true" ]; then
local n
for n in "${FLEET_RULESETS[@]}"; do
[ -f "${RULESETS_DIR}/${n}.json" ] && files+=("${RULESETS_DIR}/${n}.json") \
|| { echo "::error::no ruleset file ${n}.json in fleet dir" >&2; return 1; }
done
else
local f
for f in "${RULESETS_DIR}"/*.json; do [ -e "$f" ] && files+=("$f"); done
Expand Down
22 changes: 14 additions & 8 deletions scripts/bootstrap-new-repo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ set -euo pipefail
#
# • repo settings + security/GHAS + secret-scanning push protection come from
# scripts/apply-repo-settings.sh (which sources scripts/lib/push-protection.sh)
# • the two sanctioned rulesets — pr-quality + code-quality, each carrying the
# mandatory bypass actors dependabot-automerge-petry (Integration app) +
# • the two sanctioned fleet rulesets — pr-quality + code-quality, each carrying
# the mandatory bypass actors dependabot-automerge-petry (Integration app) +
# OrganizationAdmin, both bypass_mode "always" — come from
# scripts/apply-rulesets.sh reading .github/rulesets/*.json. Required status
# checks are carried in those ruleset JSONs, not wired here. No legacy/ad-hoc
# `main` ruleset is created.
# scripts/apply-rulesets.sh, which sources them from petry-projects/.github
# (standards/rulesets/*.json, relocated there under #575). Required status
# checks are carried in those ruleset JSONs, not wired here. The repo-local
# `release-channel-tags` ruleset is NOT applied to bootstrapped repos — it
# protects .github-private's own pr-review/** + dev-lead/** release tags only.
# No legacy/ad-hoc `main` ruleset is created.
# • the standard label set + CODEOWNERS-team verification are bootstrap data,
# applied/verified here.
#
Expand Down Expand Up @@ -313,12 +316,15 @@ step_repo_settings() {
DEV_LEAD_DRY_RUN="$dev_lead_dry" bash "$APPLY_REPO_SETTINGS" "$repo"
}

# step_rulesets <repo> — apply every codified ruleset (pr-quality, code-quality, …).
# step_rulesets <repo> — apply the sanctioned fleet rulesets (pr-quality +
# code-quality), which apply-rulesets.sh sources from petry-projects/.github. With
# no explicit RULESETS_DIR the applier is in fleet mode, so exactly those two are
# applied; release-channel-tags is repo-local to .github-private and not applied here.
step_rulesets() {
local repo="${1:-}" rulesets_dry=false
_is_dry && rulesets_dry=true
echo "[bootstrap] (3/5) sanctioned rulesets (pr-quality + code-quality + …)"
DRY_RUN="$rulesets_dry" bash "$APPLY_RULESETS" --repo "$repo"
echo "[bootstrap] (3/5) sanctioned fleet rulesets (pr-quality + code-quality)"
DRY_RUN="$rulesets_dry" RULESETS_DIR="" bash "$APPLY_RULESETS" --repo "$repo"
}
Comment thread
don-petry marked this conversation as resolved.

# step_labels <repo> — apply the standard label set (best-effort, idempotent).
Expand Down
61 changes: 56 additions & 5 deletions tests/test_apply_rulesets.bats
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,31 @@

SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
APPLY="$SCRIPT_DIR/scripts/apply-rulesets.sh"
# This repo's LOCAL ruleset dir — holds only release-channel-tags.json since the
# fleet rulesets (code-quality, pr-quality) were relocated to petry-projects/.github
# under #575. Passed explicitly via RULESETS_DIR to apply the repo-local ruleset;
# with RULESETS_DIR unset the applier is in fleet mode (materializes from .github).
RULESETS_DIR="$SCRIPT_DIR/.github/rulesets"

setup() {
STUB_BIN="$(mktemp -d)"; export PATH="$STUB_BIN:$PATH"
CALLS="$STUB_BIN/calls.log"; export CALLS
}
teardown() { [ -n "${STUB_BIN:-}" ] && rm -rf "$STUB_BIN"; return 0; }
teardown() {
[ -n "${STUB_BIN:-}" ] && rm -rf "$STUB_BIN"
[ -n "${FLEET_DIR:-}" ] && rm -rf "$FLEET_DIR"
return 0
}

# _fleet_fixture — a stand-in for petry-projects/.github standards/rulesets/. The
# authoritative fleet JSON content lives there (relocated under #575); this fixture
# only needs valid .name fields to exercise fleet-mode resolution + apply.
_fleet_fixture() {
FLEET_DIR="$(mktemp -d)"
printf '{"name":"code-quality","target":"branch","enforcement":"active"}\n' > "$FLEET_DIR/code-quality.json"
printf '{"name":"pr-quality","target":"branch","enforcement":"active"}\n' > "$FLEET_DIR/pr-quality.json"
export FLEET_RULESETS_DIR="$FLEET_DIR"
}

# gh stub: records every write (POST/PUT) to $CALLS; for the rulesets LIST it
# returns $RULESETS_LIST (default empty array → "create" path).
Expand Down Expand Up @@ -51,7 +69,7 @@ EOF
@test "apply: creates the ruleset when absent (POST)" {
_stub_gh
export RULESETS_LIST='[]'
run env RULESETS_REPO="petry-projects/.github-private" bash "$APPLY" release-channel-tags
run env RULESETS_DIR="$RULESETS_DIR" RULESETS_REPO="petry-projects/.github-private" bash "$APPLY" release-channel-tags
[ "$status" -eq 0 ]
grep -q "method POST" "$CALLS"
! grep -q "method PUT" "$CALLS"
Expand All @@ -60,7 +78,7 @@ EOF
@test "apply: updates the ruleset when present (PUT by id)" {
_stub_gh
export RULESETS_LIST='[{"id":17432201,"name":"release-channel-tags"}]'
run env RULESETS_REPO="petry-projects/.github-private" bash "$APPLY" release-channel-tags
run env RULESETS_DIR="$RULESETS_DIR" RULESETS_REPO="petry-projects/.github-private" bash "$APPLY" release-channel-tags
[ "$status" -eq 0 ]
grep -q "method PUT" "$CALLS"
grep -q "rulesets/17432201" "$CALLS"
Expand All @@ -70,15 +88,15 @@ EOF
@test "apply: --dry-run makes no write calls" {
_stub_gh
export RULESETS_LIST='[]'
run env RULESETS_REPO="petry-projects/.github-private" bash "$APPLY" --dry-run release-channel-tags
run env RULESETS_DIR="$RULESETS_DIR" RULESETS_REPO="petry-projects/.github-private" bash "$APPLY" --dry-run release-channel-tags
[ "$status" -eq 0 ]
[ ! -f "$CALLS" ]
[[ "$output" == *"dry-run"* ]]
}

@test "apply: unknown ruleset name errors" {
_stub_gh
run env RULESETS_REPO="petry-projects/.github-private" bash "$APPLY" no-such-ruleset
run env RULESETS_DIR="$RULESETS_DIR" RULESETS_REPO="petry-projects/.github-private" bash "$APPLY" no-such-ruleset
[ "$status" -ne 0 ]
}

Expand All @@ -95,3 +113,36 @@ EOF
[ "$status" -ne 0 ]
[[ "$output" == *"--repo requires a value"* ]]
}

# ── fleet mode: source code-quality + pr-quality from .github (via FLEET_RULESETS_DIR) ──
@test "fleet mode: --repo applies code-quality + pr-quality (2), never release-channel-tags" {
_stub_gh
export RULESETS_LIST='[]'
_fleet_fixture
run bash "$APPLY" --repo petry-projects/acme
[ "$status" -eq 0 ]
[[ "$output" == *"code-quality"* ]]
[[ "$output" == *"pr-quality"* ]]
[[ "$output" != *"release-channel-tags"* ]]
[[ "$output" == *"done (2 ruleset(s))"* ]]
# exactly two creates (POST), one per fleet ruleset
[ "$(grep -c 'method POST' "$CALLS" || true)" -eq 2 ]
}

@test "fleet mode: --dry-run previews both fleet rulesets and makes no writes" {
_stub_gh
export RULESETS_LIST='[]'
_fleet_fixture
run bash "$APPLY" --dry-run --repo petry-projects/acme
[ "$status" -eq 0 ]
[ ! -f "$CALLS" ]
[[ "$output" == *"code-quality"* ]]
[[ "$output" == *"pr-quality"* ]]
}

@test "fleet mode: missing FLEET_RULESETS_DIR errors clearly" {
_stub_gh
run env FLEET_RULESETS_DIR="/no/such/fleet/dir" bash "$APPLY" --repo petry-projects/acme
[ "$status" -ne 0 ]
[[ "$output" == *"FLEET_RULESETS_DIR not found"* ]]
}
Loading
Loading