diff --git a/github/data_source_github_enterprise_custom_property.go b/github/data_source_github_enterprise_custom_property.go new file mode 100644 index 0000000000..392819d1f1 --- /dev/null +++ b/github/data_source_github_enterprise_custom_property.go @@ -0,0 +1,114 @@ +package github + +import ( + "context" + + "github.com/google/go-github/v84/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceGithubEnterpriseCustomProperty() *schema.Resource { + return &schema.Resource{ + Description: "Use this data source to retrieve information about a custom property definition for a GitHub enterprise.", + + ReadContext: dataSourceGithubEnterpriseCustomPropertyRead, + + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + Description: "The slug of the enterprise.", + }, + "property_name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the custom property.", + }, + "value_type": { + Type: schema.TypeString, + Computed: true, + Description: "The type of the value for the property.", + }, + "required": { + Type: schema.TypeBool, + Computed: true, + Description: "Whether the custom property is required.", + }, + "default_values": { + Type: schema.TypeList, + Computed: true, + Description: "The default value(s) of the custom property.", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "description": { + Type: schema.TypeString, + Computed: true, + Description: "A short description of the custom property.", + }, + "allowed_values": { + Type: schema.TypeList, + Computed: true, + Description: "An ordered list of allowed values for the property.", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "values_editable_by": { + Type: schema.TypeString, + Computed: true, + Description: "Who can edit the values of the property. Can be one of 'org_actors' or 'org_and_repo_actors'.", + }, + }, + } +} + +func dataSourceGithubEnterpriseCustomPropertyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + + enterpriseSlug := d.Get("enterprise_slug").(string) + propertyName := d.Get("property_name").(string) + + property, _, err := client.Enterprise.GetCustomProperty(ctx, enterpriseSlug, propertyName) + if err != nil { + return diag.Errorf("error reading enterprise custom property %s/%s: %v", enterpriseSlug, propertyName, err) + } + + var defaultValues []string + if property.ValueType == github.PropertyValueTypeMultiSelect { + if vals, ok := property.DefaultValueStrings(); ok { + defaultValues = vals + } + } else { + if val, ok := property.DefaultValueString(); ok { + defaultValues = []string{val} + } + } + + d.SetId(buildTwoPartID(enterpriseSlug, propertyName)) + + if err := d.Set("enterprise_slug", enterpriseSlug); err != nil { + return diag.FromErr(err) + } + if err := d.Set("property_name", property.GetPropertyName()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("value_type", string(property.ValueType)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("required", property.GetRequired()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("default_values", defaultValues); err != nil { + return diag.FromErr(err) + } + if err := d.Set("description", property.GetDescription()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("allowed_values", property.AllowedValues); err != nil { + return diag.FromErr(err) + } + if err := d.Set("values_editable_by", property.GetValuesEditableBy()); err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/github/provider.go b/github/provider.go index 1baf0263ad..5eae9e842e 100644 --- a/github/provider.go +++ b/github/provider.go @@ -219,6 +219,7 @@ func Provider() *schema.Provider { "github_enterprise_actions_workflow_permissions": resourceGithubEnterpriseActionsWorkflowPermissions(), "github_actions_organization_workflow_permissions": resourceGithubActionsOrganizationWorkflowPermissions(), "github_enterprise_security_analysis_settings": resourceGithubEnterpriseSecurityAnalysisSettings(), + "github_enterprise_custom_property": resourceGithubEnterpriseCustomProperties(), "github_workflow_repository_permissions": resourceGithubWorkflowRepositoryPermissions(), }, @@ -296,6 +297,7 @@ func Provider() *schema.Provider { "github_user_external_identity": dataSourceGithubUserExternalIdentity(), "github_users": dataSourceGithubUsers(), "github_enterprise": dataSourceGithubEnterprise(), + "github_enterprise_custom_property": dataSourceGithubEnterpriseCustomProperty(), "github_repository_environment_deployment_policies": dataSourceGithubRepositoryEnvironmentDeploymentPolicies(), }, } diff --git a/github/resource_github_enterprise_custom_properties.go b/github/resource_github_enterprise_custom_properties.go new file mode 100644 index 0000000000..81dbe27b26 --- /dev/null +++ b/github/resource_github_enterprise_custom_properties.go @@ -0,0 +1,244 @@ +package github + +import ( + "context" + "log" + "net/http" + + "github.com/google/go-github/v84/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 resourceGithubEnterpriseCustomProperties() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceGithubEnterpriseCustomPropertiesCreate, + ReadContext: resourceGithubEnterpriseCustomPropertiesRead, + UpdateContext: resourceGithubEnterpriseCustomPropertiesUpdate, + DeleteContext: resourceGithubEnterpriseCustomPropertiesDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The slug of the enterprise.", + }, + "property_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The name of the custom property.", + }, + "value_type": { + Type: schema.TypeString, + Required: true, + Description: "The type of the value for the property. Can be one of: 'string', 'single_select', 'multi_select', 'true_false', 'url'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{string(github.PropertyValueTypeString), string(github.PropertyValueTypeSingleSelect), string(github.PropertyValueTypeMultiSelect), string(github.PropertyValueTypeTrueFalse), string(github.PropertyValueTypeURL)}, false)), + }, + "required": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether the custom property is required.", + }, + "default_values": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Description: "The default value(s) of the custom property. For 'multi_select' properties, multiple values may be specified. For all other types, provide a single value.", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "A short description of the custom property.", + }, + "allowed_values": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Description: "An ordered list of allowed values for the property. Only applicable to 'single_select' and 'multi_select' types.", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "values_editable_by": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Who can edit the values of the property. Can be one of: 'org_actors', 'org_and_repo_actors'. Defaults to 'org_actors'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"org_actors", "org_and_repo_actors"}, false)), + }, + }, + } +} + +func resourceGithubEnterpriseCustomPropertiesCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + + enterpriseSlug := d.Get("enterprise_slug").(string) + propertyName := d.Get("property_name").(string) + + property := buildEnterpriseCustomProperty(d) + + _, _, err := client.Enterprise.CreateOrUpdateCustomProperty(ctx, enterpriseSlug, propertyName, property) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(buildTwoPartID(enterpriseSlug, propertyName)) + return resourceGithubEnterpriseCustomPropertiesRead(ctx, d, meta) +} + +func resourceGithubEnterpriseCustomPropertiesRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + + enterpriseSlug, propertyName, err := parseTwoPartID(d.Id(), "enterprise_slug", "property_name") + if err != nil { + return diag.FromErr(err) + } + + property, resp, err := client.Enterprise.GetCustomProperty(ctx, enterpriseSlug, propertyName) + if err != nil { + if resp != nil && resp.StatusCode == http.StatusNotFound { + log.Printf("[INFO] Removing enterprise custom property %s/%s from state because it no longer exists", enterpriseSlug, propertyName) + d.SetId("") + return nil + } + return diag.FromErr(err) + } + + if err := d.Set("enterprise_slug", enterpriseSlug); err != nil { + return diag.FromErr(err) + } + if err := d.Set("property_name", property.GetPropertyName()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("value_type", string(property.ValueType)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("required", property.GetRequired()); err != nil { + return diag.FromErr(err) + } + + var defaultValues []string + if property.ValueType == github.PropertyValueTypeMultiSelect { + if vals, ok := property.DefaultValueStrings(); ok { + defaultValues = vals + } + } else { + if val, ok := property.DefaultValueString(); ok { + defaultValues = []string{val} + } + } + if err := d.Set("default_values", defaultValues); err != nil { + return diag.FromErr(err) + } + if err := d.Set("description", property.GetDescription()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("allowed_values", property.AllowedValues); err != nil { + return diag.FromErr(err) + } + if err := d.Set("values_editable_by", property.GetValuesEditableBy()); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceGithubEnterpriseCustomPropertiesUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + + enterpriseSlug, propertyName, err := parseTwoPartID(d.Id(), "enterprise_slug", "property_name") + if err != nil { + return diag.FromErr(err) + } + + property := buildEnterpriseCustomProperty(d) + + _, _, err = client.Enterprise.CreateOrUpdateCustomProperty(ctx, enterpriseSlug, propertyName, property) + if err != nil { + return diag.FromErr(err) + } + + return resourceGithubEnterpriseCustomPropertiesRead(ctx, d, meta) +} + +func resourceGithubEnterpriseCustomPropertiesDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + + enterpriseSlug, propertyName, err := parseTwoPartID(d.Id(), "enterprise_slug", "property_name") + if err != nil { + return diag.FromErr(err) + } + + resp, err := client.Enterprise.RemoveCustomProperty(ctx, enterpriseSlug, propertyName) + if err != nil { + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil + } + return diag.FromErr(err) + } + + return nil +} + +func resourceGithubEnterpriseCustomPropertiesImport(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + enterpriseSlug, propertyName, err := parseTwoPartID(d.Id(), "enterprise_slug", "property_name") + if err != nil { + return nil, err + } + + if err := d.Set("enterprise_slug", enterpriseSlug); err != nil { + return nil, err + } + if err := d.Set("property_name", propertyName); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} + +func buildEnterpriseCustomProperty(d *schema.ResourceData) *github.CustomProperty { + propertyName := d.Get("property_name").(string) + valueType := github.PropertyValueType(d.Get("value_type").(string)) + required := d.Get("required").(bool) + description := d.Get("description").(string) + + rawAllowedValues := d.Get("allowed_values").([]any) + allowedValues := make([]string, 0, len(rawAllowedValues)) + for _, v := range rawAllowedValues { + allowedValues = append(allowedValues, v.(string)) + } + + property := &github.CustomProperty{ + PropertyName: &propertyName, + ValueType: valueType, + Required: &required, + Description: &description, + AllowedValues: allowedValues, + } + + rawDefaultValues := d.Get("default_values").([]any) + defaultValues := make([]string, 0, len(rawDefaultValues)) + for _, v := range rawDefaultValues { + defaultValues = append(defaultValues, v.(string)) + } + if len(defaultValues) > 0 { + if valueType == github.PropertyValueTypeMultiSelect { + property.DefaultValue = defaultValues + } else { + property.DefaultValue = defaultValues[0] + } + } + + if val, ok := d.GetOk("values_editable_by"); ok { + str := val.(string) + property.ValuesEditableBy = &str + } + + return property +} diff --git a/github/resource_github_enterprise_custom_properties_test.go b/github/resource_github_enterprise_custom_properties_test.go new file mode 100644 index 0000000000..42224444fa --- /dev/null +++ b/github/resource_github_enterprise_custom_properties_test.go @@ -0,0 +1,199 @@ +package github + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestAccGithubEnterpriseCustomPropertiesValidation(t *testing.T) { + t.Run("rejects invalid values_editable_by value", func(t *testing.T) { + config := fmt.Sprintf(` + resource "github_enterprise_custom_property" "test" { + enterprise_slug = "%s" + property_name = "%senterprise-prop-invalid-editable-by" + value_type = "string" + values_editable_by = "invalid_value" + }`, testAccConf.enterpriseSlug, testResourcePrefix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile("invalid_value"), + }, + }, + }) + }) + + t.Run("rejects invalid value_type", func(t *testing.T) { + config := fmt.Sprintf(` + resource "github_enterprise_custom_property" "test" { + enterprise_slug = "%s" + property_name = "%senterprise-prop-invalid-type" + value_type = "invalid_type" + }`, testAccConf.enterpriseSlug, testResourcePrefix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile("invalid_type"), + }, + }, + }) + }) +} + +func TestAccGithubEnterpriseCustomProperties(t *testing.T) { + t.Run("creates custom property without error", func(t *testing.T) { + config := fmt.Sprintf(` + resource "github_enterprise_custom_property" "test" { + enterprise_slug = "%s" + allowed_values = ["Test"] + description = "Test Description" + default_value = "Test" + property_name = "%senterprise-prop-create" + required = true + value_type = "single_select" + }`, testAccConf.enterpriseSlug, testResourcePrefix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_enterprise_custom_property.test", tfjsonpath.New("value_type"), knownvalue.StringExact("single_select")), + statecheck.ExpectKnownValue("github_enterprise_custom_property.test", tfjsonpath.New("required"), knownvalue.Bool(true)), + }, + }, + }, + }) + }) + + t.Run("creates and updates a custom property", func(t *testing.T) { + configBefore := fmt.Sprintf(` + resource "github_enterprise_custom_property" "test" { + enterprise_slug = "%s" + allowed_values = ["one"] + description = "Test Description" + property_name = "%senterprise-prop-update" + value_type = "single_select" + }`, testAccConf.enterpriseSlug, testResourcePrefix) + + configAfter := fmt.Sprintf(` + resource "github_enterprise_custom_property" "test" { + enterprise_slug = "%s" + allowed_values = ["one", "two"] + description = "Test Description Updated" + property_name = "%senterprise-prop-update" + value_type = "single_select" + }`, testAccConf.enterpriseSlug, testResourcePrefix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: configBefore, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_enterprise_custom_property.test", tfjsonpath.New("allowed_values"), knownvalue.ListSizeExact(1)), + }, + }, + { + Config: configAfter, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_enterprise_custom_property.test", tfjsonpath.New("allowed_values"), knownvalue.ListSizeExact(2)), + statecheck.ExpectKnownValue("github_enterprise_custom_property.test", tfjsonpath.New("description"), knownvalue.StringExact("Test Description Updated")), + }, + }, + }, + }) + }) + + t.Run("imports enterprise custom property without error", func(t *testing.T) { + config := fmt.Sprintf(` + resource "github_enterprise_custom_property" "test" { + enterprise_slug = "%s" + description = "Test Description Import" + property_name = "%senterprise-prop-import" + value_type = "string" + }`, testAccConf.enterpriseSlug, testResourcePrefix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_enterprise_custom_property.test", tfjsonpath.New("description"), knownvalue.StringExact("Test Description Import")), + }, + }, + { + ResourceName: "github_enterprise_custom_property.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) + + t.Run("creates custom property with values_editable_by", func(t *testing.T) { + config := fmt.Sprintf(` + resource "github_enterprise_custom_property" "test" { + enterprise_slug = "%s" + property_name = "%senterprise-prop-editable-by" + value_type = "string" + description = "Test property for values_editable_by" + values_editable_by = "org_and_repo_actors" + }`, testAccConf.enterpriseSlug, testResourcePrefix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_enterprise_custom_property.test", tfjsonpath.New("values_editable_by"), knownvalue.StringExact("org_and_repo_actors")), + }, + }, + }, + }) + }) + + t.Run("defaults values_editable_by to org_actors", func(t *testing.T) { + config := fmt.Sprintf(` + resource "github_enterprise_custom_property" "test" { + enterprise_slug = "%s" + property_name = "%senterprise-prop-default-editable-by" + value_type = "string" + description = "Test property without values_editable_by" + }`, testAccConf.enterpriseSlug, testResourcePrefix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_enterprise_custom_property.test", tfjsonpath.New("values_editable_by"), knownvalue.StringExact("org_actors")), + }, + }, + }, + }) + }) +} diff --git a/github/resource_github_repository.go b/github/resource_github_repository.go index e3c2ff339e..caf5d7fe55 100644 --- a/github/resource_github_repository.go +++ b/github/resource_github_repository.go @@ -408,6 +408,13 @@ func resourceGithubRepository() *schema.Resource { Default: false, Deprecated: "This is ignored as the provider now handles lack of permissions automatically. This field will be removed in a future version.", }, + "custom_properties": { + Type: schema.TypeMap, + Optional: true, + Computed: true, + Description: "Custom properties for the repository. Key/value pairs where values must be strings. Use this to satisfy organization-required custom properties at repository creation time.", + Elem: &schema.Schema{Type: schema.TypeString}, + }, "full_name": { Type: schema.TypeString, Computed: true, @@ -647,6 +654,10 @@ func resourceGithubRepositoryObject(d *schema.ResourceData) *github.Repository { } } + if v, ok := d.GetOk("custom_properties"); ok { + repository.CustomProperties = v.(map[string]any) + } + return repository } @@ -914,6 +925,19 @@ func resourceGithubRepositoryRead(ctx context.Context, d *schema.ResourceData, m } } + customPropertyValues, _, err := client.Repositories.GetAllCustomPropertyValues(ctx, owner, repoName) + if err == nil { + customProps := make(map[string]string, len(customPropertyValues)) + for _, v := range customPropertyValues { + if strVal, ok := v.Value.(string); ok { + customProps[v.PropertyName] = strVal + } + } + if err := d.Set("custom_properties", customProps); err != nil { + return diag.Errorf("error setting custom_properties: %s", err.Error()) + } + } + return nil } @@ -962,6 +986,30 @@ func resourceGithubRepositoryUpdate(ctx context.Context, d *schema.ResourceData, } d.SetId(repo.GetName()) // It's possible that `repo.GetName()` is different from `repoName` if the repository is renamed + if d.HasChange("custom_properties") { + newProps := d.Get("custom_properties").(map[string]interface{}) + if len(newProps) > 0 { + if err := setRepositoryCustomProperties(ctx, client, owner, repoName, newProps); err != nil { + return diag.FromErr(err) + } + } else { + // custom_properties was removed from config — clear any previously set values + oldRaw, _ := d.GetChange("custom_properties") + if oldProps, ok := oldRaw.(map[string]interface{}); ok && len(oldProps) > 0 { + clearValues := make([]*github.CustomPropertyValue, 0, len(oldProps)) + for k := range oldProps { + clearValues = append(clearValues, &github.CustomPropertyValue{ + PropertyName: k, + Value: nil, + }) + } + if _, err := client.Repositories.CreateOrUpdateCustomProperties(ctx, owner, repoName, clearValues); err != nil { + return diag.FromErr(err) + } + } + } + } + if d.HasChange("pages") && !d.IsNewResource() { opts := expandPagesUpdate(d.Get("pages").([]any)) if opts != nil { @@ -1122,6 +1170,18 @@ func expandPagesUpdate(input []any) *github.PagesUpdate { return update } +func setRepositoryCustomProperties(ctx context.Context, client *github.Client, owner, repoName string, props map[string]any) error { + customPropertyValues := make([]*github.CustomPropertyValue, 0, len(props)) + for k, v := range props { + customPropertyValues = append(customPropertyValues, &github.CustomPropertyValue{ + PropertyName: k, + Value: v, + }) + } + _, err := client.Repositories.CreateOrUpdateCustomProperties(ctx, owner, repoName, customPropertyValues) + return err +} + func flattenPages(pages *github.Pages) []any { if pages == nil { return []any{} diff --git a/github/resource_github_repository_custom_properties_test.go b/github/resource_github_repository_custom_properties_test.go new file mode 100644 index 0000000000..63c25f3c09 --- /dev/null +++ b/github/resource_github_repository_custom_properties_test.go @@ -0,0 +1,273 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccGithubRepositoryCustomProperties(t *testing.T) { + t.Run("sets custom_properties at creation time", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + propertyName := fmt.Sprintf("tfacc%s", randomID) + repoName := fmt.Sprintf("%scustomprops-%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` + resource "github_organization_custom_properties" "test" { + allowed_values = ["alpha", "beta"] + property_name = "%[1]s" + value_type = "single_select" + } + + resource "github_repository" "test" { + name = "%[2]s" + auto_init = true + + custom_properties = { + (github_organization_custom_properties.test.property_name) = "alpha" + } + } + `, propertyName, repoName) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_repository.test", "custom_properties.%", "1"), + resource.TestCheckResourceAttr( + "github_repository.test", + fmt.Sprintf("custom_properties.%s", propertyName), + "alpha", + ), + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + }) + + t.Run("updates a custom property value", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + propertyName := fmt.Sprintf("tfacc%s", randomID) + repoName := fmt.Sprintf("%scustomprops-%s", testResourcePrefix, randomID) + + configTemplate := ` + resource "github_organization_custom_properties" "test" { + allowed_values = ["alpha", "beta"] + property_name = "%[1]s" + value_type = "single_select" + } + + resource "github_repository" "test" { + name = "%[2]s" + auto_init = true + + custom_properties = { + (github_organization_custom_properties.test.property_name) = "%[3]s" + } + } + ` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(configTemplate, propertyName, repoName, "alpha"), + Check: resource.TestCheckResourceAttr( + "github_repository.test", + fmt.Sprintf("custom_properties.%s", propertyName), + "alpha", + ), + }, + { + Config: fmt.Sprintf(configTemplate, propertyName, repoName, "beta"), + Check: resource.TestCheckResourceAttr( + "github_repository.test", + fmt.Sprintf("custom_properties.%s", propertyName), + "beta", + ), + }, + }, + }) + }) + + t.Run("removes a specific property key", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + propertyA := fmt.Sprintf("tfacca%s", randomID) + propertyB := fmt.Sprintf("tfaccb%s", randomID) + repoName := fmt.Sprintf("%scustomprops-%s", testResourcePrefix, randomID) + + propertiesConfig := fmt.Sprintf(` + resource "github_organization_custom_properties" "a" { + allowed_values = ["one"] + property_name = "%[1]s" + value_type = "single_select" + } + + resource "github_organization_custom_properties" "b" { + allowed_values = ["two"] + property_name = "%[2]s" + value_type = "single_select" + } + `, propertyA, propertyB) + + configBoth := propertiesConfig + fmt.Sprintf(` + resource "github_repository" "test" { + name = "%[1]s" + auto_init = true + + custom_properties = { + (github_organization_custom_properties.a.property_name) = "one" + (github_organization_custom_properties.b.property_name) = "two" + } + } + `, repoName) + + configOnlyA := propertiesConfig + fmt.Sprintf(` + resource "github_repository" "test" { + name = "%[1]s" + auto_init = true + + custom_properties = { + (github_organization_custom_properties.a.property_name) = "one" + } + } + `, repoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: configBoth, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_repository.test", "custom_properties.%", "2"), + ), + }, + { + Config: configOnlyA, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_repository.test", "custom_properties.%", "1"), + resource.TestCheckResourceAttr( + "github_repository.test", + fmt.Sprintf("custom_properties.%s", propertyA), + "one", + ), + resource.TestCheckNoResourceAttr( + "github_repository.test", + fmt.Sprintf("custom_properties.%s", propertyB), + ), + ), + }, + }, + }) + }) + + t.Run("clears all properties with empty map", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + propertyName := fmt.Sprintf("tfacc%s", randomID) + repoName := fmt.Sprintf("%scustomprops-%s", testResourcePrefix, randomID) + + propertiesConfig := fmt.Sprintf(` + resource "github_organization_custom_properties" "test" { + allowed_values = ["alpha"] + property_name = "%[1]s" + value_type = "single_select" + } + `, propertyName) + + configWithValue := propertiesConfig + fmt.Sprintf(` + resource "github_repository" "test" { + name = "%[1]s" + auto_init = true + + custom_properties = { + (github_organization_custom_properties.test.property_name) = "alpha" + } + } + `, repoName) + + configEmpty := propertiesConfig + fmt.Sprintf(` + resource "github_repository" "test" { + name = "%[1]s" + auto_init = true + + custom_properties = {} + } + `, repoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: configWithValue, + Check: resource.TestCheckResourceAttr("github_repository.test", "custom_properties.%", "1"), + }, + { + Config: configEmpty, + Check: resource.TestCheckResourceAttr("github_repository.test", "custom_properties.%", "0"), + }, + }, + }) + }) + + t.Run("removing the block leaves values untouched", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + propertyName := fmt.Sprintf("tfacc%s", randomID) + repoName := fmt.Sprintf("%scustomprops-%s", testResourcePrefix, randomID) + + propertiesConfig := fmt.Sprintf(` + resource "github_organization_custom_properties" "test" { + allowed_values = ["alpha"] + property_name = "%[1]s" + value_type = "single_select" + } + `, propertyName) + + configWithValue := propertiesConfig + fmt.Sprintf(` + resource "github_repository" "test" { + name = "%[1]s" + auto_init = true + + custom_properties = { + (github_organization_custom_properties.test.property_name) = "alpha" + } + } + `, repoName) + + configNoBlock := propertiesConfig + fmt.Sprintf(` + resource "github_repository" "test" { + name = "%[1]s" + auto_init = true + } + `, repoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: configWithValue, + Check: resource.TestCheckResourceAttr( + "github_repository.test", + fmt.Sprintf("custom_properties.%s", propertyName), + "alpha", + ), + }, + { + Config: configNoBlock, + PlanOnly: true, + ExpectNonEmptyPlan: false, + }, + }, + }) + }) +} diff --git a/website/docs/d/enterprise_custom_property.html.markdown b/website/docs/d/enterprise_custom_property.html.markdown new file mode 100644 index 0000000000..a63dc54bc6 --- /dev/null +++ b/website/docs/d/enterprise_custom_property.html.markdown @@ -0,0 +1,59 @@ +--- +layout: "github" +page_title: "GitHub: github_enterprise_custom_property" +description: |- + Get information about a GitHub enterprise custom property definition +--- + +# github_enterprise_custom_property + +Use this data source to retrieve information about a custom property definition for a GitHub enterprise. + +## Example Usage + +```hcl +data "github_enterprise_custom_property" "security_tier" { + enterprise_slug = "my-enterprise" + property_name = "securityTier" +} +``` + +## Example Usage - Reference in a Repository + +```hcl +data "github_enterprise_custom_property" "security_tier" { + enterprise_slug = "my-enterprise" + property_name = "securityTier" +} + +resource "github_repository" "example" { + name = "example" + visibility = "private" + + custom_properties = { + (data.github_enterprise_custom_property.security_tier.property_name) = "tier1" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `enterprise_slug` - (Required) The slug of the enterprise. + +* `property_name` - (Required) The name of the custom property to retrieve. + +## Attributes Reference + +* `value_type` - The type of the value for the property. Can be one of `string`, `single_select`, `multi_select`, `true_false`, or `url`. + +* `required` - Whether the custom property is required on repositories. + +* `description` - A short description of the custom property. + +* `default_values` - The default value(s) of the custom property. For `multi_select` properties this is a list of values; for all other types it is a single-element list. + +* `allowed_values` - An ordered list of allowed values for the property. Only populated when `value_type` is `single_select` or `multi_select`. + +* `values_editable_by` - Who can edit the values of the property. Can be one of `org_actors` or `org_and_repo_actors`. diff --git a/website/docs/r/enterprise_custom_property.html.markdown b/website/docs/r/enterprise_custom_property.html.markdown new file mode 100644 index 0000000000..5aca24f39a --- /dev/null +++ b/website/docs/r/enterprise_custom_property.html.markdown @@ -0,0 +1,99 @@ +--- +layout: "github" +page_title: "GitHub: github_enterprise_custom_property" +description: |- + Creates and manages custom property definitions for a GitHub enterprise +--- + +# github_enterprise_custom_property + +This resource allows you to create and manage custom property definitions for a GitHub enterprise. + +Custom properties enable you to add metadata to repositories across your enterprise. Properties defined at the enterprise level are available to all organizations within the enterprise. You can use them to add context about repositories, such as security classification, compliance requirements, or team ownership. + +~> **Note** You must be an enterprise owner to manage enterprise custom properties. + +## Example Usage + +```hcl +resource "github_enterprise_custom_property" "security_tier" { + enterprise_slug = "my-enterprise" + property_name = "securityTier" + value_type = "single_select" + required = true + description = "Security classification tier for the repository" + allowed_values = ["tier1", "tier2", "tier3"] +} +``` + +## Example Usage - String Property + +```hcl +resource "github_enterprise_custom_property" "owner" { + enterprise_slug = "my-enterprise" + property_name = "owningTeam" + value_type = "string" + required = true + description = "The team responsible for this repository" +} +``` + +## Example Usage - Boolean Property + +```hcl +resource "github_enterprise_custom_property" "contains_pii" { + enterprise_slug = "my-enterprise" + property_name = "containsPII" + value_type = "true_false" + required = false + description = "Whether this repository contains personally identifiable information" + default_values = ["false"] +} +``` + +## Example Usage - Allow Repository Actors to Edit + +```hcl +resource "github_enterprise_custom_property" "team_contact" { + enterprise_slug = "my-enterprise" + property_name = "teamContact" + value_type = "string" + required = false + description = "Contact information for the team managing this repository" + values_editable_by = "org_and_repo_actors" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `enterprise_slug` - (Required, Forces new resource) The slug of the enterprise. + +* `property_name` - (Required, Forces new resource) The name of the custom property. + +* `value_type` - (Required) The type of value for the property. Can be one of `string`, `single_select`, `multi_select`, `true_false`, or `url`. + +* `required` - (Optional) Whether the custom property is required on repositories. Defaults to `false`. + +* `description` - (Optional) A short description of the custom property. + +* `default_values` - (Optional) The default value(s) of the custom property. For `multi_select` properties, multiple values may be specified (e.g. `["b1", "b2"]`). For all other types, provide a single value in a list (e.g. `["value"]`). + +* `allowed_values` - (Optional) An ordered list of allowed values for the property. Only applicable when `value_type` is `single_select` or `multi_select`. Can have up to 200 values. + +* `values_editable_by` - (Optional) Who can edit the values of the property on repositories. Can be one of `org_actors` or `org_and_repo_actors`. When set to `org_actors` (the default), only organization owners can edit property values. When set to `org_and_repo_actors`, repository administrators with the custom properties permission can also edit values. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `property_name` - The name of the custom property. + +## Import + +Enterprise custom properties can be imported using `:`: + +```shell +terraform import github_enterprise_custom_property.security_tier my-enterprise:securityTier +``` diff --git a/website/docs/r/repository.html.markdown b/website/docs/r/repository.html.markdown index 182e98125d..59cf57a5ba 100644 --- a/website/docs/r/repository.html.markdown +++ b/website/docs/r/repository.html.markdown @@ -47,6 +47,20 @@ resource "github_repository" "example" { } ``` +## Example Usage with Required Custom Properties + +```hcl +resource "github_repository" "example" { + name = "example" + visibility = "private" + + custom_properties = { + securityTier = "tier1" + owningTeam = "platform" + } +} +``` + ## Example Usage with Repository Forking ```hcl @@ -146,6 +160,8 @@ initial repository creation and create the target branch inside of the repositor - `allow_update_branch` (Optional) - Set to `true` to always suggest updating pull request branches. +* `custom_properties` - (Optional) A map of custom property key/value pairs to set on the repository. Custom properties must first be defined at the organization or enterprise level. Use this to satisfy required custom properties at repository creation time — if your organization enforces required custom properties, they must be provided here or the creation request will be rejected. Values must be strings; for `single_select` and `true_false` types pass the value as a string (e.g. `"true"`). This attribute is also `Computed`, so GitHub-managed defaults will be reflected in state. + ### GitHub Pages Configuration The `pages` block supports the following: