From 128104cd4fcb3ec28baa3ab9f70f9f2cf656ec40 Mon Sep 17 00:00:00 2001 From: "Victor M. Varela" Date: Tue, 24 Feb 2026 12:58:08 +0100 Subject: [PATCH 1/7] feat(billing): add shared utility functions for enterprise billing data sources --- github/util_enterprise_billing.go | 313 +++++++++++++++++++++++++ github/util_enterprise_billing_test.go | 245 +++++++++++++++++++ 2 files changed, 558 insertions(+) create mode 100644 github/util_enterprise_billing.go create mode 100644 github/util_enterprise_billing_test.go diff --git a/github/util_enterprise_billing.go b/github/util_enterprise_billing.go new file mode 100644 index 0000000000..32dee79d5c --- /dev/null +++ b/github/util_enterprise_billing.go @@ -0,0 +1,313 @@ +package github + +import ( + "context" + "fmt" + "net/url" + "strconv" + + "github.com/google/go-github/v83/github" +) + +// EnterpriseBillingUsageOptions specifies optional parameters for the +// enterprise billing usage report endpoint. +type EnterpriseBillingUsageOptions struct { + Year *int `url:"year,omitempty"` + Month *int `url:"month,omitempty"` + Day *int `url:"day,omitempty"` + CostCenterID *string `url:"cost_center_id,omitempty"` +} + +// EnterprisePremiumRequestUsageOptions specifies optional parameters for the +// enterprise billing premium request usage report endpoint. +type EnterprisePremiumRequestUsageOptions struct { + Year *int `url:"year,omitempty"` + Month *int `url:"month,omitempty"` + Day *int `url:"day,omitempty"` + Organization *string `url:"organization,omitempty"` + User *string `url:"user,omitempty"` + Model *string `url:"model,omitempty"` + Product *string `url:"product,omitempty"` + CostCenterID *string `url:"cost_center_id,omitempty"` +} + +// EnterpriseUsageSummaryOptions specifies optional parameters for the +// enterprise billing usage summary endpoint. +type EnterpriseUsageSummaryOptions struct { + Year *int `url:"year,omitempty"` + Month *int `url:"month,omitempty"` + Day *int `url:"day,omitempty"` + Organization *string `url:"organization,omitempty"` + Repository *string `url:"repository,omitempty"` + Product *string `url:"product,omitempty"` + SKU *string `url:"sku,omitempty"` + CostCenterID *string `url:"cost_center_id,omitempty"` +} + +// EnterprisePremiumRequestUsageReport represents the enterprise-level +// premium request usage report response. +type EnterprisePremiumRequestUsageReport struct { + TimePeriod github.PremiumRequestUsageTimePeriod `json:"timePeriod"` + Enterprise *string `json:"enterprise,omitempty"` + UsageItems []*github.PremiumRequestUsageItem `json:"usageItems"` +} + +// EnterpriseUsageSummaryItem represents a single usage line item in an +// enterprise billing usage summary report. +type EnterpriseUsageSummaryItem struct { + Product string `json:"product"` + SKU string `json:"sku"` + UnitType string `json:"unitType"` + PricePerUnit float64 `json:"pricePerUnit"` + GrossQuantity float64 `json:"grossQuantity"` + GrossAmount float64 `json:"grossAmount"` + DiscountQuantity float64 `json:"discountQuantity"` + DiscountAmount float64 `json:"discountAmount"` + NetQuantity float64 `json:"netQuantity"` + NetAmount float64 `json:"netAmount"` +} + +// EnterpriseUsageSummaryReport represents the enterprise-level billing +// usage summary report response. +type EnterpriseUsageSummaryReport struct { + TimePeriod github.PremiumRequestUsageTimePeriod `json:"timePeriod"` + Enterprise *string `json:"enterprise,omitempty"` + UsageItems []*EnterpriseUsageSummaryItem `json:"usageItems"` +} + +// buildQueryURL constructs a URL with non-empty query parameters. +func buildQueryURL(base string, params map[string]string) string { + values := url.Values{} + for key, val := range params { + if val != "" { + values.Set(key, val) + } + } + + if len(values) == 0 { + return base + } + + return base + "?" + values.Encode() +} + +// intToString converts an int to its string representation. +// Returns an empty string if the value is zero. +func intToString(value int) string { + if value == 0 { + return "" + } + return strconv.Itoa(value) +} + +// getEnterpriseBillingUsage fetches the billing usage report for an enterprise. +func getEnterpriseBillingUsage(ctx context.Context, client *github.Client, enterprise string, opts *EnterpriseBillingUsageOptions) (*github.UsageReport, error) { + urlPath := fmt.Sprintf("enterprises/%s/settings/billing/usage", enterprise) + + params := map[string]string{} + if opts != nil { + if opts.Year != nil { + params["year"] = strconv.Itoa(*opts.Year) + } + if opts.Month != nil { + params["month"] = strconv.Itoa(*opts.Month) + } + if opts.Day != nil { + params["day"] = strconv.Itoa(*opts.Day) + } + if opts.CostCenterID != nil { + params["cost_center_id"] = *opts.CostCenterID + } + } + + urlPath = buildQueryURL(urlPath, params) + + req, err := client.NewRequest("GET", urlPath, nil) + if err != nil { + return nil, err + } + + report := new(github.UsageReport) + _, err = client.Do(ctx, req, report) + if err != nil { + return nil, err + } + + return report, nil +} + +// getEnterprisePremiumRequestUsage fetches the billing premium request usage report for an enterprise. +func getEnterprisePremiumRequestUsage(ctx context.Context, client *github.Client, enterprise string, opts *EnterprisePremiumRequestUsageOptions) (*EnterprisePremiumRequestUsageReport, error) { + urlPath := fmt.Sprintf("enterprises/%s/settings/billing/premium_request/usage", enterprise) + + params := map[string]string{} + if opts != nil { + if opts.Year != nil { + params["year"] = strconv.Itoa(*opts.Year) + } + if opts.Month != nil { + params["month"] = strconv.Itoa(*opts.Month) + } + if opts.Day != nil { + params["day"] = strconv.Itoa(*opts.Day) + } + if opts.Organization != nil { + params["organization"] = *opts.Organization + } + if opts.User != nil { + params["user"] = *opts.User + } + if opts.Model != nil { + params["model"] = *opts.Model + } + if opts.Product != nil { + params["product"] = *opts.Product + } + if opts.CostCenterID != nil { + params["cost_center_id"] = *opts.CostCenterID + } + } + + urlPath = buildQueryURL(urlPath, params) + + req, err := client.NewRequest("GET", urlPath, nil) + if err != nil { + return nil, err + } + + report := new(EnterprisePremiumRequestUsageReport) + _, err = client.Do(ctx, req, report) + if err != nil { + return nil, err + } + + return report, nil +} + +// getEnterpriseUsageSummary fetches the billing usage summary for an enterprise. +func getEnterpriseUsageSummary(ctx context.Context, client *github.Client, enterprise string, opts *EnterpriseUsageSummaryOptions) (*EnterpriseUsageSummaryReport, error) { + urlPath := fmt.Sprintf("enterprises/%s/settings/billing/usage/summary", enterprise) + + params := map[string]string{} + if opts != nil { + if opts.Year != nil { + params["year"] = strconv.Itoa(*opts.Year) + } + if opts.Month != nil { + params["month"] = strconv.Itoa(*opts.Month) + } + if opts.Day != nil { + params["day"] = strconv.Itoa(*opts.Day) + } + if opts.Organization != nil { + params["organization"] = *opts.Organization + } + if opts.Repository != nil { + params["repository"] = *opts.Repository + } + if opts.Product != nil { + params["product"] = *opts.Product + } + if opts.SKU != nil { + params["sku"] = *opts.SKU + } + if opts.CostCenterID != nil { + params["cost_center_id"] = *opts.CostCenterID + } + } + + urlPath = buildQueryURL(urlPath, params) + + req, err := client.NewRequest("GET", urlPath, nil) + if err != nil { + return nil, err + } + + report := new(EnterpriseUsageSummaryReport) + _, err = client.Do(ctx, req, report) + if err != nil { + return nil, err + } + + return report, nil +} + +// flattenUsageItems converts billing usage items to a Terraform state-compatible format. +func flattenUsageItems(items []*github.UsageItem) []map[string]any { + result := make([]map[string]any, len(items)) + for idx, item := range items { + result[idx] = map[string]any{ + "date": item.Date, + "product": item.Product, + "sku": item.SKU, + "quantity": item.Quantity, + "unit_type": item.UnitType, + "price_per_unit": item.PricePerUnit, + "gross_amount": item.GrossAmount, + "discount_amount": item.DiscountAmount, + "net_amount": item.NetAmount, + "organization_name": item.OrganizationName, + "repository_name": item.RepositoryName, + } + } + return result +} + +// flattenPremiumRequestUsageItems converts premium request usage items to a Terraform state-compatible format. +func flattenPremiumRequestUsageItems(items []*github.PremiumRequestUsageItem) []map[string]any { + result := make([]map[string]any, len(items)) + for idx, item := range items { + result[idx] = map[string]any{ + "product": item.Product, + "sku": item.SKU, + "model": item.Model, + "unit_type": item.UnitType, + "price_per_unit": item.PricePerUnit, + "gross_quantity": item.GrossQuantity, + "gross_amount": item.GrossAmount, + "discount_quantity": item.DiscountQuantity, + "discount_amount": item.DiscountAmount, + "net_quantity": item.NetQuantity, + "net_amount": item.NetAmount, + } + } + return result +} + +// flattenUsageSummaryItems converts usage summary items to a Terraform state-compatible format. +func flattenUsageSummaryItems(items []*EnterpriseUsageSummaryItem) []map[string]any { + result := make([]map[string]any, len(items)) + for idx, item := range items { + result[idx] = map[string]any{ + "product": item.Product, + "sku": item.SKU, + "unit_type": item.UnitType, + "price_per_unit": item.PricePerUnit, + "gross_quantity": item.GrossQuantity, + "gross_amount": item.GrossAmount, + "discount_quantity": item.DiscountQuantity, + "discount_amount": item.DiscountAmount, + "net_quantity": item.NetQuantity, + "net_amount": item.NetAmount, + } + } + return result +} + +// flattenTimePeriod converts a PremiumRequestUsageTimePeriod to a Terraform state-compatible format. +func flattenTimePeriod(timePeriod github.PremiumRequestUsageTimePeriod) []map[string]any { + result := map[string]any{ + "year": timePeriod.Year, + } + if timePeriod.Month != nil { + result["month"] = *timePeriod.Month + } else { + result["month"] = 0 + } + if timePeriod.Day != nil { + result["day"] = *timePeriod.Day + } else { + result["day"] = 0 + } + return []map[string]any{result} +} diff --git a/github/util_enterprise_billing_test.go b/github/util_enterprise_billing_test.go new file mode 100644 index 0000000000..9206b547cd --- /dev/null +++ b/github/util_enterprise_billing_test.go @@ -0,0 +1,245 @@ +package github + +import ( + "testing" + + "github.com/google/go-github/v83/github" + "github.com/stretchr/testify/assert" +) + +func TestBuildQueryURL(t *testing.T) { + t.Run("returns base URL when params are empty", func(t *testing.T) { + result := buildQueryURL("enterprises/test/settings/billing/usage", map[string]string{}) + assert.Equal(t, "enterprises/test/settings/billing/usage", result) + }) + + t.Run("returns base URL when all param values are empty strings", func(t *testing.T) { + result := buildQueryURL("enterprises/test/settings/billing/usage", map[string]string{ + "year": "", + "month": "", + }) + assert.Equal(t, "enterprises/test/settings/billing/usage", result) + }) + + t.Run("appends single query parameter", func(t *testing.T) { + result := buildQueryURL("enterprises/test/settings/billing/usage", map[string]string{ + "year": "2025", + }) + assert.Equal(t, "enterprises/test/settings/billing/usage?year=2025", result) + }) + + t.Run("appends multiple query parameters", func(t *testing.T) { + result := buildQueryURL("enterprises/test/settings/billing/usage", map[string]string{ + "year": "2025", + "month": "6", + }) + assert.Contains(t, result, "year=2025") + assert.Contains(t, result, "month=6") + assert.Contains(t, result, "enterprises/test/settings/billing/usage?") + }) + + t.Run("skips empty values in mixed params", func(t *testing.T) { + result := buildQueryURL("enterprises/test/settings/billing/usage", map[string]string{ + "year": "2025", + "month": "", + "cost_center_id": "cc-123", + }) + assert.Contains(t, result, "year=2025") + assert.Contains(t, result, "cost_center_id=cc-123") + assert.NotContains(t, result, "month") + }) + + t.Run("returns base URL when params map is nil-like empty", func(t *testing.T) { + result := buildQueryURL("base/path", map[string]string{}) + assert.Equal(t, "base/path", result) + }) +} + +func TestIntToString(t *testing.T) { + t.Run("returns empty string for zero", func(t *testing.T) { + assert.Equal(t, "", intToString(0)) + }) + + t.Run("converts positive integer", func(t *testing.T) { + assert.Equal(t, "2025", intToString(2025)) + }) + + t.Run("converts single digit", func(t *testing.T) { + assert.Equal(t, "6", intToString(6)) + }) +} + +func TestFlattenUsageItems(t *testing.T) { + t.Run("returns empty slice for nil input", func(t *testing.T) { + result := flattenUsageItems(nil) + assert.Empty(t, result) + }) + + t.Run("returns empty slice for empty input", func(t *testing.T) { + result := flattenUsageItems([]*github.UsageItem{}) + assert.Empty(t, result) + }) + + t.Run("flattens usage items correctly", func(t *testing.T) { + items := []*github.UsageItem{ + { + Date: "2025-01-15", + Product: "Actions", + SKU: "Actions Linux", + Quantity: 100, + UnitType: "minutes", + PricePerUnit: 0.008, + GrossAmount: 0.8, + DiscountAmount: 0, + NetAmount: 0.8, + OrganizationName: github.Ptr("test-org"), + RepositoryName: github.Ptr("test-org/example"), + }, + } + + result := flattenUsageItems(items) + assert.Len(t, result, 1) + assert.Equal(t, "2025-01-15", result[0]["date"]) + assert.Equal(t, "Actions", result[0]["product"]) + assert.Equal(t, "Actions Linux", result[0]["sku"]) + assert.Equal(t, 100.0, result[0]["quantity"]) + assert.Equal(t, "minutes", result[0]["unit_type"]) + assert.InDelta(t, 0.008, result[0]["price_per_unit"], 0.0001) + assert.InDelta(t, 0.8, result[0]["gross_amount"], 0.0001) + assert.InDelta(t, 0.0, result[0]["discount_amount"], 0.0001) + assert.InDelta(t, 0.8, result[0]["net_amount"], 0.0001) + assert.Equal(t, github.Ptr("test-org"), result[0]["organization_name"]) + assert.Equal(t, github.Ptr("test-org/example"), result[0]["repository_name"]) + }) + + t.Run("flattens items with nil optional fields", func(t *testing.T) { + items := []*github.UsageItem{ + { + Date: "2025-01-15", + Product: "Actions", + SKU: "Actions Linux", + Quantity: 50, + UnitType: "minutes", + }, + } + + result := flattenUsageItems(items) + assert.Len(t, result, 1) + assert.Nil(t, result[0]["organization_name"]) + assert.Nil(t, result[0]["repository_name"]) + }) +} + +func TestFlattenPremiumRequestUsageItems(t *testing.T) { + t.Run("returns empty slice for nil input", func(t *testing.T) { + result := flattenPremiumRequestUsageItems(nil) + assert.Empty(t, result) + }) + + t.Run("returns empty slice for empty input", func(t *testing.T) { + result := flattenPremiumRequestUsageItems([]*github.PremiumRequestUsageItem{}) + assert.Empty(t, result) + }) + + t.Run("flattens premium request usage items correctly", func(t *testing.T) { + items := []*github.PremiumRequestUsageItem{ + { + Product: "Copilot", + SKU: "Copilot Premium Request", + Model: "GPT-5", + UnitType: "requests", + PricePerUnit: 0.04, + GrossQuantity: 100, + GrossAmount: 4, + DiscountQuantity: 0, + DiscountAmount: 0, + NetQuantity: 100, + NetAmount: 4, + }, + } + + result := flattenPremiumRequestUsageItems(items) + assert.Len(t, result, 1) + assert.Equal(t, "Copilot", result[0]["product"]) + assert.Equal(t, "Copilot Premium Request", result[0]["sku"]) + assert.Equal(t, "GPT-5", result[0]["model"]) + assert.Equal(t, "requests", result[0]["unit_type"]) + assert.InDelta(t, 0.04, result[0]["price_per_unit"], 0.0001) + assert.InDelta(t, 100.0, result[0]["gross_quantity"], 0.0001) + assert.InDelta(t, 4.0, result[0]["gross_amount"], 0.0001) + assert.InDelta(t, 0.0, result[0]["discount_quantity"], 0.0001) + assert.InDelta(t, 0.0, result[0]["discount_amount"], 0.0001) + assert.InDelta(t, 100.0, result[0]["net_quantity"], 0.0001) + assert.InDelta(t, 4.0, result[0]["net_amount"], 0.0001) + }) +} + +func TestFlattenUsageSummaryItems(t *testing.T) { + t.Run("returns empty slice for nil input", func(t *testing.T) { + result := flattenUsageSummaryItems(nil) + assert.Empty(t, result) + }) + + t.Run("returns empty slice for empty input", func(t *testing.T) { + result := flattenUsageSummaryItems([]*EnterpriseUsageSummaryItem{}) + assert.Empty(t, result) + }) + + t.Run("flattens usage summary items correctly", func(t *testing.T) { + items := []*EnterpriseUsageSummaryItem{ + { + Product: "Actions", + SKU: "actions_linux", + UnitType: "minutes", + PricePerUnit: 0.008, + GrossQuantity: 1000, + GrossAmount: 8, + DiscountQuantity: 0, + DiscountAmount: 0, + NetQuantity: 1000, + NetAmount: 8, + }, + } + + result := flattenUsageSummaryItems(items) + assert.Len(t, result, 1) + assert.Equal(t, "Actions", result[0]["product"]) + assert.Equal(t, "actions_linux", result[0]["sku"]) + assert.Equal(t, "minutes", result[0]["unit_type"]) + assert.InDelta(t, 0.008, result[0]["price_per_unit"], 0.0001) + assert.InDelta(t, 1000.0, result[0]["gross_quantity"], 0.0001) + assert.InDelta(t, 8.0, result[0]["gross_amount"], 0.0001) + assert.InDelta(t, 0.0, result[0]["discount_quantity"], 0.0001) + assert.InDelta(t, 0.0, result[0]["discount_amount"], 0.0001) + assert.InDelta(t, 1000.0, result[0]["net_quantity"], 0.0001) + assert.InDelta(t, 8.0, result[0]["net_amount"], 0.0001) + }) +} + +func TestFlattenTimePeriod(t *testing.T) { + t.Run("flattens time period with year only", func(t *testing.T) { + timePeriod := github.PremiumRequestUsageTimePeriod{ + Year: 2025, + } + + result := flattenTimePeriod(timePeriod) + assert.Len(t, result, 1) + assert.Equal(t, 2025, result[0]["year"]) + assert.Equal(t, 0, result[0]["month"]) + assert.Equal(t, 0, result[0]["day"]) + }) + + t.Run("flattens time period with all fields", func(t *testing.T) { + timePeriod := github.PremiumRequestUsageTimePeriod{ + Year: 2025, + Month: github.Ptr(6), + Day: github.Ptr(15), + } + + result := flattenTimePeriod(timePeriod) + assert.Len(t, result, 1) + assert.Equal(t, 2025, result[0]["year"]) + assert.Equal(t, 6, result[0]["month"]) + assert.Equal(t, 15, result[0]["day"]) + }) +} From c122ebb93d338609324fbfa121030f47a2e755ad Mon Sep 17 00:00:00 2001 From: "Victor M. Varela" Date: Tue, 24 Feb 2026 12:58:27 +0100 Subject: [PATCH 2/7] feat(billing): add enterprise billing data sources Add three new data sources for GitHub enterprise billing: - github_enterprise_billing_usage - github_enterprise_billing_premium_request_usage - github_enterprise_billing_usage_summary --- ...nterprise_billing_premium_request_usage.go | 217 ++++++++++++++++++ ...rise_billing_premium_request_usage_test.go | 63 +++++ ..._source_github_enterprise_billing_usage.go | 148 ++++++++++++ ...github_enterprise_billing_usage_summary.go | 212 +++++++++++++++++ ...b_enterprise_billing_usage_summary_test.go | 63 +++++ ...ce_github_enterprise_billing_usage_test.go | 62 +++++ ...illing_premium_request_usage.html.markdown | 67 ++++++ .../d/enterprise_billing_usage.html.markdown | 57 +++++ ...rprise_billing_usage_summary.html.markdown | 68 ++++++ 9 files changed, 957 insertions(+) create mode 100644 github/data_source_github_enterprise_billing_premium_request_usage.go create mode 100644 github/data_source_github_enterprise_billing_premium_request_usage_test.go create mode 100644 github/data_source_github_enterprise_billing_usage.go create mode 100644 github/data_source_github_enterprise_billing_usage_summary.go create mode 100644 github/data_source_github_enterprise_billing_usage_summary_test.go create mode 100644 github/data_source_github_enterprise_billing_usage_test.go create mode 100644 website/docs/d/enterprise_billing_premium_request_usage.html.markdown create mode 100644 website/docs/d/enterprise_billing_usage.html.markdown create mode 100644 website/docs/d/enterprise_billing_usage_summary.html.markdown diff --git a/github/data_source_github_enterprise_billing_premium_request_usage.go b/github/data_source_github_enterprise_billing_premium_request_usage.go new file mode 100644 index 0000000000..78c7e50f2d --- /dev/null +++ b/github/data_source_github_enterprise_billing_premium_request_usage.go @@ -0,0 +1,217 @@ +package github + +import ( + "context" + + "github.com/google/go-github/v83/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 dataSourceGithubEnterpriseBillingPremiumRequestUsage() *schema.Resource { + return &schema.Resource{ + Description: "Gets a billing premium request usage report for a GitHub enterprise.", + ReadContext: dataSourceGithubEnterpriseBillingPremiumRequestUsageRead, + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + Description: "The slug of the enterprise.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), + }, + "year": { + Type: schema.TypeInt, + Optional: true, + Description: "If specified, only return results for a single year.", + ValidateFunc: validation.IntAtLeast(2000), + }, + "month": { + Type: schema.TypeInt, + Optional: true, + Description: "If specified, only return results for a single month. Value between 1 and 12.", + ValidateFunc: validation.IntBetween(1, 12), + }, + "day": { + Type: schema.TypeInt, + Optional: true, + Description: "If specified, only return results for a single day. Value between 1 and 31.", + ValidateFunc: validation.IntBetween(1, 31), + }, + "organization": { + Type: schema.TypeString, + Optional: true, + Description: "The organization name to query usage for.", + }, + "user": { + Type: schema.TypeString, + Optional: true, + Description: "The user name to query usage for.", + }, + "model": { + Type: schema.TypeString, + Optional: true, + Description: "The model name to query usage for.", + }, + "product": { + Type: schema.TypeString, + Optional: true, + Description: "The product name to query usage for.", + }, + "cost_center_id": { + Type: schema.TypeString, + Optional: true, + Description: "The ID corresponding to a cost center. Use `none` to target usage not associated to any cost center.", + }, + "time_period": { + Type: schema.TypeList, + Computed: true, + Description: "The time period of the report.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "year": { + Type: schema.TypeInt, + Computed: true, + Description: "The year of the time period.", + }, + "month": { + Type: schema.TypeInt, + Computed: true, + Description: "The month of the time period.", + }, + "day": { + Type: schema.TypeInt, + Computed: true, + Description: "The day of the time period.", + }, + }, + }, + }, + "enterprise": { + Type: schema.TypeString, + Computed: true, + Description: "The enterprise name from the report.", + }, + "usage_items": { + Type: schema.TypeList, + Computed: true, + Description: "The list of premium request usage items.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "product": { + Type: schema.TypeString, + Computed: true, + Description: "The product name.", + }, + "sku": { + Type: schema.TypeString, + Computed: true, + Description: "The SKU name.", + }, + "model": { + Type: schema.TypeString, + Computed: true, + Description: "The model name.", + }, + "unit_type": { + Type: schema.TypeString, + Computed: true, + Description: "The type of unit for the usage.", + }, + "price_per_unit": { + Type: schema.TypeFloat, + Computed: true, + Description: "The price per unit of usage.", + }, + "gross_quantity": { + Type: schema.TypeFloat, + Computed: true, + Description: "The gross quantity of usage.", + }, + "gross_amount": { + Type: schema.TypeFloat, + Computed: true, + Description: "The gross amount of usage.", + }, + "discount_quantity": { + Type: schema.TypeFloat, + Computed: true, + Description: "The discount quantity applied.", + }, + "discount_amount": { + Type: schema.TypeFloat, + Computed: true, + Description: "The discount amount applied.", + }, + "net_quantity": { + Type: schema.TypeFloat, + Computed: true, + Description: "The net quantity after discounts.", + }, + "net_amount": { + Type: schema.TypeFloat, + Computed: true, + Description: "The net amount after discounts.", + }, + }, + }, + }, + }, + } +} + +func dataSourceGithubEnterpriseBillingPremiumRequestUsageRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + enterpriseSlug := d.Get("enterprise_slug").(string) + + opts := &EnterprisePremiumRequestUsageOptions{} + if yearVal, ok := d.GetOk("year"); ok { + opts.Year = github.Ptr(yearVal.(int)) + } + if monthVal, ok := d.GetOk("month"); ok { + opts.Month = github.Ptr(monthVal.(int)) + } + if dayVal, ok := d.GetOk("day"); ok { + opts.Day = github.Ptr(dayVal.(int)) + } + if orgVal, ok := d.GetOk("organization"); ok { + opts.Organization = github.Ptr(orgVal.(string)) + } + if userVal, ok := d.GetOk("user"); ok { + opts.User = github.Ptr(userVal.(string)) + } + if modelVal, ok := d.GetOk("model"); ok { + opts.Model = github.Ptr(modelVal.(string)) + } + if productVal, ok := d.GetOk("product"); ok { + opts.Product = github.Ptr(productVal.(string)) + } + if costCenterID, ok := d.GetOk("cost_center_id"); ok { + opts.CostCenterID = github.Ptr(costCenterID.(string)) + } + + report, err := getEnterprisePremiumRequestUsage(ctx, client, enterpriseSlug, opts) + if err != nil { + return diag.Errorf("error getting enterprise billing premium request usage for %q: %s", enterpriseSlug, err) + } + + id, err := buildID(enterpriseSlug, "billing-premium-request-usage") + if err != nil { + return diag.FromErr(err) + } + d.SetId(id) + + if err := d.Set("time_period", flattenTimePeriod(report.TimePeriod)); err != nil { + return diag.FromErr(err) + } + if report.Enterprise != nil { + if err := d.Set("enterprise", *report.Enterprise); err != nil { + return diag.FromErr(err) + } + } + if err := d.Set("usage_items", flattenPremiumRequestUsageItems(report.UsageItems)); err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/github/data_source_github_enterprise_billing_premium_request_usage_test.go b/github/data_source_github_enterprise_billing_premium_request_usage_test.go new file mode 100644 index 0000000000..3133dab9f9 --- /dev/null +++ b/github/data_source_github_enterprise_billing_premium_request_usage_test.go @@ -0,0 +1,63 @@ +package github + +import ( + "fmt" + "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 TestAccGithubEnterpriseBillingPremiumRequestUsage(t *testing.T) { + t.Run("reads premium request usage without error", func(t *testing.T) { + config := fmt.Sprintf(` + data "github_enterprise_billing_premium_request_usage" "test" { + enterprise_slug = "%s" + } + `, testAccConf.enterpriseSlug) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("data.github_enterprise_billing_premium_request_usage.test", + tfjsonpath.New("enterprise_slug"), + knownvalue.StringExact(testAccConf.enterpriseSlug), + ), + }, + }, + }, + }) + }) + + t.Run("reads premium request usage with filters without error", func(t *testing.T) { + config := fmt.Sprintf(` + data "github_enterprise_billing_premium_request_usage" "test" { + enterprise_slug = "%s" + year = 2025 + month = 1 + } + `, testAccConf.enterpriseSlug) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("data.github_enterprise_billing_premium_request_usage.test", + tfjsonpath.New("enterprise_slug"), + knownvalue.StringExact(testAccConf.enterpriseSlug), + ), + }, + }, + }, + }) + }) +} diff --git a/github/data_source_github_enterprise_billing_usage.go b/github/data_source_github_enterprise_billing_usage.go new file mode 100644 index 0000000000..12f9e67bbe --- /dev/null +++ b/github/data_source_github_enterprise_billing_usage.go @@ -0,0 +1,148 @@ +package github + +import ( + "context" + + "github.com/google/go-github/v83/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 dataSourceGithubEnterpriseBillingUsage() *schema.Resource { + return &schema.Resource{ + Description: "Gets a billing usage report for a GitHub enterprise.", + ReadContext: dataSourceGithubEnterpriseBillingUsageRead, + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + Description: "The slug of the enterprise.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), + }, + "year": { + Type: schema.TypeInt, + Optional: true, + Description: "If specified, only return results for a single year.", + ValidateFunc: validation.IntAtLeast(2000), + }, + "month": { + Type: schema.TypeInt, + Optional: true, + Description: "If specified, only return results for a single month. Value between 1 and 12.", + ValidateFunc: validation.IntBetween(1, 12), + }, + "day": { + Type: schema.TypeInt, + Optional: true, + Description: "If specified, only return results for a single day. Value between 1 and 31.", + ValidateFunc: validation.IntBetween(1, 31), + }, + "cost_center_id": { + Type: schema.TypeString, + Optional: true, + Description: "The ID corresponding to a cost center. Use `none` to target usage not associated to any cost center.", + }, + "usage_items": { + Type: schema.TypeList, + Computed: true, + Description: "The list of billing usage items.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "date": { + Type: schema.TypeString, + Computed: true, + Description: "The date of the usage item.", + }, + "product": { + Type: schema.TypeString, + Computed: true, + Description: "The product name.", + }, + "sku": { + Type: schema.TypeString, + Computed: true, + Description: "The SKU name.", + }, + "quantity": { + Type: schema.TypeFloat, + Computed: true, + Description: "The quantity of usage.", + }, + "unit_type": { + Type: schema.TypeString, + Computed: true, + Description: "The type of unit for the usage.", + }, + "price_per_unit": { + Type: schema.TypeFloat, + Computed: true, + Description: "The price per unit of usage.", + }, + "gross_amount": { + Type: schema.TypeFloat, + Computed: true, + Description: "The gross amount of usage.", + }, + "discount_amount": { + Type: schema.TypeFloat, + Computed: true, + Description: "The discount amount applied.", + }, + "net_amount": { + Type: schema.TypeFloat, + Computed: true, + Description: "The net amount after discounts.", + }, + "organization_name": { + Type: schema.TypeString, + Computed: true, + Description: "The organization name associated with the usage.", + }, + "repository_name": { + Type: schema.TypeString, + Computed: true, + Description: "The repository name associated with the usage.", + }, + }, + }, + }, + }, + } +} + +func dataSourceGithubEnterpriseBillingUsageRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + enterpriseSlug := d.Get("enterprise_slug").(string) + + opts := &EnterpriseBillingUsageOptions{} + if yearVal, ok := d.GetOk("year"); ok { + opts.Year = github.Ptr(yearVal.(int)) + } + if monthVal, ok := d.GetOk("month"); ok { + opts.Month = github.Ptr(monthVal.(int)) + } + if dayVal, ok := d.GetOk("day"); ok { + opts.Day = github.Ptr(dayVal.(int)) + } + if costCenterID, ok := d.GetOk("cost_center_id"); ok { + opts.CostCenterID = github.Ptr(costCenterID.(string)) + } + + report, err := getEnterpriseBillingUsage(ctx, client, enterpriseSlug, opts) + if err != nil { + return diag.Errorf("error getting enterprise billing usage for %q: %s", enterpriseSlug, err) + } + + id, err := buildID(enterpriseSlug, "billing-usage") + if err != nil { + return diag.FromErr(err) + } + d.SetId(id) + + if err := d.Set("usage_items", flattenUsageItems(report.UsageItems)); err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/github/data_source_github_enterprise_billing_usage_summary.go b/github/data_source_github_enterprise_billing_usage_summary.go new file mode 100644 index 0000000000..7ece41f5b5 --- /dev/null +++ b/github/data_source_github_enterprise_billing_usage_summary.go @@ -0,0 +1,212 @@ +package github + +import ( + "context" + + "github.com/google/go-github/v83/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 dataSourceGithubEnterpriseBillingUsageSummary() *schema.Resource { + return &schema.Resource{ + Description: "Gets a billing usage summary report for a GitHub enterprise. This API is in public preview and subject to change.", + ReadContext: dataSourceGithubEnterpriseBillingUsageSummaryRead, + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + Description: "The slug of the enterprise.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), + }, + "year": { + Type: schema.TypeInt, + Optional: true, + Description: "If specified, only return results for a single year.", + ValidateFunc: validation.IntAtLeast(2000), + }, + "month": { + Type: schema.TypeInt, + Optional: true, + Description: "If specified, only return results for a single month. Value between 1 and 12.", + ValidateFunc: validation.IntBetween(1, 12), + }, + "day": { + Type: schema.TypeInt, + Optional: true, + Description: "If specified, only return results for a single day. Value between 1 and 31.", + ValidateFunc: validation.IntBetween(1, 31), + }, + "organization": { + Type: schema.TypeString, + Optional: true, + Description: "The organization name to query usage for.", + }, + "repository": { + Type: schema.TypeString, + Optional: true, + Description: "The repository name to query usage for.", + }, + "product": { + Type: schema.TypeString, + Optional: true, + Description: "The product name to query usage for.", + }, + "sku": { + Type: schema.TypeString, + Optional: true, + Description: "The SKU name to query usage for.", + }, + "cost_center_id": { + Type: schema.TypeString, + Optional: true, + Description: "The ID corresponding to a cost center. Use `none` to target usage not associated to any cost center.", + }, + "time_period": { + Type: schema.TypeList, + Computed: true, + Description: "The time period of the report.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "year": { + Type: schema.TypeInt, + Computed: true, + Description: "The year of the time period.", + }, + "month": { + Type: schema.TypeInt, + Computed: true, + Description: "The month of the time period.", + }, + "day": { + Type: schema.TypeInt, + Computed: true, + Description: "The day of the time period.", + }, + }, + }, + }, + "enterprise": { + Type: schema.TypeString, + Computed: true, + Description: "The enterprise name from the report.", + }, + "usage_items": { + Type: schema.TypeList, + Computed: true, + Description: "The list of usage summary items.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "product": { + Type: schema.TypeString, + Computed: true, + Description: "The product name.", + }, + "sku": { + Type: schema.TypeString, + Computed: true, + Description: "The SKU name.", + }, + "unit_type": { + Type: schema.TypeString, + Computed: true, + Description: "The type of unit for the usage.", + }, + "price_per_unit": { + Type: schema.TypeFloat, + Computed: true, + Description: "The price per unit of usage.", + }, + "gross_quantity": { + Type: schema.TypeFloat, + Computed: true, + Description: "The gross quantity of usage.", + }, + "gross_amount": { + Type: schema.TypeFloat, + Computed: true, + Description: "The gross amount of usage.", + }, + "discount_quantity": { + Type: schema.TypeFloat, + Computed: true, + Description: "The discount quantity applied.", + }, + "discount_amount": { + Type: schema.TypeFloat, + Computed: true, + Description: "The discount amount applied.", + }, + "net_quantity": { + Type: schema.TypeFloat, + Computed: true, + Description: "The net quantity after discounts.", + }, + "net_amount": { + Type: schema.TypeFloat, + Computed: true, + Description: "The net amount after discounts.", + }, + }, + }, + }, + }, + } +} + +func dataSourceGithubEnterpriseBillingUsageSummaryRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + enterpriseSlug := d.Get("enterprise_slug").(string) + + opts := &EnterpriseUsageSummaryOptions{} + if yearVal, ok := d.GetOk("year"); ok { + opts.Year = github.Ptr(yearVal.(int)) + } + if monthVal, ok := d.GetOk("month"); ok { + opts.Month = github.Ptr(monthVal.(int)) + } + if dayVal, ok := d.GetOk("day"); ok { + opts.Day = github.Ptr(dayVal.(int)) + } + if orgVal, ok := d.GetOk("organization"); ok { + opts.Organization = github.Ptr(orgVal.(string)) + } + if repoVal, ok := d.GetOk("repository"); ok { + opts.Repository = github.Ptr(repoVal.(string)) + } + if productVal, ok := d.GetOk("product"); ok { + opts.Product = github.Ptr(productVal.(string)) + } + if skuVal, ok := d.GetOk("sku"); ok { + opts.SKU = github.Ptr(skuVal.(string)) + } + if costCenterID, ok := d.GetOk("cost_center_id"); ok { + opts.CostCenterID = github.Ptr(costCenterID.(string)) + } + + report, err := getEnterpriseUsageSummary(ctx, client, enterpriseSlug, opts) + if err != nil { + return diag.Errorf("error getting enterprise billing usage summary for %q: %s", enterpriseSlug, err) + } + + id, err := buildID(enterpriseSlug, "billing-usage-summary") + if err != nil { + return diag.FromErr(err) + } + d.SetId(id) + + if err := d.Set("time_period", flattenTimePeriod(report.TimePeriod)); err != nil { + return diag.FromErr(err) + } + if report.Enterprise != nil { + if err := d.Set("enterprise", *report.Enterprise); err != nil { + return diag.FromErr(err) + } + } + if err := d.Set("usage_items", flattenUsageSummaryItems(report.UsageItems)); err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/github/data_source_github_enterprise_billing_usage_summary_test.go b/github/data_source_github_enterprise_billing_usage_summary_test.go new file mode 100644 index 0000000000..bc77b59895 --- /dev/null +++ b/github/data_source_github_enterprise_billing_usage_summary_test.go @@ -0,0 +1,63 @@ +package github + +import ( + "fmt" + "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 TestAccGithubEnterpriseBillingUsageSummary(t *testing.T) { + t.Run("reads billing usage summary without error", func(t *testing.T) { + config := fmt.Sprintf(` + data "github_enterprise_billing_usage_summary" "test" { + enterprise_slug = "%s" + } + `, testAccConf.enterpriseSlug) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("data.github_enterprise_billing_usage_summary.test", + tfjsonpath.New("enterprise_slug"), + knownvalue.StringExact(testAccConf.enterpriseSlug), + ), + }, + }, + }, + }) + }) + + t.Run("reads billing usage summary with filters without error", func(t *testing.T) { + config := fmt.Sprintf(` + data "github_enterprise_billing_usage_summary" "test" { + enterprise_slug = "%s" + year = 2025 + product = "Actions" + } + `, testAccConf.enterpriseSlug) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("data.github_enterprise_billing_usage_summary.test", + tfjsonpath.New("enterprise_slug"), + knownvalue.StringExact(testAccConf.enterpriseSlug), + ), + }, + }, + }, + }) + }) +} diff --git a/github/data_source_github_enterprise_billing_usage_test.go b/github/data_source_github_enterprise_billing_usage_test.go new file mode 100644 index 0000000000..96514db365 --- /dev/null +++ b/github/data_source_github_enterprise_billing_usage_test.go @@ -0,0 +1,62 @@ +package github + +import ( + "fmt" + "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 TestAccGithubEnterpriseBillingUsage(t *testing.T) { + t.Run("reads billing usage without error", func(t *testing.T) { + config := fmt.Sprintf(` + data "github_enterprise_billing_usage" "test" { + enterprise_slug = "%s" + } + `, testAccConf.enterpriseSlug) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("data.github_enterprise_billing_usage.test", + tfjsonpath.New("enterprise_slug"), + knownvalue.StringExact(testAccConf.enterpriseSlug), + ), + }, + }, + }, + }) + }) + + t.Run("reads billing usage with year filter without error", func(t *testing.T) { + config := fmt.Sprintf(` + data "github_enterprise_billing_usage" "test" { + enterprise_slug = "%s" + year = 2025 + } + `, testAccConf.enterpriseSlug) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("data.github_enterprise_billing_usage.test", + tfjsonpath.New("enterprise_slug"), + knownvalue.StringExact(testAccConf.enterpriseSlug), + ), + }, + }, + }, + }) + }) +} diff --git a/website/docs/d/enterprise_billing_premium_request_usage.html.markdown b/website/docs/d/enterprise_billing_premium_request_usage.html.markdown new file mode 100644 index 0000000000..d23d52cf9f --- /dev/null +++ b/website/docs/d/enterprise_billing_premium_request_usage.html.markdown @@ -0,0 +1,67 @@ +--- +layout: "github" +page_title: "GitHub: github_enterprise_billing_premium_request_usage" +description: |- + Gets a billing premium request usage report for a GitHub enterprise. +--- + +# github_enterprise_billing_premium_request_usage + +Use this data source to retrieve a billing premium request usage report for a GitHub enterprise. +To use this data source, you must be an administrator or billing manager of the enterprise. + +~> **Note:** Only data from the past 24 months is accessible via this data source. + +## Example Usage + +```hcl +data "github_enterprise_billing_premium_request_usage" "example" { + enterprise_slug = "my-enterprise" +} + +# Filter by a specific month and product +data "github_enterprise_billing_premium_request_usage" "copilot" { + enterprise_slug = "my-enterprise" + year = 2025 + month = 6 + product = "Copilot" +} + +# Filter by user +data "github_enterprise_billing_premium_request_usage" "user" { + enterprise_slug = "my-enterprise" + user = "octocat" +} +``` + +## Argument Reference + +* `enterprise_slug` - (Required) The slug of the enterprise. +* `year` - (Optional) If specified, only return results for a single year. +* `month` - (Optional) If specified, only return results for a single month. Value between 1 and 12. +* `day` - (Optional) If specified, only return results for a single day. Value between 1 and 31. +* `organization` - (Optional) The organization name to query usage for. +* `user` - (Optional) The user name to query usage for. +* `model` - (Optional) The model name to query usage for. +* `product` - (Optional) The product name to query usage for. +* `cost_center_id` - (Optional) The ID corresponding to a cost center. Use `none` to target usage not associated to any cost center. + +## Attributes Reference + +* `time_period` - The time period of the report. + * `year` - The year of the time period. + * `month` - The month of the time period. + * `day` - The day of the time period. +* `enterprise` - The enterprise name from the report. +* `usage_items` - The list of premium request usage items. Each item has the following attributes: + * `product` - The product name. + * `sku` - The SKU name. + * `model` - The model name. + * `unit_type` - The type of unit for the usage. + * `price_per_unit` - The price per unit of usage. + * `gross_quantity` - The gross quantity of usage. + * `gross_amount` - The gross amount of usage. + * `discount_quantity` - The discount quantity applied. + * `discount_amount` - The discount amount applied. + * `net_quantity` - The net quantity after discounts. + * `net_amount` - The net amount after discounts. diff --git a/website/docs/d/enterprise_billing_usage.html.markdown b/website/docs/d/enterprise_billing_usage.html.markdown new file mode 100644 index 0000000000..83c994d6ba --- /dev/null +++ b/website/docs/d/enterprise_billing_usage.html.markdown @@ -0,0 +1,57 @@ +--- +layout: "github" +page_title: "GitHub: github_enterprise_billing_usage" +description: |- + Gets a billing usage report for a GitHub enterprise. +--- + +# github_enterprise_billing_usage + +Use this data source to retrieve a billing usage report for a GitHub enterprise. +To use this data source, you must be an administrator or billing manager of the enterprise. + +~> **Note:** This data source is only available to enterprises with access to the enhanced billing platform. + +## Example Usage + +```hcl +data "github_enterprise_billing_usage" "example" { + enterprise_slug = "my-enterprise" +} + +# Filter by a specific month +data "github_enterprise_billing_usage" "monthly" { + enterprise_slug = "my-enterprise" + year = 2025 + month = 6 +} + +# Filter by cost center +data "github_enterprise_billing_usage" "cost_center" { + enterprise_slug = "my-enterprise" + cost_center_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + +## Argument Reference + +* `enterprise_slug` - (Required) The slug of the enterprise. +* `year` - (Optional) If specified, only return results for a single year. +* `month` - (Optional) If specified, only return results for a single month. Value between 1 and 12. +* `day` - (Optional) If specified, only return results for a single day. Value between 1 and 31. +* `cost_center_id` - (Optional) The ID corresponding to a cost center. Use `none` to target usage not associated to any cost center. + +## Attributes Reference + +* `usage_items` - The list of billing usage items. Each item has the following attributes: + * `date` - The date of the usage item. + * `product` - The product name. + * `sku` - The SKU name. + * `quantity` - The quantity of usage. + * `unit_type` - The type of unit for the usage. + * `price_per_unit` - The price per unit of usage. + * `gross_amount` - The gross amount of usage. + * `discount_amount` - The discount amount applied. + * `net_amount` - The net amount after discounts. + * `organization_name` - The organization name associated with the usage. + * `repository_name` - The repository name associated with the usage. diff --git a/website/docs/d/enterprise_billing_usage_summary.html.markdown b/website/docs/d/enterprise_billing_usage_summary.html.markdown new file mode 100644 index 0000000000..fb6221a0de --- /dev/null +++ b/website/docs/d/enterprise_billing_usage_summary.html.markdown @@ -0,0 +1,68 @@ +--- +layout: "github" +page_title: "GitHub: github_enterprise_billing_usage_summary" +description: |- + Gets a billing usage summary for a GitHub enterprise. +--- + +# github_enterprise_billing_usage_summary + +Use this data source to retrieve a billing usage summary for a GitHub enterprise. +To use this data source, you must be an administrator or billing manager of the enterprise. + +~> **Note:** This endpoint is in public preview and is subject to change. + +~> **Note:** Only data from the past 24 months is accessible via this data source. + +## Example Usage + +```hcl +data "github_enterprise_billing_usage_summary" "example" { + enterprise_slug = "my-enterprise" +} + +# Filter by a specific product +data "github_enterprise_billing_usage_summary" "actions" { + enterprise_slug = "my-enterprise" + year = 2025 + product = "Actions" +} + +# Filter by organization and repository +data "github_enterprise_billing_usage_summary" "repo" { + enterprise_slug = "my-enterprise" + organization = "my-org" + repository = "my-org/my-repo" +} +``` + +## Argument Reference + +* `enterprise_slug` - (Required) The slug of the enterprise. +* `year` - (Optional) If specified, only return results for a single year. +* `month` - (Optional) If specified, only return results for a single month. Value between 1 and 12. +* `day` - (Optional) If specified, only return results for a single day. Value between 1 and 31. +* `organization` - (Optional) The organization name to query usage for. +* `repository` - (Optional) The repository name to query for usage in the format `owner/repository`. +* `product` - (Optional) The product name to query usage for. +* `sku` - (Optional) The SKU to query for usage. +* `cost_center_id` - (Optional) The ID corresponding to a cost center. Use `none` to target usage not associated to any cost center. + +## Attributes Reference + +* `time_period` - The time period of the report. + * `year` - The year of the time period. + * `month` - The month of the time period. + * `day` - The day of the time period. +* `enterprise` - The enterprise name from the report. +* `usage_items` - The list of usage summary items. Each item has the following attributes: + * `product` - The product name. + * `sku` - The SKU name. + * `unit_type` - The type of unit for the usage. + * `price_per_unit` - The price per unit of usage. + * `gross_quantity` - The gross quantity of usage. + * `gross_amount` - The gross amount of usage. + * `discount_quantity` - The discount quantity applied. + * `discount_amount` - The discount amount applied. + * `net_quantity` - The net quantity after discounts. + * `net_amount` - The net amount after discounts. From f79b7b0889992a2ac954a6e534ba979e9ea031c9 Mon Sep 17 00:00:00 2001 From: "Victor M. Varela" Date: Tue, 24 Feb 2026 12:58:44 +0100 Subject: [PATCH 3/7] feat(billing): register enterprise billing data sources Register data sources in provider.go and add sidebar entries in github.erb. --- github/provider.go | 3 +++ go.mod | 4 ++++ website/github.erb | 9 +++++++++ 3 files changed, 16 insertions(+) diff --git a/github/provider.go b/github/provider.go index 2d5d6dc33a..aa1758a68d 100644 --- a/github/provider.go +++ b/github/provider.go @@ -294,6 +294,9 @@ func Provider() *schema.Provider { "github_user_external_identity": dataSourceGithubUserExternalIdentity(), "github_users": dataSourceGithubUsers(), "github_enterprise": dataSourceGithubEnterprise(), + "github_enterprise_billing_premium_request_usage": dataSourceGithubEnterpriseBillingPremiumRequestUsage(), + "github_enterprise_billing_usage": dataSourceGithubEnterpriseBillingUsage(), + "github_enterprise_billing_usage_summary": dataSourceGithubEnterpriseBillingUsageSummary(), "github_repository_environment_deployment_policies": dataSourceGithubRepositoryEnvironmentDeploymentPolicies(), }, } diff --git a/go.mod b/go.mod index 9580c26f8f..480da82305 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.2 github.com/hashicorp/terraform-plugin-testing v1.14.0 github.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed + github.com/stretchr/testify v1.10.0 golang.org/x/crypto v0.48.0 golang.org/x/oauth2 v0.35.0 ) @@ -20,6 +21,7 @@ require ( github.com/agext/levenshtein v1.2.2 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/cloudflare/circl v1.6.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fatih/color v1.18.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -51,6 +53,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/run v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/shurcooL/graphql v0.0.0-20240915155400-7ee5256398cf // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect @@ -66,4 +69,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect google.golang.org/grpc v1.75.1 // indirect google.golang.org/protobuf v1.36.9 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/website/github.erb b/website/github.erb index 997536b42f..c6bea4a698 100644 --- a/website/github.erb +++ b/website/github.erb @@ -100,6 +100,15 @@
  • github_enterprise
  • +
  • + github_enterprise_billing_premium_request_usage +
  • +
  • + github_enterprise_billing_usage +
  • +
  • + github_enterprise_billing_usage_summary +
  • github_external_groups
  • From fd770c6a0d0628dac15084bacc634e8c10a24453 Mon Sep 17 00:00:00 2001 From: "Victor M. Varela" Date: Wed, 18 Mar 2026 20:56:28 +0100 Subject: [PATCH 4/7] fix: update go-github import to v84 after rebase --- ...ta_source_github_enterprise_billing_premium_request_usage.go | 2 +- github/data_source_github_enterprise_billing_usage.go | 2 +- github/data_source_github_enterprise_billing_usage_summary.go | 2 +- github/util_enterprise_billing.go | 2 +- github/util_enterprise_billing_test.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/github/data_source_github_enterprise_billing_premium_request_usage.go b/github/data_source_github_enterprise_billing_premium_request_usage.go index 78c7e50f2d..0fea8040bb 100644 --- a/github/data_source_github_enterprise_billing_premium_request_usage.go +++ b/github/data_source_github_enterprise_billing_premium_request_usage.go @@ -3,7 +3,7 @@ package github import ( "context" - "github.com/google/go-github/v83/github" + "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" diff --git a/github/data_source_github_enterprise_billing_usage.go b/github/data_source_github_enterprise_billing_usage.go index 12f9e67bbe..be983f02d4 100644 --- a/github/data_source_github_enterprise_billing_usage.go +++ b/github/data_source_github_enterprise_billing_usage.go @@ -3,7 +3,7 @@ package github import ( "context" - "github.com/google/go-github/v83/github" + "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" diff --git a/github/data_source_github_enterprise_billing_usage_summary.go b/github/data_source_github_enterprise_billing_usage_summary.go index 7ece41f5b5..fedeacf480 100644 --- a/github/data_source_github_enterprise_billing_usage_summary.go +++ b/github/data_source_github_enterprise_billing_usage_summary.go @@ -3,7 +3,7 @@ package github import ( "context" - "github.com/google/go-github/v83/github" + "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" diff --git a/github/util_enterprise_billing.go b/github/util_enterprise_billing.go index 32dee79d5c..967cb68e29 100644 --- a/github/util_enterprise_billing.go +++ b/github/util_enterprise_billing.go @@ -6,7 +6,7 @@ import ( "net/url" "strconv" - "github.com/google/go-github/v83/github" + "github.com/google/go-github/v84/github" ) // EnterpriseBillingUsageOptions specifies optional parameters for the diff --git a/github/util_enterprise_billing_test.go b/github/util_enterprise_billing_test.go index 9206b547cd..57f8e71642 100644 --- a/github/util_enterprise_billing_test.go +++ b/github/util_enterprise_billing_test.go @@ -3,7 +3,7 @@ package github import ( "testing" - "github.com/google/go-github/v83/github" + "github.com/google/go-github/v84/github" "github.com/stretchr/testify/assert" ) From 18f5381b8aafaa975c69368df8260fded650d9fa Mon Sep 17 00:00:00 2001 From: "Victor M. Varela" Date: Fri, 20 Mar 2026 13:03:38 +0100 Subject: [PATCH 5/7] fix(billing): address code review issues before upstream PR - Dereference *string pointer fields (organization_name, repository_name) in flattenUsageItems to avoid storing pointers in TypeString schema fields - Replace deprecated ValidateFunc with ValidateDiagFunc for year/month/day integer validators across all three enterprise billing data sources - Replace fork-local buildID() with upstream-compatible buildTwoPartID() in all three data source Read functions - Update unit test assertions to match dereferenced string values --- ...nterprise_billing_premium_request_usage.go | 30 ++++++++----------- ..._source_github_enterprise_billing_usage.go | 30 ++++++++----------- ...github_enterprise_billing_usage_summary.go | 30 ++++++++----------- github/util_enterprise_billing.go | 12 ++++++-- github/util_enterprise_billing_test.go | 8 ++--- 5 files changed, 53 insertions(+), 57 deletions(-) diff --git a/github/data_source_github_enterprise_billing_premium_request_usage.go b/github/data_source_github_enterprise_billing_premium_request_usage.go index 0fea8040bb..b6b748e3f7 100644 --- a/github/data_source_github_enterprise_billing_premium_request_usage.go +++ b/github/data_source_github_enterprise_billing_premium_request_usage.go @@ -21,22 +21,22 @@ func dataSourceGithubEnterpriseBillingPremiumRequestUsage() *schema.Resource { ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), }, "year": { - Type: schema.TypeInt, - Optional: true, - Description: "If specified, only return results for a single year.", - ValidateFunc: validation.IntAtLeast(2000), + Type: schema.TypeInt, + Optional: true, + Description: "If specified, only return results for a single year.", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(2000)), }, "month": { - Type: schema.TypeInt, - Optional: true, - Description: "If specified, only return results for a single month. Value between 1 and 12.", - ValidateFunc: validation.IntBetween(1, 12), + Type: schema.TypeInt, + Optional: true, + Description: "If specified, only return results for a single month. Value between 1 and 12.", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(1, 12)), }, "day": { - Type: schema.TypeInt, - Optional: true, - Description: "If specified, only return results for a single day. Value between 1 and 31.", - ValidateFunc: validation.IntBetween(1, 31), + Type: schema.TypeInt, + Optional: true, + Description: "If specified, only return results for a single day. Value between 1 and 31.", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(1, 31)), }, "organization": { Type: schema.TypeString, @@ -195,11 +195,7 @@ func dataSourceGithubEnterpriseBillingPremiumRequestUsageRead(ctx context.Contex return diag.Errorf("error getting enterprise billing premium request usage for %q: %s", enterpriseSlug, err) } - id, err := buildID(enterpriseSlug, "billing-premium-request-usage") - if err != nil { - return diag.FromErr(err) - } - d.SetId(id) + d.SetId(buildTwoPartID(enterpriseSlug, "billing-premium-request-usage")) if err := d.Set("time_period", flattenTimePeriod(report.TimePeriod)); err != nil { return diag.FromErr(err) diff --git a/github/data_source_github_enterprise_billing_usage.go b/github/data_source_github_enterprise_billing_usage.go index be983f02d4..3ebcf07cf8 100644 --- a/github/data_source_github_enterprise_billing_usage.go +++ b/github/data_source_github_enterprise_billing_usage.go @@ -21,22 +21,22 @@ func dataSourceGithubEnterpriseBillingUsage() *schema.Resource { ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), }, "year": { - Type: schema.TypeInt, - Optional: true, - Description: "If specified, only return results for a single year.", - ValidateFunc: validation.IntAtLeast(2000), + Type: schema.TypeInt, + Optional: true, + Description: "If specified, only return results for a single year.", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(2000)), }, "month": { - Type: schema.TypeInt, - Optional: true, - Description: "If specified, only return results for a single month. Value between 1 and 12.", - ValidateFunc: validation.IntBetween(1, 12), + Type: schema.TypeInt, + Optional: true, + Description: "If specified, only return results for a single month. Value between 1 and 12.", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(1, 12)), }, "day": { - Type: schema.TypeInt, - Optional: true, - Description: "If specified, only return results for a single day. Value between 1 and 31.", - ValidateFunc: validation.IntBetween(1, 31), + Type: schema.TypeInt, + Optional: true, + Description: "If specified, only return results for a single day. Value between 1 and 31.", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(1, 31)), }, "cost_center_id": { Type: schema.TypeString, @@ -134,11 +134,7 @@ func dataSourceGithubEnterpriseBillingUsageRead(ctx context.Context, d *schema.R return diag.Errorf("error getting enterprise billing usage for %q: %s", enterpriseSlug, err) } - id, err := buildID(enterpriseSlug, "billing-usage") - if err != nil { - return diag.FromErr(err) - } - d.SetId(id) + d.SetId(buildTwoPartID(enterpriseSlug, "billing-usage")) if err := d.Set("usage_items", flattenUsageItems(report.UsageItems)); err != nil { return diag.FromErr(err) diff --git a/github/data_source_github_enterprise_billing_usage_summary.go b/github/data_source_github_enterprise_billing_usage_summary.go index fedeacf480..ec9ee12fd0 100644 --- a/github/data_source_github_enterprise_billing_usage_summary.go +++ b/github/data_source_github_enterprise_billing_usage_summary.go @@ -21,22 +21,22 @@ func dataSourceGithubEnterpriseBillingUsageSummary() *schema.Resource { ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), }, "year": { - Type: schema.TypeInt, - Optional: true, - Description: "If specified, only return results for a single year.", - ValidateFunc: validation.IntAtLeast(2000), + Type: schema.TypeInt, + Optional: true, + Description: "If specified, only return results for a single year.", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(2000)), }, "month": { - Type: schema.TypeInt, - Optional: true, - Description: "If specified, only return results for a single month. Value between 1 and 12.", - ValidateFunc: validation.IntBetween(1, 12), + Type: schema.TypeInt, + Optional: true, + Description: "If specified, only return results for a single month. Value between 1 and 12.", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(1, 12)), }, "day": { - Type: schema.TypeInt, - Optional: true, - Description: "If specified, only return results for a single day. Value between 1 and 31.", - ValidateFunc: validation.IntBetween(1, 31), + Type: schema.TypeInt, + Optional: true, + Description: "If specified, only return results for a single day. Value between 1 and 31.", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(1, 31)), }, "organization": { Type: schema.TypeString, @@ -190,11 +190,7 @@ func dataSourceGithubEnterpriseBillingUsageSummaryRead(ctx context.Context, d *s return diag.Errorf("error getting enterprise billing usage summary for %q: %s", enterpriseSlug, err) } - id, err := buildID(enterpriseSlug, "billing-usage-summary") - if err != nil { - return diag.FromErr(err) - } - d.SetId(id) + d.SetId(buildTwoPartID(enterpriseSlug, "billing-usage-summary")) if err := d.Set("time_period", flattenTimePeriod(report.TimePeriod)); err != nil { return diag.FromErr(err) diff --git a/github/util_enterprise_billing.go b/github/util_enterprise_billing.go index 967cb68e29..326792c2d5 100644 --- a/github/util_enterprise_billing.go +++ b/github/util_enterprise_billing.go @@ -236,6 +236,14 @@ func getEnterpriseUsageSummary(ctx context.Context, client *github.Client, enter func flattenUsageItems(items []*github.UsageItem) []map[string]any { result := make([]map[string]any, len(items)) for idx, item := range items { + orgName := "" + if item.OrganizationName != nil { + orgName = *item.OrganizationName + } + repoName := "" + if item.RepositoryName != nil { + repoName = *item.RepositoryName + } result[idx] = map[string]any{ "date": item.Date, "product": item.Product, @@ -246,8 +254,8 @@ func flattenUsageItems(items []*github.UsageItem) []map[string]any { "gross_amount": item.GrossAmount, "discount_amount": item.DiscountAmount, "net_amount": item.NetAmount, - "organization_name": item.OrganizationName, - "repository_name": item.RepositoryName, + "organization_name": orgName, + "repository_name": repoName, } } return result diff --git a/github/util_enterprise_billing_test.go b/github/util_enterprise_billing_test.go index 57f8e71642..a0e07af9ad 100644 --- a/github/util_enterprise_billing_test.go +++ b/github/util_enterprise_billing_test.go @@ -108,8 +108,8 @@ func TestFlattenUsageItems(t *testing.T) { assert.InDelta(t, 0.8, result[0]["gross_amount"], 0.0001) assert.InDelta(t, 0.0, result[0]["discount_amount"], 0.0001) assert.InDelta(t, 0.8, result[0]["net_amount"], 0.0001) - assert.Equal(t, github.Ptr("test-org"), result[0]["organization_name"]) - assert.Equal(t, github.Ptr("test-org/example"), result[0]["repository_name"]) + assert.Equal(t, "test-org", result[0]["organization_name"]) + assert.Equal(t, "test-org/example", result[0]["repository_name"]) }) t.Run("flattens items with nil optional fields", func(t *testing.T) { @@ -125,8 +125,8 @@ func TestFlattenUsageItems(t *testing.T) { result := flattenUsageItems(items) assert.Len(t, result, 1) - assert.Nil(t, result[0]["organization_name"]) - assert.Nil(t, result[0]["repository_name"]) + assert.Equal(t, "", result[0]["organization_name"]) + assert.Equal(t, "", result[0]["repository_name"]) }) } From aed1a52c118a8b3464d1f794a3e6589423d7ebde Mon Sep 17 00:00:00 2001 From: "Victor M. Varela" Date: Fri, 20 Mar 2026 13:12:08 +0100 Subject: [PATCH 6/7] test(billing-usage): improve acceptance tests with ID format and structural checks Add assertions for: - ID format using StringRegexp (e.g. 'slug:billing-usage') - usage_items and time_period are NotNull - Additional filter variants (year+month) for billing usage data source --- ...rise_billing_premium_request_usage_test.go | 21 +++++++++ ...b_enterprise_billing_usage_summary_test.go | 21 +++++++++ ...ce_github_enterprise_billing_usage_test.go | 43 +++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/github/data_source_github_enterprise_billing_premium_request_usage_test.go b/github/data_source_github_enterprise_billing_premium_request_usage_test.go index 3133dab9f9..be448b13ef 100644 --- a/github/data_source_github_enterprise_billing_premium_request_usage_test.go +++ b/github/data_source_github_enterprise_billing_premium_request_usage_test.go @@ -2,6 +2,7 @@ package github import ( "fmt" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -29,6 +30,18 @@ func TestAccGithubEnterpriseBillingPremiumRequestUsage(t *testing.T) { tfjsonpath.New("enterprise_slug"), knownvalue.StringExact(testAccConf.enterpriseSlug), ), + statecheck.ExpectKnownValue("data.github_enterprise_billing_premium_request_usage.test", + tfjsonpath.New("id"), + knownvalue.StringRegexp(regexp.MustCompile(`^.+:billing-premium-request-usage$`)), + ), + statecheck.ExpectKnownValue("data.github_enterprise_billing_premium_request_usage.test", + tfjsonpath.New("usage_items"), + knownvalue.NotNull(), + ), + statecheck.ExpectKnownValue("data.github_enterprise_billing_premium_request_usage.test", + tfjsonpath.New("time_period"), + knownvalue.NotNull(), + ), }, }, }, @@ -55,6 +68,14 @@ func TestAccGithubEnterpriseBillingPremiumRequestUsage(t *testing.T) { tfjsonpath.New("enterprise_slug"), knownvalue.StringExact(testAccConf.enterpriseSlug), ), + statecheck.ExpectKnownValue("data.github_enterprise_billing_premium_request_usage.test", + tfjsonpath.New("usage_items"), + knownvalue.NotNull(), + ), + statecheck.ExpectKnownValue("data.github_enterprise_billing_premium_request_usage.test", + tfjsonpath.New("time_period"), + knownvalue.NotNull(), + ), }, }, }, diff --git a/github/data_source_github_enterprise_billing_usage_summary_test.go b/github/data_source_github_enterprise_billing_usage_summary_test.go index bc77b59895..07694d92f4 100644 --- a/github/data_source_github_enterprise_billing_usage_summary_test.go +++ b/github/data_source_github_enterprise_billing_usage_summary_test.go @@ -2,6 +2,7 @@ package github import ( "fmt" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -29,6 +30,18 @@ func TestAccGithubEnterpriseBillingUsageSummary(t *testing.T) { tfjsonpath.New("enterprise_slug"), knownvalue.StringExact(testAccConf.enterpriseSlug), ), + statecheck.ExpectKnownValue("data.github_enterprise_billing_usage_summary.test", + tfjsonpath.New("id"), + knownvalue.StringRegexp(regexp.MustCompile(`^.+:billing-usage-summary$`)), + ), + statecheck.ExpectKnownValue("data.github_enterprise_billing_usage_summary.test", + tfjsonpath.New("usage_items"), + knownvalue.NotNull(), + ), + statecheck.ExpectKnownValue("data.github_enterprise_billing_usage_summary.test", + tfjsonpath.New("time_period"), + knownvalue.NotNull(), + ), }, }, }, @@ -55,6 +68,14 @@ func TestAccGithubEnterpriseBillingUsageSummary(t *testing.T) { tfjsonpath.New("enterprise_slug"), knownvalue.StringExact(testAccConf.enterpriseSlug), ), + statecheck.ExpectKnownValue("data.github_enterprise_billing_usage_summary.test", + tfjsonpath.New("usage_items"), + knownvalue.NotNull(), + ), + statecheck.ExpectKnownValue("data.github_enterprise_billing_usage_summary.test", + tfjsonpath.New("time_period"), + knownvalue.NotNull(), + ), }, }, }, diff --git a/github/data_source_github_enterprise_billing_usage_test.go b/github/data_source_github_enterprise_billing_usage_test.go index 96514db365..1571646b51 100644 --- a/github/data_source_github_enterprise_billing_usage_test.go +++ b/github/data_source_github_enterprise_billing_usage_test.go @@ -2,6 +2,7 @@ package github import ( "fmt" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -29,6 +30,14 @@ func TestAccGithubEnterpriseBillingUsage(t *testing.T) { tfjsonpath.New("enterprise_slug"), knownvalue.StringExact(testAccConf.enterpriseSlug), ), + statecheck.ExpectKnownValue("data.github_enterprise_billing_usage.test", + tfjsonpath.New("id"), + knownvalue.StringRegexp(regexp.MustCompile(`^.+:billing-usage$`)), + ), + statecheck.ExpectKnownValue("data.github_enterprise_billing_usage.test", + tfjsonpath.New("usage_items"), + knownvalue.NotNull(), + ), }, }, }, @@ -54,6 +63,40 @@ func TestAccGithubEnterpriseBillingUsage(t *testing.T) { tfjsonpath.New("enterprise_slug"), knownvalue.StringExact(testAccConf.enterpriseSlug), ), + statecheck.ExpectKnownValue("data.github_enterprise_billing_usage.test", + tfjsonpath.New("usage_items"), + knownvalue.NotNull(), + ), + }, + }, + }, + }) + }) + + t.Run("reads billing usage with month filter without error", func(t *testing.T) { + config := fmt.Sprintf(` + data "github_enterprise_billing_usage" "test" { + enterprise_slug = "%s" + year = 2025 + month = 1 + } + `, testAccConf.enterpriseSlug) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("data.github_enterprise_billing_usage.test", + tfjsonpath.New("enterprise_slug"), + knownvalue.StringExact(testAccConf.enterpriseSlug), + ), + statecheck.ExpectKnownValue("data.github_enterprise_billing_usage.test", + tfjsonpath.New("usage_items"), + knownvalue.NotNull(), + ), }, }, }, From 9f05b147431ab4643fe9e585ec091098ad8787bf Mon Sep 17 00:00:00 2001 From: "Victor M. Varela" Date: Mon, 23 Mar 2026 20:20:37 +0100 Subject: [PATCH 7/7] chore(lint): exclude modernize newexpr rule for Ptr calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The modernize/newexpr rule incorrectly suggests replacing github.Ptr(x) with new(x), but new() takes a type not an expression — the auto-fix generates invalid Go code. Exclude this specific rule pattern. --- .golangci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index 900dce093b..27dc87a4ed 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -47,6 +47,9 @@ linters: - linters: - staticcheck text: "SA1019.*ByID.*deprecated" + - linters: + - modernize + text: "newexpr: call of Ptr" issues: max-issues-per-linter: 0