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([