From 4f5c549fd9e16c1d9a76cc22e13cab86c1a67de1 Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 26 Jul 2026 21:32:10 -0700 Subject: [PATCH] feat(rulesets): let org and repo admins bypass requirements Add bypassActors for OrganizationAdmin and repository admin on the default ruleset, and default classic branch protection to not enforce on admins so emergency merges are possible on every managed repo. --- config/rulesets.yaml | 8 ++++++++ config/schema/rulesets.schema.json | 28 ++++++++++++++++++++++++++++ src/resources/rulesets.ts | 2 ++ src/setup/resolve.ts | 2 +- src/types/config.ts | 15 +++++++++++++++ test/resolve.test.ts | 2 +- test/resources.test.ts | 22 ++++++++++++++++++++++ 7 files changed, 77 insertions(+), 2 deletions(-) diff --git a/config/rulesets.yaml b/config/rulesets.yaml index 18fa01f..ecdfe05 100644 --- a/config/rulesets.yaml +++ b/config/rulesets.yaml @@ -15,6 +15,14 @@ rulesets: name: Default branch protection target: branch enforcement: active + # Org owners + repo admins can skip required checks/reviews when needed. + bypassActors: + - actorType: OrganizationAdmin + actorId: 1 + bypassMode: always + - actorType: RepositoryRole + actorId: 5 # repository admin + bypassMode: always conditions: refName: includes: diff --git a/config/schema/rulesets.schema.json b/config/schema/rulesets.schema.json index 11aedf9..1a5182d 100644 --- a/config/schema/rulesets.schema.json +++ b/config/schema/rulesets.schema.json @@ -249,6 +249,34 @@ "requiredDeployments" ], "additionalProperties": false + }, + "bypassActors": { + "type": "array", + "description": "Actors allowed to bypass this ruleset (e.g. org/repo admins).", + "items": { + "type": "object", + "properties": { + "actorType": { + "type": "string", + "enum": [ + "RepositoryRole", + "Team", + "Integration", + "OrganizationAdmin", + "DeployKey" + ] + }, + "actorId": { + "type": "number" + }, + "bypassMode": { + "type": "string", + "enum": ["always", "pullRequest", "exempt"] + } + }, + "required": ["actorType", "bypassMode"], + "additionalProperties": false + } } }, "required": ["id", "target", "conditions", "rules"], diff --git a/src/resources/rulesets.ts b/src/resources/rulesets.ts index d1eeff5..6e21dd6 100644 --- a/src/resources/rulesets.ts +++ b/src/resources/rulesets.ts @@ -141,6 +141,7 @@ export function createRulesets( excludes: conditions.repositoryName?.excludes ?? [], }, }, + bypassActors: r.bypassActors, rules: toOrganizationRulesetRules(rules, r), }); }); @@ -173,6 +174,7 @@ export function createRepositoryRulesets( excludes: refExcludes, }, }, + bypassActors: r.bypassActors, rules: toRepositoryRulesetRules(r.rules, r), }, pulumi.mergeOptions( diff --git a/src/setup/resolve.ts b/src/setup/resolve.ts index fd13281..798dc50 100644 --- a/src/setup/resolve.ts +++ b/src/setup/resolve.ts @@ -32,7 +32,7 @@ function toBranchProtectionEntry( }; return { - enforceAdmins: config.enforceAdmins ?? true, + enforceAdmins: config.enforceAdmins ?? false, allowsDeletions: config.allowsDeletions, allowsForcePushes: config.allowsForcePushes, requireSignedCommits: config.requireSignedCommits, diff --git a/src/types/config.ts b/src/types/config.ts index dff2ed6..58fcce2 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -179,6 +179,20 @@ const RulesetConditionsSchema = v.strictObject({ ), }); +export const RulesetBypassActorSchema = v.strictObject({ + actorType: v.picklist([ + "RepositoryRole", + "Team", + "Integration", + "OrganizationAdmin", + "DeployKey", + ]), + actorId: v.optional(v.number()), + bypassMode: v.picklist(["always", "pullRequest", "exempt"]), +}); + +export type RulesetBypassActor = v.InferOutput; + export const RulesetConfigSchema = v.strictObject({ id: v.string(), name: v.optional(v.string()), @@ -189,6 +203,7 @@ export const RulesetConfigSchema = v.strictObject({ ), conditions: RulesetConditionsSchema, rules: RulesetRulesSchema, + bypassActors: v.optional(v.array(RulesetBypassActorSchema)), }); const RulesetsArraySchema = v.array(RulesetConfigSchema); diff --git a/test/resolve.test.ts b/test/resolve.test.ts index 88f834d..06ffe69 100644 --- a/test/resolve.test.ts +++ b/test/resolve.test.ts @@ -113,7 +113,7 @@ describe("buildRepoConfig", () => { expect(Object.keys(built.resolvedBranchProtection)).toEqual(["main"]); const bp = built.resolvedBranchProtection.main; if (!bp) throw new Error("expected branch protection for main"); - expect(bp.enforceAdmins).toBe(true); + expect(bp.enforceAdmins).toBe(false); // requiredPullRequestReviews is typed as a Pulumi Input union; at build // time it's a plain array, so narrow it for the assertion. const reviews = bp.requiredPullRequestReviews as Array<{ diff --git a/test/resources.test.ts b/test/resources.test.ts index de0f569..c52390c 100644 --- a/test/resources.test.ts +++ b/test/resources.test.ts @@ -332,6 +332,28 @@ describe("createRepositoryRulesets", () => { }); }); +it("passes bypassActors through to repository rulesets", async () => { + const repo = new github.Repository("r", { name: "r" }); + await settle(); + createRepositoryRulesets("website", repo, [ + mainRuleset({ + id: "with-bypass", + bypassActors: [ + { actorType: "OrganizationAdmin", actorId: 1, bypassMode: "always" }, + { actorType: "RepositoryRole", actorId: 5, bypassMode: "always" }, + ], + }), + ]); + await settle(); + const rs = first( + findByType("github:index/repositoryRuleset:RepositoryRuleset"), + ); + expect(rs.inputs.bypassActors).toEqual([ + { actorType: "OrganizationAdmin", actorId: 1, bypassMode: "always" }, + { actorType: "RepositoryRole", actorId: 5, bypassMode: "always" }, + ]); +}); + describe("createRulesets", () => { it("drops disabled rulesets", async () => { const result = createRulesets([