Skip to content

Commit 164df45

Browse files
Merge branch 'main' into fix/importing-rulesets
2 parents ac0304d + d63ad78 commit 164df45

8 files changed

+282
-20
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

+102-6
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ func TestGithubRepositoryRulesets(t *testing.T) {
2020
config := fmt.Sprintf(`
2121
resource "github_repository" "test" {
2222
name = "tf-acc-test-%s"
23-
auto_init = false
24-
vulnerability_alerts = true
23+
auto_init = true
24+
default_branch = "main"
25+
vulnerability_alerts = true
2526
}
2627
2728
resource "github_repository_environment" "example" {
@@ -37,7 +38,7 @@ func TestGithubRepositoryRulesets(t *testing.T) {
3738
3839
conditions {
3940
ref_name {
40-
include = ["~ALL"]
41+
include = ["refs/heads/main"]
4142
exclude = []
4243
}
4344
}
@@ -56,6 +57,16 @@ func TestGithubRepositoryRulesets(t *testing.T) {
5657
5758
required_signatures = false
5859
60+
merge_queue {
61+
check_response_timeout_minutes = 10
62+
grouping_strategy = "ALLGREEN"
63+
max_entries_to_build = 5
64+
max_entries_to_merge = 5
65+
merge_method = "MERGE"
66+
min_entries_to_merge = 1
67+
min_entries_to_merge_wait_minutes = 60
68+
}
69+
5970
pull_request {
6071
required_approving_review_count = 2
6172
required_review_thread_resolution = true
@@ -298,8 +309,9 @@ func TestGithubRepositoryRulesets(t *testing.T) {
298309
resource "github_repository" "test" {
299310
name = "tf-acc-test-import-%[1]s"
300311
description = "Terraform acceptance tests %[1]s"
301-
auto_init = false
302-
vulnerability_alerts = true
312+
auto_init = true
313+
default_branch = "main"
314+
vulnerability_alerts = true
303315
}
304316
305317
resource "github_repository_environment" "example" {
@@ -315,7 +327,7 @@ func TestGithubRepositoryRulesets(t *testing.T) {
315327
316328
conditions {
317329
ref_name {
318-
include = ["~ALL"]
330+
include = ["refs/heads/main"]
319331
exclude = []
320332
}
321333
}
@@ -342,6 +354,16 @@ func TestGithubRepositoryRulesets(t *testing.T) {
342354
require_last_push_approval = true
343355
}
344356
357+
merge_queue {
358+
check_response_timeout_minutes = 30
359+
grouping_strategy = "HEADGREEN"
360+
max_entries_to_build = 4
361+
max_entries_to_merge = 4
362+
merge_method = "SQUASH"
363+
min_entries_to_merge = 2
364+
min_entries_to_merge_wait_minutes = 10
365+
}
366+
345367
required_status_checks {
346368
347369
required_check {
@@ -395,6 +417,80 @@ func TestGithubRepositoryRulesets(t *testing.T) {
395417

396418
})
397419

420+
t.Run("Creates repository ruleset with merge queue SQUASH method", func(t *testing.T) {
421+
422+
config := fmt.Sprintf(`
423+
resource "github_repository" "test" {
424+
name = "tf-acc-test-merge-queue-%s"
425+
auto_init = true
426+
default_branch = "main"
427+
}
428+
429+
resource "github_repository_ruleset" "test" {
430+
name = "merge-queue-test"
431+
repository = github_repository.test.id
432+
target = "branch"
433+
enforcement = "active"
434+
435+
conditions {
436+
ref_name {
437+
include = ["refs/heads/main"]
438+
exclude = []
439+
}
440+
}
441+
442+
rules {
443+
merge_queue {
444+
check_response_timeout_minutes = 30
445+
grouping_strategy = "HEADGREEN"
446+
max_entries_to_build = 4
447+
max_entries_to_merge = 4
448+
merge_method = "SQUASH"
449+
min_entries_to_merge = 2
450+
min_entries_to_merge_wait_minutes = 10
451+
}
452+
}
453+
}
454+
`, randomID)
455+
456+
check := resource.ComposeTestCheckFunc(
457+
resource.TestCheckResourceAttr(
458+
"github_repository_ruleset.test", "name",
459+
"merge-queue-test",
460+
),
461+
resource.TestCheckResourceAttr(
462+
"github_repository_ruleset.test", "rules.0.merge_queue.0.merge_method",
463+
"SQUASH",
464+
),
465+
)
466+
467+
testCase := func(t *testing.T, mode string) {
468+
resource.Test(t, resource.TestCase{
469+
PreCheck: func() { skipUnlessMode(t, mode) },
470+
Providers: testAccProviders,
471+
Steps: []resource.TestStep{
472+
{
473+
Config: config,
474+
Check: check,
475+
},
476+
},
477+
})
478+
}
479+
480+
t.Run("with an anonymous account", func(t *testing.T) {
481+
t.Skip("anonymous account not supported for this operation")
482+
})
483+
484+
t.Run("with an individual account", func(t *testing.T) {
485+
testCase(t, individual)
486+
})
487+
488+
t.Run("with an organization account", func(t *testing.T) {
489+
testCase(t, organization)
490+
})
491+
492+
})
493+
398494
}
399495

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

0 commit comments

Comments
 (0)