From 070b8610fa6142777519144baf766a37d41baa6d Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Thu, 4 Jun 2026 17:26:01 +0100 Subject: [PATCH 1/6] fix: Refactor repository custom property Signed-off-by: Steve Hipwell --- ARCHITECTURE.md | 9 +- docs/resources/repository_custom_property.md | 41 +++- .../import-by-string-id.tf | 4 + .../import.sh | 1 + .../resource_1.tf} | 3 + ...ource_github_repository_custom_property.go | 219 +++++++++++++----- ...ub_repository_custom_property_migration.go | 74 ++++++ ...pository_custom_property_migration_test.go | 51 ++++ ..._github_repository_custom_property_test.go | 122 +++++++++- .../repository_custom_property.md.tmpl | 43 ++-- 10 files changed, 480 insertions(+), 87 deletions(-) create mode 100644 examples/resources/github_repository_custom_property/import-by-string-id.tf create mode 100644 examples/resources/github_repository_custom_property/import.sh rename examples/resources/{repository_custom_property/example_1.tf => github_repository_custom_property/resource_1.tf} (71%) create mode 100644 github/resource_github_repository_custom_property_migration.go create mode 100644 github/resource_github_repository_custom_property_migration_test.go diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8875deb388..5981096384 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -178,7 +178,10 @@ func resourceGithubExample() *schema.Resource { StateContext: resourceGithubExampleImport, }, - // Include SchemaVersion and StateUpgraders if state migrations exist + // Only if required. + CustomizeDiff: diffExample, + + // Include SchemaVersion and StateUpgraders if state migrations exist. SchemaVersion: 1, StateUpgraders: []schema.StateUpgrader{ { @@ -188,8 +191,10 @@ func resourceGithubExample() *schema.Resource { }, }, + Description: "Manages an example GitHub resource.", + Schema: map[string]*schema.Schema{ - // Schema definition + // Schema definition. }, } } diff --git a/docs/resources/repository_custom_property.md b/docs/resources/repository_custom_property.md index 3c492eba00..5db27cb1ff 100644 --- a/docs/resources/repository_custom_property.md +++ b/docs/resources/repository_custom_property.md @@ -1,22 +1,25 @@ --- page_title: "github_repository_custom_property (Resource) - GitHub" +subcategory: "" description: |- - Creates and a specific custom property for a GitHub repository + Resource to manage GitHub repository custom properties. --- # github_repository_custom_property (Resource) -This resource allows you to create and manage a specific custom property for a GitHub repository. +Resource to manage GitHub repository custom properties. +For more information, see the [GitHub API documentation](https://docs.github.com/rest/metadata/custom-properties#create-or-update-repository-custom-property). ## Example Usage -> Note that this assumes there already is a custom property defined on the org level called `my-cool-property` of type `string` - ```terraform +# NOTE: This assumes there already is a custom property defined on the org level called `my-cool-property` of type `string` + resource "github_repository" "example" { name = "example" description = "My awesome codebase" } + resource "github_repository_custom_property" "string" { repository = github_repository.example.name property_name = "my-cool-property" @@ -25,21 +28,35 @@ resource "github_repository_custom_property" "string" { } ``` -## Argument Reference + +## Schema -The following arguments are supported: +### Required -- `repository` - (Required) The repository of the environment. +- `property_name` (String) Name of the custom property. +- `property_type` (String) Type of the custom property. Valid values are `string`, `single_select`, `multi_select`, `true_false`, and `url`. +- `property_value` (Set of String) Value of the custom property. For `string`, `single_select`, `true_false`, and `url` property types, this should be a single value. For `multi_select` property types, this can be multiple values. +- `repository` (String) Name of the repository. -- `property_type` - (Required) Type of the custom property. Can be one of `single_select`, `multi_select`, `string`, or `true_false` +### Read-Only -- `property_name` - (Required) Name of the custom property. Note that a pre-requisiste for this resource is that a custom property of this name has already been defined on the organization level - -- `property_value` - (Required) Value of the custom property in the form of an array. Properties of type `single_select`, `string`, and `true_false` are represented as a string array of length 1 +- `id` (String) The ID of this resource. +- `repository_id` (Number) ID of the repository. ## Import -GitHub Repository Custom Property can be imported using an ID made up of a combination of the names of the organization, repository, custom property separated by a `:` character, e.g. +Import is supported using the following syntax: + +In Terraform v1.5.0 and later, the [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used with the `id` attribute, for example: + +```terraform +import { + to = github_repository_custom_property.example + id = "organization-name:repo-name:custom-property-name" +} +``` + +The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: ```shell terraform import github_repository_custom_property.example organization-name:repo-name:custom-property-name diff --git a/examples/resources/github_repository_custom_property/import-by-string-id.tf b/examples/resources/github_repository_custom_property/import-by-string-id.tf new file mode 100644 index 0000000000..b65f685043 --- /dev/null +++ b/examples/resources/github_repository_custom_property/import-by-string-id.tf @@ -0,0 +1,4 @@ +import { + to = github_repository_custom_property.example + id = "organization-name:repo-name:custom-property-name" +} diff --git a/examples/resources/github_repository_custom_property/import.sh b/examples/resources/github_repository_custom_property/import.sh new file mode 100644 index 0000000000..4f3c81dfaa --- /dev/null +++ b/examples/resources/github_repository_custom_property/import.sh @@ -0,0 +1 @@ +terraform import github_repository_custom_property.example organization-name:repo-name:custom-property-name diff --git a/examples/resources/repository_custom_property/example_1.tf b/examples/resources/github_repository_custom_property/resource_1.tf similarity index 71% rename from examples/resources/repository_custom_property/example_1.tf rename to examples/resources/github_repository_custom_property/resource_1.tf index be5eb8fb8c..45fcb9caf2 100644 --- a/examples/resources/repository_custom_property/example_1.tf +++ b/examples/resources/github_repository_custom_property/resource_1.tf @@ -1,7 +1,10 @@ +# NOTE: This assumes there already is a custom property defined on the org level called `my-cool-property` of type `string` + resource "github_repository" "example" { name = "example" description = "My awesome codebase" } + resource "github_repository_custom_property" "string" { repository = github_repository.example.name property_name = "my-cool-property" diff --git a/github/resource_github_repository_custom_property.go b/github/resource_github_repository_custom_property.go index ae0f0fb8ff..a601bfc54d 100644 --- a/github/resource_github_repository_custom_property.go +++ b/github/resource_github_repository_custom_property.go @@ -2,33 +2,53 @@ package github import ( "context" - "fmt" + "errors" + "strings" "github.com/google/go-github/v88/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceGithubRepositoryCustomProperty() *schema.Resource { return &schema.Resource{ - Create: resourceGithubRepositoryCustomPropertyCreate, - Read: resourceGithubRepositoryCustomPropertyRead, - Delete: resourceGithubRepositoryCustomPropertyDelete, + CreateContext: resourceGithubRepositoryCustomPropertyCreate, + ReadContext: resourceGithubRepositoryCustomPropertyRead, + UpdateContext: resourceGithubRepositoryCustomPropertyUpdate, + DeleteContext: resourceGithubRepositoryCustomPropertyDelete, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: resourceGithubRepositoryCustomPropertyImport, }, + CustomizeDiff: diffRepository, + + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubRepositoryCustomPropertyV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubRepositoryCustomPropertyStateUpgradeV0, + Version: 0, + }, + }, + + Description: "Resource to manage GitHub repository custom properties.", + Schema: map[string]*schema.Schema{ "repository": { Type: schema.TypeString, Required: true, - Description: "Name of the repository which the custom properties should be on.", - ForceNew: true, + Description: "Name of the repository.", + }, + "repository_id": { + Type: schema.TypeInt, + Computed: true, + Description: "ID of the repository.", }, "property_type": { Type: schema.TypeString, Required: true, - Description: "Type of the custom property", + Description: "Type of the custom property. Valid values are `string`, `single_select`, `multi_select`, `true_false`, and `url`.", ForceNew: true, ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{string(github.PropertyValueTypeString), string(github.PropertyValueTypeSingleSelect), string(github.PropertyValueTypeMultiSelect), string(github.PropertyValueTypeTrueFalse), string(github.PropertyValueTypeURL)}, false)), }, @@ -42,25 +62,27 @@ func resourceGithubRepositoryCustomProperty() *schema.Resource { Type: schema.TypeSet, MinItems: 1, Required: true, - Description: "Value of the custom property.", + Description: "Value of the custom property. For `string`, `single_select`, `true_false`, and `url` property types, this should be a single value. For `multi_select` property types, this can be multiple values.", Elem: &schema.Schema{ - Type: schema.TypeString, + Type: schema.TypeString, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), }, - ForceNew: true, }, }, } } -func resourceGithubRepositoryCustomPropertyCreate(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - ctx := context.Background() +func resourceGithubRepositoryCustomPropertyCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta, _ := m.(*Owner) + client := meta.v3client + owner := meta.name - owner := meta.(*Owner).name - repoName := d.Get("repository").(string) - propertyName := d.Get("property_name").(string) - propertyType := github.PropertyValueType(d.Get("property_type").(string)) - propertyValue := expandStringList(d.Get("property_value").(*schema.Set).List()) + repoName, _ := d.Get("repository").(string) + propertyName, _ := d.Get("property_name").(string) + propertyTypeVal, _ := d.Get("property_type").(string) + propertyType := github.PropertyValueType(propertyTypeVal) + propertyValueVal, _ := d.Get("property_value").(*schema.Set) + propertyValue := expandStringList(propertyValueVal.List()) customProperty := github.CustomPropertyValue{ PropertyName: propertyName, @@ -73,89 +95,178 @@ func resourceGithubRepositoryCustomPropertyCreate(d *schema.ResourceData, meta a case github.PropertyValueTypeMultiSelect: customProperty.Value = propertyValue default: - return fmt.Errorf("custom property type is not valid: %v", propertyType) + return diag.Errorf("custom property type is not valid: %v", propertyType) } _, err := client.Repositories.CreateOrUpdateCustomProperties(ctx, owner, repoName, []*github.CustomPropertyValue{&customProperty}) if err != nil { - return err + return diag.FromErr(err) } - d.SetId(buildThreePartID(owner, repoName, propertyName)) + id, err := buildID(owner, repoName, propertyName) + if err != nil { + return diag.FromErr(err) + } + d.SetId(id) - return resourceGithubRepositoryCustomPropertyRead(d, meta) + repo, _, err := client.Repositories.Get(ctx, owner, repoName) + if err != nil { + return diag.FromErr(err) + } + repoID := int(repo.GetID()) + + if err := d.Set("repository_id", repoID); err != nil { + return diag.FromErr(err) + } + + return nil } -func resourceGithubRepositoryCustomPropertyRead(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - ctx := context.Background() +func resourceGithubRepositoryCustomPropertyRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta, _ := m.(*Owner) + client := meta.v3client + owner := meta.name - owner, repoName, propertyName, err := parseID3(d.Id()) + repoName, _ := d.Get("repository").(string) + propertyName, _ := d.Get("property_name").(string) + + props, _, err := client.Repositories.GetAllCustomPropertyValues(ctx, owner, repoName) if err != nil { - return err + if err, ok := errors.AsType[*github.ErrorResponse](err); ok && err.Response.StatusCode == 404 { + d.SetId("") + return nil + } + return diag.FromErr(err) } - wantedCustomPropertyValue, err := readRepositoryCustomPropertyValue(ctx, client, owner, repoName, propertyName) - if err != nil { - return err + var property *github.CustomPropertyValue + for _, prop := range props { + if prop.PropertyName == propertyName { + property = prop + break + } } - if wantedCustomPropertyValue == nil { + if property == nil { d.SetId("") return nil } - d.SetId(buildThreePartID(owner, repoName, propertyName)) - _ = d.Set("repository", repoName) - _ = d.Set("property_name", propertyName) - _ = d.Set("property_value", wantedCustomPropertyValue) + propertyValue, err := parseRepositoryCustomPropertyValueToStringSlice(property) + if err != nil { + return diag.FromErr(err) + } + + id, err := buildID(owner, repoName, propertyName) + if err != nil { + return diag.FromErr(err) + } + d.SetId(id) + + if err := d.Set("property_value", propertyValue); err != nil { + return diag.FromErr(err) + } return nil } -func resourceGithubRepositoryCustomPropertyDelete(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - ctx := context.Background() +func resourceGithubRepositoryCustomPropertyUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta, _ := m.(*Owner) + client := meta.v3client + owner := meta.name - owner, repoName, propertyName, err := parseID3(d.Id()) + repoName, _ := d.Get("repository").(string) + propertyName, _ := d.Get("property_name").(string) + propertyTypeVal, _ := d.Get("property_type").(string) + propertyType := github.PropertyValueType(propertyTypeVal) + propertyValueVal, _ := d.Get("property_value").(*schema.Set) + propertyValue := expandStringList(propertyValueVal.List()) + + customProperty := github.CustomPropertyValue{ + PropertyName: propertyName, + } + + // The propertyValue can either be a list of strings or a string + switch propertyType { + case github.PropertyValueTypeString, github.PropertyValueTypeSingleSelect, github.PropertyValueTypeURL, github.PropertyValueTypeTrueFalse: + customProperty.Value = propertyValue[0] + case github.PropertyValueTypeMultiSelect: + customProperty.Value = propertyValue + default: + return diag.Errorf("custom property type is not valid: %v", propertyType) + } + + _, err := client.Repositories.CreateOrUpdateCustomProperties(ctx, owner, repoName, []*github.CustomPropertyValue{&customProperty}) + if err != nil { + return diag.FromErr(err) + } + + id, err := buildID(owner, repoName, propertyName) if err != nil { - return err + return diag.FromErr(err) } + d.SetId(id) + + return nil +} + +func resourceGithubRepositoryCustomPropertyDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta, _ := m.(*Owner) + client := meta.v3client + owner := meta.name + + repoName, _ := d.Get("repository").(string) + propertyName, _ := d.Get("property_name").(string) customProperty := github.CustomPropertyValue{ PropertyName: propertyName, Value: nil, } - _, err = client.Repositories.CreateOrUpdateCustomProperties(ctx, owner, repoName, []*github.CustomPropertyValue{&customProperty}) + _, err := client.Repositories.CreateOrUpdateCustomProperties(ctx, owner, repoName, []*github.CustomPropertyValue{&customProperty}) if err != nil { - return err + return diag.FromErr(err) } return nil } -func readRepositoryCustomPropertyValue(ctx context.Context, client *github.Client, owner, repoName, propertyName string) ([]string, error) { - allCustomProperties, _, err := client.Repositories.GetAllCustomPropertyValues(ctx, owner, repoName) +func resourceGithubRepositoryCustomPropertyImport(ctx context.Context, d *schema.ResourceData, m any) ([]*schema.ResourceData, error) { + meta, _ := m.(*Owner) + client := meta.v3client + + owner, repoName, propertyName, err := parseID3(d.Id()) if err != nil { return nil, err } - var wantedCustomProperty *github.CustomPropertyValue - for _, customProperty := range allCustomProperties { - if customProperty.PropertyName == propertyName { - wantedCustomProperty = customProperty - } + if !strings.EqualFold(owner, meta.name) { + return nil, errors.New("owner in id must match authenticated owner") } - if wantedCustomProperty == nil { - return nil, nil + repo, _, err := client.Repositories.Get(ctx, owner, repoName) + if err != nil { + return nil, err } + repoID := int(repo.GetID()) - wantedPropertyValue, err := parseRepositoryCustomPropertyValueToStringSlice(wantedCustomProperty) + cp, _, err := client.Organizations.GetCustomProperty(ctx, owner, propertyName) if err != nil { return nil, err } - return wantedPropertyValue, nil + if err := d.Set("repository", repoName); err != nil { + return nil, err + } + if err := d.Set("repository_id", repoID); err != nil { + return nil, err + } + if err := d.Set("property_type", cp.GetValueType()); err != nil { + return nil, err + } + if err := d.Set("property_name", propertyName); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil } diff --git a/github/resource_github_repository_custom_property_migration.go b/github/resource_github_repository_custom_property_migration.go new file mode 100644 index 0000000000..5d126c616d --- /dev/null +++ b/github/resource_github_repository_custom_property_migration.go @@ -0,0 +1,74 @@ +package github + +import ( + "context" + "fmt" + "log" + + "github.com/google/go-github/v88/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceGithubRepositoryCustomPropertyV0() *schema.Resource { + return &schema.Resource{ + SchemaVersion: 0, + + Schema: map[string]*schema.Schema{ + "repository": { + Type: schema.TypeString, + Required: true, + Description: "Name of the repository which the custom properties should be on.", + ForceNew: true, + }, + "property_type": { + Type: schema.TypeString, + Required: true, + Description: "Type of the custom property", + ForceNew: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{string(github.PropertyValueTypeString), string(github.PropertyValueTypeSingleSelect), string(github.PropertyValueTypeMultiSelect), string(github.PropertyValueTypeTrueFalse), string(github.PropertyValueTypeURL)}, false)), + }, + "property_name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the custom property.", + ForceNew: true, + }, + "property_value": { + Type: schema.TypeSet, + MinItems: 1, + Required: true, + Description: "Value of the custom property.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + ForceNew: true, + }, + }, + } +} + +func resourceGithubRepositoryCustomPropertyStateUpgradeV0(ctx context.Context, rawState map[string]any, m any) (map[string]any, error) { + meta, _ := m.(*Owner) + client := meta.v3client + owner := meta.name + + log.Printf("[DEBUG] GitHub Repository Custom Property Attributes before migration: %#v", rawState) + + repoName, ok := rawState["repository"].(string) + if !ok { + return nil, fmt.Errorf("repository not found or is not a string") + } + + repo, _, err := client.Repositories.Get(ctx, owner, repoName) + if err != nil { + return nil, fmt.Errorf("failed to retrieve repository %s: %w", repoName, err) + } + + repoID := int(repo.GetID()) + rawState["repository_id"] = repoID + + log.Printf("[DEBUG] GitHub Repository Custom Property Attributes after migration: %#v", rawState) + + return rawState, nil +} diff --git a/github/resource_github_repository_custom_property_migration_test.go b/github/resource_github_repository_custom_property_migration_test.go new file mode 100644 index 0000000000..138afb872a --- /dev/null +++ b/github/resource_github_repository_custom_property_migration_test.go @@ -0,0 +1,51 @@ +package github + +// TODO: Enable this test once we have a pattern to create a mock client for the test. + +// import ( +// "context" +// "reflect" +// "testing" +// ) + +// func Test_resourceGithubCustomPropertyStateUpgradeV0(t *testing.T) { +// t.Parallel() + +// for _, d := range []struct { +// testName string +// rawState map[string]any +// want map[string]any +// shouldError bool +// }{ +// { +// testName: "migrates v0 to v1", +// rawState: map[string]any{ +// "id": "my-org:my-repo:my-property", +// "repository": "my-repo", +// "property_name": "my-property", +// "property_value": "my-value", +// }, +// want: map[string]any{ +// "id": "my-org:my-repo:my-property", +// "repository": "my-repo", +// "repository_id": 123456, +// "property_name": "my-property", +// "property_value": "my-value", +// }, +// shouldError: false, +// }, +// } { +// t.Run(d.testName, func(t *testing.T) { +// t.Parallel() + +// got, err := resourceGithubCustomPropertyStateUpgradeV0(t.Context(), d.rawState, nil) +// if (err != nil) != d.shouldError { +// t.Fatalf("unexpected error state") +// } + +// if !d.shouldError && !reflect.DeepEqual(got, d.want) { +// t.Fatalf("got %+v, want %+v", got, d.want) +// } +// }) +// } +// } diff --git a/github/resource_github_repository_custom_property_test.go b/github/resource_github_repository_custom_property_test.go index 9fcb28b496..027796cc91 100644 --- a/github/resource_github_repository_custom_property_test.go +++ b/github/resource_github_repository_custom_property_test.go @@ -2,6 +2,7 @@ package github import ( "fmt" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" @@ -9,7 +10,7 @@ import ( ) func TestAccGithubRepositoryCustomProperty(t *testing.T) { - t.Run("creates custom property of type single_select without error", func(t *testing.T) { + t.Run("creates_single_select", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) repoName := fmt.Sprintf("%srepo-custom-prop-%s", testResourcePrefix, randomID) propertyName := fmt.Sprintf("tf-acc-test-property-%s", randomID) @@ -51,7 +52,7 @@ func TestAccGithubRepositoryCustomProperty(t *testing.T) { }) }) - t.Run("creates custom property of type multi_select without error", func(t *testing.T) { + t.Run("creates_multi_select", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) repoName := fmt.Sprintf("%srepo-custom-prop-%s", testResourcePrefix, randomID) propertyName := fmt.Sprintf("tf-acc-test-property-%s", randomID) @@ -94,7 +95,7 @@ func TestAccGithubRepositoryCustomProperty(t *testing.T) { }) }) - t.Run("creates custom property of type true-false without error", func(t *testing.T) { + t.Run("creates_true_false", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) repoName := fmt.Sprintf("%srepo-custom-prop-%s", testResourcePrefix, randomID) propertyName := fmt.Sprintf("tf-acc-test-property-%s", randomID) @@ -135,7 +136,46 @@ func TestAccGithubRepositoryCustomProperty(t *testing.T) { }) }) - t.Run("creates custom property of type string without error", func(t *testing.T) { + t.Run("creates_url", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + propertyName := fmt.Sprintf("tf-acc-test-property-%s", randomID) + + config := fmt.Sprintf(` + resource "github_organization_custom_properties" "test" { + description = "Test Description" + property_name = "%s" + value_type = "url" + } + resource "github_repository" "test" { + name = "%s" + auto_init = true + } + resource "github_repository_custom_property" "test" { + repository = github_repository.test.name + property_name = github_organization_custom_properties.test.property_name + property_type = github_organization_custom_properties.test.value_type + property_value = ["https://example.com"] + } + `, propertyName, repoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_name", propertyName), + resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_value.#", "1"), + resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_value.0", "https://example.com"), + ), + }, + }, + }) + }) + + t.Run("creates_string", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) repoName := fmt.Sprintf("%srepo-custom-prop-%s", testResourcePrefix, randomID) propertyName := fmt.Sprintf("tf-acc-test-property-%s", randomID) @@ -175,4 +215,78 @@ func TestAccGithubRepositoryCustomProperty(t *testing.T) { }, }) }) + + t.Run("import", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + propertyName := fmt.Sprintf("tf-acc-test-property-%s", randomID) + + config := fmt.Sprintf(` + resource "github_organization_custom_properties" "test" { + description = "Test Description" + property_name = "%s" + value_type = "string" + } + resource "github_repository" "test" { + name = "%s" + auto_init = true + } + resource "github_repository_custom_property" "test" { + repository = github_repository.test.name + property_name = github_organization_custom_properties.test.property_name + property_type = github_organization_custom_properties.test.value_type + property_value = ["text"] + } + `, propertyName, repoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + }, + { + ResourceName: "github_repository_custom_property.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) + + t.Run("fails_when_property_value_contains_empty_string", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + propertyName := fmt.Sprintf("tf-acc-test-property-%s", randomID) + + config := fmt.Sprintf(` + resource "github_organization_custom_properties" "test" { + description = "Test Description" + property_name = "%s" + value_type = "string" + } + resource "github_repository" "test" { + name = "%s" + auto_init = true + } + resource "github_repository_custom_property" "test" { + repository = github_repository.test.name + property_name = github_organization_custom_properties.test.property_name + property_type = github_organization_custom_properties.test.value_type + property_value = [""] + } + `, propertyName, repoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile(`to not be an empty string`), + }, + }, + }) + }) } diff --git a/templates/resources/repository_custom_property.md.tmpl b/templates/resources/repository_custom_property.md.tmpl index a09187b434..266c960778 100644 --- a/templates/resources/repository_custom_property.md.tmpl +++ b/templates/resources/repository_custom_property.md.tmpl @@ -1,35 +1,48 @@ --- page_title: "{{.Name}} ({{.Type}}) - {{.RenderedProviderName}}" +subcategory: "" description: |- - Creates and a specific custom property for a GitHub repository +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} --- # {{.Name}} ({{.Type}}) -This resource allows you to create and manage a specific custom property for a GitHub repository. +{{ .Description | trimspace }} +For more information, see the [GitHub API documentation](https://docs.github.com/rest/metadata/custom-properties#create-or-update-repository-custom-property). +{{ if .HasExamples -}} ## Example Usage -> Note that this assumes there already is a custom property defined on the org level called `my-cool-property` of type `string` +{{- range .ExampleFiles }} -{{ tffile "examples/resources/repository_custom_property/example_1.tf" }} +{{ tffile . }} +{{- end }} +{{- end }} -## Argument Reference +{{ .SchemaMarkdown | trimspace }} +{{- if or .HasImport .HasImportIDConfig .HasImportIdentityConfig }} -The following arguments are supported: +## Import -- `repository` - (Required) The repository of the environment. +Import is supported using the following syntax: +{{- end }} +{{- if .HasImportIdentityConfig }} -- `property_type` - (Required) Type of the custom property. Can be one of `single_select`, `multi_select`, `string`, or `true_false` +In Terraform v1.12.0 and later, the [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used with the `identity` attribute, for example: -- `property_name` - (Required) Name of the custom property. Note that a pre-requisiste for this resource is that a custom property of this name has already been defined on the organization level +{{tffile .ImportIdentityConfigFile }} -- `property_value` - (Required) Value of the custom property in the form of an array. Properties of type `single_select`, `string`, and `true_false` are represented as a string array of length 1 +{{ .IdentitySchemaMarkdown | trimspace }} +{{- end }} +{{- if .HasImportIDConfig }} -## Import +In Terraform v1.5.0 and later, the [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used with the `id` attribute, for example: + +{{tffile .ImportIDConfigFile }} +{{- end }} +{{- if .HasImport }} -GitHub Repository Custom Property can be imported using an ID made up of a combination of the names of the organization, repository, custom property separated by a `:` character, e.g. +The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: -```shell -terraform import github_repository_custom_property.example organization-name:repo-name:custom-property-name -``` +{{codefile "shell" .ImportFile }} +{{- end }} From 53fe44ab7b723653e7fbe624929641cba32523dc Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Fri, 5 Jun 2026 13:00:44 +0100 Subject: [PATCH 2/6] fixup! fix: Refactor repository custom property --- ...ource_github_repository_custom_property.go | 37 +- ..._github_repository_custom_property_test.go | 423 ++++++++++-------- 2 files changed, 256 insertions(+), 204 deletions(-) diff --git a/github/resource_github_repository_custom_property.go b/github/resource_github_repository_custom_property.go index a601bfc54d..5b1c96db43 100644 --- a/github/resource_github_repository_custom_property.go +++ b/github/resource_github_repository_custom_property.go @@ -3,10 +3,13 @@ package github import ( "context" "errors" + "fmt" "strings" "github.com/google/go-github/v88/github" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) @@ -21,7 +24,7 @@ func resourceGithubRepositoryCustomProperty() *schema.Resource { StateContext: resourceGithubRepositoryCustomPropertyImport, }, - CustomizeDiff: diffRepository, + CustomizeDiff: customdiff.All(diffRepository, resourceGithubRepositoryCustomPropertyDiff), SchemaVersion: 1, StateUpgraders: []schema.StateUpgrader{ @@ -72,6 +75,30 @@ func resourceGithubRepositoryCustomProperty() *schema.Resource { } } +func resourceGithubRepositoryCustomPropertyDiff(ctx context.Context, d *schema.ResourceDiff, _ any) error { + tflog.Debug(ctx, "Diffing GitHub repository custom property") + + propertyTypeVal, _ := d.Get("property_type").(string) + propertyType := github.PropertyValueType(propertyTypeVal) + propertyValueVal, _ := d.Get("property_value").(*schema.Set) + propertyValue := expandStringList(propertyValueVal.List()) + propertyCount := len(propertyValue) + + // The propertyValue can either be a list of strings or a string + switch propertyType { + case github.PropertyValueTypeMultiSelect: + if propertyCount < 1 { + return fmt.Errorf("custom property type %v requires at least one value", propertyType) + } + default: + if propertyCount != 1 { + return fmt.Errorf("custom property type %v requires exactly one value", propertyType) + } + } + + return nil +} + func resourceGithubRepositoryCustomPropertyCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { meta, _ := m.(*Owner) client := meta.v3client @@ -90,12 +117,10 @@ func resourceGithubRepositoryCustomPropertyCreate(ctx context.Context, d *schema // The propertyValue can either be a list of strings or a string switch propertyType { - case github.PropertyValueTypeString, github.PropertyValueTypeSingleSelect, github.PropertyValueTypeURL, github.PropertyValueTypeTrueFalse: - customProperty.Value = propertyValue[0] case github.PropertyValueTypeMultiSelect: customProperty.Value = propertyValue default: - return diag.Errorf("custom property type is not valid: %v", propertyType) + customProperty.Value = propertyValue[0] } _, err := client.Repositories.CreateOrUpdateCustomProperties(ctx, owner, repoName, []*github.CustomPropertyValue{&customProperty}) @@ -188,12 +213,10 @@ func resourceGithubRepositoryCustomPropertyUpdate(ctx context.Context, d *schema // The propertyValue can either be a list of strings or a string switch propertyType { - case github.PropertyValueTypeString, github.PropertyValueTypeSingleSelect, github.PropertyValueTypeURL, github.PropertyValueTypeTrueFalse: - customProperty.Value = propertyValue[0] case github.PropertyValueTypeMultiSelect: customProperty.Value = propertyValue default: - return diag.Errorf("custom property type is not valid: %v", propertyType) + customProperty.Value = propertyValue[0] } _, err := client.Repositories.CreateOrUpdateCustomProperties(ctx, owner, repoName, []*github.CustomPropertyValue{&customProperty}) diff --git a/github/resource_github_repository_custom_property_test.go b/github/resource_github_repository_custom_property_test.go index 027796cc91..b92bafa749 100644 --- a/github/resource_github_repository_custom_property_test.go +++ b/github/resource_github_repository_custom_property_test.go @@ -7,244 +7,244 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) func TestAccGithubRepositoryCustomProperty(t *testing.T) { - t.Run("creates_single_select", func(t *testing.T) { - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-custom-prop-%s", testResourcePrefix, randomID) - propertyName := fmt.Sprintf("tf-acc-test-property-%s", randomID) - - config := fmt.Sprintf(` - resource "github_organization_custom_properties" "test" { - allowed_values = ["option1", "option2"] - description = "Test Description" - property_name = "%s" - value_type = "single_select" - } - resource "github_repository" "test" { - name = "%s" - auto_init = true - } - resource "github_repository_custom_property" "test" { - repository = github_repository.test.name - property_name = github_organization_custom_properties.test.property_name - property_type = github_organization_custom_properties.test.value_type - property_value = ["option1"] - } - `, propertyName, repoName) - - check := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_name", propertyName), - resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_value.#", "1"), - resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_value.0", "option1"), - ) + t.Parallel() - resource.Test(t, resource.TestCase{ - PreCheck: func() { skipUnlessHasOrgs(t) }, - ProviderFactories: providerFactories, - Steps: []resource.TestStep{ - { - Config: config, - Check: check, - }, - }, - }) - }) + t.Run("single_select", func(t *testing.T) { + t.Parallel() - t.Run("creates_multi_select", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-custom-prop-%s", testResourcePrefix, randomID) - propertyName := fmt.Sprintf("tf-acc-test-property-%s", randomID) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + propertyName := fmt.Sprintf("%sproperty-%s", testResourcePrefix, randomID) + option1 := "option1" + option2 := "option2" config := fmt.Sprintf(` - resource "github_organization_custom_properties" "test" { - allowed_values = ["option1", "option2"] - description = "Test Description" - property_name = "%s" - value_type = "multi_select" - } - resource "github_repository" "test" { - name = "%s" - auto_init = true - } - resource "github_repository_custom_property" "test" { - repository = github_repository.test.name - property_name = github_organization_custom_properties.test.property_name - property_type = github_organization_custom_properties.test.value_type - property_value = ["option1", "option2"] - } - `, propertyName, repoName) - - checkWithOwner := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_name", propertyName), - resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_value.#", "2"), - resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_value.0", "option1"), - resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_value.1", "option2"), - ) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { skipUnlessHasOrgs(t) }, - ProviderFactories: providerFactories, - Steps: []resource.TestStep{ - { - Config: config, - Check: checkWithOwner, - }, - }, - }) - }) +resource "github_organization_custom_properties" "test" { + allowed_values = ["%s", "%s"] + description = "Test Description" + property_name = "%s" + value_type = "single_select" +} - t.Run("creates_true_false", func(t *testing.T) { - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-custom-prop-%s", testResourcePrefix, randomID) - propertyName := fmt.Sprintf("tf-acc-test-property-%s", randomID) +resource "github_repository" "test" { + name = "%s" + auto_init = true +} - config := fmt.Sprintf(` - resource "github_organization_custom_properties" "test" { - description = "Test Description" - property_name = "%s" - value_type = "true_false" - } - resource "github_repository" "test" { - name = "%s" - auto_init = true - } - resource "github_repository_custom_property" "test" { - repository = github_repository.test.name - property_name = github_organization_custom_properties.test.property_name - property_type = github_organization_custom_properties.test.value_type - property_value = ["true"] - } - `, propertyName, repoName) - - checkWithOwner := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_name", propertyName), - resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_value.#", "1"), - resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_value.0", "true"), - ) +resource "github_repository_custom_property" "test" { + repository = github_repository.test.name + property_name = github_organization_custom_properties.test.property_name + property_type = github_organization_custom_properties.test.value_type + property_value = %%s +} +`, option1, option2, propertyName, repoName) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: config, - Check: checkWithOwner, + Config: fmt.Sprintf(config, fmt.Sprintf(`["%s"]`, option1)), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_repository_custom_property.test", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + }, + }, + { + Config: fmt.Sprintf(config, `["invalid_option"]`), + ExpectError: regexp.MustCompile(`is not allowed for property`), + }, + { + Config: fmt.Sprintf(config, fmt.Sprintf(`["%s"]`, option2)), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("github_repository_custom_property.test", plancheck.ResourceActionUpdate), + }, + }, + }, + { + ResourceName: "github_repository_custom_property.test", + ImportState: true, + ImportStateVerify: true, }, }, }) }) - t.Run("creates_url", func(t *testing.T) { + t.Run("multi_select", func(t *testing.T) { + t.Parallel() + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) - propertyName := fmt.Sprintf("tf-acc-test-property-%s", randomID) + propertyName := fmt.Sprintf("%sproperty-%s", testResourcePrefix, randomID) + option1 := "option1" + option2 := "option2" + option3 := "option3" config := fmt.Sprintf(` - resource "github_organization_custom_properties" "test" { - description = "Test Description" - property_name = "%s" - value_type = "url" - } - resource "github_repository" "test" { - name = "%s" - auto_init = true - } - resource "github_repository_custom_property" "test" { - repository = github_repository.test.name - property_name = github_organization_custom_properties.test.property_name - property_type = github_organization_custom_properties.test.value_type - property_value = ["https://example.com"] - } - `, propertyName, repoName) +resource "github_organization_custom_properties" "test" { + allowed_values = ["%s", "%s", "%s"] + description = "Test Description" + property_name = "%s" + value_type = "multi_select" +} + +resource "github_repository" "test" { + name = "%s" + auto_init = true +} + +resource "github_repository_custom_property" "test" { + repository = github_repository.test.name + property_name = github_organization_custom_properties.test.property_name + property_type = github_organization_custom_properties.test.value_type + property_value = %%s +} +`, option1, option2, option3, propertyName, repoName) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: config, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_name", propertyName), - resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_value.#", "1"), - resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_value.0", "https://example.com"), - ), + Config: fmt.Sprintf(config, fmt.Sprintf(`["%s", "%s"]`, option1, option2)), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_repository_custom_property.test", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + }, + }, + { + Config: fmt.Sprintf(config, `["invalid_option"]`), + ExpectError: regexp.MustCompile(`is not allowed for property`), + }, + { + Config: fmt.Sprintf(config, fmt.Sprintf(`["%s"]`, option3)), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("github_repository_custom_property.test", plancheck.ResourceActionUpdate), + }, + }, + }, + { + ResourceName: "github_repository_custom_property.test", + ImportState: true, + ImportStateVerify: true, }, }, }) }) - t.Run("creates_string", func(t *testing.T) { + t.Run("true_false", func(t *testing.T) { + t.Parallel() + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-custom-prop-%s", testResourcePrefix, randomID) - propertyName := fmt.Sprintf("tf-acc-test-property-%s", randomID) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + propertyName := fmt.Sprintf("%sproperty-%s", testResourcePrefix, randomID) config := fmt.Sprintf(` - resource "github_organization_custom_properties" "test" { - description = "Test Description" - property_name = "%s" - value_type = "string" - } - resource "github_repository" "test" { - name = "%s" - auto_init = true - } - resource "github_repository_custom_property" "test" { - repository = github_repository.test.name - property_name = github_organization_custom_properties.test.property_name - property_type = github_organization_custom_properties.test.value_type - property_value = ["text"] - } - `, propertyName, repoName) - - checkWithOwner := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_name", propertyName), - resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_value.#", "1"), - resource.TestCheckResourceAttr("github_repository_custom_property.test", "property_value.0", "text"), - ) +resource "github_organization_custom_properties" "test" { + description = "Test Description" + property_name = "%s" + value_type = "true_false" +} + +resource "github_repository" "test" { + name = "%s" + auto_init = true +} + +resource "github_repository_custom_property" "test" { + repository = github_repository.test.name + property_name = github_organization_custom_properties.test.property_name + property_type = github_organization_custom_properties.test.value_type + property_value = %%s +} +`, propertyName, repoName) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: config, - Check: checkWithOwner, + Config: fmt.Sprintf(config, `["true"]`), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_repository_custom_property.test", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + }, + }, + { + Config: fmt.Sprintf(config, `["invalid_option"]`), + ExpectError: regexp.MustCompile(`is not allowed for property`), + }, + { + Config: fmt.Sprintf(config, `["false"]`), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("github_repository_custom_property.test", plancheck.ResourceActionUpdate), + }, + }, + }, + { + ResourceName: "github_repository_custom_property.test", + ImportState: true, + ImportStateVerify: true, }, }, }) }) - t.Run("import", func(t *testing.T) { + t.Run("url", func(t *testing.T) { + t.Parallel() + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) - propertyName := fmt.Sprintf("tf-acc-test-property-%s", randomID) + propertyName := fmt.Sprintf("%sproperty-%s", testResourcePrefix, randomID) config := fmt.Sprintf(` - resource "github_organization_custom_properties" "test" { - description = "Test Description" - property_name = "%s" - value_type = "string" - } - resource "github_repository" "test" { - name = "%s" - auto_init = true - } - resource "github_repository_custom_property" "test" { - repository = github_repository.test.name - property_name = github_organization_custom_properties.test.property_name - property_type = github_organization_custom_properties.test.value_type - property_value = ["text"] - } - `, propertyName, repoName) +resource "github_organization_custom_properties" "test" { + description = "Test Description" + property_name = "%s" + value_type = "url" +} + +resource "github_repository" "test" { + name = "%s" + auto_init = true +} + +resource "github_repository_custom_property" "test" { + repository = github_repository.test.name + property_name = github_organization_custom_properties.test.property_name + property_type = github_organization_custom_properties.test.value_type + property_value = %%s +} +`, propertyName, repoName) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: config, + Config: fmt.Sprintf(config, `["https://example.com"]`), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_repository_custom_property.test", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + }, + }, + { + Config: fmt.Sprintf(config, `["xxxx"]`), + ExpectError: regexp.MustCompile(`URL must be absolute`), + }, + { + Config: fmt.Sprintf(config, `["https://example.com/test"]`), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("github_repository_custom_property.test", plancheck.ResourceActionUpdate), + }, + }, }, { ResourceName: "github_repository_custom_property.test", @@ -255,36 +255,65 @@ func TestAccGithubRepositoryCustomProperty(t *testing.T) { }) }) - t.Run("fails_when_property_value_contains_empty_string", func(t *testing.T) { + t.Run("string", func(t *testing.T) { + t.Parallel() + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) - propertyName := fmt.Sprintf("tf-acc-test-property-%s", randomID) + propertyName := fmt.Sprintf("%sproperty-%s", testResourcePrefix, randomID) config := fmt.Sprintf(` - resource "github_organization_custom_properties" "test" { - description = "Test Description" - property_name = "%s" - value_type = "string" - } - resource "github_repository" "test" { - name = "%s" - auto_init = true - } - resource "github_repository_custom_property" "test" { - repository = github_repository.test.name - property_name = github_organization_custom_properties.test.property_name - property_type = github_organization_custom_properties.test.value_type - property_value = [""] - } - `, propertyName, repoName) +resource "github_organization_custom_properties" "test" { + description = "Test Description" + property_name = "%s" + value_type = "string" +} + +resource "github_repository" "test" { + name = "%s" + auto_init = true +} + +resource "github_repository_custom_property" "test" { + repository = github_repository.test.name + property_name = github_organization_custom_properties.test.property_name + property_type = github_organization_custom_properties.test.value_type + property_value = %%s +} +`, propertyName, repoName) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: config, + Config: fmt.Sprintf(config, `[]`), + ExpectError: regexp.MustCompile(`Not enough list items`), + PlanOnly: true, + }, + { + Config: fmt.Sprintf(config, `[""]`), ExpectError: regexp.MustCompile(`to not be an empty string`), + PlanOnly: true, + }, + { + Config: fmt.Sprintf(config, `["text"]`), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_repository_custom_property.test", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + }, + }, + { + Config: fmt.Sprintf(config, `["new text"]`), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("github_repository_custom_property.test", plancheck.ResourceActionUpdate), + }, + }, + }, + { + ResourceName: "github_repository_custom_property.test", + ImportState: true, + ImportStateVerify: true, }, }, }) From e1fe18ed5f1508deaca110746ba0707ce1e3431d Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Fri, 5 Jun 2026 13:19:03 +0100 Subject: [PATCH 3/6] fixup! fix: Refactor repository custom property --- docs/resources/repository_custom_property.md | 6 +++--- .../github_repository_custom_property/resource_1.tf | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/resources/repository_custom_property.md b/docs/resources/repository_custom_property.md index 5db27cb1ff..af783a12a9 100644 --- a/docs/resources/repository_custom_property.md +++ b/docs/resources/repository_custom_property.md @@ -13,16 +13,16 @@ For more information, see the [GitHub API documentation](https://docs.github.com ## Example Usage ```terraform -# NOTE: This assumes there already is a custom property defined on the org level called `my-cool-property` of type `string` +# NOTE: This assumes there already is a custom property defined on the org level called `my-cool-string` of type `string` resource "github_repository" "example" { name = "example" description = "My awesome codebase" } -resource "github_repository_custom_property" "string" { +resource "github_repository_custom_property" "example" { repository = github_repository.example.name - property_name = "my-cool-property" + property_name = "my-cool-string" property_type = "string" property_value = ["test"] } diff --git a/examples/resources/github_repository_custom_property/resource_1.tf b/examples/resources/github_repository_custom_property/resource_1.tf index 45fcb9caf2..11921d7303 100644 --- a/examples/resources/github_repository_custom_property/resource_1.tf +++ b/examples/resources/github_repository_custom_property/resource_1.tf @@ -1,13 +1,13 @@ -# NOTE: This assumes there already is a custom property defined on the org level called `my-cool-property` of type `string` +# NOTE: This assumes there already is a custom property defined on the org level called `my-cool-string` of type `string` resource "github_repository" "example" { name = "example" description = "My awesome codebase" } -resource "github_repository_custom_property" "string" { +resource "github_repository_custom_property" "example" { repository = github_repository.example.name - property_name = "my-cool-property" + property_name = "my-cool-string" property_type = "string" property_value = ["test"] } From 8ceea2238c287062d3e61cd674004b4ff736015a Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Fri, 5 Jun 2026 13:38:00 +0100 Subject: [PATCH 4/6] fixup! fix: Refactor repository custom property --- github/resource_github_repository_custom_property.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/github/resource_github_repository_custom_property.go b/github/resource_github_repository_custom_property.go index 5b1c96db43..a38b04250a 100644 --- a/github/resource_github_repository_custom_property.go +++ b/github/resource_github_repository_custom_property.go @@ -78,6 +78,10 @@ func resourceGithubRepositoryCustomProperty() *schema.Resource { func resourceGithubRepositoryCustomPropertyDiff(ctx context.Context, d *schema.ResourceDiff, _ any) error { tflog.Debug(ctx, "Diffing GitHub repository custom property") + if !d.NewValueKnown("property_type") || !d.NewValueKnown("property_value") { + return nil + } + propertyTypeVal, _ := d.Get("property_type").(string) propertyType := github.PropertyValueType(propertyTypeVal) propertyValueVal, _ := d.Get("property_value").(*schema.Set) @@ -248,6 +252,9 @@ func resourceGithubRepositoryCustomPropertyDelete(ctx context.Context, d *schema _, err := client.Repositories.CreateOrUpdateCustomProperties(ctx, owner, repoName, []*github.CustomPropertyValue{&customProperty}) if err != nil { + if err, ok := errors.AsType[*github.ErrorResponse](err); ok && err.Response.StatusCode == 404 { + return nil + } return diag.FromErr(err) } From f4677689e8f7d0e91d5c3b7befd58cacc34eb366 Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Fri, 5 Jun 2026 13:45:31 +0100 Subject: [PATCH 5/6] fixup! fix: Refactor repository custom property --- github/acc_helpers_test.go | 80 +++++++++ ...ource_github_repository_custom_property.go | 6 - ...ub_repository_custom_property_migration.go | 6 +- ..._github_repository_custom_property_test.go | 158 +++++++----------- 4 files changed, 139 insertions(+), 111 deletions(-) create mode 100644 github/acc_helpers_test.go diff --git a/github/acc_helpers_test.go b/github/acc_helpers_test.go new file mode 100644 index 0000000000..af95e9c55b --- /dev/null +++ b/github/acc_helpers_test.go @@ -0,0 +1,80 @@ +package github + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/google/go-github/v88/github" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" +) + +const testRandomIDLength = 5 + +func mustCreateTestOrganizationRepositoryCustomProperty(t *testing.T, valType string, allowed []string) *github.CustomProperty { + t.Helper() + + meta, err := getTestMeta() + if err != nil { + t.Fatalf("failed to get test meta: %v", err) + } + + randomID := acctest.RandString(testRandomIDLength) + name := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + + req := &github.CustomProperty{ + PropertyName: &name, + ValueType: github.PropertyValueType(valType), + AllowedValues: allowed, + } + + prop, _, err := meta.v3client.Organizations.CreateOrUpdateCustomProperty(t.Context(), meta.name, name, req) + if err != nil { + t.Fatalf("failed to create test organization repository custom property: %v", err) + } + + t.Cleanup(func() { + if _, err := meta.v3client.Organizations.RemoveCustomProperty(context.Background(), meta.name, name); err != nil { + if err, ok := errors.AsType[*github.ErrorResponse](err); ok && err.Response.StatusCode == 404 { + return + } + t.Logf("failed to delete test organization repository custom property %s: %v", name, err) + } + }) + + return prop +} + +func mustCreateTestRepository(t *testing.T) *github.Repository { + t.Helper() + + meta, err := getTestMeta() + if err != nil { + t.Fatalf("failed to get test meta: %v", err) + } + + randomID := acctest.RandString(testRandomIDLength) + name := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + + req := &github.Repository{ + Name: &name, + AutoInit: new(true), + } + + repo, _, err := meta.v3client.Repositories.Create(t.Context(), meta.name, req) + if err != nil { + t.Fatalf("failed to create test repository: %v", err) + } + + t.Cleanup(func() { + if _, err := meta.v3client.Repositories.Delete(context.Background(), meta.name, name); err != nil { + if err, ok := errors.AsType[*github.ErrorResponse](err); ok && err.Response.StatusCode == 404 { + return + } + t.Logf("failed to delete test repository %s: %v", name, err) + } + }) + + return repo +} diff --git a/github/resource_github_repository_custom_property.go b/github/resource_github_repository_custom_property.go index a38b04250a..66489657ae 100644 --- a/github/resource_github_repository_custom_property.go +++ b/github/resource_github_repository_custom_property.go @@ -186,12 +186,6 @@ func resourceGithubRepositoryCustomPropertyRead(ctx context.Context, d *schema.R return diag.FromErr(err) } - id, err := buildID(owner, repoName, propertyName) - if err != nil { - return diag.FromErr(err) - } - d.SetId(id) - if err := d.Set("property_value", propertyValue); err != nil { return diag.FromErr(err) } diff --git a/github/resource_github_repository_custom_property_migration.go b/github/resource_github_repository_custom_property_migration.go index 5d126c616d..9c411a3882 100644 --- a/github/resource_github_repository_custom_property_migration.go +++ b/github/resource_github_repository_custom_property_migration.go @@ -3,9 +3,9 @@ package github import ( "context" "fmt" - "log" "github.com/google/go-github/v88/github" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) @@ -53,7 +53,7 @@ func resourceGithubRepositoryCustomPropertyStateUpgradeV0(ctx context.Context, r client := meta.v3client owner := meta.name - log.Printf("[DEBUG] GitHub Repository Custom Property Attributes before migration: %#v", rawState) + tflog.Debug(ctx, "GitHub Repository Custom Property Attributes before migration: %#v", rawState) repoName, ok := rawState["repository"].(string) if !ok { @@ -68,7 +68,7 @@ func resourceGithubRepositoryCustomPropertyStateUpgradeV0(ctx context.Context, r repoID := int(repo.GetID()) rawState["repository_id"] = repoID - log.Printf("[DEBUG] GitHub Repository Custom Property Attributes after migration: %#v", rawState) + tflog.Debug(ctx, "GitHub Repository Custom Property Attributes after migration: %#v", rawState) return rawState, nil } diff --git a/github/resource_github_repository_custom_property_test.go b/github/resource_github_repository_custom_property_test.go index b92bafa749..667e0e43f6 100644 --- a/github/resource_github_repository_custom_property_test.go +++ b/github/resource_github_repository_custom_property_test.go @@ -5,7 +5,6 @@ import ( "regexp" "testing" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/knownvalue" "github.com/hashicorp/terraform-plugin-testing/plancheck" @@ -19,39 +18,30 @@ func TestAccGithubRepositoryCustomProperty(t *testing.T) { t.Run("single_select", func(t *testing.T) { t.Parallel() - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) - propertyName := fmt.Sprintf("%sproperty-%s", testResourcePrefix, randomID) - option1 := "option1" - option2 := "option2" + prop := mustCreateTestOrganizationRepositoryCustomProperty(t, "single_select", []string{"option1", "option2"}) + repo := mustCreateTestRepository(t) + allowed := prop.GetAllowedValues() config := fmt.Sprintf(` -resource "github_organization_custom_properties" "test" { - allowed_values = ["%s", "%s"] - description = "Test Description" - property_name = "%s" - value_type = "single_select" -} - -resource "github_repository" "test" { - name = "%s" - auto_init = true -} - resource "github_repository_custom_property" "test" { - repository = github_repository.test.name - property_name = github_organization_custom_properties.test.property_name - property_type = github_organization_custom_properties.test.value_type + repository = "%s" + property_name = "%s" + property_type = "%s" property_value = %%s } -`, option1, option2, propertyName, repoName) +`, repo.GetName(), prop.GetPropertyName(), prop.GetValueType()) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: fmt.Sprintf(config, fmt.Sprintf(`["%s"]`, option1)), + Config: fmt.Sprintf(config, `[]`), + ExpectError: regexp.MustCompile(`Not enough list items`), + PlanOnly: true, + }, + { + Config: fmt.Sprintf(config, fmt.Sprintf(`["%s"]`, allowed[0])), ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue("github_repository_custom_property.test", tfjsonpath.New("repository_id"), knownvalue.NotNull()), }, @@ -61,7 +51,7 @@ resource "github_repository_custom_property" "test" { ExpectError: regexp.MustCompile(`is not allowed for property`), }, { - Config: fmt.Sprintf(config, fmt.Sprintf(`["%s"]`, option2)), + Config: fmt.Sprintf(config, fmt.Sprintf(`["%s"]`, allowed[1])), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectResourceAction("github_repository_custom_property.test", plancheck.ResourceActionUpdate), @@ -80,40 +70,30 @@ resource "github_repository_custom_property" "test" { t.Run("multi_select", func(t *testing.T) { t.Parallel() - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) - propertyName := fmt.Sprintf("%sproperty-%s", testResourcePrefix, randomID) - option1 := "option1" - option2 := "option2" - option3 := "option3" + prop := mustCreateTestOrganizationRepositoryCustomProperty(t, "multi_select", []string{"option1", "option2", "option3"}) + repo := mustCreateTestRepository(t) + allowed := prop.GetAllowedValues() config := fmt.Sprintf(` -resource "github_organization_custom_properties" "test" { - allowed_values = ["%s", "%s", "%s"] - description = "Test Description" - property_name = "%s" - value_type = "multi_select" -} - -resource "github_repository" "test" { - name = "%s" - auto_init = true -} - resource "github_repository_custom_property" "test" { - repository = github_repository.test.name - property_name = github_organization_custom_properties.test.property_name - property_type = github_organization_custom_properties.test.value_type + repository = "%s" + property_name = "%s" + property_type = "%s" property_value = %%s } -`, option1, option2, option3, propertyName, repoName) +`, repo.GetName(), prop.GetPropertyName(), prop.GetValueType()) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: fmt.Sprintf(config, fmt.Sprintf(`["%s", "%s"]`, option1, option2)), + Config: fmt.Sprintf(config, `[]`), + ExpectError: regexp.MustCompile(`Not enough list items`), + PlanOnly: true, + }, + { + Config: fmt.Sprintf(config, fmt.Sprintf(`["%s", "%s"]`, allowed[0], allowed[1])), ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue("github_repository_custom_property.test", tfjsonpath.New("repository_id"), knownvalue.NotNull()), }, @@ -123,7 +103,7 @@ resource "github_repository_custom_property" "test" { ExpectError: regexp.MustCompile(`is not allowed for property`), }, { - Config: fmt.Sprintf(config, fmt.Sprintf(`["%s"]`, option3)), + Config: fmt.Sprintf(config, fmt.Sprintf(`["%s"]`, allowed[2])), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectResourceAction("github_repository_custom_property.test", plancheck.ResourceActionUpdate), @@ -142,34 +122,27 @@ resource "github_repository_custom_property" "test" { t.Run("true_false", func(t *testing.T) { t.Parallel() - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) - propertyName := fmt.Sprintf("%sproperty-%s", testResourcePrefix, randomID) + prop := mustCreateTestOrganizationRepositoryCustomProperty(t, "true_false", nil) + repo := mustCreateTestRepository(t) config := fmt.Sprintf(` -resource "github_organization_custom_properties" "test" { - description = "Test Description" - property_name = "%s" - value_type = "true_false" -} - -resource "github_repository" "test" { - name = "%s" - auto_init = true -} - resource "github_repository_custom_property" "test" { - repository = github_repository.test.name - property_name = github_organization_custom_properties.test.property_name - property_type = github_organization_custom_properties.test.value_type + repository = "%s" + property_name = "%s" + property_type = "%s" property_value = %%s } -`, propertyName, repoName) +`, repo.GetName(), prop.GetPropertyName(), prop.GetValueType()) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, `[]`), + ExpectError: regexp.MustCompile(`Not enough list items`), + PlanOnly: true, + }, { Config: fmt.Sprintf(config, `["true"]`), ConfigStateChecks: []statecheck.StateCheck{ @@ -200,34 +173,27 @@ resource "github_repository_custom_property" "test" { t.Run("url", func(t *testing.T) { t.Parallel() - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) - propertyName := fmt.Sprintf("%sproperty-%s", testResourcePrefix, randomID) + prop := mustCreateTestOrganizationRepositoryCustomProperty(t, "url", nil) + repo := mustCreateTestRepository(t) config := fmt.Sprintf(` -resource "github_organization_custom_properties" "test" { - description = "Test Description" - property_name = "%s" - value_type = "url" -} - -resource "github_repository" "test" { - name = "%s" - auto_init = true -} - resource "github_repository_custom_property" "test" { - repository = github_repository.test.name - property_name = github_organization_custom_properties.test.property_name - property_type = github_organization_custom_properties.test.value_type + repository = "%s" + property_name = "%s" + property_type = "%s" property_value = %%s } -`, propertyName, repoName) +`, repo.GetName(), prop.GetPropertyName(), prop.GetValueType()) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, `[]`), + ExpectError: regexp.MustCompile(`Not enough list items`), + PlanOnly: true, + }, { Config: fmt.Sprintf(config, `["https://example.com"]`), ConfigStateChecks: []statecheck.StateCheck{ @@ -258,29 +224,17 @@ resource "github_repository_custom_property" "test" { t.Run("string", func(t *testing.T) { t.Parallel() - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) - propertyName := fmt.Sprintf("%sproperty-%s", testResourcePrefix, randomID) + prop := mustCreateTestOrganizationRepositoryCustomProperty(t, "string", nil) + repo := mustCreateTestRepository(t) config := fmt.Sprintf(` -resource "github_organization_custom_properties" "test" { - description = "Test Description" - property_name = "%s" - value_type = "string" -} - -resource "github_repository" "test" { - name = "%s" - auto_init = true -} - resource "github_repository_custom_property" "test" { - repository = github_repository.test.name - property_name = github_organization_custom_properties.test.property_name - property_type = github_organization_custom_properties.test.value_type + repository = "%s" + property_name = "%s" + property_type = "%s" property_value = %%s } -`, propertyName, repoName) +`, repo.GetName(), prop.GetPropertyName(), prop.GetValueType()) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, From 60f00869f916ec9addb1a5819f9b8fca1834843e Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Fri, 5 Jun 2026 18:06:48 +0100 Subject: [PATCH 6/6] fixup! fix: Refactor repository custom property --- github/acc_helpers_test.go | 26 ++++ github/data_source_github_app_token_test.go | 2 +- github/helpers_test.go | 15 -- ...pository_custom_property_migration_test.go | 131 +++++++++++------- ...e_github_repository_file_migration_test.go | 2 +- github/transport_test.go | 14 +- 6 files changed, 117 insertions(+), 73 deletions(-) delete mode 100644 github/helpers_test.go diff --git a/github/acc_helpers_test.go b/github/acc_helpers_test.go index af95e9c55b..607cccd8b9 100644 --- a/github/acc_helpers_test.go +++ b/github/acc_helpers_test.go @@ -2,6 +2,7 @@ package github import ( "context" + "encoding/json" "errors" "fmt" "testing" @@ -12,6 +13,31 @@ import ( const testRandomIDLength = 5 +func mustGetTestMockResponse(t *testing.T, uri string, statusCode int, body any) *mockResponse { + resp := &mockResponse{ + ExpectedUri: uri, + StatusCode: statusCode, + } + + if body != nil { + bodyBytes, err := json.Marshal(body) + if err != nil { + t.Fatalf("failed to marshal mock response body: %v", err) + } + resp.ResponseBody = string(bodyBytes) + } + + return resp +} + +func mustCreateTestGitHubClient(t *testing.T, baseURL string, opts ...github.ClientOptionsFunc) *github.Client { + client, err := github.NewClient(append([]github.ClientOptionsFunc{github.WithURLs(&baseURL, nil)}, opts...)...) + if err != nil { + t.Fatalf("failed to create GitHub client: %s", err) + } + return client +} + func mustCreateTestOrganizationRepositoryCustomProperty(t *testing.T, valType string, allowed []string) *github.CustomProperty { t.Helper() diff --git a/github/data_source_github_app_token_test.go b/github/data_source_github_app_token_test.go index 89253e7048..a9099c077a 100644 --- a/github/data_source_github_app_token_test.go +++ b/github/data_source_github_app_token_test.go @@ -32,7 +32,7 @@ func TestAccGithubAppTokenDataSource(t *testing.T) { }) defer ts.Close() - client := mustGitHubClient(t, ts.URL) + client := mustCreateTestGitHubClient(t, ts.URL) meta := &Owner{ name: owner, diff --git a/github/helpers_test.go b/github/helpers_test.go deleted file mode 100644 index 9d473eb99a..0000000000 --- a/github/helpers_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package github - -import ( - "testing" - - "github.com/google/go-github/v88/github" -) - -func mustGitHubClient(t *testing.T, baseURL string, opts ...github.ClientOptionsFunc) *github.Client { - client, err := github.NewClient(append([]github.ClientOptionsFunc{github.WithURLs(&baseURL, nil)}, opts...)...) - if err != nil { - t.Fatalf("failed to create GitHub client: %s", err) - } - return client -} diff --git a/github/resource_github_repository_custom_property_migration_test.go b/github/resource_github_repository_custom_property_migration_test.go index 138afb872a..851dfebcd3 100644 --- a/github/resource_github_repository_custom_property_migration_test.go +++ b/github/resource_github_repository_custom_property_migration_test.go @@ -1,51 +1,84 @@ package github -// TODO: Enable this test once we have a pattern to create a mock client for the test. - -// import ( -// "context" -// "reflect" -// "testing" -// ) - -// func Test_resourceGithubCustomPropertyStateUpgradeV0(t *testing.T) { -// t.Parallel() - -// for _, d := range []struct { -// testName string -// rawState map[string]any -// want map[string]any -// shouldError bool -// }{ -// { -// testName: "migrates v0 to v1", -// rawState: map[string]any{ -// "id": "my-org:my-repo:my-property", -// "repository": "my-repo", -// "property_name": "my-property", -// "property_value": "my-value", -// }, -// want: map[string]any{ -// "id": "my-org:my-repo:my-property", -// "repository": "my-repo", -// "repository_id": 123456, -// "property_name": "my-property", -// "property_value": "my-value", -// }, -// shouldError: false, -// }, -// } { -// t.Run(d.testName, func(t *testing.T) { -// t.Parallel() - -// got, err := resourceGithubCustomPropertyStateUpgradeV0(t.Context(), d.rawState, nil) -// if (err != nil) != d.shouldError { -// t.Fatalf("unexpected error state") -// } - -// if !d.shouldError && !reflect.DeepEqual(got, d.want) { -// t.Fatalf("got %+v, want %+v", got, d.want) -// } -// }) -// } -// } +import ( + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-github/v88/github" +) + +func Test_resourceGithubCustomPropertyStateUpgradeV0(t *testing.T) { + for _, tt := range []struct { + testName string + statusCode int + body *github.Repository + rawState map[string]any + want map[string]any + wantErr *string + }{ + { + testName: "succeeds_if_repo_found", + statusCode: 200, + body: &github.Repository{ + ID: new(int64(123456)), + }, + rawState: map[string]any{ + "id": "my-org:my-repo:my-property", + "repository": "my-repo", + "property_name": "my-property", + "property_value": "my-value", + }, + want: map[string]any{ + "id": "my-org:my-repo:my-property", + "repository": "my-repo", + "repository_id": 123456, + "property_name": "my-property", + "property_value": "my-value", + }, + }, + { + testName: "fails_if_repo_not_found", + statusCode: 404, + body: nil, + rawState: map[string]any{ + "id": "my-org:my-repo:my-property", + "repository": "my-repo", + "property_name": "my-property", + "property_value": "my-value", + }, + wantErr: new("failed to retrieve repository"), + }, + } { + t.Run(tt.testName, func(t *testing.T) { + ts := githubApiMock([]*mockResponse{mustGetTestMockResponse(t, "/repos/my-org/my-repo", tt.statusCode, tt.body)}) + defer ts.Close() + + meta := &Owner{ + name: "my-org", + v3client: mustCreateTestGitHubClient(t, ts.URL), + } + + got, err := resourceGithubRepositoryCustomPropertyStateUpgradeV0(t.Context(), tt.rawState, meta) + if err != nil { + if tt.wantErr == nil { + t.Fatalf("unexpected error: %s", err) + } + + if !regexp.MustCompile(regexp.QuoteMeta(*tt.wantErr)).MatchString(err.Error()) { + t.Fatalf("unexpected error: %s", err) + } + + return + } + + if tt.wantErr != nil { + t.Fatalf("expected error: %s", *tt.wantErr) + } + + if diff := cmp.Diff(got, tt.want); diff != "" { + t.Fatalf("got %+v, want %+v: %s", got, tt.want, diff) + } + }) + } +} diff --git a/github/resource_github_repository_file_migration_test.go b/github/resource_github_repository_file_migration_test.go index a9081639cc..65cc041ecb 100644 --- a/github/resource_github_repository_file_migration_test.go +++ b/github/resource_github_repository_file_migration_test.go @@ -165,7 +165,7 @@ func Test_resourceGithubRepositoryFileStateUpgradeV0toV1(t *testing.T) { ts := githubApiMock(buildMockResponsesForRepositoryFileMigrationV0toV1(meta.name, wantRepositoryName, wantRepositoryID)) defer ts.Close() - client := mustGitHubClient(t, ts.URL) + client := mustCreateTestGitHubClient(t, ts.URL) meta.v3client = client got, err := resourceGithubRepositoryFileStateUpgradeV0(t.Context(), d.rawState, meta) diff --git a/github/transport_test.go b/github/transport_test.go index 55fb6b98ca..8409470700 100644 --- a/github/transport_test.go +++ b/github/transport_test.go @@ -28,7 +28,7 @@ func TestEtagTransport(t *testing.T) { }) defer ts.Close() - client := mustGitHubClient(t, ts.URL, github.WithTransport(NewEtagTransport(http.DefaultTransport))) + client := mustCreateTestGitHubClient(t, ts.URL, github.WithTransport(NewEtagTransport(http.DefaultTransport))) ctx := context.WithValue(t.Context(), ctxEtag, "something") r, _, err := client.Repositories.Get(ctx, "test", "blah") if err != nil { @@ -135,7 +135,7 @@ func TestRateLimitTransport_abuseLimit_get(t *testing.T) { }) defer ts.Close() - client := mustGitHubClient(t, ts.URL, github.WithTransport(NewRateLimitTransport(http.DefaultTransport))) + client := mustCreateTestGitHubClient(t, ts.URL, github.WithTransport(NewRateLimitTransport(http.DefaultTransport))) ctx := context.WithValue(t.Context(), ctxId, t.Name()) r, _, err := client.Repositories.Get(ctx, "test", "blah") @@ -164,7 +164,7 @@ func TestRateLimitTransport_abuseLimit_get_cancelled(t *testing.T) { }) defer ts.Close() - client := mustGitHubClient(t, ts.URL, github.WithTransport(NewRateLimitTransport(http.DefaultTransport))) + client := mustCreateTestGitHubClient(t, ts.URL, github.WithTransport(NewRateLimitTransport(http.DefaultTransport))) ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond) defer cancel() @@ -206,7 +206,7 @@ func TestRateLimitTransport_abuseLimit_post(t *testing.T) { }) defer ts.Close() - client := mustGitHubClient(t, ts.URL, github.WithTransport(NewRateLimitTransport(http.DefaultTransport))) + client := mustCreateTestGitHubClient(t, ts.URL, github.WithTransport(NewRateLimitTransport(http.DefaultTransport))) ctx := context.WithValue(t.Context(), ctxId, t.Name()) r, _, err := client.Repositories.Create(ctx, "tada", &github.Repository{ @@ -261,7 +261,7 @@ func TestRateLimitTransport_abuseLimit_post_error(t *testing.T) { }) defer ts.Close() - client := mustGitHubClient(t, ts.URL, github.WithTransport(NewRateLimitTransport(http.DefaultTransport))) + client := mustCreateTestGitHubClient(t, ts.URL, github.WithTransport(NewRateLimitTransport(http.DefaultTransport))) ctx := context.WithValue(t.Context(), ctxId, t.Name()) _, _, err := client.Repositories.Create(ctx, "tada", &github.Repository{ @@ -389,7 +389,7 @@ func TestRetryTransport_retry_post_error(t *testing.T) { }) defer ts.Close() - client := mustGitHubClient(t, ts.URL, github.WithTransport(NewRetryTransport(http.DefaultTransport, WithMaxRetries(1)))) + client := mustCreateTestGitHubClient(t, ts.URL, github.WithTransport(NewRetryTransport(http.DefaultTransport, WithMaxRetries(1)))) ctx := context.WithValue(t.Context(), ctxId, t.Name()) _, _, err := client.Repositories.Create(ctx, "tada", &github.Repository{ @@ -447,7 +447,7 @@ func TestRetryTransport_retry_post_success(t *testing.T) { }) defer ts.Close() - client := mustGitHubClient(t, ts.URL, github.WithTransport(NewRetryTransport(http.DefaultTransport, WithMaxRetries(2), WithRetryDelay(time.Second)))) + client := mustCreateTestGitHubClient(t, ts.URL, github.WithTransport(NewRetryTransport(http.DefaultTransport, WithMaxRetries(2), WithRetryDelay(time.Second)))) ctx := context.WithValue(t.Context(), ctxId, t.Name()) _, _, err := client.Repositories.Create(ctx, "tada", &github.Repository{