Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
a3526c3
Update descriptions and validations
deiga Nov 27, 2025
b5e87b9
Add `CustomizeDiff` logic to validate on `plan`
deiga Nov 27, 2025
ebae94b
Add first validation test
deiga Nov 29, 2025
92a6ea1
Add validation error when `conditions` is missing
deiga Nov 30, 2025
0fc34a1
Add further validation tests
deiga Nov 30, 2025
088cb08
Remove unnecessary skip blocks as `individual` and `anonymous` access…
deiga Dec 2, 2025
e618a98
Switch `ref_name` to `Optional` as `push` doesn't need a `ref_name`
deiga Dec 2, 2025
80c6a39
Add Debug logging with `tflog` to validation
deiga Dec 3, 2025
fe67604
Fix validation as `ref_name`, `repository_name` and `repository_id` a…
deiga Dec 3, 2025
f85842b
Remove unnecessary panic test
deiga Dec 3, 2025
cdad261
Improve validation output messages
deiga Dec 3, 2025
a2df271
Fix condition to require only one of `repository_name` or `repository…
deiga Dec 3, 2025
d251249
Add validation to `required_workflow.path`
deiga Dec 6, 2025
f149925
Rename test resources for easier debugging
deiga Dec 7, 2025
dbccc72
Fix linter issues
deiga Dec 7, 2025
6b25976
Add validation to ensure `rules.required_status_checks.required_check…
deiga Dec 8, 2025
65aa87b
Add test to ensure that `required_checks` is always required
deiga Dec 8, 2025
7319af8
Fix tests after rebase
deiga Dec 10, 2025
e585959
Improve legibility of `conditions` description
deiga Dec 10, 2025
7bf84e0
Update descriptions
deiga Dec 14, 2025
6190529
Add Acc test for push ruleset.
deiga Dec 14, 2025
6e29a23
Add failing test for `flattenConditions` with no `ref_name` condition
deiga Dec 14, 2025
1bd4c6d
Fix `flattenConditions` to work with `push` rulesets
deiga Dec 15, 2025
47d34a6
Add more tests for `flattenConditions`
deiga Dec 15, 2025
b4e2626
Enable debug logging in `flattenConditions`
deiga Dec 15, 2025
2da9d97
Ensures that `flattenConditions` returns an empty list on empty API r…
deiga Dec 15, 2025
f330e2e
Add validation for `push` `rules`
deiga Dec 16, 2025
a814af7
Get `TestAccGithubRepositoryRulesets` to work
deiga Dec 17, 2025
6765136
`repository_ruleset`: Add tests for validations
deiga Dec 17, 2025
d3278c0
`repository_ruleset`: Implement validations for `target`, `conditions…
deiga Dec 17, 2025
134f529
Updated ruleset docs
deiga Dec 17, 2025
95f7182
Extract validation functions to separate utils file with unit tests
deiga Dec 31, 2025
40d21fe
Fix push ruleset test config
deiga Jan 10, 2026
6d2f5d5
Remove `repository` target after thorough testing that it doesn't do …
deiga Jan 10, 2026
6a7434f
Use idiomatic test naming convention
deiga Jan 13, 2026
64af81e
Address code structure comment in tests
deiga Jan 13, 2026
fe65205
Fix inconsistent logging
deiga Jan 13, 2026
2a39e82
Refactor to use typed constant string for ruleset `Target`
deiga Jan 13, 2026
d60c2e0
Fix indentation issue with `github_repository_file` and heredocs
deiga Jan 14, 2026
4c6430f
Rename files to be more sensible
deiga Jan 14, 2026
4a0e1a3
Refactor validation functions so that Repo and Org share almost every…
deiga Jan 14, 2026
946d0fe
Address review comments
deiga Jan 21, 2026
155f642
Use `github.RepositoryRuleType` instead of strings
deiga Jan 29, 2026
5dd238c
Refactor `flattenRules` and `expandRules` to use context
deiga Jan 29, 2026
4e67141
Fix failing validation
deiga Jan 29, 2026
bde553d
Refactor to use `ValidateDiagFunc` and `validation.ToDiagFunc` in rep…
deiga Jan 30, 2026
05be1ae
Use `strings.Join` to make `Description` of `target` dynamic
deiga Jan 30, 2026
88836c0
Add validation to `operator` attributes
deiga Jan 30, 2026
4005665
Add missing validations to repo ruleset attributes
deiga Jan 30, 2026
229752f
Refactor to use `ValidateDiagFunc` and `validation.ToDiagFunc` in org…
deiga Jan 30, 2026
7147526
Use `strings.Join` to make `Description` of `target` dynamic
deiga Jan 30, 2026
dc8953e
Add validation to `operator` attributes
deiga Jan 30, 2026
02b402a
Add missing validations to org ruleset attributes
deiga Jan 30, 2026
cc7d1f2
Ensure attribute names map correctly to rule names
deiga Jan 30, 2026
c2a8bb3
Don't ignore errors
deiga Jan 30, 2026
00afcdb
Address review comments
deiga Feb 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 150 additions & 83 deletions github/resource_github_organization_ruleset.go

Large diffs are not rendered by default.

344 changes: 327 additions & 17 deletions github/resource_github_organization_ruleset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package github

import (
"fmt"
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
Expand All @@ -18,6 +19,19 @@ func TestAccGithubOrganizationRuleset(t *testing.T) {
workflowFilePath := ".github/workflows/echo.yaml"

config := fmt.Sprintf(`
locals {
workflow_content = <<EOT
name: Echo Workflow

on: [pull_request]

jobs:
echo:
runs-on: ubuntu-latest
steps:
- run: echo "Hello, world!"
EOT
}
resource "github_repository" "test" {
name = "%s"
visibility = "private"
Expand All @@ -28,17 +42,7 @@ resource "github_repository_file" "workflow_file" {
repository = github_repository.test.name
branch = "main"
file = "%[3]s"
content = <<EOT
name: Echo Workflow

on: [pull_request]

jobs:
echo:
runs-on: linux
steps:
- run: echo \"Hello, world!\"
EOT
content = replace(local.workflow_content, "\t", " ") # NOTE: 'content' must be indented with spaces, not tabs
commit_message = "Managed by Terraform"
commit_author = "Terraform User"
commit_email = "[email protected]"
Expand Down Expand Up @@ -210,11 +214,6 @@ resource "github_organization_ruleset" "test" {
include = ["~ALL"]
exclude = []
}

ref_name {
include = ["~ALL"]
exclude = []
}
}

rules {
Expand Down Expand Up @@ -499,6 +498,317 @@ resource "github_organization_ruleset" "test" {
},
})
})

t.Run("validates_branch_target_requires_ref_name_condition", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
config := fmt.Sprintf(`
resource "github_organization_ruleset" "test" {
name = "test-validation-%s"
target = "branch"
enforcement = "active"

conditions {
repository_name {
include = ["~ALL"]
exclude = []
}
}

rules {
creation = true
}
}
`, randomID)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessHasPaidOrgs(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
ExpectError: regexp.MustCompile("ref_name must be set for branch target"),
},
},
})
})

t.Run("validates_tag_target_requires_ref_name_condition", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
config := fmt.Sprintf(`
resource "github_organization_ruleset" "test" {
name = "test-tag-no-conditions-%s"
target = "tag"
enforcement = "active"

conditions {
repository_name {
include = ["~ALL"]
exclude = []
}
}

rules {
creation = true
}
}
`, randomID)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessHasPaidOrgs(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
ExpectError: regexp.MustCompile("ref_name must be set for tag target"),
},
},
})
})

t.Run("validates_push_target_rejects_ref_name_condition", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
resourceName := "test-push-reject-ref-name"
config := fmt.Sprintf(`
resource "github_organization_ruleset" "%s" {
name = "test-push-with-ref-%s"
target = "push"
enforcement = "active"

conditions {
ref_name {
include = ["~ALL"]
exclude = []
}
repository_name {
include = ["~ALL"]
exclude = []
}
}

rules {
# Push rulesets only support push-specific rules
max_file_size {
max_file_size = 100
}
}
}
`, resourceName, randomID)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessHasPaidOrgs(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
ExpectError: regexp.MustCompile("ref_name must not be set for push target"),
},
},
})
})

t.Run("validates_push_target_rejects_branch_or_tag_rules", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
resourceName := "test-push-reject-branch-rules"
config := fmt.Sprintf(`
resource "github_organization_ruleset" "%s" {
name = "test-push-branch-rule-%s"
target = "push"
enforcement = "active"

conditions {
repository_name {
include = ["~ALL"]
exclude = []
}
}

rules {
# 'creation' is a branch/tag rule, not valid for push target
creation = true
}
}
`, resourceName, randomID)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessHasPaidOrgs(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
ExpectError: regexp.MustCompile("rule .* is not valid for push target"),
},
},
})
})

t.Run("validates_branch_target_rejects_push-only_rules", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
resourceName := "test-branch-reject-push-rules"
config := fmt.Sprintf(`
resource "github_organization_ruleset" "%s" {
name = "test-branch-push-rule-%s"
target = "branch"
enforcement = "active"

conditions {
ref_name {
include = ["~ALL"]
exclude = []
}
repository_name {
include = ["~ALL"]
exclude = []
}
}

rules {
# 'max_file_size' is a push-only rule, not valid for branch target
max_file_size {
max_file_size = 100
}
}
}
`, resourceName, randomID)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessHasPaidOrgs(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
ExpectError: regexp.MustCompile("rule .* is not valid for branch target"),
},
},
})
})

t.Run("creates_push_ruleset", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
rulesetName := fmt.Sprintf("%stest-push-%s", testResourcePrefix, randomID)
resourceName := "test-push-ruleset"
resourceFullName := fmt.Sprintf("github_organization_ruleset.%s", resourceName)
config := fmt.Sprintf(`
resource "github_organization_ruleset" "%s" {
name = "%s"
target = "push"
enforcement = "active"

conditions {
repository_name {
include = ["~ALL"]
exclude = []
}
}

rules {
# Push rulesets only support push-specific rules:
# file_path_restriction, max_file_path_length, file_extension_restriction, max_file_size
max_file_size {
max_file_size = 100
}
}
}
`, resourceName, rulesetName)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessHasPaidOrgs(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceFullName, "name", rulesetName),
resource.TestCheckResourceAttr(resourceFullName, "target", "push"),
resource.TestCheckResourceAttr(resourceFullName, "enforcement", "active"),
resource.TestCheckResourceAttr(resourceFullName, "rules.0.max_file_size.0.max_file_size", "100"),
),
},
},
})
})

t.Run("validates_rules__required_status_checks_block", func(t *testing.T) {
t.Run("required_check__context_block_should_not_be_empty", func(t *testing.T) {
resourceName := "test-required-status-checks-context-is-not-empty"
randomID := acctest.RandString(5)
config := fmt.Sprintf(`
resource "github_organization_ruleset" "%s" {
name = "test-context-is-not-empty-%s"
target = "branch"
enforcement = "active"

conditions {
ref_name {
include = ["~ALL"]
exclude = []
}
repository_name {
include = ["~ALL"]
exclude = []
}
}

rules {
required_status_checks {
required_check {
context = ""
}
}
}
}
`, resourceName, randomID)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessHasPaidOrgs(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
ExpectError: regexp.MustCompile("expected \"context\" to not be an empty string"),
},
},
})
})
t.Run("required_check_should_be_required_when_strict_required_status_checks_policy_is_set", func(t *testing.T) {
resourceName := "test-required-check-is-required"
randomID := acctest.RandString(5)
config := fmt.Sprintf(`
resource "github_organization_ruleset" "%s" {
name = "test-required-with-%s"
target = "branch"
enforcement = "active"

conditions {
ref_name {
include = ["~ALL"]
exclude = []
}
repository_name {
include = ["~ALL"]
exclude = []
}
}

rules {
required_status_checks {
strict_required_status_checks_policy = true
}
}
}
`, resourceName, randomID)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessHasPaidOrgs(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
ExpectError: regexp.MustCompile("Insufficient required_check blocks"),
},
},
})
})
})
}

func TestOrganizationPushRulesetSupport(t *testing.T) {
Expand Down Expand Up @@ -578,7 +888,7 @@ func TestOrganizationPushRulesetSupport(t *testing.T) {
}

// Test flatten functionality (organization rulesets use org=true)
flattenedResult := flattenRules(expandedRules, true)
flattenedResult := flattenRules(t.Context(), expandedRules, true)

if len(flattenedResult) != 1 {
t.Fatalf("Expected 1 flattened result, got %d", len(flattenedResult))
Expand Down
Loading