Skip to content

Commit 86f830e

Browse files
authored
Merge branch 'main' into support-workflow-permissions
2 parents 7c39716 + d63ad78 commit 86f830e

8 files changed

+279
-17
lines changed

github/apps.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func GenerateOAuthTokenFromApp(baseURL, appID, appInstallationID, pemData string
3131
}
3232

3333
func getInstallationAccessToken(baseURL string, jwt string, installationID string) (string, error) {
34-
if baseURL != "https://api.github.com/" {
34+
if baseURL != "https://api.github.com/" && !GHECDataResidencyMatch.MatchString(baseURL) {
3535
baseURL += "api/v3/"
3636
}
3737

github/config.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net/http"
66
"net/url"
77
"path"
8+
"regexp"
89
"strings"
910
"time"
1011

@@ -36,6 +37,10 @@ type Owner struct {
3637
IsOrganization bool
3738
}
3839

40+
// GHECDataResidencyMatch is a regex to match a GitHub Enterprise Cloud data residency URL:
41+
// https://[hostname].ghe.com instances expect paths that behave similar to GitHub.com, not GitHub Enterprise Server.
42+
var GHECDataResidencyMatch = regexp.MustCompile(`^https:\/\/[a-zA-Z0-9.\-]*\.ghe\.com$`)
43+
3944
func RateLimitedHTTPClient(client *http.Client, writeDelay time.Duration, readDelay time.Duration, retryDelay time.Duration, parallelRequests bool, retryableErrors map[int]bool, maxRetries int) *http.Client {
4045

4146
client.Transport = NewEtagTransport(client.Transport)
@@ -80,7 +85,7 @@ func (c *Config) NewGraphQLClient(client *http.Client) (*githubv4.Client, error)
8085
return nil, err
8186
}
8287

83-
if uv4.String() != "https://api.github.com/" {
88+
if uv4.String() != "https://api.github.com/" && !GHECDataResidencyMatch.MatchString(uv4.String()) {
8489
uv4.Path = path.Join(uv4.Path, "api/graphql/")
8590
} else {
8691
uv4.Path = path.Join(uv4.Path, "graphql")
@@ -96,7 +101,7 @@ func (c *Config) NewRESTClient(client *http.Client) (*github.Client, error) {
96101
return nil, err
97102
}
98103

99-
if uv3.String() != "https://api.github.com/" {
104+
if uv3.String() != "https://api.github.com/" && !GHECDataResidencyMatch.MatchString(uv3.String()) {
100105
uv3.Path = uv3.Path + "api/v3/"
101106
}
102107

github/config_test.go

+58
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,64 @@ import (
77
"github.com/shurcooL/githubv4"
88
)
99

10+
func TestGHECDataResidencyMatch(t *testing.T) {
11+
testCases := []struct {
12+
url string
13+
matches bool
14+
description string
15+
}{
16+
{
17+
url: "https://customer.ghe.com",
18+
matches: true,
19+
description: "GHEC data residency URL with customer name",
20+
},
21+
{
22+
url: "https://customer-name.ghe.com",
23+
matches: true,
24+
description: "GHEC data residency URL with hyphenated name",
25+
},
26+
{
27+
url: "https://api.github.com",
28+
matches: false,
29+
description: "GitHub.com API URL",
30+
},
31+
{
32+
url: "https://github.com",
33+
matches: false,
34+
description: "GitHub.com URL",
35+
},
36+
{
37+
url: "https://example.com",
38+
matches: false,
39+
description: "Generic URL",
40+
},
41+
{
42+
url: "http://customer.ghe.com",
43+
matches: false,
44+
description: "Non-HTTPS GHEC URL",
45+
},
46+
{
47+
url: "https://customer.ghe.com/api/v3",
48+
matches: false,
49+
description: "GHEC URL with path",
50+
},
51+
{
52+
url: "https://ghe.com",
53+
matches: false,
54+
description: "GHEC domain without subdomain",
55+
},
56+
}
57+
58+
for _, tc := range testCases {
59+
t.Run(tc.description, func(t *testing.T) {
60+
matches := GHECDataResidencyMatch.MatchString(tc.url)
61+
if matches != tc.matches {
62+
t.Errorf("URL %q: expected match=%v, got %v", tc.url, tc.matches, matches)
63+
}
64+
})
65+
}
66+
}
67+
1068
func TestAccConfigMeta(t *testing.T) {
1169

1270
// FIXME: Skip test runs during travis lint checking

github/resource_github_actions_repository_oidc_subject_claim_customization_template.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateRead(d
9595
template, _, err := client.Actions.GetRepoOIDCSubjectClaimCustomTemplate(ctx, owner, repository)
9696

9797
if err != nil {
98-
return err
98+
return deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "actions repository oidc subject claim customization template (%s, %s)", owner, repository)
9999
}
100100

101101
if err = d.Set("repository", repository); err != nil {

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

0 commit comments

Comments
 (0)