From 4ddce79b317208d5da48be80e4fbf80a03121e52 Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Mon, 22 Jun 2026 19:50:19 -0700 Subject: [PATCH 01/13] feat: add JSON schemas and Copilot prompts for filter/override configuration - Add auto-generated JSON Schema files (schemas/extractor-config.schema.json, schemas/override-config.schema.json) for IDE autocompletion in YAML configs - Add scripts/generate-schemas.mjs that derives schemas from TypeScript interfaces and runs on every build/lint/test - Add Copilot prompt files (apiops-configure-filter, apiops-configure-overrides) that conversationally guide users through creating filter and override files - Update apiops init to lay down the new prompt files alongside identity setup - Include {#[TOKEN_NAME]#} placeholder syntax guidance and Key Vault patterns in the overrides prompt (per issue #117) - Update docs for filtering-resources, environment-overrides, and init command Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/commands/init.md | 27 +- docs/guides/environment-overrides.md | 31 ++ docs/guides/filtering-resources.md | 33 ++ package.json | 6 +- schemas/extractor-config.schema.json | 221 +++++++++++ schemas/override-config.schema.json | 170 ++++++++ scripts/embed-markdown-templates.mjs | 8 + scripts/generate-schemas.mjs | 372 ++++++++++++++++++ src/services/init-service.ts | 56 +++ src/templates/configs/filter-config.ts | 3 +- src/templates/configs/override-config.ts | 3 +- .../copilot/configure-filter-prompt.md | 64 +++ .../copilot/configure-filter-prompt.ts | 19 + .../copilot/configure-overrides-prompt.md | 111 ++++++ .../copilot/configure-overrides-prompt.ts | 27 ++ tests/unit/services/init-service.test.ts | 58 +++ .../copilot/configure-filter-prompt.test.ts | 19 + .../configure-overrides-prompt.test.ts | 21 + 18 files changed, 1240 insertions(+), 9 deletions(-) create mode 100644 schemas/extractor-config.schema.json create mode 100644 schemas/override-config.schema.json create mode 100644 scripts/generate-schemas.mjs create mode 100644 src/templates/copilot/configure-filter-prompt.md create mode 100644 src/templates/copilot/configure-filter-prompt.ts create mode 100644 src/templates/copilot/configure-overrides-prompt.md create mode 100644 src/templates/copilot/configure-overrides-prompt.ts create mode 100644 tests/unit/templates/copilot/configure-filter-prompt.test.ts create mode 100644 tests/unit/templates/copilot/configure-overrides-prompt.test.ts diff --git a/docs/commands/init.md b/docs/commands/init.md index 3dd14eb1..2aaedb08 100644 --- a/docs/commands/init.md +++ b/docs/commands/init.md @@ -83,6 +83,8 @@ In interactive mode (the default when running in a terminal), `apiops init` prom | `configuration.extractor.yaml` | Sample filter configuration for extraction | | `configuration.{env}.yaml` | Override templates per environment (e.g., `configuration.dev.yaml`, `configuration.prod.yaml`) | | `IDENTITY-SETUP-GITHUB.md` | Step-by-step guide for configuring federated credentials | +| `.github/prompts/apiops-configure-filter.prompt.md` | Copilot prompt for configuring resource filters | +| `.github/prompts/apiops-configure-overrides.prompt.md` | Copilot prompt for configuring environment overrides | ### Azure DevOps (`--ci azure-devops`) @@ -93,12 +95,16 @@ In interactive mode (the default when running in a terminal), `apiops init` prom | `configuration.extractor.yaml` | Sample filter configuration for extraction | | `configuration.{env}.yaml` | Override templates per environment | | `IDENTITY-SETUP-AZDO.md` | Step-by-step guide for configuring service connections | +| `.github/prompts/apiops-configure-filter.prompt.md` | Copilot prompt for configuring resource filters | +| `.github/prompts/apiops-configure-overrides.prompt.md` | Copilot prompt for configuring environment overrides | ### Both platforms | File | Purpose | |------|---------| | `.github/prompts/apiops-setup-identity.prompt.md` | Copilot prompt for identity setup | +| `.github/prompts/apiops-configure-filter.prompt.md` | Copilot prompt for creating extraction filter files | +| `.github/prompts/apiops-configure-overrides.prompt.md` | Copilot prompt for creating environment override files | | `/` | Empty artifact directory (default: `./apim-artifacts`) | ## Package consumption modes @@ -116,11 +122,24 @@ If you pass `--cli-package `, the tarball is copied into a `.apiops/` dire ## Next steps after init -1. **Set up identity** — Follow the generated `IDENTITY-SETUP-*.md` guide to configure Azure credentials for your CI/CD platform. +1. **Set up identity** — Follow the generated `IDENTITY-SETUP-*.md` guide to configure Azure credentials for your CI/CD platform. Or use the `.github/prompts/apiops-setup-identity.prompt.md` Copilot prompt. 2. **Extract your first snapshot** — Run [`apiops extract`](./extract.md) to pull your current APIM configuration into the artifact directory. -3. **Commit and push** — Check the generated files into version control. -4. **Configure overrides** — Edit `configuration.{env}.yaml` files with environment-specific values. See the [environment overrides guide](../guides/environment-overrides.md). -5. **Run your pipeline** — Trigger the publish pipeline to deploy artifacts to your target APIM instance. +3. **Configure filters** — Edit `configuration.extractor.yaml` to control which resources are extracted. Use the `.github/prompts/apiops-configure-filter.prompt.md` Copilot prompt for guided setup. +4. **Commit and push** — Check the generated files into version control. +5. **Configure overrides** — Edit `configuration.{env}.yaml` files with environment-specific values. Use the `.github/prompts/apiops-configure-overrides.prompt.md` Copilot prompt for guided setup. See the [environment overrides guide](../guides/environment-overrides.md). +6. **Run your pipeline** — Trigger the publish pipeline to deploy artifacts to your target APIM instance. + +## JSON Schemas for IDE Autocomplete + +Generated `configuration.extractor.yaml` and `configuration.{env}.yaml` files include a YAML language server schema comment that provides IDE autocomplete when editing: + +```yaml +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/extractor-config.schema.json +``` + +The schemas are available at: +- **Filter config:** [`schemas/extractor-config.schema.json`](https://github.com/Azure/apiops-cli/blob/main/schemas/extractor-config.schema.json) +- **Override config:** [`schemas/override-config.schema.json`](https://github.com/Azure/apiops-cli/blob/main/schemas/override-config.schema.json) ## Related docs diff --git a/docs/guides/environment-overrides.md b/docs/guides/environment-overrides.md index 77617d16..a2a04719 100644 --- a/docs/guides/environment-overrides.md +++ b/docs/guides/environment-overrides.md @@ -23,6 +23,16 @@ apiops publish \ --overrides ./configuration.prod.yaml ``` +## IDE Autocomplete (JSON Schema) + +Add this comment as the first line of your override file to enable autocomplete in VS Code and other YAML-aware editors: + +```yaml +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/override-config.schema.json +``` + +The schema validates section names, entry structure, nested sub-resource overrides, and supports `{#[TOKEN_NAME]#}` placeholder values. It is published at [`schemas/override-config.schema.json`](https://github.com/Azure/apiops-cli/blob/main/schemas/override-config.schema.json). + ## Override file format (APIOps Toolkit-compatible) `apiops-cli` uses the [APIOps Toolkit](https://github.com/Azure/apiops) override layout: @@ -482,3 +492,24 @@ apiops publish --overrides configuration.prod.yaml --dry-run \ - [Authentication Guide](authentication.md) - [Scenarios and Workflows](scenarios-and-workflows.md) - [GitHub Actions Integration](../ci-cd/github-actions.md) + +--- + +## JSON Schema and Copilot Prompt + +### IDE Autocompletion with JSON Schema + +A JSON Schema is available for `configuration.{env}.yaml` override files. Add this comment at the top of your override file to enable autocompletion in VS Code (with the YAML extension): + +```yaml +# yaml-language-server: $schema=./schemas/override-config.schema.json +``` + +The schema provides: +- Property name autocompletion for all resource sections +- Validation of the override structure (name + properties format) +- Inline documentation including token substitution syntax + +### Copilot-Assisted Configuration + +If you ran `apiops init`, a Copilot prompt file was generated at `.github/prompts/apiops-configure-overrides.prompt.md`. Open it in VS Code and ask GitHub Copilot to help you configure environment overrides — it will guide you through setting up environment-specific values interactively. diff --git a/docs/guides/filtering-resources.md b/docs/guides/filtering-resources.md index 93e655a4..43f1a1cb 100644 --- a/docs/guides/filtering-resources.md +++ b/docs/guides/filtering-resources.md @@ -36,6 +36,18 @@ Only `petstore-api`, `orders-api`, and their transitive dependencies are extract --- +## IDE Autocomplete (JSON Schema) + +Add this comment as the first line of your filter file to enable autocomplete in VS Code and other YAML-aware editors: + +```yaml +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/extractor-config.schema.json +``` + +The schema validates field names, array structure, and sub-resource filters. It is published at [`schemas/extractor-config.schema.json`](https://github.com/Azure/apiops-cli/blob/main/schemas/extractor-config.schema.json). + +--- + ## Filter YAML Format The filter file is a YAML document where each key is a resource type and the value is an array of resource names: @@ -336,3 +348,24 @@ backends: - [Environment Overrides](environment-overrides.md) — per-environment configuration - [Configuration Reference](../reference/configuration.md) — config priority chain - [APIM Glossary](../reference/apim-glossary.md) — APIM resource terminology + +--- + +## JSON Schema and Copilot Prompt + +### IDE Autocompletion with JSON Schema + +A JSON Schema is available for `configuration.extractor.yaml` files. Add this comment at the top of your filter file to enable autocompletion in VS Code (with the YAML extension): + +```yaml +# yaml-language-server: $schema=./schemas/extractor-config.schema.json +``` + +The schema provides: +- Property name autocompletion for all resource types +- Validation of the filter structure +- Inline documentation for each field + +### Copilot-Assisted Configuration + +If you ran `apiops init`, a Copilot prompt file was generated at `.github/prompts/apiops-configure-filter.prompt.md`. Open it in VS Code and ask GitHub Copilot to help you configure your filter — it will walk you through selecting resources interactively. diff --git a/package.json b/package.json index b88ff20d..e7fda1d7 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,9 @@ ], "readme": "README.md", "scripts": { - "prelint": "node scripts/embed-markdown-templates.mjs", - "prebuild": "node scripts/embed-markdown-templates.mjs", - "pretest": "node scripts/embed-markdown-templates.mjs", + "prelint": "node scripts/embed-markdown-templates.mjs && node scripts/generate-schemas.mjs", + "prebuild": "node scripts/embed-markdown-templates.mjs && node scripts/generate-schemas.mjs", + "pretest": "node scripts/embed-markdown-templates.mjs && node scripts/generate-schemas.mjs", "build": "tsc", "test": "vitest run", "test:watch": "vitest", diff --git a/schemas/extractor-config.schema.json b/schemas/extractor-config.schema.json new file mode 100644 index 00000000..29fd34c3 --- /dev/null +++ b/schemas/extractor-config.schema.json @@ -0,0 +1,221 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$comment": "Copyright (c) Microsoft Corporation. Licensed under the MIT license.", + "$id": "https://github.com/Azure/apiops-cli/schemas/extractor-config.schema.json", + "title": "APIOps Filter Configuration", + "description": "Validates configuration.extractor.yaml files used by APIOps CLI to select which Azure API Management resources are extracted. All resource sections are optional.", + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema URI for editor and IDE validation." + }, + "apis": { + "type": "array", + "description": "APIs to extract. Each item can be either a plain API name or wildcard pattern, or an object with a single API name mapped to nested API sub-filters. Matching is case-insensitive and supports * and ? wildcards.", + "items": { + "$ref": "#/definitions/apiSelector" + } + }, + "backends": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Backends to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "products": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Products to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "namedValues": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Named values to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "loggers": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Loggers to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "diagnostics": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Diagnostics to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "tags": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Tags to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "policyFragments": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Policy fragments to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "gateways": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Gateways to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "versionSets": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Version sets to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "groups": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Groups to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "subscriptions": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Subscriptions to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "schemas": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Schemas to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "policies": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Service-level policies to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "policyRestrictions": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Policy restrictions to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "documentations": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Documentations to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "workspaces": { + "type": "array", + "description": "Workspaces to extract. Each item can be either a plain workspace name or wildcard pattern, or an object with a single workspace name mapped to nested workspace sub-filters. Matching is case-insensitive and supports * and ? wildcards.", + "items": { + "$ref": "#/definitions/workspaceSelector" + } + } + }, + "definitions": { + "resourcePattern": { + "type": "string", + "description": "A resource name or wildcard pattern. Matching is case-insensitive. Supported wildcards: * matches zero or more characters, and ? matches a single character." + }, + "resourcePatternArray": { + "type": "array", + "description": "A list of resource names or wildcard patterns. Matching is case-insensitive. Supported wildcards: * and ?.", + "items": { + "$ref": "#/definitions/resourcePattern" + } + }, + "apiSelector": { + "oneOf": [ + { + "$ref": "#/definitions/resourcePattern" + }, + { + "type": "object", + "description": "A single API name mapped to sub-resource filters for that API.", + "minProperties": 1, + "maxProperties": 1, + "patternProperties": { + "^.+$": { + "$ref": "#/definitions/apiSubFilter" + } + }, + "additionalProperties": false + } + ] + }, + "workspaceSelector": { + "oneOf": [ + { + "$ref": "#/definitions/resourcePattern" + }, + { + "type": "object", + "description": "A single workspace name mapped to sub-resource filters for that workspace.", + "minProperties": 1, + "maxProperties": 1, + "patternProperties": { + "^.+$": { + "$ref": "#/definitions/workspaceSubFilter" + } + }, + "additionalProperties": false + } + ] + }, + "apiSubFilter": { + "type": "object", + "description": "Sub-resource filters for a specific API. Omit a property to include all sub-resources of that type, or set it to an empty array to exclude all of that type. Matching is case-insensitive and supports * and ? wildcards.", + "additionalProperties": false, + "properties": { + "operations": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Operations to extract for this API. Matching is case-insensitive and supports * and ? wildcards." + }, + "diagnostics": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Diagnostics to extract for this API. Matching is case-insensitive and supports * and ? wildcards." + }, + "schemas": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Schemas to extract for this API. Matching is case-insensitive and supports * and ? wildcards." + }, + "releases": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Releases to extract for this API. Matching is case-insensitive and supports * and ? wildcards." + } + } + }, + "workspaceSubFilter": { + "type": "object", + "description": "Sub-resource filters for a specific workspace. Omit a property to include all resources of that type, or set it to an empty array to exclude all of that type. Matching is case-insensitive and supports * and ? wildcards.", + "additionalProperties": false, + "properties": { + "apis": { + "type": "array", + "description": "APIs within this workspace to extract. Each item can be either a plain API name or wildcard pattern, or an object with a single API name mapped to nested API sub-filters. Matching is case-insensitive and supports * and ? wildcards.", + "items": { + "$ref": "#/definitions/apiSelector" + } + }, + "backends": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Backends within this workspace to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "diagnostics": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Diagnostics within this workspace to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "groups": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Groups within this workspace to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "loggers": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Loggers within this workspace to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "namedValues": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Named values within this workspace to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "policyFragments": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Policy fragments within this workspace to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "products": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Products within this workspace to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "schemas": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Schemas within this workspace to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "subscriptions": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Subscriptions within this workspace to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "tags": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Tags within this workspace to extract. Matching is case-insensitive and supports * and ? wildcards." + }, + "versionSets": { + "$ref": "#/definitions/resourcePatternArray", + "description": "Version sets within this workspace to extract. Matching is case-insensitive and supports * and ? wildcards." + } + } + } + } +} diff --git a/schemas/override-config.schema.json b/schemas/override-config.schema.json new file mode 100644 index 00000000..d48cd644 --- /dev/null +++ b/schemas/override-config.schema.json @@ -0,0 +1,170 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$comment": "Copyright (c) Microsoft Corporation. Licensed under the MIT license.", + "$id": "https://github.com/Azure/apiops-cli/schemas/override-config.schema.json", + "title": "APIOps Override Configuration", + "description": "Validates configuration.{env}.yaml override files used by APIOps CLI to apply environment-specific property overrides during publish. All resource sections are optional.", + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema URI for editor and IDE validation." + }, + "namedValues": { + "$ref": "#/definitions/overrideSection", + "description": "Named value overrides. Use the properties object to deep-merge resource properties. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + }, + "backends": { + "$ref": "#/definitions/overrideSection", + "description": "Backend overrides. Use the properties object to deep-merge resource properties such as URLs or credentials. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + }, + "apis": { + "$ref": "#/definitions/apiOverrideSection", + "description": "API overrides. Each entry can override API properties and optionally define nested diagnostics, operations, policies, and releases. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + }, + "diagnostics": { + "$ref": "#/definitions/overrideSection", + "description": "Diagnostics overrides. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + }, + "loggers": { + "$ref": "#/definitions/overrideSection", + "description": "Loggers overrides. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + }, + "policies": { + "$ref": "#/definitions/overrideSection", + "description": "Service-level policies overrides. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + }, + "gateways": { + "$ref": "#/definitions/overrideSection", + "description": "Gateways overrides. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + }, + "versionSets": { + "$ref": "#/definitions/overrideSection", + "description": "Version sets overrides. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + }, + "groups": { + "$ref": "#/definitions/overrideSection", + "description": "Groups overrides. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + }, + "subscriptions": { + "$ref": "#/definitions/overrideSection", + "description": "Subscriptions overrides. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + }, + "products": { + "$ref": "#/definitions/overrideSection", + "description": "Products overrides. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + }, + "tags": { + "$ref": "#/definitions/overrideSection", + "description": "Tags overrides. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + }, + "policyFragments": { + "$ref": "#/definitions/overrideSection", + "description": "Policy fragments overrides. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + }, + "workspaces": { + "$ref": "#/definitions/overrideSection", + "description": "Workspaces overrides. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + } + }, + "definitions": { + "propertiesObject": { + "type": "object", + "description": "Properties to deep-merge into the target resource. Any property name is allowed. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution.", + "additionalProperties": true + }, + "overrideEntry": { + "type": "object", + "required": [ + "name", + "properties" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Resource name to match for this override entry." + }, + "properties": { + "$ref": "#/definitions/propertiesObject" + } + } + }, + "overrideSection": { + "type": "array", + "description": "A list of override entries for a resource type.", + "items": { + "$ref": "#/definitions/overrideEntry" + } + }, + "operationOverrideEntry": { + "type": "object", + "required": [ + "name", + "properties" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Operation name to match for this override entry." + }, + "properties": { + "$ref": "#/definitions/propertiesObject" + }, + "policies": { + "$ref": "#/definitions/overrideSection", + "description": "Policy overrides nested under this operation. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + } + } + }, + "operationOverrideSection": { + "type": "array", + "description": "A list of operation override entries.", + "items": { + "$ref": "#/definitions/operationOverrideEntry" + } + }, + "apiOverrideEntry": { + "type": "object", + "required": [ + "name", + "properties" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "API name to match for this override entry." + }, + "properties": { + "$ref": "#/definitions/propertiesObject" + }, + "diagnostics": { + "$ref": "#/definitions/overrideSection", + "description": "Diagnostic overrides nested under this API. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + }, + "operations": { + "$ref": "#/definitions/operationOverrideSection", + "description": "Operation overrides nested under this API. Each operation can define its own nested policies. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + }, + "policies": { + "$ref": "#/definitions/overrideSection", + "description": "Policy overrides nested directly under this API. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + }, + "releases": { + "$ref": "#/definitions/overrideSection", + "description": "Release overrides nested under this API. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." + } + } + }, + "apiOverrideSection": { + "type": "array", + "description": "A list of API override entries.", + "items": { + "$ref": "#/definitions/apiOverrideEntry" + } + } + } +} diff --git a/scripts/embed-markdown-templates.mjs b/scripts/embed-markdown-templates.mjs index 75502c89..a32fba29 100644 --- a/scripts/embed-markdown-templates.mjs +++ b/scripts/embed-markdown-templates.mjs @@ -13,6 +13,14 @@ const templates = [ exportName: 'copilotAzureDevOpsIdentitySetupPromptTemplate', path: 'src/templates/copilot/identity-setup-prompt-azure-devops.md', }, + { + exportName: 'copilotConfigureFilterPromptTemplate', + path: 'src/templates/copilot/configure-filter-prompt.md', + }, + { + exportName: 'copilotConfigureOverridesPromptTemplate', + path: 'src/templates/copilot/configure-overrides-prompt.md', + }, { exportName: 'azureDevOpsIdentitySetupCoreTemplate', path: 'src/templates/shared/identity-setup-azure-devops-core.md', diff --git a/scripts/generate-schemas.mjs b/scripts/generate-schemas.mjs new file mode 100644 index 00000000..9a7f3f23 --- /dev/null +++ b/scripts/generate-schemas.mjs @@ -0,0 +1,372 @@ +/** + * Generates JSON Schema files for the filter (extractor) and override configurations. + * Derives schemas from the canonical TypeScript interfaces in src/models/config.ts. + * + * Run: node scripts/generate-schemas.mjs + * Hooked into: prebuild, prelint, pretest (alongside embed-markdown-templates) + * + * The script reads config.ts to extract the field names from FilterConfig and + * OverrideConfig interfaces, then generates the full JSON Schema files into + * schemas/. This ensures the schemas stay in sync with the TypeScript types + * without requiring a heavy ts-json-schema-generator dependency. + */ + +import { readFile, writeFile, mkdir } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +const repoRoot = resolve(import.meta.dirname, '..'); +const configPath = resolve(repoRoot, 'src/models/config.ts'); +const schemasDir = resolve(repoRoot, 'schemas'); + +// Read the config.ts source to extract interface fields +const configSource = await readFile(configPath, 'utf8'); + +/** + * Extract field names from a TypeScript interface definition. + * Matches lines like: `fieldName?: Type;` + */ +function extractFields(interfaceName, source) { + const regex = new RegExp( + `interface\\s+${interfaceName}\\s*\\{([^}]+)\\}`, + 's' + ); + const match = source.match(regex); + if (!match) { + throw new Error(`Could not find interface ${interfaceName} in config.ts`); + } + const body = match[1]; + const fieldRegex = /^\s*(?:\/\*\*[^*]*\*\/\s*)?(\w+)\??\s*:/gm; + const fields = []; + let m; + while ((m = fieldRegex.exec(body)) !== null) { + fields.push(m[1]); + } + return fields; +} + +// --- Extractor (Filter) Config Schema --- + +const filterFields = extractFields('FilterConfig', configSource); +const apiSubFilterFields = extractFields('ApiSubFilter', configSource); +const workspaceSubFilterFields = extractFields('WorkspaceSubFilter', configSource); + +// Fields that use special handling (not simple resourcePatternArray) +const SPECIAL_FILTER_FIELDS = new Set([ + 'apis', 'workspaces', 'apiSubFilters', 'workspaceSubFilters', +]); + +// Human-friendly labels for resource types +const RESOURCE_LABELS = { + apis: 'APIs', + backends: 'Backends', + products: 'Products', + namedValues: 'Named values', + loggers: 'Loggers', + diagnostics: 'Diagnostics', + tags: 'Tags', + policyFragments: 'Policy fragments', + gateways: 'Gateways', + versionSets: 'Version sets', + groups: 'Groups', + subscriptions: 'Subscriptions', + schemas: 'Schemas', + policies: 'Service-level policies', + policyRestrictions: 'Policy restrictions', + documentations: 'Documentations', + workspaces: 'Workspaces', +}; + +function buildFilterProperties() { + const props = { + $schema: { + type: 'string', + description: 'Optional schema URI for editor and IDE validation.', + }, + }; + + for (const field of filterFields) { + // Skip internal-only fields that don't appear in YAML + if (field === 'apiSubFilters' || field === 'workspaceSubFilters') continue; + + const label = RESOURCE_LABELS[field] || field; + + if (field === 'apis') { + props.apis = { + type: 'array', + description: `${label} to extract. Each item can be either a plain API name or wildcard pattern, or an object with a single API name mapped to nested API sub-filters. Matching is case-insensitive and supports * and ? wildcards.`, + items: { $ref: '#/definitions/apiSelector' }, + }; + } else if (field === 'workspaces') { + props.workspaces = { + type: 'array', + description: `${label} to extract. Each item can be either a plain workspace name or wildcard pattern, or an object with a single workspace name mapped to nested workspace sub-filters. Matching is case-insensitive and supports * and ? wildcards.`, + items: { $ref: '#/definitions/workspaceSelector' }, + }; + } else { + props[field] = { + $ref: '#/definitions/resourcePatternArray', + description: `${label} to extract. Matching is case-insensitive and supports * and ? wildcards.`, + }; + } + } + + return props; +} + +function buildApiSubFilterProperties() { + const props = {}; + for (const field of apiSubFilterFields) { + const label = field.charAt(0).toUpperCase() + field.slice(1); + props[field] = { + $ref: '#/definitions/resourcePatternArray', + description: `${label} to extract for this API. Matching is case-insensitive and supports * and ? wildcards.`, + }; + } + return props; +} + +function buildWorkspaceSubFilterProperties() { + const props = {}; + for (const field of workspaceSubFilterFields) { + // Skip internal-only fields + if (field === 'apiSubFilters') continue; + + const label = RESOURCE_LABELS[field] || field; + + if (field === 'apis') { + props.apis = { + type: 'array', + description: `${label} within this workspace to extract. Each item can be either a plain API name or wildcard pattern, or an object with a single API name mapped to nested API sub-filters. Matching is case-insensitive and supports * and ? wildcards.`, + items: { $ref: '#/definitions/apiSelector' }, + }; + } else { + props[field] = { + $ref: '#/definitions/resourcePatternArray', + description: `${label} within this workspace to extract. Matching is case-insensitive and supports * and ? wildcards.`, + }; + } + } + return props; +} + +const LICENSE_COMMENT = 'Copyright (c) Microsoft Corporation. Licensed under the MIT license.'; + +const extractorSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + $comment: LICENSE_COMMENT, + $id: 'https://github.com/Azure/apiops-cli/schemas/extractor-config.schema.json', + title: 'APIOps Filter Configuration', + description: + 'Validates configuration.extractor.yaml files used by APIOps CLI to select which Azure API Management resources are extracted. All resource sections are optional.', + type: 'object', + additionalProperties: false, + properties: buildFilterProperties(), + definitions: { + resourcePattern: { + type: 'string', + description: + 'A resource name or wildcard pattern. Matching is case-insensitive. Supported wildcards: * matches zero or more characters, and ? matches a single character.', + }, + resourcePatternArray: { + type: 'array', + description: + 'A list of resource names or wildcard patterns. Matching is case-insensitive. Supported wildcards: * and ?.', + items: { $ref: '#/definitions/resourcePattern' }, + }, + apiSelector: { + oneOf: [ + { $ref: '#/definitions/resourcePattern' }, + { + type: 'object', + description: + 'A single API name mapped to sub-resource filters for that API.', + minProperties: 1, + maxProperties: 1, + patternProperties: { + '^.+$': { $ref: '#/definitions/apiSubFilter' }, + }, + additionalProperties: false, + }, + ], + }, + workspaceSelector: { + oneOf: [ + { $ref: '#/definitions/resourcePattern' }, + { + type: 'object', + description: + 'A single workspace name mapped to sub-resource filters for that workspace.', + minProperties: 1, + maxProperties: 1, + patternProperties: { + '^.+$': { $ref: '#/definitions/workspaceSubFilter' }, + }, + additionalProperties: false, + }, + ], + }, + apiSubFilter: { + type: 'object', + description: + 'Sub-resource filters for a specific API. Omit a property to include all sub-resources of that type, or set it to an empty array to exclude all of that type. Matching is case-insensitive and supports * and ? wildcards.', + additionalProperties: false, + properties: buildApiSubFilterProperties(), + }, + workspaceSubFilter: { + type: 'object', + description: + 'Sub-resource filters for a specific workspace. Omit a property to include all resources of that type, or set it to an empty array to exclude all of that type. Matching is case-insensitive and supports * and ? wildcards.', + additionalProperties: false, + properties: buildWorkspaceSubFilterProperties(), + }, + }, +}; + +// --- Override Config Schema --- + +const overrideFields = extractFields('OverrideConfig', configSource); + +function buildOverrideProperties() { + const props = { + $schema: { + type: 'string', + description: 'Optional schema URI for editor and IDE validation.', + }, + }; + + for (const field of overrideFields) { + const label = RESOURCE_LABELS[field] || field.charAt(0).toUpperCase() + field.slice(1); + const tokenNote = 'Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution.'; + + if (field === 'apis') { + props.apis = { + $ref: '#/definitions/apiOverrideSection', + description: `API overrides. Each entry can override API properties and optionally define nested diagnostics, operations, policies, and releases. ${tokenNote}`, + }; + } else if (field === 'backends') { + props.backends = { + $ref: '#/definitions/overrideSection', + description: `Backend overrides. Use the properties object to deep-merge resource properties such as URLs or credentials. ${tokenNote}`, + }; + } else if (field === 'namedValues') { + props.namedValues = { + $ref: '#/definitions/overrideSection', + description: `Named value overrides. Use the properties object to deep-merge resource properties. ${tokenNote}`, + }; + } else { + props[field] = { + $ref: '#/definitions/overrideSection', + description: `${label} overrides. ${tokenNote}`, + }; + } + } + + return props; +} + +const overrideSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + $comment: LICENSE_COMMENT, + $id: 'https://github.com/Azure/apiops-cli/schemas/override-config.schema.json', + title: 'APIOps Override Configuration', + description: + 'Validates configuration.{env}.yaml override files used by APIOps CLI to apply environment-specific property overrides during publish. All resource sections are optional.', + type: 'object', + additionalProperties: false, + properties: buildOverrideProperties(), + definitions: { + propertiesObject: { + type: 'object', + description: + 'Properties to deep-merge into the target resource. Any property name is allowed. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution.', + additionalProperties: true, + }, + overrideEntry: { + type: 'object', + required: ['name', 'properties'], + additionalProperties: false, + properties: { + name: { + type: 'string', + description: 'Resource name to match for this override entry.', + }, + properties: { $ref: '#/definitions/propertiesObject' }, + }, + }, + overrideSection: { + type: 'array', + description: 'A list of override entries for a resource type.', + items: { $ref: '#/definitions/overrideEntry' }, + }, + operationOverrideEntry: { + type: 'object', + required: ['name', 'properties'], + additionalProperties: false, + properties: { + name: { + type: 'string', + description: 'Operation name to match for this override entry.', + }, + properties: { $ref: '#/definitions/propertiesObject' }, + policies: { + $ref: '#/definitions/overrideSection', + description: + 'Policy overrides nested under this operation. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution.', + }, + }, + }, + operationOverrideSection: { + type: 'array', + description: 'A list of operation override entries.', + items: { $ref: '#/definitions/operationOverrideEntry' }, + }, + apiOverrideEntry: { + type: 'object', + required: ['name', 'properties'], + additionalProperties: false, + properties: { + name: { + type: 'string', + description: 'API name to match for this override entry.', + }, + properties: { $ref: '#/definitions/propertiesObject' }, + diagnostics: { + $ref: '#/definitions/overrideSection', + description: + 'Diagnostic overrides nested under this API. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution.', + }, + operations: { + $ref: '#/definitions/operationOverrideSection', + description: + 'Operation overrides nested under this API. Each operation can define its own nested policies. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution.', + }, + policies: { + $ref: '#/definitions/overrideSection', + description: + 'Policy overrides nested directly under this API. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution.', + }, + releases: { + $ref: '#/definitions/overrideSection', + description: + 'Release overrides nested under this API. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution.', + }, + }, + }, + apiOverrideSection: { + type: 'array', + description: 'A list of API override entries.', + items: { $ref: '#/definitions/apiOverrideEntry' }, + }, + }, +}; + +// Write schemas +await mkdir(schemasDir, { recursive: true }); +await writeFile( + resolve(schemasDir, 'extractor-config.schema.json'), + JSON.stringify(extractorSchema, null, 2) + '\n' +); +await writeFile( + resolve(schemasDir, 'override-config.schema.json'), + JSON.stringify(overrideSchema, null, 2) + '\n' +); diff --git a/src/services/init-service.ts b/src/services/init-service.ts index 9ed311fc..8ee2a4de 100644 --- a/src/services/init-service.ts +++ b/src/services/init-service.ts @@ -32,6 +32,8 @@ import { generateFilterConfig } from '../templates/configs/filter-config.js'; import { generateOverrideConfig } from '../templates/configs/override-config.js'; import { generatePackageJson } from '../templates/configs/package-json.js'; import { generateIdentitySetupPrompt } from '../templates/copilot/identity-setup-prompt.js'; +import { generateConfigureFilterPrompt } from '../templates/copilot/configure-filter-prompt.js'; +import { generateConfigureOverridesPrompt } from '../templates/copilot/configure-overrides-prompt.js'; /** Placeholder values used in generated identity setup guides */ const PLACEHOLDER_SUBSCRIPTION_ID = ''; @@ -146,6 +148,14 @@ class InitServiceImpl implements InitService { config.outputDir, '.github/prompts/apiops-setup-identity.prompt.md' ); + const filterPromptFile = path.join( + config.outputDir, + '.github/prompts/apiops-configure-filter.prompt.md' + ); + const overridesPromptFile = path.join( + config.outputDir, + '.github/prompts/apiops-configure-overrides.prompt.md' + ); const identityGuide = path.join( config.outputDir, 'IDENTITY-SETUP-GITHUB.md' @@ -160,6 +170,12 @@ class InitServiceImpl implements InitService { if (await this.fileExists(promptFile)) { conflictingFiles.push(promptFile); } + if (await this.fileExists(filterPromptFile)) { + conflictingFiles.push(filterPromptFile); + } + if (await this.fileExists(overridesPromptFile)) { + conflictingFiles.push(overridesPromptFile); + } if (await this.fileExists(identityGuide)) { conflictingFiles.push(identityGuide); } @@ -180,6 +196,14 @@ class InitServiceImpl implements InitService { config.outputDir, '.github/prompts/apiops-setup-identity.prompt.md' ); + const filterPromptFile = path.join( + config.outputDir, + '.github/prompts/apiops-configure-filter.prompt.md' + ); + const overridesPromptFile = path.join( + config.outputDir, + '.github/prompts/apiops-configure-overrides.prompt.md' + ); if (await this.fileExists(extractPipeline)) { conflictingFiles.push(extractPipeline); @@ -193,6 +217,12 @@ class InitServiceImpl implements InitService { if (await this.fileExists(promptFile)) { conflictingFiles.push(promptFile); } + if (await this.fileExists(filterPromptFile)) { + conflictingFiles.push(filterPromptFile); + } + if (await this.fileExists(overridesPromptFile)) { + conflictingFiles.push(overridesPromptFile); + } } // Check for config files @@ -317,6 +347,7 @@ class InitServiceImpl implements InitService { generatedFiles.pipelines.push('.github/workflows/run-apim-publisher.yml'); await this.generateCopilotIdentitySetupPrompt(config, generatedFiles); + await this.generateCopilotConfigurationPrompts(config, generatedFiles); } /** @@ -350,6 +381,7 @@ class InitServiceImpl implements InitService { generatedFiles.pipelines.push('.azdo/pipelines/run-apim-publisher.yml'); await this.generateCopilotIdentitySetupPrompt(config, generatedFiles); + await this.generateCopilotConfigurationPrompts(config, generatedFiles); } private async generateCopilotIdentitySetupPrompt( @@ -367,6 +399,30 @@ class InitServiceImpl implements InitService { generatedFiles.configs.push('.github/prompts/apiops-setup-identity.prompt.md'); } + private async generateCopilotConfigurationPrompts( + config: InitConfig, + generatedFiles: GeneratedFiles + ): Promise { + const promptsDir = path.join(config.outputDir, '.github/prompts'); + await fs.mkdir(promptsDir, { recursive: true }); + + // Filter configuration prompt + const filterPromptContent = generateConfigureFilterPrompt({ + environments: config.environments, + }); + const filterPromptPath = path.join(promptsDir, 'apiops-configure-filter.prompt.md'); + await fs.writeFile(filterPromptPath, filterPromptContent); + generatedFiles.configs.push('.github/prompts/apiops-configure-filter.prompt.md'); + + // Override configuration prompt + const overridesPromptContent = generateConfigureOverridesPrompt({ + environments: config.environments, + }); + const overridesPromptPath = path.join(promptsDir, 'apiops-configure-overrides.prompt.md'); + await fs.writeFile(overridesPromptPath, overridesPromptContent); + generatedFiles.configs.push('.github/prompts/apiops-configure-overrides.prompt.md'); + } + /** * Generate configuration files */ diff --git a/src/templates/configs/filter-config.ts b/src/templates/configs/filter-config.ts index a992c12b..e2f7df59 100644 --- a/src/templates/configs/filter-config.ts +++ b/src/templates/configs/filter-config.ts @@ -6,7 +6,8 @@ */ export function generateFilterConfig(): string { - return `# APIM Extract Filter Configuration + return `# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/extractor-config.schema.json +# APIM Extract Filter Configuration # Customize this file to control which resources are extracted # For full format details and examples, see: # https://github.com/Azure/apiops-cli/blob/main/docs/guides/filtering-resources.md diff --git a/src/templates/configs/override-config.ts b/src/templates/configs/override-config.ts index 0aae9a58..d3e013c6 100644 --- a/src/templates/configs/override-config.ts +++ b/src/templates/configs/override-config.ts @@ -6,7 +6,8 @@ */ export function generateOverrideConfig(environment: string): string { - return `# APIM Override Configuration for ${environment} environment + return `# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/override-config.schema.json +# APIM Override Configuration for ${environment} environment # Customize resource properties for this specific environment # For full format details and examples, see: # https://github.com/Azure/apiops-cli/blob/main/docs/guides/environment-overrides.md diff --git a/src/templates/copilot/configure-filter-prompt.md b/src/templates/copilot/configure-filter-prompt.md new file mode 100644 index 00000000..69e5dc23 --- /dev/null +++ b/src/templates/copilot/configure-filter-prompt.md @@ -0,0 +1,64 @@ +# Configure APIOps Extractor Filters + +> **How to use:** Open this file in VS Code with GitHub Copilot and ask +> Copilot to help you design a `configuration.extractor.yaml` file for your +> repository. + +## Goal + +Create a `configuration.extractor.yaml` file that limits APIOps extraction to +the Azure API Management resources your team wants to manage in source control. + +--- + +## Step 1 — Gather Requirements + +Copilot, ask the user which APIM resources should be included or excluded from +extraction. Confirm details such as: + +- APIs to include or exclude +- Products to include or exclude +- Named values, backends, loggers, gateways, tags, and version sets +- Whether the team prefers broad extraction first, then tightening filters later + +Summarize the answers before generating any YAML. + +--- + +## Step 2 — Propose a Filter Strategy + +Based on the user's answers: + +1. Recommend the smallest filter that safely captures the intended scope +2. Explain any tradeoffs between broad and narrow filters +3. Call out any risk of accidentally excluding required dependencies + +If the user is unsure, start with a conservative filter that is easy to refine. + +--- + +## Step 3 — Generate `configuration.extractor.yaml` + +Create the full YAML file content for `configuration.extractor.yaml`. + +Requirements: + +- Include the schema comment at the top of the file: + `# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/extractor-config.schema.json` +- Output valid YAML only when generating the final file +- Preserve any APIOps-supported filter structure the user requests +- Prefer readable comments only when they help explain a non-obvious choice +- Do not invent resource names — ask the user or use placeholders when needed + +--- + +## Step 4 — Validate the Result + +Before finishing: + +1. Review the generated YAML for syntax issues +2. Confirm the filters align with the user's intended extraction scope +3. Remind the user to run the extractor and inspect the artifact output + +If the extractor output is too broad or too narrow, help the user refine the +filter file iteratively. diff --git a/src/templates/copilot/configure-filter-prompt.ts b/src/templates/copilot/configure-filter-prompt.ts new file mode 100644 index 00000000..0ab7c4fa --- /dev/null +++ b/src/templates/copilot/configure-filter-prompt.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +/** + * GitHub Copilot prompt template for configuring resource filters. + * Generates a .prompt.md file that guides Copilot through creating + * a configuration.extractor.yaml filter file. + */ + +import { copilotConfigureFilterPromptTemplate } from '../generated/embedded-markdown.js'; + +export interface ConfigureFilterPromptConfig { + environments: string[]; +} + +export function generateConfigureFilterPrompt(_config: ConfigureFilterPromptConfig): string { + // The filter prompt is static — no token substitution needed currently. + // The config parameter is accepted for future extensibility and consistency. + return copilotConfigureFilterPromptTemplate; +} diff --git a/src/templates/copilot/configure-overrides-prompt.md b/src/templates/copilot/configure-overrides-prompt.md new file mode 100644 index 00000000..72343b89 --- /dev/null +++ b/src/templates/copilot/configure-overrides-prompt.md @@ -0,0 +1,111 @@ +# Configure APIOps Environment Overrides + +> **How to use:** Open this file in VS Code with GitHub Copilot and ask +> Copilot to help you create environment-specific APIOps override files. + +## Goal + +Create one `configuration.{environment}.yaml` file per deployment environment +so APIOps publish runs can promote the same artifacts across environments with +environment-specific settings. + +Environments: {{ENVIRONMENT_LIST}} + +--- + +## Step 1 — Gather Environment-Specific Values + +Copilot, work through each environment in this list: **{{ENVIRONMENT_LIST}}**. + +For each environment, ask the user for any values that differ by environment, +such as: + +- Backend URLs +- Named value contents +- Service URLs +- Product settings +- Policy fragments or references +- Any other APIM setting that should change between environments + +Summarize the collected values before generating files. + +--- + +## Step 2 — Recommend an Override Layout + +For each environment, explain: + +1. Which settings belong in `configuration.{environment}.yaml` +2. Which settings should remain in the extracted base artifacts +3. Any values that should be stored securely rather than committed directly + +Prefer a minimal override file that only contains values that truly vary by +environment. + +--- + +## Step 3 — Secrets and External Resources + +For values that must remain secret (API keys, connection strings, credentials): + +- Use **`{#[TOKEN_NAME]#}`** placeholder syntax for pipeline secret substitution. + The pipeline replaces these placeholders with environment variables at runtime. + Example: + ```yaml + namedValues: + - name: payment-api-key + properties: + value: "{#[PAYMENT_API_KEY]#}" + ``` + +- For Azure Key Vault references, use the `keyVault` property: + ```yaml + namedValues: + - name: db-connection-string + properties: + keyVault: + secretIdentifier: "https://{env}-kv.vault.azure.net/secrets/db-conn" + identityClientId: "{#[MANAGED_IDENTITY_CLIENT_ID]#}" + ``` + +Guide the user on when to use each approach: +- **`{#[TOKEN_NAME]#}` placeholders** — simple secrets stored in pipeline variables +- **Key Vault references** — secrets that should be managed centrally in Azure +- **Plain values** — non-sensitive settings like URLs or feature flags + +--- + +## Step 4 — Generate the Override Files + +Create one YAML file per environment: + +- `configuration.dev.yaml` +- `configuration.staging.yaml` +- `configuration.prod.yaml` + +Only generate files that match the user's actual environment list. Replace the +example names above as needed. + +Requirements: + +- Output valid YAML for each file +- Include the schema comment at the top of each file: + `# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/override-config.schema.json` +- Keep the files easy to compare across environments +- Use `{#[TOKEN_NAME]#}` placeholders for secrets (never commit real secret values) +- Use Key Vault references for centrally-managed secrets +- Avoid duplicating unchanged base configuration + +--- + +## Step 5 — Validate the Promotion Model + +Before finishing: + +1. Verify every generated override file matches the intended environment +2. Confirm no unresolved `{{ENVIRONMENT_LIST}}` placeholders remain +3. Verify all secrets use either `{#[TOKEN_NAME]#}` or Key Vault references +4. Remind the user to add the corresponding secret values to their pipeline's + secret store (GitHub Actions Secrets or Azure DevOps variable groups) +5. Remind the user to test publish for a lower environment before promoting + further diff --git a/src/templates/copilot/configure-overrides-prompt.ts b/src/templates/copilot/configure-overrides-prompt.ts new file mode 100644 index 00000000..30c3501f --- /dev/null +++ b/src/templates/copilot/configure-overrides-prompt.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +/** + * GitHub Copilot prompt template for configuring environment overrides. + * Generates a .prompt.md file that guides Copilot through creating + * configuration.{env}.yaml override files for environment promotion. + */ + +import { copilotConfigureOverridesPromptTemplate } from '../generated/embedded-markdown.js'; + +export interface ConfigureOverridesPromptConfig { + environments: string[]; +} + +function renderTemplate(template: string, tokens: Record): string { + return Object.entries(tokens).reduce( + (rendered, [key, value]) => rendered.replaceAll(`{{${key}}}`, value), + template + ); +} + +export function generateConfigureOverridesPrompt(config: ConfigureOverridesPromptConfig): string { + const environmentList = config.environments.join(', '); + return renderTemplate(copilotConfigureOverridesPromptTemplate, { + ENVIRONMENT_LIST: environmentList, + }); +} diff --git a/tests/unit/services/init-service.test.ts b/tests/unit/services/init-service.test.ts index eb4c8849..d9b3f89b 100644 --- a/tests/unit/services/init-service.test.ts +++ b/tests/unit/services/init-service.test.ts @@ -357,6 +357,35 @@ describe('init-service', () => { expect(content).toContain('gh secret set'); }); + it('should generate Copilot configuration prompts for GitHub Actions', async () => { + const config: InitConfig = { + ciProvider: 'github-actions', + nonInteractive: true, + artifactDir: './apim-artifacts', + environments: ['dev', 'prod'], + outputDir: '/test', + cliPackage: TEST_CLI_PACKAGE, + force: false, + }; + + const result = await initService.run(config); + + expect(result.configs).toContain('.github/prompts/apiops-configure-filter.prompt.md'); + expect(result.configs).toContain('.github/prompts/apiops-configure-overrides.prompt.md'); + + const filterPromptCalls = vi.mocked(fs.writeFile).mock.calls.filter( + (call) => call[0] === path.join('/test', '.github/prompts/apiops-configure-filter.prompt.md') + ); + expect(filterPromptCalls).toHaveLength(1); + expect(filterPromptCalls[0][1]).toContain('Configure APIOps Extractor Filters'); + + const overridesPromptCalls = vi.mocked(fs.writeFile).mock.calls.filter( + (call) => call[0] === path.join('/test', '.github/prompts/apiops-configure-overrides.prompt.md') + ); + expect(overridesPromptCalls).toHaveLength(1); + expect(overridesPromptCalls[0][1]).toContain('Configure APIOps Environment Overrides'); + }); + it('should generate Copilot identity setup prompt for Azure DevOps', async () => { const config: InitConfig = { ciProvider: 'azure-devops', @@ -380,6 +409,34 @@ describe('init-service', () => { expect(content).toContain('az devops service-endpoint create --service-endpoint-configuration'); }); + it('should detect conflicts for Copilot configuration prompts', async () => { + vi.mocked(fs.access).mockImplementation(async (filePath: PathLike) => { + const p = filePath.toString(); + if ( + p === TEST_CLI_PACKAGE_RESOLVED || + p.includes('apiops-configure-filter.prompt.md') || + p.includes('apiops-configure-overrides.prompt.md') + ) { + return Promise.resolve(); + } + throw new Error('ENOENT'); + }); + + const config: InitConfig = { + ciProvider: 'azure-devops', + nonInteractive: true, + artifactDir: './apim-artifacts', + environments: ['dev'], + outputDir: '/test', + cliPackage: TEST_CLI_PACKAGE, + force: false, + }; + + await expect(initService.run(config)).rejects.toThrow( + 'Use --force to overwrite existing files' + ); + }); + it('should copy CLI tarball into .apiops directory', async () => { const config: InitConfig = { ciProvider: 'github-actions', @@ -602,5 +659,6 @@ describe('init-service', () => { expect(pkg.dependencies.lodash).toBe('^4.17.21'); expect(pkg.dependencies['@peterhauge/apiops-cli']).toBe('latest'); }); + }); }); diff --git a/tests/unit/templates/copilot/configure-filter-prompt.test.ts b/tests/unit/templates/copilot/configure-filter-prompt.test.ts new file mode 100644 index 00000000..398b2012 --- /dev/null +++ b/tests/unit/templates/copilot/configure-filter-prompt.test.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +/** + * Unit tests for GitHub Copilot filter configuration prompt template + */ + +import { describe, it, expect } from 'vitest'; +import { generateConfigureFilterPrompt } from '../../../../src/templates/copilot/configure-filter-prompt.js'; + +describe('copilot/configure-filter-prompt', () => { + it('should return the static filter prompt template', () => { + const prompt = generateConfigureFilterPrompt({ environments: ['dev', 'prod'] }); + + expect(prompt).toContain('# Configure APIOps Extractor Filters'); + expect(prompt).toContain('configuration.extractor.yaml'); + expect(prompt).toContain('## Step 1'); + expect(prompt).toContain('## Step 4'); + }); +}); diff --git a/tests/unit/templates/copilot/configure-overrides-prompt.test.ts b/tests/unit/templates/copilot/configure-overrides-prompt.test.ts new file mode 100644 index 00000000..fd008154 --- /dev/null +++ b/tests/unit/templates/copilot/configure-overrides-prompt.test.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +/** + * Unit tests for GitHub Copilot environment override prompt template + */ + +import { describe, it, expect } from 'vitest'; +import { generateConfigureOverridesPrompt } from '../../../../src/templates/copilot/configure-overrides-prompt.js'; + +describe('copilot/configure-overrides-prompt', () => { + it('should render the environment list into the prompt', () => { + const prompt = generateConfigureOverridesPrompt({ + environments: ['dev', 'staging', 'prod'], + }); + + expect(prompt).toContain('# Configure APIOps Environment Overrides'); + expect(prompt).toContain('Environments: dev, staging, prod'); + expect(prompt).toContain('configuration.{environment}.yaml'); + expect(prompt).not.toMatch(/\{\{[^}]+\}\}/); + }); +}); From 4a8c5dd72a161b12408d0f74a8c4d8dec46d63fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 04:09:40 +0000 Subject: [PATCH 02/13] docs: address PR review feedback on init docs and prompt templates --- docs/commands/init.md | 16 -- docs/guides/environment-overrides.md | 12 +- docs/guides/filtering-resources.md | 17 +- .../copilot/configure-filter-prompt.md | 39 +++- .../copilot/configure-overrides-prompt.md | 175 +++++++++++------- .../copilot/configure-overrides-prompt.ts | 8 +- .../copilot/identity-setup-prompt.ts | 8 +- src/templates/shared/template-utils.ts | 17 ++ 8 files changed, 162 insertions(+), 130 deletions(-) create mode 100644 src/templates/shared/template-utils.ts diff --git a/docs/commands/init.md b/docs/commands/init.md index 2aaedb08..4879c4eb 100644 --- a/docs/commands/init.md +++ b/docs/commands/init.md @@ -83,8 +83,6 @@ In interactive mode (the default when running in a terminal), `apiops init` prom | `configuration.extractor.yaml` | Sample filter configuration for extraction | | `configuration.{env}.yaml` | Override templates per environment (e.g., `configuration.dev.yaml`, `configuration.prod.yaml`) | | `IDENTITY-SETUP-GITHUB.md` | Step-by-step guide for configuring federated credentials | -| `.github/prompts/apiops-configure-filter.prompt.md` | Copilot prompt for configuring resource filters | -| `.github/prompts/apiops-configure-overrides.prompt.md` | Copilot prompt for configuring environment overrides | ### Azure DevOps (`--ci azure-devops`) @@ -95,8 +93,6 @@ In interactive mode (the default when running in a terminal), `apiops init` prom | `configuration.extractor.yaml` | Sample filter configuration for extraction | | `configuration.{env}.yaml` | Override templates per environment | | `IDENTITY-SETUP-AZDO.md` | Step-by-step guide for configuring service connections | -| `.github/prompts/apiops-configure-filter.prompt.md` | Copilot prompt for configuring resource filters | -| `.github/prompts/apiops-configure-overrides.prompt.md` | Copilot prompt for configuring environment overrides | ### Both platforms @@ -129,18 +125,6 @@ If you pass `--cli-package `, the tarball is copied into a `.apiops/` dire 5. **Configure overrides** — Edit `configuration.{env}.yaml` files with environment-specific values. Use the `.github/prompts/apiops-configure-overrides.prompt.md` Copilot prompt for guided setup. See the [environment overrides guide](../guides/environment-overrides.md). 6. **Run your pipeline** — Trigger the publish pipeline to deploy artifacts to your target APIM instance. -## JSON Schemas for IDE Autocomplete - -Generated `configuration.extractor.yaml` and `configuration.{env}.yaml` files include a YAML language server schema comment that provides IDE autocomplete when editing: - -```yaml -# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/extractor-config.schema.json -``` - -The schemas are available at: -- **Filter config:** [`schemas/extractor-config.schema.json`](https://github.com/Azure/apiops-cli/blob/main/schemas/extractor-config.schema.json) -- **Override config:** [`schemas/override-config.schema.json`](https://github.com/Azure/apiops-cli/blob/main/schemas/override-config.schema.json) - ## Related docs - [apiops extract](./extract.md) — extract APIM configuration to local files diff --git a/docs/guides/environment-overrides.md b/docs/guides/environment-overrides.md index a2a04719..d1266be6 100644 --- a/docs/guides/environment-overrides.md +++ b/docs/guides/environment-overrides.md @@ -495,21 +495,19 @@ apiops publish --overrides configuration.prod.yaml --dry-run \ --- -## JSON Schema and Copilot Prompt +## Copilot-Assisted Configuration -### IDE Autocompletion with JSON Schema +If you ran `apiops init`, a Copilot prompt file was generated at `.github/prompts/apiops-configure-overrides.prompt.md`. Open it in VS Code and ask GitHub Copilot to help you configure environment overrides — it will guide you through setting up environment-specific values interactively. + +## IDE Autocompletion with JSON Schema A JSON Schema is available for `configuration.{env}.yaml` override files. Add this comment at the top of your override file to enable autocompletion in VS Code (with the YAML extension): ```yaml -# yaml-language-server: $schema=./schemas/override-config.schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/override-config.schema.json ``` The schema provides: - Property name autocompletion for all resource sections - Validation of the override structure (name + properties format) - Inline documentation including token substitution syntax - -### Copilot-Assisted Configuration - -If you ran `apiops init`, a Copilot prompt file was generated at `.github/prompts/apiops-configure-overrides.prompt.md`. Open it in VS Code and ask GitHub Copilot to help you configure environment overrides — it will guide you through setting up environment-specific values interactively. diff --git a/docs/guides/filtering-resources.md b/docs/guides/filtering-resources.md index 43f1a1cb..0135eb07 100644 --- a/docs/guides/filtering-resources.md +++ b/docs/guides/filtering-resources.md @@ -351,21 +351,6 @@ backends: --- -## JSON Schema and Copilot Prompt - -### IDE Autocompletion with JSON Schema - -A JSON Schema is available for `configuration.extractor.yaml` files. Add this comment at the top of your filter file to enable autocompletion in VS Code (with the YAML extension): - -```yaml -# yaml-language-server: $schema=./schemas/extractor-config.schema.json -``` - -The schema provides: -- Property name autocompletion for all resource types -- Validation of the filter structure -- Inline documentation for each field - -### Copilot-Assisted Configuration +## Copilot-Assisted Configuration If you ran `apiops init`, a Copilot prompt file was generated at `.github/prompts/apiops-configure-filter.prompt.md`. Open it in VS Code and ask GitHub Copilot to help you configure your filter — it will walk you through selecting resources interactively. diff --git a/src/templates/copilot/configure-filter-prompt.md b/src/templates/copilot/configure-filter-prompt.md index 69e5dc23..d1cf3eef 100644 --- a/src/templates/copilot/configure-filter-prompt.md +++ b/src/templates/copilot/configure-filter-prompt.md @@ -1,3 +1,8 @@ +--- +mode: 'agent' +description: 'Configure APIOps resource extraction filters' +--- + # Configure APIOps Extractor Filters > **How to use:** Open this file in VS Code with GitHub Copilot and ask @@ -13,13 +18,29 @@ the Azure API Management resources your team wants to manage in source control. ## Step 1 — Gather Requirements -Copilot, ask the user which APIM resources should be included or excluded from -extraction. Confirm details such as: - -- APIs to include or exclude -- Products to include or exclude -- Named values, backends, loggers, gateways, tags, and version sets -- Whether the team prefers broad extraction first, then tightening filters later +Copilot, for each APIM resource type that has a corresponding info or metadata +file in the artifact directory, ask the user whether they want to: + +- **Extract ALL** — include every resource of this type (omit this type from + the filter; APIOps extracts everything by default) +- **Extract NONE** — exclude all resources of this type (create a filter with + an empty array for this type) +- **Extract SOME** — include only specific resources (help them build the + filter for that type, asking which names to include or exclude) + +Resource types to ask about (ask only for types that appear to exist in the +artifact directory or that the user mentions): + +- APIs +- Products +- Named values +- Backends +- Loggers +- Gateways +- Tags +- Version sets +- Policy fragments +- Subscriptions Summarize the answers before generating any YAML. @@ -39,6 +60,10 @@ If the user is unsure, start with a conservative filter that is easy to refine. ## Step 3 — Generate `configuration.extractor.yaml` +> **Note:** The file `configuration.extractor.yaml` may already exist if the +> user ran `apiops init`. Check for its presence before creating a new one — +> if it exists, update it in place rather than overwriting it. + Create the full YAML file content for `configuration.extractor.yaml`. Requirements: diff --git a/src/templates/copilot/configure-overrides-prompt.md b/src/templates/copilot/configure-overrides-prompt.md index 72343b89..6f5a9f7e 100644 --- a/src/templates/copilot/configure-overrides-prompt.md +++ b/src/templates/copilot/configure-overrides-prompt.md @@ -1,3 +1,8 @@ +--- +mode: 'agent' +description: 'Configure APIOps environment overrides' +--- + # Configure APIOps Environment Overrides > **How to use:** Open this file in VS Code with GitHub Copilot and ask @@ -13,88 +18,116 @@ Environments: {{ENVIRONMENT_LIST}} --- -## Step 1 — Gather Environment-Specific Values - -Copilot, work through each environment in this list: **{{ENVIRONMENT_LIST}}**. - -For each environment, ask the user for any values that differ by environment, -such as: +## Step 1 — Gather Information -- Backend URLs -- Named value contents -- Service URLs -- Product settings -- Policy fragments or references -- Any other APIM setting that should change between environments +Copilot, start by collecting the following from the user: -Summarize the collected values before generating files. +1. **Environment names** — Confirm or update the list: **{{ENVIRONMENT_LIST}}**. + Note that many teams only need `stage` and `prod` (dev may share the same + settings as stage, or dev may not be managed by APIOps at all). ---- +2. **Existing override config files** — Check whether + `configuration.{env}.yaml` files already exist in the repository: + - If they exist, use those as the starting point. + - If they don't, ask the user whether to create new ones or whether they + may already exist under a different name or location. -## Step 2 — Recommend an Override Layout +3. **APIM artifacts location** — Ask the user where the APIOps artifact + directory is (default: `./apim-artifacts`). You will need to inspect the + artifacts in the next step. -For each environment, explain: +Summarize what you've learned before moving on. -1. Which settings belong in `configuration.{environment}.yaml` -2. Which settings should remain in the extracted base artifacts -3. Any values that should be stored securely rather than committed directly +--- -Prefer a minimal override file that only contains values that truly vary by -environment. +## Step 2 — Investigate APIM Artifacts and Create Stub Override Files + +Using the artifact directory identified in Step 1: + +1. Scan the artifacts for references to **external resources** — these are the + things that typically need overrides between environments. Examples: + - Backend service URLs + - Named values (especially those referencing Key Vault secrets) + - Product subscription settings + - Logger resource IDs + - Gateway or VNet references + - Policy fragment references to external endpoints + + > **Note:** References to sub-resources of the same APIM instance (e.g., + > one API referencing another API's policy) are handled automatically by + > APIOps and do **not** need overrides. + +2. For each environment, create a **stub** `configuration.{env}.yaml` that + covers all the commonly-overridden items you found. Use placeholder example + values so the user can see the shape of the file. + +3. **Add everything as YAML comments initially** — a commented-out section is + safe because it will not cause a publish failure. The user can uncomment + and fill in values in Step 3. Example pattern: + ```yaml + # namedValues: + # - name: payment-api-key + # properties: + # value: "{#[PAYMENT_API_KEY]#}" + ``` --- -## Step 3 — Secrets and External Resources - -For values that must remain secret (API keys, connection strings, credentials): - -- Use **`{#[TOKEN_NAME]#}`** placeholder syntax for pipeline secret substitution. - The pipeline replaces these placeholders with environment variables at runtime. - Example: - ```yaml - namedValues: - - name: payment-api-key - properties: - value: "{#[PAYMENT_API_KEY]#}" - ``` - -- For Azure Key Vault references, use the `keyVault` property: - ```yaml - namedValues: - - name: db-connection-string - properties: - keyVault: - secretIdentifier: "https://{env}-kv.vault.azure.net/secrets/db-conn" - identityClientId: "{#[MANAGED_IDENTITY_CLIENT_ID]#}" - ``` - -Guide the user on when to use each approach: -- **`{#[TOKEN_NAME]#}` placeholders** — simple secrets stored in pipeline variables -- **Key Vault references** — secrets that should be managed centrally in Azure -- **Plain values** — non-sensitive settings like URLs or feature flags +## Step 3 — Work With the User to Fill In Values + +Go through each environment one at a time. For each environment: + +1. **Pipeline environment variables (tokens)** — Ask the user whether + environment variables are available in the publish pipeline. Common ones: + - Subscription ID + - Resource group name + - APIM service instance name + + These can be added as `{#[TOKEN_NAME]#}` placeholders so the pipeline + substitutes the real value at runtime. This avoids hardcoding + environment-specific IDs in files committed to source control. + +2. **Shared values** — Sometimes a value does not need to differ by + environment (e.g., dev and stage may use the same Key Vault). Confirm with + the user before duplicating values. + +3. **Key Vault pattern** — A common pattern is for one Key Vault to hold all + secrets per environment (e.g., `https://{env}-kv.vault.azure.net/secrets/`). + Users often define a named-value token for the Key Vault secrets base URL and + then append the secret name — this avoids human error. For example: + ```yaml + namedValues: + - name: kv-base-url + properties: + value: "{#[KV_BASE_URL]#}" + - name: db-connection-string + properties: + keyVault: + secretIdentifier: "{#[KV_BASE_URL]#}db-conn" + identityClientId: "{#[MANAGED_IDENTITY_CLIENT_ID]#}" + ``` + +4. For values that must remain secret (API keys, connection strings): + - Use **`{#[TOKEN_NAME]#}`** for pipeline-injected secrets. + - Use `keyVault.secretIdentifier` for Azure Key Vault-managed secrets. + - Use plain values only for non-sensitive settings like URLs or feature + flags that are safe to commit. + +Uncomment and populate each stub entry as the user provides or confirms values. --- ## Step 4 — Generate the Override Files -Create one YAML file per environment: - -- `configuration.dev.yaml` -- `configuration.staging.yaml` -- `configuration.prod.yaml` - -Only generate files that match the user's actual environment list. Replace the -example names above as needed. - -Requirements: +Once all values are confirmed, produce the final YAML files: -- Output valid YAML for each file +- Output valid YAML for each file. - Include the schema comment at the top of each file: `# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/override-config.schema.json` -- Keep the files easy to compare across environments -- Use `{#[TOKEN_NAME]#}` placeholders for secrets (never commit real secret values) -- Use Key Vault references for centrally-managed secrets -- Avoid duplicating unchanged base configuration +- Keep files easy to compare across environments. +- Use `{#[TOKEN_NAME]#}` placeholders for secrets (never commit real secret values). +- Use Key Vault references for centrally-managed secrets. +- Avoid duplicating unchanged base configuration. --- @@ -102,10 +135,12 @@ Requirements: Before finishing: -1. Verify every generated override file matches the intended environment -2. Confirm no unresolved `{{ENVIRONMENT_LIST}}` placeholders remain -3. Verify all secrets use either `{#[TOKEN_NAME]#}` or Key Vault references -4. Remind the user to add the corresponding secret values to their pipeline's - secret store (GitHub Actions Secrets or Azure DevOps variable groups) +1. Verify every generated override file matches the intended environment. +2. Confirm no unresolved `{{ENVIRONMENT_LIST}}` placeholders remain. +3. Verify all secrets use either `{#[TOKEN_NAME]#}` or Key Vault references. +4. Remind the user to add any `{#[TOKEN_NAME]#}` tokens to their pipeline's + secret store (GitHub Actions Secrets or Azure DevOps variable groups). + Help the user with this step if they ask. Note that the pipeline will fail + with an error if any tokens are missed. 5. Remind the user to test publish for a lower environment before promoting - further + further. diff --git a/src/templates/copilot/configure-overrides-prompt.ts b/src/templates/copilot/configure-overrides-prompt.ts index 30c3501f..59b3dffc 100644 --- a/src/templates/copilot/configure-overrides-prompt.ts +++ b/src/templates/copilot/configure-overrides-prompt.ts @@ -7,18 +7,12 @@ */ import { copilotConfigureOverridesPromptTemplate } from '../generated/embedded-markdown.js'; +import { renderTemplate } from '../shared/template-utils.js'; export interface ConfigureOverridesPromptConfig { environments: string[]; } -function renderTemplate(template: string, tokens: Record): string { - return Object.entries(tokens).reduce( - (rendered, [key, value]) => rendered.replaceAll(`{{${key}}}`, value), - template - ); -} - export function generateConfigureOverridesPrompt(config: ConfigureOverridesPromptConfig): string { const environmentList = config.environments.join(', '); return renderTemplate(copilotConfigureOverridesPromptTemplate, { diff --git a/src/templates/copilot/identity-setup-prompt.ts b/src/templates/copilot/identity-setup-prompt.ts index 94a31644..164b82f5 100644 --- a/src/templates/copilot/identity-setup-prompt.ts +++ b/src/templates/copilot/identity-setup-prompt.ts @@ -16,19 +16,13 @@ import { copilotGithubEnvironmentSecretCommandsTemplate, copilotGitHubActionsIdentitySetupPromptTemplate, } from '../generated/embedded-markdown.js'; +import { renderTemplate } from '../shared/template-utils.js'; export interface IdentitySetupPromptConfig { environments: string[]; ciProvider?: 'github-actions' | 'azure-devops'; } -function renderTemplate(template: string, tokens: Record): string { - return Object.entries(tokens).reduce( - (rendered, [key, value]) => rendered.replaceAll(`{{${key}}}`, value), - template - ); -} - export function generateIdentitySetupPrompt(config: IdentitySetupPromptConfig): string { if (config.ciProvider === 'azure-devops') { const environmentsArrayPowerShell = config.environments diff --git a/src/templates/shared/template-utils.ts b/src/templates/shared/template-utils.ts new file mode 100644 index 00000000..e1bd533d --- /dev/null +++ b/src/templates/shared/template-utils.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +/** + * Shared template rendering utility for Copilot prompt templates. + * Replaces {{TOKEN}} placeholders with provided values. + */ + +/** + * Replaces all `{{KEY}}` placeholders in a template string with the + * corresponding values from the tokens map. + */ +export function renderTemplate(template: string, tokens: Record): string { + return Object.entries(tokens).reduce( + (rendered, [key, value]) => rendered.replaceAll(`{{${key}}}`, value), + template + ); +} From e749efbd1f3eb0aca249ecf738188eceadbd652c Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Mon, 22 Jun 2026 21:27:26 -0700 Subject: [PATCH 03/13] refactor: remove environment injection from overrides prompt, auto-detect instead The overrides prompt now instructs Copilot to detect environments by scanning for existing configuration.*.yaml files before falling back to asking the user. This makes the prompt reusable outside of apiops init context. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/services/init-service.ts | 4 +- .../copilot/configure-overrides-prompt.md | 41 ++++++++++++------- .../copilot/configure-overrides-prompt.ts | 12 +----- .../configure-overrides-prompt.test.ts | 9 ++-- 4 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/services/init-service.ts b/src/services/init-service.ts index 8ee2a4de..210d6637 100644 --- a/src/services/init-service.ts +++ b/src/services/init-service.ts @@ -415,9 +415,7 @@ class InitServiceImpl implements InitService { generatedFiles.configs.push('.github/prompts/apiops-configure-filter.prompt.md'); // Override configuration prompt - const overridesPromptContent = generateConfigureOverridesPrompt({ - environments: config.environments, - }); + const overridesPromptContent = generateConfigureOverridesPrompt(); const overridesPromptPath = path.join(promptsDir, 'apiops-configure-overrides.prompt.md'); await fs.writeFile(overridesPromptPath, overridesPromptContent); generatedFiles.configs.push('.github/prompts/apiops-configure-overrides.prompt.md'); diff --git a/src/templates/copilot/configure-overrides-prompt.md b/src/templates/copilot/configure-overrides-prompt.md index 6f5a9f7e..565e5db4 100644 --- a/src/templates/copilot/configure-overrides-prompt.md +++ b/src/templates/copilot/configure-overrides-prompt.md @@ -14,25 +14,39 @@ Create one `configuration.{environment}.yaml` file per deployment environment so APIOps publish runs can promote the same artifacts across environments with environment-specific settings. -Environments: {{ENVIRONMENT_LIST}} +--- + +## Step 0 — Detect Environments + +Before asking the user anything, look for existing environment configuration +files in the repository: + +1. Search for files matching `configuration.*.yaml` (excluding + `configuration.extractor.yaml`). The `*` portion is the environment name. +2. Also check CI/CD workflow files (`.github/workflows/` or + `.azdo/pipelines/`) for environment references. + +If existing config files are found, present the detected environments to the +user and ask them to confirm or update the list. + +If no config files are found, ask the user: +> "What environments do you deploy to? Common patterns include `dev, stage, prod` +> or `stage, prod` (if dev shares the same APIM instance as stage)." + +Once the environment list is confirmed, proceed. --- ## Step 1 — Gather Information -Copilot, start by collecting the following from the user: - -1. **Environment names** — Confirm or update the list: **{{ENVIRONMENT_LIST}}**. - Note that many teams only need `stage` and `prod` (dev may share the same - settings as stage, or dev may not be managed by APIOps at all). +Copilot, collect the following from the user: -2. **Existing override config files** — Check whether - `configuration.{env}.yaml` files already exist in the repository: - - If they exist, use those as the starting point. - - If they don't, ask the user whether to create new ones or whether they - may already exist under a different name or location. +1. **Existing override config files** — If `configuration.{env}.yaml` files + already exist: + - Use those as the starting point. + - Ask whether the user wants to update them or start fresh. -3. **APIM artifacts location** — Ask the user where the APIOps artifact +2. **APIM artifacts location** — Ask the user where the APIOps artifact directory is (default: `./apim-artifacts`). You will need to inspect the artifacts in the next step. @@ -136,8 +150,7 @@ Once all values are confirmed, produce the final YAML files: Before finishing: 1. Verify every generated override file matches the intended environment. -2. Confirm no unresolved `{{ENVIRONMENT_LIST}}` placeholders remain. -3. Verify all secrets use either `{#[TOKEN_NAME]#}` or Key Vault references. +2. Verify all secrets use either `{#[TOKEN_NAME]#}` or Key Vault references. 4. Remind the user to add any `{#[TOKEN_NAME]#}` tokens to their pipeline's secret store (GitHub Actions Secrets or Azure DevOps variable groups). Help the user with this step if they ask. Note that the pipeline will fail diff --git a/src/templates/copilot/configure-overrides-prompt.ts b/src/templates/copilot/configure-overrides-prompt.ts index 59b3dffc..9acf3d9a 100644 --- a/src/templates/copilot/configure-overrides-prompt.ts +++ b/src/templates/copilot/configure-overrides-prompt.ts @@ -7,15 +7,7 @@ */ import { copilotConfigureOverridesPromptTemplate } from '../generated/embedded-markdown.js'; -import { renderTemplate } from '../shared/template-utils.js'; -export interface ConfigureOverridesPromptConfig { - environments: string[]; -} - -export function generateConfigureOverridesPrompt(config: ConfigureOverridesPromptConfig): string { - const environmentList = config.environments.join(', '); - return renderTemplate(copilotConfigureOverridesPromptTemplate, { - ENVIRONMENT_LIST: environmentList, - }); +export function generateConfigureOverridesPrompt(): string { + return copilotConfigureOverridesPromptTemplate; } diff --git a/tests/unit/templates/copilot/configure-overrides-prompt.test.ts b/tests/unit/templates/copilot/configure-overrides-prompt.test.ts index fd008154..05f5e5dc 100644 --- a/tests/unit/templates/copilot/configure-overrides-prompt.test.ts +++ b/tests/unit/templates/copilot/configure-overrides-prompt.test.ts @@ -8,14 +8,13 @@ import { describe, it, expect } from 'vitest'; import { generateConfigureOverridesPrompt } from '../../../../src/templates/copilot/configure-overrides-prompt.js'; describe('copilot/configure-overrides-prompt', () => { - it('should render the environment list into the prompt', () => { - const prompt = generateConfigureOverridesPrompt({ - environments: ['dev', 'staging', 'prod'], - }); + it('should produce a static prompt with environment auto-detection instructions', () => { + const prompt = generateConfigureOverridesPrompt(); expect(prompt).toContain('# Configure APIOps Environment Overrides'); - expect(prompt).toContain('Environments: dev, staging, prod'); expect(prompt).toContain('configuration.{environment}.yaml'); + expect(prompt).toContain('Detect Environments'); + expect(prompt).toContain('configuration.*.yaml'); expect(prompt).not.toMatch(/\{\{[^}]+\}\}/); }); }); From a37a489f6ba8b27594570a4b66c5d7e18ad561ab Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Mon, 22 Jun 2026 21:38:48 -0700 Subject: [PATCH 04/13] refactor: consolidate renderTemplate into src/lib/render-template.ts Move the shared renderTemplate function from src/templates/shared/template-utils.ts to src/lib/render-template.ts and remove the duplicate private method from identity-guide-service.ts. All template consumers now import from the single canonical location. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../template-utils.ts => lib/render-template.ts} | 4 ++-- src/services/identity-guide-service.ts | 14 ++++---------- .../copilot/configure-overrides-prompt.md | 15 +++------------ src/templates/copilot/identity-setup-prompt.ts | 2 +- 4 files changed, 10 insertions(+), 25 deletions(-) rename src/{templates/shared/template-utils.ts => lib/render-template.ts} (77%) diff --git a/src/templates/shared/template-utils.ts b/src/lib/render-template.ts similarity index 77% rename from src/templates/shared/template-utils.ts rename to src/lib/render-template.ts index e1bd533d..65b9a766 100644 --- a/src/templates/shared/template-utils.ts +++ b/src/lib/render-template.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. /** - * Shared template rendering utility for Copilot prompt templates. - * Replaces {{TOKEN}} placeholders with provided values. + * Template rendering utility. + * Replaces {{TOKEN}} placeholders in template strings with provided values. */ /** diff --git a/src/services/identity-guide-service.ts b/src/services/identity-guide-service.ts index 474d5731..366c1839 100644 --- a/src/services/identity-guide-service.ts +++ b/src/services/identity-guide-service.ts @@ -11,6 +11,7 @@ import { azureDevOpsIdentityGuideTemplate, githubActionsIdentityGuideTemplate, } from '../templates/generated/embedded-markdown.js'; +import { renderTemplate } from '../lib/render-template.js'; export interface IdentityGuideService { generateGitHubActionsGuide( @@ -25,13 +26,6 @@ export interface IdentityGuideService { } class IdentityGuideServiceImpl implements IdentityGuideService { - private renderTemplate(template: string, tokens: Record): string { - return Object.entries(tokens).reduce( - (rendered, [key, value]) => rendered.replaceAll(`{{${key}}}`, value), - template - ); - } - generateGitHubActionsGuide( subscriptionId: string, resourceGroup: string, @@ -52,7 +46,7 @@ class IdentityGuideServiceImpl implements IdentityGuideService { - \`APIM_SERVICE_NAME_${env.toUpperCase()}\`: APIM service name for ${env} `).join('\n'); - return this.renderTemplate(githubActionsIdentityGuideTemplate, { + return renderTemplate(githubActionsIdentityGuideTemplate, { SUBSCRIPTION_ID: subscriptionId, RESOURCE_GROUP: resourceGroup, FEDERATED_CREDENTIALS_PER_ENV: federatedCredentialsPerEnvironment, @@ -70,12 +64,12 @@ class IdentityGuideServiceImpl implements IdentityGuideService { .map((environment) => `"${environment}"`) .join(' '); - const coreSteps = this.renderTemplate(azureDevOpsIdentitySetupCoreTemplate, { + const coreSteps = renderTemplate(azureDevOpsIdentitySetupCoreTemplate, { ENVIRONMENTS_ARRAY_POWERSHELL: environmentsArrayPowerShell, ENVIRONMENTS_ARRAY_BASH: environmentsArrayBash, }); - return this.renderTemplate(azureDevOpsIdentityGuideTemplate, { + return renderTemplate(azureDevOpsIdentityGuideTemplate, { AZURE_DEVOPS_CORE_STEPS: coreSteps, }); } diff --git a/src/templates/copilot/configure-overrides-prompt.md b/src/templates/copilot/configure-overrides-prompt.md index 565e5db4..838c58d1 100644 --- a/src/templates/copilot/configure-overrides-prompt.md +++ b/src/templates/copilot/configure-overrides-prompt.md @@ -72,18 +72,9 @@ Using the artifact directory identified in Step 1: > APIOps and do **not** need overrides. 2. For each environment, create a **stub** `configuration.{env}.yaml` that - covers all the commonly-overridden items you found. Use placeholder example - values so the user can see the shape of the file. - -3. **Add everything as YAML comments initially** — a commented-out section is - safe because it will not cause a publish failure. The user can uncomment - and fill in values in Step 3. Example pattern: - ```yaml - # namedValues: - # - name: payment-api-key - # properties: - # value: "{#[PAYMENT_API_KEY]#}" - ``` + covers all the commonly-overridden items you found. Use placeholder values + (e.g., `TODO` or `{#[TOKEN_NAME]#}`) so the user can see the shape of the + file and fill in real values in Step 3. --- diff --git a/src/templates/copilot/identity-setup-prompt.ts b/src/templates/copilot/identity-setup-prompt.ts index 164b82f5..41734811 100644 --- a/src/templates/copilot/identity-setup-prompt.ts +++ b/src/templates/copilot/identity-setup-prompt.ts @@ -16,7 +16,7 @@ import { copilotGithubEnvironmentSecretCommandsTemplate, copilotGitHubActionsIdentitySetupPromptTemplate, } from '../generated/embedded-markdown.js'; -import { renderTemplate } from '../shared/template-utils.js'; +import { renderTemplate } from '../../lib/render-template.js'; export interface IdentitySetupPromptConfig { environments: string[]; From 3476a45c6a2b48b25a8076a1cc800f5bdf84ec46 Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Tue, 23 Jun 2026 20:09:15 +0000 Subject: [PATCH 05/13] adding properties auto-complete for overrides. --- .devcontainer/devcontainer.json | 1 + schemas/override-config.schema.json | 556 +++++++++++++++++++++++++++- scripts/generate-schemas.mjs | 293 ++++++++++++++- 3 files changed, 832 insertions(+), 18 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 692e5be0..3367323b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -38,6 +38,7 @@ "vitest.dev", "ms-vscode.powershell", "ms-azuretools.vscode-bicep", + "redhat.vscode-yaml", "bierner.markdown-preview-github-styles" ] } diff --git a/schemas/override-config.schema.json b/schemas/override-config.schema.json index d48cd644..a76f916e 100644 --- a/schemas/override-config.schema.json +++ b/schemas/override-config.schema.json @@ -12,11 +12,11 @@ "description": "Optional schema URI for editor and IDE validation." }, "namedValues": { - "$ref": "#/definitions/overrideSection", + "$ref": "#/definitions/namedValueOverrideSection", "description": "Named value overrides. Use the properties object to deep-merge resource properties. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." }, "backends": { - "$ref": "#/definitions/overrideSection", + "$ref": "#/definitions/backendOverrideSection", "description": "Backend overrides. Use the properties object to deep-merge resource properties such as URLs or credentials. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." }, "apis": { @@ -24,15 +24,15 @@ "description": "API overrides. Each entry can override API properties and optionally define nested diagnostics, operations, policies, and releases. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." }, "diagnostics": { - "$ref": "#/definitions/overrideSection", + "$ref": "#/definitions/diagnosticOverrideSection", "description": "Diagnostics overrides. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." }, "loggers": { - "$ref": "#/definitions/overrideSection", + "$ref": "#/definitions/loggerOverrideSection", "description": "Loggers overrides. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." }, "policies": { - "$ref": "#/definitions/overrideSection", + "$ref": "#/definitions/policyOverrideSection", "description": "Service-level policies overrides. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." }, "gateways": { @@ -98,6 +98,126 @@ "$ref": "#/definitions/overrideEntry" } }, + "namedValueOverrideEntry": { + "type": "object", + "required": [ + "name", + "properties" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Named value name to match for this override entry." + }, + "properties": { + "$ref": "#/definitions/namedValuePropertiesObject" + } + } + }, + "namedValueOverrideSection": { + "type": "array", + "description": "A list of named value override entries.", + "items": { + "$ref": "#/definitions/namedValueOverrideEntry" + } + }, + "backendOverrideEntry": { + "type": "object", + "required": [ + "name", + "properties" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Backend name to match for this override entry." + }, + "properties": { + "$ref": "#/definitions/backendPropertiesObject" + } + } + }, + "backendOverrideSection": { + "type": "array", + "description": "A list of backend override entries.", + "items": { + "$ref": "#/definitions/backendOverrideEntry" + } + }, + "loggerOverrideEntry": { + "type": "object", + "required": [ + "name", + "properties" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Logger name to match for this override entry." + }, + "properties": { + "$ref": "#/definitions/loggerPropertiesObject" + } + } + }, + "loggerOverrideSection": { + "type": "array", + "description": "A list of logger override entries.", + "items": { + "$ref": "#/definitions/loggerOverrideEntry" + } + }, + "diagnosticOverrideEntry": { + "type": "object", + "required": [ + "name", + "properties" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Diagnostic name to match for this override entry." + }, + "properties": { + "$ref": "#/definitions/diagnosticPropertiesObject" + } + } + }, + "diagnosticOverrideSection": { + "type": "array", + "description": "A list of diagnostic override entries.", + "items": { + "$ref": "#/definitions/diagnosticOverrideEntry" + } + }, + "policyOverrideEntry": { + "type": "object", + "required": [ + "name", + "properties" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Policy name to match for this override entry." + }, + "properties": { + "$ref": "#/definitions/policyPropertiesObject" + } + } + }, + "policyOverrideSection": { + "type": "array", + "description": "A list of policy override entries.", + "items": { + "$ref": "#/definitions/policyOverrideEntry" + } + }, "operationOverrideEntry": { "type": "object", "required": [ @@ -114,7 +234,7 @@ "$ref": "#/definitions/propertiesObject" }, "policies": { - "$ref": "#/definitions/overrideSection", + "$ref": "#/definitions/policyOverrideSection", "description": "Policy overrides nested under this operation. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." } } @@ -139,10 +259,10 @@ "description": "API name to match for this override entry." }, "properties": { - "$ref": "#/definitions/propertiesObject" + "$ref": "#/definitions/apiPropertiesObject" }, "diagnostics": { - "$ref": "#/definitions/overrideSection", + "$ref": "#/definitions/diagnosticOverrideSection", "description": "Diagnostic overrides nested under this API. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." }, "operations": { @@ -150,7 +270,7 @@ "description": "Operation overrides nested under this API. Each operation can define its own nested policies. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." }, "policies": { - "$ref": "#/definitions/overrideSection", + "$ref": "#/definitions/policyOverrideSection", "description": "Policy overrides nested directly under this API. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution." }, "releases": { @@ -165,6 +285,424 @@ "items": { "$ref": "#/definitions/apiOverrideEntry" } + }, + "apiPropertiesObject": { + "type": "object", + "description": "Common API properties with editor autocomplete. Additional API properties are allowed.", + "properties": { + "displayName": { + "type": "string", + "description": "Friendly API display name in the APIM portal." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Optional API description." + }, + "path": { + "type": "string", + "description": "API URL suffix/path." + }, + "serviceUrl": { + "type": [ + "string", + "null" + ], + "description": "Backend service URL for this API." + }, + "apiType": { + "type": "string", + "description": "API kind used by APIM import/export logic.", + "enum": [ + "http", + "soap", + "graphql", + "websocket", + "odata", + "grpc", + "mcp", + "a2a" + ] + }, + "type": { + "type": "string", + "description": "Source API type from extracted API metadata. Use the same values as apiType.", + "enum": [ + "http", + "soap", + "graphql", + "websocket", + "odata", + "grpc", + "mcp", + "a2a" + ] + }, + "protocols": { + "type": "array", + "description": "Supported frontend protocols for this API.", + "items": { + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss" + ] + }, + "uniqueItems": true + }, + "subscriptionRequired": { + "type": "boolean", + "description": "Whether a subscription key is required to call this API." + }, + "subscriptionKeyParameterNames": { + "type": "object", + "description": "Custom subscription key header/query names.", + "properties": { + "header": { + "type": "string" + }, + "query": { + "type": "string" + } + }, + "additionalProperties": false + }, + "apiRevision": { + "type": "string", + "description": "API revision identifier." + }, + "apiRevisionDescription": { + "type": [ + "string", + "null" + ], + "description": "Description for the API revision." + }, + "apiVersion": { + "type": "string", + "description": "API version label." + }, + "isCurrent": { + "type": "boolean", + "description": "Marks this API revision as current." + }, + "apiVersionSetId": { + "type": "string", + "description": "Reference to the API version set resource." + }, + "format": { + "type": "string", + "description": "Specification format used for API import/export payloads.", + "enum": [ + "openapi", + "openapi+json", + "openapi-link", + "swagger-json", + "swagger-link", + "wsdl", + "wsdl-link", + "wadl-xml", + "wadl-link", + "graphql-link" + ] + }, + "value": { + "type": "string", + "description": "Inline specification content or URL depending on format." + }, + "authenticationSettings": { + "type": "object", + "description": "Authentication settings for backend authorization.", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "namedValuePropertiesObject": { + "type": "object", + "description": "Common named value properties with editor autocomplete. Additional named value properties are allowed.", + "properties": { + "displayName": { + "type": "string", + "description": "Friendly named value display name in the APIM portal." + }, + "value": { + "type": [ + "string", + "null" + ], + "description": "Literal named value content (avoid putting secrets directly in source control)." + }, + "secret": { + "type": "boolean", + "description": "Whether the named value is treated as a secret." + }, + "tags": { + "type": "array", + "description": "Named value tags.", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "keyVault": { + "type": "object", + "description": "Key Vault secret reference for this named value.", + "properties": { + "secretIdentifier": { + "type": "string", + "description": "Full Key Vault secret identifier URL." + }, + "identityClientId": { + "type": [ + "string", + "null" + ], + "description": "User-assigned managed identity client ID used to access the secret." + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "backendPropertiesObject": { + "type": "object", + "description": "Common backend properties with editor autocomplete. Additional backend properties are allowed.", + "properties": { + "title": { + "type": [ + "string", + "null" + ], + "description": "Optional backend title." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Optional backend description." + }, + "url": { + "type": [ + "string", + "null" + ], + "description": "Backend runtime URL." + }, + "protocol": { + "type": "string", + "description": "Backend protocol.", + "enum": [ + "http", + "soap" + ] + }, + "resourceId": { + "type": [ + "string", + "null" + ], + "description": "Linked Azure resource ID, when applicable." + }, + "credentials": { + "type": "object", + "description": "Backend authentication credentials object.", + "additionalProperties": true + }, + "proxy": { + "type": "object", + "description": "Proxy settings for backend connectivity.", + "additionalProperties": true + }, + "tls": { + "type": "object", + "description": "TLS settings for backend connectivity.", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "loggerPropertiesObject": { + "type": "object", + "description": "Common logger properties with editor autocomplete. Additional logger properties are allowed.", + "properties": { + "loggerType": { + "type": "string", + "description": "Logger type.", + "enum": [ + "applicationInsights", + "azureEventHub" + ] + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Optional logger description." + }, + "resourceId": { + "type": [ + "string", + "null" + ], + "description": "Linked Azure resource ID for the logger target." + }, + "isBuffered": { + "type": "boolean", + "description": "Whether messages are buffered before being sent." + }, + "credentials": { + "type": "object", + "description": "Logger credentials (for example instrumentationKey or connection string).", + "properties": { + "instrumentationKey": { + "type": "string", + "description": "Application Insights instrumentation key or named value reference." + }, + "name": { + "type": "string", + "description": "Event Hub name, where applicable." + }, + "connectionString": { + "type": "string", + "description": "Event Hub connection string or named value reference." + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "diagnosticPropertiesObject": { + "type": "object", + "description": "Common diagnostic properties with editor autocomplete. Additional diagnostic properties are allowed.", + "properties": { + "alwaysLog": { + "type": [ + "string", + "null" + ], + "description": "Diagnostic always-log behavior.", + "enum": [ + "allErrors", + "always", + null + ] + }, + "httpCorrelationProtocol": { + "type": [ + "string", + "null" + ], + "description": "HTTP correlation protocol for tracing.", + "enum": [ + "Legacy", + "W3C", + "None", + null + ] + }, + "logClientIp": { + "type": "boolean", + "description": "Whether client IP address is logged." + }, + "verbosity": { + "type": "string", + "description": "Diagnostic verbosity level.", + "enum": [ + "verbose", + "information", + "error", + "Verbose", + "Information", + "Error" + ] + }, + "loggerId": { + "type": [ + "string", + "null" + ], + "description": "Target logger ARM resource ID." + }, + "sampling": { + "type": "object", + "description": "Diagnostic sampling configuration.", + "properties": { + "samplingType": { + "type": "string", + "enum": [ + "fixed" + ] + }, + "percentage": { + "type": "number" + } + }, + "additionalProperties": true + }, + "frontend": { + "type": [ + "object", + "null" + ], + "description": "Frontend request/response diagnostic settings.", + "additionalProperties": true + }, + "backend": { + "type": [ + "object", + "null" + ], + "description": "Backend request/response diagnostic settings.", + "additionalProperties": true + }, + "largeLanguageModel": { + "type": [ + "object", + "null" + ], + "description": "LLM diagnostic settings.", + "additionalProperties": true + }, + "tags": { + "type": [ + "object", + "null" + ], + "description": "Diagnostic tags.", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "policyPropertiesObject": { + "type": "object", + "description": "Common policy properties with editor autocomplete. Additional policy properties are allowed.", + "properties": { + "format": { + "type": "string", + "description": "Policy content format.", + "enum": [ + "rawxml", + "rawxml-link", + "xml", + "xml-link" + ] + }, + "value": { + "type": "string", + "description": "Inline policy XML or linked value, depending on format." + } + }, + "additionalProperties": true } } } diff --git a/scripts/generate-schemas.mjs b/scripts/generate-schemas.mjs index 9a7f3f23..c5c41b45 100644 --- a/scripts/generate-schemas.mjs +++ b/scripts/generate-schemas.mjs @@ -243,15 +243,30 @@ function buildOverrideProperties() { $ref: '#/definitions/apiOverrideSection', description: `API overrides. Each entry can override API properties and optionally define nested diagnostics, operations, policies, and releases. ${tokenNote}`, }; + } else if (field === 'namedValues') { + props.namedValues = { + $ref: '#/definitions/namedValueOverrideSection', + description: `Named value overrides. Use the properties object to deep-merge resource properties. ${tokenNote}`, + }; } else if (field === 'backends') { props.backends = { - $ref: '#/definitions/overrideSection', + $ref: '#/definitions/backendOverrideSection', description: `Backend overrides. Use the properties object to deep-merge resource properties such as URLs or credentials. ${tokenNote}`, }; - } else if (field === 'namedValues') { - props.namedValues = { - $ref: '#/definitions/overrideSection', - description: `Named value overrides. Use the properties object to deep-merge resource properties. ${tokenNote}`, + } else if (field === 'loggers') { + props.loggers = { + $ref: '#/definitions/loggerOverrideSection', + description: `Loggers overrides. ${tokenNote}`, + }; + } else if (field === 'diagnostics') { + props.diagnostics = { + $ref: '#/definitions/diagnosticOverrideSection', + description: `Diagnostics overrides. ${tokenNote}`, + }; + } else if (field === 'policies') { + props.policies = { + $ref: '#/definitions/policyOverrideSection', + description: `Service-level policies overrides. ${tokenNote}`, }; } else { props[field] = { @@ -298,6 +313,88 @@ const overrideSchema = { description: 'A list of override entries for a resource type.', items: { $ref: '#/definitions/overrideEntry' }, }, + + // --- Named value typed section --- + namedValueOverrideEntry: { + type: 'object', + required: ['name', 'properties'], + additionalProperties: false, + properties: { + name: { type: 'string', description: 'Named value name to match for this override entry.' }, + properties: { $ref: '#/definitions/namedValuePropertiesObject' }, + }, + }, + namedValueOverrideSection: { + type: 'array', + description: 'A list of named value override entries.', + items: { $ref: '#/definitions/namedValueOverrideEntry' }, + }, + + // --- Backend typed section --- + backendOverrideEntry: { + type: 'object', + required: ['name', 'properties'], + additionalProperties: false, + properties: { + name: { type: 'string', description: 'Backend name to match for this override entry.' }, + properties: { $ref: '#/definitions/backendPropertiesObject' }, + }, + }, + backendOverrideSection: { + type: 'array', + description: 'A list of backend override entries.', + items: { $ref: '#/definitions/backendOverrideEntry' }, + }, + + // --- Logger typed section --- + loggerOverrideEntry: { + type: 'object', + required: ['name', 'properties'], + additionalProperties: false, + properties: { + name: { type: 'string', description: 'Logger name to match for this override entry.' }, + properties: { $ref: '#/definitions/loggerPropertiesObject' }, + }, + }, + loggerOverrideSection: { + type: 'array', + description: 'A list of logger override entries.', + items: { $ref: '#/definitions/loggerOverrideEntry' }, + }, + + // --- Diagnostic typed section --- + diagnosticOverrideEntry: { + type: 'object', + required: ['name', 'properties'], + additionalProperties: false, + properties: { + name: { type: 'string', description: 'Diagnostic name to match for this override entry.' }, + properties: { $ref: '#/definitions/diagnosticPropertiesObject' }, + }, + }, + diagnosticOverrideSection: { + type: 'array', + description: 'A list of diagnostic override entries.', + items: { $ref: '#/definitions/diagnosticOverrideEntry' }, + }, + + // --- Policy typed section --- + policyOverrideEntry: { + type: 'object', + required: ['name', 'properties'], + additionalProperties: false, + properties: { + name: { type: 'string', description: 'Policy name to match for this override entry.' }, + properties: { $ref: '#/definitions/policyPropertiesObject' }, + }, + }, + policyOverrideSection: { + type: 'array', + description: 'A list of policy override entries.', + items: { $ref: '#/definitions/policyOverrideEntry' }, + }, + + // --- Operation override --- operationOverrideEntry: { type: 'object', required: ['name', 'properties'], @@ -309,7 +406,7 @@ const overrideSchema = { }, properties: { $ref: '#/definitions/propertiesObject' }, policies: { - $ref: '#/definitions/overrideSection', + $ref: '#/definitions/policyOverrideSection', description: 'Policy overrides nested under this operation. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution.', }, @@ -320,6 +417,8 @@ const overrideSchema = { description: 'A list of operation override entries.', items: { $ref: '#/definitions/operationOverrideEntry' }, }, + + // --- API typed section --- apiOverrideEntry: { type: 'object', required: ['name', 'properties'], @@ -329,9 +428,9 @@ const overrideSchema = { type: 'string', description: 'API name to match for this override entry.', }, - properties: { $ref: '#/definitions/propertiesObject' }, + properties: { $ref: '#/definitions/apiPropertiesObject' }, diagnostics: { - $ref: '#/definitions/overrideSection', + $ref: '#/definitions/diagnosticOverrideSection', description: 'Diagnostic overrides nested under this API. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution.', }, @@ -341,7 +440,7 @@ const overrideSchema = { 'Operation overrides nested under this API. Each operation can define its own nested policies. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution.', }, policies: { - $ref: '#/definitions/overrideSection', + $ref: '#/definitions/policyOverrideSection', description: 'Policy overrides nested directly under this API. Values may include {#[TOKEN_NAME]#} placeholders for CI/CD secret token substitution.', }, @@ -357,6 +456,182 @@ const overrideSchema = { description: 'A list of API override entries.', items: { $ref: '#/definitions/apiOverrideEntry' }, }, + + // --- Typed properties objects --- + apiPropertiesObject: { + type: 'object', + description: 'Common API properties with editor autocomplete. Additional API properties are allowed.', + properties: { + displayName: { type: 'string', description: 'Friendly API display name in the APIM portal.' }, + description: { type: ['string', 'null'], description: 'Optional API description.' }, + path: { type: 'string', description: 'API URL suffix/path.' }, + serviceUrl: { type: ['string', 'null'], description: 'Backend service URL for this API.' }, + apiType: { + type: 'string', + description: 'API kind used by APIM import/export logic.', + enum: ['http', 'soap', 'graphql', 'websocket', 'odata', 'grpc', 'mcp', 'a2a'], + }, + type: { + type: 'string', + description: 'Source API type from extracted API metadata. Use the same values as apiType.', + enum: ['http', 'soap', 'graphql', 'websocket', 'odata', 'grpc', 'mcp', 'a2a'], + }, + protocols: { + type: 'array', + description: 'Supported frontend protocols for this API.', + items: { type: 'string', enum: ['http', 'https', 'ws', 'wss'] }, + uniqueItems: true, + }, + subscriptionRequired: { type: 'boolean', description: 'Whether a subscription key is required to call this API.' }, + subscriptionKeyParameterNames: { + type: 'object', + description: 'Custom subscription key header/query names.', + properties: { + header: { type: 'string' }, + query: { type: 'string' }, + }, + additionalProperties: false, + }, + apiRevision: { type: 'string', description: 'API revision identifier.' }, + apiRevisionDescription: { type: ['string', 'null'], description: 'Description for the API revision.' }, + apiVersion: { type: 'string', description: 'API version label.' }, + isCurrent: { type: 'boolean', description: 'Marks this API revision as current.' }, + apiVersionSetId: { type: 'string', description: 'Reference to the API version set resource.' }, + format: { + type: 'string', + description: 'Specification format used for API import/export payloads.', + enum: [ + 'openapi', 'openapi+json', 'openapi-link', + 'swagger-json', 'swagger-link', + 'wsdl', 'wsdl-link', + 'wadl-xml', 'wadl-link', + 'graphql-link', + ], + }, + value: { type: 'string', description: 'Inline specification content or URL depending on format.' }, + authenticationSettings: { + type: 'object', + description: 'Authentication settings for backend authorization.', + additionalProperties: true, + }, + }, + additionalProperties: true, + }, + + namedValuePropertiesObject: { + type: 'object', + description: 'Common named value properties with editor autocomplete. Additional named value properties are allowed.', + properties: { + displayName: { type: 'string', description: 'Friendly named value display name in the APIM portal.' }, + value: { type: ['string', 'null'], description: 'Literal named value content (avoid putting secrets directly in source control).' }, + secret: { type: 'boolean', description: 'Whether the named value is treated as a secret.' }, + tags: { type: 'array', description: 'Named value tags.', items: { type: 'string' }, uniqueItems: true }, + keyVault: { + type: 'object', + description: 'Key Vault secret reference for this named value.', + properties: { + secretIdentifier: { type: 'string', description: 'Full Key Vault secret identifier URL.' }, + identityClientId: { type: ['string', 'null'], description: 'User-assigned managed identity client ID used to access the secret.' }, + }, + additionalProperties: true, + }, + }, + additionalProperties: true, + }, + + backendPropertiesObject: { + type: 'object', + description: 'Common backend properties with editor autocomplete. Additional backend properties are allowed.', + properties: { + title: { type: ['string', 'null'], description: 'Optional backend title.' }, + description: { type: ['string', 'null'], description: 'Optional backend description.' }, + url: { type: ['string', 'null'], description: 'Backend runtime URL.' }, + protocol: { type: 'string', description: 'Backend protocol.', enum: ['http', 'soap'] }, + resourceId: { type: ['string', 'null'], description: 'Linked Azure resource ID, when applicable.' }, + credentials: { type: 'object', description: 'Backend authentication credentials object.', additionalProperties: true }, + proxy: { type: 'object', description: 'Proxy settings for backend connectivity.', additionalProperties: true }, + tls: { type: 'object', description: 'TLS settings for backend connectivity.', additionalProperties: true }, + }, + additionalProperties: true, + }, + + loggerPropertiesObject: { + type: 'object', + description: 'Common logger properties with editor autocomplete. Additional logger properties are allowed.', + properties: { + loggerType: { + type: 'string', + description: 'Logger type.', + enum: ['applicationInsights', 'azureEventHub'], + }, + description: { type: ['string', 'null'], description: 'Optional logger description.' }, + resourceId: { type: ['string', 'null'], description: 'Linked Azure resource ID for the logger target.' }, + isBuffered: { type: 'boolean', description: 'Whether messages are buffered before being sent.' }, + credentials: { + type: 'object', + description: 'Logger credentials (for example instrumentationKey or connection string).', + properties: { + instrumentationKey: { type: 'string', description: 'Application Insights instrumentation key or named value reference.' }, + name: { type: 'string', description: 'Event Hub name, where applicable.' }, + connectionString: { type: 'string', description: 'Event Hub connection string or named value reference.' }, + }, + additionalProperties: true, + }, + }, + additionalProperties: true, + }, + + diagnosticPropertiesObject: { + type: 'object', + description: 'Common diagnostic properties with editor autocomplete. Additional diagnostic properties are allowed.', + properties: { + alwaysLog: { + type: ['string', 'null'], + description: 'Diagnostic always-log behavior.', + enum: ['allErrors', 'always', null], + }, + httpCorrelationProtocol: { + type: ['string', 'null'], + description: 'HTTP correlation protocol for tracing.', + enum: ['Legacy', 'W3C', 'None', null], + }, + logClientIp: { type: 'boolean', description: 'Whether client IP address is logged.' }, + verbosity: { + type: 'string', + description: 'Diagnostic verbosity level.', + enum: ['verbose', 'information', 'error', 'Verbose', 'Information', 'Error'], + }, + loggerId: { type: ['string', 'null'], description: 'Target logger ARM resource ID.' }, + sampling: { + type: 'object', + description: 'Diagnostic sampling configuration.', + properties: { + samplingType: { type: 'string', enum: ['fixed'] }, + percentage: { type: 'number' }, + }, + additionalProperties: true, + }, + frontend: { type: ['object', 'null'], description: 'Frontend request/response diagnostic settings.', additionalProperties: true }, + backend: { type: ['object', 'null'], description: 'Backend request/response diagnostic settings.', additionalProperties: true }, + largeLanguageModel: { type: ['object', 'null'], description: 'LLM diagnostic settings.', additionalProperties: true }, + tags: { type: ['object', 'null'], description: 'Diagnostic tags.', additionalProperties: true }, + }, + additionalProperties: true, + }, + + policyPropertiesObject: { + type: 'object', + description: 'Common policy properties with editor autocomplete. Additional policy properties are allowed.', + properties: { + format: { + type: 'string', + description: 'Policy content format.', + enum: ['rawxml', 'rawxml-link', 'xml', 'xml-link'], + }, + value: { type: 'string', description: 'Inline policy XML or linked value, depending on format.' }, + }, + additionalProperties: true, + }, }, }; From 5c71970f147d3ff84ba85d4c4d2a9358dcd786b0 Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Thu, 25 Jun 2026 18:50:51 +0000 Subject: [PATCH 06/13] updating overrides prompt file --- .../copilot/configure-overrides-prompt.md | 289 +++++++++++++----- 1 file changed, 212 insertions(+), 77 deletions(-) diff --git a/src/templates/copilot/configure-overrides-prompt.md b/src/templates/copilot/configure-overrides-prompt.md index 838c58d1..258173e2 100644 --- a/src/templates/copilot/configure-overrides-prompt.md +++ b/src/templates/copilot/configure-overrides-prompt.md @@ -16,30 +16,60 @@ environment-specific settings. --- -## Step 0 — Detect Environments +## How Copilot must work through this prompt + +These rules apply to **every** step below. Follow them strictly: + +1. **Confirm before proceeding.** At the end of every step, summarize what you + learned or propose, then **STOP and wait for the user to confirm** before + moving to the next step. Never chain steps together without an explicit + "yes" / "go ahead" from the user. + - **Hard stop rule:** When you ask for confirmation, end the response there. + Do **not** include the next question, next override, or any forward action + in the same message. + - This hard stop applies to **step boundaries** (Step 0, Step 1, Step 2, + Step 3, Step 5, Step 6). In Step 4, follow the single-setting cadence below. +2. **Never assume a value.** Do not invent backend URLs, service URLs, + resource IDs, instrumentation keys, secret names, Key Vault URLs, or token + names. If you don't know a value, **ask the user**. +3. **Do not tokenize everything.** A `{#[TOKEN_NAME]#}` placeholder is only for + values the user explicitly wants injected by the pipeline (see Step 4 for + how to classify each value). Many values are plain, non-sensitive settings + that should be written literally. +4. **Ask, don't guess, about pipeline tokens.** Only use a token after the user + has told you that token exists (or will be added) in their pipeline. -Before asking the user anything, look for existing environment configuration -files in the repository: +--- + +## Step 0 — Detect and Confirm Environments + +Before asking the user anything else, look for existing environment +configuration files in the repository: 1. Search for files matching `configuration.*.yaml` (excluding `configuration.extractor.yaml`). The `*` portion is the environment name. 2. Also check CI/CD workflow files (`.github/workflows/` or `.azdo/pipelines/`) for environment references. -If existing config files are found, present the detected environments to the -user and ask them to confirm or update the list. +Then **present the detected environments to the user** and ask which ones they +want override files for: -If no config files are found, ask the user: +> "I found these environments: ``. Which of these do you want to create +> or update override files for? If you deploy to other environments I didn't +> detect, list them too." + +If no config files are found, ask: > "What environments do you deploy to? Common patterns include `dev, stage, prod` > or `stage, prod` (if dev shares the same APIM instance as stage)." -Once the environment list is confirmed, proceed. +**STOP. Do not proceed until the user has explicitly confirmed the exact list +of environments to work on.** --- ## Step 1 — Gather Information -Copilot, collect the following from the user: +Once the environment list is confirmed, collect the following: 1. **Existing override config files** — If `configuration.{env}.yaml` files already exist: @@ -47,23 +77,24 @@ Copilot, collect the following from the user: - Ask whether the user wants to update them or start fresh. 2. **APIM artifacts location** — Ask the user where the APIOps artifact - directory is (default: `./apim-artifacts`). You will need to inspect the - artifacts in the next step. + directory is (default: `./apim-artifacts`). You will inspect the artifacts + in the next step. -Summarize what you've learned before moving on. +Summarize what you've learned and **STOP for confirmation** before continuing. --- -## Step 2 — Investigate APIM Artifacts and Create Stub Override Files +## Step 2 — Investigate Artifacts and Create Stub Override Files -Using the artifact directory identified in Step 1: +Using the artifact directory confirmed in Step 1: -1. Scan the artifacts for references to **external resources** — these are the - things that typically need overrides between environments. Examples: - - Backend service URLs - - Named values (especially those referencing Key Vault secrets) - - Product subscription settings - - Logger resource IDs +1. Scan the artifacts for references to **external resources** — the things + that typically differ between environments. Examples: + - API `serviceUrl` values + - Backend service URLs and linked `resourceId`s + - Named values (secrets and plain config values) + - Logger `resourceId`s and credentials + - Diagnostic `loggerId` references - Gateway or VNet references - Policy fragment references to external endpoints @@ -71,80 +102,184 @@ Using the artifact directory identified in Step 1: > one API referencing another API's policy) are handled automatically by > APIOps and do **not** need overrides. -2. For each environment, create a **stub** `configuration.{env}.yaml` that - covers all the commonly-overridden items you found. Use placeholder values - (e.g., `TODO` or `{#[TOKEN_NAME]#}`) so the user can see the shape of the - file and fill in real values in Step 3. +2. Produce a **plain list of override candidates** grouped by resource type + (e.g., "APIs needing a serviceUrl: `src-graphql-passthrough`, + `src-rest-versioned-v1`…"). Do **not** decide yet which are tokens versus + literals — that happens in Step 3. + +3. Present this list and ask the user to confirm which items actually need + per-environment overrides and which can be left as-is. + +4. Once the candidate list is confirmed, **create the stub override files** — + one `configuration.{env}.yaml` per confirmed environment — containing every + confirmed candidate as an entry with the correct `name` and structure but + **blank values** (e.g., empty strings `""` or empty `properties`). This + shows the shape of each file; the actual values are filled in during + Step 4. Include the schema comment as the first line of each file: + `# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/override-config.schema.json` + +**STOP for confirmation before continuing to Step 3.** + +--- + +## Step 3 — Confirm Available Pipeline Tokens + +Before filling in any value, **ask the user** which environment variables / +pipeline variables are available for this environment, and get the **exact, +case-sensitive** token names. + +**Known `apiops init` tokens.** If the user scaffolded the repo with +`apiops init`, the generated publish pipeline already wires up a standard set +of pipeline variables / secrets that are usable as `{#[...]#}` tokens. You may +**ask** whether the user has these (substitute `` with the uppercased +environment name, e.g. `STAGE`): + +- `AZURE_SUBSCRIPTION_ID_` (e.g., `AZURE_SUBSCRIPTION_ID_STAGE`) +- `APIM_RESOURCE_GROUP_` (e.g., `APIM_RESOURCE_GROUP_STAGE`) +- `APIM_SERVICE_NAME_` (e.g., `APIM_SERVICE_NAME_STAGE`) +- GitHub Actions only: `AZURE_CLIENT_ID`, `AZURE_TENANT_ID` (used for login; + rarely needed inside override files) + +> Note: some repos use `AZURE_SUBSCRIPTION_ID` (global, no env suffix) as the +> default init-generated name. Others customize to `AZURE_SUBSCRIPTION_ID_`. +> Ask the user which naming pattern exists in their pipeline. + +Ask the user to **confirm which of these they actually have**, then ask whether +there are **any other** tokens: + +> "If you ran `apiops init`, you may already have these pipeline variables: +> `AZURE_SUBSCRIPTION_ID` (or `AZURE_SUBSCRIPTION_ID_STAGE`), +> `APIM_RESOURCE_GROUP_STAGE`, `APIM_SERVICE_NAME_STAGE`. +> Which of these do you have? And are there any other pipeline variables (with +> their exact, case-sensitive names) I should use as tokens?" + +> **Beyond the known `init` tokens, do NOT propose, guess, or pre-populate +> tokens.** Do not invent token names for secrets, URLs, or resource segments. +> Let the user tell you what else exists. + +Record this list of confirmed tokens. You may **only** use these token names +later. Never invent a token, and never wrap a value in `{#[TOKEN_NAME]#}` +unless its token is on this confirmed list. The publish step fails if a token +has no matching pipeline variable. + +**STOP and confirm the token list before continuing to Step 4.** --- -## Step 3 — Work With the User to Fill In Values - -Go through each environment one at a time. For each environment: - -1. **Pipeline environment variables (tokens)** — Ask the user whether - environment variables are available in the publish pipeline. Common ones: - - Subscription ID - - Resource group name - - APIM service instance name - - These can be added as `{#[TOKEN_NAME]#}` placeholders so the pipeline - substitutes the real value at runtime. This avoids hardcoding - environment-specific IDs in files committed to source control. - -2. **Shared values** — Sometimes a value does not need to differ by - environment (e.g., dev and stage may use the same Key Vault). Confirm with - the user before duplicating values. - -3. **Key Vault pattern** — A common pattern is for one Key Vault to hold all - secrets per environment (e.g., `https://{env}-kv.vault.azure.net/secrets/`). - Users often define a named-value token for the Key Vault secrets base URL and - then append the secret name — this avoids human error. For example: - ```yaml - namedValues: - - name: kv-base-url - properties: - value: "{#[KV_BASE_URL]#}" - - name: db-connection-string - properties: - keyVault: - secretIdentifier: "{#[KV_BASE_URL]#}db-conn" - identityClientId: "{#[MANAGED_IDENTITY_CLIENT_ID]#}" - ``` - -4. For values that must remain secret (API keys, connection strings): - - Use **`{#[TOKEN_NAME]#}`** for pipeline-injected secrets. - - Use `keyVault.secretIdentifier` for Azure Key Vault-managed secrets. - - Use plain values only for non-sensitive settings like URLs or feature - flags that are safe to commit. - -Uncomment and populate each stub entry as the user provides or confirms values. +## Step 4 — Fill In Each Override With the User + +Walk through the stub entries created in Step 2 **one setting/property at a +time** (for example one `resourceId` or one `value` field), not one whole +override object at a time. Do not batch multiple settings in a single prompt. + +**Single-setting cadence for Step 4:** + +- Ask for exactly one setting when information is missing. +- If the user provides that setting unambiguously, write it immediately. +- After writing it, proceed by asking for the next single missing setting. +- Only pause for confirmation when the user explicitly asks for confirmation, + or when the value is ambiguous and you need clarification. +- Do not ask the user to reconfirm a setting they just provided unless there is + a concrete ambiguity. + +For each override value, classify how it should be supplied using the confirmed +token list from Step 3. There are three kinds of values — do not default to +tokens: + +| Kind | When to use | How it's written | +| --- | --- | --- | +| **Literal value** | Non-sensitive settings that are safe to commit — API/backend URLs, resource IDs, Application Insights logger resource IDs, **Application Insights instrumentation keys** (telemetry ingestion keys, **not secrets**), feature flags. | Plain YAML value, e.g. `url: "https://api.contoso.com"` | +| **Pipeline token** | Secrets or values the user wants injected at publish time from the pipeline's secret store (GitHub Actions secrets / Azure DevOps variable groups). | `value: "{#[TOKEN_NAME]#}"` — only use a token the user confirms exists | +| **Key Vault reference** | Secrets stored centrally in Azure Key Vault and referenced by named values. | A `keyVault.secretIdentifier` URL (see pattern below) | + +For each candidate value, ask the user something like: + +> "For `.`, what is the value in ****? Is it a fixed +> value I can write directly, a secret your pipeline injects via a token, or a +> Key Vault secret?" + +Concrete guidance to follow while classifying: + +- **API service URLs and backend URLs** — Ask the user for the actual URL per + environment. These are usually plain literal values, **not tokens**, unless + the user specifically wants them injected by the pipeline. +- **Application Insights instrumentation keys** — These are **not secrets**. + Write the value the user provides directly, or leave the extracted value in + place. Do **not** wrap them in `{#[TOKEN_NAME]#}` unless the user asks. +- **Resource IDs (loggers, backends, diagnostics)** — Usually literal values. + Only the subscription ID / resource group / service name segments need + tokenizing if the user wants them injected; ask first. +- **Connection strings, API keys, passwords** — These are secrets. Use a + pipeline token or a Key Vault reference based on the user's preference. + +### Key Vault reference — the correct pattern + +A Key Vault-backed named value uses a **`keyVault.secretIdentifier`** that is a +**full secret URL**. Do **not** create a separate named value just to hold a +Key Vault base URL, and do **not** concatenate a token with a secret name. + +Correct — literal full secret identifier: + +```yaml +namedValues: + - name: db-connection-string + properties: + keyVault: + secretIdentifier: "https://prod-kv.vault.azure.net/secrets/db-conn" + identityClientId: "{#[MANAGED_IDENTITY_CLIENT_ID]#}" +``` + +Also acceptable — tokenize the whole secret identifier when the user wants the +pipeline to supply it: + +```yaml +namedValues: + - name: db-connection-string + properties: + keyVault: + secretIdentifier: "{#[DB_CONN_SECRET_IDENTIFIER]#}" + identityClientId: "{#[MANAGED_IDENTITY_CLIENT_ID]#}" +``` + +As you fill in each override, write it into the stub file using the right form: + +- Write literal values directly; use `{#[TOKEN_NAME]#}` only for confirmed + tokens; use full `keyVault.secretIdentifier` URLs for Key Vault secrets. +- Never commit real secret values — those must be tokens or Key Vault + references. + +Continue setting-by-setting until there are no missing values. --- -## Step 4 — Generate the Override Files +## Step 5 — Finalize and Review the Override Files + +Once every stub override has been filled in across all environments: -Once all values are confirmed, produce the final YAML files: +- Re-read each `configuration.{env}.yaml` file and confirm it is valid YAML + with no leftover blank values from the stubs. +- Confirm the schema comment is present as the first line of each file. +- Keep files easy to compare across environments and avoid duplicating + unchanged base configuration. -- Output valid YAML for each file. -- Include the schema comment at the top of each file: - `# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/override-config.schema.json` -- Keep files easy to compare across environments. -- Use `{#[TOKEN_NAME]#}` placeholders for secrets (never commit real secret values). -- Use Key Vault references for centrally-managed secrets. -- Avoid duplicating unchanged base configuration. +Show the finalized files and **STOP for confirmation** before treating them as +final. --- -## Step 5 — Validate the Promotion Model +## Step 6 — Validate the Promotion Model Before finishing: 1. Verify every generated override file matches the intended environment. -2. Verify all secrets use either `{#[TOKEN_NAME]#}` or Key Vault references. +2. Verify all **secrets** use either `{#[TOKEN_NAME]#}` or a Key Vault + reference — and that non-secrets (URLs, resource IDs, instrumentation keys) + are written as plain values, not tokens. +3. Confirm every `{#[TOKEN_NAME]#}` used corresponds to a token the user said + exists in their pipeline. 4. Remind the user to add any `{#[TOKEN_NAME]#}` tokens to their pipeline's secret store (GitHub Actions Secrets or Azure DevOps variable groups). - Help the user with this step if they ask. Note that the pipeline will fail - with an error if any tokens are missed. + Help with this if they ask. Note that the pipeline fails with an error if + any tokens are missing. 5. Remind the user to test publish for a lower environment before promoting further. From 032484c9936abd53df0ac510db1c5111df332c2f Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Thu, 25 Jun 2026 20:14:29 +0000 Subject: [PATCH 07/13] updating prompt files --- .../copilot/configure-filter-prompt.md | 192 ++++++++++++++---- .../copilot/configure-overrides-prompt.md | 18 +- 2 files changed, 165 insertions(+), 45 deletions(-) diff --git a/src/templates/copilot/configure-filter-prompt.md b/src/templates/copilot/configure-filter-prompt.md index d1cf3eef..c29ba6d9 100644 --- a/src/templates/copilot/configure-filter-prompt.md +++ b/src/templates/copilot/configure-filter-prompt.md @@ -16,64 +16,169 @@ the Azure API Management resources your team wants to manage in source control. --- -## Step 1 — Gather Requirements - -Copilot, for each APIM resource type that has a corresponding info or metadata -file in the artifact directory, ask the user whether they want to: - -- **Extract ALL** — include every resource of this type (omit this type from - the filter; APIOps extracts everything by default) -- **Extract NONE** — exclude all resources of this type (create a filter with - an empty array for this type) -- **Extract SOME** — include only specific resources (help them build the - filter for that type, asking which names to include or exclude) - -Resource types to ask about (ask only for types that appear to exist in the -artifact directory or that the user mentions): - -- APIs -- Products -- Named values -- Backends -- Loggers -- Gateways -- Tags -- Version sets -- Policy fragments -- Subscriptions - -Summarize the answers before generating any YAML. +## How Copilot must work through this prompt + +These rules apply to **every** step below. Follow them strictly: + +1. **Confirm before proceeding.** At the end of every step, summarize what you + learned or propose, then **STOP and wait for the user to confirm** before + moving to the next step. Never chain steps together without an explicit + "yes" / "go ahead" from the user. + - **Hard stop rule:** When you ask for confirmation, end the response there. + Do **not** include the next question, next resource type, or any forward + action in the same message. + - This hard stop applies to **step boundaries** (Step 0, Step 2, Step 3, + Step 4). In Step 1, follow the single-resource-type cadence below. +2. **Never assume or invent names.** Do not invent API, product, backend, + named value, or any other resource names. Use only names that come from the + live APIM instance or that the user explicitly provides. The local artifact + directory is not authoritative — it may be stale or empty. When unsure, ask. +3. **Default is extract-everything.** APIOps extracts **all** resources of a + type when that type is **omitted** from the filter. Only add a type to the + filter when the user wants to narrow it (SOME) or exclude it (NONE). Do not + add a type just to list every resource. +4. **Empty array means exclude all.** Setting a type to `[]` excludes every + resource of that type. Use this only when the user explicitly wants NONE. +5. **The JSON schema is the source of structure.** To determine which resource + types support sub-entries and what those sub-entries are (for example, + `apis` → `operations`, `diagnostics`, `schemas`, `releases`), consult the + `extractor-config` JSON schema referenced in the file's + `# yaml-language-server: $schema=...` comment (the public schema URL). + +--- + +## Step 0 — Determine the Authoritative Resource List + +The filter runs at **extraction time against the live Azure API Management +instance**. The local artifact directory may be stale, partial, or empty, so it +is **not** an authoritative list of what exists in Azure. Establish the source +of truth first: + +1. **Prefer querying the live APIM instance.** Ask the user for (or reuse if + already known) the subscription ID, resource group, and APIM service name, + and whether the Azure CLI is logged in. If Azure is reachable, enumerate the + resource types and names directly from the instance (for example with + `az apim` / `az rest` calls) and use that as the source of truth. +2. **Fallback when Azure cannot be queried.** Do **not** treat the local + artifacts as the definitive list. Instead, in Step 1 ask the user + type-by-type; for SOME, the user must provide the resource (and + sub-resource) names themselves. +3. Check whether `configuration.extractor.yaml` already exists (it may have + been created by `apiops init`). If it exists, note its current contents — + you will update it in place rather than overwriting it. + +Tell the user which mode you will use (live-Azure list vs. user-provided +names), and confirm the connection details if querying Azure. + +**STOP. Do not proceed until the user confirms the source of truth.** + +--- + +## Step 1 — Decide Scope Per Resource Type (one type at a time) + +Walk through the resource types **one type at a time**. For each type, ask the +user which scope they want: + +- **Extract ALL** — include every resource of this type. Leave this type + **out** of the filter (APIOps extracts everything by default). +- **Extract NONE** — exclude all resources of this type. Add the type with an + empty array: `tags: []`. +- **Extract SOME** — include only specific resources. The user provides which + names (or wildcard patterns) to include. Matching is case-insensitive and + supports `*` and `?` wildcards. + +**Single-resource-type cadence for Step 1:** + +- Ask about exactly **one** resource type at a time. Do not batch multiple + types into one prompt. +- **Ask ALL / NONE / SOME first.** Do **not** enumerate or query any names up + front. For ALL or NONE, record the answer and move on — no enumeration is + needed. +- **Only when the user answers SOME**, then gather names: + - If you can query the live APIM instance, list that type's names from Azure + to help the user choose. + - Otherwise, ask the user to provide the names/patterns. Do **not** invent + names or pull them from the local artifacts. +- When the user answers a type unambiguously, record the decision and move to + the next type. +- **Update `configuration.extractor.yaml` immediately after each decision that + affects the file** (SOME or NONE adds/updates that type's section; ALL means + no change since the type is omitted). Keep the file in sync as you go rather + than waiting until the end. +- Only pause for clarification when the answer is ambiguous. + +Resource types to consider (ask only about types that exist in the +instance/artifacts or that the user mentions): + +`apis`, `products`, `namedValues`, `backends`, `loggers`, `diagnostics`, +`tags`, `versionSets`, `policyFragments`, `gateways`, `groups`, +`subscriptions`, `schemas`, `policies`, `workspaces`. + +> **APIs can be filtered at the sub-resource level.** Whenever `apis` is SOME +> and specific API names are listed in the filter, **ask about each listed +> API's sub-resources** — `operations`, `diagnostics`, `schemas`, and +> `releases`. The user may want everything for that API, or only a subset +> (for example, a single revision or release). Omit a sub-filter to include +> all of that sub-type; set it to `[]` to exclude all. + +> **Workspaces apply only if the APIM instance uses workspaces.** Skip this +> type entirely if there are no workspaces. When a user wants SOME for +> `workspaces`, each workspace can also be narrowed by its own sub-resources +> (`apis`, `backends`, `diagnostics`, `groups`, `loggers`, `namedValues`, +> `policyFragments`, `products`, `schemas`, `subscriptions`, `tags`, +> `versionSets`). Omit a sub-filter to include all of that sub-type; set it to +> `[]` to exclude all. Only offer this depth if the user wants it. + +> **Service-level `policies` is effectively a single global policy.** For this +> type, ask only **include (ALL)** or **exclude (NONE)** — SOME does not apply. + +After all types are decided, summarize the per-type decisions and **STOP for +confirmation** before generating YAML. --- ## Step 2 — Propose a Filter Strategy -Based on the user's answers: +Based on the recorded decisions: 1. Recommend the smallest filter that safely captures the intended scope -2. Explain any tradeoffs between broad and narrow filters -3. Call out any risk of accidentally excluding required dependencies + (remember: omitted types are fully extracted, so only NONE/SOME types + appear in the file). +2. Explain any tradeoffs between broad and narrow filters. +3. Call out any risk of accidentally excluding required dependencies — for + example, excluding a named value or backend that an included API's policy + references. -If the user is unsure, start with a conservative filter that is easy to refine. +If the user is unsure, recommend a conservative filter that is easy to refine, +then **STOP for confirmation**. --- ## Step 3 — Generate `configuration.extractor.yaml` > **Note:** The file `configuration.extractor.yaml` may already exist if the -> user ran `apiops init`. Check for its presence before creating a new one — -> if it exists, update it in place rather than overwriting it. +> user ran `apiops init`. If it exists, **update it in place** rather than +> overwriting unrelated content. -Create the full YAML file content for `configuration.extractor.yaml`. +Create the YAML file content reflecting the confirmed decisions. Requirements: -- Include the schema comment at the top of the file: - `# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/extractor-config.schema.json` -- Output valid YAML only when generating the final file -- Preserve any APIOps-supported filter structure the user requests -- Prefer readable comments only when they help explain a non-obvious choice -- Do not invent resource names — ask the user or use placeholders when needed +- **Preserve the existing schema comment.** If the file already has a + `# yaml-language-server: $schema=...` line (as `apiops init` generates), keep + it **exactly as-is** — it already points at the correct schema version. Only + if the file has **no** schema comment, add one referencing the current schema + version: + `# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/v1/extractor-config.schema.json` +- Output valid YAML. +- Only include resource types the user chose to narrow (SOME) or exclude + (NONE). Leave ALL types out of the file. +- Use only names/patterns that exist in the artifacts or that the user + provided — do not invent names. +- Add a short comment only when it explains a non-obvious choice. + +Show the generated file and **STOP for confirmation** before treating it as +final. --- @@ -81,9 +186,10 @@ Requirements: Before finishing: -1. Review the generated YAML for syntax issues -2. Confirm the filters align with the user's intended extraction scope -3. Remind the user to run the extractor and inspect the artifact output +1. Review the generated YAML for syntax issues and schema validity. +2. Confirm the filters align with the user's intended extraction scope, and + that no type the user wanted is accidentally excluded or fully extracted. +3. Remind the user to run the extractor and inspect the artifact output. If the extractor output is too broad or too narrow, help the user refine the filter file iteratively. diff --git a/src/templates/copilot/configure-overrides-prompt.md b/src/templates/copilot/configure-overrides-prompt.md index 258173e2..7bbdedd8 100644 --- a/src/templates/copilot/configure-overrides-prompt.md +++ b/src/templates/copilot/configure-overrides-prompt.md @@ -38,6 +38,13 @@ These rules apply to **every** step below. Follow them strictly: that should be written literally. 4. **Ask, don't guess, about pipeline tokens.** Only use a token after the user has told you that token exists (or will be added) in their pipeline. +5. **The JSON schema is the source of structure.** To determine the valid + shape of an override entry and its nested properties (for example a Key + Vault named value's `keyVault.secretIdentifier` / `identityClientId`), + consult the `override-config` JSON schema referenced in each file's + `# yaml-language-server: $schema=...` comment (the public schema URL). Do + **not** rely on the `apiops-cli` source repository — end users only have + the built npm package and the published schema URL. --- @@ -97,6 +104,9 @@ Using the artifact directory confirmed in Step 1: - Diagnostic `loggerId` references - Gateway or VNet references - Policy fragment references to external endpoints + - Workspace-scoped resources (only if the APIM instance uses **workspaces**) + — workspaces can contain their own APIs, backends, named values, loggers, + etc. that may need per-environment overrides > **Note:** References to sub-resources of the same APIM instance (e.g., > one API referencing another API's policy) are handled automatically by @@ -115,8 +125,12 @@ Using the artifact directory confirmed in Step 1: confirmed candidate as an entry with the correct `name` and structure but **blank values** (e.g., empty strings `""` or empty `properties`). This shows the shape of each file; the actual values are filled in during - Step 4. Include the schema comment as the first line of each file: - `# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/override-config.schema.json` + Step 4. **Preserve any existing schema comment.** If a file already has a + `# yaml-language-server: $schema=...` line (as `apiops init` generates), + keep it **exactly as-is** — it already points at the correct schema version. + Only when creating a brand-new file with no schema comment, add one + referencing the current schema version: + `# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/v1/override-config.schema.json` **STOP for confirmation before continuing to Step 3.** From a3d497b2d7fc2a13e957c35f8dad95491168964a Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Thu, 25 Jun 2026 20:16:18 +0000 Subject: [PATCH 08/13] versioning schema files --- docs/guides/environment-overrides.md | 6 +- docs/guides/filtering-resources.md | 4 +- package.json | 1 + schemas/{ => v1}/extractor-config.schema.json | 4 +- schemas/{ => v1}/override-config.schema.json | 4 +- scripts/embed-markdown-templates.mjs | 8 ++ scripts/generate-schemas.mjs | 23 +++- src/templates/configs/filter-config.ts | 126 +----------------- src/templates/configs/filter-config.yaml | 118 ++++++++++++++++ src/templates/configs/override-config.ts | 101 ++------------ src/templates/configs/override-config.yaml | 92 +++++++++++++ src/templates/configs/schema-ref.ts | 28 ++++ .../package-build/package-build.test.ts | 14 +- .../configs/config-templates.test.ts | 23 ++++ .../configure-overrides-prompt.test.ts | 2 +- 15 files changed, 320 insertions(+), 234 deletions(-) rename schemas/{ => v1}/extractor-config.schema.json (98%) rename schemas/{ => v1}/override-config.schema.json (99%) create mode 100644 src/templates/configs/filter-config.yaml create mode 100644 src/templates/configs/override-config.yaml create mode 100644 src/templates/configs/schema-ref.ts diff --git a/docs/guides/environment-overrides.md b/docs/guides/environment-overrides.md index d1266be6..3b495a2a 100644 --- a/docs/guides/environment-overrides.md +++ b/docs/guides/environment-overrides.md @@ -28,10 +28,10 @@ apiops publish \ Add this comment as the first line of your override file to enable autocomplete in VS Code and other YAML-aware editors: ```yaml -# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/override-config.schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/v1/override-config.schema.json ``` -The schema validates section names, entry structure, nested sub-resource overrides, and supports `{#[TOKEN_NAME]#}` placeholder values. It is published at [`schemas/override-config.schema.json`](https://github.com/Azure/apiops-cli/blob/main/schemas/override-config.schema.json). +The schema validates section names, entry structure, nested sub-resource overrides, and supports `{#[TOKEN_NAME]#}` placeholder values. It is published at [`schemas/v1/override-config.schema.json`](https://github.com/Azure/apiops-cli/blob/main/schemas/v1/override-config.schema.json). ## Override file format (APIOps Toolkit-compatible) @@ -504,7 +504,7 @@ If you ran `apiops init`, a Copilot prompt file was generated at `.github/prompt A JSON Schema is available for `configuration.{env}.yaml` override files. Add this comment at the top of your override file to enable autocompletion in VS Code (with the YAML extension): ```yaml -# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/override-config.schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/v1/override-config.schema.json ``` The schema provides: diff --git a/docs/guides/filtering-resources.md b/docs/guides/filtering-resources.md index 0135eb07..df45976e 100644 --- a/docs/guides/filtering-resources.md +++ b/docs/guides/filtering-resources.md @@ -41,10 +41,10 @@ Only `petstore-api`, `orders-api`, and their transitive dependencies are extract Add this comment as the first line of your filter file to enable autocomplete in VS Code and other YAML-aware editors: ```yaml -# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/extractor-config.schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/v1/extractor-config.schema.json ``` -The schema validates field names, array structure, and sub-resource filters. It is published at [`schemas/extractor-config.schema.json`](https://github.com/Azure/apiops-cli/blob/main/schemas/extractor-config.schema.json). +The schema validates field names, array structure, and sub-resource filters. It is published at [`schemas/v1/extractor-config.schema.json`](https://github.com/Azure/apiops-cli/blob/main/schemas/v1/extractor-config.schema.json). --- diff --git a/package.json b/package.json index e7fda1d7..0e11ff0d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "@peterhauge/apiops-cli", "version": "0.2.1-alpha.0", + "schemaVersion": "1", "description": "CLI tool for Azure API Management configuration-as-code", "type": "module", "private": false, diff --git a/schemas/extractor-config.schema.json b/schemas/v1/extractor-config.schema.json similarity index 98% rename from schemas/extractor-config.schema.json rename to schemas/v1/extractor-config.schema.json index 29fd34c3..1ea17a9b 100644 --- a/schemas/extractor-config.schema.json +++ b/schemas/v1/extractor-config.schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$comment": "Copyright (c) Microsoft Corporation. Licensed under the MIT license.", - "$id": "https://github.com/Azure/apiops-cli/schemas/extractor-config.schema.json", + "$comment": "Copyright (c) Microsoft Corporation. Licensed under the MIT license. Schema version: v1.", + "$id": "https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/v1/extractor-config.schema.json", "title": "APIOps Filter Configuration", "description": "Validates configuration.extractor.yaml files used by APIOps CLI to select which Azure API Management resources are extracted. All resource sections are optional.", "type": "object", diff --git a/schemas/override-config.schema.json b/schemas/v1/override-config.schema.json similarity index 99% rename from schemas/override-config.schema.json rename to schemas/v1/override-config.schema.json index a76f916e..b701c43d 100644 --- a/schemas/override-config.schema.json +++ b/schemas/v1/override-config.schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$comment": "Copyright (c) Microsoft Corporation. Licensed under the MIT license.", - "$id": "https://github.com/Azure/apiops-cli/schemas/override-config.schema.json", + "$comment": "Copyright (c) Microsoft Corporation. Licensed under the MIT license. Schema version: v1.", + "$id": "https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/v1/override-config.schema.json", "title": "APIOps Override Configuration", "description": "Validates configuration.{env}.yaml override files used by APIOps CLI to apply environment-specific property overrides during publish. All resource sections are optional.", "type": "object", diff --git a/scripts/embed-markdown-templates.mjs b/scripts/embed-markdown-templates.mjs index a32fba29..9f8af03f 100644 --- a/scripts/embed-markdown-templates.mjs +++ b/scripts/embed-markdown-templates.mjs @@ -41,6 +41,14 @@ const templates = [ exportName: 'copilotGithubEnvironmentSecretCommandsTemplate', path: 'src/templates/shared/github-environment-secret-commands.md', }, + { + exportName: 'filterConfigTemplate', + path: 'src/templates/configs/filter-config.yaml', + }, + { + exportName: 'overrideConfigTemplate', + path: 'src/templates/configs/override-config.yaml', + }, ]; const rendered = await Promise.all( diff --git a/scripts/generate-schemas.mjs b/scripts/generate-schemas.mjs index c5c41b45..dc87d9c8 100644 --- a/scripts/generate-schemas.mjs +++ b/scripts/generate-schemas.mjs @@ -16,7 +16,19 @@ import { resolve } from 'node:path'; const repoRoot = resolve(import.meta.dirname, '..'); const configPath = resolve(repoRoot, 'src/models/config.ts'); -const schemasDir = resolve(repoRoot, 'schemas'); + +// Schemas are versioned independently of the CLI package version. Each schema +// version lives at a frozen path (schemas/v/...) on the `main` branch: +// backward-compatible edits update the current version in place, while a +// breaking change introduces a new version folder. The `main` ref always +// resolves, and the versioned path keeps existing configs pointing at the +// schema shape they were written against. +const pkg = JSON.parse(await readFile(resolve(repoRoot, 'package.json'), 'utf8')); +const schemaVersion = pkg.schemaVersion ?? '1'; +const schemaDirName = `v${schemaVersion}`; +const schemasDir = resolve(repoRoot, 'schemas', schemaDirName); +const SCHEMA_BASE = 'https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas'; +const schemaId = (fileName) => `${SCHEMA_BASE}/${schemaDirName}/${fileName}`; // Read the config.ts source to extract interface fields const configSource = await readFile(configPath, 'utf8'); @@ -150,11 +162,12 @@ function buildWorkspaceSubFilterProperties() { } const LICENSE_COMMENT = 'Copyright (c) Microsoft Corporation. Licensed under the MIT license.'; +const versionComment = `${LICENSE_COMMENT} Schema version: ${schemaDirName}.`; const extractorSchema = { $schema: 'http://json-schema.org/draft-07/schema#', - $comment: LICENSE_COMMENT, - $id: 'https://github.com/Azure/apiops-cli/schemas/extractor-config.schema.json', + $comment: versionComment, + $id: schemaId('extractor-config.schema.json'), title: 'APIOps Filter Configuration', description: 'Validates configuration.extractor.yaml files used by APIOps CLI to select which Azure API Management resources are extracted. All resource sections are optional.', @@ -281,8 +294,8 @@ function buildOverrideProperties() { const overrideSchema = { $schema: 'http://json-schema.org/draft-07/schema#', - $comment: LICENSE_COMMENT, - $id: 'https://github.com/Azure/apiops-cli/schemas/override-config.schema.json', + $comment: versionComment, + $id: schemaId('override-config.schema.json'), title: 'APIOps Override Configuration', description: 'Validates configuration.{env}.yaml override files used by APIOps CLI to apply environment-specific property overrides during publish. All resource sections are optional.', diff --git a/src/templates/configs/filter-config.ts b/src/templates/configs/filter-config.ts index e2f7df59..96f62fb0 100644 --- a/src/templates/configs/filter-config.ts +++ b/src/templates/configs/filter-config.ts @@ -5,124 +5,12 @@ * Generates a sample configuration.extractor.yaml file */ -export function generateFilterConfig(): string { - return `# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/extractor-config.schema.json -# APIM Extract Filter Configuration -# Customize this file to control which resources are extracted -# For full format details and examples, see: -# https://github.com/Azure/apiops-cli/blob/main/docs/guides/filtering-resources.md - -# Extract only specific APIs by name (or wildcard pattern) -# apis: -# - echo-api -# - petstore-api -# - 'prod-*' # Wildcard: all APIs starting with prod- -# - '*-internal-*' # Wildcard: all APIs containing -internal- - -# Advanced: Filter API sub-resources (operations, diagnostics, schemas, releases) -# apis: -# - echo-api # Include all sub-resources -# - petstore-api: # Control sub-resources -# operations: -# - get-pets -# - create-pet -# - 'list-*' # Wildcard: all operations starting with list- -# diagnostics: -# - applicationinsights -# schemas: [] # Exclude all schemas -# releases: -# - v1 - -# Extract only specific products -# products: -# - starter -# - unlimited - -# Extract only specific backends -# backends: -# - backend-api -# - legacy-backend - -# Extract only specific named values -# namedValues: -# - api-key -# - connection-string - -# Extract only specific loggers -# loggers: -# - appinsights-logger - -# Extract only specific diagnostics -# diagnostics: -# - applicationinsights - -# Extract only specific tags -# tags: -# - production -# - external - -# Extract only specific policy fragments -# policyFragments: -# - rate-limit-fragment -# - cors-fragment - -# Extract only specific gateways -# gateways: -# - default -# - internal-gateway +import { filterConfigTemplate } from '../generated/embedded-markdown.js'; +import { renderTemplate } from '../../lib/render-template.js'; +import { schemaUrl } from './schema-ref.js'; -# Extract only specific version sets -# versionSets: -# - payments-v1 - -# Extract only specific groups -# groups: -# - administrators - -# Extract only specific subscriptions -# subscriptions: -# - starter-subscription - -# Extract only specific schemas -# schemas: -# - pet-schema - -# Filter service-level policies -# policies: -# - policy - -# Extract only specific policy restrictions -# policyRestrictions: -# - global-policy-restriction - -# Extract only specific documentations -# documentations: -# - getting-started - -# Extract only specific workspaces -# workspaces: -# - dev-workspace - -# Advanced: Filter workspace sub-resources -# workspaces: -# - team-workspace: -# apis: -# - team-api-1 -# - team-api-2 -# backends: -# - team-backend -# namedValues: -# - team-api-key - -# Filter behavior: -# - Leave a section commented out to include ALL resources of that type -# - Set a section to an empty array ([]) to exclude ALL resources of that type -# Example: -# gateways: [] -# subscriptions: [] -# - Use * to match any characters: prod-* matches prod-api, prod-users -# - Use ? to match a single character: api-v? matches api-v1, api-v2 -# - Exact names and wildcard patterns can be mixed in the same list -# - All matching is case-insensitive -`; +export function generateFilterConfig(): string { + return renderTemplate(filterConfigTemplate, { + SCHEMA_URL: schemaUrl('extractor-config.schema.json'), + }); } diff --git a/src/templates/configs/filter-config.yaml b/src/templates/configs/filter-config.yaml new file mode 100644 index 00000000..7a935ebd --- /dev/null +++ b/src/templates/configs/filter-config.yaml @@ -0,0 +1,118 @@ +# yaml-language-server: $schema={{SCHEMA_URL}} +# APIM Extract Filter Configuration +# Customize this file to control which resources are extracted +# For full format details and examples, see: +# https://github.com/Azure/apiops-cli/blob/main/docs/guides/filtering-resources.md + +# Extract only specific APIs by name (or wildcard pattern) +# apis: +# - echo-api +# - petstore-api +# - 'prod-*' # Wildcard: all APIs starting with prod- +# - '*-internal-*' # Wildcard: all APIs containing -internal- + +# Advanced: Filter API sub-resources (operations, diagnostics, schemas, releases) +# apis: +# - echo-api # Include all sub-resources +# - petstore-api: # Control sub-resources +# operations: +# - get-pets +# - create-pet +# - 'list-*' # Wildcard: all operations starting with list- +# diagnostics: +# - applicationinsights +# schemas: [] # Exclude all schemas +# releases: +# - v1 + +# Extract only specific products +# products: +# - starter +# - unlimited + +# Extract only specific backends +# backends: +# - backend-api +# - legacy-backend + +# Extract only specific named values +# namedValues: +# - api-key +# - connection-string + +# Extract only specific loggers +# loggers: +# - appinsights-logger + +# Extract only specific diagnostics +# diagnostics: +# - applicationinsights + +# Extract only specific tags +# tags: +# - production +# - external + +# Extract only specific policy fragments +# policyFragments: +# - rate-limit-fragment +# - cors-fragment + +# Extract only specific gateways +# gateways: +# - default +# - internal-gateway + +# Extract only specific version sets +# versionSets: +# - payments-v1 + +# Extract only specific groups +# groups: +# - administrators + +# Extract only specific subscriptions +# subscriptions: +# - starter-subscription + +# Extract only specific schemas +# schemas: +# - pet-schema + +# Filter service-level policies +# policies: +# - policy + +# Extract only specific policy restrictions +# policyRestrictions: +# - global-policy-restriction + +# Extract only specific documentations +# documentations: +# - getting-started + +# Extract only specific workspaces +# workspaces: +# - dev-workspace + +# Advanced: Filter workspace sub-resources +# workspaces: +# - team-workspace: +# apis: +# - team-api-1 +# - team-api-2 +# backends: +# - team-backend +# namedValues: +# - team-api-key + +# Filter behavior: +# - Leave a section commented out to include ALL resources of that type +# - Set a section to an empty array ([]) to exclude ALL resources of that type +# Example: +# gateways: [] +# subscriptions: [] +# - Use * to match any characters: prod-* matches prod-api, prod-users +# - Use ? to match a single character: api-v? matches api-v1, api-v2 +# - Exact names and wildcard patterns can be mixed in the same list +# - All matching is case-insensitive diff --git a/src/templates/configs/override-config.ts b/src/templates/configs/override-config.ts index d3e013c6..efb0b9e8 100644 --- a/src/templates/configs/override-config.ts +++ b/src/templates/configs/override-config.ts @@ -5,98 +5,13 @@ * Generates environment-specific configuration.{env}.yaml files */ -export function generateOverrideConfig(environment: string): string { - return `# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/override-config.schema.json -# APIM Override Configuration for ${environment} environment -# Customize resource properties for this specific environment -# For full format details and examples, see: -# https://github.com/Azure/apiops-cli/blob/main/docs/guides/environment-overrides.md - -# Override named values (e.g., API keys, connection strings) -# namedValues: -# - name: api-key -# properties: -# value: "${environment}-api-key-value" -# - name: connection-string -# properties: -# value: "{#[DB_Connection_String]#}" -# - name: secret-from-keyvault -# properties: -# keyVault: -# secretIdentifier: "https://${environment}-kv.vault.azure.net/secrets/my-secret" -# identityClientId: "00000000-0000-0000-0000-000000000000" - -# Override backend URLs per environment -# backends: -# - name: backend-api -# properties: -# url: "https://${environment}-api.example.com" -# - name: legacy-backend -# properties: -# url: "https://${environment}-legacy.example.com" -# resourceId: "/subscriptions/.../sites/${environment}-backend" - -# Override API service URLs (with optional nested sub-resource overrides) -# apis: -# - name: echo-api -# properties: -# serviceUrl: "https://${environment}-echo.example.com" -# - name: petstore-api -# properties: -# serviceUrl: "https://${environment}-petstore.example.com" -# displayName: "Petstore API (${environment})" -# diagnostics: -# - name: applicationinsights -# properties: -# loggerId: "appinsights-logger-${environment}" -# verbosity: Error -# policies: -# - name: policy -# properties: -# format: rawxml - -# Override diagnostic logger references -# diagnostics: -# - name: applicationinsights -# properties: -# loggerId: "appinsights-logger-${environment}" -# verbosity: Error +import { overrideConfigTemplate } from '../generated/embedded-markdown.js'; +import { renderTemplate } from '../../lib/render-template.js'; +import { schemaUrl } from './schema-ref.js'; -# Override logger credentials or resource IDs -# loggers: -# - name: appinsights-logger -# properties: -# loggerType: applicationInsights -# resourceId: "/subscriptions/xxxxx/resourceGroups/${environment}-rg/providers/microsoft.insights/components/${environment}-appinsights" -# isBuffered: true -# credentials: -# instrumentationKey: "" - -# Override service-level policies -# policies: -# - name: policy -# properties: -# format: rawxml - -# Override gateway properties -# gateways: -# - name: on-prem-gateway -# properties: -# locationData: -# name: "${environment} datacenter" - -# Override version sets, groups, subscriptions, products, tags, policy fragments -# versionSets: -# - name: my-version-set -# properties: -# displayName: "My Version Set (${environment})" -# products: -# - name: starter -# properties: -# displayName: "Starter Plan (${environment})" -# tags: -# - name: env-tag -# properties: -# displayName: "${environment}" -`; +export function generateOverrideConfig(environment: string): string { + return renderTemplate(overrideConfigTemplate, { + SCHEMA_URL: schemaUrl('override-config.schema.json'), + ENVIRONMENT: environment, + }); } diff --git a/src/templates/configs/override-config.yaml b/src/templates/configs/override-config.yaml new file mode 100644 index 00000000..369e04a9 --- /dev/null +++ b/src/templates/configs/override-config.yaml @@ -0,0 +1,92 @@ +# yaml-language-server: $schema={{SCHEMA_URL}} +# APIM Override Configuration for {{ENVIRONMENT}} environment +# Customize resource properties for this specific environment +# For full format details and examples, see: +# https://github.com/Azure/apiops-cli/blob/main/docs/guides/environment-overrides.md + +# Override named values (e.g., API keys, connection strings) +# namedValues: +# - name: api-key +# properties: +# value: "{{ENVIRONMENT}}-api-key-value" +# - name: connection-string +# properties: +# value: "{#[DB_Connection_String]#}" +# - name: secret-from-keyvault +# properties: +# keyVault: +# secretIdentifier: "https://{{ENVIRONMENT}}-kv.vault.azure.net/secrets/my-secret" +# identityClientId: "00000000-0000-0000-0000-000000000000" + +# Override backend URLs per environment +# backends: +# - name: backend-api +# properties: +# url: "https://{{ENVIRONMENT}}-api.example.com" +# - name: legacy-backend +# properties: +# url: "https://{{ENVIRONMENT}}-legacy.example.com" +# resourceId: "/subscriptions/.../sites/{{ENVIRONMENT}}-backend" + +# Override API service URLs (with optional nested sub-resource overrides) +# apis: +# - name: echo-api +# properties: +# serviceUrl: "https://{{ENVIRONMENT}}-echo.example.com" +# - name: petstore-api +# properties: +# serviceUrl: "https://{{ENVIRONMENT}}-petstore.example.com" +# displayName: "Petstore API ({{ENVIRONMENT}})" +# diagnostics: +# - name: applicationinsights +# properties: +# loggerId: "appinsights-logger-{{ENVIRONMENT}}" +# verbosity: Error +# policies: +# - name: policy +# properties: +# format: rawxml + +# Override diagnostic logger references +# diagnostics: +# - name: applicationinsights +# properties: +# loggerId: "appinsights-logger-{{ENVIRONMENT}}" +# verbosity: Error + +# Override logger credentials or resource IDs +# loggers: +# - name: appinsights-logger +# properties: +# loggerType: applicationInsights +# resourceId: "/subscriptions/xxxxx/resourceGroups/{{ENVIRONMENT}}-rg/providers/microsoft.insights/components/{{ENVIRONMENT}}-appinsights" +# isBuffered: true +# credentials: +# instrumentationKey: "" + +# Override service-level policies +# policies: +# - name: policy +# properties: +# format: rawxml + +# Override gateway properties +# gateways: +# - name: on-prem-gateway +# properties: +# locationData: +# name: "{{ENVIRONMENT}} datacenter" + +# Override version sets, groups, subscriptions, products, tags, policy fragments +# versionSets: +# - name: my-version-set +# properties: +# displayName: "My Version Set ({{ENVIRONMENT}})" +# products: +# - name: starter +# properties: +# displayName: "Starter Plan ({{ENVIRONMENT}})" +# tags: +# - name: env-tag +# properties: +# displayName: "{{ENVIRONMENT}}" diff --git a/src/templates/configs/schema-ref.ts b/src/templates/configs/schema-ref.ts new file mode 100644 index 00000000..3b26cb08 --- /dev/null +++ b/src/templates/configs/schema-ref.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +/** + * Builds URLs to the published JSON schemas. + * + * The schema is only an editor/IDE validation aid (yaml-language-server); the + * CLI does its own validation at runtime. Schemas are versioned independently + * of the CLI package version: each schema version lives at a frozen path + * (`schemas/v/...`) on the `main` branch. Backward-compatible edits update + * the current version in place; a breaking change introduces a new version + * folder. The `main` ref always resolves, and the versioned path keeps existing + * config files pointing at the schema shape they were written against. + */ + +import packageJson from '../../../package.json' with { type: 'json' }; + +const SCHEMA_BASE = 'https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas'; + +/** + * Returns the raw URL for a published schema file at the current schema version. + * + * @param fileName Schema file name, e.g. `extractor-config.schema.json`. + */ +export function schemaUrl(fileName: string): string { + const { schemaVersion } = packageJson as { schemaVersion?: string }; + const version = schemaVersion ?? '1'; + return `${SCHEMA_BASE}/v${version}/${fileName}`; +} diff --git a/tests/integration/package-build/package-build.test.ts b/tests/integration/package-build/package-build.test.ts index 06071e19..59039312 100644 --- a/tests/integration/package-build/package-build.test.ts +++ b/tests/integration/package-build/package-build.test.ts @@ -45,14 +45,14 @@ async function runNpm(args: string[]): Promise { return result.stdout; } -async function collectMarkdownFiles(dirPath: string): Promise { +async function collectEmbeddableTemplateFiles(dirPath: string): Promise { const entries = await fs.readdir(dirPath, { withFileTypes: true }); const nested = await Promise.all(entries.map(async (entry) => { const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { - return collectMarkdownFiles(fullPath); + return collectEmbeddableTemplateFiles(fullPath); } - return entry.name.endsWith('.md') ? [fullPath] : []; + return entry.name.endsWith('.md') || entry.name.endsWith('.yaml') ? [fullPath] : []; })); return nested.flat(); @@ -94,14 +94,14 @@ describe('package build integration', () => { expect(distFiles.length).toBeGreaterThan(0); }, 240_000); - it('should include all src/templates markdown files in packed output via embedded template constants', async () => { + it('should include all src/templates markdown and yaml files in packed output via embedded template constants', async () => { await runNpm(['run', 'build']); const templateRoot = path.join(repoRoot, 'src/templates'); - const markdownFiles = await collectMarkdownFiles(templateRoot); - expect(markdownFiles.length).toBeGreaterThan(0); + const templateFiles = await collectEmbeddableTemplateFiles(templateRoot); + expect(templateFiles.length).toBeGreaterThan(0); - const expectedTemplateContents = await Promise.all(markdownFiles.map(async (filePath) => { + const expectedTemplateContents = await Promise.all(templateFiles.map(async (filePath) => { const relPath = normalizePath(path.relative(templateRoot, filePath)); const content = await fs.readFile(filePath, 'utf8'); return { relPath, content }; diff --git a/tests/unit/templates/configs/config-templates.test.ts b/tests/unit/templates/configs/config-templates.test.ts index 69a0e3f1..7d85014d 100644 --- a/tests/unit/templates/configs/config-templates.test.ts +++ b/tests/unit/templates/configs/config-templates.test.ts @@ -69,6 +69,17 @@ describe('configs/filter-config', () => { const lines = config.split('\n').filter((line) => line.trim() && !line.trim().startsWith('#')); expect(lines).toHaveLength(0); }); + + it('should render the extractor schema URL in the yaml-language-server header', () => { + const config = generateFilterConfig(); + expect(config).toContain('# yaml-language-server: $schema='); + expect(config).toContain('schemas/v1/extractor-config.schema.json'); + }); + + it('should not leave any unrendered template placeholders', () => { + const config = generateFilterConfig(); + expect(config).not.toMatch(/\{\{[^}]+\}\}/); + }); }); }); @@ -140,5 +151,17 @@ describe('configs/override-config', () => { const lines = config.split('\n').filter((line) => line.trim() && !line.trim().startsWith('#')); expect(lines).toHaveLength(0); }); + + it('should render the override schema URL in the yaml-language-server header', () => { + const config = generateOverrideConfig('dev'); + expect(config).toContain('# yaml-language-server: $schema='); + expect(config).toContain('schemas/v1/override-config.schema.json'); + }); + + it('should substitute every environment placeholder', () => { + const config = generateOverrideConfig('staging'); + expect(config).not.toMatch(/\{\{[^}]+\}\}/); + expect(config).toContain('staging-api-key-value'); + }); }); }); diff --git a/tests/unit/templates/copilot/configure-overrides-prompt.test.ts b/tests/unit/templates/copilot/configure-overrides-prompt.test.ts index 05f5e5dc..713ea6ae 100644 --- a/tests/unit/templates/copilot/configure-overrides-prompt.test.ts +++ b/tests/unit/templates/copilot/configure-overrides-prompt.test.ts @@ -13,7 +13,7 @@ describe('copilot/configure-overrides-prompt', () => { expect(prompt).toContain('# Configure APIOps Environment Overrides'); expect(prompt).toContain('configuration.{environment}.yaml'); - expect(prompt).toContain('Detect Environments'); + expect(prompt).toContain('Detect and Confirm Environments'); expect(prompt).toContain('configuration.*.yaml'); expect(prompt).not.toMatch(/\{\{[^}]+\}\}/); }); From 5aae6b2217312d71bd07cbdcc353d041d31df31f Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Thu, 25 Jun 2026 20:19:55 +0000 Subject: [PATCH 09/13] adding skill to update schema version. --- .../update-config-schema-version/SKILL.md | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 .github/skills/update-config-schema-version/SKILL.md diff --git a/.github/skills/update-config-schema-version/SKILL.md b/.github/skills/update-config-schema-version/SKILL.md new file mode 100644 index 00000000..ef7bba8b --- /dev/null +++ b/.github/skills/update-config-schema-version/SKILL.md @@ -0,0 +1,129 @@ +--- +name: "update-config-schema-version" +description: "Bump the filter/override JSON Schema version (schemas/v) and update every reference so docs, Copilot prompts, init-generated configs, and tests point at the new version. Use when changing the configuration.extractor.yaml / configuration..yaml schema shape, introducing a breaking schema change, or when 'schemaVersion' in package.json needs to move." +domain: "configuration-schema" +confidence: "high" +source: "manual + observed from schemas/v1 layout and schema-ref.ts/generate-schemas.mjs wiring" +--- + +## Context + +The filter (extractor) and override config JSON Schemas are versioned +**independently of the CLI package version**. Each schema version lives at a +frozen path `schemas/v/` on the `main` branch: + +- **Backward-compatible edits** (add an optional field, loosen validation): + keep the same version and regenerate in place. +- **Breaking changes** (rename/remove a field, tighten validation): introduce a + **new** version folder `schemas/v/` and leave the old folder frozen so + existing config files keep resolving against the shape they were written for. + +The single source of truth is `schemaVersion` in [`package.json`](../../../package.json). + +## What updates automatically (do NOT hand-edit) + +Both of these read `schemaVersion` at build time — bumping `package.json` is +enough for them: + +- [`scripts/generate-schemas.mjs`](../../../scripts/generate-schemas.mjs) — + writes the schema files to `schemas/v/` and stamps the `$id` URL. +- [`src/templates/configs/schema-ref.ts`](../../../src/templates/configs/schema-ref.ts) — + builds the `# yaml-language-server: $schema=` URL injected into the + `apiops init` generated configs (`filter-config.yaml` / `override-config.yaml` + templates use the `{{SCHEMA_URL}}` placeholder). + +Regenerating happens on `prebuild` / `prelint` / `pretest`, or run manually: + +```bash +node scripts/generate-schemas.mjs && node scripts/embed-markdown-templates.mjs +``` + +## What must be updated by hand + +These hardcode `schemas/v/` and will NOT change on their own. Update every +one to the new version. Find them all first: + +```bash +grep -rn "schemas/v[0-9]" \ + src/templates docs tests \ + | grep -v node_modules +``` + +Files that reference the version directly today: + +1. **Copilot prompt templates** (embedded into the package — must be edited at + the source, then re-embed): + - [`src/templates/copilot/configure-filter-prompt.md`](../../../src/templates/copilot/configure-filter-prompt.md) + → `schemas/v/extractor-config.schema.json` + - [`src/templates/copilot/configure-overrides-prompt.md`](../../../src/templates/copilot/configure-overrides-prompt.md) + → `schemas/v/override-config.schema.json` +2. **Docs**: + - [`docs/guides/filtering-resources.md`](../../../docs/guides/filtering-resources.md) + (the `$schema` example line and the "published at `schemas/v/...`" link) + - [`docs/guides/environment-overrides.md`](../../../docs/guides/environment-overrides.md) + (two `$schema` example lines and the "published at `schemas/v/...`" link) +3. **Unit tests** (assert the rendered schema URL): + - [`tests/unit/templates/configs/config-templates.test.ts`](../../../tests/unit/templates/configs/config-templates.test.ts) + (`expect(config).toContain('schemas/v/extractor-config.schema.json')` and + the override equivalent) + +## Procedure + +### 1) Decide compatible vs breaking + +- **Backward-compatible:** keep `schemaVersion` as-is, edit + `src/models/config.ts`, regenerate, done. No reference updates needed. +- **Breaking:** bump `schemaVersion` and continue below. + +### 2) Bump the source of truth + +```jsonc +// package.json +"schemaVersion": "2", +``` + +### 3) Regenerate derived artifacts + +```bash +node scripts/generate-schemas.mjs # creates schemas/v2/*.schema.json +node scripts/embed-markdown-templates.mjs # re-embeds prompt md after step 4 +``` + +`schemas/v2/` is created; **keep `schemas/v1/` in place** (frozen for existing +configs). Do not delete the old version folder. + +### 4) Update every hardcoded reference + +Edit each file listed in "What must be updated by hand" to `v2`, then re-run the +embed script so the prompt changes land in +`src/templates/generated/embedded-markdown.ts`. + +### 5) Verify nothing still points at the old version unintentionally + +```bash +# Should only match the intentionally-frozen old folder and any "previous +# version" historical notes — not docs/prompts/tests describing the current shape. +grep -rn "schemas/v1" src docs tests scripts +``` + +### 6) Build and test + +```bash +npm run build +npx vitest run tests/unit/templates/configs/config-templates.test.ts \ + tests/integration/package-build/package-build.test.ts +``` + +The package-build integration test confirms the embedded templates (including +the updated prompts) ship in the npm pack output. + +## Gotchas + +- The `schemas/v/` folder is intentionally retained on a breaking bump — + removing it breaks configs already pinned to that version. +- Prompt `.md` edits do nothing until `embed-markdown-templates.mjs` re-runs and + the project rebuilds; the embedded constant is what ships, not the raw file. +- `apiops init` output needs no manual change — it derives the URL from + `schemaVersion` via `schema-ref.ts`. +- Keep `package.json schemaVersion` a bare integer string (`"2"`), not `"v2"`; + the `v` prefix is added by the scripts. From 25a887b4439922f3ce418c891b19217d18886e8f Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Thu, 25 Jun 2026 20:39:53 +0000 Subject: [PATCH 10/13] updating multi-repo skill --- .github/skills/codespace-clone-repo/SKILL.md | 121 ----------- .github/skills/codespace-multi-repo/SKILL.md | 199 +++++++++++++++++++ 2 files changed, 199 insertions(+), 121 deletions(-) delete mode 100644 .github/skills/codespace-clone-repo/SKILL.md create mode 100644 .github/skills/codespace-multi-repo/SKILL.md diff --git a/.github/skills/codespace-clone-repo/SKILL.md b/.github/skills/codespace-clone-repo/SKILL.md deleted file mode 100644 index 395a426d..00000000 --- a/.github/skills/codespace-clone-repo/SKILL.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -name: "codespace-clone-repo" -description: "Clone another GitHub repository from a Codespace/dev container using GitHub CLI web login (no PAT), especially when GITHUB_TOKEN defaults interfere with auth. Use when gh auth login fails or when testing apiops init/pipeline changes across repos." -domain: "developer-workflow" -confidence: "high" -source: "manual + repeated Codespaces troubleshooting" ---- - -## Context - -Use this skill when a user needs to clone another repository from a Codespace or dev container and must avoid PAT-based auth and environment-provided `GITHUB_TOKEN`/`GH_TOKEN` credentials. - -This pattern is useful for cross-repo validation such as `apiops init` pipeline testing. - -## Fast Path - -Run these commands in order: - -```bash -unset GITHUB_TOKEN GH_TOKEN - -# Optional but recommended to clear stale account state -gh auth logout -h github.com - -# Web/device login, no PAT -env -u GITHUB_TOKEN -u GH_TOKEN gh auth login \ - -h github.com \ - -p https \ - -w \ - --clipboard \ - --insecure-storage - -# Verify auth source is account login, not env token -env -u GITHUB_TOKEN -u GH_TOKEN gh auth status -``` - -Then clone to `/workspaces`: - -```bash -cd /workspaces -gh clone / -``` - -After cloning, open workspaces so all repos can be opened: - -1. In VS Code, go to **File → Open Folder...** -2. Navigate to and select `/workspaces` -3. Click **Open** - -VS Code will reload with the cloned repository as your workspace. - -## If Login Appears Stuck - -If `gh auth login` shows: - -- `First copy your one-time code: XXXX-YYYY` -- `Press Enter to open https://github.com/login/device in your browser...` - -Use a second terminal: - -```bash -$BROWSER https://github.com/login/device -``` - -Enter the one-time code in the browser and authorize, then return to the original terminal and press Enter if needed. - -## Common Failure Modes - -- `gh auth status` shows `(GITHUB_TOKEN)`: - environment token is still active. Re-run with `env -u GITHUB_TOKEN -u GH_TOKEN`. - -- Login exits with code `130`: - usually interrupted (`Ctrl+C`) while waiting for device authorization. Restart login and complete browser step. - -- Browser does not launch from container: - run `$BROWSER https://github.com/login/device` manually from a separate terminal. - -- Clone prompts for credentials unexpectedly: - verify git protocol is HTTPS in `gh auth status` and re-run `gh auth login -p https` if necessary. - -- `git push`/`git fetch` returns `403` even though `gh auth status` looks correct: - Codespaces can inject `GITHUB_TOKEN`/`GH_TOKEN` that override account auth for git operations. - Use token-sanitized commands and force gh credentials for the operation: - - ```bash - env -u GITHUB_TOKEN -u GH_TOKEN git \ - -c credential.helper= \ - -c credential.helper='!gh auth git-credential' \ - push origin - ``` - - For fetch/pull, use the same pattern with `fetch` or `pull`. - -## Safety Notes - -- Never request a PAT for this workflow unless the user explicitly asks for a PAT-based approach. -- Do not route secrets through chat prompts. -- Keep authentication interactive in user terminal when account sign-in is required. - -## Example End-to-End - -```bash -unset GITHUB_TOKEN GH_TOKEN -gh auth logout -h github.com -env -u GITHUB_TOKEN -u GH_TOKEN gh auth login -h github.com -p https -w --clipboard --insecure-storage -env -u GITHUB_TOKEN -u GH_TOKEN gh auth status -cd /workspaces -gh clone / -``` - -If you later need to push from a Codespace, use: - -```bash -cd /workspaces/ -env -u GITHUB_TOKEN -u GH_TOKEN git \ - -c credential.helper= \ - -c credential.helper='!gh auth git-credential' \ - push origin -``` - -Then use **File → Open Folder...** to open `/workspaces` in VS Code. diff --git a/.github/skills/codespace-multi-repo/SKILL.md b/.github/skills/codespace-multi-repo/SKILL.md new file mode 100644 index 00000000..90e3052a --- /dev/null +++ b/.github/skills/codespace-multi-repo/SKILL.md @@ -0,0 +1,199 @@ +--- +name: "codespace-multi-repo" +description: "Clone, pull, and push across multiple repositories from a Codespace/dev container without PATs — GitHub repos via GitHub CLI web login (avoiding GITHUB_TOKEN/GH_TOKEN interference) and Azure DevOps repos via a short-lived az CLI OAuth token. Use when working across more than one repo, gh auth login fails, git push/fetch returns 403/401, or when testing apiops init/pipeline changes across repos." +domain: "developer-workflow" +confidence: "high" +source: "manual + repeated Codespaces troubleshooting" +--- + +## Context + +Use this skill when working with **multiple repositories** from a Codespace or +dev container and you need to **clone, pull, and push** to each without PAT-based +auth or environment-provided `GITHUB_TOKEN`/`GH_TOKEN` credentials. + +Repos may live on different hosts (github.com and Azure DevOps), each with its +own auth path: + +| Host | Clone / pull / push auth | +|------|--------------------------| +| github.com | GitHub CLI account login (`gh auth login -w`), token-sanitized git | +| dev.azure.com | Short-lived OAuth token from `az account get-access-token` | + +This pattern is useful for cross-repo validation such as `apiops init` pipeline +testing, where the CLI repo lives on GitHub and the scaffolded sample repo lives +on Azure DevOps. + +## GitHub Repos: Clone, Pull, Push + +Run these commands in order: + +```bash +unset GITHUB_TOKEN GH_TOKEN + +# Optional but recommended to clear stale account state +gh auth logout -h github.com + +# Web/device login, no PAT +env -u GITHUB_TOKEN -u GH_TOKEN gh auth login \ + -h github.com \ + -p https \ + -w \ + --clipboard \ + --insecure-storage + +# Verify auth source is account login, not env token +env -u GITHUB_TOKEN -u GH_TOKEN gh auth status +``` + +Then clone to `/workspaces`: + +```bash +cd /workspaces +gh clone / +``` + +After cloning, open workspaces so all repos can be opened: + +1. In VS Code, go to **File → Open Folder...** +2. Navigate to and select `/workspaces` +3. Click **Open** + +VS Code will reload with the cloned repository as your workspace. + +Pull and push use the same account auth. If the Codespace-injected env token +overrides it (see Common Failure Modes), force gh credentials per operation: + +```bash +cd /workspaces/ + +# Pull +env -u GITHUB_TOKEN -u GH_TOKEN git \ + -c credential.helper= \ + -c credential.helper='!gh auth git-credential' \ + pull origin + +# Push +env -u GITHUB_TOKEN -u GH_TOKEN git \ + -c credential.helper= \ + -c credential.helper='!gh auth git-credential' \ + push origin +``` + +## If Login Appears Stuck + +If `gh auth login` shows: + +- `First copy your one-time code: XXXX-YYYY` +- `Press Enter to open https://github.com/login/device in your browser...` + +Use a second terminal: + +```bash +$BROWSER https://github.com/login/device +``` + +Enter the one-time code in the browser and authorize, then return to the original terminal and press Enter if needed. + +## Common Failure Modes + +- `gh auth status` shows `(GITHUB_TOKEN)`: + environment token is still active. Re-run with `env -u GITHUB_TOKEN -u GH_TOKEN`. + +- Login exits with code `130`: + usually interrupted (`Ctrl+C`) while waiting for device authorization. Restart login and complete browser step. + +- Browser does not launch from container: + run `$BROWSER https://github.com/login/device` manually from a separate terminal. + +- Clone prompts for credentials unexpectedly: + verify git protocol is HTTPS in `gh auth status` and re-run `gh auth login -p https` if necessary. + +- `git push`/`git fetch` returns `403` even though `gh auth status` looks correct: + Codespaces can inject `GITHUB_TOKEN`/`GH_TOKEN` that override account auth for git operations. + Use token-sanitized commands and force gh credentials for the operation: + + ```bash + env -u GITHUB_TOKEN -u GH_TOKEN git \ + -c credential.helper= \ + -c credential.helper='!gh auth git-credential' \ + push origin + ``` + + For fetch/pull, use the same pattern with `fetch` or `pull`. + +## Azure DevOps Repos: Clone, Pull, Push (no PAT) + +Azure DevOps remotes (`https://dev.azure.com///_git/`) are +not covered by the GitHub credential helper. In a Codespace the only configured +helper is for github.com, so `git push`/`pull`/`clone` hangs or fails auth. +Instead of a PAT, mint a short-lived OAuth token from the Azure CLI and pass it +inline — never echo or persist it. + +```bash +# Ensure the Azure CLI is logged in (interactive if needed — do NOT pass secrets via chat) +az account show --query "{user:user.name, tenant:tenantId}" -o json + +# Well-known, public Azure DevOps resource (application) ID — the same constant +# for every org worldwide; NOT a secret and not specific to your repo/tenant. +# It tells Entra ID to issue a token scoped to Azure DevOps. +ADO_RESOURCE_ID=499b84ac-1321-427f-aa17-267ca6975798 +ADO_TOKEN=$(az account get-access-token \ + --resource "$ADO_RESOURCE_ID" \ + --query accessToken -o tsv) + +# Push using the token as a bearer header; token stays in a variable, never printed +git -c http.extraheader="AUTHORIZATION: Bearer $ADO_TOKEN" push origin +``` + +Clone and pull use the identical header pattern: + +```bash +# Clone +git -c http.extraheader="AUTHORIZATION: Bearer $ADO_TOKEN" \ + clone https://dev.azure.com///_git/ + +# Pull +git -c http.extraheader="AUTHORIZATION: Bearer $ADO_TOKEN" pull origin +``` + +Notes: +- `499b84ac-1321-427f-aa17-267ca6975798` is the fixed, public Azure DevOps + application ID in Microsoft Entra ID — identical for all organizations and not + a credential. Use it as-is for `--resource` (or `.../.default` for scope-based + requests). It is required so Entra ID issues a token Azure DevOps will accept. +- The same `-c http.extraheader=...` pattern works for `fetch`/`pull`/`clone`. +- The az login tenant must be the tenant that backs the Azure DevOps org. If the + push returns `TF400813`/`401`, the logged-in identity has no access to that org + — `az login --tenant ` against the correct tenant first. +- Keep the token in a shell variable (`ADO_TOKEN=$(...)`) and reference it; do not + print it or place it directly in a literal command. + +## Safety Notes + +- Never request a PAT for this workflow unless the user explicitly asks for a PAT-based approach. +- Do not route secrets through chat prompts. +- Keep authentication interactive in user terminal when account sign-in is required. + +## Example End-to-End + +```bash +unset GITHUB_TOKEN GH_TOKEN +gh auth logout -h github.com +env -u GITHUB_TOKEN -u GH_TOKEN gh auth login -h github.com -p https -w --clipboard --insecure-storage +env -u GITHUB_TOKEN -u GH_TOKEN gh auth status +cd /workspaces +gh clone / +``` + +If you later need to push from a Codespace, use: + +```bash +cd /workspaces/ +env -u GITHUB_TOKEN -u GH_TOKEN git \ + -c credential.helper= \ + -c credential.helper='!gh auth git-credential' \ + push origin +``` + +Then use **File → Open Folder...** to open `/workspaces` in VS Code. From bc72cbcb4a36d28a3a7168bedebc654418699b14 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Jun 2026 21:39:09 +0000 Subject: [PATCH 11/13] fix: make override schema match runtime by allowing omitted properties --- schemas/v1/override-config.schema.json | 3 +-- scripts/generate-schemas.mjs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/schemas/v1/override-config.schema.json b/schemas/v1/override-config.schema.json index b701c43d..51197471 100644 --- a/schemas/v1/override-config.schema.json +++ b/schemas/v1/override-config.schema.json @@ -77,8 +77,7 @@ "overrideEntry": { "type": "object", "required": [ - "name", - "properties" + "name" ], "additionalProperties": false, "properties": { diff --git a/scripts/generate-schemas.mjs b/scripts/generate-schemas.mjs index dc87d9c8..deaf9790 100644 --- a/scripts/generate-schemas.mjs +++ b/scripts/generate-schemas.mjs @@ -311,7 +311,7 @@ const overrideSchema = { }, overrideEntry: { type: 'object', - required: ['name', 'properties'], + required: ['name'], additionalProperties: false, properties: { name: { From 2b3182999d29e399da4a3357efd7b23105db624e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Jun 2026 21:40:40 +0000 Subject: [PATCH 12/13] docs: explain why overrideEntry properties is optional in schema generator --- scripts/generate-schemas.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/generate-schemas.mjs b/scripts/generate-schemas.mjs index deaf9790..66760298 100644 --- a/scripts/generate-schemas.mjs +++ b/scripts/generate-schemas.mjs @@ -311,6 +311,8 @@ const overrideSchema = { }, overrideEntry: { type: 'object', + // Only 'name' is required: the loader treats 'properties' as optional and + // falls back to inline fields when it is omitted (see src/lib/config-loader.ts). required: ['name'], additionalProperties: false, properties: { From 30bee259531ffc5428bb9f63355db02ca284b97b Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Thu, 25 Jun 2026 21:53:08 +0000 Subject: [PATCH 13/13] removing duplicate paragraph. --- docs/guides/environment-overrides.md | 31 +++++++++------------------- docs/guides/filtering-resources.md | 2 +- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/docs/guides/environment-overrides.md b/docs/guides/environment-overrides.md index 5818edc8..0f355488 100644 --- a/docs/guides/environment-overrides.md +++ b/docs/guides/environment-overrides.md @@ -23,15 +23,22 @@ apiops publish \ --overrides ./configuration.prod.yaml ``` -## IDE Autocomplete (JSON Schema) +## Copilot-Assisted Configuration + +If you ran `apiops init`, a Copilot prompt file was generated at `.github/prompts/apiops-configure-overrides.prompt.md`. Open it in VS Code and ask GitHub Copilot to help you configure environment overrides — it will guide you through setting up environment-specific values interactively. -Add this comment as the first line of your override file to enable autocomplete in VS Code and other YAML-aware editors: +## IDE Autocompletion with JSON Schema + +A JSON Schema is available for `configuration.{env}.yaml` override files. Add yaml-language-server comment at the top of your override file. Requires yaml language extension in VSCode. ```yaml # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/v1/override-config.schema.json ``` -The schema validates section names, entry structure, nested sub-resource overrides, and supports `{#[TOKEN_NAME]#}` placeholder values. It is published at [`schemas/v1/override-config.schema.json`](https://github.com/Azure/apiops-cli/blob/main/schemas/v1/override-config.schema.json). +The schema provides: +- Property name autocompletion for all resource sections +- Validation of the override structure (name + properties format) +- Inline documentation including token substitution syntax ## Override file format (APIOps Toolkit-compatible) @@ -567,21 +574,3 @@ apiops publish --overrides configuration.prod.yaml --dry-run \ - [Scenarios and Workflows](scenarios-and-workflows.md) - [GitHub Actions Integration](../ci-cd/github-actions.md) ---- - -## Copilot-Assisted Configuration - -If you ran `apiops init`, a Copilot prompt file was generated at `.github/prompts/apiops-configure-overrides.prompt.md`. Open it in VS Code and ask GitHub Copilot to help you configure environment overrides — it will guide you through setting up environment-specific values interactively. - -## IDE Autocompletion with JSON Schema - -A JSON Schema is available for `configuration.{env}.yaml` override files. Add this comment at the top of your override file to enable autocompletion in VS Code (with the YAML extension): - -```yaml -# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/v1/override-config.schema.json -``` - -The schema provides: -- Property name autocompletion for all resource sections -- Validation of the override structure (name + properties format) -- Inline documentation including token substitution syntax diff --git a/docs/guides/filtering-resources.md b/docs/guides/filtering-resources.md index df45976e..8d0b77e8 100644 --- a/docs/guides/filtering-resources.md +++ b/docs/guides/filtering-resources.md @@ -38,7 +38,7 @@ Only `petstore-api`, `orders-api`, and their transitive dependencies are extract ## IDE Autocomplete (JSON Schema) -Add this comment as the first line of your filter file to enable autocomplete in VS Code and other YAML-aware editors: +Add yaml-language-server comment at the top of your override file. Requires yaml language extension in VSCode. ```yaml # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/apiops-cli/main/schemas/v1/extractor-config.schema.json