Skip to content

[FEAT] Add support for Enterprise Cost Centers - #3482

Open
timja wants to merge 39 commits into
integrations:mainfrom
timja:cost-centers
Open

timja wants to merge 39 commits into
integrations:mainfrom
timja:cost-centers

Conversation

@timja

@timja timja commented Jun 10, 2026

Copy link
Copy Markdown

Resubmission of #3000

Thanks to vmvarela for the initial contribution.

Note

This PR includes substantial GitHub Copilot-assisted completion work. The assisted commits credit Copilot App with Co-authored-by trailers.

Initial contributor acceptance-test validation, before the later reconciliation and pagination fixes:

❯ make testacc TESTARGS="-run TestAccGithubEnterpriseCostCenter"
==> Running acceptance tests on branch: 🌿 cost-centers 🌿...
TF_ACC=1 CGO_ENABLED=0 go test ./github/... -v -run '^TestAcc'  -run TestAccGithubEnterpriseCostCenter -timeout 120m -count=1
=== RUN   TestAccGithubEnterpriseCostCenterDataSource
--- PASS: TestAccGithubEnterpriseCostCenterDataSource (9.31s)
=== RUN   TestAccGithubEnterpriseCostCentersDataSource
--- PASS: TestAccGithubEnterpriseCostCentersDataSource (9.29s)
=== RUN   TestAccGithubEnterpriseCostCenterOrganizations
=== RUN   TestAccGithubEnterpriseCostCenterOrganizations/manages_organization_assignments_without_error
--- PASS: TestAccGithubEnterpriseCostCenterOrganizations (12.88s)
    --- PASS: TestAccGithubEnterpriseCostCenterOrganizations/manages_organization_assignments_without_error (12.88s)
=== RUN   TestAccGithubEnterpriseCostCenterRepositories
=== RUN   TestAccGithubEnterpriseCostCenterRepositories/manages_repository_assignments_without_error
--- PASS: TestAccGithubEnterpriseCostCenterRepositories (13.48s)
    --- PASS: TestAccGithubEnterpriseCostCenterRepositories/manages_repository_assignments_without_error (13.48s)
=== RUN   TestAccGithubEnterpriseCostCenter
=== RUN   TestAccGithubEnterpriseCostCenter/creates_cost_center_without_error
=== RUN   TestAccGithubEnterpriseCostCenter/updates_cost_center_name_without_error
=== RUN   TestAccGithubEnterpriseCostCenter/imports_cost_center_without_error
--- PASS: TestAccGithubEnterpriseCostCenter (26.23s)
    --- PASS: TestAccGithubEnterpriseCostCenter/creates_cost_center_without_error (6.30s)
    --- PASS: TestAccGithubEnterpriseCostCenter/updates_cost_center_name_without_error (11.62s)
    --- PASS: TestAccGithubEnterpriseCostCenter/imports_cost_center_without_error (8.31s)
=== RUN   TestAccGithubEnterpriseCostCenterUsers
=== RUN   TestAccGithubEnterpriseCostCenterUsers/manages_user_assignments_without_error
--- PASS: TestAccGithubEnterpriseCostCenterUsers (13.28s)
    --- PASS: TestAccGithubEnterpriseCostCenterUsers/manages_user_assignments_without_error (13.28s)
PASS
ok      github.com/integrations/terraform-provider-github/v6/github     85.019s

Current-head validation:

  • make test
  • make lintcheck
  • make lintcheck-new
  • go test ./github -run '^$' to compile the provider and acceptance-test package
  • terraform fmt -check -recursive examples
  • Documentation generation with --provider-name github, tfplugindocs validate, reproducibility comparison, and rumdl check docs

The current update/reconciliation and pagination paths require enterprise credentials and fixtures that were unavailable locally, so the credential-dependent acceptance tests were not rerun after those changes.

Resolves #2739


Before the change?

  • No support for enterprise cost center resources or data sources

After the change?

  • Adds four resources for cost centers and authoritative user, organization, and repository assignments
  • Adds two data sources for individual cost centers and filtered cost center lists
  • Supports import, update reconciliation, batched assignment mutations, and paginated assignment reads

Authentication must have the enterprise billing access documented by the GitHub Cost centers REST API.

Pull request checklist

  • Schema migrations have been created if needed (not required because this PR adds new resources and data sources)
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been reviewed and added / updated if needed (for bug fixes / features)

Does this introduce a breaking change?

Please see our docs on breaking changes to help!

  • Yes
  • No

@github-actions

Copy link
Copy Markdown

👋 Hi, and thank you for this contribution!

This repo is maintained by GitHub and community members on a best-effort basis. We'll get to this as soon as we can.

You can help us prioritize by joining the discussion on open issues and PRs, sharing details on the changes you need, and reviewing other contributions.


🤖 This is an automated message.

@timja timja changed the title cost centers [FEAT] Add support for Enterprise Cost Centers Jun 10, 2026
@eyalgal eyalgal added this to the v6.14.0 milestone Jul 20, 2026
@deiga
deiga requested a balanced review from Copilot August 18, 2026 09:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

These provider review instructions are being used.

Adds enterprise cost-center management to the provider.

Changes:

  • Adds cost-center and authoritative assignment resources.
  • Adds single/list data sources, retries, batching, and acceptance tests.
  • Adds documentation and an example configuration.

Blocking findings include incompatible go-github v88 imports, unsafe partial batch creation, and missing update-path tests.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
github/util.go Adds error and batching helpers.
github/util_test.go Tests new helpers.
github/util_enterprise_cost_center.go Adds assignment retry helpers.
github/resource_github_enterprise_cost_center.go Manages cost centers.
github/resource_github_enterprise_cost_center_test.go Tests cost-center lifecycle.
github/resource_github_enterprise_cost_center_users.go Manages user assignments.
github/resource_github_enterprise_cost_center_users_test.go Tests user assignments.
github/resource_github_enterprise_cost_center_organizations.go Manages organization assignments.
github/resource_github_enterprise_cost_center_organizations_test.go Tests organization assignments.
github/resource_github_enterprise_cost_center_repositories.go Manages repository assignments.
github/resource_github_enterprise_cost_center_repositories_test.go Tests repository assignments.
github/provider.go Registers new resources and data sources.
github/data_source_github_enterprise_cost_center.go Reads one cost center.
github/data_source_github_enterprise_cost_center_test.go Tests single lookup.
github/data_source_github_enterprise_cost_centers.go Lists cost centers.
github/data_source_github_enterprise_cost_centers_test.go Tests filtered listing.
examples/cost_centers/main.tf Demonstrates cost-center workflows.
docs/resources/enterprise_cost_center.html.markdown Documents the cost-center resource.
docs/resources/enterprise_cost_center_users.html.markdown Documents user assignments.
docs/resources/enterprise_cost_center_organizations.html.markdown Documents organization assignments.
docs/resources/enterprise_cost_center_repositories.html.markdown Documents repository assignments.
docs/data-sources/enterprise_cost_center.html.markdown Documents single lookup.
docs/data-sources/enterprise_cost_centers.html.markdown Documents cost-center listing.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread github/resource_github_enterprise_cost_center.go Outdated
Comment thread github/util_enterprise_cost_center.go Outdated
Comment thread github/resource_github_enterprise_cost_center_users.go Outdated
Comment thread github/resource_github_enterprise_cost_center_organizations.go Outdated
Comment thread github/resource_github_enterprise_cost_center_repositories.go Outdated
Comment thread github/resource_github_enterprise_cost_center_organizations_test.go
Comment thread github/resource_github_enterprise_cost_center_repositories_test.go
Comment thread github/resource_github_enterprise_cost_center_users.go Outdated
Comment thread github/resource_github_enterprise_cost_center_repositories.go Outdated
Comment thread github/resource_github_enterprise_cost_center_organizations.go Outdated
@timja

timja commented Aug 18, 2026

Copy link
Copy Markdown
Author

Note: I'm away for a couple weeks after today and likely don't have time to get to this today, may be mid September before I can get back to this unless you're able to take it over.

github-actions Bot and others added 22 commits September 17, 2026 10:08
Add utility functions needed for enterprise cost center resources:
- errIs404(): check if error is a GitHub 404 Not Found response
- errIsRetryable(): check if error is retryable (409, 5xx)
- expandStringSet(): convert schema.Set to []string
- chunkStringSlice(): split slice into chunks for batching
Add util_enterprise_cost_center.go with helper functions for managing
cost center assignments with proper retry logic and batching support.
Manages GitHub Enterprise cost center entities (create, read, update, archive).

Includes:
- Resource implementation with CRUD operations
- Acceptance tests
- Documentation
Authoritative management of user assignments to cost centers.

Includes:
- Resource implementation with batched add/remove operations
- Acceptance tests
- Documentation
…esource

Authoritative management of organization assignments to cost centers.

Includes:
- Resource implementation with batched add/remove operations
- Acceptance tests
- Documentation
…source

Authoritative management of repository assignments to cost centers.

Includes:
- Resource implementation with batched add/remove operations
- Acceptance tests
- Documentation
Add two data sources:
- github_enterprise_cost_center: retrieve a cost center by ID
- github_enterprise_cost_centers: list cost centers with optional state filter

Includes:
- Data source implementations
- Acceptance tests
- Documentation
- Register 4 resources in provider.go:
  - github_enterprise_cost_center
  - github_enterprise_cost_center_users
  - github_enterprise_cost_center_organizations
  - github_enterprise_cost_center_repositories
- Register 2 data sources in provider.go:
  - github_enterprise_cost_center
  - github_enterprise_cost_centers
- Add navigation links in website/github.erb

T_EDITOR=true git rebase --continue
t status
Add example Terraform configuration demonstrating how to:
- Create a cost center
- Assign users, organizations, and repositories
- Use data sources to query cost centers
Update import paths from go-github/v81 to go-github/v82 to match
the current version in upstream/main.
Co-authored-by: Timo Sand <timo.sand@iki.fi>
Remove expandStringSet from util.go and replace usages with direct
expandStringList(set.List()) calls. The function was unnecessary since
schema.Set from d.Get() is never nil.

Resolves PR comments integrations#1-2.
Move the archived/deleted state check from Update to Read function.
If the cost center is archived (deleted), it will be removed from
Terraform state during Read rather than blocking updates.

Resolves PR comment integrations#3.
Split resourceGithubEnterpriseCostCenterUsersCreateOrUpdate into
separate Create and Update functions. Create only adds users,
Update handles the full diff. Both return nil instead of calling Read.

Resolves PR comments integrations#4-5.
Split resourceGithubEnterpriseCostCenterOrganizationsCreateOrUpdate into
separate Create and Update functions. Create only adds organizations,
Update handles the full diff. Both return nil instead of calling Read.

Resolves PR comments integrations#6-7.
Split resourceGithubEnterpriseCostCenterRepositoriesCreateOrUpdate into
separate Create and Update functions. Create only adds repositories,
Update handles the full diff. Both return nil instead of calling Read.

Resolves PR comments integrations#8-9.
…tions

The API returns type strings as 'User', 'Org', and 'Repo' but the tests
were checking for lowercase 'user', 'organization', and 'repository'.
This fix ensures CheckDestroy properly detects remaining assignments.
Add CostCenterResourceType constants (User, Org, Repo) to avoid
magic strings throughout the codebase. This prevents typos and
makes the code more maintainable.

Addresses review feedback from @deiga.
Replace terraform-plugin-sdk/v2 test imports with terraform-plugin-testing
to fix flag redefinition conflict ('sweep' flag registered twice).
This aligns cost center tests with the rest of the codebase.
Keep chunkStringSlice(items, maxSize) generic in util.go to avoid coupling with cost-center-specific constants.

Add nolint:unparam with explicit rationale because current call sites pass the same value, while preserving future reuse for other resources.
vmvarela and others added 16 commits September 17, 2026 10:11
The import function now calls GetCostCenter to populate the 'name'
field from the API response. Without this, the Required 'name' field
would be empty after import, causing recreate churn on the next plan.

Addresses review comment from @stevehipwell.
Sub-resources (_users, _organizations, _repositories) now use a simple
cost_center_id as their Terraform ID, consistent with the main
github_enterprise_cost_center resource.

Also moves d.SetId() to after the API call succeeds, preventing
corrupt state if the add-resources call fails.

Tests updated to use ImportStateIdPrefix and read attributes from
state instead of parsing the now-simple ID.

Addresses review comments from @stevehipwell.
Before adding resources to a cost center, check via API whether
the cost center already has resources of the managed type assigned.
If so, return an error asking the user to import or remove them
manually. This prevents silently clobbering pre-existing assignments.
Replace the two-map diff (currentX + desiredX) with a single map
where false=remove and true=keep. Desired items not in the map are
added. This is shorter and avoids constructing a second map.
Instead of reading resource names from Terraform state (which may be
stale or incomplete), Delete now calls GetCostCenter to fetch the
current list of resources of the managed type from the API and removes
them all. Also handles 404 gracefully (cost center already gone).
Update cost center API usage to go-github v89, retain assignment IDs before batched mutations, and add update reconciliation coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Move cost center examples and import syntax into tfplugindocs source directories and complete the runnable root example structure.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Aggregate every assignment page before authoritative reconciliation, validate the API name limit, and document enterprise billing permissions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread examples/cost_centers/main.tf Outdated
required_providers {
github = {
source = "integrations/github"
version = "~> 6.11"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: let's not add version numbers to our docs, unless they are specifically needed

return &schema.Resource{
Description: "Retrieves information about a specific GitHub enterprise cost center.",
ReadContext: dataSourceGithubEnterpriseCostCenterRead,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a Timeouts block

Comment thread github/util.go
// errIs404 checks if the error is a GitHub 404 Not Found response.
func errIs404(err error) bool {
var ghErr *github.ErrorResponse
if errors.As(err, &ghErr) && ghErr.Response != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use errors.AsType

Comment thread github/util.go
Comment on lines +211 to +222
// chunkStringSlice splits a slice into chunks of the specified max size.
func chunkStringSlice(items []string, maxSize int) [][]string {
if len(items) == 0 {
return nil
}
chunks := make([][]string, 0, (len(items)+maxSize-1)/maxSize)
for start := 0; start < len(items); start += maxSize {
end := min(start+maxSize, len(items))
chunks = append(chunks, items[start:end])
}
return chunks
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be replaced by slices.Chunk?

Comment on lines +37 to +43
func costCenterOwner(meta any) (*Owner, error) {
owner, ok := meta.(*Owner)
if !ok {
return nil, fmt.Errorf("unexpected provider metadata type %T", meta)
}
return owner, nil
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is useless abstraction, use existing funcs that do the same thing

Comment on lines +45 to +64
func costCenterString(d *schema.ResourceData, key string) (string, error) {
value, ok := resourceKeysGetOk[string](d, key)
if !ok {
return "", fmt.Errorf("expected %q to be a non-empty string", key)
}
return value, nil
}

func costCenterStringSet(d *schema.ResourceData, key string) ([]string, error) {
values, ok := resourceKeysGetOk[*schema.Set](d, key)
if !ok {
return nil, fmt.Errorf("expected %q to be a non-empty set of strings", key)
}

result := expandStringList(values.List())
if len(result) != values.Len() {
return nil, fmt.Errorf("expected %q to contain only non-empty strings", key)
}
return result, nil
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no reason these are scoped to cost center

Comment on lines +75 to +87
endpoint := fmt.Sprintf(
"enterprises/%s/settings/billing/cost-centers/%s?%s",
url.PathEscape(enterpriseSlug),
url.PathEscape(costCenterID),
query.Encode(),
)
req, err := client.NewRequest(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}

result := new(costCenterPage)
_, err = client.Do(req, result)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: no bare API calls, use go-github


// retryCostCenterRemoveResources removes resources from a cost center with retry logic.
// Uses retry.RetryContext for exponential backoff on transient errors.
func retryCostCenterRemoveResources(ctx context.Context, client *github.Client, enterpriseSlug, costCenterID string, req github.CostCenterResourceRequest) diag.Diagnostics {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: use existing retry functions

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Pagination, archived-state handling, input validation, and deletion-test coverage have unresolved correctness gaps.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (3)

github/resource_github_enterprise_cost_center_organizations.go:188

  • Archived cost centers are returned successfully with state deleted (the main cost-center resource handles this at lines 107-114), so this read currently leaves the organization-assignment resource in state and later plans can try to mutate an archived center. Clear the resource ID when cc.GetState() == "deleted".
	cc, err := getEnterpriseCostCenter(ctx, owner.v3client, enterpriseSlug, costCenterID)
	if err != nil {
		return diag.FromErr(deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "cost center organizations %s/%s", enterpriseSlug, costCenterID))
	}

github/resource_github_enterprise_cost_center_users.go:187

  • Archived cost centers are returned successfully with state deleted (the main cost-center resource handles this at lines 107-114), so this read currently leaves the user-assignment resource in state and later plans can try to mutate an archived center. Clear the resource ID when cc.GetState() == "deleted".
    github/resource_github_enterprise_cost_center_repositories.go:187
  • Archived cost centers are returned successfully with state deleted (the main cost-center resource handles this at lines 107-114), so this read currently leaves the repository-assignment resource in state and later plans can try to mutate an archived center. Clear the resource ID when cc.GetState() == "deleted".
	cc, err := getEnterpriseCostCenter(ctx, owner.v3client, enterpriseSlug, costCenterID)
	if err != nil {
		if errIs404(err) {
  • Files reviewed: 43/43 changed files
  • Comments generated: 5
  • Review effort level: Balanced

opts.State = &stateFilter
}

result, _, err := owner.v3client.Enterprise.ListCostCenters(ctx, enterpriseSlug, &opts)
Type: schema.TypeSet,
Required: true,
MinItems: 1,
Elem: &schema.Schema{Type: schema.TypeString},
Type: schema.TypeSet,
Required: true,
MinItems: 1,
Elem: &schema.Schema{Type: schema.TypeString},
Comment on lines +32 to +35
resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessEnterprise(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
Type: schema.TypeSet,
Required: true,
MinItems: 1,
Elem: &schema.Schema{Type: schema.TypeString},
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT]: Add support for Enterprise billing resources

6 participants