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
18 changes: 9 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Read the relevant standard *before* making changes that touch CI, repo settings,
| **Workflow templates** | [`standards/workflows/`](https://github.com/petry-projects/.github/tree/main/standards/workflows) | Copy-paste-ready templates: `agent-shield.yml`, `dev-lead.yml`, `dependabot-automerge.yml`, `dependabot-rebase.yml`, `dependency-audit.yml`, `feature-ideation.yml` |
| **Agent configuration** | [`standards/agent-standards.md`](https://github.com/petry-projects/.github/blob/main/standards/agent-standards.md) | CLAUDE.md / AGENTS.md / SKILL.md required structure, frontmatter rules, cross-references |
| **Repo settings + labels** | [`standards/github-settings.md`](https://github.com/petry-projects/.github/blob/main/standards/github-settings.md) | Required settings, label set with exact colors, code-quality ruleset, branch protection |
| **Rulesets** | [`standards/rulesets/`](https://github.com/petry-projects/.github/tree/main/standards/rulesets) | Codified source-of-truth JSON for the org-wide `pr-quality` + `code-quality` rulesets (applied by `apply-rulesets.sh`). `release-channel-tags` stays repo-local in `.github-private` |
| **Rulesets** | [`standards/rulesets/`](https://github.com/petry-projects/.github/tree/main/standards/rulesets) | Codified source-of-truth JSON for the org-wide `pr-quality`, `code-quality`, and `release-channel-tags` rulesets (applied by `apply-rulesets.sh`) |
| **Dependabot config** | [`standards/dependabot-policy.md`](https://github.com/petry-projects/.github/blob/main/standards/dependabot-policy.md) and [`standards/dependabot/`](https://github.com/petry-projects/.github/tree/main/standards/dependabot) | Per-ecosystem dependabot.yml templates and policy |
| **Advanced Security (GHAS)** | [`standards/advanced-security.md`](https://github.com/petry-projects/.github/blob/main/standards/advanced-security.md) | Org-wide GHAS enablement via the "GitHub recommended" code security configuration, licensing model, and how to verify push protection actually enforces |
| **Push protection** | [`standards/push-protection.md`](https://github.com/petry-projects/.github/blob/main/standards/push-protection.md) | Secret scanning + push protection, local gitleaks hooks, CI secret-scan job, incident response |
Expand All @@ -42,14 +42,14 @@ ruleset JSONs is `standards/rulesets/`.

**`petry-projects/.github-private` is scoped to agents, skills, and their reusable
workflows/assets.** It must **not** be the source of truth for org-wide policy.
The only ruleset that legitimately lives there is `release-channel-tags` — it
protects `.github-private`'s own `pr-review/**` and `dev-lead/**` agent-release tags.

**Rule of thumb:** if a standard or ruleset applies to the whole fleet, it belongs
in `.github/standards/`. If it protects an agent's/skill's own assets, it stays in
`.github-private`. When in doubt, put it in `.github` and have `.github-private`
**consume** it — the way repo-seeding tooling fetches `standards/workflows/` from
`.github`. Do **not** add new org-wide standards or rulesets to `.github-private`.
No rulesets should be defined there; all rulesets are owned by `.github/standards/rulesets/`.

**Rule of thumb:** all standards and rulesets belong in `.github/standards/`, whether
they apply fleet-wide (like `pr-quality` and `code-quality`) or are targeted to specific
repos (like `release-channel-tags` for meta-repos). When in doubt, put it in `.github`
and have `.github-private` **consume** it — the way repo-seeding tooling fetches
`standards/workflows/` from `.github`. Do **not** add new org-wide standards or rulesets
to `.github-private`.

> **Known exception being remediated:** `code-quality.json` and `pr-quality.json`
> currently live in `.github-private/.github/rulesets/` (added ad-hoc under
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 21 additions & 11 deletions scripts/apply-rulesets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ set -euo pipefail
# Rulesets live ON each repo: editing a JSON here changes the desired state, not any
# live ruleset, until this applier runs.
#
# Default set: with NO ruleset name, only the FLEET_RULESETS allowlist (code-quality,
# pr-quality) is applied — NOT every *.json in the dir. standards/rulesets/ also holds
# release-channel-tags.json (a tag ruleset protecting the reusable-hosting meta-repos'
# channel/version tags); it is targeted, not fleet-wide, so it is applied ONLY when
# named explicitly: ./scripts/apply-rulesets.sh --repo petry-projects/.github release-channel-tags
#
# Usage:
# GH_TOKEN=<admin> ./scripts/apply-rulesets.sh --repo owner/repo [--dry-run] [<name>...]
# GH_TOKEN=<admin> ./scripts/apply-rulesets.sh <repo-name> [--dry-run] # bare name → $ORG/<name>
Expand All @@ -42,6 +48,13 @@ ORG="${ORG:-petry-projects}"
RULESETS_DIR="${RULESETS_DIR:-$(cd "${SCRIPT_DIR}/.." && pwd)/standards/rulesets}"
DRY_RUN="${DRY_RUN:-false}"

# The fleet-wide compliance rulesets applied by DEFAULT (when no ruleset name is
# given), on --repo and --all alike. This is an ALLOWLIST, not "every *.json in the
# dir": standards/rulesets/ also holds release-channel-tags.json, which is a targeted
# tag ruleset for the reusable-hosting meta-repos only and must NOT be swept fleet-
# wide — it is applied only when named explicitly (e.g. `--repo <meta> release-channel-tags`).
readonly FLEET_RULESETS=(code-quality pr-quality)

# 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 @@ -123,18 +136,15 @@ main() {
gh auth status >/dev/null 2>&1 || { echo "::error::GitHub authentication failed — set GH_TOKEN or run 'gh auth login'" >&2; return 1; }
[ -d "$RULESETS_DIR" ] || { echo "::error::rulesets dir not found: $RULESETS_DIR" >&2; return 1; }

# Select the ruleset files (all *.json, or only the named ones).
# Select the ruleset files: the named ones, else the fleet allowlist (NOT every
# *.json — that would sweep release-channel-tags fleet-wide; see FLEET_RULESETS).
local files=()
if [ "${#names[@]}" -gt 0 ]; then
local n
for n in "${names[@]}"; do
[ -f "${RULESETS_DIR}/${n}.json" ] && files+=("${RULESETS_DIR}/${n}.json") \
|| { echo "::error::no ruleset file ${n}.json in ${RULESETS_DIR}" >&2; return 1; }
done
else
local f
for f in "${RULESETS_DIR}"/*.json; do [ -e "$f" ] && files+=("$f"); done
fi
if [ "${#names[@]}" -eq 0 ]; then names=("${FLEET_RULESETS[@]}"); fi
local n
for n in "${names[@]}"; do
[ -f "${RULESETS_DIR}/${n}.json" ] && files+=("${RULESETS_DIR}/${n}.json") \
|| { echo "::error::no ruleset file ${n}.json in ${RULESETS_DIR}" >&2; return 1; }
done
[ "${#files[@]}" -gt 0 ] || { echo " no ruleset files to apply"; return 0; }

if [ "$all" = true ]; then
Expand Down
23 changes: 10 additions & 13 deletions standards/github-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,13 @@ rules are deprecated — migrate existing classic rules to rulesets.

### Source of truth & repo boundary

The codified ruleset JSONs are the source of truth for the two sanctioned
rulesets; `scripts/apply-rulesets.sh` (and any org automation consuming it) applies them to each repo.
The codified ruleset JSONs are the source of truth for all rulesets;
`scripts/apply-rulesets.sh` (and any org automation consuming it) applies them to each repo.
As **org-wide compliance policy they are owned by `petry-projects/.github`**, and
their canonical home is `standards/rulesets/`. Do **not** author them in
their canonical home is `standards/rulesets/`. Do **not** author rulesets in
`petry-projects/.github-private` — that repo is scoped to agents/skills and their
reusable assets. The only ruleset that belongs there is `release-channel-tags`
(it protects `.github-private`'s own `pr-review/**` / `dev-lead/**` release tags).
reusable assets. All rulesets, including targeted ones like `release-channel-tags`,
are defined in `.github/standards/rulesets/`.
Comment on lines +128 to +134

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider scoping the "only sanctioned rulesets" line for clarity.

The unchanged sentence above ("pr-quality and code-quality are the only sanctioned rulesets") sits in the same section that now lists release-channel-tags as a third codified ruleset (lines 155-159). It's technically about branch-protection consolidation (tags aren't in scope there), but as written it reads as contradicting the new content just below it.

✏️ Suggested clarification (outside reviewed range, at line 142)
-**`pr-quality` and `code-quality` are the only sanctioned rulesets.** Legacy
+**`pr-quality` and `code-quality` are the only sanctioned *branch-protection* rulesets.** Legacy
 `protect-branches` rulesets and ad-hoc `main` rulesets are deprecated: they

Also applies to: 155-159

🧰 Tools
🪛 LanguageTool

[uncategorized] ~134-~134: The official name of this software platform is spelled with a capital “H”.
Context: ... release-channel-tags, are defined in .github/standards/rulesets/. See the repo-boun...

(GITHUB)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@standards/github-settings.md` around lines 128 - 134, The “only sanctioned
rulesets” statement in github-settings.md is now ambiguous because the section
also introduces release-channel-tags as a separate codified ruleset. Update the
surrounding wording in this section to clearly scope that claim to
branch-protection rulesets only, using the existing ruleset names referenced
here (`pr-quality`, `code-quality`, and `release-channel-tags`) so it no longer
reads like a contradiction.

See the repo-boundary rule in [`AGENTS.md`](../AGENTS.md).

> **In transit:** `code-quality.json` and `pr-quality.json` currently still live
Expand All @@ -152,14 +152,11 @@ silently drops a merge gate. `check_legacy_rulesets()` in
legacy ruleset and reports the exact migration delta (checks to move into
`code-quality` first, or "safe to delete").

**Source of truth.** The codified `pr-quality` and `code-quality` ruleset JSONs
live in this repo at [`standards/rulesets/`](rulesets/) — `.github` owns org-wide
compliance policy (see [`AGENTS.md`](../AGENTS.md#organization-standards) for the
repo-boundary rule, codified in #576). Run `apply-rulesets.sh` to converge each
repo's live ruleset to the desired state documented here. The one ruleset that stays in
`petry-projects/.github-private` is `release-channel-tags` — it protects that
repo's own `pr-review/**` and `dev-lead/**` agent-release tags and is therefore
correctly repo-local.
**Source of truth.** All ruleset JSONs — `pr-quality`, `code-quality`, and
`release-channel-tags` — live in this repo at [`standards/rulesets/`](rulesets/).
`.github` owns org-wide compliance policy (see [`AGENTS.md`](../AGENTS.md#organization-standards)
for the repo-boundary rule, codified in #576). Run `apply-rulesets.sh` to converge each
repo's live ruleset to the desired state documented here.

> **Remediating ruleset findings is a manual, admin-token procedure** —
> `compliance-remediate.sh` skips the `rulesets` category. Follow the
Expand Down
37 changes: 31 additions & 6 deletions standards/rulesets/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Repository Rulesets — codified source of truth

> **These are org-level policy, applied per-repo.** Every JSON here is an
> **organization-level** ruleset in intent — including `release-channel-tags`, whose
> bypass actors are org-scoped identities (`OrganizationAdmin` + org-installed Apps).
> GitHub's native **org-level rulesets require the Team plan**; `petry-projects` is on
> Free (`GET /orgs/petry-projects/rulesets` → 403 *"Upgrade to GitHub Team"*), so
> `scripts/apply-rulesets.sh` **replicates each definition onto its target repos** as
> repo-level rulesets. That per-repo projection is our stand-in for the paywalled
> feature — do not mistake these for repo-specific config. If the org moves to Team,
> the simplification is to define each once at `/orgs/{org}/rulesets` (with
> repository-targeting conditions) and retire the replication loop.

This directory holds the **org-wide compliance ruleset JSONs** for `petry-projects`.
`.github` owns org-wide standards and compliance policy; its canonical home is
`standards/`. See [`AGENTS.md`](../../AGENTS.md#organization-standards) for the
Expand All @@ -11,9 +22,13 @@ these rulesets are enforced.
|------|---------|--------|----------|
| [`pr-quality.json`](pr-quality.json) | `pr-quality` | `~DEFAULT_BRANCH` | 1 approval, code-owner review, thread resolution, dismiss-stale, squash-only merge |
| [`code-quality.json`](code-quality.json) | `code-quality` | `~DEFAULT_BRANCH` | Required status checks (SonarCloud, CodeQL, agent-shield, dependency-audit) |
| [`release-channel-tags.json`](release-channel-tags.json) | `release-channel-tags` | `refs/tags/**` | Blocks unauthorized **update/deletion** of any tag (protects moving channel pointers + version tags); creation stays free |

Both carry the two mandatory bypass actors — `OrganizationAdmin` and the
`dependabot-automerge-petry` Integration app (id `3167543`), both `bypass_mode: always`.
`pr-quality` / `code-quality` carry the two mandatory bypass actors —
`OrganizationAdmin` and the `dependabot-automerge-petry` Integration app (id
`3167543`), both `bypass_mode: always`. `release-channel-tags` additionally grants
bypass to the **release-manager** Integration app (id `4193127`) so channel promotion
and dev-lead autocut can move channel tags.

## Applying

Expand All @@ -29,10 +44,20 @@ GH_TOKEN=<admin-token> bash scripts/apply-rulesets.sh <repo> --dry-run

## Scope boundary

- **Fleet-wide → here.** `pr-quality` / `code-quality` are org-wide policy.
- **Protects an agent/skill's own assets → stays in `.github-private`.**
`release-channel-tags` lives in `petry-projects/.github-private` because it
protects that repo's own `pr-review/**` and `dev-lead/**` release tags.
- **Org standards → here.** All ruleset definitions are org standards owned by
`.github`. `pr-quality` / `code-quality` are applied **fleet-wide** (the default
set); `release-channel-tags` is an org standard but **targeted** — applied only to
the reusable-hosting meta-repos (`.github`, `.github-private`), whose tags are all
release-management tags. It is applied by name, never swept fleet-wide:
Comment thread
don-petry marked this conversation as resolved.

```bash
GH_TOKEN=<admin> bash scripts/apply-rulesets.sh --repo petry-projects/.github release-channel-tags
GH_TOKEN=<admin> bash scripts/apply-rulesets.sh --repo petry-projects/.github-private release-channel-tags
```

- **The default (no-name) set is the `FLEET_RULESETS` allowlist**, *not* every
`*.json` here — so adding a targeted ruleset like `release-channel-tags` never
leaks into `--all` / `--repo` fleet runs.

## Changing the required-check set

Expand Down
31 changes: 31 additions & 0 deletions standards/rulesets/release-channel-tags.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "release-channel-tags",
"target": "tag",
"enforcement": "active",
"bypass_actors": [
{
"actor_type": "OrganizationAdmin",
"bypass_mode": "always"
},
{
"actor_id": 3167543,
"actor_type": "Integration",
"bypass_mode": "always"
},
{
"actor_id": 4193127,
"actor_type": "Integration",
"bypass_mode": "always"
}
],
"conditions": {
"ref_name": {
"include": ["refs/tags/**"],
"exclude": []
}
},
"rules": [
{"type": "update"},
{"type": "deletion"}
]
}
32 changes: 32 additions & 0 deletions test/scripts/apply-rulesets/apply-rulesets.bats
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ EOF
done
}

@test "release-channel-tags.json: tag ruleset, generic refs/tags/** include, update+deletion only" {
run jq -er '.target == "tag"
and (.conditions.ref_name.include == ["refs/tags/**"])
and ([.rules[].type] | sort == ["deletion","update"])' "$RULESETS_DIR/release-channel-tags.json"
[ "$status" -eq 0 ]
}

@test "release-channel-tags.json: carries OrgAdmin + both Integration apps (automation 3167543 + release-manager 4193127)" {
run jq -er '([.bypass_actors[] | select(.actor_type=="Integration") | .actor_id] | sort == [3167543,4193127])
and ([.bypass_actors[].actor_type] | index("OrganizationAdmin"))' "$RULESETS_DIR/release-channel-tags.json"
[ "$status" -eq 0 ]
}

# ── apply behavior: create / update / dry-run ─────────────────────────────────
@test "apply --repo: creates rulesets when absent (POST per file)" {
_stub_gh
Expand Down Expand Up @@ -97,6 +110,25 @@ EOF
[[ "$output" == *"done (1 ruleset(s))"* ]]
}

@test "apply: default set is the fleet allowlist only — release-channel-tags is NOT swept in" {
_stub_gh
export RULESETS_LIST='[]'
run bash "$APPLY" --repo petry-projects/acme
[ "$status" -eq 0 ]
# code-quality + pr-quality applied; release-channel-tags excluded despite being in the dir.
[[ "$output" == *"done (2 ruleset(s))"* ]]
[[ "$output" != *"release-channel-tags"* ]]
}

@test "apply: release-channel-tags applies only when named explicitly" {
_stub_gh
export RULESETS_LIST='[]'
run bash "$APPLY" --repo petry-projects/.github release-channel-tags --dry-run
[ "$status" -eq 0 ]
[[ "$output" == *"release-channel-tags"* ]]
[[ "$output" == *"done (1 ruleset(s))"* ]]
}

@test "apply: unknown ruleset name errors" {
_stub_gh
run bash "$APPLY" --repo petry-projects/acme no-such-ruleset
Expand Down
Loading