From 4bdb7a498b0bcb2caf8cbb86a3ec1bd43e03cc27 Mon Sep 17 00:00:00 2001 From: Ivan Pavlovic Date: Tue, 24 Mar 2026 12:28:57 +0100 Subject: [PATCH 1/4] Add support for custom_properties --- .github/actions/graformer/action.yaml | 2 + .schemas/repository-config.gr-oss.schema.json | 30 +++++++++++ .schemas/repository-config.schema.json | 6 +++ .../pkg/github/custom_properties.go | 41 ++++++++++++++ .../github-repo-importer/pkg/github/github.go | 6 +++ .../pkg/github/repositories.go | 1 + feature/github-repo-provisioning/main.tf | 54 +++++++++++++++++++ 7 files changed, 140 insertions(+) create mode 100644 .schemas/repository-config.gr-oss.schema.json create mode 100644 feature/github-repo-importer/pkg/github/custom_properties.go diff --git a/.github/actions/graformer/action.yaml b/.github/actions/graformer/action.yaml index 100fe39..2eab419 100644 --- a/.github/actions/graformer/action.yaml +++ b/.github/actions/graformer/action.yaml @@ -120,6 +120,8 @@ runs: return `${resourceChange.type} :: ${match[1]}/${actualChange.pattern}`; }else if(resourceChange.type === 'github_repository_environment'){ return `${resourceChange.type} :: ${actualChange.repository}/${actualChange.environment}`; + }else if(resourceChange.type === 'github_repository_custom_property'){ + return `${resourceChange.type} :: ${actualChange.repository}/${actualChange.property_name}`; } else { return `unknown type ${resourceChange.type}. resource address: ${resourceChange.address}` } diff --git a/.schemas/repository-config.gr-oss.schema.json b/.schemas/repository-config.gr-oss.schema.json new file mode 100644 index 0000000..1db8b67 --- /dev/null +++ b/.schemas/repository-config.gr-oss.schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/G-Research/github-terraformer/refs/heads/main/.schemas/repository-config.gr-oss.schema.json", + "title": "Repository Configuration (GR-OSS)", + "allOf": [ + { + "$ref": "repository-config.schema.json" + }, + { + "properties": { + "custom_properties": { + "properties": { + "oss_lifecycle": { + "type": "string", + "enum": [ + "published", + "active", + "contributing", + "maintained", + "evaluating", + "deprecated", + "archived" + ] + } + } + } + } + } + ] +} diff --git a/.schemas/repository-config.schema.json b/.schemas/repository-config.schema.json index 24cccf1..5007703 100644 --- a/.schemas/repository-config.schema.json +++ b/.schemas/repository-config.schema.json @@ -421,6 +421,12 @@ }, "type": "array" }, + "custom_properties": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, "high_integrity": { "$ref": "#/$defs/HighIntegrityConfig" } diff --git a/feature/github-repo-importer/pkg/github/custom_properties.go b/feature/github-repo-importer/pkg/github/custom_properties.go new file mode 100644 index 0000000..05a948e --- /dev/null +++ b/feature/github-repo-importer/pkg/github/custom_properties.go @@ -0,0 +1,41 @@ +package github + +import ( + "context" + "fmt" + "net/http" + + "github.com/google/go-github/v67/github" + "github.com/gr-oss-devops/github-repo-importer/pkg/file" +) + +func FetchCustomProperties(client *github.Client, owner, repo string, dumpManager *file.DumpManager) (map[string]string, error) { + customPropertyValues, r, err := client.Repositories.GetAllCustomPropertyValues(context.Background(), owner, repo) + if err != nil { + if r != nil && r.StatusCode == http.StatusForbidden { + fmt.Printf("skipping custom properties due to insufficient permissions: %v\n", err) + return nil, nil + } + return nil, fmt.Errorf("failed to get custom properties: %w", err) + } + + if err := dumpManager.WriteJSONFile("custom_properties.json", customPropertyValues); err != nil { + fmt.Printf("failed to write custom_properties.json: %v\n", err) + } + + result := make(map[string]string) + for _, prop := range customPropertyValues { + if prop.Value == nil { + continue + } + if v, ok := prop.Value.(string); ok { + result[prop.PropertyName] = v + } + } + + if len(result) == 0 { + return nil, nil + } + + return result, nil +} diff --git a/feature/github-repo-importer/pkg/github/github.go b/feature/github-repo-importer/pkg/github/github.go index 7c251bf..fa363f7 100644 --- a/feature/github-repo-importer/pkg/github/github.go +++ b/feature/github-repo-importer/pkg/github/github.go @@ -157,6 +157,11 @@ func ImportRepo(repoName string) (*Repository, error) { fmt.Printf("failed to resolve rulesets: %v\n", err) } + customProperties, err := FetchCustomProperties(v3client, repoNameSplit[0], repoNameSplit[1], dumpManager) + if err != nil { + fmt.Printf("failed to fetch custom properties: %v\n", err) + } + return &Repository{ Name: repo.GetName(), Owner: repo.GetOwner().GetLogin(), @@ -200,6 +205,7 @@ func ImportRepo(repoName string) (*Repository, error) { Rulesets: resolvedRulesets, VulnerabilityAlertsEnabled: &vulnerabilityAlertsEnabled, BranchProtectionsV4: resolveBranchProtectionsFromGraphQL(&branchProtectionRulesGraphQLQuery), + CustomProperties: customProperties, }, nil } diff --git a/feature/github-repo-importer/pkg/github/repositories.go b/feature/github-repo-importer/pkg/github/repositories.go index bc1c878..16ca5a0 100644 --- a/feature/github-repo-importer/pkg/github/repositories.go +++ b/feature/github-repo-importer/pkg/github/repositories.go @@ -45,6 +45,7 @@ type Repository struct { Rulesets []Ruleset `yaml:"rulesets,omitempty"` VulnerabilityAlertsEnabled *bool `yaml:"vulnerability_alerts_enabled,omitempty"` BranchProtectionsV4 []*BranchProtectionV4 `yaml:"branch_protections_v4,omitempty"` + CustomProperties map[string]string `yaml:"custom_properties,omitempty"` } type RepositoryTemplate struct { diff --git a/feature/github-repo-provisioning/main.tf b/feature/github-repo-provisioning/main.tf index 7ed838a..c7ab166 100644 --- a/feature/github-repo-provisioning/main.tf +++ b/feature/github-repo-provisioning/main.tf @@ -547,3 +547,57 @@ resource "github_repository_ruleset" "ruleset" { } } } + +locals { + new_custom_properties_flattened = flatten([ + for repo, config in local.new_repos : [ + for prop_name, prop_value in try(config.custom_properties, {}) : { + repository = repo + property_name = prop_name + property_value = prop_value + } + ] + ]) + + new_custom_properties_map = { + for item in local.new_custom_properties_flattened : + "${item.repository}/${item.property_name}" => item + } + + generated_custom_properties_flattened = flatten([ + for repo, config in local.generated_repos : [ + for prop_name, prop_value in try(config.custom_properties, {}) : { + repository = repo + property_name = prop_name + property_value = prop_value + } + ] + ]) + + generated_custom_properties_map = { + for item in local.generated_custom_properties_flattened : + "${item.repository}/${item.property_name}" => item + } + + all_custom_properties_map = merge(local.new_custom_properties_map, local.generated_custom_properties_map) +} + +import { + for_each = local.generated_custom_properties_map + to = github_repository_custom_property.custom_property[each.key] + id = format("%s:%s:%s", var.owner, each.value.repository, each.value.property_name) +} + +resource "github_repository_custom_property" "custom_property" { + depends_on = [module.repository] + for_each = local.all_custom_properties_map + + repository = each.value.repository + property_name = each.value.property_name + property_type = "string" + property_value = [each.value.property_value] + + lifecycle { + ignore_changes = [property_type] # At this moment the provider does not support updates to the property_type field, so we set it to a default value and ignore changes to it + } +} From 067dc8679ebeb4d2aff55cfbd481b9cc1102f21f Mon Sep 17 00:00:00 2001 From: Ivan Pavlovic Date: Tue, 24 Mar 2026 14:32:49 +0100 Subject: [PATCH 2/4] Update developers guide --- DEVELOPERS_GUIDE.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/DEVELOPERS_GUIDE.md b/DEVELOPERS_GUIDE.md index 0b687cc..165228c 100644 --- a/DEVELOPERS_GUIDE.md +++ b/DEVELOPERS_GUIDE.md @@ -102,6 +102,8 @@ These are the primary configuration options for each repository. - **`branch_protections_v4`**: *(optional, object[] [BranchProtectionV4](#branch-protection-configuration-v4))* Configuration for branch protection rules. +- **`custom_properties`**: *(optional, map[string]string)* A map of GitHub organization custom property names to their string values. See [Custom Properties](#custom-properties). + - **`high_integrity`**: *(optional, object [HighIntegrity](#high-integrity-configuration))* Expansion directives for high-integrity repositories. This field is consumed by the `expand` command and is **not** passed to Terraform — it is removed from the output after expansion. ## High Integrity Configuration @@ -124,6 +126,35 @@ high_integrity: enabled: true ``` +## Custom Properties + +A map of [GitHub organization custom property](https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization) names to their string values. Custom properties must be defined at the organization level before they can be set on a repository. + +Each key is the property name and each value is a string. Only `string`-type custom properties are supported. + +Example: + +```yaml +custom_properties: + oss_lifecycle: active + team: platform +``` + +### GR-OSS: `oss_lifecycle` + +The `oss_lifecycle` property is used by GR-OSS to track the lifecycle stage of a repository. Allowed values are: + +- `published` — Newly released project. Assigned for the first 90 days after public release. +- `active` — Project is actively developed and maintained. Regular releases and community engagement. +- `contributing` — We contribute to an upstream project we don't own. Tracks participation in external projects. +- `maintained` — Stable project receiving security patches and dependency updates, but no active feature development. +- `evaluating` — Project under review to determine next steps. 90-day review cycle. +- `deprecated` — No longer recommended for use. Not maintained. Should include migration guidance. +- `archived` — Read-only. No further development, patches, or support. + +> [!NOTE] +> A supplementary JSON schema enforcing these values is available at `.schemas/repository-config.gr-oss.schema.json`. + ## Template Configuration Options for configuring a repository from a template. From 682ed0a22cb3bc218c9d89d4921ebd465b3e3ef8 Mon Sep 17 00:00:00 2001 From: Ivan Pavlovic Date: Tue, 24 Mar 2026 14:54:56 +0100 Subject: [PATCH 3/4] Update installation guide. Write down that Custom Properties read and write scope is needed for github configuration app --- INSTALLATION_GUIDE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/INSTALLATION_GUIDE.md b/INSTALLATION_GUIDE.md index ee4d003..bc4f5f2 100644 --- a/INSTALLATION_GUIDE.md +++ b/INSTALLATION_GUIDE.md @@ -32,8 +32,9 @@ You must have: | Administration | Read & Write | | Checks | Read & Write | | Contents | Read & Write | +| Custom Properties | Read & Write | | Dependabot Alerts | Read & Write | -| Metadata | Read-only | +| Metadata | Read-only | | Pages | Read & Write | | Pull Requests | Read & Write | From 0d2a2168f431c53a48f4d34b3972e86471658ccc Mon Sep 17 00:00:00 2001 From: Ivan Pavlovic Date: Tue, 24 Mar 2026 22:30:27 +0100 Subject: [PATCH 4/4] Address PR comments --- .schemas/repository-config.gr-oss.schema.json | 30 ------------------- DEVELOPERS_GUIDE.md | 19 ++---------- 2 files changed, 2 insertions(+), 47 deletions(-) delete mode 100644 .schemas/repository-config.gr-oss.schema.json diff --git a/.schemas/repository-config.gr-oss.schema.json b/.schemas/repository-config.gr-oss.schema.json deleted file mode 100644 index 1db8b67..0000000 --- a/.schemas/repository-config.gr-oss.schema.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/G-Research/github-terraformer/refs/heads/main/.schemas/repository-config.gr-oss.schema.json", - "title": "Repository Configuration (GR-OSS)", - "allOf": [ - { - "$ref": "repository-config.schema.json" - }, - { - "properties": { - "custom_properties": { - "properties": { - "oss_lifecycle": { - "type": "string", - "enum": [ - "published", - "active", - "contributing", - "maintained", - "evaluating", - "deprecated", - "archived" - ] - } - } - } - } - } - ] -} diff --git a/DEVELOPERS_GUIDE.md b/DEVELOPERS_GUIDE.md index 165228c..b4309d6 100644 --- a/DEVELOPERS_GUIDE.md +++ b/DEVELOPERS_GUIDE.md @@ -128,9 +128,9 @@ high_integrity: ## Custom Properties -A map of [GitHub organization custom property](https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization) names to their string values. Custom properties must be defined at the organization level before they can be set on a repository. +[GitHub organization custom properties](https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization) allow organizations to attach structured metadata to repositories — such as lifecycle stage, team ownership, compliance classification, or any other organization-defined attribute. They are defined once at the organization level and can then be set per repository. -Each key is the property name and each value is a string. Only `string`-type custom properties are supported. +The `custom_properties` field is a map of property names to their string values. Custom properties must be defined at the organization level before they can be set on a repository. Only `string`-type custom properties are supported. Example: @@ -140,21 +140,6 @@ custom_properties: team: platform ``` -### GR-OSS: `oss_lifecycle` - -The `oss_lifecycle` property is used by GR-OSS to track the lifecycle stage of a repository. Allowed values are: - -- `published` — Newly released project. Assigned for the first 90 days after public release. -- `active` — Project is actively developed and maintained. Regular releases and community engagement. -- `contributing` — We contribute to an upstream project we don't own. Tracks participation in external projects. -- `maintained` — Stable project receiving security patches and dependency updates, but no active feature development. -- `evaluating` — Project under review to determine next steps. 90-day review cycle. -- `deprecated` — No longer recommended for use. Not maintained. Should include migration guidance. -- `archived` — Read-only. No further development, patches, or support. - -> [!NOTE] -> A supplementary JSON schema enforcing these values is available at `.schemas/repository-config.gr-oss.schema.json`. - ## Template Configuration Options for configuring a repository from a template.