From 104ac9f9699571f5b7141ef5ef470fd71f6fccd6 Mon Sep 17 00:00:00 2001 From: Anne-Marie <102995847+am-stead@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:43:24 +0100 Subject: [PATCH 1/4] Deprecate gpt 4.1 (#61553) Co-authored-by: Jenni C <97056108+dihydroJenoxide@users.noreply.github.com> --- .../data-residency/github-copilot-with-data-residency.md | 2 ++ content/copilot/reference/ai-models/model-comparison.md | 2 +- content/copilot/reference/ai-models/model-hosting.md | 1 - .../reference/copilot-billing/models-and-pricing.md | 2 +- .../copilot/annual-subscriber-model-multipliers.yml | 3 --- data/tables/copilot/auto-model-selection.yml | 4 ---- data/tables/copilot/model-comparison.yml | 4 ---- data/tables/copilot/model-deprecation-history.yml | 4 ++++ data/tables/copilot/model-multipliers.yml | 4 ---- data/tables/copilot/model-release-status.yml | 6 ------ data/tables/copilot/model-supported-clients.yml | 9 --------- data/tables/copilot/model-supported-plans.yml | 7 ------- data/tables/copilot/models-and-pricing.yml | 7 ------- 13 files changed, 8 insertions(+), 47 deletions(-) diff --git a/content/admin/data-residency/github-copilot-with-data-residency.md b/content/admin/data-residency/github-copilot-with-data-residency.md index 79843a7a7eb6..aabef4cdb599 100644 --- a/content/admin/data-residency/github-copilot-with-data-residency.md +++ b/content/admin/data-residency/github-copilot-with-data-residency.md @@ -42,6 +42,8 @@ The enforcement happens at multiple levels: The models available for {% data variables.product.prodname_copilot_short %} vary by region. {% data reusables.copilot.model-compliance.models-intro %} +> [!NOTE] Some models listed may only be available as utility models. See [AUTOTITLE](/copilot/concepts/models/utility-models). + ### United States {% data reusables.copilot.model-compliance.us-models %} diff --git a/content/copilot/reference/ai-models/model-comparison.md b/content/copilot/reference/ai-models/model-comparison.md index 216f76e69cea..1ae59aef9bd2 100644 --- a/content/copilot/reference/ai-models/model-comparison.md +++ b/content/copilot/reference/ai-models/model-comparison.md @@ -138,7 +138,7 @@ If your task involves deep reasoning or large-scale refactoring, consider a mode [^mai-code-1-flash]: {% data variables.copilot.copilot_mai_code_1_flash %} is a continuously improving model. Performance and behavior may evolve over time as new checkpoints are released. -Choosing the right model helps you get the most out of {% data variables.product.prodname_copilot_short %}. If you're not sure which model to use, start with a general-purpose option like {% data variables.copilot.copilot_gpt_41 %}, then adjust based on your needs. +Choosing the right model helps you get the most out of {% data variables.product.prodname_copilot_short %}. If you're not sure which model to use, start with a general-purpose option like {% data variables.copilot.copilot_gpt_5_mini %}, then adjust based on your needs. * For detailed model specs and pricing, see [AUTOTITLE](/copilot/using-github-copilot/ai-models/supported-ai-models-in-copilot). * For more examples of how to use different models, see [AUTOTITLE](/copilot/using-github-copilot/ai-models/comparing-ai-models-using-different-tasks). diff --git a/content/copilot/reference/ai-models/model-hosting.md b/content/copilot/reference/ai-models/model-hosting.md index bcea48af9866..aadc5f32c649 100644 --- a/content/copilot/reference/ai-models/model-hosting.md +++ b/content/copilot/reference/ai-models/model-hosting.md @@ -20,7 +20,6 @@ contentType: reference Used for: -* {% data variables.copilot.copilot_gpt_41 %} * {% data variables.copilot.copilot_gpt_5_mini %} * {% data variables.copilot.copilot_gpt_52 %} * {% data variables.copilot.copilot_gpt_52_codex %} diff --git a/content/copilot/reference/copilot-billing/models-and-pricing.md b/content/copilot/reference/copilot-billing/models-and-pricing.md index 100e70f239c4..d2b42654a306 100644 --- a/content/copilot/reference/copilot-billing/models-and-pricing.md +++ b/content/copilot/reference/copilot-billing/models-and-pricing.md @@ -37,7 +37,7 @@ All prices are **per 1 million tokens**. | {{ entry.model }} | {{ entry.release_status }} | {{ entry.category }} | {{ entry.input }} | {{ entry.cached_input }} | {{ entry.output }} | | {% endif %}{% endfor %} | -[^1]: {% data variables.copilot.copilot_gpt_41 %} and {% data variables.copilot.copilot_gpt_5_mini %} are included models. +[^1]: {% data variables.copilot.copilot_gpt_5_mini %} is an included model. [^2]: {% data variables.copilot.copilot_gpt_54 %} pricing applies to prompts with ≤272K tokens. ### Anthropic diff --git a/data/tables/copilot/annual-subscriber-model-multipliers.yml b/data/tables/copilot/annual-subscriber-model-multipliers.yml index 4ff52842bc29..c96ba48c4c67 100644 --- a/data/tables/copilot/annual-subscriber-model-multipliers.yml +++ b/data/tables/copilot/annual-subscriber-model-multipliers.yml @@ -47,9 +47,6 @@ - model: 'GPT-4o mini' new_multiplier: '0.33' -- model: 'GPT-4.1' - new_multiplier: '1' - - model: 'GPT-5.1' new_multiplier: '3' diff --git a/data/tables/copilot/auto-model-selection.yml b/data/tables/copilot/auto-model-selection.yml index 8ec9520b7cee..e490be383757 100644 --- a/data/tables/copilot/auto-model-selection.yml +++ b/data/tables/copilot/auto-model-selection.yml @@ -18,10 +18,6 @@ # - cli: Availability for Copilot CLI. # OpenAI -- name: GPT-4.1 - cloud_agent: false - chat: true - cli: false - name: GPT-5 mini cloud_agent: false diff --git a/data/tables/copilot/model-comparison.yml b/data/tables/copilot/model-comparison.yml index 7d3346899fd1..8edfede5736d 100644 --- a/data/tables/copilot/model-comparison.yml +++ b/data/tables/copilot/model-comparison.yml @@ -7,10 +7,6 @@ # 2. Within each provider group, alphabetically by model name. # OpenAI -- name: GPT-4.1 - task_area: General-purpose coding and writing - excels_at: Fast, accurate code completions and explanations - further_reading: '[GPT-4.1 model card](https://openai.com/index/gpt-4-1/)' - name: GPT-5 mini task_area: General-purpose coding and writing diff --git a/data/tables/copilot/model-deprecation-history.yml b/data/tables/copilot/model-deprecation-history.yml index 098080f4c291..6a3914955c34 100644 --- a/data/tables/copilot/model-deprecation-history.yml +++ b/data/tables/copilot/model-deprecation-history.yml @@ -11,6 +11,10 @@ # - retirement_date: The official retirement date for the model (YYYY-MM-DD). # - suggested_alternative: The model recommended for migration. +- name: 'GPT-4.1' + retirement_date: '2026-06-01' + suggested_alternative: 'GPT-5.5' + - name: 'Grok Code Fast 1' retirement_date: '2026-05-15' suggested_alternative: 'GPT-5 mini' diff --git a/data/tables/copilot/model-multipliers.yml b/data/tables/copilot/model-multipliers.yml index b12e88b65f5e..c65d4e26624a 100644 --- a/data/tables/copilot/model-multipliers.yml +++ b/data/tables/copilot/model-multipliers.yml @@ -57,10 +57,6 @@ multiplier_paid: 14 multiplier_free: Not applicable -- name: GPT-4.1 - multiplier_paid: 0 - multiplier_free: 1 - - name: GPT-4o multiplier_paid: 0 multiplier_free: 1 diff --git a/data/tables/copilot/model-release-status.yml b/data/tables/copilot/model-release-status.yml index f25f3e1560d0..dfbeaf396ca1 100644 --- a/data/tables/copilot/model-release-status.yml +++ b/data/tables/copilot/model-release-status.yml @@ -18,12 +18,6 @@ # - edit_mode: true = ✓, false = ✗ # OpenAI models -- name: 'GPT-4.1' - provider: 'OpenAI' - release_status: 'Closing down 2026-06-01' - agent_mode: true - ask_mode: true - edit_mode: true - name: 'GPT-5 mini' provider: 'OpenAI' diff --git a/data/tables/copilot/model-supported-clients.yml b/data/tables/copilot/model-supported-clients.yml index 994bd90efae1..e11d75902e77 100644 --- a/data/tables/copilot/model-supported-clients.yml +++ b/data/tables/copilot/model-supported-clients.yml @@ -122,15 +122,6 @@ xcode: true jetbrains: true -- name: GPT-4.1 - dotcom: true - cli: true - vscode: true - vs: true - eclipse: true - xcode: true - jetbrains: true - - name: MAI-Code-1-Flash dotcom: false cli: false diff --git a/data/tables/copilot/model-supported-plans.yml b/data/tables/copilot/model-supported-plans.yml index 2bb8eba4ce42..4a8118bd402d 100644 --- a/data/tables/copilot/model-supported-plans.yml +++ b/data/tables/copilot/model-supported-plans.yml @@ -96,13 +96,6 @@ business: true enterprise: true -- name: GPT-4.1 - pro: true - pro_plus: true - max: true - business: true - enterprise: true - - name: MAI-Code-1-Flash pro: true pro_plus: true diff --git a/data/tables/copilot/models-and-pricing.yml b/data/tables/copilot/models-and-pricing.yml index 7faab4d7056f..16a14217120e 100644 --- a/data/tables/copilot/models-and-pricing.yml +++ b/data/tables/copilot/models-and-pricing.yml @@ -16,13 +16,6 @@ # - notes: Optional notes about the model. # OpenAI -- model: 'GPT-4.1[^1]' - provider: openai - release_status: GA - category: Versatile - input: $2.00 - cached_input: $0.50 - output: $8.00 - model: 'GPT-5 mini[^1]' provider: openai From b148c1cc370e0a13ef3f4cee1efc1f8dbbbdda92 Mon Sep 17 00:00:00 2001 From: Siara <108543037+SiaraMist@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:53:16 -0700 Subject: [PATCH 2/4] Add note to experimental CLI feature docs (#61555) --- .../copilot-cli/automate-copilot-cli/schedule-prompts.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/content/copilot/how-tos/copilot-cli/automate-copilot-cli/schedule-prompts.md b/content/copilot/how-tos/copilot-cli/automate-copilot-cli/schedule-prompts.md index abf088be6fee..e6f404b3d7c0 100644 --- a/content/copilot/how-tos/copilot-cli/automate-copilot-cli/schedule-prompts.md +++ b/content/copilot/how-tos/copilot-cli/automate-copilot-cli/schedule-prompts.md @@ -13,6 +13,9 @@ category: - Build with Copilot CLI # Copilot CLI bespoke page --- +> [!NOTE] +> The `/every` and `/after` commands are currently experimental features and are only available if you have used the `/experimental on` slash command, or the `--experimental` command line option. + In an interactive {% data variables.copilot.copilot_cli_short %} session you can schedule a prompt to be submitted automatically. This is useful when you want {% data variables.product.prodname_copilot_short %} to repeat a task at a regular cadence or to perform a one-off task after a delay, without you having to remember to submit the prompt manually. There are two slash commands for this: From 78d7950c7282b34b8b5d22241e69d8ac6c802201 Mon Sep 17 00:00:00 2001 From: docs-bot <77750099+docs-bot@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:28:25 -0700 Subject: [PATCH 3/4] Update OpenAPI Description (#61556) Co-authored-by: Joe Clark <31087804+jc-clark@users.noreply.github.com> --- .../fine-grained-pat-permissions.json | 9 + .../data/fpt-2022-11-28/fine-grained-pat.json | 6 + .../server-to-server-permissions.json | 11 + .../fpt-2022-11-28/server-to-server-rest.json | 6 + .../fpt-2022-11-28/user-to-server-rest.json | 6 + .../fine-grained-pat-permissions.json | 9 + .../data/fpt-2026-03-10/fine-grained-pat.json | 6 + .../server-to-server-permissions.json | 11 + .../fpt-2026-03-10/server-to-server-rest.json | 6 + .../fpt-2026-03-10/user-to-server-rest.json | 6 + src/github-apps/lib/config.json | 2 +- src/rest/data/fpt-2022-11-28/billing.json | 633 +++++++++++++++++- src/rest/data/fpt-2026-03-10/billing.json | 633 +++++++++++++++++- src/rest/data/ghec-2022-11-28/billing.json | 274 +++++++- src/rest/data/ghec-2026-03-10/billing.json | 274 +++++++- src/rest/lib/config.json | 2 +- src/webhooks/lib/config.json | 2 +- 17 files changed, 1859 insertions(+), 37 deletions(-) diff --git a/src/github-apps/data/fpt-2022-11-28/fine-grained-pat-permissions.json b/src/github-apps/data/fpt-2022-11-28/fine-grained-pat-permissions.json index ce358a44543d..9dd453365692 100644 --- a/src/github-apps/data/fpt-2022-11-28/fine-grained-pat-permissions.json +++ b/src/github-apps/data/fpt-2022-11-28/fine-grained-pat-permissions.json @@ -144,6 +144,15 @@ "additional-permissions": false, "access": "read" }, + { + "category": "billing", + "slug": "create-a-budget-for-an-organization", + "subcategory": "budgets", + "verb": "post", + "requestPath": "/organizations/{org}/settings/billing/budgets", + "additional-permissions": false, + "access": "write" + }, { "category": "billing", "slug": "get-a-budget-by-id-for-an-organization", diff --git a/src/github-apps/data/fpt-2022-11-28/fine-grained-pat.json b/src/github-apps/data/fpt-2022-11-28/fine-grained-pat.json index 1c6ef461e3f9..8453987c963d 100644 --- a/src/github-apps/data/fpt-2022-11-28/fine-grained-pat.json +++ b/src/github-apps/data/fpt-2022-11-28/fine-grained-pat.json @@ -1386,6 +1386,12 @@ "verb": "get", "requestPath": "/organizations/{org}/settings/billing/budgets" }, + { + "slug": "create-a-budget-for-an-organization", + "subcategory": "budgets", + "verb": "post", + "requestPath": "/organizations/{org}/settings/billing/budgets" + }, { "slug": "get-a-budget-by-id-for-an-organization", "subcategory": "budgets", diff --git a/src/github-apps/data/fpt-2022-11-28/server-to-server-permissions.json b/src/github-apps/data/fpt-2022-11-28/server-to-server-permissions.json index 40e88d6c4229..7a3c2308a982 100644 --- a/src/github-apps/data/fpt-2022-11-28/server-to-server-permissions.json +++ b/src/github-apps/data/fpt-2022-11-28/server-to-server-permissions.json @@ -428,6 +428,17 @@ "server-to-server": true, "additional-permissions": false }, + { + "category": "billing", + "slug": "create-a-budget-for-an-organization", + "subcategory": "budgets", + "verb": "post", + "requestPath": "/organizations/{org}/settings/billing/budgets", + "access": "write", + "user-to-server": true, + "server-to-server": true, + "additional-permissions": false + }, { "category": "billing", "slug": "get-a-budget-by-id-for-an-organization", diff --git a/src/github-apps/data/fpt-2022-11-28/server-to-server-rest.json b/src/github-apps/data/fpt-2022-11-28/server-to-server-rest.json index 91a58f9015bd..a8c2d8bfdffc 100644 --- a/src/github-apps/data/fpt-2022-11-28/server-to-server-rest.json +++ b/src/github-apps/data/fpt-2022-11-28/server-to-server-rest.json @@ -1466,6 +1466,12 @@ "verb": "get", "requestPath": "/organizations/{org}/settings/billing/budgets" }, + { + "slug": "create-a-budget-for-an-organization", + "subcategory": "budgets", + "verb": "post", + "requestPath": "/organizations/{org}/settings/billing/budgets" + }, { "slug": "get-a-budget-by-id-for-an-organization", "subcategory": "budgets", diff --git a/src/github-apps/data/fpt-2022-11-28/user-to-server-rest.json b/src/github-apps/data/fpt-2022-11-28/user-to-server-rest.json index f973e92f8a1a..6436d3495075 100644 --- a/src/github-apps/data/fpt-2022-11-28/user-to-server-rest.json +++ b/src/github-apps/data/fpt-2022-11-28/user-to-server-rest.json @@ -1508,6 +1508,12 @@ "verb": "get", "requestPath": "/organizations/{org}/settings/billing/budgets" }, + { + "slug": "create-a-budget-for-an-organization", + "subcategory": "budgets", + "verb": "post", + "requestPath": "/organizations/{org}/settings/billing/budgets" + }, { "slug": "get-a-budget-by-id-for-an-organization", "subcategory": "budgets", diff --git a/src/github-apps/data/fpt-2026-03-10/fine-grained-pat-permissions.json b/src/github-apps/data/fpt-2026-03-10/fine-grained-pat-permissions.json index ce358a44543d..9dd453365692 100644 --- a/src/github-apps/data/fpt-2026-03-10/fine-grained-pat-permissions.json +++ b/src/github-apps/data/fpt-2026-03-10/fine-grained-pat-permissions.json @@ -144,6 +144,15 @@ "additional-permissions": false, "access": "read" }, + { + "category": "billing", + "slug": "create-a-budget-for-an-organization", + "subcategory": "budgets", + "verb": "post", + "requestPath": "/organizations/{org}/settings/billing/budgets", + "additional-permissions": false, + "access": "write" + }, { "category": "billing", "slug": "get-a-budget-by-id-for-an-organization", diff --git a/src/github-apps/data/fpt-2026-03-10/fine-grained-pat.json b/src/github-apps/data/fpt-2026-03-10/fine-grained-pat.json index 1c6ef461e3f9..8453987c963d 100644 --- a/src/github-apps/data/fpt-2026-03-10/fine-grained-pat.json +++ b/src/github-apps/data/fpt-2026-03-10/fine-grained-pat.json @@ -1386,6 +1386,12 @@ "verb": "get", "requestPath": "/organizations/{org}/settings/billing/budgets" }, + { + "slug": "create-a-budget-for-an-organization", + "subcategory": "budgets", + "verb": "post", + "requestPath": "/organizations/{org}/settings/billing/budgets" + }, { "slug": "get-a-budget-by-id-for-an-organization", "subcategory": "budgets", diff --git a/src/github-apps/data/fpt-2026-03-10/server-to-server-permissions.json b/src/github-apps/data/fpt-2026-03-10/server-to-server-permissions.json index 40e88d6c4229..7a3c2308a982 100644 --- a/src/github-apps/data/fpt-2026-03-10/server-to-server-permissions.json +++ b/src/github-apps/data/fpt-2026-03-10/server-to-server-permissions.json @@ -428,6 +428,17 @@ "server-to-server": true, "additional-permissions": false }, + { + "category": "billing", + "slug": "create-a-budget-for-an-organization", + "subcategory": "budgets", + "verb": "post", + "requestPath": "/organizations/{org}/settings/billing/budgets", + "access": "write", + "user-to-server": true, + "server-to-server": true, + "additional-permissions": false + }, { "category": "billing", "slug": "get-a-budget-by-id-for-an-organization", diff --git a/src/github-apps/data/fpt-2026-03-10/server-to-server-rest.json b/src/github-apps/data/fpt-2026-03-10/server-to-server-rest.json index 91a58f9015bd..a8c2d8bfdffc 100644 --- a/src/github-apps/data/fpt-2026-03-10/server-to-server-rest.json +++ b/src/github-apps/data/fpt-2026-03-10/server-to-server-rest.json @@ -1466,6 +1466,12 @@ "verb": "get", "requestPath": "/organizations/{org}/settings/billing/budgets" }, + { + "slug": "create-a-budget-for-an-organization", + "subcategory": "budgets", + "verb": "post", + "requestPath": "/organizations/{org}/settings/billing/budgets" + }, { "slug": "get-a-budget-by-id-for-an-organization", "subcategory": "budgets", diff --git a/src/github-apps/data/fpt-2026-03-10/user-to-server-rest.json b/src/github-apps/data/fpt-2026-03-10/user-to-server-rest.json index f973e92f8a1a..6436d3495075 100644 --- a/src/github-apps/data/fpt-2026-03-10/user-to-server-rest.json +++ b/src/github-apps/data/fpt-2026-03-10/user-to-server-rest.json @@ -1508,6 +1508,12 @@ "verb": "get", "requestPath": "/organizations/{org}/settings/billing/budgets" }, + { + "slug": "create-a-budget-for-an-organization", + "subcategory": "budgets", + "verb": "post", + "requestPath": "/organizations/{org}/settings/billing/budgets" + }, { "slug": "get-a-budget-by-id-for-an-organization", "subcategory": "budgets", diff --git a/src/github-apps/lib/config.json b/src/github-apps/lib/config.json index f536dceb583a..54e51d55f9b8 100644 --- a/src/github-apps/lib/config.json +++ b/src/github-apps/lib/config.json @@ -60,5 +60,5 @@ "2022-11-28" ] }, - "sha": "c4a52d9b0b4f5db4e7de178c9f8f90b5f6360563" + "sha": "0d4e436c347b444cd71b4eb1bd73948fd51c3402" } \ No newline at end of file diff --git a/src/rest/data/fpt-2022-11-28/billing.json b/src/rest/data/fpt-2022-11-28/billing.json index 7180cbcef1a4..39c07f56e1f6 100644 --- a/src/rest/data/fpt-2022-11-28/billing.json +++ b/src/rest/data/fpt-2022-11-28/billing.json @@ -45,9 +45,19 @@ "enterprise", "organization", "repository", - "cost_center" + "cost_center", + "multi_user_customer", + "user" ] } + }, + { + "name": "user", + "description": "

Filter consumed amount details for budgets by the specified user login.

", + "in": "query", + "schema": { + "type": "string" + } } ], "bodyParameters": [], @@ -55,7 +65,7 @@ "codeExamples": [ { "request": { - "description": "Example", + "description": "Example 1: Status Code 200", "acceptHeader": "application/vnd.github.v3+json", "parameters": { "org": "ORG" @@ -157,12 +167,217 @@ }, "budget_scope": { "type": "string", - "description": "The scope of the budget (enterprise, organization, repository, cost center)" + "description": "The scope of the budget", + "enum": [ + "enterprise", + "organization", + "repository", + "cost_center", + "multi_user_customer", + "user" + ] + }, + "budget_entity_name": { + "type": "string", + "description": "The name of the entity for the budget (enterprise does not require a name)." + }, + "user": { + "type": "string", + "description": "The user login when the budget is scoped to a single user (`user` scope)." + }, + "budget_product_sku": { + "type": "string", + "description": "A single product or sku to apply the budget to." + }, + "budget_alerting": { + "type": "object", + "properties": { + "will_alert": { + "type": "boolean", + "description": "Whether alerts are enabled for this budget" + }, + "alert_recipients": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of user login names who will receive alerts" + } + }, + "required": [ + "will_alert", + "alert_recipients" + ] + } + }, + "required": [ + "id", + "budget_type", + "budget_product_sku", + "budget_scope", + "budget_amount", + "prevent_further_usage", + "budget_alerting" + ] + }, + "description": "Array of budget objects for the enterprise" + }, + "user": { + "type": "string", + "description": "User login included when the response is scoped with the `user` query parameter." + }, + "effective_budget": { + "type": "object", + "description": "Effective user-level budget details returned when the response is scoped with the `user` query parameter.", + "properties": { + "id": { + "type": "string", + "description": "The unique identifier of the effective budget." + }, + "budget_amount": { + "type": "integer", + "description": "The budget amount for the effective budget." + }, + "consumed_amount": { + "type": "number", + "description": "The consumed amount for the specified user within the effective budget." + } + }, + "required": [ + "id", + "budget_amount", + "consumed_amount" + ] + }, + "has_next_page": { + "type": "boolean", + "description": "Indicates if there are more pages of results available (maps to hasNextPage from billing platform)" + }, + "total_count": { + "type": "integer", + "description": "Total number of budgets matching the query" + } + }, + "required": [ + "budgets" + ] + } + } + }, + { + "request": { + "description": "Example 2: Status Code 200", + "acceptHeader": "application/vnd.github.v3+json", + "parameters": { + "org": "ORG" + } + }, + "response": { + "statusCode": "200", + "contentType": "application/json", + "description": "

Response when getting all budgets

", + "example": { + "budgets": [ + { + "id": "2066deda-923f-43f9-88d2-62395a28c0cdd", + "budget_type": "ProductPricing", + "budget_product_skus": [ + "actions" + ], + "budget_scope": "multi_user_customer", + "budget_amount": 1000, + "prevent_further_usage": true, + "budget_alerting": { + "will_alert": true, + "alert_recipients": [ + "enterprise-admin", + "billing-manager" + ] + } + }, + { + "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "budget_type": "BundlePricing", + "budget_product_skus": [ + "ai_credits" + ], + "budget_scope": "user", + "budget_amount": 500, + "prevent_further_usage": true, + "budget_alerting": { + "will_alert": true, + "alert_recipients": [ + "org-owner" + ] + } + } + ], + "user": "octocat", + "effective_budget": { + "id": "9a7d04e8-6600-44f5-94ef-65ca92b95f0b", + "budget_amount": 1000, + "consumed_amount": 42 + }, + "has_next_page": false, + "total_count": 2 + }, + "schema": { + "type": "object", + "properties": { + "budgets": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The unique identifier for the budget" + }, + "budget_type": { + "description": "The type of pricing for the budget", + "oneOf": [ + { + "type": "string", + "enum": [ + "SkuPricing" + ] + }, + { + "type": "string", + "enum": [ + "ProductPricing" + ] + } + ] + }, + "budget_amount": { + "type": "integer", + "description": "The budget amount limit in whole dollars. For license-based products, this represents the number of licenses." + }, + "prevent_further_usage": { + "type": "boolean", + "description": "The type of limit enforcement for the budget" + }, + "budget_scope": { + "type": "string", + "description": "The scope of the budget", + "enum": [ + "enterprise", + "organization", + "repository", + "cost_center", + "multi_user_customer", + "user" + ] }, "budget_entity_name": { "type": "string", "description": "The name of the entity for the budget (enterprise does not require a name)." }, + "user": { + "type": "string", + "description": "The user login when the budget is scoped to a single user (`user` scope)." + }, "budget_product_sku": { "type": "string", "description": "A single product or sku to apply the budget to." @@ -200,6 +415,33 @@ }, "description": "Array of budget objects for the enterprise" }, + "user": { + "type": "string", + "description": "User login included when the response is scoped with the `user` query parameter." + }, + "effective_budget": { + "type": "object", + "description": "Effective user-level budget details returned when the response is scoped with the `user` query parameter.", + "properties": { + "id": { + "type": "string", + "description": "The unique identifier of the effective budget." + }, + "budget_amount": { + "type": "integer", + "description": "The budget amount for the effective budget." + }, + "consumed_amount": { + "type": "number", + "description": "The consumed amount for the specified user within the effective budget." + } + }, + "required": [ + "id", + "budget_amount", + "consumed_amount" + ] + }, "has_next_page": { "type": "boolean", "description": "Indicates if there are more pages of results available (maps to hasNextPage from billing platform)" @@ -246,6 +488,373 @@ ] } }, + { + "serverUrl": "https://api.github.com", + "verb": "post", + "requestPath": "/organizations/{org}/settings/billing/budgets", + "title": "Create a budget for an organization", + "category": "billing", + "subcategory": "budgets", + "parameters": [ + { + "name": "org", + "description": "

The organization name. The name is not case sensitive.

", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "bodyParameters": [ + { + "type": "integer", + "name": "budget_amount", + "description": "

The budget amount in whole dollars. For license-based products, this represents the number of licenses.

" + }, + { + "type": "boolean", + "name": "prevent_further_usage", + "description": "

Whether to prevent additional spending once the budget is exceeded. For user and multi_user_customer scopes, this must be true.

" + }, + { + "type": "object", + "name": "budget_alerting", + "description": "", + "childParamsGroups": [ + { + "type": "boolean", + "name": "will_alert", + "description": "

Whether alerts are enabled for this budget

" + }, + { + "type": "array of strings", + "name": "alert_recipients", + "description": "

Array of user login names who will receive alerts

" + } + ] + }, + { + "type": "string", + "name": "budget_scope", + "description": "

The scope of the budget for this organization. Use 'organization' for org-level budgets or 'repository' for repo-specific budgets within the organization. user and multi_user_customer scopes are only supported when budget_product_sku is ai_credits or premium_requests.

", + "enum": [ + "organization", + "repository", + "multi_user_customer", + "user" + ] + }, + { + "type": "string", + "name": "budget_entity_name", + "description": "

The name of the entity to apply the budget to

", + "default": "" + }, + { + "type": "string", + "name": "budget_type", + "description": "

The type of pricing for the budget

" + }, + { + "type": "string", + "name": "budget_product_sku", + "description": "

A single product or SKU that will be covered in the budget

" + } + ], + "descriptionHTML": "

Note

\n

\nThis endpoint is in public preview and is subject to change.

\n
\n

Creates a new budget for an organization. The authenticated user must be an\norganization admin or billing manager.

", + "codeExamples": [ + { + "request": { + "contentType": "application/json", + "description": "Create organization budget example", + "acceptHeader": "application/vnd.github.v3+json", + "bodyParameters": { + "budget_amount": 500, + "prevent_further_usage": true, + "budget_scope": "organization", + "budget_entity_name": "", + "budget_type": "ProductPricing", + "budget_product_sku": "actions", + "budget_alerting": { + "will_alert": false, + "alert_recipients": [] + } + }, + "parameters": { + "org": "ORG" + } + }, + "response": { + "statusCode": "200", + "contentType": "application/json", + "description": "

Budget created successfully

", + "example": { + "message": "Budget successfully created.", + "budget": { + "description": "Response when updating a budget", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID of the budget." + }, + "budget_scope": { + "type": "string", + "description": "The type of scope for the budget", + "enum": [ + "enterprise", + "organization", + "repository", + "cost_center", + "multi_user_customer", + "user" + ], + "examples": [ + "enterprise" + ] + }, + "budget_entity_name": { + "type": "string", + "description": "The name of the entity to apply the budget to", + "examples": [ + "octocat/hello-world" + ] + }, + "user": { + "type": "string", + "description": "The user login when the budget is scoped to a single user (`user` scope).", + "examples": [ + "octocat" + ] + }, + "budget_amount": { + "type": "integer", + "description": "The budget amount in whole dollars. For license-based products, this represents the number of licenses." + }, + "prevent_further_usage": { + "type": "boolean", + "description": "Whether to prevent additional spending once the budget is exceeded", + "examples": [ + true + ] + }, + "budget_product_sku": { + "type": "string", + "description": "A single product or sku to apply the budget to.", + "examples": [ + "actions_linux" + ] + }, + "budget_type": { + "description": "The type of pricing for the budget", + "oneOf": [ + { + "type": "string", + "enum": [ + "ProductPricing" + ] + }, + { + "type": "string", + "enum": [ + "SkuPricing" + ] + } + ], + "examples": [ + "ProductPricing" + ] + }, + "budget_alerting": { + "type": "object", + "properties": { + "will_alert": { + "type": "boolean", + "description": "Whether alerts are enabled for this budget", + "examples": [ + true + ] + }, + "alert_recipients": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of user login names who will receive alerts", + "examples": [ + "mona", + "lisa" + ] + } + } + } + }, + "required": [ + "id", + "budget_amount", + "prevent_further_usage", + "budget_product_sku", + "budget_type", + "budget_alerting", + "budget_scope", + "budget_entity_name" + ] + }, + "examples": { + "default": { + "value": { + "id": "2066deda-923f-43f9-88d2-62395a28c0cdd", + "budget_type": "ProductPricing", + "budget_product_sku": "actions_linux", + "budget_scope": "repository", + "budget_entity_name": "example-repo-name", + "budget_amount": 0, + "prevent_further_usage": true, + "budget_alerting": { + "will_alert": true, + "alert_recipients": [ + "mona", + "lisa" + ] + } + } + } + } + } + } + } + }, + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "budget": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID of the budget." + }, + "budget_amount": { + "type": "number", + "format": "float", + "description": "The budget amount in whole dollars. For license-based products, this represents the number of licenses." + }, + "prevent_further_usage": { + "type": "boolean", + "description": "Whether to prevent additional spending once the budget is exceeded. For `user` and `multi_user_customer` scopes, this must be `true`." + }, + "budget_alerting": { + "type": "object", + "required": [ + "will_alert", + "alert_recipients" + ], + "properties": { + "will_alert": { + "type": "boolean", + "description": "Whether alerts are enabled for this budget" + }, + "alert_recipients": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of user login names who will receive alerts" + } + } + }, + "budget_scope": { + "type": "string", + "description": "The scope of the budget", + "enum": [ + "organization", + "repository", + "multi_user_customer", + "user" + ] + }, + "budget_entity_name": { + "type": "string", + "description": "The name of the entity to apply the budget to", + "default": "" + }, + "budget_type": { + "description": "The type of pricing for the budget", + "oneOf": [ + { + "type": "string", + "enum": [ + "ProductPricing" + ] + }, + { + "type": "string", + "enum": [ + "SkuPricing" + ] + } + ] + }, + "budget_product_sku": { + "type": "string", + "description": "A single product or SKU that will be covered in the budget" + } + } + } + } + } + } + } + ], + "statusCodes": [ + { + "httpStatusCode": "200", + "description": "

Budget created successfully

" + }, + { + "httpStatusCode": "400", + "description": "

Bad Request

" + }, + { + "httpStatusCode": "401", + "description": "

Requires authentication

" + }, + { + "httpStatusCode": "403", + "description": "

Insufficient permissions

" + }, + { + "httpStatusCode": "404", + "description": "

Feature not enabled or organization not found

" + }, + { + "httpStatusCode": "422", + "description": "

Validation failed, or the endpoint has been spammed.

" + }, + { + "httpStatusCode": "500", + "description": "

Internal server error

" + } + ], + "previews": [], + "progAccess": { + "userToServerRest": true, + "serverToServer": true, + "fineGrainedPat": true, + "permissions": [ + { + "\"Administration\" organization permissions": "write" + } + ] + } + }, { "serverUrl": "https://api.github.com", "verb": "get", @@ -319,13 +928,19 @@ "enterprise", "organization", "repository", - "cost_center" + "cost_center", + "multi_user_customer", + "user" ] }, "budget_entity_name": { "type": "string", "description": "The name of the entity to apply the budget to" }, + "user": { + "type": "string", + "description": "The user login when the budget is scoped to a single user (`user` scope)." + }, "budget_amount": { "type": "integer", "description": "The budget amount in whole dollars. For license-based products, this represents the number of licenses." @@ -460,7 +1075,7 @@ { "type": "boolean", "name": "prevent_further_usage", - "description": "

Whether to prevent additional spending once the budget is exceeded

" + "description": "

Whether to prevent additional spending once the budget is exceeded. For budgets with user or multi_user_customer scope, this must remain true.

" }, { "type": "object", @@ -487,7 +1102,9 @@ "enterprise", "organization", "repository", - "cost_center" + "cost_center", + "multi_user_customer", + "user" ] }, { @@ -598,7 +1215,9 @@ "enterprise", "organization", "repository", - "cost_center" + "cost_center", + "multi_user_customer", + "user" ] }, "budget_entity_name": { diff --git a/src/rest/data/fpt-2026-03-10/billing.json b/src/rest/data/fpt-2026-03-10/billing.json index 7180cbcef1a4..39c07f56e1f6 100644 --- a/src/rest/data/fpt-2026-03-10/billing.json +++ b/src/rest/data/fpt-2026-03-10/billing.json @@ -45,9 +45,19 @@ "enterprise", "organization", "repository", - "cost_center" + "cost_center", + "multi_user_customer", + "user" ] } + }, + { + "name": "user", + "description": "

Filter consumed amount details for budgets by the specified user login.

", + "in": "query", + "schema": { + "type": "string" + } } ], "bodyParameters": [], @@ -55,7 +65,7 @@ "codeExamples": [ { "request": { - "description": "Example", + "description": "Example 1: Status Code 200", "acceptHeader": "application/vnd.github.v3+json", "parameters": { "org": "ORG" @@ -157,12 +167,217 @@ }, "budget_scope": { "type": "string", - "description": "The scope of the budget (enterprise, organization, repository, cost center)" + "description": "The scope of the budget", + "enum": [ + "enterprise", + "organization", + "repository", + "cost_center", + "multi_user_customer", + "user" + ] + }, + "budget_entity_name": { + "type": "string", + "description": "The name of the entity for the budget (enterprise does not require a name)." + }, + "user": { + "type": "string", + "description": "The user login when the budget is scoped to a single user (`user` scope)." + }, + "budget_product_sku": { + "type": "string", + "description": "A single product or sku to apply the budget to." + }, + "budget_alerting": { + "type": "object", + "properties": { + "will_alert": { + "type": "boolean", + "description": "Whether alerts are enabled for this budget" + }, + "alert_recipients": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of user login names who will receive alerts" + } + }, + "required": [ + "will_alert", + "alert_recipients" + ] + } + }, + "required": [ + "id", + "budget_type", + "budget_product_sku", + "budget_scope", + "budget_amount", + "prevent_further_usage", + "budget_alerting" + ] + }, + "description": "Array of budget objects for the enterprise" + }, + "user": { + "type": "string", + "description": "User login included when the response is scoped with the `user` query parameter." + }, + "effective_budget": { + "type": "object", + "description": "Effective user-level budget details returned when the response is scoped with the `user` query parameter.", + "properties": { + "id": { + "type": "string", + "description": "The unique identifier of the effective budget." + }, + "budget_amount": { + "type": "integer", + "description": "The budget amount for the effective budget." + }, + "consumed_amount": { + "type": "number", + "description": "The consumed amount for the specified user within the effective budget." + } + }, + "required": [ + "id", + "budget_amount", + "consumed_amount" + ] + }, + "has_next_page": { + "type": "boolean", + "description": "Indicates if there are more pages of results available (maps to hasNextPage from billing platform)" + }, + "total_count": { + "type": "integer", + "description": "Total number of budgets matching the query" + } + }, + "required": [ + "budgets" + ] + } + } + }, + { + "request": { + "description": "Example 2: Status Code 200", + "acceptHeader": "application/vnd.github.v3+json", + "parameters": { + "org": "ORG" + } + }, + "response": { + "statusCode": "200", + "contentType": "application/json", + "description": "

Response when getting all budgets

", + "example": { + "budgets": [ + { + "id": "2066deda-923f-43f9-88d2-62395a28c0cdd", + "budget_type": "ProductPricing", + "budget_product_skus": [ + "actions" + ], + "budget_scope": "multi_user_customer", + "budget_amount": 1000, + "prevent_further_usage": true, + "budget_alerting": { + "will_alert": true, + "alert_recipients": [ + "enterprise-admin", + "billing-manager" + ] + } + }, + { + "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "budget_type": "BundlePricing", + "budget_product_skus": [ + "ai_credits" + ], + "budget_scope": "user", + "budget_amount": 500, + "prevent_further_usage": true, + "budget_alerting": { + "will_alert": true, + "alert_recipients": [ + "org-owner" + ] + } + } + ], + "user": "octocat", + "effective_budget": { + "id": "9a7d04e8-6600-44f5-94ef-65ca92b95f0b", + "budget_amount": 1000, + "consumed_amount": 42 + }, + "has_next_page": false, + "total_count": 2 + }, + "schema": { + "type": "object", + "properties": { + "budgets": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The unique identifier for the budget" + }, + "budget_type": { + "description": "The type of pricing for the budget", + "oneOf": [ + { + "type": "string", + "enum": [ + "SkuPricing" + ] + }, + { + "type": "string", + "enum": [ + "ProductPricing" + ] + } + ] + }, + "budget_amount": { + "type": "integer", + "description": "The budget amount limit in whole dollars. For license-based products, this represents the number of licenses." + }, + "prevent_further_usage": { + "type": "boolean", + "description": "The type of limit enforcement for the budget" + }, + "budget_scope": { + "type": "string", + "description": "The scope of the budget", + "enum": [ + "enterprise", + "organization", + "repository", + "cost_center", + "multi_user_customer", + "user" + ] }, "budget_entity_name": { "type": "string", "description": "The name of the entity for the budget (enterprise does not require a name)." }, + "user": { + "type": "string", + "description": "The user login when the budget is scoped to a single user (`user` scope)." + }, "budget_product_sku": { "type": "string", "description": "A single product or sku to apply the budget to." @@ -200,6 +415,33 @@ }, "description": "Array of budget objects for the enterprise" }, + "user": { + "type": "string", + "description": "User login included when the response is scoped with the `user` query parameter." + }, + "effective_budget": { + "type": "object", + "description": "Effective user-level budget details returned when the response is scoped with the `user` query parameter.", + "properties": { + "id": { + "type": "string", + "description": "The unique identifier of the effective budget." + }, + "budget_amount": { + "type": "integer", + "description": "The budget amount for the effective budget." + }, + "consumed_amount": { + "type": "number", + "description": "The consumed amount for the specified user within the effective budget." + } + }, + "required": [ + "id", + "budget_amount", + "consumed_amount" + ] + }, "has_next_page": { "type": "boolean", "description": "Indicates if there are more pages of results available (maps to hasNextPage from billing platform)" @@ -246,6 +488,373 @@ ] } }, + { + "serverUrl": "https://api.github.com", + "verb": "post", + "requestPath": "/organizations/{org}/settings/billing/budgets", + "title": "Create a budget for an organization", + "category": "billing", + "subcategory": "budgets", + "parameters": [ + { + "name": "org", + "description": "

The organization name. The name is not case sensitive.

", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "bodyParameters": [ + { + "type": "integer", + "name": "budget_amount", + "description": "

The budget amount in whole dollars. For license-based products, this represents the number of licenses.

" + }, + { + "type": "boolean", + "name": "prevent_further_usage", + "description": "

Whether to prevent additional spending once the budget is exceeded. For user and multi_user_customer scopes, this must be true.

" + }, + { + "type": "object", + "name": "budget_alerting", + "description": "", + "childParamsGroups": [ + { + "type": "boolean", + "name": "will_alert", + "description": "

Whether alerts are enabled for this budget

" + }, + { + "type": "array of strings", + "name": "alert_recipients", + "description": "

Array of user login names who will receive alerts

" + } + ] + }, + { + "type": "string", + "name": "budget_scope", + "description": "

The scope of the budget for this organization. Use 'organization' for org-level budgets or 'repository' for repo-specific budgets within the organization. user and multi_user_customer scopes are only supported when budget_product_sku is ai_credits or premium_requests.

", + "enum": [ + "organization", + "repository", + "multi_user_customer", + "user" + ] + }, + { + "type": "string", + "name": "budget_entity_name", + "description": "

The name of the entity to apply the budget to

", + "default": "" + }, + { + "type": "string", + "name": "budget_type", + "description": "

The type of pricing for the budget

" + }, + { + "type": "string", + "name": "budget_product_sku", + "description": "

A single product or SKU that will be covered in the budget

" + } + ], + "descriptionHTML": "

Note

\n

\nThis endpoint is in public preview and is subject to change.

\n
\n

Creates a new budget for an organization. The authenticated user must be an\norganization admin or billing manager.

", + "codeExamples": [ + { + "request": { + "contentType": "application/json", + "description": "Create organization budget example", + "acceptHeader": "application/vnd.github.v3+json", + "bodyParameters": { + "budget_amount": 500, + "prevent_further_usage": true, + "budget_scope": "organization", + "budget_entity_name": "", + "budget_type": "ProductPricing", + "budget_product_sku": "actions", + "budget_alerting": { + "will_alert": false, + "alert_recipients": [] + } + }, + "parameters": { + "org": "ORG" + } + }, + "response": { + "statusCode": "200", + "contentType": "application/json", + "description": "

Budget created successfully

", + "example": { + "message": "Budget successfully created.", + "budget": { + "description": "Response when updating a budget", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID of the budget." + }, + "budget_scope": { + "type": "string", + "description": "The type of scope for the budget", + "enum": [ + "enterprise", + "organization", + "repository", + "cost_center", + "multi_user_customer", + "user" + ], + "examples": [ + "enterprise" + ] + }, + "budget_entity_name": { + "type": "string", + "description": "The name of the entity to apply the budget to", + "examples": [ + "octocat/hello-world" + ] + }, + "user": { + "type": "string", + "description": "The user login when the budget is scoped to a single user (`user` scope).", + "examples": [ + "octocat" + ] + }, + "budget_amount": { + "type": "integer", + "description": "The budget amount in whole dollars. For license-based products, this represents the number of licenses." + }, + "prevent_further_usage": { + "type": "boolean", + "description": "Whether to prevent additional spending once the budget is exceeded", + "examples": [ + true + ] + }, + "budget_product_sku": { + "type": "string", + "description": "A single product or sku to apply the budget to.", + "examples": [ + "actions_linux" + ] + }, + "budget_type": { + "description": "The type of pricing for the budget", + "oneOf": [ + { + "type": "string", + "enum": [ + "ProductPricing" + ] + }, + { + "type": "string", + "enum": [ + "SkuPricing" + ] + } + ], + "examples": [ + "ProductPricing" + ] + }, + "budget_alerting": { + "type": "object", + "properties": { + "will_alert": { + "type": "boolean", + "description": "Whether alerts are enabled for this budget", + "examples": [ + true + ] + }, + "alert_recipients": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of user login names who will receive alerts", + "examples": [ + "mona", + "lisa" + ] + } + } + } + }, + "required": [ + "id", + "budget_amount", + "prevent_further_usage", + "budget_product_sku", + "budget_type", + "budget_alerting", + "budget_scope", + "budget_entity_name" + ] + }, + "examples": { + "default": { + "value": { + "id": "2066deda-923f-43f9-88d2-62395a28c0cdd", + "budget_type": "ProductPricing", + "budget_product_sku": "actions_linux", + "budget_scope": "repository", + "budget_entity_name": "example-repo-name", + "budget_amount": 0, + "prevent_further_usage": true, + "budget_alerting": { + "will_alert": true, + "alert_recipients": [ + "mona", + "lisa" + ] + } + } + } + } + } + } + } + }, + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "budget": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID of the budget." + }, + "budget_amount": { + "type": "number", + "format": "float", + "description": "The budget amount in whole dollars. For license-based products, this represents the number of licenses." + }, + "prevent_further_usage": { + "type": "boolean", + "description": "Whether to prevent additional spending once the budget is exceeded. For `user` and `multi_user_customer` scopes, this must be `true`." + }, + "budget_alerting": { + "type": "object", + "required": [ + "will_alert", + "alert_recipients" + ], + "properties": { + "will_alert": { + "type": "boolean", + "description": "Whether alerts are enabled for this budget" + }, + "alert_recipients": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of user login names who will receive alerts" + } + } + }, + "budget_scope": { + "type": "string", + "description": "The scope of the budget", + "enum": [ + "organization", + "repository", + "multi_user_customer", + "user" + ] + }, + "budget_entity_name": { + "type": "string", + "description": "The name of the entity to apply the budget to", + "default": "" + }, + "budget_type": { + "description": "The type of pricing for the budget", + "oneOf": [ + { + "type": "string", + "enum": [ + "ProductPricing" + ] + }, + { + "type": "string", + "enum": [ + "SkuPricing" + ] + } + ] + }, + "budget_product_sku": { + "type": "string", + "description": "A single product or SKU that will be covered in the budget" + } + } + } + } + } + } + } + ], + "statusCodes": [ + { + "httpStatusCode": "200", + "description": "

Budget created successfully

" + }, + { + "httpStatusCode": "400", + "description": "

Bad Request

" + }, + { + "httpStatusCode": "401", + "description": "

Requires authentication

" + }, + { + "httpStatusCode": "403", + "description": "

Insufficient permissions

" + }, + { + "httpStatusCode": "404", + "description": "

Feature not enabled or organization not found

" + }, + { + "httpStatusCode": "422", + "description": "

Validation failed, or the endpoint has been spammed.

" + }, + { + "httpStatusCode": "500", + "description": "

Internal server error

" + } + ], + "previews": [], + "progAccess": { + "userToServerRest": true, + "serverToServer": true, + "fineGrainedPat": true, + "permissions": [ + { + "\"Administration\" organization permissions": "write" + } + ] + } + }, { "serverUrl": "https://api.github.com", "verb": "get", @@ -319,13 +928,19 @@ "enterprise", "organization", "repository", - "cost_center" + "cost_center", + "multi_user_customer", + "user" ] }, "budget_entity_name": { "type": "string", "description": "The name of the entity to apply the budget to" }, + "user": { + "type": "string", + "description": "The user login when the budget is scoped to a single user (`user` scope)." + }, "budget_amount": { "type": "integer", "description": "The budget amount in whole dollars. For license-based products, this represents the number of licenses." @@ -460,7 +1075,7 @@ { "type": "boolean", "name": "prevent_further_usage", - "description": "

Whether to prevent additional spending once the budget is exceeded

" + "description": "

Whether to prevent additional spending once the budget is exceeded. For budgets with user or multi_user_customer scope, this must remain true.

" }, { "type": "object", @@ -487,7 +1102,9 @@ "enterprise", "organization", "repository", - "cost_center" + "cost_center", + "multi_user_customer", + "user" ] }, { @@ -598,7 +1215,9 @@ "enterprise", "organization", "repository", - "cost_center" + "cost_center", + "multi_user_customer", + "user" ] }, "budget_entity_name": { diff --git a/src/rest/data/ghec-2022-11-28/billing.json b/src/rest/data/ghec-2022-11-28/billing.json index 2bfab8f282cc..88bddaad0a02 100644 --- a/src/rest/data/ghec-2022-11-28/billing.json +++ b/src/rest/data/ghec-2022-11-28/billing.json @@ -230,9 +230,19 @@ "enterprise", "organization", "repository", - "cost_center" + "cost_center", + "multi_user_customer", + "user" ] } + }, + { + "name": "user", + "description": "

Filter consumed amount details for budgets by the specified user login.

", + "in": "query", + "schema": { + "type": "string" + } } ], "bodyParameters": [], @@ -240,7 +250,7 @@ "codeExamples": [ { "request": { - "description": "Example", + "description": "Example 1: Status Code 200", "acceptHeader": "application/vnd.github.v3+json", "parameters": { "enterprise": "ENTERPRISE" @@ -342,12 +352,24 @@ }, "budget_scope": { "type": "string", - "description": "The scope of the budget (enterprise, organization, repository, cost center)" + "description": "The scope of the budget", + "enum": [ + "enterprise", + "organization", + "repository", + "cost_center", + "multi_user_customer", + "user" + ] }, "budget_entity_name": { "type": "string", "description": "The name of the entity for the budget (enterprise does not require a name)." }, + "user": { + "type": "string", + "description": "The user login when the budget is scoped to a single user (`user` scope)." + }, "budget_product_sku": { "type": "string", "description": "A single product or sku to apply the budget to." @@ -385,6 +407,226 @@ }, "description": "Array of budget objects for the enterprise" }, + "user": { + "type": "string", + "description": "User login included when the response is scoped with the `user` query parameter." + }, + "effective_budget": { + "type": "object", + "description": "Effective user-level budget details returned when the response is scoped with the `user` query parameter.", + "properties": { + "id": { + "type": "string", + "description": "The unique identifier of the effective budget." + }, + "budget_amount": { + "type": "integer", + "description": "The budget amount for the effective budget." + }, + "consumed_amount": { + "type": "number", + "description": "The consumed amount for the specified user within the effective budget." + } + }, + "required": [ + "id", + "budget_amount", + "consumed_amount" + ] + }, + "has_next_page": { + "type": "boolean", + "description": "Indicates if there are more pages of results available (maps to hasNextPage from billing platform)" + }, + "total_count": { + "type": "integer", + "description": "Total number of budgets matching the query" + } + }, + "required": [ + "budgets" + ] + } + } + }, + { + "request": { + "description": "Example 2: Status Code 200", + "acceptHeader": "application/vnd.github.v3+json", + "parameters": { + "enterprise": "ENTERPRISE" + } + }, + "response": { + "statusCode": "200", + "contentType": "application/json", + "description": "

Response when getting all budgets

", + "example": { + "budgets": [ + { + "id": "2066deda-923f-43f9-88d2-62395a28c0cdd", + "budget_type": "ProductPricing", + "budget_product_skus": [ + "actions" + ], + "budget_scope": "multi_user_customer", + "budget_amount": 1000, + "prevent_further_usage": true, + "budget_alerting": { + "will_alert": true, + "alert_recipients": [ + "enterprise-admin", + "billing-manager" + ] + } + }, + { + "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "budget_type": "BundlePricing", + "budget_product_skus": [ + "ai_credits" + ], + "budget_scope": "user", + "budget_amount": 500, + "prevent_further_usage": true, + "budget_alerting": { + "will_alert": true, + "alert_recipients": [ + "org-owner" + ] + } + } + ], + "user": "octocat", + "effective_budget": { + "id": "9a7d04e8-6600-44f5-94ef-65ca92b95f0b", + "budget_amount": 1000, + "consumed_amount": 42 + }, + "has_next_page": false, + "total_count": 2 + }, + "schema": { + "type": "object", + "properties": { + "budgets": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The unique identifier for the budget" + }, + "budget_type": { + "description": "The type of pricing for the budget", + "oneOf": [ + { + "type": "string", + "enum": [ + "SkuPricing" + ] + }, + { + "type": "string", + "enum": [ + "ProductPricing" + ] + } + ] + }, + "budget_amount": { + "type": "integer", + "description": "The budget amount limit in whole dollars. For license-based products, this represents the number of licenses." + }, + "prevent_further_usage": { + "type": "boolean", + "description": "The type of limit enforcement for the budget" + }, + "budget_scope": { + "type": "string", + "description": "The scope of the budget", + "enum": [ + "enterprise", + "organization", + "repository", + "cost_center", + "multi_user_customer", + "user" + ] + }, + "budget_entity_name": { + "type": "string", + "description": "The name of the entity for the budget (enterprise does not require a name)." + }, + "user": { + "type": "string", + "description": "The user login when the budget is scoped to a single user (`user` scope)." + }, + "budget_product_sku": { + "type": "string", + "description": "A single product or sku to apply the budget to." + }, + "budget_alerting": { + "type": "object", + "properties": { + "will_alert": { + "type": "boolean", + "description": "Whether alerts are enabled for this budget" + }, + "alert_recipients": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of user login names who will receive alerts" + } + }, + "required": [ + "will_alert", + "alert_recipients" + ] + } + }, + "required": [ + "id", + "budget_type", + "budget_product_sku", + "budget_scope", + "budget_amount", + "prevent_further_usage", + "budget_alerting" + ] + }, + "description": "Array of budget objects for the enterprise" + }, + "user": { + "type": "string", + "description": "User login included when the response is scoped with the `user` query parameter." + }, + "effective_budget": { + "type": "object", + "description": "Effective user-level budget details returned when the response is scoped with the `user` query parameter.", + "properties": { + "id": { + "type": "string", + "description": "The unique identifier of the effective budget." + }, + "budget_amount": { + "type": "integer", + "description": "The budget amount for the effective budget." + }, + "consumed_amount": { + "type": "number", + "description": "The consumed amount for the specified user within the effective budget." + } + }, + "required": [ + "id", + "budget_amount", + "consumed_amount" + ] + }, "has_next_page": { "type": "boolean", "description": "Indicates if there are more pages of results available (maps to hasNextPage from billing platform)" @@ -451,7 +693,7 @@ { "type": "boolean", "name": "prevent_further_usage", - "description": "

Whether to prevent additional spending once the budget is exceeded

", + "description": "

Whether to prevent additional spending once the budget is exceeded. For user and multi_user_customer scopes, this must be true.

", "isRequired": true }, { @@ -477,13 +719,15 @@ { "type": "string", "name": "budget_scope", - "description": "

The scope of the budget

", + "description": "

The scope of the budget. user and multi_user_customer scopes are only supported when budget_product_sku is ai_credits or premium_requests.

", "isRequired": true, "enum": [ "enterprise", "organization", "repository", - "cost_center" + "cost_center", + "multi_user_customer", + "user" ] }, { @@ -555,7 +799,9 @@ "enterprise", "organization", "repository", - "cost_center" + "cost_center", + "multi_user_customer", + "user" ] }, "budget_entity_name": { @@ -731,13 +977,19 @@ "enterprise", "organization", "repository", - "cost_center" + "cost_center", + "multi_user_customer", + "user" ] }, "budget_entity_name": { "type": "string", "description": "The name of the entity to apply the budget to" }, + "user": { + "type": "string", + "description": "The user login when the budget is scoped to a single user (`user` scope)." + }, "budget_amount": { "type": "integer", "description": "The budget amount in whole dollars. For license-based products, this represents the number of licenses." @@ -868,7 +1120,7 @@ { "type": "boolean", "name": "prevent_further_usage", - "description": "

Whether to prevent additional spending once the budget is exceeded

" + "description": "

Whether to prevent additional spending once the budget is exceeded. For budgets with user or multi_user_customer scope, this must remain true.

" }, { "type": "object", @@ -895,7 +1147,9 @@ "enterprise", "organization", "repository", - "cost_center" + "cost_center", + "multi_user_customer", + "user" ] }, { diff --git a/src/rest/data/ghec-2026-03-10/billing.json b/src/rest/data/ghec-2026-03-10/billing.json index 2bfab8f282cc..88bddaad0a02 100644 --- a/src/rest/data/ghec-2026-03-10/billing.json +++ b/src/rest/data/ghec-2026-03-10/billing.json @@ -230,9 +230,19 @@ "enterprise", "organization", "repository", - "cost_center" + "cost_center", + "multi_user_customer", + "user" ] } + }, + { + "name": "user", + "description": "

Filter consumed amount details for budgets by the specified user login.

", + "in": "query", + "schema": { + "type": "string" + } } ], "bodyParameters": [], @@ -240,7 +250,7 @@ "codeExamples": [ { "request": { - "description": "Example", + "description": "Example 1: Status Code 200", "acceptHeader": "application/vnd.github.v3+json", "parameters": { "enterprise": "ENTERPRISE" @@ -342,12 +352,24 @@ }, "budget_scope": { "type": "string", - "description": "The scope of the budget (enterprise, organization, repository, cost center)" + "description": "The scope of the budget", + "enum": [ + "enterprise", + "organization", + "repository", + "cost_center", + "multi_user_customer", + "user" + ] }, "budget_entity_name": { "type": "string", "description": "The name of the entity for the budget (enterprise does not require a name)." }, + "user": { + "type": "string", + "description": "The user login when the budget is scoped to a single user (`user` scope)." + }, "budget_product_sku": { "type": "string", "description": "A single product or sku to apply the budget to." @@ -385,6 +407,226 @@ }, "description": "Array of budget objects for the enterprise" }, + "user": { + "type": "string", + "description": "User login included when the response is scoped with the `user` query parameter." + }, + "effective_budget": { + "type": "object", + "description": "Effective user-level budget details returned when the response is scoped with the `user` query parameter.", + "properties": { + "id": { + "type": "string", + "description": "The unique identifier of the effective budget." + }, + "budget_amount": { + "type": "integer", + "description": "The budget amount for the effective budget." + }, + "consumed_amount": { + "type": "number", + "description": "The consumed amount for the specified user within the effective budget." + } + }, + "required": [ + "id", + "budget_amount", + "consumed_amount" + ] + }, + "has_next_page": { + "type": "boolean", + "description": "Indicates if there are more pages of results available (maps to hasNextPage from billing platform)" + }, + "total_count": { + "type": "integer", + "description": "Total number of budgets matching the query" + } + }, + "required": [ + "budgets" + ] + } + } + }, + { + "request": { + "description": "Example 2: Status Code 200", + "acceptHeader": "application/vnd.github.v3+json", + "parameters": { + "enterprise": "ENTERPRISE" + } + }, + "response": { + "statusCode": "200", + "contentType": "application/json", + "description": "

Response when getting all budgets

", + "example": { + "budgets": [ + { + "id": "2066deda-923f-43f9-88d2-62395a28c0cdd", + "budget_type": "ProductPricing", + "budget_product_skus": [ + "actions" + ], + "budget_scope": "multi_user_customer", + "budget_amount": 1000, + "prevent_further_usage": true, + "budget_alerting": { + "will_alert": true, + "alert_recipients": [ + "enterprise-admin", + "billing-manager" + ] + } + }, + { + "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "budget_type": "BundlePricing", + "budget_product_skus": [ + "ai_credits" + ], + "budget_scope": "user", + "budget_amount": 500, + "prevent_further_usage": true, + "budget_alerting": { + "will_alert": true, + "alert_recipients": [ + "org-owner" + ] + } + } + ], + "user": "octocat", + "effective_budget": { + "id": "9a7d04e8-6600-44f5-94ef-65ca92b95f0b", + "budget_amount": 1000, + "consumed_amount": 42 + }, + "has_next_page": false, + "total_count": 2 + }, + "schema": { + "type": "object", + "properties": { + "budgets": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The unique identifier for the budget" + }, + "budget_type": { + "description": "The type of pricing for the budget", + "oneOf": [ + { + "type": "string", + "enum": [ + "SkuPricing" + ] + }, + { + "type": "string", + "enum": [ + "ProductPricing" + ] + } + ] + }, + "budget_amount": { + "type": "integer", + "description": "The budget amount limit in whole dollars. For license-based products, this represents the number of licenses." + }, + "prevent_further_usage": { + "type": "boolean", + "description": "The type of limit enforcement for the budget" + }, + "budget_scope": { + "type": "string", + "description": "The scope of the budget", + "enum": [ + "enterprise", + "organization", + "repository", + "cost_center", + "multi_user_customer", + "user" + ] + }, + "budget_entity_name": { + "type": "string", + "description": "The name of the entity for the budget (enterprise does not require a name)." + }, + "user": { + "type": "string", + "description": "The user login when the budget is scoped to a single user (`user` scope)." + }, + "budget_product_sku": { + "type": "string", + "description": "A single product or sku to apply the budget to." + }, + "budget_alerting": { + "type": "object", + "properties": { + "will_alert": { + "type": "boolean", + "description": "Whether alerts are enabled for this budget" + }, + "alert_recipients": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of user login names who will receive alerts" + } + }, + "required": [ + "will_alert", + "alert_recipients" + ] + } + }, + "required": [ + "id", + "budget_type", + "budget_product_sku", + "budget_scope", + "budget_amount", + "prevent_further_usage", + "budget_alerting" + ] + }, + "description": "Array of budget objects for the enterprise" + }, + "user": { + "type": "string", + "description": "User login included when the response is scoped with the `user` query parameter." + }, + "effective_budget": { + "type": "object", + "description": "Effective user-level budget details returned when the response is scoped with the `user` query parameter.", + "properties": { + "id": { + "type": "string", + "description": "The unique identifier of the effective budget." + }, + "budget_amount": { + "type": "integer", + "description": "The budget amount for the effective budget." + }, + "consumed_amount": { + "type": "number", + "description": "The consumed amount for the specified user within the effective budget." + } + }, + "required": [ + "id", + "budget_amount", + "consumed_amount" + ] + }, "has_next_page": { "type": "boolean", "description": "Indicates if there are more pages of results available (maps to hasNextPage from billing platform)" @@ -451,7 +693,7 @@ { "type": "boolean", "name": "prevent_further_usage", - "description": "

Whether to prevent additional spending once the budget is exceeded

", + "description": "

Whether to prevent additional spending once the budget is exceeded. For user and multi_user_customer scopes, this must be true.

", "isRequired": true }, { @@ -477,13 +719,15 @@ { "type": "string", "name": "budget_scope", - "description": "

The scope of the budget

", + "description": "

The scope of the budget. user and multi_user_customer scopes are only supported when budget_product_sku is ai_credits or premium_requests.

", "isRequired": true, "enum": [ "enterprise", "organization", "repository", - "cost_center" + "cost_center", + "multi_user_customer", + "user" ] }, { @@ -555,7 +799,9 @@ "enterprise", "organization", "repository", - "cost_center" + "cost_center", + "multi_user_customer", + "user" ] }, "budget_entity_name": { @@ -731,13 +977,19 @@ "enterprise", "organization", "repository", - "cost_center" + "cost_center", + "multi_user_customer", + "user" ] }, "budget_entity_name": { "type": "string", "description": "The name of the entity to apply the budget to" }, + "user": { + "type": "string", + "description": "The user login when the budget is scoped to a single user (`user` scope)." + }, "budget_amount": { "type": "integer", "description": "The budget amount in whole dollars. For license-based products, this represents the number of licenses." @@ -868,7 +1120,7 @@ { "type": "boolean", "name": "prevent_further_usage", - "description": "

Whether to prevent additional spending once the budget is exceeded

" + "description": "

Whether to prevent additional spending once the budget is exceeded. For budgets with user or multi_user_customer scope, this must remain true.

" }, { "type": "object", @@ -895,7 +1147,9 @@ "enterprise", "organization", "repository", - "cost_center" + "cost_center", + "multi_user_customer", + "user" ] }, { diff --git a/src/rest/lib/config.json b/src/rest/lib/config.json index e827eb1d2189..69e07510774e 100644 --- a/src/rest/lib/config.json +++ b/src/rest/lib/config.json @@ -50,5 +50,5 @@ ] } }, - "sha": "c4a52d9b0b4f5db4e7de178c9f8f90b5f6360563" + "sha": "0d4e436c347b444cd71b4eb1bd73948fd51c3402" } \ No newline at end of file diff --git a/src/webhooks/lib/config.json b/src/webhooks/lib/config.json index cfe335c71832..617b5b5ed6e8 100644 --- a/src/webhooks/lib/config.json +++ b/src/webhooks/lib/config.json @@ -1,3 +1,3 @@ { - "sha": "c4a52d9b0b4f5db4e7de178c9f8f90b5f6360563" + "sha": "0d4e436c347b444cd71b4eb1bd73948fd51c3402" } \ No newline at end of file From deb8c6fc6944044797e6bdf4fbf421b6bd6d7a11 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Wed, 3 Jun 2026 14:51:25 -0700 Subject: [PATCH 4/4] Stagger Fastly second purge 20s while keeping 10s language cadence (#61545) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../purge-fastly-edge-cache-per-language.ts | 102 +++++++++++++++--- 1 file changed, 90 insertions(+), 12 deletions(-) diff --git a/src/languages/scripts/purge-fastly-edge-cache-per-language.ts b/src/languages/scripts/purge-fastly-edge-cache-per-language.ts index df987328d05e..41e0fc43e3b7 100644 --- a/src/languages/scripts/purge-fastly-edge-cache-per-language.ts +++ b/src/languages/scripts/purge-fastly-edge-cache-per-language.ts @@ -13,17 +13,58 @@ import purgeEdgeCache from '@/workflows/purge-edge-cache' * ... * * ...and so on for all languages. - * But to avoid a stampeding herd after each purge, and to avoid unnecessary - * load on the backend, put a slight delay between each language. - * This gives the backend a chance to finish processing all the now stale - * URLs, for that language, before tackling the next. + * + * Each surrogate key is purged twice because of Fastly shielding: the first + * purge clears the edge nodes and the second clears the origin shield. See + * `purge-edge-cache.ts` for the details. + * + * Two delays shape the schedule: + * + * - To avoid a stampeding herd after each purge, and to avoid unnecessary load + * on the backend, there's a slight delay between each language's FIRST purge. + * This gives the backend a chance to finish processing all the now stale URLs, + * for that language, before tackling the next. + * - The SECOND purge of a key happens a while after its first purge, long enough + * for the now-stale content to be re-fetched and re-shielded before we clear + * the shield again. Fastly's docs recommend ~2s for surrogate-key purges, but + * in practice that's been too short and stale content survives the shielding + * race, so we use a larger margin. See the "Race conditions" subsection of + * https://www.fastly.com/documentation/guides/concepts/cache/purging#race-conditions + * (the 30s figure there is for purge-all, which we don't use). The value must + * stay a multiple of DELAY_BETWEEN_LANGUAGES to keep the slot alignment below. + * + * Rather than block on the second purge (which would serialize everything and + * make the whole run take `DELAY_BEFORE_SECOND_PURGE` per key), we schedule all + * purges against a wall-clock timeline up front. Because the second-purge delay + * is a multiple of the between-languages delay, a key's second purge lands on + * the same slot as a later key's first purge. For example, with a 10s cadence + * and a 20s second-purge delay: + * + * t=0 no-language (1st) + * t=10 en (1st) + * t=20 es (1st) + no-language (2nd) + * t=30 ja (1st) + en (2nd) + * t=40 pt (1st) + es (2nd) + * ... */ const DELAY_BETWEEN_LANGUAGES = 10 * 1000 +const DELAY_BEFORE_SECOND_PURGE = 20 * 1000 + +// The pipelining only lines up if the second-purge delay is a whole number of +// language slots; otherwise second purges would drift off the cadence. Enforce +// it so a future tweak to either constant can't silently break the schedule. +if (DELAY_BEFORE_SECOND_PURGE % DELAY_BETWEEN_LANGUAGES !== 0) { + throw new Error( + `DELAY_BEFORE_SECOND_PURGE (${DELAY_BEFORE_SECOND_PURGE}ms) must be a multiple of ` + + `DELAY_BETWEEN_LANGUAGES (${DELAY_BETWEEN_LANGUAGES}ms) to keep second purges ` + + `aligned with later first-purge slots`, + ) +} const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)) -// This covers things like `/api/webhooks` which isn't language specific. -await purgeEdgeCache(makeLanguageSurrogateKey()) +type PurgePhase = 'first' | 'second' +type PurgeOutcome = { key: string; phase: PurgePhase; error?: unknown } const languages = process.env.LANGUAGES ? languagesFromString(process.env.LANGUAGES) @@ -32,12 +73,49 @@ const languages = process.env.LANGUAGES // in production as soon as possible. languageKeys.sort((a) => (a === 'en' ? -1 : 1)) -for (const language of languages) { - console.log( - `Sleeping ${DELAY_BETWEEN_LANGUAGES / 1000} seconds before purging for '${language}'...`, - ) - await sleep(DELAY_BETWEEN_LANGUAGES) - await purgeEdgeCache(makeLanguageSurrogateKey(language)) +// This covers things like `/api/webhooks` which isn't language specific, hence +// the no-language key (an empty `makeLanguageSurrogateKey()`) leading the list. +const surrogateKeys = [ + makeLanguageSurrogateKey(), + ...languages.map((language) => makeLanguageSurrogateKey(language)), +] + +// Schedule every purge against a single wall-clock start time so the cadence +// doesn't drift with per-purge network latency, and so each second purge aligns +// with a later first purge as described above. +const startTime = Date.now() +const purges: Promise[] = [] + +// Each call returns a promise that resolves (never rejects) to an outcome: the +// internal try/catch means a failed purge can't become an unhandled rejection +// while we wait for the rest of the schedule, which can outlast an early second +// purge. Failures are surfaced after all purges settle, below. +async function runPurge(key: string, phase: PurgePhase, targetTime: number): Promise { + await sleep(Math.max(0, targetTime - Date.now())) + try { + // `purgeEdgeCache` logs its own "first Fastly purge" line; word this as the + // scheduled phase trigger so the two purges of a key are distinguishable. + console.log(`Triggering ${phase}-phase purge for '${key}'...`) + await purgeEdgeCache(key, { purgeTwice: false }) + return { key, phase } + } catch (error) { + return { key, phase, error } + } +} + +for (const [index, key] of surrogateKeys.entries()) { + const slotStart = startTime + index * DELAY_BETWEEN_LANGUAGES + purges.push(runPurge(key, 'first', slotStart)) + purges.push(runPurge(key, 'second', slotStart + DELAY_BEFORE_SECOND_PURGE)) +} + +const outcomes = await Promise.all(purges) +const failures = outcomes.filter((outcome) => outcome.error) +if (failures.length) { + for (const failure of failures) { + console.error(`Fastly ${failure.phase} purge failed for '${failure.key}':`, failure.error) + } + throw new Error(`${failures.length} Fastly purge(s) failed`) } function languagesFromString(str: string): string[] {