Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/actions/graformer/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
}
Expand Down
6 changes: 6 additions & 0 deletions .schemas/repository-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,12 @@
},
"type": "array"
},
"custom_properties": {
"additionalProperties": {
"type": "string"
},
"type": "object"
},
"high_integrity": {
"$ref": "#/$defs/HighIntegrityConfig"
}
Expand Down
16 changes: 16 additions & 0 deletions DEVELOPERS_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -124,6 +126,20 @@ high_integrity:
enabled: true
```

## Custom Properties

[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.

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:

```yaml
custom_properties:
oss_lifecycle: active
team: platform
```

## Template Configuration

Options for configuring a repository from a template.
Expand Down
3 changes: 2 additions & 1 deletion INSTALLATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
41 changes: 41 additions & 0 deletions feature/github-repo-importer/pkg/github/custom_properties.go
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 6 additions & 0 deletions feature/github-repo-importer/pkg/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -200,6 +205,7 @@ func ImportRepo(repoName string) (*Repository, error) {
Rulesets: resolvedRulesets,
VulnerabilityAlertsEnabled: &vulnerabilityAlertsEnabled,
BranchProtectionsV4: resolveBranchProtectionsFromGraphQL(&branchProtectionRulesGraphQLQuery),
CustomProperties: customProperties,
}, nil
}

Expand Down
1 change: 1 addition & 0 deletions feature/github-repo-importer/pkg/github/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
54 changes: 54 additions & 0 deletions feature/github-repo-provisioning/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Loading