Skip to content

Commit e5f0c4c

Browse files
MXfivekfcampbell
andauthored
feat: add support for merge queues in repository rulesets (#2380)
* feat: add repository ruleset merge queue support * docs: add docs for repository rulesets merge queue * feat: add default values for merge_queue on rulesets * test: add merge_queue block to ruleset create and import tests * Fix: merge queue repository rules flattening * Test fixes, additional squash merge queue test case --------- Co-authored-by: Keegan Campbell <[email protected]>
1 parent 1ca7092 commit e5f0c4c

4 files changed

+212
-13
lines changed

github/resource_github_repository_ruleset.go

+59
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,65 @@ func resourceGithubRepositoryRuleset() *schema.Resource {
258258
},
259259
},
260260
},
261+
"merge_queue": {
262+
Type: schema.TypeList,
263+
MaxItems: 1,
264+
Optional: true,
265+
Description: "Merges must be performed via a merge queue.",
266+
Elem: &schema.Resource{
267+
Schema: map[string]*schema.Schema{
268+
"check_response_timeout_minutes": {
269+
Type: schema.TypeInt,
270+
Optional: true,
271+
Default: 60,
272+
ValidateDiagFunc: toDiagFunc(validation.IntBetween(0, 360), "check_response_timeout_minutes"),
273+
Description: "Maximum time for a required status check to report a conclusion. After this much time has elapsed, checks that have not reported a conclusion will be assumed to have failed. Defaults to `60`.",
274+
},
275+
"grouping_strategy": {
276+
Type: schema.TypeString,
277+
Optional: true,
278+
Default: "ALLGREEN",
279+
ValidateDiagFunc: toDiagFunc(validation.StringInSlice([]string{"ALLGREEN", "HEADGREEN"}, false), "grouping_strategy"),
280+
Description: "When set to ALLGREEN, the merge commit created by merge queue for each PR in the group must pass all required checks to merge. When set to HEADGREEN, only the commit at the head of the merge group, i.e. the commit containing changes from all of the PRs in the group, must pass its required checks to merge. Can be one of: ALLGREEN, HEADGREEN. Defaults to `ALLGREEN`.",
281+
},
282+
"max_entries_to_build": {
283+
Type: schema.TypeInt,
284+
Optional: true,
285+
Default: 5,
286+
ValidateDiagFunc: toDiagFunc(validation.IntBetween(0, 100), "max_entries_to_merge"),
287+
Description: "Limit the number of queued pull requests requesting checks and workflow runs at the same time. Defaults to `5`.",
288+
},
289+
"max_entries_to_merge": {
290+
Type: schema.TypeInt,
291+
Optional: true,
292+
Default: 5,
293+
ValidateDiagFunc: toDiagFunc(validation.IntBetween(0, 100), "max_entries_to_merge"),
294+
Description: "The maximum number of PRs that will be merged together in a group. Defaults to `5`.",
295+
},
296+
"merge_method": {
297+
Type: schema.TypeString,
298+
Optional: true,
299+
Default: "MERGE",
300+
ValidateDiagFunc: toDiagFunc(validation.StringInSlice([]string{"MERGE", "SQUASH", "REBASE"}, false), "merge_method"),
301+
Description: "Method to use when merging changes from queued pull requests. Can be one of: MERGE, SQUASH, REBASE. Defaults to `MERGE`.",
302+
},
303+
"min_entries_to_merge": {
304+
Type: schema.TypeInt,
305+
Optional: true,
306+
Default: 1,
307+
ValidateDiagFunc: toDiagFunc(validation.IntBetween(0, 100), "min_entries_to_merge"),
308+
Description: "The minimum number of PRs that will be merged together in a group. Defaults to `1`.",
309+
},
310+
"min_entries_to_merge_wait_minutes": {
311+
Type: schema.TypeInt,
312+
Optional: true,
313+
Default: 5,
314+
ValidateDiagFunc: toDiagFunc(validation.IntBetween(0, 360), "min_entries_to_merge_wait_minutes"),
315+
Description: "The time merge queue should wait after the first PR is added to the queue for the minimum group size to be met. After this time has elapsed, the minimum group size will be ignored and a smaller group will be merged. Defaults to `5`.",
316+
},
317+
},
318+
},
319+
},
261320
"non_fast_forward": {
262321
Type: schema.TypeBool,
263322
Optional: true,

github/resource_github_repository_ruleset_test.go

+100-4
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ func TestGithubRepositoryRulesets(t *testing.T) {
2020
config := fmt.Sprintf(`
2121
resource "github_repository" "test" {
2222
name = "tf-acc-test-%s"
23-
auto_init = false
23+
auto_init = true
24+
default_branch = "main"
2425
}
2526
2627
resource "github_repository_environment" "example" {
@@ -36,7 +37,7 @@ func TestGithubRepositoryRulesets(t *testing.T) {
3637
3738
conditions {
3839
ref_name {
39-
include = ["~ALL"]
40+
include = ["refs/heads/main"]
4041
exclude = []
4142
}
4243
}
@@ -55,6 +56,16 @@ func TestGithubRepositoryRulesets(t *testing.T) {
5556
5657
required_signatures = false
5758
59+
merge_queue {
60+
check_response_timeout_minutes = 10
61+
grouping_strategy = "ALLGREEN"
62+
max_entries_to_build = 5
63+
max_entries_to_merge = 5
64+
merge_method = "MERGE"
65+
min_entries_to_merge = 1
66+
min_entries_to_merge_wait_minutes = 60
67+
}
68+
5869
pull_request {
5970
required_approving_review_count = 2
6071
required_review_thread_resolution = true
@@ -270,7 +281,8 @@ func TestGithubRepositoryRulesets(t *testing.T) {
270281
resource "github_repository" "test" {
271282
name = "tf-acc-test-import-%[1]s"
272283
description = "Terraform acceptance tests %[1]s"
273-
auto_init = false
284+
auto_init = true
285+
default_branch = "main"
274286
}
275287
276288
resource "github_repository_environment" "example" {
@@ -286,7 +298,7 @@ func TestGithubRepositoryRulesets(t *testing.T) {
286298
287299
conditions {
288300
ref_name {
289-
include = ["~ALL"]
301+
include = ["refs/heads/main"]
290302
exclude = []
291303
}
292304
}
@@ -313,6 +325,16 @@ func TestGithubRepositoryRulesets(t *testing.T) {
313325
require_last_push_approval = true
314326
}
315327
328+
merge_queue {
329+
check_response_timeout_minutes = 30
330+
grouping_strategy = "HEADGREEN"
331+
max_entries_to_build = 4
332+
max_entries_to_merge = 4
333+
merge_method = "SQUASH"
334+
min_entries_to_merge = 2
335+
min_entries_to_merge_wait_minutes = 10
336+
}
337+
316338
required_status_checks {
317339
318340
required_check {
@@ -366,6 +388,80 @@ func TestGithubRepositoryRulesets(t *testing.T) {
366388

367389
})
368390

391+
t.Run("Creates repository ruleset with merge queue SQUASH method", func(t *testing.T) {
392+
393+
config := fmt.Sprintf(`
394+
resource "github_repository" "test" {
395+
name = "tf-acc-test-merge-queue-%s"
396+
auto_init = true
397+
default_branch = "main"
398+
}
399+
400+
resource "github_repository_ruleset" "test" {
401+
name = "merge-queue-test"
402+
repository = github_repository.test.id
403+
target = "branch"
404+
enforcement = "active"
405+
406+
conditions {
407+
ref_name {
408+
include = ["refs/heads/main"]
409+
exclude = []
410+
}
411+
}
412+
413+
rules {
414+
merge_queue {
415+
check_response_timeout_minutes = 30
416+
grouping_strategy = "HEADGREEN"
417+
max_entries_to_build = 4
418+
max_entries_to_merge = 4
419+
merge_method = "SQUASH"
420+
min_entries_to_merge = 2
421+
min_entries_to_merge_wait_minutes = 10
422+
}
423+
}
424+
}
425+
`, randomID)
426+
427+
check := resource.ComposeTestCheckFunc(
428+
resource.TestCheckResourceAttr(
429+
"github_repository_ruleset.test", "name",
430+
"merge-queue-test",
431+
),
432+
resource.TestCheckResourceAttr(
433+
"github_repository_ruleset.test", "rules.0.merge_queue.0.merge_method",
434+
"SQUASH",
435+
),
436+
)
437+
438+
testCase := func(t *testing.T, mode string) {
439+
resource.Test(t, resource.TestCase{
440+
PreCheck: func() { skipUnlessMode(t, mode) },
441+
Providers: testAccProviders,
442+
Steps: []resource.TestStep{
443+
{
444+
Config: config,
445+
Check: check,
446+
},
447+
},
448+
})
449+
}
450+
451+
t.Run("with an anonymous account", func(t *testing.T) {
452+
t.Skip("anonymous account not supported for this operation")
453+
})
454+
455+
t.Run("with an individual account", func(t *testing.T) {
456+
testCase(t, individual)
457+
})
458+
459+
t.Run("with an organization account", func(t *testing.T) {
460+
testCase(t, organization)
461+
})
462+
463+
})
464+
369465
}
370466

371467
func importRepositoryRulesetByResourcePaths(repoLogicalName, rulesetLogicalName string) resource.ImportStateIdFunc {

github/respository_rules_utils.go

+35
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,22 @@ func expandRules(input []interface{}, org bool) []*github.RepositoryRule {
300300
rulesSlice = append(rulesSlice, github.NewPullRequestRule(params))
301301
}
302302

303+
// Merge queue rule
304+
if v, ok := rulesMap["merge_queue"].([]interface{}); ok && len(v) != 0 {
305+
mergeQueueMap := v[0].(map[string]interface{})
306+
params := &github.MergeQueueRuleParameters{
307+
CheckResponseTimeoutMinutes: mergeQueueMap["check_response_timeout_minutes"].(int),
308+
GroupingStrategy: mergeQueueMap["grouping_strategy"].(string),
309+
MaxEntriesToBuild: mergeQueueMap["max_entries_to_build"].(int),
310+
MaxEntriesToMerge: mergeQueueMap["max_entries_to_merge"].(int),
311+
MergeMethod: mergeQueueMap["merge_method"].(string),
312+
MinEntriesToMerge: mergeQueueMap["min_entries_to_merge"].(int),
313+
MinEntriesToMergeWaitMinutes: mergeQueueMap["min_entries_to_merge_wait_minutes"].(int),
314+
}
315+
316+
rulesSlice = append(rulesSlice, github.NewMergeQueueRule(params))
317+
}
318+
303319
// Required status checks rule
304320
if v, ok := rulesMap["required_status_checks"].([]interface{}); ok && len(v) != 0 {
305321
requiredStatusMap := v[0].(map[string]interface{})
@@ -507,6 +523,25 @@ func flattenRules(rules []*github.RepositoryRule, org bool) []interface{} {
507523
rule["strict_required_status_checks_policy"] = params.StrictRequiredStatusChecksPolicy
508524
rule["do_not_enforce_on_create"] = params.DoNotEnforceOnCreate
509525
rulesMap[v.Type] = []map[string]interface{}{rule}
526+
527+
case "merge_queue":
528+
var params github.MergeQueueRuleParameters
529+
530+
err := json.Unmarshal(*v.Parameters, &params)
531+
if err != nil {
532+
log.Printf("[INFO] Unexpected error unmarshalling rule %s with parameters: %v",
533+
v.Type, v.Parameters)
534+
}
535+
536+
rule := make(map[string]interface{})
537+
rule["check_response_timeout_minutes"] = params.CheckResponseTimeoutMinutes
538+
rule["grouping_strategy"] = params.GroupingStrategy
539+
rule["max_entries_to_build"] = params.MaxEntriesToBuild
540+
rule["max_entries_to_merge"] = params.MaxEntriesToMerge
541+
rule["merge_method"] = params.MergeMethod
542+
rule["min_entries_to_merge"] = params.MinEntriesToMerge
543+
rule["min_entries_to_merge_wait_minutes"] = params.MinEntriesToMergeWaitMinutes
544+
rulesMap[v.Type] = []map[string]interface{}{rule}
510545
}
511546
}
512547

website/docs/r/repository_ruleset.html.markdown

+18-9
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ resource "github_repository_ruleset" "example" {
7474

7575
The `rules` block supports the following:
7676

77-
7877
* `branch_name_pattern` - (Optional) (Block List, Max: 1) Parameters to be used for the branch_name_pattern rule. This rule only applies to repositories within an enterprise, it cannot be applied to repositories owned by individuals or regular organizations. Conflicts with `tag_name_pattern` as it only applied to rulesets with target `branch`. (see [below for nested schema](#rules.branch_name_pattern))
7978

8079
* `commit_author_email_pattern` - (Optional) (Block List, Max: 1) Parameters to be used for the commit_author_email_pattern rule. This rule only applies to repositories within an enterprise, it cannot be applied to repositories owned by individuals or regular organizations. (see [below for nested schema](#rules.commit_author_email_pattern))
@@ -89,6 +88,8 @@ The `rules` block supports the following:
8988

9089
* `non_fast_forward` - (Optional) (Boolean) Prevent users with push access from force pushing to branches.
9190

91+
* `merge_queue` - (Optional) (Block List, Max: 1) Merges must be performed via a merge queue.
92+
9293
* `pull_request` - (Optional) (Block List, Max: 1) Require all commits be made to a non-target branch and submitted via a pull request before they can be merged. (see [below for nested schema](#rules.pull_request))
9394

9495
* `required_deployments` - (Optional) (Block List, Max: 1) Choose which environments must be successfully deployed to before branches can be merged into a branch that matches this rule. (see [below for nested schema](#rules.required_deployments))
@@ -117,7 +118,6 @@ The `rules` block supports the following:
117118

118119
* `negate` - (Optional) (Boolean) If true, the rule will fail if the pattern matches.
119120

120-
121121
#### rules.commit_author_email_pattern ####
122122

123123
* `operator` - (Required) (String) The operator to use for matching. Can be one of: `starts_with`, `ends_with`, `contains`, `regex`.
@@ -128,7 +128,6 @@ The `rules` block supports the following:
128128

129129
* `negate` - (Optional) (Boolean) If true, the rule will fail if the pattern matches.
130130

131-
132131
#### rules.commit_message_pattern ####
133132

134133
* `operator` - (Required) (String) The operator to use for matching. Can be one of: `starts_with`, `ends_with`, `contains`, `regex`.
@@ -139,7 +138,6 @@ The `rules` block supports the following:
139138

140139
* `negate` - (Optional) (Boolean) If true, the rule will fail if the pattern matches.
141140

142-
143141
#### rules.committer_email_pattern ####
144142

145143
* `operator` - (Required) (String) The operator to use for matching. Can be one of: `starts_with`, `ends_with`, `contains`, `regex`.
@@ -150,6 +148,21 @@ The `rules` block supports the following:
150148

151149
* `negate` - (Optional) (Boolean) If true, the rule will fail if the pattern matches.
152150

151+
#### rules.merge_queue ####
152+
153+
* `check_response_timeout_minutes` - (Required) (Number)Maximum time for a required status check to report a conclusion. After this much time has elapsed, checks that have not reported a conclusion will be assumed to have failed. Defaults to `60`.
154+
155+
* `grouping_strategy` - (Required) (String)When set to ALLGREEN, the merge commit created by merge queue for each PR in the group must pass all required checks to merge. When set to HEADGREEN, only the commit at the head of the merge group, i.e. the commit containing changes from all of the PRs in the group, must pass its required checks to merge. Can be one of: ALLGREEN, HEADGREEN. Defaults to `ALLGREEN`.
156+
157+
* `max_entries_to_build` - (Required) (Number) Limit the number of queued pull requests requesting checks and workflow runs at the same time. Defaults to `5`.
158+
159+
* `max_entries_to_merge` - (Required) (Number) Limit the number of queued pull requests requesting checks and workflow runs at the same time. Defaults to `5`.
160+
161+
* `merge_method` - (Required) (String) Method to use when merging changes from queued pull requests. Can be one of: MERGE, SQUASH, REBASE. Defaults to `MERGE`.
162+
163+
* `min_entries_to_merge` - (Required) (Number) The minimum number of PRs that will be merged together in a group. Defaults to `1`.
164+
165+
* `min_entries_to_merge_wait_minutes` - (Required) (Number) The time merge queue should wait after the first PR is added to the queue for the minimum group size to be met. After this time has elapsed, the minimum group size will be ignored and a smaller group will be merged. Defaults to `5`.
153166

154167
#### rules.pull_request ####
155168

@@ -163,12 +176,10 @@ The `rules` block supports the following:
163176

164177
* `required_review_thread_resolution` - (Optional) (Boolean) All conversations on code must be resolved before a pull request can be merged. Defaults to `false`.
165178

166-
167179
#### rules.required_deployments ####
168180

169181
* `required_deployment_environments` - (Required) (List of String) The environments that must be successfully deployed to before branches can be merged.
170182

171-
172183
#### rules.required_status_checks ####
173184

174185
* `required_check` - (Required) (Block Set, Min: 1) Status checks that are required. Several can be defined. (see [below for nested schema](#rules.required_status_checks.required_check))
@@ -214,13 +225,13 @@ The `rules` block supports the following:
214225
* `bypass_mode` - (Optional) (String) When the specified actor can bypass the ruleset. pull_request means that an actor can only bypass rules on pull requests. Can be one of: `always`, `pull_request`.
215226

216227
~> Note: at the time of writing this, the following actor types correspond to the following actor IDs:
228+
217229
* `OrganizationAdmin` -> `1`
218230
* `RepositoryRole` (This is the actor type, the following are the base repository roles and their associated IDs.)
219231
* `maintain` -> `2`
220232
* `write` -> `4`
221233
* `admin` -> `5`
222234

223-
224235
#### conditions ####
225236

226237
* `ref_name` - (Required) (Block List, Min: 1, Max: 1) (see [below for nested schema](#conditions.ref_name))
@@ -235,14 +246,12 @@ The `rules` block supports the following:
235246

236247
The following additional attributes are exported:
237248

238-
239249
* `etag` (String)
240250

241251
* `node_id` (String) GraphQL global node id for use with v4 API.
242252

243253
* `ruleset_id` (Number) GitHub ID for the ruleset.
244254

245-
246255
## Import
247256

248257
GitHub Repository Rulesets can be imported using the GitHub repository name and ruleset ID e.g.

0 commit comments

Comments
 (0)