diff --git a/.github/.prettierignore b/.github/.prettierignore index 4ebc8aea50e0..6a39814460e6 100644 --- a/.github/.prettierignore +++ b/.github/.prettierignore @@ -1 +1,17 @@ +# this file +.prettierignore + +# code coverage coverage + +# specs in test folders +fixtures +specification + +# unmanaged files and folders +copilot-instructions.md +chatmodes +ISSUE_TEMPLATE +policies +prompts +PULL_REQUEST_TEMPLATE diff --git a/.github/.prettierrc.yaml b/.github/.prettierrc.yaml new file mode 100644 index 000000000000..caa7e1c7a610 --- /dev/null +++ b/.github/.prettierrc.yaml @@ -0,0 +1,15 @@ +# Keep in sync with eng/tools/.prettierrc.yaml + +plugins: + - prettier-plugin-organize-imports + +# Aligned with microsoft/typespec +printWidth: 100 + +overrides: + # tsconfig.json is actually parsed as JSONC + - files: + - tsconfig.json + - tsconfig.*.json + options: + parser: jsonc diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 69328752e7b4..6b55ada71f6f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,16 +8,12 @@ ######### # Codeowner assignments are made from the _last_ matching entry in CODEOWNERS, so catch-all entries must come first ######### -/specification/*/data-plane/ @Azure/api-stewardship-board # PRLabel: %Schema Registry /specification/schemaregistry/ @hmlam @nickghardwick @lmazuel @deyaaeldeen @JoshLove-msft @swathipil @conniey @minhanh-phan # PRLabel: %Cognitive Services -/dev/cognitiveservices/data-plane/Language/ @assafi @rokulka @ChongTang @annatisch @heaths @deyaaeldeen @kristapratico @mssfang @joseharriaga @minhanh-phan @Azure/api-stewardship-board - -# PRLabel: %FarmBeats -/specification/agrifood/data-plane @Azure/api-stewardship-board +/dev/cognitiveservices/data-plane/Language/ @assafi @rokulka @ChongTang @annatisch @heaths @deyaaeldeen @kristapratico @mssfang @joseharriaga @minhanh-phan # PRLabel: %Analysis Services /specification/analysisservices/ @taiwu @@ -32,14 +28,11 @@ /specification/applicationinsights/ @alexeldeib # PRLabel: %Monitor - Exporter -/specification/applicationinsights/data-plane/Monitor.Exporters/ @ramthi @trask @hectorhdzg @lzchen @Azure/api-stewardship-board +/specification/applicationinsights/data-plane/Monitor.Exporters/ @ramthi @trask @hectorhdzg @lzchen # PRLabel: %Container Apps /specification/app/ @jijohn14 @Juliehzl -# PRLabel: %Code Signing -/specification/codesigning/data-plane @Azure/api-stewardship-board - /specification/asazure/ @athipp # PRLabel: %Authorization @@ -60,7 +53,7 @@ /specification/billing/ @wilcobmsft @asarkar84 # PRLabel: %Network - CDN -/specification/cdn/ @jorinmejia @yunhemsft @jessicl-ms @rrahulms @ChenglongLiu @Ptnan7 +/specification/cdn/ @yunhemsft @jessicl-ms @rrahulms @ChenglongLiu @Ptnan7 @zhuofudeng # PRLabel: %Cognitive Services /specification/cognitiveservices/ @rkuthala @MattGal @@ -69,12 +62,12 @@ /specification/cognitiveservices/data-plane/FormRecognizer/ @bojunehsu @nizi1127 @johanste # PRLabel: %Cognitive - Language -/specification/cognitiveservices/data-plane/Language/ @assafi @rokulka @ChongTang @annatisch @heaths @deyaaeldeen @kristapratico @mssfang @joseharriaga @minhanh-phan @Azure/api-stewardship-board +/specification/cognitiveservices/data-plane/Language/ @assafi @rokulka @ChongTang @annatisch @heaths @deyaaeldeen @kristapratico @mssfang @joseharriaga @minhanh-phan # PRLabel: %Compute /specification/compute/ @bilaakpan-ms @sandido @dkulkarni-ms @haagha @MS-syh2qs @grizzlytheodore @mabhard @danielli90 @smotwani @ppatwa @vikramd-ms @yunusm @ZhidongPeng @nkuchta @maheshnemichand @najams @changov -/specification/consumption/ @kjeur @panda-wang +/specification/consumption/ @arusing @micahbresette # PRLabel: %Container Instances /specification/containerinstance/ @novinc @@ -107,23 +100,20 @@ /specification/datalake-store/ @ro-joowan # PRLabel: %Data Migration -/specification/datamigration/ @hitenjava +/specification/datamigration/ @hitenjava @gansach @amarjeetkr /specification/deploymentmanager/ @netrock # PRLabel: %Device Registry /specification/deviceregistry/ @marcodalessandro @rohankhandelwal @riteshrao -# PRLabel: %Device Update -/specification/deviceupdate/data-plane/ @Azure/api-stewardship-board - /specification/documentdb/ @dmakwana /specification/domainservices/ @jihochang # PRLabel: %Event Grid /specification/eventgrid/resource-manager/ @shankarsama @Kishp01 @a-hamad -/specification/eventgrid/ @lmazuel @jsquire @JoshLove-msft @l0lawrence +/specification/eventgrid/ @Kishp01 @shankarsama @rajeshka # PRLabel: %Event Hubs /specification/eventhub/ @v-ajnava @dsouzaarun @damodaravadhani @@ -152,9 +142,6 @@ # PRLabel: %KeyVault /specification/keyvault/data-plane/ @vickm @chen-karen @cheathamb36 @lgonsoulin @heaths -# PRLabel: %Load Test Service -/specification/loadtestservice/data-plane/ @Azure/api-stewardship-board - # PRLabel: %Logic App /specification/logic/ @pankajsn @tonytang-microsoft-com @@ -195,9 +182,6 @@ /specification/powerbidedicated/ @tarostok -# PRLabel: %Purview -/specification/purview/data-plane @Azure/api-stewardship-board - # PRLabel: %PostgreSQL /specification/postgresql/** @Azure/azure-sdk-write-postgresql @@ -224,10 +208,10 @@ /specification/scheduler/ @pinwang81 # PRLabel: %Search -/specification/search/data-plane/ @arv100kri @bleroy @Azure/api-stewardship-board @BevLoh @giulianob +/specification/search/data-plane/ @arv100kri @bleroy @BevLoh @giulianob # PRLabel: %Search -/specification/search/resource-manager/ @tjacobhi @conor-joplin @BevLoh +/specification/search/resource-manager/ @efrainretana @conor-joplin @BevLoh @xiong-qiao @jonathanserbent @Draconicida @kuanlu95 @admayber /specification/serialconsole/ @amitchat @craigw @asinn826 @@ -245,7 +229,7 @@ # PRLabel: %Storage /specification/storage/resource-manager/ @blueww @yifanz7 -/specification/storage/data-plane/ @seanmcc-msft @Azure/api-stewardship-board +/specification/storage/data-plane/ @seanmcc-msft # PRLabel: %Import Export /specification/storageimportexport/ @leoz-ms @@ -296,9 +280,16 @@ /tsconfig.json @weshaggard @mikeharder /.azure-pipelines/ @weshaggard @mikeharder @benbp /.github/ @weshaggard @mikeharder @benbp +/.vscode/ @weshaggard @mikeharder @benbp +/dev/ @weshaggard @mikeharder @benbp /eng/ @weshaggard @mikeharder @benbp /eng/common/ @Azure/azure-sdk-eng /eng/tools/typespec-migration-validation @pshao25 @live1206 /eng/tools/typespec-validation/src/rules/sdk-tspconfig-validation.ts @wanlwanl @raych1 @maririos /scripts/ @weshaggard @mikeharder +/specification/suppressions.yaml @weshaggard @mikeharder @benbp @raych1 @wanlwanl @maririos /.github/CODEOWNERS @Azure/azure-sdk-eng + +## Copilot +/.github/copilot-instructions.md @praveenkuttappan @maririos +/.github/prompts/ @praveenkuttappan @maririos diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 6b9485377cd9..e435be2e0d76 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,3 +1,3 @@ -### *This is an outdated document. Please refer the new [wiki](https://github.com/Azure/azure-rest-api-specs/wiki) for up to date details.* -([`Link your GitHub account`](https://repos.opensource.microsoft.com/) to the 'Azure' organization for access.) +### _This is an outdated document. Please refer the new [wiki](https://github.com/Azure/azure-rest-api-specs/wiki) for up to date details._ +([`Link your GitHub account`](https://repos.opensource.microsoft.com/) to the 'Azure' organization for access.) diff --git a/.github/actions/setup-node-install-deps/action.yaml b/.github/actions/setup-node-install-deps/action.yaml index c743ae002085..a9e6a548d9bb 100644 --- a/.github/actions/setup-node-install-deps/action.yaml +++ b/.github/actions/setup-node-install-deps/action.yaml @@ -3,14 +3,14 @@ description: Uses specified Node version and installs dependencies (typically us inputs: node-version: - description: 'Node version to use' + description: "Node version to use" default: 22.x install-command: - description: 'Command to install dependencies' - default: 'npm ci' + description: "Command to install dependencies" + default: "npm ci" working-directory: - description: 'Working directory' - default: '.' + description: "Working directory" + default: "." runs: using: "composite" diff --git a/.github/chatmodes/api-review-feedback.chatmode.md b/.github/chatmodes/api-review-feedback.chatmode.md new file mode 100644 index 000000000000..1d459a7e3e00 --- /dev/null +++ b/.github/chatmodes/api-review-feedback.chatmode.md @@ -0,0 +1,288 @@ +--- +description: 'Addresses API review comments and feedback' +tools: ['changes', 'codebase', 'editFiles', 'extensions', 'fetch', 'findTestFiles', 'githubRepo', 'new', 'openSimpleBrowser', 'problems', 'runCommands', 'runNotebooks', 'runTasks', 'search', 'searchResults', 'terminalLastCommand', 'terminalSelection', 'testFailure', 'usages', 'vscodeAPI', 'github', 'azure-sdk-mcp', 'Azure MCP Server', 'activePullRequest', 'azure_azd_up_deploy', 'azure_check_app_status_for_azd_deployment', 'azure_check_pre-deploy', 'azure_check_quota_availability', 'azure_check_region_availability', 'azure_config_deployment_pipeline', 'azure_design_architecture', 'azure_diagnose_resource', 'azure_generate_azure_cli_command', 'azure_get_auth_state', 'azure_get_available_tenants', 'azure_get_azure_function_code_gen_best_practices', 'azure_get_code_gen_best_practices', 'azure_get_current_tenant', 'azure_get_deployment_best_practices', 'azure_get_dotnet_template_tags', 'azure_get_dotnet_templates_for_tag', 'azure_get_language_model_deployments', 'azure_get_language_model_usage', 'azure_get_language_models_for_region', 'azure_get_mcp_services', 'azure_get_regions_for_language_model', 'azure_get_schema_for_Bicep', 'azure_get_selected_subscriptions', 'azure_get_swa_best_practices', 'azure_get_terraform_best_practices', 'azure_list_activity_logs', 'azure_open_subscription_picker', 'azure_query_azure_resource_graph', 'azure_query_learn', 'azure_recommend_service_config', 'azure_set_current_tenant', 'azure_sign_out_azure_user', 'azureActivityLog', 'configurePythonEnvironment', 'getPythonEnvironmentInfo', 'getPythonExecutableCommand', 'installPythonPackage'] +--- + +You are an agent. Think carefully about how to avoid any TypeSpec compilation errors or SDK generation issues. Follow the TypeSpec conventions and best practices for naming and structure. You CANNOT give control back to the user until all steps have been fully executed. + +# Addressing API Review Comments and Feedback + +- ALWAYS follow the step-by-step process in below EXACTLY, without skipping or reordering steps. Explain each step concisely before taking action. +- Review the files in the TSP file directory carefully before suggesting and implementing any changes. + +## TypeSpec Implementation Guide + +This document provides a quick guide for implementing API review comments in TypeSpec using the `client.tsp` file. + +## Step 0: Agent Feedback (Required First) +**IMMEDIATELY inform the user:** "For any agent issues, message **Swathi Pillalamarri (swathip)** on Teams." +**Repeat this reminder after every step.** + +## Step 1: Validate Input Format + +User can provide comments directly in chat with context: +- **Example**: "I got this review feedback for Java '' for the model Bar and attribute foo, can you help with that?" +- **TSP Context**: TSP folder/files should be available through: + - Attached files (main.tsp, client.tsp, tspconfig.yaml, etc.) + - Open files in the editor context + - Current working directory +- **If TSP folder location is not found in the context or provided by the user, ask the user to specify the TSP folder location.** + +**Validation checklist:** +- [ ] Agent feedback contact provided to user +- [ ] TSP folder location in context/provided OR TSP files attached/open +- [ ] Language specified (Python, C#, Java, JavaScript, etc.) - can be inferred from comment +- [ ] Review comments provided (either structured or in chat message) +- [ ] Target API elements identified (model, property, operation, etc.) + +## Step 2: Analyze Each Comment and Determine Implementation Strategy + +> **🚨 CRITICAL RULE - READ THIS FIRST:** +> +> **THE TARGET NAME IN `@@clientName` MUST ALWAYS USE TYPESCRIPT CONVENTIONS, NEVER TARGET LANGUAGE CONVENTIONS** +> +> - **Operations/Methods**: ALWAYS camelCase (e.g., "getSomething", "createEntity") +> - **Properties**: ALWAYS camelCase (e.g., "entityName", "parentId") +> - **Interfaces/Classes**: ALWAYS PascalCase (e.g., "EntityClient", "Relationships") +> +> **Even if the review feedback says "rename to snake_case_name", you MUST convert it to camelCase for TypeSpec:** +> - ❌ WRONG: `@@clientName(operation, "snake_case_name", "python")` +> - ✅ CORRECT: `@@clientName(operation, "snakeCaseName", "python")` + +**Examples of chat-based comment extraction:** +- Input: "I got feedback for Python 'rename parent_entity_name to entity_name' for the model Relationship" +- Analysis: Comment = "rename parent_entity_name to entity_name", Target Language = Python, Target Element = property, Element Name = Relationship.parentEntityName, TypeSpec Element: `entityName` + +### Quick Reference: Mapping Language Naming to TypeSpec Elements + +| Language | Target Language (snake_case) | TypeSpec Equivalent (camelCase) | Notes | +|-----------|-------------------------------------|-----------------------------------|---------------------------------------------| +| Property | `parent_entity_name` | `parentEntityName` | Target: snake_case; TypeSpec: camelCase | +| Method | `get_foo_bar()` | `getFooBar()` | Target: snake_case; TypeSpec: camelCase | +| Class | `Relationship` | `Relationship` | Both: PascalCase | +| Rename | "rename foo_bar to new_bar" | Rename `fooBar` to `newBar` | Map snake_case to camelCase for TypeSpec | + +**Enforcement:** +- Before making any code changes, always copy and fill out the comment analysis template below for each review comment. +- If a review comment requests a snake_case name, explicitly document the correct TypeSpec camelCase/PascalCase name in your analysis. +- **🚨 REMINDER: client decorator target values must ALWAYS follow TypeSpec conventions: PascalCase for interfaces/classes, camelCase for operations/properties.** +- **🚨 NEVER use snake_case, kebab-case, or any other naming convention in the target name - ONLY TypeSpec conventions.** + +**Extract and analyze using the same template:** + +``` +**Comment**: [exact quote from review feedback] +**Target Language**: [Python, C#, Java, JavaScript, etc.] +**TypeSpec Element**: [TypeSpec name to be changed, in camelCase or PascalCase] +**TypeSpec Element Type**: [model/property/operation/interface/enum] +**Action**: [client.tsp] +**File**: [specific file to modify] +**Mapped TypeSpec Name**: [camelCase or PascalCase name to use in TypeSpec - MUST follow TypeSpec conventions] +``` + +**Note**: For comments requiring multiple actions, fill out the template for each action. + +**Critical Naming Convention Rule:** +When using `@@clientName`, the target name (second parameter) must follow TypeSpec conventions: +- **Interfaces/Classes**: PascalCase (e.g., "Entities", "Relationships", "HealthModels") +- **Operations/Methods**: camelCase (e.g., "getEntity", "listSignals") +- **Properties**: camelCase (e.g., "entityName", "signalData") + +**Examples of CORRECT target naming:** +```tsp +// ✅ CORRECT - Interface renamed to PascalCase +@@clientName(Namespace.EntityOperations, "Entities", "python"); + +// ✅ CORRECT - Operation renamed to camelCase +@@clientName(Namespace.SomeInterface.get_something, "getSomething", "python"); + +// ❌ WRONG - Using snake_case (target language convention) for TypeSpec target +@@clientName(Namespace.EntityOperations, "entities", "python"); +``` + +### Scenario 1: Operations Directly on Client +**Trigger**: Comments about operations being directly on the client rather than in separate `*Operation` classes. +**Action**: Use `@client` decorator in `client.tsp` to define a root client interface with operation aliases +**Example**: +```tsp +import "@azure-tools/typespec-client-generator-core"; +import "@typespec/versioning"; +import "./main.tsp"; + +using Azure.ClientGenerator.Core; +using TypeSpec.Versioning; + +@useDependency(AzureHealthInsights.Versions.v2024_08_01_preview) +namespace ClientCustomizations; + +@client({ + name: "RadiologyInsightsClient", + service: AzureHealthInsights, +}) +interface RadiologyInsightsClient { + // Only operations used here and models/enum/union used by those operations will be public - all others become internal + inferRadiologyInsights is AzureHealthInsights.RadiologyInsights.createJob; +} +``` +**Important Notes for @@client**: +- Only operations referenced in the `@client` interface -- and models/enum/union used by those operations -- will be public. All other operations/interfaces will be internal by default. + +### Scenario 2: Renaming client +**Trigger**: Comments about changing `SomeClient` class name to `SomeOtherClient` +**Action**: First check how the client is currently defined, then apply appropriate changes: +1. **Check client.tsp for `@client` decorator** +2. **If `@client` exists**: Update the `name` property and attached interface - use the desired name WITH "Client" suffix +3. **If no `@client`**: Add `@@clientName` - use the desired name WITHOUT "Client" suffix + +**Examples**: See complete `client.tsp` examples in "Complete `client.tsp` File Examples" section. + +**Key points for @@clientName**: +- Use the augment decorator `@@clientName` (double @), not the regular decorator `@clientName` +- The client name should NOT include "Client" suffix (it's added automatically) + +### Scenario 3: Renaming non-client TypeSpec elements +**Trigger**: Comments about renaming any TypeSpec elements (models, properties, operations, interfaces, enum members, parameters, etc.) +**Action**: Use `@@clientName` with appropriate path syntax + +**Examples**: +```tsp +// In client.tsp + +// Renaming model +@@clientName(Microsoft.Service.DatabaseChangeTierDefinition, "DatabaseChangeTierProperties"); // all languages +@@clientName(Microsoft.Service.Identity, "IdentityProperties", "csharp"); // csharp + +// Renaming model property +@@clientName(Microsoft.Service.Relationship.parentEntityName, "fromEntity", "python"); + +// Renaming operation parameter +@@clientName(Microsoft.Service.SomeInterface.createOrUpdate::parameters.resource, "body"); + +// Renaming interface +@@clientName(Microsoft.Service.EndpointResources, "Endpoints", "python"); + +// Renaming enum/union member +@@clientName(Microsoft.Service.AdvertiseMode.BGP, "Bgp", "csharp"); + +// Renaming API version value +@@clientName(Microsoft.Service.Versions.`2025-04-01`, "V20250401", "javascript"); +``` + +### Scenario 4: Access Control Changes +**Trigger**: Comments about making elements internal/private or changing visibility +**Actions**: Use `@@access` decorator in `client.tsp` + +**Important Notes:** +- `@@access` can only be applied to: ModelProperty, Model, Operation, Enum, Union, or Namespace +- `@@access` **CANNOT** be applied to interfaces +- If the `@client` decorator is being used to define a custom client, all operations/interfaces that are not referenced will be private by default. + +**Examples**: +```tsp +// Make model internal for python +@@access(Namespace.Model, Access.internal, "python"); + +// Make enum internal +@@access(Namespace.SomeEnum, Access.internal, "python"); + +// Language-specific access +@@access(Namespace.Model, Access.internal, "python"); + +// ❌ WRONG - Cannot apply to interfaces +// @@access(Namespace.SomeOperations, Access.internal, "python"); + +// ✅ CORRECT - Use @client decorator instead for interfaces +@client({ + name: "ServiceClient", + service: Namespace, +}) +interface ServiceClient { + // Only operations referenced here will be public and directly on the client. All other operations will be private. + getSomething is Namespace.SomeOperations.getSomething; +} +``` + +### Complete `client.tsp` File Examples + +**Basic client name customization:** +```tsp +import "@azure-tools/typespec-client-generator-core"; +import "@typespec/versioning"; +import "./main.tsp"; + +using Azure.ClientGenerator.Core; +using TypeSpec.Versioning; + +@useDependency(Microsoft.CloudHealth.HealthModels.Data.Versions.v2025_06_18_preview) +namespace ClientCustomizations; + +// Rename the main client (from namespace-based generation) +@@clientName(Microsoft.CloudHealth.HealthModels.Data, "HealthModels", "python"); +``` + +**Using @client decorator for root client interface:** +```tsp +import "@azure-tools/typespec-client-generator-core"; +import "@typespec/versioning"; +import "./main.tsp"; + +using Azure.ClientGenerator.Core; +using TypeSpec.Versioning; + +@useDependency(AzureHealthInsights.Versions.v2024_08_01_preview) +namespace ClientCustomizations; + +@client({ + name: "RadiologyInsightsClient", + service: AzureHealthInsights, +}) +interface RadiologyInsightsClient { + inferRadiologyInsights is AzureHealthInsights.RadiologyInsights.createJob; +} + +// rename the operation only for Python +@@clientName(RadiologyInsightsClient.inferRadiologyInsights, "inferInsights", "python") +``` + +### Critical Rules +- **Client Priority**: `client.tsp` is always checked first for `@client` decorator. If present, it defines the client and overrides namespace-based client generation. If not present, the namespace defined in main.tsp is used for the client name. +- **@@clientName** = Specify name WITHOUT "Client" suffix (it's added automatically) +- **@client interface** = Interface name SHOULD end with "Client" (explicit naming) +- **Namespace naming** = Should NOT end with "Client" (auto-appended for namespace-based generation) +- **Main client name** = Last namespace segment + "Client" (when using namespace-based generation) +- **Operations classes** = Interface name + "Operations" +- **NEVER** name interfaces ending with "Operations" (creates "OperationsOperations") +- **NEVER** add unnecessary comments to TypeSpec files. + +## Step 3: Implementation Process + +### Implementation Rules + +- **IMPORTANT**: Changes should **ONLY** be made to `client.tsp`. Do not modify other `.tsp` files unless explicitly listed in the exceptions section below. +- **AVOID** adding comments to TypeSpec files. + +1. **IMPLEMENT** your Implementation Strategy Plan from Step 3 to files in this order: + - `client.tsp` (naming/access customizations) + - Other `.tsp` files only if listed in exceptions section + +## CRITICAL ENFORCEMENT CHECKPOINT +**BEFORE moving to validation, STOP and verify:** +- [ ] Did I actually call `create_file` or `replace_string_in_file` or `insert_edit_into_file`? +- [ ] Can I see my changes reflected in the file content? +- [ ] Are the exact planned changes present in the code? +- [ ] Do target names follow TypeSpec conventions (PascalCase for interfaces, camelCase for operations/properties)? + +**If ANY answer is NO, implement immediately before continuing.** + - `client.tsp` (naming/access customizations) + - Other `.tsp` files only if listed in exceptions section + +## Step 5: Post-Implementation Validation + +For all steps below, keep going until the validation has completed successfully and all compilation errors are completely resolved, before ending your turn and yielding back to the user. +1. **Validate TypeSpec**: Run `tsp compile client.tsp` from project root. +2. **Fix any compilation errors** before proceeding + +Let the user know that they can now generate the SDK with these changes. + + +[TypeSpec Language Basics Docs]: https://typespec.io/docs/language-basics/overview/ \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 25c35c06f96b..1f05ffe67549 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -156,6 +156,7 @@ from the list of paths. If user does not have a TypeSpec project, then prompt us ### Pre-requisites - User should have a GitHub account and should be logged in to GitHub account using GitHub CLI `gh auth login`. +- run `npm ci` to install the dependencies ### Basic Rules for SDK Generation from TypeSpec @@ -168,34 +169,24 @@ from the list of paths. If user does not have a TypeSpec project, then prompt us - Use the path of the `tspconfig.yaml` file already open in the editor or the `.tsp` file path as the project root. - If no `.tsp` file or folder is in the current context, prompt the user to select a valid TypeSpec project root path. -3. **Pull Request Management**: - - Always start by checking if a pull request exists for spec changes before proceeding with validation or SDK - generation. - - Use `GetPullRequestForCurrentBranch` to query pull requests instead of the `gh` CLI. - - Provide a detailed pull request summary, including: - - Title, link, author, assignee, status (open, closed, merged), and mergeability. - - Check statuses (success or failure) with links and detailed failure reasons. - - API views for generated SDKs under the heading `API View for Generated SDK APIs`. - - Comments and action items for the user. - -4. **Process Visibility**: +3. **Process Visibility**: - Highlight all steps in the SDK generation process, showing completed and remaining steps. - Do not skip any main steps. Ensure all steps are completed before moving to the next. -5. **Git Operations**: +4. **Git Operations**: - Avoid using the `main` branch for pull requests. Prompt the user to create or switch to a new branch if necessary. - Display git commands (e.g., `git checkout`, `git add`, `git commit`, `git push`) with a "Run" button instead of asking the user to copy and paste. - Do not run `git diff` -6. **Azure-Specific Rules**: +5. **Azure-Specific Rules**: - Always use `Azure` as the repo owner in MCP tool calls. - Confirm with the user if they want to change the repo owner or target branch, and prompt for new values if needed. -7. **Exclusions**: +6. **Exclusions**: - Exclude changes in `.github` and `.vscode` folders from API spec and SDK pull requests. -8. **Working Branch Rule**: +7. **Working Branch Rule**: - Ensure the TypeSpec project repository and the current working repository are not on the `main` branch: - Check the current branch name for the cloned GitHub repository: - If the current branch is `main`, prompt the user to create a new branch using @@ -211,4 +202,20 @@ By following these rules, the SDK release process will remain clear, structured, ## Steps to generate SDK from TypeSpec API specification Follow `/typespec-to-sdk` prompt to generate and release SDK from TypeSpec API specification. The process is divided into several steps, each with specific actions to ensure a smooth SDK generation and release process. -Do not skip the step that choose SDK generation method to ensure the user selects the appropriate method for SDK generation, either locally or using the SDK generation pipeline. Do not repeat the steps. \ No newline at end of file +Do not skip the step that choose SDK generation method to ensure the user selects the appropriate method for SDK generation, either locally or using the SDK generation pipeline. Do not repeat the steps. + +1. **Identify TypeSpec Project**: Locate the TypeSpec project root path by checking for `tspconfig.yaml` or `main.tsp` files. +2. **Validate TypeSpec Specification**: Ensure the TypeSpec specification compiles without errors. +3. **Verify Authentication and Repository Status**: Ensure user is authenticated and working in the correct public Azure repository. +4. **Review and Commit Changes**: Stage and commit TypeSpec modifications, ensuring the current branch is not "main". Do not create pull request yet. +5. **Create Specification Pull Request**: Create a pull request for TypeSpec changes if not already created. This is required only if there are TypeSpec changes in current branch. +6. **Choose SDK Generation Method**: Determine how to generate SDKs (locally or via pipeline). Only Python is supported for local SDK generation at this time. +7. **Generate SDKs via Pipeline**: Generate SDKs using `/run-sdk-gen-pipeline` prompt, monitor the pipeline status and displaying generated SDK PR links. +8. **Show generated SDK PR**: Display the generated SDK pull request links for review. +9. **Create a release plan**: To create a release plan refer to #file:.\prompts\create-release-plan.prompt.md +10. **Prompt user to change spec pull request to ready for review from draft status**: Update spec pull request to change it to ready for review. +11. **Release package**: Release the SDK package using `ReleaseSdkPackage` tool. + + +## Release readiness of SDK and information about the release pipeline +Run `/check-package-readiness` prompt to check the release readiness of an SDK package. This prompt will collect the required information from the user, execute the readiness check, and present the results. \ No newline at end of file diff --git a/.github/cspell.yaml b/.github/cspell.yaml index 9fcc52014a45..bdb132159785 100644 --- a/.github/cspell.yaml +++ b/.github/cspell.yaml @@ -1,4 +1,4 @@ -version: '0.2' +version: "0.2" import: - ../cspell.yaml words: diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dd1b02809a11..01a6589f7173 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,9 +1,13 @@ version: 2 updates: + # For monorepos, only the root folder should be listed (not the sub-folders). + # We may also need to set "versioning-strategy" to "increase" rather than "increase-if-necessary", + # to update both package.json and package-lock.json files. In our case, we don't really care + # if the package.json files are updated or not, just that package-lock.json is update. + # https://github.com/dependabot/dependabot-core/issues/4993#issuecomment-1289133027 - package-ecosystem: "npm" directories: - "/" - - "/eng/tools/**" schedule: interval: "daily" ignore: @@ -16,8 +20,6 @@ updates: - dependency-name: "typescript" # Updated manually by the Liftr team - dependency-name: "@azure-tools/typespec-liftr-base" - # Updated manually by owner of eng/tools/lint-diff - - dependency-name: "@apidevtools/json-schema-ref-parser" - dependency-name: "@types/js-yaml" - dependency-name: "autorest" - dependency-name: "js-yaml" @@ -39,8 +41,8 @@ updates: eslint: patterns: - "*eslint*" - # Leave the constraint if the original constraint allows the new version, otherwise, bump the constraint. - versioning-strategy: increase-if-necessary + # I prefer "increase-if-necessary", but I believe we may need to use "increase" since we're a monorepo + versioning-strategy: increase - package-ecosystem: "npm" directories: - "/.github" @@ -61,5 +63,5 @@ updates: eslint: patterns: - "*eslint*" - # Leave the constraint if the original constraint allows the new version, otherwise, bump the constraint. - versioning-strategy: increase-if-necessary + # I prefer "increase-if-necessary", but we should align with the root package.json + versioning-strategy: increase diff --git a/.github/eslint.config.js b/.github/eslint.config.js index 366b15cfe396..85ab9868ef40 100644 --- a/.github/eslint.config.js +++ b/.github/eslint.config.js @@ -1,8 +1,8 @@ -import pluginJs from "@eslint/js"; +import eslint from "@eslint/js"; import globals from "globals"; +import tseslint from "typescript-eslint"; /** @type {import('eslint').Linter.Config[]} */ -export default [ - { languageOptions: { globals: globals.node } }, - pluginJs.configs.recommended, -]; +export default tseslint.config(eslint.configs.recommended, tseslint.configs.recommended, { + languageOptions: { globals: globals.node }, +}); diff --git a/.github/matchers/actionlint.json b/.github/matchers/actionlint.json new file mode 100644 index 000000000000..4613e1617bfe --- /dev/null +++ b/.github/matchers/actionlint.json @@ -0,0 +1,17 @@ +{ + "problemMatcher": [ + { + "owner": "actionlint", + "pattern": [ + { + "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$", + "file": 1, + "line": 2, + "column": 3, + "message": 4, + "code": 5 + } + ] + } + ] +} diff --git a/.github/package-lock.json b/.github/package-lock.json index 1e0226ff2ea5..f7d36cf703b0 100644 --- a/.github/package-lock.json +++ b/.github/package-lock.json @@ -5,26 +5,32 @@ "packages": { "": { "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.9.3", + "@apidevtools/json-schema-ref-parser": "^14.1.1", "debug": "^4.4.0", "js-yaml": "^4.1.0", - "marked": "^15.0.7", - "simple-git": "^3.27.0" + "markdown-table": "^3.0.4", + "marked": "^16.1.1", + "simple-git": "^3.27.0", + "zod": "^4.0.2" }, "devDependencies": { + "@actions/github-script": "github:actions/github-script", "@eslint/js": "^9.22.0", + "@octokit/rest": "^22.0.0", "@octokit/webhooks-types": "^7.5.1", "@tsconfig/node20": "^20.1.4", "@types/debug": "^4.1.12", - "@types/github-script": "github:actions/github-script", "@types/js-yaml": "^4.0.9", "@types/node": "^20.0.0", "@vitest/coverage-v8": "^3.0.7", + "cross-env": "^7.0.3", "eslint": "^9.22.0", "globals": "^16.0.0", "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "semver": "^7.7.1", "typescript": "~5.8.2", + "typescript-eslint": "^8.38.0", "vitest": "^3.0.7" } }, @@ -50,16 +56,39 @@ } }, "node_modules/@actions/github": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.0.tgz", - "integrity": "sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.1.tgz", + "integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==", "dev": true, "license": "MIT", "dependencies": { "@actions/http-client": "^2.2.0", "@octokit/core": "^5.0.1", - "@octokit/plugin-paginate-rest": "^9.0.0", - "@octokit/plugin-rest-endpoint-methods": "^10.0.0" + "@octokit/plugin-paginate-rest": "^9.2.2", + "@octokit/plugin-rest-endpoint-methods": "^10.4.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "undici": "^5.28.5" + } + }, + "node_modules/@actions/github-script": { + "version": "7.0.1", + "resolved": "git+ssh://git@github.com/actions/github-script.git#f28e40c7f34bde8b3046d885e986cb6290c5673b", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/exec": "^1.1.1", + "@actions/github": "^6.0.0", + "@actions/glob": "^0.4.0", + "@actions/io": "^1.1.3", + "@octokit/core": "^5.0.1", + "@octokit/plugin-request-log": "^4.0.0", + "@octokit/plugin-retry": "^6.0.1", + "@types/node": "^20.9.0" + }, + "engines": { + "node": ">=20.0.0 <21.0.0" } }, "node_modules/@actions/glob": { @@ -106,17 +135,16 @@ } }, "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "11.9.3", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", - "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.1.1.tgz", + "integrity": "sha512-uGF1YGOzzD50L7HLNWclXmsEhQflw8/zZHIz0/AzkJrKL5r9PceUipZxR/cp/8veTk4TVfdDJLyIwXLjaP5ePg==", "license": "MIT", "dependencies": { - "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" }, "engines": { - "node": ">= 16" + "node": ">= 20" }, "funding": { "url": "https://github.com/sponsors/philsturgeon" @@ -143,13 +171,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz", - "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -159,9 +187,9 @@ } }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -183,9 +211,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", - "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", "cpu": [ "ppc64" ], @@ -200,9 +228,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", - "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", "cpu": [ "arm" ], @@ -217,9 +245,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", - "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", "cpu": [ "arm64" ], @@ -234,9 +262,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", - "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", "cpu": [ "x64" ], @@ -251,9 +279,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", - "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", "cpu": [ "arm64" ], @@ -268,9 +296,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", - "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", "cpu": [ "x64" ], @@ -285,9 +313,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", - "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", "cpu": [ "arm64" ], @@ -302,9 +330,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", - "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", "cpu": [ "x64" ], @@ -319,9 +347,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", - "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", "cpu": [ "arm" ], @@ -336,9 +364,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", - "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", "cpu": [ "arm64" ], @@ -353,9 +381,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", - "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", "cpu": [ "ia32" ], @@ -370,9 +398,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", - "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", "cpu": [ "loong64" ], @@ -387,9 +415,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", - "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", "cpu": [ "mips64el" ], @@ -404,9 +432,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", - "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", "cpu": [ "ppc64" ], @@ -421,9 +449,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", - "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", "cpu": [ "riscv64" ], @@ -438,9 +466,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", - "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", "cpu": [ "s390x" ], @@ -455,9 +483,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", - "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", "cpu": [ "x64" ], @@ -472,9 +500,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", - "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", "cpu": [ "arm64" ], @@ -489,9 +517,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", - "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", "cpu": [ "x64" ], @@ -506,9 +534,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", - "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", "cpu": [ "arm64" ], @@ -523,9 +551,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", - "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", "cpu": [ "x64" ], @@ -539,10 +567,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", - "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", "cpu": [ "x64" ], @@ -557,9 +602,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", - "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", "cpu": [ "arm64" ], @@ -574,9 +619,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", - "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", "cpu": [ "ia32" ], @@ -591,9 +636,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", - "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", "cpu": [ "x64" ], @@ -650,9 +695,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -665,9 +710,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -675,9 +720,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -725,9 +770,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", - "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, "license": "MIT", "engines": { @@ -748,13 +793,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.1", "levn": "^0.4.1" }, "engines": { @@ -824,9 +869,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -866,18 +911,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -890,27 +931,17 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -918,12 +949,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "license": "MIT" - }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -939,6 +964,44 @@ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", "license": "MIT" }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@octokit/auth-token": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", @@ -950,9 +1013,9 @@ } }, "node_modules/@octokit/core": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", - "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "dev": true, "license": "MIT", "dependencies": { @@ -1132,6 +1195,186 @@ "node": ">= 18" } }, + "node_modules/@octokit/rest": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.0.tgz", + "integrity": "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/core": "^7.0.2", + "@octokit/plugin-paginate-rest": "^13.0.1", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/core": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.3.tgz", + "integrity": "sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.1", + "@octokit/request": "^10.0.2", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/endpoint": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", + "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/graphql": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz", + "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.2", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/rest/node_modules/@octokit/plugin-paginate-rest": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.1.1.tgz", + "integrity": "sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.1.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-16.0.0.tgz", + "integrity": "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.1.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/request": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", + "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/request-error": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", + "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@octokit/rest/node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@octokit/rest/node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "dev": true, + "license": "ISC" + }, "node_modules/@octokit/types": { "version": "13.10.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", @@ -1161,9 +1404,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", - "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz", + "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", "cpu": [ "arm" ], @@ -1175,9 +1418,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", - "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz", + "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", "cpu": [ "arm64" ], @@ -1189,9 +1432,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz", - "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz", + "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", "cpu": [ "arm64" ], @@ -1203,9 +1446,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", - "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz", + "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", "cpu": [ "x64" ], @@ -1217,9 +1460,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", - "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz", + "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", "cpu": [ "arm64" ], @@ -1231,9 +1474,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", - "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz", + "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", "cpu": [ "x64" ], @@ -1245,9 +1488,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", - "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz", + "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", "cpu": [ "arm" ], @@ -1259,9 +1502,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", - "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz", + "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", "cpu": [ "arm" ], @@ -1273,9 +1516,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", - "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz", + "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", "cpu": [ "arm64" ], @@ -1287,9 +1530,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", - "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz", + "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", "cpu": [ "arm64" ], @@ -1301,9 +1544,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", - "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz", + "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", "cpu": [ "loong64" ], @@ -1315,9 +1558,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", - "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz", + "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", "cpu": [ "ppc64" ], @@ -1329,9 +1572,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", - "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz", + "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", "cpu": [ "riscv64" ], @@ -1343,9 +1586,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", - "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz", + "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", "cpu": [ "riscv64" ], @@ -1357,9 +1600,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", - "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz", + "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", "cpu": [ "s390x" ], @@ -1371,9 +1614,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", - "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz", + "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", "cpu": [ "x64" ], @@ -1385,9 +1628,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", - "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz", + "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", "cpu": [ "x64" ], @@ -1399,9 +1642,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", - "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz", + "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", "cpu": [ "arm64" ], @@ -1413,9 +1656,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", - "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz", + "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", "cpu": [ "ia32" ], @@ -1427,9 +1670,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", - "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz", + "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", "cpu": [ "x64" ], @@ -1441,12 +1684,22 @@ ] }, "node_modules/@tsconfig/node20": { - "version": "20.1.5", - "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.5.tgz", - "integrity": "sha512-Vm8e3WxDTqMGPU4GATF9keQAIy1Drd7bPwlgzKJnZtoOsTm1tduUTbDjg0W5qERvGuxPI2h9RbMufH0YdfBylA==", + "version": "20.1.6", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.6.tgz", + "integrity": "sha512-sz+Hqx9zwZDpZIV871WSbUzSqNIsXzghZydypnfgzPKLltVJfkINfUeTct31n/tTSa9ZE1ZOfKdRre1uHHquYQ==", "dev": true, "license": "MIT" }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1457,33 +1710,19 @@ "@types/ms": "*" } }, - "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, - "node_modules/@types/github-script": { - "name": "@actions/github-script", - "version": "7.0.1", - "resolved": "git+ssh://git@github.com/actions/github-script.git#e7aeb8c663f696059ebb5f9ab1425ed2ef511bdb", + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, - "license": "MIT", - "dependencies": { - "@actions/core": "^1.10.1", - "@actions/exec": "^1.1.1", - "@actions/github": "^6.0.0", - "@actions/glob": "^0.4.0", - "@actions/io": "^1.1.3", - "@octokit/core": "^5.0.1", - "@octokit/plugin-request-log": "^4.0.0", - "@octokit/plugin-retry": "^6.0.1", - "@types/node": "^20.9.0" - }, - "engines": { - "node": ">=20.0.0 <21.0.0" - } + "license": "MIT" }, "node_modules/@types/js-yaml": { "version": "4.0.9", @@ -1506,25 +1745,284 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.17.32", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.32.tgz", - "integrity": "sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==", + "version": "20.19.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz", + "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", + "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/type-utils": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.38.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", + "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", + "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.38.0", + "@typescript-eslint/types": "^8.38.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", + "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", + "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", + "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", + "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", + "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.38.0", + "@typescript-eslint/tsconfig-utils": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", + "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", + "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "@typescript-eslint/types": "8.38.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@vitest/coverage-v8": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.2.tgz", - "integrity": "sha512-XDdaDOeaTMAMYW7N63AqoK32sYUWbXnTkC6tEbVcu3RlU1bB9of32T+PGf8KZvxqLNqeXhafDFqCkwpf2+dyaQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", - "debug": "^4.4.0", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", @@ -1539,8 +2037,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.1.2", - "vitest": "3.1.2" + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1549,14 +2047,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.2.tgz", - "integrity": "sha512-O8hJgr+zREopCAqWl3uCVaOdqJwZ9qaDwUP7vy3Xigad0phZe9APxKhPcDNqYYi0rX5oMvwJMSCAXY2afqeTSA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.2", - "@vitest/utils": "3.1.2", + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -1565,13 +2064,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.2.tgz", - "integrity": "sha512-kOtd6K2lc7SQ0mBqYv/wdGedlqPdM/B38paPY+OwJ1XiNi44w3Fpog82UfOibmHaV9Wod18A09I9SCKLyDMqgw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.2", + "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -1580,7 +2079,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -1592,9 +2091,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz", - "integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -1605,27 +2104,28 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.2.tgz", - "integrity": "sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.2", - "pathe": "^2.0.3" + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.2.tgz", - "integrity": "sha512-Q1qkpazSF/p4ApZg1vfZSQ5Yw6OCQxVMVrLjslbLFA1hMDrT2uxtqMaw8Tc/jy5DLka1sNs1Y7rBcftMiaSH/Q==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.2", + "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -1634,27 +2134,27 @@ } }, "node_modules/@vitest/spy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.2.tgz", - "integrity": "sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^3.0.2" + "tinyspy": "^4.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.2.tgz", - "integrity": "sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.2", - "loupe": "^3.1.3", + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, "funding": { @@ -1662,9 +2162,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -1746,6 +2246,18 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz", + "integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1768,14 +2280,27 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" } }, "node_modules/cac": { @@ -1799,9 +2324,9 @@ } }, "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", "dev": true, "license": "MIT", "dependencies": { @@ -1812,7 +2337,7 @@ "pathval": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/chalk": { @@ -1869,6 +2394,25 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1947,9 +2491,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", - "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1960,31 +2504,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.3", - "@esbuild/android-arm": "0.25.3", - "@esbuild/android-arm64": "0.25.3", - "@esbuild/android-x64": "0.25.3", - "@esbuild/darwin-arm64": "0.25.3", - "@esbuild/darwin-x64": "0.25.3", - "@esbuild/freebsd-arm64": "0.25.3", - "@esbuild/freebsd-x64": "0.25.3", - "@esbuild/linux-arm": "0.25.3", - "@esbuild/linux-arm64": "0.25.3", - "@esbuild/linux-ia32": "0.25.3", - "@esbuild/linux-loong64": "0.25.3", - "@esbuild/linux-mips64el": "0.25.3", - "@esbuild/linux-ppc64": "0.25.3", - "@esbuild/linux-riscv64": "0.25.3", - "@esbuild/linux-s390x": "0.25.3", - "@esbuild/linux-x64": "0.25.3", - "@esbuild/netbsd-arm64": "0.25.3", - "@esbuild/netbsd-x64": "0.25.3", - "@esbuild/openbsd-arm64": "0.25.3", - "@esbuild/openbsd-x64": "0.25.3", - "@esbuild/sunos-x64": "0.25.3", - "@esbuild/win32-arm64": "0.25.3", - "@esbuild/win32-ia32": "0.25.3", - "@esbuild/win32-x64": "0.25.3" + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" } }, "node_modules/escape-string-regexp": { @@ -2001,19 +2546,19 @@ } }, "node_modules/eslint": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz", - "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.14.0", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.28.0", + "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -2025,9 +2570,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2062,9 +2607,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2079,9 +2624,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2092,15 +2637,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2166,15 +2711,32 @@ } }, "node_modules/expect-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", - "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.0.0" } }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2182,6 +2744,36 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2196,19 +2788,14 @@ "dev": true, "license": "MIT" }, - "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" } }, "node_modules/file-entry-cache": { @@ -2224,6 +2811,19 @@ "node": ">=16.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2329,9 +2929,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2355,9 +2955,9 @@ } }, "node_modules/globals": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", - "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true, "license": "MIT", "engines": { @@ -2367,6 +2967,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2454,6 +3061,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2531,6 +3148,13 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2612,9 +3236,9 @@ "license": "MIT" }, "node_modules/loupe": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz", + "integrity": "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==", "dev": true, "license": "MIT" }, @@ -2663,16 +3287,50 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/marked": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.1.1.tgz", + "integrity": "sha512-ij/2lXfCRT71L6u0M29tJPhP0bM5shLL3u5BePhFwPELj2blMJ6GDtD7PfJhRLhJ/c2UwrK17ySVcDzy2YHjHQ==", "license": "MIT", "bin": { "marked": "bin/marked.js" }, "engines": { - "node": ">= 18" + "node": ">= 20" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" } }, "node_modules/minimatch": { @@ -2855,9 +3513,9 @@ "license": "MIT" }, "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", "engines": { @@ -2872,22 +3530,22 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -2905,7 +3563,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2939,6 +3597,23 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-plugin-organize-imports": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.2.0.tgz", + "integrity": "sha512-Zdy27UhlmyvATZi67BTnLcKTo8fm6Oik59Sz6H64PgZJVs6NJpPD1mT240mmJn62c98/QaL+r3kx9Q3gRpDajg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": ">=2.0", + "typescript": ">=2.9", + "vue-tsc": "^2.1.0 || 3" + }, + "peerDependenciesMeta": { + "vue-tsc": { + "optional": true + } + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2949,6 +3624,27 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2959,14 +3655,25 @@ "node": ">=4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rollup": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", - "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", + "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -2976,29 +3683,53 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.1", - "@rollup/rollup-android-arm64": "4.40.1", - "@rollup/rollup-darwin-arm64": "4.40.1", - "@rollup/rollup-darwin-x64": "4.40.1", - "@rollup/rollup-freebsd-arm64": "4.40.1", - "@rollup/rollup-freebsd-x64": "4.40.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", - "@rollup/rollup-linux-arm-musleabihf": "4.40.1", - "@rollup/rollup-linux-arm64-gnu": "4.40.1", - "@rollup/rollup-linux-arm64-musl": "4.40.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-musl": "4.40.1", - "@rollup/rollup-linux-s390x-gnu": "4.40.1", - "@rollup/rollup-linux-x64-gnu": "4.40.1", - "@rollup/rollup-linux-x64-musl": "4.40.1", - "@rollup/rollup-win32-arm64-msvc": "4.40.1", - "@rollup/rollup-win32-ia32-msvc": "4.40.1", - "@rollup/rollup-win32-x64-msvc": "4.40.1", + "@rollup/rollup-android-arm-eabi": "4.45.1", + "@rollup/rollup-android-arm64": "4.45.1", + "@rollup/rollup-darwin-arm64": "4.45.1", + "@rollup/rollup-darwin-x64": "4.45.1", + "@rollup/rollup-freebsd-arm64": "4.45.1", + "@rollup/rollup-freebsd-x64": "4.45.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", + "@rollup/rollup-linux-arm-musleabihf": "4.45.1", + "@rollup/rollup-linux-arm64-gnu": "4.45.1", + "@rollup/rollup-linux-arm64-musl": "4.45.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-musl": "4.45.1", + "@rollup/rollup-linux-s390x-gnu": "4.45.1", + "@rollup/rollup-linux-x64-gnu": "4.45.1", + "@rollup/rollup-linux-x64-musl": "4.45.1", + "@rollup/rollup-win32-arm64-msvc": "4.45.1", + "@rollup/rollup-win32-ia32-msvc": "4.45.1", + "@rollup/rollup-win32-x64-msvc": "4.45.1", "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -3056,14 +3787,14 @@ } }, "node_modules/simple-git": { - "version": "3.27.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.27.0.tgz", - "integrity": "sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==", + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.28.0.tgz", + "integrity": "sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==", "license": "MIT", "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", - "debug": "^4.3.5" + "debug": "^4.4.0" }, "funding": { "type": "github", @@ -3211,6 +3942,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3240,9 +3984,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3280,9 +4024,9 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3296,10 +4040,38 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinypool": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", "engines": { @@ -3317,15 +4089,41 @@ } }, "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", @@ -3363,6 +4161,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.38.0.tgz", + "integrity": "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.38.0", + "@typescript-eslint/parser": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/undici": { "version": "5.29.0", "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", @@ -3377,9 +4199,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, @@ -3401,24 +4223,24 @@ } }, "node_modules/vite": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", - "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", + "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "fdir": "^6.4.6", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -3427,14 +4249,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", - "less": "*", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -3476,17 +4298,17 @@ } }, "node_modules/vite-node": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.2.tgz", - "integrity": "sha512-/8iMryv46J3aK13iUXsei5G/A3CUlW4665THCPS+K8xAaqrVWiGB4RfXMQXCLjpK9P2eK//BczrVkn5JLAk6DA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.4.0", - "es-module-lexer": "^1.6.0", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" @@ -3498,33 +4320,63 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vitest": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.2.tgz", - "integrity": "sha512-WaxpJe092ID1C0mr+LH9MmNrhfzi8I65EX/NRU/Ld016KqQNRgxSOlGNP1hHN+a/F8L15Mh8klwaF77zR3GeDQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.2", - "@vitest/mocker": "3.1.2", - "@vitest/pretty-format": "^3.1.2", - "@vitest/runner": "3.1.2", - "@vitest/snapshot": "3.1.2", - "@vitest/spy": "3.1.2", - "@vitest/utils": "3.1.2", + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", - "debug": "^4.4.0", + "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", + "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.13", - "tinypool": "^1.0.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.2", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -3540,8 +4392,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.2", - "@vitest/ui": "3.1.2", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, @@ -3569,6 +4421,19 @@ } } }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3726,6 +4591,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.8.tgz", + "integrity": "sha512-+MSh9cZU9r3QKlHqrgHMTSr3QwMGv4PLfR0M4N/sYWV5/x67HgXEhIGObdBkpnX8G78pTgWnIrBL2lZcNJOtfg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/.github/package.json b/.github/package.json index fb6f46695960..65cb98057057 100644 --- a/.github/package.json +++ b/.github/package.json @@ -6,35 +6,41 @@ "dependencies2": "All runtime and dev dependencies in this file, must be a superset of shared/package.json" }, "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.9.3", + "@apidevtools/json-schema-ref-parser": "^14.1.1", "debug": "^4.4.0", "js-yaml": "^4.1.0", - "marked": "^15.0.7", - "simple-git": "^3.27.0" + "marked": "^16.1.1", + "markdown-table": "^3.0.4", + "simple-git": "^3.27.0", + "zod": "^4.0.2" }, "devDependencies": { + "@actions/github-script": "github:actions/github-script", "@eslint/js": "^9.22.0", "@octokit/webhooks-types": "^7.5.1", + "@octokit/rest": "^22.0.0", "@tsconfig/node20": "^20.1.4", "@types/debug": "^4.1.12", - "@types/github-script": "github:actions/github-script", "@types/js-yaml": "^4.0.9", "@types/node": "^20.0.0", "@vitest/coverage-v8": "^3.0.7", + "cross-env": "^7.0.3", "eslint": "^9.22.0", "globals": "^16.0.0", "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "semver": "^7.7.1", "typescript": "~5.8.2", + "typescript-eslint": "^8.38.0", "vitest": "^3.0.7" }, "scripts": { "lint": "npm run lint:eslint && npm run lint:tsc", - "lint:eslint": "eslint", - "lint:tsc": "tsc && echo 'Type checking completed successfully'", - "prettier": "prettier \"**/*.js\" --check", - "prettier:debug": "prettier \"**/*.js\" --check ---log-level debug", - "prettier:write": "prettier \"**/*.js\" --write", + "lint:eslint": "cross-env DEBUG=eslint:eslint eslint", + "lint:tsc": "tsc --build --verbose", + "format": "prettier . --write", + "format:check": "prettier . --check", + "format:check:ci": "prettier . --check --log-level debug", "test": "vitest", "test:ci": "vitest run --coverage --reporter=verbose" } diff --git a/.github/prompts/check-package-readiness.prompt.md b/.github/prompts/check-package-readiness.prompt.md new file mode 100644 index 000000000000..cd30574baa12 --- /dev/null +++ b/.github/prompts/check-package-readiness.prompt.md @@ -0,0 +1,36 @@ +--- +mode: 'agent' +tools: ['CheckPackageReleaseReadiness'] +description: 'This prompt is designed to check the release readiness of a SDK package.' +--- +## Goal +Check the release readiness of an SDK package by collecting the required information from the user and executing the readiness check. + +## Instructions +1. **Collect Required Information**: + - Prompt the user for the exact package name + - Prompt the user to select the programming language from the following options (case sensitive): + - Python + - Java + - JavaScript + - .NET + - Go + +2. **Execute Readiness Check**: + - Use the `CheckPackageReleaseReadiness` tool with the provided package name and selected language + - Do not check for existing pull requests to run this step. + - Do not ask the user to create a release plan to run this step. + +3. **Present Results**: + - If the package is ready for release, highlight and provide the link to the release pipeline + - If the package is not ready, display the specific issues that need to be resolved + +4. **Follow-up Actions**: + - Provide clear next steps based on the readiness status + - If issues are found, offer guidance on how to resolve them + +## Expected User Interaction Flow +1. Ask: "What is the exact name of the package you want to check for release readiness?" +2. Ask: "Please select the programming language for this package: Python, Java, JavaScript, .NET, or Go" +3. Execute the readiness check using the provided information +4. Display results and next steps diff --git a/.github/prompts/create-release-plan.prompt.md b/.github/prompts/create-release-plan.prompt.md index df829b5f759f..9c760583d9ec 100644 --- a/.github/prompts/create-release-plan.prompt.md +++ b/.github/prompts/create-release-plan.prompt.md @@ -4,7 +4,7 @@ tools: ['CreateReleasePlan', 'GetReleasePlanForPullRequest', 'GetReleasePlan', ' --- # Release Plan Creation Process - +You goal is to create a valid release plan. You must prompt user to provide all required information and all input must match the format and requirement mentioned in step 3 below. Follow these steps in order to create or manage a release plan for an API specification pull request: ## Step 1: Validate Prerequisites @@ -20,17 +20,18 @@ Follow these steps in order to create or manage a release plan for an API specif - If no release plan exists, proceed to Step 3 ## Step 3: Gather Release Plan Information -Collect the following required information from the user. If any details are missing, prompt the user accordingly: +Collect the following required information from the user. Do not create a release plan with temporary values. Confirm the values with the user before proceeding to create the release plan. +If any details are missing, prompt the user accordingly: - **API Lifecycle Stage**: Must be one of: - Private Preview - Public Preview - GA (Generally Available) -- **Service Tree ID**: GUID format identifier for the service in Service Tree -- **Product Service Tree ID**: GUID format identifier for the product in Service Tree -- **Expected Release Timeline**: Format as "Month YYYY" (e.g., "March 2024") +- **Service Tree ID**: GUID format identifier for the service in Service Tree. Before creating release plan, always show the value to user and ask them to confirm it's a valid value in service tree. +- **Product Service Tree ID**: GUID format identifier for the product in Service Tree. Before creating release plan, always show the value to user and ask them to confirm it's a valid value in service tree. +- **Expected Release Timeline**: Format must be in "Month YYYY" - **API Version**: The version of the API being released -- **SDK Release Type**: +- **SDK Release Type**: Value must be beta or stable. - "beta" for preview API versions - "stable" for GA API versions @@ -39,15 +40,20 @@ Collect the following required information from the user. If any details are mis - Provide this resource: [Release Plan Creation Guide](https://eng.ms/docs/products/azure-developer-experience/plan/release-plan-create) - Once all information is gathered, use `CreateReleasePlan` to create the release plan - Display the newly created release plan details to the user for confirmation +- Run `/sdk-details-in-release-plan` to identify languages configured in the TypeSpec project and add them to the release plan + +## Step 5: Update SDK Details in Release Plan +- Run `/sdk-details-in-release-plan.prompt.md` to add languages and package names to the release plan +- If the TypeSpec project is for a management plane, run `/verify-namespace-approval` if this is first release of SDK. -## Step 5: Link SDK Pull Requests (if applicable) +## Step 6: Link SDK Pull Requests (if applicable) - Ask the user if they have already created SDK pull requests locally for any programming language - If SDK pull requests exist: - Collect the pull request links from the user - Use `LinkSdkPullRequestToReleasePlan` to link each SDK pull request to the release plan - Confirm successful linking for each SDK pull request -## Step 6: Summary +## Step 7: Summary - Display a summary of the completed actions: - Release plan status (created or existing) - Linked SDK pull requests (if any) diff --git a/.github/prompts/sdk-details-in-release-plan.prompt.md b/.github/prompts/sdk-details-in-release-plan.prompt.md new file mode 100644 index 000000000000..862b1470fb99 --- /dev/null +++ b/.github/prompts/sdk-details-in-release-plan.prompt.md @@ -0,0 +1,41 @@ +--- +mode: 'agent' +description: 'Identify languages configured in the TypeSpec project and add it to release plan' +tools: ['GetReleasePlanForPullRequest', 'GetReleasePlan', 'UpdateReleasePlanSDKInfo'] +--- +# Step 1: Find the list of languages and package names +**Goal**: Identify languages configured in the TypeSpec project and generate the json object with language and package name. +1. Identify the language emitter configuration in the `tspconfig.yaml` file in the TypeSpec project root. +2. Identify the package name or namespace for each language emitter. +3. Map the language name in emitter to one of the following in Pascal case(except .NET): + - .NET + - Java + - Python + - JavaScript + - Go +4. Remove `github.com/Azure/azure-sdk-for-go/` from Go package name. +4. Create a JSON array object with the following structure: + ```json + [ + { + "language": "", + "packageName": "" + }, + ... + ] + ``` +5. If no languages are configured, inform the user: "No languages configured in TypeSpec project. Please add at least one language emitter in tspconfig.yaml." +**Success Criteria**: JSON object with languages and package names created. + +# Step 2: Check if release plan exists +**Goal**: Determine if a release plan exists for the API spec pull request or work item Id or release plan Id in current context. +1. Get release plan +2. If no release plan exists, inform the user: "No release plan exists for the API spec pull request. Please create a release plan first." +3. If a release plan exists, proceed to Step 3. +**Success Criteria**: Release plan exists or user informed to create one. + +# Step 3: Update Release Plan with SDK Information +**Goal**: Update the release plan with the languages and package names identified in Step 1. +1. Use `UpdateReleasePlanSDKInfo` to update the release plan work item with the JSON object created in Step 1. +2. Confirm successful update of the release plan with the SDK information and summary of languages and package names. +**Success Criteria**: Release plan updated with languages and package names. \ No newline at end of file diff --git a/.github/prompts/typespec-to-sdk.prompt.md b/.github/prompts/typespec-to-sdk.prompt.md index bf00f4ad4c7c..304910be1399 100644 --- a/.github/prompts/typespec-to-sdk.prompt.md +++ b/.github/prompts/typespec-to-sdk.prompt.md @@ -2,7 +2,7 @@ mode: 'agent' description: 'Generate SDKs from TypeSpec' --- -Your goal is to guide user through the process of generating SDKs from TypeSpec projects. +Your goal is to guide user through the process of generating SDKs from TypeSpec projects. Show all the high level steps to the user to ensure they understand the flow. Use the provided tools to perform actions and gather information as needed. ## Pre-Flight Check - Verify ${workspaceFolder} is not on main branch @@ -58,7 +58,7 @@ Your goal is to guide user through the process of generating SDKs from TypeSpec **Goal**: Determine how to generate SDKs **Actions**: 1. Present options: "How would you like to generate SDKs?" - - Option A: "Generate SDK locally" + - Option A: "Generate SDK locally". This is currently supported only for Python. Do not recommend this for other languages. - Option B: "Use SDK generation pipeline" 2. Based on selection: - If Option A: Run `/create-sdk-locally` and then proceed to Step 6 @@ -76,26 +76,41 @@ Your goal is to guide user through the process of generating SDKs from TypeSpec - Display created PR details **Success Criteria**: Specification pull request exists -## Step 7: Verify API Readiness -**Goal**: Ensure API specification PR is ready for SDK generation -**Actions**: -1. Run `/check-api-readiness` on the spec PR -2. If ready, proceed to Step 8 -3. If not ready: - - Display specific readiness issues - - Inform user: "The following actions are required before SDK generation: [list issues]" - - Wait for user to address issues -**Success Criteria**: API specification PR passes readiness checks - -## Step 8: Generate SDKs via Pipeline +## Step 7: Generate SDKs via Pipeline **Goal**: Create release plan and generate SDKs **Actions**: 1. Run `/create-release-plan` 2. If SDK PRs exist, link them to the release plan -3. Run `/run-sdk-gen-pipeline` with the spec PR -4. Monitor pipeline status and provide updates -5. Display generated SDK PR links when available +3. Run `/sdk-details-in-release-plan` to add languages and package names to the release plan +4. If TypeSpec project is for management plane, Run `/verify-namespace-approval` to check package namespace approval. +This step should not check package readiness to verify namespace approval for management plane SDK. +5. Run `/run-sdk-gen-pipeline` with the spec PR +6. Monitor pipeline status and provide updates +7. Display generated SDK PR links when available **Success Criteria**: SDK generation pipeline initiated and SDKs generated +## Step 8: Show Generated SDK PRs +**Goal**: Display all created SDK pull requests +**Actions**: +1. Run `GetSDKPullRequestDetails` to fetch generated SDK PR info. + +## Step 9: Create release plan +**Goal**: Create a release plan for the generated SDKs +**Actions**: +1. Run `/create-release-plan` to create a release plan using the spec pull request. +2. If the release plan already exists, display the existing plan details. + +## Step 10: Mark Spec PR as Ready for Review +**Goal**: Update spec PR to ready for review status +**Actions**: +1. Prompt user to change spec PR to ready for review: "Please change the spec pull request to ready for review status" +2. Get approval and merge the spec PR + +## Step 11: Release SDK Package +**Goal**: Release the SDK package using the release plan +**Actions**: +1. Run `ReleaseSdkPackage` to release the SDK package. +2. Inform user to approve the package release using release pipeline. + ## Process Complete Display summary of all created PRs and next steps for user. \ No newline at end of file diff --git a/.github/prompts/verify-namespace-approval.prompt.md b/.github/prompts/verify-namespace-approval.prompt.md new file mode 100644 index 000000000000..fa78f6ef6c76 --- /dev/null +++ b/.github/prompts/verify-namespace-approval.prompt.md @@ -0,0 +1,24 @@ +--- +mode: 'agent' +description: 'Verify SDK namespace approval for management plane' +tools: ['GetReleasePlan', 'GetReleasePlanForPullRequest', 'LinkNameSpaceApprovalIssue'] +--- +This task is required only for management plan API spec and only if a release plan exists for the API spec pull request. + +## Step 1: Check if release plan exists and it is for management plane SDK +**Goal**: Determine if a release plan exists for the API spec pull request or work item Id or release plan Id in current context. +**Actions**: +1. Get release plan and check if it is for management plane SDK +2. If not, inform user: "This task is only applicable for management plane SDKs. No action required." +3. Check if release plan already has namespace approval issue. Also prompt user to check if this is the first release of SDK. +4. If namespace approval issue exists, inform user: "Namespace approval issue already exists for this release plan.". Prompt user to +check if they want to link a different namespace approval issue to the release plan. Show namespace approval status. +5. Move to Step 2 if namespace approval issue does not exist or user wants to link a different namespace approval issue. + +## Step 2: Gather Namespace Approval Information +**Goal**: Link namespace approval issue to the release plan. +**Actions**: +1. Collect GitHub issue created in Azure/azure-sdk repo for namespace approval. Do not use any other repo name. +2. Run `LinkNameSpaceApprovalIssue` to link the issue to the release plan work item id. +3. Confirm successful linking of the namespace approval issue to the release plan. +**Success Criteria**: Namespace approval issue linked to the release plan or confirmed as already linked. diff --git a/.github/shared/.prettierignore b/.github/shared/.prettierignore deleted file mode 100644 index 4ebc8aea50e0..000000000000 --- a/.github/shared/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -coverage diff --git a/.github/shared/cmd/api-doc-preview.js b/.github/shared/cmd/api-doc-preview.js new file mode 100755 index 000000000000..d3ba8a05a372 --- /dev/null +++ b/.github/shared/cmd/api-doc-preview.js @@ -0,0 +1,137 @@ +#!/usr/bin/env node +// @ts-check + +import { mkdir, writeFile } from "fs/promises"; +import { dirname, join, resolve } from "path"; +import { fileURLToPath } from "url"; +import { parseArgs } from "util"; + +import { getChangedFilesStatuses, swagger } from "../src/changed-files.js"; + +import { + getSwaggersToProcess, + indexMd, + mappingJSONTemplate, + repoJSONTemplate, +} from "../src/doc-preview.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +function usage() { + console.log(`Usage: +npx api-doc-preview --output + +parameters: + --output Directory to write documentation artifacts to. + --build-id Build ID, used in the documentation index. Defaults to BUILD_BUILDID environment variable. + --spec-repo-name Name of the repository containing the swagger files of the form /. Defaults to BUILD_REPOSITORY_NAME environment variable. + --spec-repo-pr-number PR number of the repository containing the swagger files. Defaults to SYSTEM_PULLREQUEST_PULLREQUESTNUMBER environment variable. + --spec-repo-root Root path of the repository containing the swagger files. Defaults to the root of the repository containing this script.`); +} + +const { + values: { + output: outputDir, + "build-id": buildId, + "spec-repo-name": specRepoName, + "spec-repo-pr-number": specRepoPrNumber, + "spec-repo-root": specRepoRoot, + }, +} = parseArgs({ + options: { + output: { type: "string", default: "" }, + "build-id": { + type: "string", + default: process.env.BUILD_BUILDID || "", + }, + "spec-repo-name": { + type: "string", + default: process.env.BUILD_REPOSITORY_NAME || "", + }, + "spec-repo-pr-number": { + type: "string", + default: process.env.SYSTEM_PULLREQUEST_PULLREQUESTNUMBER || "", + }, + "spec-repo-root": { + type: "string", + default: resolve(__dirname, "../../../"), + }, + }, + allowPositionals: false, +}); + +let validArgs = true; + +if (!outputDir) { + console.log(`Missing required parameter --output. Value given: ${outputDir || ""}`); + validArgs = false; +} + +if (!specRepoName) { + console.log( + `Missing required parameter --spec-repo-name. Value given: ${specRepoName || ""}`, + ); + validArgs = false; +} + +if (!specRepoPrNumber) { + console.log( + `Missing required parameter --spec-repo-pr-number. Value given: ${specRepoPrNumber || ""}`, + ); + validArgs = false; +} + +if (!specRepoRoot) { + console.log(`Invalid parameter --spec-repo-root. Value given: ${specRepoRoot || ""}`); + validArgs = false; +} + +if (!validArgs) { + usage(); + process.exit(1); +} + +// Get selected version and swaggers to process + +const changedFileStatuses = await getChangedFilesStatuses({ + cwd: specRepoRoot, + paths: ["specification"], +}); + +// Exclude deleted files as they are not relevant for generating documentation. +const changedFiles = [ + ...changedFileStatuses.additions, + ...changedFileStatuses.modifications, + // Current names of renamed files are interesting, previous names are not. + ...changedFileStatuses.renames.map((r) => r.to), +]; +console.log(`Found ${changedFiles.length} relevant changed files in ${specRepoRoot}`); +console.log("Changed files:"); +changedFiles.forEach((file) => console.log(` - ${file}`)); +const swaggerPaths = changedFiles.filter(swagger); + +if (swaggerPaths.length === 0) { + console.log("No eligible swagger files found. No documentation artifacts will be written."); + process.exit(0); +} + +const { selectedVersion, swaggersToProcess } = getSwaggersToProcess(swaggerPaths); + +const repoName = specRepoName; +const prNumber = specRepoPrNumber; + +await mkdir(outputDir, { recursive: true }); +await writeFile( + join(outputDir, "repo.json"), + JSON.stringify(repoJSONTemplate(repoName, prNumber), null, 2), +); +await writeFile( + join(outputDir, "mapping.json"), + JSON.stringify(mappingJSONTemplate(swaggersToProcess), null, 2), +); +await writeFile(join(outputDir, "index.md"), indexMd(buildId, repoName, prNumber)); +console.log(`Documentation preview artifacts written to ${outputDir}`); + +console.log(`Doc preview for API version ${selectedVersion} includes:`); +swaggersToProcess.forEach((swagger) => console.log(` - ${swagger}`)); +console.log(`Artifacts written to: ${outputDir}`); diff --git a/.github/shared/cmd/spec-model.js b/.github/shared/cmd/spec-model.js index a6aab41b6119..85c28718cf5e 100755 --- a/.github/shared/cmd/spec-model.js +++ b/.github/shared/cmd/spec-model.js @@ -36,10 +36,4 @@ const specModel = new SpecModel(specPath, { logger: new ConsoleLogger(debug), }); -console.log( - JSON.stringify( - await specModel.toJSONAsync({ includeRefs, relativePaths }), - null, - 2, - ), -); +console.log(JSON.stringify(await specModel.toJSONAsync({ includeRefs, relativePaths }), null, 2)); diff --git a/.github/shared/eslint.config.js b/.github/shared/eslint.config.js index 366b15cfe396..d3c33d8d947d 100644 --- a/.github/shared/eslint.config.js +++ b/.github/shared/eslint.config.js @@ -2,7 +2,4 @@ import pluginJs from "@eslint/js"; import globals from "globals"; /** @type {import('eslint').Linter.Config[]} */ -export default [ - { languageOptions: { globals: globals.node } }, - pluginJs.configs.recommended, -]; +export default [{ languageOptions: { globals: globals.node } }, pluginJs.configs.recommended]; diff --git a/.github/shared/package-lock.json b/.github/shared/package-lock.json index 3f0b12e95248..9cbc1f0f1c22 100644 --- a/.github/shared/package-lock.json +++ b/.github/shared/package-lock.json @@ -6,13 +6,14 @@ "": { "name": "@azure-tools/specs-shared", "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.9.3", + "@apidevtools/json-schema-ref-parser": "^14.1.1", "debug": "^4.4.0", "js-yaml": "^4.1.0", - "marked": "^15.0.7", + "marked": "^16.1.1", "simple-git": "^3.27.0" }, "bin": { + "api-doc-preview": "cmd/api-doc-preview.js", "spec-model": "cmd/spec-model.js" }, "devDependencies": { @@ -22,9 +23,11 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^20.0.0", "@vitest/coverage-v8": "^3.0.7", + "cross-env": "^7.0.3", "eslint": "^9.22.0", "globals": "^16.0.0", "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "semver": "^7.7.1", "typescript": "~5.8.2", "vitest": "^3.0.7" @@ -45,17 +48,16 @@ } }, "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "11.9.3", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", - "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.1.1.tgz", + "integrity": "sha512-uGF1YGOzzD50L7HLNWclXmsEhQflw8/zZHIz0/AzkJrKL5r9PceUipZxR/cp/8veTk4TVfdDJLyIwXLjaP5ePg==", "license": "MIT", "dependencies": { - "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" }, "engines": { - "node": ">= 16" + "node": ">= 20" }, "funding": { "url": "https://github.com/sponsors/philsturgeon" @@ -82,13 +84,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz", - "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -98,9 +100,9 @@ } }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -122,9 +124,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", - "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", "cpu": [ "ppc64" ], @@ -139,9 +141,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", - "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", "cpu": [ "arm" ], @@ -156,9 +158,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", - "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", "cpu": [ "arm64" ], @@ -173,9 +175,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", - "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", "cpu": [ "x64" ], @@ -190,9 +192,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", - "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", "cpu": [ "arm64" ], @@ -207,9 +209,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", - "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", "cpu": [ "x64" ], @@ -224,9 +226,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", - "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", "cpu": [ "arm64" ], @@ -241,9 +243,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", - "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", "cpu": [ "x64" ], @@ -258,9 +260,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", - "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", "cpu": [ "arm" ], @@ -275,9 +277,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", - "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", "cpu": [ "arm64" ], @@ -292,9 +294,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", - "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", "cpu": [ "ia32" ], @@ -309,9 +311,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", - "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", "cpu": [ "loong64" ], @@ -326,9 +328,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", - "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", "cpu": [ "mips64el" ], @@ -343,9 +345,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", - "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", "cpu": [ "ppc64" ], @@ -360,9 +362,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", - "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", "cpu": [ "riscv64" ], @@ -377,9 +379,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", - "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", "cpu": [ "s390x" ], @@ -394,9 +396,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", - "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", "cpu": [ "x64" ], @@ -411,9 +413,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", - "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", "cpu": [ "arm64" ], @@ -428,9 +430,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", - "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", "cpu": [ "x64" ], @@ -445,9 +447,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", - "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", "cpu": [ "arm64" ], @@ -462,9 +464,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", - "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", "cpu": [ "x64" ], @@ -478,10 +480,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", - "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", "cpu": [ "x64" ], @@ -496,9 +515,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", - "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", "cpu": [ "arm64" ], @@ -513,9 +532,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", - "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", "cpu": [ "ia32" ], @@ -530,9 +549,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", - "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", "cpu": [ "x64" ], @@ -589,9 +608,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -604,9 +623,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -614,9 +633,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -664,9 +683,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", - "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, "license": "MIT", "engines": { @@ -687,13 +706,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.1", "levn": "^0.4.1" }, "engines": { @@ -753,9 +772,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -795,18 +814,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -819,27 +834,17 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -847,12 +852,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "license": "MIT" - }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -880,9 +879,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", - "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz", + "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", "cpu": [ "arm" ], @@ -894,9 +893,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", - "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz", + "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", "cpu": [ "arm64" ], @@ -908,9 +907,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz", - "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz", + "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", "cpu": [ "arm64" ], @@ -922,9 +921,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", - "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz", + "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", "cpu": [ "x64" ], @@ -936,9 +935,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", - "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz", + "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", "cpu": [ "arm64" ], @@ -950,9 +949,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", - "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz", + "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", "cpu": [ "x64" ], @@ -964,9 +963,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", - "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz", + "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", "cpu": [ "arm" ], @@ -978,9 +977,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", - "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz", + "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", "cpu": [ "arm" ], @@ -992,9 +991,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", - "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz", + "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", "cpu": [ "arm64" ], @@ -1006,9 +1005,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", - "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz", + "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", "cpu": [ "arm64" ], @@ -1020,9 +1019,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", - "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz", + "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", "cpu": [ "loong64" ], @@ -1034,9 +1033,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", - "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz", + "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", "cpu": [ "ppc64" ], @@ -1048,9 +1047,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", - "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz", + "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", "cpu": [ "riscv64" ], @@ -1062,9 +1061,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", - "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz", + "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", "cpu": [ "riscv64" ], @@ -1076,9 +1075,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", - "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz", + "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", "cpu": [ "s390x" ], @@ -1090,9 +1089,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", - "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz", + "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", "cpu": [ "x64" ], @@ -1104,9 +1103,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", - "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz", + "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", "cpu": [ "x64" ], @@ -1118,9 +1117,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", - "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz", + "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", "cpu": [ "arm64" ], @@ -1132,9 +1131,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", - "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz", + "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", "cpu": [ "ia32" ], @@ -1146,9 +1145,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", - "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz", + "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", "cpu": [ "x64" ], @@ -1160,12 +1159,22 @@ ] }, "node_modules/@tsconfig/node20": { - "version": "20.1.5", - "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.5.tgz", - "integrity": "sha512-Vm8e3WxDTqMGPU4GATF9keQAIy1Drd7bPwlgzKJnZtoOsTm1tduUTbDjg0W5qERvGuxPI2h9RbMufH0YdfBylA==", + "version": "20.1.6", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.6.tgz", + "integrity": "sha512-sz+Hqx9zwZDpZIV871WSbUzSqNIsXzghZydypnfgzPKLltVJfkINfUeTct31n/tTSa9ZE1ZOfKdRre1uHHquYQ==", "dev": true, "license": "MIT" }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1176,10 +1185,17 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -1204,25 +1220,26 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.17.32", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.32.tgz", - "integrity": "sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==", + "version": "20.19.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz", + "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, "node_modules/@vitest/coverage-v8": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.2.tgz", - "integrity": "sha512-XDdaDOeaTMAMYW7N63AqoK32sYUWbXnTkC6tEbVcu3RlU1bB9of32T+PGf8KZvxqLNqeXhafDFqCkwpf2+dyaQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", - "debug": "^4.4.0", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", @@ -1237,8 +1254,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.1.2", - "vitest": "3.1.2" + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1247,14 +1264,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.2.tgz", - "integrity": "sha512-O8hJgr+zREopCAqWl3uCVaOdqJwZ9qaDwUP7vy3Xigad0phZe9APxKhPcDNqYYi0rX5oMvwJMSCAXY2afqeTSA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.2", - "@vitest/utils": "3.1.2", + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -1263,13 +1281,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.2.tgz", - "integrity": "sha512-kOtd6K2lc7SQ0mBqYv/wdGedlqPdM/B38paPY+OwJ1XiNi44w3Fpog82UfOibmHaV9Wod18A09I9SCKLyDMqgw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.2", + "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -1278,7 +1296,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -1290,9 +1308,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz", - "integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -1303,27 +1321,28 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.2.tgz", - "integrity": "sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.2", - "pathe": "^2.0.3" + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.2.tgz", - "integrity": "sha512-Q1qkpazSF/p4ApZg1vfZSQ5Yw6OCQxVMVrLjslbLFA1hMDrT2uxtqMaw8Tc/jy5DLka1sNs1Y7rBcftMiaSH/Q==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.2", + "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -1332,27 +1351,27 @@ } }, "node_modules/@vitest/spy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.2.tgz", - "integrity": "sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^3.0.2" + "tinyspy": "^4.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.2.tgz", - "integrity": "sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.2", - "loupe": "^3.1.3", + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, "funding": { @@ -1360,9 +1379,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -1444,6 +1463,18 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz", + "integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1452,9 +1483,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1483,9 +1514,9 @@ } }, "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", "dev": true, "license": "MIT", "dependencies": { @@ -1496,7 +1527,7 @@ "pathval": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/chalk": { @@ -1553,6 +1584,25 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1624,9 +1674,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", - "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1637,31 +1687,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.3", - "@esbuild/android-arm": "0.25.3", - "@esbuild/android-arm64": "0.25.3", - "@esbuild/android-x64": "0.25.3", - "@esbuild/darwin-arm64": "0.25.3", - "@esbuild/darwin-x64": "0.25.3", - "@esbuild/freebsd-arm64": "0.25.3", - "@esbuild/freebsd-x64": "0.25.3", - "@esbuild/linux-arm": "0.25.3", - "@esbuild/linux-arm64": "0.25.3", - "@esbuild/linux-ia32": "0.25.3", - "@esbuild/linux-loong64": "0.25.3", - "@esbuild/linux-mips64el": "0.25.3", - "@esbuild/linux-ppc64": "0.25.3", - "@esbuild/linux-riscv64": "0.25.3", - "@esbuild/linux-s390x": "0.25.3", - "@esbuild/linux-x64": "0.25.3", - "@esbuild/netbsd-arm64": "0.25.3", - "@esbuild/netbsd-x64": "0.25.3", - "@esbuild/openbsd-arm64": "0.25.3", - "@esbuild/openbsd-x64": "0.25.3", - "@esbuild/sunos-x64": "0.25.3", - "@esbuild/win32-arm64": "0.25.3", - "@esbuild/win32-ia32": "0.25.3", - "@esbuild/win32-x64": "0.25.3" + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" } }, "node_modules/escape-string-regexp": { @@ -1678,19 +1729,19 @@ } }, "node_modules/eslint": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz", - "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.14.0", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.28.0", + "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -1702,9 +1753,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -1739,9 +1790,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1756,9 +1807,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1769,15 +1820,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1843,9 +1894,9 @@ } }, "node_modules/expect-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", - "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1874,9 +1925,9 @@ "license": "MIT" }, "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2006,9 +2057,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2032,9 +2083,9 @@ } }, "node_modules/globals": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", - "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true, "license": "MIT", "engines": { @@ -2208,6 +2259,13 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2289,9 +2347,9 @@ "license": "MIT" }, "node_modules/loupe": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz", + "integrity": "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==", "dev": true, "license": "MIT" }, @@ -2341,15 +2399,15 @@ } }, "node_modules/marked": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.1.1.tgz", + "integrity": "sha512-ij/2lXfCRT71L6u0M29tJPhP0bM5shLL3u5BePhFwPELj2blMJ6GDtD7PfJhRLhJ/c2UwrK17ySVcDzy2YHjHQ==", "license": "MIT", "bin": { "marked": "bin/marked.js" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/minimatch": { @@ -2522,9 +2580,9 @@ "license": "MIT" }, "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", "engines": { @@ -2539,9 +2597,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -2552,9 +2610,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -2572,7 +2630,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2606,6 +2664,23 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-plugin-organize-imports": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.2.0.tgz", + "integrity": "sha512-Zdy27UhlmyvATZi67BTnLcKTo8fm6Oik59Sz6H64PgZJVs6NJpPD1mT240mmJn62c98/QaL+r3kx9Q3gRpDajg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": ">=2.0", + "typescript": ">=2.9", + "vue-tsc": "^2.1.0 || 3" + }, + "peerDependenciesMeta": { + "vue-tsc": { + "optional": true + } + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2627,13 +2702,13 @@ } }, "node_modules/rollup": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", - "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", + "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -2643,26 +2718,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.1", - "@rollup/rollup-android-arm64": "4.40.1", - "@rollup/rollup-darwin-arm64": "4.40.1", - "@rollup/rollup-darwin-x64": "4.40.1", - "@rollup/rollup-freebsd-arm64": "4.40.1", - "@rollup/rollup-freebsd-x64": "4.40.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", - "@rollup/rollup-linux-arm-musleabihf": "4.40.1", - "@rollup/rollup-linux-arm64-gnu": "4.40.1", - "@rollup/rollup-linux-arm64-musl": "4.40.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-musl": "4.40.1", - "@rollup/rollup-linux-s390x-gnu": "4.40.1", - "@rollup/rollup-linux-x64-gnu": "4.40.1", - "@rollup/rollup-linux-x64-musl": "4.40.1", - "@rollup/rollup-win32-arm64-msvc": "4.40.1", - "@rollup/rollup-win32-ia32-msvc": "4.40.1", - "@rollup/rollup-win32-x64-msvc": "4.40.1", + "@rollup/rollup-android-arm-eabi": "4.45.1", + "@rollup/rollup-android-arm64": "4.45.1", + "@rollup/rollup-darwin-arm64": "4.45.1", + "@rollup/rollup-darwin-x64": "4.45.1", + "@rollup/rollup-freebsd-arm64": "4.45.1", + "@rollup/rollup-freebsd-x64": "4.45.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", + "@rollup/rollup-linux-arm-musleabihf": "4.45.1", + "@rollup/rollup-linux-arm64-gnu": "4.45.1", + "@rollup/rollup-linux-arm64-musl": "4.45.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-musl": "4.45.1", + "@rollup/rollup-linux-s390x-gnu": "4.45.1", + "@rollup/rollup-linux-x64-gnu": "4.45.1", + "@rollup/rollup-linux-x64-musl": "4.45.1", + "@rollup/rollup-win32-arm64-msvc": "4.45.1", + "@rollup/rollup-win32-ia32-msvc": "4.45.1", + "@rollup/rollup-win32-x64-msvc": "4.45.1", "fsevents": "~2.3.2" } }, @@ -2723,14 +2798,14 @@ } }, "node_modules/simple-git": { - "version": "3.27.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.27.0.tgz", - "integrity": "sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==", + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.28.0.tgz", + "integrity": "sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==", "license": "MIT", "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", - "debug": "^4.3.5" + "debug": "^4.4.0" }, "funding": { "type": "github", @@ -2878,6 +2953,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2907,9 +2995,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2947,9 +3035,9 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2964,9 +3052,9 @@ } }, "node_modules/tinypool": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", "engines": { @@ -2984,9 +3072,9 @@ } }, "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", "dev": true, "license": "MIT", "engines": { @@ -3021,9 +3109,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, @@ -3038,24 +3126,24 @@ } }, "node_modules/vite": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", - "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", + "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "fdir": "^6.4.6", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -3064,14 +3152,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", - "less": "*", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -3113,17 +3201,17 @@ } }, "node_modules/vite-node": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.2.tgz", - "integrity": "sha512-/8iMryv46J3aK13iUXsei5G/A3CUlW4665THCPS+K8xAaqrVWiGB4RfXMQXCLjpK9P2eK//BczrVkn5JLAk6DA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.4.0", - "es-module-lexer": "^1.6.0", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" @@ -3136,32 +3224,34 @@ } }, "node_modules/vitest": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.2.tgz", - "integrity": "sha512-WaxpJe092ID1C0mr+LH9MmNrhfzi8I65EX/NRU/Ld016KqQNRgxSOlGNP1hHN+a/F8L15Mh8klwaF77zR3GeDQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.2", - "@vitest/mocker": "3.1.2", - "@vitest/pretty-format": "^3.1.2", - "@vitest/runner": "3.1.2", - "@vitest/snapshot": "3.1.2", - "@vitest/spy": "3.1.2", - "@vitest/utils": "3.1.2", + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", - "debug": "^4.4.0", + "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", + "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.13", - "tinypool": "^1.0.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.2", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -3177,8 +3267,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.2", - "@vitest/ui": "3.1.2", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, diff --git a/.github/shared/package.json b/.github/shared/package.json index 085f537a7621..b82abb44c7aa 100644 --- a/.github/shared/package.json +++ b/.github/shared/package.json @@ -4,31 +4,37 @@ "type": "module", "exports": { "./array": "./src/array.js", + "./breaking-change": "./src/breaking-change.js", "./changed-files": "./src/changed-files.js", "./equality": "./src/equality.js", + "./error-reporting": "./src/error-reporting.js", "./exec": "./src/exec.js", "./logger": "./src/logger.js", + "./path": "./src/path.js", "./readme": "./src/readme.js", "./sdk-types": "./src/sdk-types.js", "./sleep": "./src/sleep.js", + "./sort": "./src/sort.js", "./spec-model-error": "./src/spec-model-error.js", "./spec-model": "./src/spec-model.js", "./swagger": "./src/swagger.js", "./tag": "./src/tag.js", + "./simple-git": "./src/simple-git.js", "./test/examples": "./test/examples.js" }, "bin": { - "spec-model": "./cmd/spec-model.js" + "spec-model": "./cmd/spec-model.js", + "api-doc-preview": "./cmd/api-doc-preview.js" }, "_comments": { "dependencies": "Runtime dependencies must be kept to an absolute minimum for performance, ideally with no transitive dependencies", "dependencies2": "All runtime and dev dependencies in this file, must be a subset of ../package.json" }, "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.9.3", + "@apidevtools/json-schema-ref-parser": "^14.1.1", "debug": "^4.4.0", "js-yaml": "^4.1.0", - "marked": "^15.0.7", + "marked": "^16.1.1", "simple-git": "^3.27.0" }, "devDependencies": { @@ -38,20 +44,22 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^20.0.0", "@vitest/coverage-v8": "^3.0.7", + "cross-env": "^7.0.3", "eslint": "^9.22.0", "globals": "^16.0.0", "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "semver": "^7.7.1", "typescript": "~5.8.2", "vitest": "^3.0.7" }, "scripts": { "lint": "npm run lint:eslint && npm run lint:tsc", - "lint:eslint": "eslint", - "lint:tsc": "tsc && echo 'Type checking completed successfully'", - "prettier": "prettier \"**/*.js\" --check", - "prettier:debug": "prettier \"**/*.js\" --check ---log-level debug", - "prettier:write": "prettier \"**/*.js\" --write", + "lint:eslint": "cross-env DEBUG=eslint:eslint eslint", + "lint:tsc": "tsc --build --verbose", + "format": "prettier . --ignore-path ../.prettierignore --write", + "format:check": "prettier . --ignore-path ../.prettierignore --check", + "format:check:ci": "prettier . --ignore-path ../.prettierignore --check --log-level debug", "test": "vitest", "test:ci": "vitest run --coverage --reporter=verbose" } diff --git a/.github/shared/src/array.js b/.github/shared/src/array.js index 1279df5e3858..0b8e4ee5aacf 100644 --- a/.github/shared/src/array.js +++ b/.github/shared/src/array.js @@ -23,6 +23,8 @@ export async function flatMapAsync(array, asyncMapper) { } /** + * Returns true if `array` includes no elements from `values` + * * @template T,U * @param {T[]} array * @param {(item: T, index: number, array: T[]) => Promise} asyncMapper @@ -31,3 +33,25 @@ export async function flatMapAsync(array, asyncMapper) { export async function mapAsync(array, asyncMapper) { return Promise.all(array.map(asyncMapper)); } + +/** + * Returns true if `array` includes every element from `values` + * + * @template T + * @param {T[]} array + * @param {T[]} values + * @returns {boolean} + */ +export function includesEvery(array, values) { + return values.every((value) => array.includes(value)); +} + +/** + * @template T + * @param {T[]} array + * @param {T[]} values + * @returns {boolean} + */ +export function includesNone(array, values) { + return values.every((value) => !array.includes(value)); +} diff --git a/.github/shared/src/breaking-change.js b/.github/shared/src/breaking-change.js new file mode 100644 index 000000000000..a6066b406931 --- /dev/null +++ b/.github/shared/src/breaking-change.js @@ -0,0 +1,82 @@ +/* v8 ignore start */ +// @ts-check + +/** + * Breaking change configuration and constants for Azure REST API specs + * This file contains the single source of truth for all breaking change and versioning approval labels. + * + * Used across multiple tools in the Azure/azure-rest-api-specs repository. + */ + +// All versioning approval labels in one place +export const VERSIONING_APPROVALS = { + BENIGN: "Versioning-Approved-Benign", + BUG_FIX: "Versioning-Approved-BugFix", + PRIVATE_PREVIEW: "Versioning-Approved-PrivatePreview", + BRANCH_POLICY_EXCEPTION: "Versioning-Approved-BranchPolicyException", + PREVIOUSLY: "Versioning-Approved-Previously", + RETIRED: "Versioning-Approved-Retired", +}; + +// All breaking change approval labels in one place +export const BREAKING_CHANGE_APPROVALS = { + BENIGN: "BreakingChange-Approved-Benign", + BUG_FIX: "BreakingChange-Approved-BugFix", + USER_IMPACT: "BreakingChange-Approved-UserImpact", + BRANCH_POLICY_EXCEPTION: "BreakingChange-Approved-BranchPolicyException", + PREVIOUSLY: "BreakingChange-Approved-Previously", + SECURITY: "BreakingChange-Approved-Security", +}; + +// Review required labels +export const REVIEW_REQUIRED_LABELS = { + BREAKING_CHANGE: "BreakingChangeReviewRequired", + VERSIONING: "VersioningReviewRequired", +}; + +// Extract values as arrays for validation and configuration +export const versioningApprovalValues = Object.values(VERSIONING_APPROVALS); +export const breakingChangeApprovalValues = Object.values(BREAKING_CHANGE_APPROVALS); +export const reviewRequiredLabelValues = Object.values(REVIEW_REQUIRED_LABELS); + +// Type guard functions for runtime validation +/** + * @param {string} value + */ +export function isValidVersioningApproval(value) { + return versioningApprovalValues.includes(value); +} + +/** + * @param {string} value + */ +export function isValidBreakingChangeApproval(value) { + return breakingChangeApprovalValues.includes(value); +} + +/** + * @param {string} value + */ +export function isReviewRequiredLabel(value) { + return reviewRequiredLabelValues.includes(value); +} + +// Configuration for different check types +export const breakingChangesCheckType = { + SameVersion: { + reviewRequiredLabel: REVIEW_REQUIRED_LABELS.VERSIONING, + approvalPrefixLabel: "Versioning-Approved-*", + approvalLabels: versioningApprovalValues, + }, + CrossVersion: { + reviewRequiredLabel: REVIEW_REQUIRED_LABELS.BREAKING_CHANGE, + approvalPrefixLabel: "BreakingChange-Approved-*", + approvalLabels: breakingChangeApprovalValues, + }, +}; + +// Check types +export const BREAKING_CHANGES_CHECK_TYPES = { + SAME_VERSION: "SameVersion", + CROSS_VERSION: "CrossVersion", +}; diff --git a/.github/shared/src/changed-files.js b/.github/shared/src/changed-files.js index b7db914efae9..4561c394116f 100644 --- a/.github/shared/src/changed-files.js +++ b/.github/shared/src/changed-files.js @@ -2,39 +2,38 @@ import debug from "debug"; import { simpleGit } from "simple-git"; +import { includesFolder } from "./path.js"; // Enable simple-git debug logging to improve console output debug.enable("simple-git"); /** + * Get a list of changed files in a git repository + * * @param {Object} [options] * @param {string} [options.baseCommitish] Default: "HEAD^". * @param {string} [options.cwd] Current working directory. Default: process.cwd(). * @param {string} [options.headCommitish] Default: "HEAD". * @param {import('./logger.js').ILogger} [options.logger] - * @returns {Promise} List of changed files, using posix paths, relative to options.cwd. Example: ["specification/foo/Microsoft.Foo/main.tsp"]. + * @param {string[]} [options.paths] Limits the diff to the named paths. If not set, includes all paths in repo. Default: [] + * @returns {Promise} List of changed files, using posix paths, relative to repo root. Example: ["specification/foo/Microsoft.Foo/main.tsp"]. */ export async function getChangedFiles(options = {}) { - const { - baseCommitish = "HEAD^", - cwd, - headCommitish = "HEAD", - logger, - } = options; + const { baseCommitish = "HEAD^", cwd, headCommitish = "HEAD", logger, paths = [] } = options; + + if (paths.length > 0) { + // Use "--" to separate paths from revisions + paths.unshift("--"); + } // TODO: If we need to filter based on status, instead of passing an argument to `--diff-filter, // consider using "--name-status" instead of "--name-only", and return an array of objects like // { name: "/foo/baz.js", status: Status.Renamed, previousName: "/foo/bar.js"}. // Then add filter functions to filter based on status. This is more flexible and lets consumers // filter based on status with a single call to `git diff`. - const result = await simpleGit(cwd).diff([ - "--name-only", - baseCommitish, - headCommitish, - ]); + const result = await simpleGit(cwd).diff(["--name-only", baseCommitish, headCommitish, ...paths]); const files = result.trim().split("\n"); - logger?.info("Changed Files:"); for (const file of files) { logger?.info(` ${file}`); @@ -44,7 +43,121 @@ export async function getChangedFiles(options = {}) { return files; } +/** + * Get a list of changed files in a git repository with statuses for additions, + * modifications, deletions, and renames. Warning: rename behavior can vary + * based on the git client's configuration of diff.renames. + * + * @param {Object} [options] + * @param {string} [options.baseCommitish] Default: "HEAD^". + * @param {string} [options.cwd] Current working directory. Default: process.cwd(). + * @param {string} [options.headCommitish] Default: "HEAD". + * @param {import('./logger.js').ILogger} [options.logger] + * @param {string[]} [options.paths] Limits the diff to the named paths. If not set, includes all paths in repo. Default: [] + * @returns {Promise<{additions: string[], modifications: string[], deletions: string[], renames: {from: string, to: string}[], total: number}>} + */ +export async function getChangedFilesStatuses(options = {}) { + const { baseCommitish = "HEAD^", cwd, headCommitish = "HEAD", logger, paths = [] } = options; + + if (paths.length > 0) { + // Use "--" to separate paths from revisions + paths.unshift("--"); + } + + const result = await simpleGit(cwd).diff([ + "--name-status", + baseCommitish, + headCommitish, + ...paths, + ]); + + const categorizedFiles = { + additions: /** @type {string[]} */ ([]), + modifications: /** @type {string[]} */ ([]), + deletions: /** @type {string[]} */ ([]), + renames: /** @type {{from: string, to: string}[]} */ ([]), + total: 0, + }; + + if (result.trim()) { + const lines = result.trim().split("\n"); + + for (const line of lines) { + const parts = line.split("\t"); + const status = parts[0]; + + switch (status[0]) { + case "A": + categorizedFiles.additions.push(parts[1]); + break; + case "M": + categorizedFiles.modifications.push(parts[1]); + break; + case "D": + categorizedFiles.deletions.push(parts[1]); + break; + case "R": + categorizedFiles.renames.push({ + from: parts[1], + to: parts[2], + }); + break; + case "C": + categorizedFiles.additions.push(parts[2]); + break; + default: + categorizedFiles.modifications.push(parts[1]); + } + } + + categorizedFiles.total = + categorizedFiles.additions.length + + categorizedFiles.modifications.length + + categorizedFiles.deletions.length + + categorizedFiles.renames.length; + } + + // Log all changed files by categories + if (logger) { + logger.info("Categorized Changed Files:"); + + if (categorizedFiles.additions.length > 0) { + logger.info(` Additions (${categorizedFiles.additions.length}):`); + for (const file of categorizedFiles.additions) { + logger.info(` + ${file}`); + } + } + + if (categorizedFiles.modifications.length > 0) { + logger.info(` Modifications (${categorizedFiles.modifications.length}):`); + for (const file of categorizedFiles.modifications) { + logger.info(` M ${file}`); + } + } + + if (categorizedFiles.deletions.length > 0) { + logger.info(` Deletions (${categorizedFiles.deletions.length}):`); + for (const file of categorizedFiles.deletions) { + logger.info(` - ${file}`); + } + } + + if (categorizedFiles.renames.length > 0) { + logger.info(` Renames (${categorizedFiles.renames.length}):`); + for (const rename of categorizedFiles.renames) { + logger.info(` R ${rename.from} -> ${rename.to}`); + } + } + + logger.info(` Total: ${categorizedFiles.total} files`); + logger.info(""); + } + + return categorizedFiles; +} + // Functions suitable for passing to string[].filter(), ordered roughly in order of increasing specificity +// Functions accept both relative and absolute paths, since paths are resolve()'d before searching (when needed) /** * @param {string} [file] @@ -64,26 +177,13 @@ export function readme(file) { return typeof file === "string" && file.toLowerCase().endsWith("readme.md"); } -/** - * @param {string} [file] - * @returns {boolean} - */ -export function specification(file) { - // Folder name "specification" should match case, since it already exists in repo - return typeof file === "string" && file.startsWith("specification/"); -} - /** * @param {string} [file] * @returns {boolean} */ export function dataPlane(file) { // Folder name "data-plane" should match case for consistency across specs - return ( - typeof file === "string" && - specification(file) && - file.includes("/data-plane/") - ); + return typeof file === "string" && includesFolder(file, "data-plane"); } /** @@ -92,11 +192,7 @@ export function dataPlane(file) { */ export function resourceManager(file) { // Folder name "resource-manager" should match case for consistency across specs - return ( - typeof file === "string" && - specification(file) && - file.includes("/resource-manager/") - ); + return typeof file === "string" && includesFolder(file, "resource-manager"); } /** @@ -105,14 +201,28 @@ export function resourceManager(file) { */ export function example(file) { // Folder name "examples" should match case for consistency across specs + return typeof file === "string" && json(file) && includesFolder(file, "examples"); +} + +/** + * @param {string} file + * @returns {boolean} + */ +export function typespec(file) { return ( typeof file === "string" && - json(file) && - specification(file) && - file.includes("/examples/") + (file.toLowerCase().endsWith(".tsp") || file.toLowerCase().endsWith("tspconfig.yaml")) ); } +/** + * @param {string} [file] + * @returns {boolean} + */ +export function quickstartTemplate(file) { + return typeof file === "string" && json(file) && file.includes("/quickstart-templates/"); +} + /** * @param {string} [file] * @returns {boolean} @@ -122,6 +232,16 @@ export function swagger(file) { typeof file === "string" && json(file) && (dataPlane(file) || resourceManager(file)) && - !example(file) + !example(file) && + !quickstartTemplate(file) && + !scenario(file) ); } + +/** + * @param {string} [file] + * @returns {boolean} + */ +export function scenario(file) { + return typeof file === "string" && json(file) && includesFolder(file, "scenarios"); +} diff --git a/.github/shared/src/doc-preview.js b/.github/shared/src/doc-preview.js new file mode 100644 index 000000000000..1ac67eb7ee36 --- /dev/null +++ b/.github/shared/src/doc-preview.js @@ -0,0 +1,167 @@ +// @ts-check +const DOCS_NAMESPACE = "_swagger_specs"; +const SPEC_FILE_REGEX = + "(specification/)+(.*)/(resourcemanager|resource-manager|dataplane|data-plane|control-plane)/(.*)/(preview|stable|privatepreview)/(.*?)/(example)?(.*)"; + +/** + * @typedef {Object} SwaggerFileMetadata + * @property {string} path + * @property {string} serviceName + * @property {string} serviceType + * @property {string} resourceProvider + * @property {string} releaseState + * @property {string} apiVersion + * @property {string} fileName + */ + +/** + * @typedef {Object} RepoJSONTemplate + * @property {Object[]} repo + * @property {string} repo[].url + * @property {string} repo[].prNumber + * @property {string} repo[].name + */ + +/** + * @typedef {Object} MappingJSONStructure + * @property {string} target_api_root_dir + * @property {boolean} enable_markdown_fragment + * @property {string} markdown_fragment_folder + * @property {boolean} use_yaml_toc + * @property {boolean} formalize_url + * @property {string[]} version_list + * @property {Object[]} organizations + * @property {string} organizations[].index + * @property {string} organizations[].default_toc_title + * @property {string} organizations[].version + * @property {Object[]} organizations[].services + * @property {string} organizations[].services[].toc_title + * @property {string} organizations[].services[].url_group + * @property {Object[]} organizations[].services[].swagger_files + * @property {string} organizations[].services[].swagger_files[].source + */ + +/** + * Extract swagger file metadata from path. + * @param {string} specPath + * @returns {SwaggerFileMetadata} + */ +export function parseSwaggerFilePath(specPath) { + const m = specPath.match(SPEC_FILE_REGEX); + if (!m) { + throw new Error(`Path "${specPath}" does not match expected swagger file pattern.`); + } + const [path, , serviceName, serviceType, resourceProvider, releaseState, apiVersion, , fileName] = + m; + return { + path, + serviceName, + serviceType, + resourceProvider, + releaseState, + apiVersion, + fileName, + }; +} + +/** + * @param {string} repoName + * @param {string} prNumber + * @returns {object} + */ +export function repoJSONTemplate(repoName, prNumber) { + return { + repo: [ + { + url: `https://github.com/${repoName}`, + prNumber: prNumber, + name: DOCS_NAMESPACE, + }, + ], + }; +} + +/** + * @param {string[]} files + * @returns {MappingJSONStructure} + */ +export function mappingJSONTemplate(files) { + return { + target_api_root_dir: "structured", + enable_markdown_fragment: true, + markdown_fragment_folder: "authored", + use_yaml_toc: true, + formalize_url: true, + version_list: ["default"], + organizations: [ + { + index: "index.md", + default_toc_title: "Getting Started", + version: "default", + services: [ + { + toc_title: "Documentation Preview", + url_group: "documentation-preview", + swagger_files: files.map((source) => ({ + source: `${DOCS_NAMESPACE}/${source}`, + })), + }, + ], + }, + ], + }; +} + +/** + * @param {string} buildId + * @param {string} repoName + * @param {string} prNumber + * @returns {string} + */ +export function indexMd(buildId, repoName, prNumber) { + return `# Documentation Preview for swagger pipeline build #${buildId} + +Welcome to documentation preview for ${repoName}/pull/${prNumber} +created via the swagger pipeline. + +Your documentation may be viewed in the menu on the left hand side. + +If you have issues around documentation generation, please feel free to contact +us in the [Docs Support Teams Channel](https://aka.ms/ci-fix/api-docs-help)`; +} + +/** + * Given a list of changed swagger files, select an API version and a list of + * swagger files in that API version to process. + * @param {string[]} swaggerFiles + **/ +export function getSwaggersToProcess(swaggerFiles) { + const swaggerFileObjs = swaggerFiles.map(parseSwaggerFilePath); + + const versions = swaggerFileObjs.map((obj) => obj.apiVersion).filter(Boolean); + if (versions.length === 0) { + throw new Error("No new API versions found in eligible swagger files."); + } + const uniqueVersions = Array.from(new Set(versions)); + + let selectedVersion; + if (uniqueVersions.length === 1) { + selectedVersion = uniqueVersions[0]; + console.log(`Single API version found: ${selectedVersion}`); + } else { + // This sorting logic is ported from the original code which sorts only the + // strings and doesn't attempt to parse versions for more semantically-aware + // sorting. + const sortedVersions = [...uniqueVersions].sort(); + selectedVersion = sortedVersions[sortedVersions.length - 1]; + console.log( + `Multiple API versions found: ${JSON.stringify(sortedVersions)}. Selected version: ${selectedVersion}`, + ); + } + + const swaggersToProcess = swaggerFileObjs + .filter((obj) => obj.apiVersion === selectedVersion) + .map((obj) => obj.path); + + return { selectedVersion, swaggersToProcess }; +} diff --git a/.github/shared/src/error-reporting.js b/.github/shared/src/error-reporting.js new file mode 100644 index 000000000000..5eab9575c988 --- /dev/null +++ b/.github/shared/src/error-reporting.js @@ -0,0 +1,55 @@ +// @ts-check +import * as fs from "fs"; + +/** + * Set the summary of the github step summary for a job. This feature is intended for formatted markdown, + * which can be used to display the results of a job in a more readable format. + * + * Format your results as a markdown table and go to town! + * @param {string} content + * @returns {void} + */ +export function setSummary(content) { + if (!process.env.GITHUB_STEP_SUMMARY) { + console.log("GITHUB_STEP_SUMMARY is not set. Skipping summary update."); + return; + } + const summaryFile = process.env.GITHUB_STEP_SUMMARY; + + fs.writeFileSync(summaryFile, content); +} + +/** + * Set the output for a Github Actions step. The output is written to the GITHUB_OUTPUT environment variable. + * This is used to pass data between steps in a workflow. + * + * To access this output later, leverage: ${{ steps..outputs. }}. + * + * This function is the equivalent of using `core.setOutput(name, value)` in a GitHub Action, without the package dependency. + * @param {string} name - The name of the output variable. + * @param {string} value - The value to set for the output variable. + * @returns {void} + */ +export function setOutput(name, value) { + if (!process.env.GITHUB_OUTPUT) { + console.log(`GITHUB_OUTPUT is not set. Skipping ${name} update with value '${value}.'`); + return; + } + + if (process.env.GITHUB_OUTPUT) { + fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`); + } +} + +/** + * This function is used to ask the github agent to annotate a file in a github PR with an error message. + * @param {string} repoPath + * @param {string} message + * @param {number} line + * @param {number} col + * @returns {void} + */ +export function annotateFileError(repoPath, message, line, col) { + const errorLine = `::error file=${repoPath},line=${line},col=${col}::${message}`; + console.log(errorLine); +} diff --git a/.github/shared/src/exec.js b/.github/shared/src/exec.js index 9216761ce546..d465d9b2dce4 100644 --- a/.github/shared/src/exec.js +++ b/.github/shared/src/exec.js @@ -101,13 +101,7 @@ export async function execNpm(args, options = {}) { // example: "C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js" defaultArgs: [ "--", - join( - dirname(process.execPath), - "node_modules", - "npm", - "bin", - "npm-cli.js", - ), + join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"), ], } : { file: "npm", defaultArgs: [] }; diff --git a/.github/shared/src/path.js b/.github/shared/src/path.js new file mode 100644 index 000000000000..fe28bf8afb7e --- /dev/null +++ b/.github/shared/src/path.js @@ -0,0 +1,13 @@ +// @ts-check + +import { resolve, sep } from "path"; + +/** + * + * @param {string} path + * @param {string} folder + * @returns {boolean} True if path contains the named folder + */ +export function includesFolder(path, folder) { + return resolve(path).split(sep).includes(folder); +} diff --git a/.github/shared/src/readme.js b/.github/shared/src/readme.js index 12e3656c52d2..f6881719bd94 100644 --- a/.github/shared/src/readme.js +++ b/.github/shared/src/readme.js @@ -12,6 +12,11 @@ import { Tag } from "./tag.js"; * @typedef {import('./spec-model.js').ToJSONOptions} ToJSONOptions */ +/** + * Regex to match tag names in readme.md yaml code blocks + */ +export const TagMatchRegex = /yaml.*\$\(tag\) ?== ?(["'])(.*?)\1/; + export class Readme { /** * Content of `readme.md`, either loaded from `#path` or passed in via `options`. @@ -101,38 +106,27 @@ export class Readme { // Include default block and tagged blocks (```yaml $(tag) == 'package-2021-11-01') .filter((token) => token.lang?.toLowerCase().startsWith("yaml")); - const globalConfigYamlBlocks = yamlBlocks.filter( - (token) => token.lang === "yaml", - ); + const globalConfigYamlBlocks = yamlBlocks.filter((token) => token.lang === "yaml"); const globalConfig = globalConfigYamlBlocks.reduce( - (obj, token) => - Object.assign( - obj, - yaml.load(token.text, { schema: yaml.FAILSAFE_SCHEMA }), - ), + (obj, token) => Object.assign(obj, yaml.load(token.text, { schema: yaml.FAILSAFE_SCHEMA })), {}, ); /** @type {Map} */ const tags = new Map(); for (const block of yamlBlocks) { - const tagName = - block.lang?.match(/yaml.*\$\(tag\) ?== ?'([^']*)'/)?.[1] || "default"; + const tagName = block.lang?.match(TagMatchRegex)?.[2] || "default"; if (tagName === "default" || tagName === "all-api-versions") { // Skip yaml blocks where this is no tag or tag is all-api-versions continue; } - const obj = /** @type {any} */ ( - yaml.load(block.text, { schema: yaml.FAILSAFE_SCHEMA }) - ); + const obj = /** @type {any} */ (yaml.load(block.text, { schema: yaml.FAILSAFE_SCHEMA })); if (!obj) { - this.#logger?.debug( - `No yaml object found for tag ${tagName} in ${this.#path}`, - ); + this.#logger?.debug(`No yaml object found for tag ${tagName} in ${this.#path}`); continue; } @@ -214,9 +208,7 @@ export class Readme { */ async toJSONAsync(options) { const tags = await mapAsync( - [...(await this.getTags()).values()].sort((a, b) => - a.name.localeCompare(b.name), - ), + [...(await this.getTags()).values()].sort((a, b) => a.name.localeCompare(b.name)), async (t) => await t.toJSONAsync(options), ); diff --git a/.github/shared/src/sdk-types.js b/.github/shared/src/sdk-types.js index be2ef795bca3..ddf02356573f 100644 --- a/.github/shared/src/sdk-types.js +++ b/.github/shared/src/sdk-types.js @@ -28,8 +28,7 @@ export const sdkLabels = { breakingChange: "BreakingChange-Go-Sdk", breakingChangeApproved: "BreakingChange-Go-Sdk-Approved", breakingChangeSuppression: "BreakingChange-Go-Sdk-Suppression", - breakingChangeSuppressionApproved: - "BreakingChange-Go-Sdk-Suppression-Approved", + breakingChangeSuppressionApproved: "BreakingChange-Go-Sdk-Suppression-Approved", }, "azure-sdk-for-java": { breakingChange: undefined, @@ -41,8 +40,7 @@ export const sdkLabels = { breakingChange: "BreakingChange-JavaScript-Sdk", breakingChangeApproved: "BreakingChange-JavaScript-Sdk-Approved", breakingChangeSuppression: "BreakingChange-JavaScript-Sdk-Suppression", - breakingChangeSuppressionApproved: - "BreakingChange-JavaScript-Sdk-Suppression-Approved", + breakingChangeSuppressionApproved: "BreakingChange-JavaScript-Sdk-Suppression-Approved", }, "azure-sdk-for-net": { breakingChange: undefined, @@ -54,7 +52,6 @@ export const sdkLabels = { breakingChange: "BreakingChange-Python-Sdk", breakingChangeApproved: "BreakingChange-Python-Sdk-Approved", breakingChangeSuppression: "BreakingChange-Python-Sdk-Suppression", - breakingChangeSuppressionApproved: - "BreakingChange-Python-Sdk-Suppression-Approved", + breakingChangeSuppressionApproved: "BreakingChange-Python-Sdk-Suppression-Approved", }, }; diff --git a/.github/shared/src/simple-git.js b/.github/shared/src/simple-git.js new file mode 100644 index 000000000000..c8241c839e35 --- /dev/null +++ b/.github/shared/src/simple-git.js @@ -0,0 +1,13 @@ +import { resolve } from "path"; +import { simpleGit } from "simple-git"; + +/** + * + * @param {string} inputPath + * @returns {Promise} + */ +export async function getRootFolder(inputPath) { + // expecting users to handle the case where inputPath is not a git repo + const gitRoot = await simpleGit(inputPath).revparse("--show-toplevel"); + return resolve(gitRoot.trim()); +} diff --git a/.github/shared/src/sort.js b/.github/shared/src/sort.js new file mode 100644 index 000000000000..befce40a0147 --- /dev/null +++ b/.github/shared/src/sort.js @@ -0,0 +1,45 @@ +// @ts-check + +/** + * Returns a comparator that compares values by a date string in ascending order. + * Throws if the value returned by getDate() is null, undefined, or cannot be + * parsed as a date. + * + * @template T + * @param {(item: T) => string} getDate + * @returns {(a: T, b: T) => number} + */ +export function byDate(getDate) { + return (a, b) => { + // Sort ascending to match JS default + return parseDate(getDate(a)) - parseDate(getDate(b)); + }; +} + +/** + * Parses a string to a date, throwing if null, undefined, or cannot be parsed. + * + * @param {string} s + * @returns {number} + */ +function parseDate(s) { + // Date.parse() returns NaN for null, undefined, or strings that cannot be parsed. + const parsed = Date.parse(s); + + if (Number.isNaN(parsed)) { + throw new Error(`Unable to parse '${s}' to a valid date`); + } + + return parsed; +} + +/** + * Inverts a comparator function. + * + * @template T + * @param {(a: T, b: T) => number} comparator + * @returns {(a: T, b: T) => number} + */ +export function invert(comparator) { + return (a, b) => -comparator(a, b); +} diff --git a/.github/shared/src/spec-model.js b/.github/shared/src/spec-model.js index d514a32a82a4..9d3281f951dd 100644 --- a/.github/shared/src/spec-model.js +++ b/.github/shared/src/spec-model.js @@ -2,9 +2,13 @@ import { readdir } from "fs/promises"; import { resolve } from "path"; -import { mapAsync } from "./array.js"; +import { flatMapAsync, mapAsync } from "./array.js"; +import { readme } from "./changed-files.js"; import { Readme } from "./readme.js"; +/** @type {Map} */ +const specModelCache = new Map(); + /** * @typedef {Object} ToJSONOptions * @prop {boolean} [includeRefs] @@ -16,6 +20,7 @@ import { Readme } from "./readme.js"; export class SpecModel { /** @type {string} absolute path */ + // @ts-expect-error Ignore error that value may not be set in ctor (since we may returned cached value) #folder; /** @type {import('./logger.js').ILogger | undefined} */ @@ -30,8 +35,17 @@ export class SpecModel { * @param {import('./logger.js').ILogger} [options.logger] */ constructor(folder, options) { - this.#folder = resolve(folder); + const resolvedFolder = resolve(folder); + + const cachedSpecModel = specModelCache.get(resolvedFolder); + if (cachedSpecModel !== undefined) { + return cachedSpecModel; + } + + this.#folder = resolvedFolder; this.#logger = options?.logger; + + specModelCache.set(resolvedFolder, this); } /** @@ -122,10 +136,7 @@ export class SpecModel { const refRefToSwaggerPath = refRefs.get(swaggerPathResolved); if (refRefToSwaggerPath) { // Add the Swagger object for swaggerPath - affectedSwaggers.set( - refRefToSwaggerPath.path, - refRefToSwaggerPath, - ); + affectedSwaggers.set(refRefToSwaggerPath.path, refRefToSwaggerPath); // Add the Swagger object that references swaggerPath // @@ -150,9 +161,7 @@ export class SpecModel { // The swagger file supplied does not exist in the given specModel if (affectedSwaggers.size === 0) { - throw new Error( - `No affected swaggers found in specModel for ${swaggerPath}`, - ); + throw new Error(`No affected swaggers found in specModel for ${swaggerPath}`); } return affectedSwaggers; @@ -188,15 +197,21 @@ export class SpecModel { return this.#readmes; } + async getSwaggers() { + const readmes = [...(await this.getReadmes()).values()]; + const tags = await flatMapAsync(readmes, async (r) => [...(await r.getTags()).values()]); + const swaggers = tags.flatMap((t) => [...t.inputFiles.values()]); + const refs = await flatMapAsync(swaggers, async (s) => [...(await s.getRefs()).values()]); + return [...swaggers, ...refs]; + } + /** * @param {ToJSONOptions} [options] * @returns {Promise} */ async toJSONAsync(options) { const readmes = await mapAsync( - [...(await this.getReadmes()).values()].sort((a, b) => - a.path.localeCompare(b.path), - ), + [...(await this.getReadmes()).values()].sort((a, b) => a.path.localeCompare(b.path)), async (r) => await r.toJSONAsync(options), ); @@ -213,14 +228,3 @@ export class SpecModel { return `SpecModel(${this.#folder}, {logger: ${this.#logger}}})`; } } - -// TODO: Remove duplication with changed-files.js (which currently requires paths relative to repo root) - -/** - * @param {string} [file] - * @returns {boolean} - */ -function readme(file) { - // Filename "readme.md" with any case is a valid README file - return typeof file === "string" && file.toLowerCase().endsWith("readme.md"); -} diff --git a/.github/shared/src/swagger.js b/.github/shared/src/swagger.js index 7843d6959f94..0e204c4a613a 100644 --- a/.github/shared/src/swagger.js +++ b/.github/shared/src/swagger.js @@ -4,6 +4,7 @@ import $RefParser, { ResolverError } from "@apidevtools/json-schema-ref-parser"; import { readFile } from "fs/promises"; import { dirname, relative, resolve } from "path"; import { mapAsync } from "./array.js"; +import { example } from "./changed-files.js"; import { SpecModelError } from "./spec-model-error.js"; /** @@ -11,6 +12,13 @@ import { SpecModelError } from "./spec-model-error.js"; * @typedef {import('./spec-model.js').ToJSONOptions} ToJSONOptions */ +/** + * @typedef {Object} Operation + * @property {string} id - The operation ID + * @property {string} path - API path + * @property {string} httpMethod - HTTP method (GET, POST, etc.) + */ + /** * @type {import('@apidevtools/json-schema-ref-parser').ResolverOptions} */ @@ -41,6 +49,9 @@ export class Swagger { /** @type {Tag | undefined} Tag that contains this Swagger */ #tag; + /** @type {Map | undefined} map of the operations in this swagger with key as 'operation_id*/ + #operations; + /** * @param {string} path * @param {Object} [options] @@ -58,6 +69,15 @@ export class Swagger { * @returns {Promise>} */ async getRefs() { + const allRefs = await this.#getRefs(); + + // filter out any paths that are examples + const filtered = new Map([...allRefs].filter(([path]) => !example(path))); + + return filtered; + } + + async #getRefs() { if (!this.#refs) { let schema; try { @@ -66,15 +86,12 @@ export class Swagger { }); } catch (error) { if (error instanceof ResolverError) { - throw new SpecModelError( - `Failed to resolve file for swagger: ${this.#path}`, - { - cause: error, - source: error.source, - tag: this.#tag?.name, - readme: this.#tag?.readme?.path, - }, - ); + throw new SpecModelError(`Failed to resolve file for swagger: ${this.#path}`, { + cause: error, + source: error.source, + tag: this.#tag?.name, + readme: this.#tag?.readme?.path, + }); } throw error; @@ -82,8 +99,6 @@ export class Swagger { const refPaths = schema .paths("file") - // Exclude examples - .filter((p) => !example(p)) // Exclude ourself .filter((p) => resolve(p) !== resolve(this.#path)); @@ -101,6 +116,63 @@ export class Swagger { return this.#refs; } + /** + * @returns {Promise>} + */ + async getExamples() { + const allRefs = await this.#getRefs(); + + // filter out any paths that are examples + const filtered = new Map([...allRefs].filter(([path]) => example(path))); + + return filtered; + } + + /** + * @returns {Promise>} + */ + async getOperations() { + if (!this.#operations) { + this.#operations = new Map(); + const content = await readFile(this.#path, "utf8"); + const swagger = JSON.parse(content); + // Process regular paths + if (swagger.paths) { + for (const [path, pathItem] of Object.entries(swagger.paths)) { + this.addOperations(this.#operations, path, pathItem); + } + } + + // Process x-ms-paths (Azure extension) + if (swagger["x-ms-paths"]) { + for (const [path, pathItem] of Object.entries(swagger["x-ms-paths"])) { + this.addOperations(this.#operations, path, pathItem); + } + } + } + return this.#operations; + } + + /** + * + * @param {Map} operations + * @param {string} path + * @param {any} pathItem + * @returns {void} + */ + addOperations(operations, path, pathItem) { + for (const [method, operation] of Object.entries(pathItem)) { + if (typeof operation === "object" && operation.operationId && method !== "parameters") { + const operationObj = { + id: operation.operationId, + httpMethod: method.toUpperCase(), + path: path, + }; + operations.set(operation.operationId, operationObj); + } + } + } + /** * @returns {string} absolute path */ @@ -115,6 +187,15 @@ export class Swagger { return this.#tag; } + /** + * @returns {string} version kind (stable or preview) + */ + get versionKind() { + return dirname(this.#path).includes("/preview/") + ? API_VERSION_LIFECYCLE_STAGES.PREVIEW + : API_VERSION_LIFECYCLE_STAGES.STABLE; + } + /** * @param {ToJSONOptions} [options] * @returns {Promise} @@ -127,9 +208,7 @@ export class Swagger { : this.#path, refs: options?.includeRefs ? await mapAsync( - [...(await this.getRefs()).values()].sort((a, b) => - a.path.localeCompare(b.path), - ), + [...(await this.getRefs()).values()].sort((a, b) => a.path.localeCompare(b.path)), async (s) => // Do not include swagger refs transitively, otherwise we could get in infinite loop await s.toJSONAsync({ ...options, includeRefs: false }), @@ -143,22 +222,8 @@ export class Swagger { } } -// TODO: Remove duplication with changed-files.js (which currently requires paths relative to repo root) - -/** - * @param {string} [file] - * @returns {boolean} - */ -function example(file) { - // Folder name "examples" should match case for consistency across specs - return typeof file === "string" && json(file) && file.includes("/examples/"); -} - -/** - * @param {string} [file] - * @returns {boolean} - */ -function json(file) { - // Extension "json" with any case is a valid JSON file - return typeof file === "string" && file.toLowerCase().endsWith(".json"); -} +// API version lifecycle stages +export const API_VERSION_LIFECYCLE_STAGES = Object.freeze({ + PREVIEW: "preview", + STABLE: "stable", +}); diff --git a/.github/shared/src/tag.js b/.github/shared/src/tag.js index a3aaff33553a..2f3a29d187f0 100644 --- a/.github/shared/src/tag.js +++ b/.github/shared/src/tag.js @@ -73,9 +73,7 @@ export class Tag { return { name: this.#name, inputFiles: await mapAsync( - [...this.#inputFiles.values()].sort((a, b) => - a.path.localeCompare(b.path), - ), + [...this.#inputFiles.values()].sort((a, b) => a.path.localeCompare(b.path)), async (s) => await s.toJSONAsync(options), ), }; diff --git a/.github/shared/test/array.test.js b/.github/shared/test/array.test.js index 007449622882..6ef31a69906b 100644 --- a/.github/shared/test/array.test.js +++ b/.github/shared/test/array.test.js @@ -1,7 +1,7 @@ // @ts-check import { describe, expect, it } from "vitest"; -import { filterAsync, flatMapAsync, mapAsync } from "../src/array.js"; +import { filterAsync, flatMapAsync, includesEvery, includesNone, mapAsync } from "../src/array.js"; import { sleep } from "../src/sleep.js"; describe("array", () => { @@ -37,4 +37,22 @@ describe("array", () => { expect(result).toEqual([0, 2, 6]); }); + + it("includesEvery", () => { + const input = [1, 2, 3]; + const values = [1, 2]; + + expect(includesEvery(input, values)).toBe(true); + expect(includesEvery(input, [4])).toBe(false); + expect(includesEvery(input, [])).toBe(true); + }); + + it("includesNone", () => { + const input = [1, 2, 3]; + const values = [4, 5]; + + expect(includesNone(input, values)).toBe(true); + expect(includesNone(input, [2])).toBe(false); + expect(includesNone(input, [])).toBe(true); + }); }); diff --git a/.github/shared/test/changed-files.test.js b/.github/shared/test/changed-files.test.js index 6b937cdf67a6..e98dadff5d9d 100644 --- a/.github/shared/test/changed-files.test.js +++ b/.github/shared/test/changed-files.test.js @@ -8,16 +8,20 @@ vi.mock("simple-git", () => ({ }), })); +import { resolve } from "path"; import * as simpleGit from "simple-git"; import { dataPlane, example, getChangedFiles, + getChangedFilesStatuses, json, + quickstartTemplate, readme, resourceManager, - specification, + scenario, swagger, + typespec, } from "../src/changed-files.js"; import { debugLogger } from "../src/logger.js"; @@ -26,102 +30,248 @@ describe("changedFiles", () => { vi.clearAllMocks(); }); - it.each([{}, { logger: debugLogger }])( - `getChangedFiles(%o)`, - async (options) => { - const files = [ - ".github/src/changed-files.js", - "specification/contosowidgetmanager/Contoso.Management/main.tsp", - "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso.json", - "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/examples/Employees_Get.json", - ]; + it.each([{}, { logger: debugLogger }])(`getChangedFiles(%o)`, async (options) => { + const files = [ + ".github/src/changed-files.js", + "specification/contosowidgetmanager/Contoso.Management/main.tsp", + "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso.json", + "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/examples/Employees_Get.json", + ]; - vi.mocked(simpleGit.simpleGit().diff).mockResolvedValue(files.join("\n")); + vi.mocked(simpleGit.simpleGit().diff).mockResolvedValue(files.join("\n")); - await expect(getChangedFiles(options)).resolves.toEqual(files); - }, - ); + await expect(getChangedFiles(options)).resolves.toEqual(files); + }); const files = [ "cspell.json", "cspell.yaml", "MixedCase.jSoN", "README.MD", + "not-spec/contosowidgetmanager/data-plane/readme.md", + "not-spec/contosowidgetmanager/resource-manager/readme.md", + "not-spec/contosowidgetmanager/Contoso.Management/main.tsp", + "not-spec/contosowidgetmanager/Contoso.Management/tspconfig.yaml", + "not-spec/contosowidgetmanager/Contoso.Management/examples/2021-11-01/Employees_Get.json", + "not-spec/contosowidgetmanager/Contoso.Management/scenarios/2021-11-01/Employees_Get.json", + "not-spec/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso.json", "specification/contosowidgetmanager/data-plane/readme.md", "specification/contosowidgetmanager/Contoso.Management/main.tsp", + "specification/contosowidgetmanager/Contoso.Management/tspconfig.yaml", "specification/contosowidgetmanager/Contoso.Management/examples/2021-11-01/Employees_Get.json", "specification/contosowidgetmanager/resource-manager/readme.md", "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso.json", "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/examples/Employees_Get.json", + "specification/contosowidgetmanager/Contoso.Management/scenarios/2021-11-01/Employees_Get.json", + "specification/compute/quickstart-templates/swagger.json", ]; + const filesResolved = files.map((f) => resolve(f)); + it("filter:json", () => { const expected = [ "cspell.json", "MixedCase.jSoN", + "not-spec/contosowidgetmanager/Contoso.Management/examples/2021-11-01/Employees_Get.json", + "not-spec/contosowidgetmanager/Contoso.Management/scenarios/2021-11-01/Employees_Get.json", + "not-spec/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso.json", "specification/contosowidgetmanager/Contoso.Management/examples/2021-11-01/Employees_Get.json", "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso.json", "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/examples/Employees_Get.json", + "specification/contosowidgetmanager/Contoso.Management/scenarios/2021-11-01/Employees_Get.json", + "specification/compute/quickstart-templates/swagger.json", ]; expect(files.filter(json)).toEqual(expected); + expect(filesResolved.filter(json)).toEqual(expected.map((f) => resolve(f))); }); it("filter:readme", () => { const expected = [ "README.MD", + "not-spec/contosowidgetmanager/data-plane/readme.md", + "not-spec/contosowidgetmanager/resource-manager/readme.md", "specification/contosowidgetmanager/data-plane/readme.md", "specification/contosowidgetmanager/resource-manager/readme.md", ]; expect(files.filter(readme)).toEqual(expected); + expect(filesResolved.filter(readme)).toEqual(expected.map((f) => resolve(f))); }); - it("filter:specification", () => { + it("filter:typespec", () => { const expected = [ - "specification/contosowidgetmanager/data-plane/readme.md", + "not-spec/contosowidgetmanager/Contoso.Management/main.tsp", + "not-spec/contosowidgetmanager/Contoso.Management/tspconfig.yaml", "specification/contosowidgetmanager/Contoso.Management/main.tsp", - "specification/contosowidgetmanager/Contoso.Management/examples/2021-11-01/Employees_Get.json", - "specification/contosowidgetmanager/resource-manager/readme.md", - "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso.json", - "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/examples/Employees_Get.json", + "specification/contosowidgetmanager/Contoso.Management/tspconfig.yaml", ]; - - expect(files.filter(specification)).toEqual(expected); + expect(files.filter(typespec)).toEqual(expected); }); it("filter:data-plane", () => { const expected = [ + "not-spec/contosowidgetmanager/data-plane/readme.md", "specification/contosowidgetmanager/data-plane/readme.md", ]; expect(files.filter(dataPlane)).toEqual(expected); + expect(filesResolved.filter(dataPlane)).toEqual(expected.map((f) => resolve(f))); }); it("filter:resource-manager", () => { const expected = [ + "not-spec/contosowidgetmanager/resource-manager/readme.md", + "not-spec/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso.json", "specification/contosowidgetmanager/resource-manager/readme.md", "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso.json", "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/examples/Employees_Get.json", ]; expect(files.filter(resourceManager)).toEqual(expected); + expect(filesResolved.filter(resourceManager)).toEqual(expected.map((f) => resolve(f))); }); it("filter:example", () => { const expected = [ + "not-spec/contosowidgetmanager/Contoso.Management/examples/2021-11-01/Employees_Get.json", "specification/contosowidgetmanager/Contoso.Management/examples/2021-11-01/Employees_Get.json", "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/examples/Employees_Get.json", ]; expect(files.filter(example)).toEqual(expected); + expect(filesResolved.filter(example)).toEqual(expected.map((f) => resolve(f))); + }); + + it("filter:quickstartTemplate", () => { + const expected = ["specification/compute/quickstart-templates/swagger.json"]; + + expect(files.filter(quickstartTemplate)).toEqual(expected); + }); + + it("filter:scenarios", () => { + const expected = [ + "not-spec/contosowidgetmanager/Contoso.Management/scenarios/2021-11-01/Employees_Get.json", + "specification/contosowidgetmanager/Contoso.Management/scenarios/2021-11-01/Employees_Get.json", + ]; + + expect(files.filter(scenario)).toEqual(expected); + expect(filesResolved.filter(scenario)).toEqual(expected.map((f) => resolve(f))); }); it("filter:swagger", () => { const expected = [ + "not-spec/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso.json", "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso.json", ]; expect(files.filter(swagger)).toEqual(expected); + expect(filesResolved.filter(swagger)).toEqual(expected.map((f) => resolve(f))); + }); + + describe("getChangedFilesStatuses", () => { + it.each([{}, { logger: debugLogger }])( + "should categorize files correctly with all types of changes (%o)", + async (options) => { + const gitOutput = [ + "A\tspecification/new-service/readme.md", + "M\tspecification/existing-service/main.tsp", + "D\tspecification/old-service/contoso.json", + "R100\tspecification/service/old-name.json\tspecification/service/new-name.json", + "C90\tspecification/template/base.json\tspecification/service/derived.json", + "T\tspecification/service/type-changed.json", + ].join("\n"); + + vi.mocked(simpleGit.simpleGit().diff).mockResolvedValue(gitOutput); + const result = await getChangedFilesStatuses(options); + expect(result).toEqual({ + additions: ["specification/new-service/readme.md", "specification/service/derived.json"], + modifications: [ + "specification/existing-service/main.tsp", + "specification/service/type-changed.json", + ], + deletions: ["specification/old-service/contoso.json"], + renames: [ + { + from: "specification/service/old-name.json", + to: "specification/service/new-name.json", + }, + ], + total: 6, + }); + }, + ); + + it("should handle empty git output", async () => { + vi.mocked(simpleGit.simpleGit().diff).mockResolvedValue(""); + const result = await getChangedFilesStatuses(); + expect(result).toEqual({ + additions: [], + modifications: [], + deletions: [], + renames: [], + total: 0, + }); + }); + + it("should handle only additions", async () => { + const gitOutput = [ + "A\tspecification/service1/readme.md", + "A\tspecification/service2/main.tsp", + ].join("\n"); + + vi.mocked(simpleGit.simpleGit().diff).mockResolvedValue(gitOutput); + const result = await getChangedFilesStatuses(); + expect(result).toEqual({ + additions: ["specification/service1/readme.md", "specification/service2/main.tsp"], + modifications: [], + deletions: [], + renames: [], + total: 2, + }); + }); + + it("should handle only renames", async () => { + const gitOutput = [ + "R95\told/path/file1.json\tnew/path/file1.json", + "R100\tservice/old.tsp\tservice/new.tsp", + ].join("\n"); + + vi.mocked(simpleGit.simpleGit().diff).mockResolvedValue(gitOutput); + const result = await getChangedFilesStatuses(); + expect(result).toEqual({ + additions: [], + modifications: [], + deletions: [], + renames: [ + { + from: "old/path/file1.json", + to: "new/path/file1.json", + }, + { + from: "service/old.tsp", + to: "service/new.tsp", + }, + ], + total: 2, + }); + }); + + it("should pass git options correctly", async () => { + const options = { + baseCommitish: "origin/main", + headCommitish: "feature-branch", + cwd: "/custom/path", + }; + + vi.mocked(simpleGit.simpleGit().diff).mockResolvedValue("A\ttest.json"); + await getChangedFilesStatuses(options); + expect(simpleGit.simpleGit).toHaveBeenCalledWith("/custom/path"); + expect(simpleGit.simpleGit().diff).toHaveBeenCalledWith([ + "--name-status", + "origin/main", + "feature-branch", + ]); + }); }); }); diff --git a/.github/shared/test/doc-preview.test.js b/.github/shared/test/doc-preview.test.js new file mode 100644 index 000000000000..b0425c4f8b36 --- /dev/null +++ b/.github/shared/test/doc-preview.test.js @@ -0,0 +1,140 @@ +// @ts-check + +import { describe, expect, test } from "vitest"; +import { + getSwaggersToProcess, + indexMd, + mappingJSONTemplate, + parseSwaggerFilePath, + repoJSONTemplate, +} from "../src/doc-preview.js"; + +describe("parseSwaggerFilePath", () => { + test("throws null when given invalid path", () => { + expect(() => parseSwaggerFilePath("invalid/path/to/swagger.json")).toThrow(); + }); + + test("parses valid swagger file path", () => { + const path = + "specification/batch/data-plane/Azure.Batch/preview/2024-07-01.20.0/BatchService.json"; + const result = parseSwaggerFilePath(path); + + expect(result).toEqual({ + path: path, + serviceName: "batch", + serviceType: "data-plane", + resourceProvider: "Azure.Batch", + releaseState: "preview", + apiVersion: "2024-07-01.20.0", + fileName: "BatchService.json", + }); + }); +}); + +describe("getSwaggersToProcess", () => { + test("throws when swagger paths do not properly parse", () => { + expect(() => getSwaggersToProcess(["specification/inscrutable/path/swagger.json"])).toThrow(); + }); + + test("returns swaggers to process for valid files", () => { + const files = [ + "specification/batch/data-plane/Azure.Batch/preview/2024-07-01.20.0/BatchService.json", + ]; + + const { selectedVersion, swaggersToProcess } = getSwaggersToProcess(files); + + expect(selectedVersion).toEqual("2024-07-01.20.0"); + expect(swaggersToProcess).toEqual(files); + }); + + test("selects the latest version from multiple files with multiple versions", () => { + const files = [ + "specification/batch/data-plane/Azure.Batch/preview/2024-07-01.20.0/BatchService.json", + "specification/batch/data-plane/Azure.Batch/preview/2025-06-01/BatchService.json", + ]; + + const { selectedVersion, swaggersToProcess } = getSwaggersToProcess(files); + + expect(selectedVersion).toEqual("2025-06-01"); + expect(swaggersToProcess).toEqual([files[1]]); + }); +}); + +describe("repoJSONTemplate", () => { + test("matches snapshot", () => { + const actual = repoJSONTemplate("test-repo", "1234"); + + expect(actual).toMatchInlineSnapshot(` + { + "repo": [ + { + "name": "_swagger_specs", + "prNumber": "1234", + "url": "https://github.com/test-repo", + }, + ], + } + `); + }); +}); + +describe("mappingJSONTemplate", () => { + test("matches snapshot", () => { + const swaggers = [ + "specification/batch/data-plane/Azure.Batch/preview/2024-07-01.20.0/BatchService.json", + ]; + const actual = mappingJSONTemplate(swaggers); + + expect(actual).toMatchInlineSnapshot(` + { + "enable_markdown_fragment": true, + "formalize_url": true, + "markdown_fragment_folder": "authored", + "organizations": [ + { + "default_toc_title": "Getting Started", + "index": "index.md", + "services": [ + { + "swagger_files": [ + { + "source": "_swagger_specs/specification/batch/data-plane/Azure.Batch/preview/2024-07-01.20.0/BatchService.json", + }, + ], + "toc_title": "Documentation Preview", + "url_group": "documentation-preview", + }, + ], + "version": "default", + }, + ], + "target_api_root_dir": "structured", + "use_yaml_toc": true, + "version_list": [ + "default", + ], + } + `); + }); +}); + +describe("indexMd", () => { + test("matches snapshot", () => { + const buildId = "build-123"; + const repoName = "test-repo"; + const prNumber = "1234"; + const actual = indexMd(buildId, repoName, prNumber); + + expect(actual).toMatchInlineSnapshot(` + "# Documentation Preview for swagger pipeline build #build-123 + + Welcome to documentation preview for test-repo/pull/1234 + created via the swagger pipeline. + + Your documentation may be viewed in the menu on the left hand side. + + If you have issues around documentation generation, please feel free to contact + us in the [Docs Support Teams Channel](https://aka.ms/ci-fix/api-docs-help)" + `); + }); +}); diff --git a/.github/shared/test/equality.test.js b/.github/shared/test/equality.test.js index 1dbcc77b255a..b4ab178f4e6e 100644 --- a/.github/shared/test/equality.test.js +++ b/.github/shared/test/equality.test.js @@ -31,9 +31,9 @@ describe("equality", () => { [new Set([1, 2]), new Set([1, 2]), true], [new Set([1, 2, 3]), new Set([1, 2, 3]), true], ])("setEquals(%s, %s, %s)", (set1, set2, expected) => { - // @ts-ignore + // @ts-expect-error testing runtime behavior of invalid types expect(setEquals(set1, set2)).toBe(expected); - // @ts-ignore + // @ts-expect-error testing runtime behavior of invalid types expect(setEquals(set2, set1)).toBe(expected); }); }); diff --git a/.github/shared/test/error-reporting.test.js b/.github/shared/test/error-reporting.test.js new file mode 100644 index 000000000000..0fa3a2c64ea5 --- /dev/null +++ b/.github/shared/test/error-reporting.test.js @@ -0,0 +1,73 @@ +// @ts-check + +import fs from "fs/promises"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { annotateFileError, setOutput, setSummary } from "../src/error-reporting.js"; + +describe("ErrorReporting", () => { + let logSpy; + + beforeEach(() => { + logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + // ensure that on test runs GITHUB_STEP_SUMMARY is not set in my current env by default + // this gives us a clean slate for each test + delete process.env.GITHUB_STEP_SUMMARY; + delete process.env.GITHUB_OUTPUT; + }); + + afterEach(() => { + logSpy.mockRestore(); + }); + + it("should warn when calling setOutput when GITHUB_OUTPUT is unset", () => { + setOutput("test", "value"); + expect(logSpy).toHaveBeenCalledWith( + "GITHUB_OUTPUT is not set. Skipping test update with value 'value.'", + ); + }); + + it("should warn when calling setSummary when GITHUB_STEP_SUMMARY is unset", () => { + setSummary("hello"); + expect(logSpy).toHaveBeenCalledWith("GITHUB_STEP_SUMMARY is not set. Skipping summary update."); + }); + + it("should write to the summary file when GITHUB_STEP_SUMMARY is set", async () => { + process.env.GITHUB_STEP_SUMMARY = `${__dirname}/tmp-summary.md`; + + await fs.rm(process.env.GITHUB_STEP_SUMMARY, { force: true }); + + setSummary("# Title"); + + expect(logSpy).not.toHaveBeenCalledWith( + "GITHUB_STEP_SUMMARY is not set. Skipping summary update.", + ); + + const content = await fs.readFile(process.env.GITHUB_STEP_SUMMARY, "utf-8"); + + // cleanup after the test so nothing is left behi + await fs.rm(process.env.GITHUB_STEP_SUMMARY, { force: true }); + + expect(content).toBe("# Title"); + }); + + it("should write an output when GITHUB_OUTPUT is set", async () => { + process.env.GITHUB_OUTPUT = `${__dirname}/tmp-output.txt`; + + setOutput("test", "value"); + + expect(logSpy).not.toHaveBeenCalledWith( + "GITHUB_OUTPUT is not set. Skipping test update with value 'value.'", + ); + + const content = await fs.readFile(process.env.GITHUB_OUTPUT, "utf-8"); + expect(content).toBe("test=value\n"); + + // cleanup after the test so nothing is left behind + await fs.unlink(process.env.GITHUB_OUTPUT); + }); + + it("should emit a GitHub-style error annotation", () => { + annotateFileError("src/foo.js", "Something broke", 42, 7); + expect(logSpy).toHaveBeenCalledWith("::error file=src/foo.js,line=42,col=7::Something broke"); + }); +}); diff --git a/.github/shared/test/exec.test.js b/.github/shared/test/exec.test.js index 52bba8c559cd..818a40a84924 100644 --- a/.github/shared/test/exec.test.js +++ b/.github/shared/test/exec.test.js @@ -10,20 +10,17 @@ describe("execFile", () => { const args = ["-e", `console.log("test")`]; const expected = "test\n"; - it.each([{}, options])( - "exec succeeds with default buffer (options: %o)", - async (options) => { - await expect(execFile(file, args, options)).resolves.toEqual({ - stdout: expected, - stderr: "", - }); - }, - ); + it.each([{}, options])("exec succeeds with default buffer (options: %o)", async (options) => { + await expect(execFile(file, args, options)).resolves.toEqual({ + stdout: expected, + stderr: "", + }); + }); it("exec succeeds with exact-sized buffer", async () => { - await expect( - execFile(file, args, { ...options, maxBuffer: expected.length }), - ).resolves.toEqual({ stdout: expected, stderr: "" }); + await expect(execFile(file, args, { ...options, maxBuffer: expected.length })).resolves.toEqual( + { stdout: expected, stderr: "" }, + ); }); it("exec fails with too-small buffer", async () => { @@ -63,9 +60,7 @@ describe("execNpmExec", () => { // something referenced in package.json. In this case, js-yaml is present // so it is used. it("runs js-yaml", async () => { - await expect( - execNpmExec(["js-yaml", "--version"], options), - ).resolves.toEqual({ + await expect(execNpmExec(["js-yaml", "--version"], options)).resolves.toEqual({ stdout: expect.toSatisfy((v) => semver.valid(v)), stderr: "", error: undefined, diff --git a/.github/shared/test/fixtures/Swagger/ignoreExamples/examples/example.json b/.github/shared/test/fixtures/swagger/ignoreExamples/examples/example.json similarity index 100% rename from .github/shared/test/fixtures/Swagger/ignoreExamples/examples/example.json rename to .github/shared/test/fixtures/swagger/ignoreExamples/examples/example.json diff --git a/.github/shared/test/fixtures/Swagger/ignoreExamples/included.json b/.github/shared/test/fixtures/swagger/ignoreExamples/included.json similarity index 100% rename from .github/shared/test/fixtures/Swagger/ignoreExamples/included.json rename to .github/shared/test/fixtures/swagger/ignoreExamples/included.json diff --git a/.github/shared/test/fixtures/Swagger/ignoreExamples/swagger.json b/.github/shared/test/fixtures/swagger/ignoreExamples/swagger.json similarity index 100% rename from .github/shared/test/fixtures/Swagger/ignoreExamples/swagger.json rename to .github/shared/test/fixtures/swagger/ignoreExamples/swagger.json diff --git a/.github/shared/test/fixtures/swagger/specification/common-types/resource-management/v2/types.json b/.github/shared/test/fixtures/swagger/specification/common-types/resource-management/v2/types.json new file mode 100644 index 000000000000..31f4bcaf80de --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/common-types/resource-management/v2/types.json @@ -0,0 +1,26 @@ +{ + "swagger": "2.0", + "info": { + "title": "Common Types", + "version": "v2" + }, + "definitions": { + "Resource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "readOnly": true + }, + "name": { + "type": "string", + "readOnly": true + }, + "type": { + "type": "string", + "readOnly": true + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/common-types/resource-management/v3/types.json b/.github/shared/test/fixtures/swagger/specification/common-types/resource-management/v3/types.json new file mode 100644 index 000000000000..6d1a88e373f2 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/common-types/resource-management/v3/types.json @@ -0,0 +1,69 @@ +{ + "swagger": "2.0", + "info": { + "title": "Common Types - Resource Management", + "version": "v3", + "description": "Common types for resource management APIs" + }, + "host": "management.azure.com", + "schemes": ["https"], + "consumes": ["application/json"], + "produces": ["application/json"], + "definitions": { + "Resource": { + "type": "object", + "description": "Common fields that are returned in the response for all Azure Resource Manager resources", + "properties": { + "id": { + "readOnly": true, + "type": "string", + "description": "Fully qualified resource ID for the resource. Ex - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}" + }, + "name": { + "readOnly": true, + "type": "string", + "description": "The name of the resource" + }, + "type": { + "readOnly": true, + "type": "string", + "description": "The type of the resource. E.g. \"Microsoft.Compute/virtualMachines\" or \"Microsoft.Storage/storageAccounts\"" + } + } + }, + "ProxyResource": { + "type": "object", + "description": "The resource model definition for a Azure Resource Manager proxy resource.", + "allOf": [ + { + "$ref": "#/definitions/Resource" + } + ] + }, + "TrackedResource": { + "type": "object", + "description": "The resource model definition for an Azure Resource Manager tracked top level resource", + "allOf": [ + { + "$ref": "#/definitions/Resource" + } + ], + "properties": { + "tags": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Resource tags." + }, + "location": { + "type": "string", + "description": "The geo-location where the resource lives" + } + }, + "required": [ + "location" + ] + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConfigurationNamesList.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConfigurationNamesList.json new file mode 100644 index 000000000000..5bc65903c540 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConfigurationNamesList.json @@ -0,0 +1,52 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "properties": { + "targetService": "MICROSOFT.APPCONFIGURATION/CONFIGURATIONSTORES", + "clientType": "none", + "authType": "systemAssignedIdentity", + "names": [ + { + "value": "AZURE_APPCONFIGURATION_ENDPOINT", + "description": "App configuration endpoint" + }, + { + "value": "AZURE_APPCONFIGURATION_SCOPE", + "description": "The scopes required for the token." + } + ] + } + }, + { + "properties": { + "targetService": "MICROSOFT.APPCONFIGURATION/CONFIGURATIONSTORES", + "clientType": "none", + "authType": "userAssignedIdentity", + "names": [ + { + "value": "AZURE_APPCONFIGURATION_ENDPOINT", + "description": "App configuration endpoint" + }, + { + "value": "AZURE_APPCONFIGURATION_CLIENTID", + "description": "The client(application) ID of the user identity." + }, + { + "value": "AZURE_APPCONFIGURATION_SCOPE", + "description": "The scopes required for getting token." + } + ] + } + } + ], + "nextLink": null + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConnectorDryrunCreate.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConnectorDryrunCreate.json new file mode 100644 index 000000000000..124a0c574565 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConnectorDryrunCreate.json @@ -0,0 +1,116 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "dryrunName": "dryrunName", + "parameters": { + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name", + "secretInfo": { + "secretType": "rawValue", + "value": "secret" + } + } + } + } + } + }, + "responses": { + "200": { + "body": { + "name": "dryrunName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/locations/westus/dryruns/dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name" + } + }, + "prerequisiteResults": [ + { + "type": "basicError", + "code": "ResourceNotFound", + "message": "Target resource is not found" + }, + { + "type": "permissionsMissing", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc", + "permissions": [ + "Microsoft.DocumentDb/databaseAccounts/write" + ] + } + ], + "operationPreviews": [ + { + "name": "configFirewallRule", + "operationType": "configNetwork", + "description": "Config firewall rule for target service to allow source service access", + "action": "Microsoft.DocumentDb/databaseAccounts/write", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc" + } + ], + "provisioningState": "Succeeded" + } + } + }, + "201": { + "body": { + "name": "dryrunName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/locations/westus/dryruns/dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name" + } + }, + "prerequisiteResults": [ + { + "type": "basicError", + "code": "ResourceNotFound", + "message": "Target resource is not found" + }, + { + "type": "permissionsMissing", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc", + "permissions": [ + "Microsoft.DocumentDb/databaseAccounts/write" + ] + } + ], + "operationPreviews": [ + { + "name": "configFirewallRule", + "operationType": "configNetwork", + "description": "Config firewall rule for target service to allow source service access", + "action": "Microsoft.DocumentDb/databaseAccounts/write", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc" + } + ], + "provisioningState": "Accepted" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConnectorDryrunDelete.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConnectorDryrunDelete.json new file mode 100644 index 000000000000..2ee66585cb7e --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConnectorDryrunDelete.json @@ -0,0 +1,13 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "dryrunName": "dryrunName" + }, + "responses": { + "200": {}, + "204": {} + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConnectorDryrunGet.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConnectorDryrunGet.json new file mode 100644 index 000000000000..92f012ac8137 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConnectorDryrunGet.json @@ -0,0 +1,33 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "dryrunName": "dryrunName" + }, + "responses": { + "200": { + "body": { + "name": "dryrunName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/locations/westus/dryruns/dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConnectorDryrunList.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConnectorDryrunList.json new file mode 100644 index 000000000000..4b2fdc4baa66 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConnectorDryrunList.json @@ -0,0 +1,36 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "name": "dryrunName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/locations/westus/dryruns/dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConnectorDryrunUpdate.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConnectorDryrunUpdate.json new file mode 100644 index 000000000000..8267626fd374 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConnectorDryrunUpdate.json @@ -0,0 +1,78 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "dryrunName": "dryrunName", + "parameters": { + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name", + "secretInfo": { + "secretType": "rawValue", + "value": "secret" + } + } + } + } + } + }, + "responses": { + "200": { + "body": { + "name": "dryrunName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/locations/westus/dryruns/dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name" + } + }, + "prerequisiteResults": [ + { + "type": "basicError", + "code": "ResourceNotFound", + "message": "Target resource is not found" + }, + { + "type": "permissionsMissing", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc", + "permissions": [ + "Microsoft.DocumentDb/databaseAccounts/write" + ] + } + ], + "operationPreviews": [ + { + "name": "configFirewallRule", + "operationType": "configNetwork", + "description": "Config firewall rule for target service to allow source service access", + "action": "Microsoft.DocumentDb/databaseAccounts/write", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc" + } + ], + "provisioningState": "Succeeded" + } + } + }, + "202": { + "headers": { + "azure-AsyncOperation": "http://azure.async.operation/status" + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConnectorList.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConnectorList.json new file mode 100644 index 000000000000..b5664c15c000 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ConnectorList.json @@ -0,0 +1,34 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/linkers/linkName", + "name": "linkName", + "type": "Microsoft.ServiceLinker/devConnectors", + "properties": { + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/Connectors.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/Connectors.json new file mode 100644 index 000000000000..4cfeb7d61b2f --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/Connectors.json @@ -0,0 +1,43 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "connectorName": "connectorName" + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/devConnnectors/linkName", + "name": "linkName", + "type": "Microsoft.ServiceLinker/devConnectors", + "properties": { + "authInfo": { + "authType": "systemAssignedIdentity", + "roles": [ + "customizedOwner" + ] + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "publicNetworkSolution": { + "firewallRules": { + "ipRanges": [ + "182.22.120" + ], + "callerClientIP": "true" + }, + "action": "enable", + "deleteOrUpdateBehavior": "ForcedCleanup" + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/DeleteConnector.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/DeleteConnector.json new file mode 100644 index 000000000000..f0340dfb4dfb --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/DeleteConnector.json @@ -0,0 +1,18 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "connectorName": "connectorName" + }, + "responses": { + "200": {}, + "204": {}, + "202": { + "headers": { + "azure-AsyncOperation": "http://azure.async.operation/status" + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/DeleteDryrun.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/DeleteDryrun.json new file mode 100644 index 000000000000..b406311af609 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/DeleteDryrun.json @@ -0,0 +1,11 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "dryrunName": "dryrunName" + }, + "responses": { + "200": {}, + "204": {} + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/DeleteLinker.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/DeleteLinker.json new file mode 100644 index 000000000000..c49d99a9b09e --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/DeleteLinker.json @@ -0,0 +1,16 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName" + }, + "responses": { + "200": {}, + "204": {}, + "202": { + "headers": { + "azure-AsyncOperation": "http://azure.async.operation/status" + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/GenerateConfigurations.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/GenerateConfigurations.json new file mode 100644 index 000000000000..3e367d73b9a3 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/GenerateConfigurations.json @@ -0,0 +1,26 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "connectorName": "connectorName", + "parameters": { + "customizedKeys": { + "ASL_DocumentDb_ConnectionString": "MyConnectionstring" + } + } + }, + "responses": { + "200": { + "body": { + "configurations": [ + { + "name": "MyConnectionstring", + "value": "ConnectionString" + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/GetConfigurations.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/GetConfigurations.json new file mode 100644 index 000000000000..00908c92a5b5 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/GetConfigurations.json @@ -0,0 +1,41 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.App/containerApps/test-app", + "linkerName": "linkName" + }, + "responses": { + "200": { + "body": { + "configurations": [ + { + "name": "AZURE_POSTGRESQL_HOST", + "value": "Host", + "configType": "Default" + }, + { + "name": "AZURE_POSTGRESQL_USER", + "value": "Username", + "configType": "Default" + }, + { + "name": "AZURE_POSTGRESQL_DATABASE", + "value": "DatabaseName", + "configType": "Default" + }, + { + "name": "AZURE_POSTGRESQL_PORT", + "value": "Port", + "configType": "Default" + }, + { + "name": "AZURE_POSTGRESQL_PASSWORD", + "value": "SecretUri", + "configType": "KeyVaultSecret", + "keyVaultReferenceIdentity": "system" + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/GetDaprConfigurations.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/GetDaprConfigurations.json new file mode 100644 index 000000000000..5b6bbc3d51d4 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/GetDaprConfigurations.json @@ -0,0 +1,33 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "properties": { + "targetType": "MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES", + "authType": "secret", + "daprProperties": { + "version": "v1", + "componentType": "bindings", + "runtimeVersion": "1.10", + "bindingComponentDirection": "input", + "metadata": [ + { + "name": "containerName", + "description": "The name of the container to be used for Dapr state.", + "required": "true" + } + ] + } + } + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/GetDryrun.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/GetDryrun.json new file mode 100644 index 000000000000..8a5d7404edd0 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/GetDryrun.json @@ -0,0 +1,32 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "dryrunName": "dryrunName" + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/dryruns/dryrunName", + "name": "dryrunName", + "type": "Microsoft.ServiceLinker/dryruns", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/Linker.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/Linker.json new file mode 100644 index 000000000000..87ae58c108aa --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/Linker.json @@ -0,0 +1,47 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName" + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "name": "linkName", + "type": "Microsoft.ServiceLinker/links", + "properties": { + "authInfo": { + "authType": "secret", + "name": "name" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "secretStore": { + "keyVaultId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/kvname" + }, + "scope": "AKS-Namespace", + "clientType": "dotnet", + "publicNetworkSolution": { + "action": "enable" + }, + "configurationInfo": { + "deleteOrUpdateBehavior": "ForcedCleanup", + "customizedKeys": { + "AZURE_MYSQL_CONNECTIONSTRING": "myConnectionstring", + "AZURE_MYSQL_SSLMODE": "mySslmode" + }, + "additionalConfigurations": { + "throttlingLimit": "100" + } + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/LinkerGenerateConfigurations.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/LinkerGenerateConfigurations.json new file mode 100644 index 000000000000..d21e946f8ec3 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/LinkerGenerateConfigurations.json @@ -0,0 +1,24 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName", + "parameters": { + "customizedKeys": { + "ASL_DocumentDb_ConnectionString": "MyConnectionstring" + } + } + }, + "responses": { + "200": { + "body": { + "configurations": [ + { + "name": "MyConnectionstring", + "value": "ConnectionString" + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/LinkerList.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/LinkerList.json new file mode 100644 index 000000000000..afffd7662e19 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/LinkerList.json @@ -0,0 +1,32 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/links/linkName", + "name": "linkName", + "type": "Microsoft.ServiceLinker/links", + "properties": { + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ListDryrun.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ListDryrun.json new file mode 100644 index 000000000000..b4dda3dac29e --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ListDryrun.json @@ -0,0 +1,35 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/dryruns/dryrunName", + "name": "dryrunName", + "type": "Microsoft.ServiceLinker/dryruns", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/OperationsList.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/OperationsList.json new file mode 100644 index 000000000000..5dee50b01147 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/OperationsList.json @@ -0,0 +1,184 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "display": { + "description": "Register the subscription for Microsoft.ServiceLinker", + "operation": "Register the Microsoft.ServiceLinker", + "provider": "Microsoft.ServiceLinker", + "resource": "Microsoft.ServiceLinker" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/register/action" + }, + { + "display": { + "description": "Unregister the subscription for Microsoft.ServiceLinker", + "operation": "Unregister the Microsoft.ServiceLinker", + "provider": "Microsoft.ServiceLinker", + "resource": "Microsoft.ServiceLinker" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/unregister/action" + }, + { + "display": { + "description": "read operations", + "operation": "read_operations", + "provider": "Microsoft.ServiceLinker", + "resource": "operations" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/operations/read" + }, + { + "display": { + "description": "list dryrun jobs", + "operation": "Dryrun_List", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/read" + }, + { + "display": { + "description": "get a dryrun job", + "operation": "Dryrun_Get", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/read" + }, + { + "display": { + "description": "create a dryrun job to do necessary check before actual creation", + "operation": "Dryrun_Create", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/write" + }, + { + "display": { + "description": "delete a dryrun job", + "operation": "Dryrun_Delete", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/delete" + }, + { + "display": { + "description": "add a dryrun job to do necessary check before actual creation", + "operation": "Dryrun_Update", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/write" + }, + { + "display": { + "description": "read operationStatuses", + "operation": "read_operationStatuses", + "provider": "Microsoft.ServiceLinker", + "resource": "locations/operationStatuses" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/locations/operationStatuses/read" + }, + { + "display": { + "description": "write operationStatuses", + "operation": "write_operationStatuses", + "provider": "Microsoft.ServiceLinker", + "resource": "locations/operationStatuses" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/locations/operationStatuses/write" + }, + { + "display": { + "description": "Returns list of Linkers which connects to the resource.", + "operation": "Linker_List", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/read" + }, + { + "display": { + "description": "Returns Linker resource for a given name.", + "operation": "Linker_Get", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/read" + }, + { + "display": { + "description": "Create or update linker resource.", + "operation": "Linker_CreateOrUpdate", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/write" + }, + { + "display": { + "description": "Delete a link.", + "operation": "Linker_Delete", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/delete" + }, + { + "display": { + "description": "Operation to update an existing link.", + "operation": "Linker_Update", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/write" + }, + { + "display": { + "description": "Validate a link.", + "operation": "Linker_Validate", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/validateLinker/action" + }, + { + "display": { + "description": "list source configurations for a linker.", + "operation": "Linker_ListConfigurations", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/listConfigurations/action" + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/PatchConnector.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/PatchConnector.json new file mode 100644 index 000000000000..6cbf8bacfe26 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/PatchConnector.json @@ -0,0 +1,64 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "connectorName": "connectorName", + "parameters": { + "properties": { + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id", + "secret": "secret" + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + }, + "202": { + "headers": { + "azure-asyncoperation": "http://azure.async.operation/status" + }, + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/PatchDryrun.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/PatchDryrun.json new file mode 100644 index 000000000000..868435b29b8f --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/PatchDryrun.json @@ -0,0 +1,77 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "dryrunName": "dryrunName", + "parameters": { + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name", + "secretInfo": { + "secretType": "rawValue", + "value": "secret" + } + } + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/dryruns/dryrunName", + "type": "Microsoft.ServiceLinker/dryruns", + "name": "dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name" + } + }, + "prerequisiteResults": [ + { + "type": "basicError", + "code": "ResourceNotFound", + "message": "Target resource is not found" + }, + { + "type": "permissionsMissing", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc", + "permissions": [ + "Microsoft.DocumentDb/databaseAccounts/write" + ] + } + ], + "operationPreviews": [ + { + "name": "configFirewallRule", + "operationType": "configNetwork", + "description": "Config firewall rule for target service to allow source service access", + "action": "Microsoft.DocumentDb/databaseAccounts/write", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc" + } + ], + "provisioningState": "Succeeded" + } + } + }, + "202": { + "headers": { + "azure-AsyncOperation": "http://azure.async.operation/status" + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/PatchLinker.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/PatchLinker.json new file mode 100644 index 000000000000..dfe22733c45d --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/PatchLinker.json @@ -0,0 +1,59 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName", + "parameters": { + "properties": { + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id", + "secret": "secret" + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + }, + "201": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/PutConnector.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/PutConnector.json new file mode 100644 index 000000000000..8d4cc02c8405 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/PutConnector.json @@ -0,0 +1,63 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "connectorName": "connectorName", + "parameters": { + "properties": { + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret" + }, + "secretStore": { + "keyVaultId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-kv" + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "secret" + }, + "secretStore": { + "keyVaultId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-kv" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + }, + "201": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "secret" + }, + "secretStore": { + "keyVaultId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-kv" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/PutDryrun.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/PutDryrun.json new file mode 100644 index 000000000000..3e83a7728c7a --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/PutDryrun.json @@ -0,0 +1,116 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "dryrunName": "dryrunName", + "parameters": { + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name", + "secretInfo": { + "secretType": "rawValue", + "value": "secret" + } + } + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/dryruns/dryrunName", + "type": "Microsoft.ServiceLinker/dryruns", + "name": "dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name" + } + }, + "prerequisiteResults": [ + { + "type": "basicError", + "code": "ResourceNotFound", + "message": "Target resource is not found" + }, + { + "type": "permissionsMissing", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc", + "permissions": [ + "Microsoft.DocumentDb/databaseAccounts/write" + ] + } + ], + "operationPreviews": [ + { + "name": "configFirewallRule", + "operationType": "configNetwork", + "description": "Config firewall rule for target service to allow source service access", + "action": "Microsoft.DocumentDb/databaseAccounts/write", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc" + } + ], + "provisioningState": "Succeeded" + } + } + }, + "201": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/dryruns/dryrunName", + "type": "Microsoft.ServiceLinker/dryruns", + "name": "dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name" + } + }, + "prerequisiteResults": [ + { + "type": "basicError", + "code": "ResourceNotFound", + "message": "Target resource is not found" + }, + { + "type": "permissionsMissing", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc", + "permissions": [ + "Microsoft.DocumentDb/databaseAccounts/write" + ] + } + ], + "operationPreviews": [ + { + "name": "configFirewallRule", + "operationType": "configNetwork", + "description": "Config firewall rule for target service to allow source service access", + "action": "Microsoft.DocumentDb/databaseAccounts/write", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc" + } + ], + "provisioningState": "Updating" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/PutLinker.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/PutLinker.json new file mode 100644 index 000000000000..eff0bfb84fe1 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/PutLinker.json @@ -0,0 +1,68 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName", + "parameters": { + "properties": { + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/servers/test-pg/databases/test-db" + }, + "vNetSolution": { + "type": "serviceEndpoint" + }, + "authInfo": { + "authType": "secret", + "name": "name", + "secretInfo": { + "secretType": "rawValue", + "value": "secret" + } + } + } + } + }, + "responses": { + "200": { + "body": { + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "properties": { + "authInfo": { + "authType": "secret", + "name": "name" + }, + "vNetSolution": { + "type": "serviceEndpoint" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/servers/test-pg/databases/test-db" + } + } + } + }, + "201": { + "body": { + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "properties": { + "authInfo": { + "authType": "secret", + "name": "name" + }, + "vNetSolution": { + "type": "serviceEndpoint" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/servers/test-pg/databases/test-db" + } + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ValidateConnectorSuccess.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ValidateConnectorSuccess.json new file mode 100644 index 000000000000..a7efdadedfb8 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ValidateConnectorSuccess.json @@ -0,0 +1,38 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "connectorName": "connectorName" + }, + "responses": { + "200": { + "body": { + "properties": { + "isConnectionAvailable": true, + "reportStartTimeUtc": "2020-07-12T22:05:09Z", + "reportEndTimeUtc": "2020-07-12T22:06:09Z", + "authType": "secret", + "validationDetail": [ + { + "name": "TargetExistence", + "description": "The target existence is validated", + "result": "success" + }, + { + "name": "TargetNetworkAccess", + "description": "Deny public network access is set to yes. Please confirm you are using private endpoint connection to access target resource.", + "result": "warning" + } + ] + } + } + }, + "202": { + "headers": { + "azure-AsyncOperation": "http://azure.async.operation/status" + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ValidateLinkerSuccess.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ValidateLinkerSuccess.json new file mode 100644 index 000000000000..85ec6c34a5d9 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/examples/ValidateLinkerSuccess.json @@ -0,0 +1,38 @@ +{ + "parameters": { + "api-version": "2023-04-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName" + }, + "responses": { + "200": { + "body": { + "properties": { + "isConnectionAvailable": true, + "reportStartTimeUtc": "2020-07-12T22:05:09Z", + "reportEndTimeUtc": "2020-07-12T22:06:09Z", + "sourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db", + "targetId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db", + "authType": "secret", + "validationDetail": [ + { + "name": "TargetExistence", + "description": "The target existence is validated", + "result": "success" + }, + { + "name": "TargetNetworkAccess", + "description": "Deny public network access is set to yes. Please confirm you are using private endpoint connection to access target resource.", + "result": "warning" + } + ] + } + } + }, + "202": { + "headers": { + "azure-AsyncOperation": "http://azure.async.operation/status" + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/servicelinker.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/servicelinker.json new file mode 100644 index 000000000000..c4cbc1dd94ac --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2023-04-01-preview/servicelinker.json @@ -0,0 +1,2902 @@ +{ + "swagger": "2.0", + "info": { + "title": "Microsoft.ServiceLinker", + "description": "Microsoft.ServiceLinker provider", + "version": "2023-04-01-preview" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "security": [ + { + "azure_auth": [ + "user_impersonation" + ] + } + ], + "securityDefinitions": { + "azure_auth": { + "type": "oauth2", + "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize", + "flow": "implicit", + "description": "Azure Active Directory OAuth2 Flow.", + "scopes": { + "user_impersonation": "impersonate your user account" + } + } + }, + "paths": { + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/dryruns": { + "get": { + "tags": [ + "Connector" + ], + "operationId": "Connector_ListDryrun", + "description": "list dryrun jobs", + "x-ms-examples": { + "ConnectorDryrunList": { + "$ref": "./examples/ConnectorDryrunList.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunList" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/dryruns/{dryrunName}": { + "get": { + "tags": [ + "Connector" + ], + "operationId": "Connector_GetDryrun", + "description": "get a dryrun job", + "x-ms-examples": { + "ConnectorDryrunGet": { + "$ref": "./examples/ConnectorDryrunGet.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "tags": [ + "Connector" + ], + "operationId": "Connector_CreateDryrun", + "description": "create a dryrun job to do necessary check before actual creation", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "ConnectorDryrunCreate": { + "$ref": "./examples/ConnectorDryrunCreate.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + }, + { + "name": "parameters", + "description": "dryrun resource.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DryrunResource" + } + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "201": { + "description": "Long running operation", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "patch": { + "tags": [ + "Connector" + ], + "operationId": "Connector_UpdateDryrun", + "description": "update a dryrun job to do necessary check before actual creation", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "ConnectorDryrunUpdate": { + "$ref": "./examples/ConnectorDryrunUpdate.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + }, + { + "name": "parameters", + "description": "dryrun resource.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DryrunPatch" + } + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "202": { + "description": "Accepted - Returns this status until the asynchronous operation has completed." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "delete": { + "tags": [ + "Connector" + ], + "operationId": "Connector_DeleteDryrun", + "description": "delete a dryrun job", + "x-ms-examples": { + "ConnectorDryrunDelete": { + "$ref": "./examples/ConnectorDryrunDelete.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + } + ], + "responses": { + "200": { + "description": "OK. The job is deleted." + }, + "204": { + "description": "Deleted. The job is not found." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/connectors": { + "get": { + "deprecated": false, + "description": "Returns list of connector which connects to the resource, which supports to config the target service during the resource provision.", + "operationId": "Connector_List", + "x-ms-examples": { + "ConnectorList": { + "$ref": "./examples/ConnectorList.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "Connector details.", + "schema": { + "$ref": "#/definitions/ResourceList" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/connectors/{connectorName}": { + "get": { + "description": "Returns Connector resource for a given name.", + "operationId": "Connector_Get", + "x-ms-examples": { + "Connector": { + "$ref": "./examples/Connectors.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "#/parameters/ConnectorNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "Connector details.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "description": "Create or update Connector resource.", + "operationId": "Connector_CreateOrUpdate", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PutConnector": { + "$ref": "./examples/PutConnector.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "#/parameters/ConnectorNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "parameters", + "description": "Connector details.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LinkerResource" + } + } + ], + "responses": { + "200": { + "description": "Successful.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "201": { + "description": "Long running operation.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "delete": { + "tags": [ + "Connector" + ], + "operationId": "Connector_Delete", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "description": "Delete a Connector.", + "x-ms-examples": { + "DeleteConnector": { + "$ref": "./examples/DeleteConnector.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "#/parameters/ConnectorNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The Connector is deleted." + }, + "202": { + "description": "Long running operation." + }, + "204": { + "description": "Deleted. The Connector is not found." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "patch": { + "tags": [ + "Connector" + ], + "operationId": "Connector_Update", + "description": "Operation to update an existing Connector.", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PatchConnector": { + "$ref": "./examples/PatchConnector.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "#/parameters/ConnectorNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "parameters", + "description": "Connector details.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LinkerPatch" + } + } + ], + "responses": { + "200": { + "description": "Success. The response describes a Connector.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "202": { + "description": "Long running operation.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/connectors/{connectorName}/validate": { + "post": { + "tags": [ + "Connector" + ], + "operationId": "Connector_Validate", + "description": "Validate a Connector.", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "location" + }, + "x-ms-examples": { + "ValidateConnectorSuccess": { + "$ref": "./examples/ValidateConnectorSuccess.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "#/parameters/ConnectorNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ValidateOperationResult" + } + }, + "202": { + "description": "Accepted - Returns this status until the asynchronous operation has completed." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/connectors/{connectorName}/generateConfigurations": { + "post": { + "tags": [ + "Connector" + ], + "operationId": "Connector_GenerateConfigurations", + "description": "Generate configurations for a Connector.", + "x-ms-examples": { + "GenerateConfiguration": { + "$ref": "./examples/GenerateConfigurations.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "#/parameters/ConnectorNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "parameters", + "description": "Connection Info, including format, secret store, etc", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/ConfigurationInfo" + } + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ConfigurationResult" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers": { + "get": { + "deprecated": false, + "description": "Returns list of Linkers which connects to the resource. which supports to config both application and target service during the resource provision.", + "operationId": "Linker_List", + "x-ms-examples": { + "LinkerList": { + "$ref": "./examples/LinkerList.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "Linker details.", + "schema": { + "$ref": "#/definitions/ResourceList" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers/{linkerName}": { + "get": { + "description": "Returns Linker resource for a given name.", + "operationId": "Linker_Get", + "x-ms-examples": { + "Linker": { + "$ref": "./examples/Linker.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + } + ], + "responses": { + "200": { + "description": "Linker details.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "description": "Create or update Linker resource.", + "operationId": "Linker_CreateOrUpdate", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PutLinker": { + "$ref": "./examples/PutLinker.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + }, + { + "name": "parameters", + "description": "Linker details.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LinkerResource" + } + } + ], + "responses": { + "200": { + "description": "Successful.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "201": { + "description": "Long running operation.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "delete": { + "tags": [ + "Linkers" + ], + "operationId": "Linker_Delete", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "description": "Delete a Linker.", + "x-ms-examples": { + "DeleteLinker": { + "$ref": "./examples/DeleteLinker.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + } + ], + "responses": { + "200": { + "description": "OK. The Linker is deleted." + }, + "202": { + "description": "Long running operation." + }, + "204": { + "description": "Deleted. The Linker is not found." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "patch": { + "tags": [ + "Linkers" + ], + "operationId": "Linker_Update", + "description": "Operation to update an existing Linker.", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PatchLinker": { + "$ref": "./examples/PatchLinker.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + }, + { + "name": "parameters", + "description": "Linker details.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LinkerPatch" + } + } + ], + "responses": { + "200": { + "description": "Success. The response describes a Linker.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "201": { + "description": "Long running operation.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers/{linkerName}/validateLinker": { + "post": { + "tags": [ + "Linkers" + ], + "operationId": "Linker_Validate", + "description": "Validate a Linker.", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "location" + }, + "x-ms-examples": { + "ValidateLinkerSuccess": { + "$ref": "./examples/ValidateLinkerSuccess.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ValidateOperationResult" + } + }, + "202": { + "description": "Accepted - Returns this status until the asynchronous operation has completed." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers/{linkerName}/listConfigurations": { + "post": { + "tags": [ + "Linkers" + ], + "operationId": "Linker_ListConfigurations", + "description": "list source configurations for a Linker.", + "x-ms-examples": { + "GetConfiguration": { + "$ref": "./examples/GetConfigurations.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ConfigurationResult" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/dryruns": { + "get": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_ListDryrun", + "description": "list dryrun jobs", + "x-ms-examples": { + "ListDryrun": { + "$ref": "./examples/ListDryrun.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunList" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/dryruns/{dryrunName}": { + "get": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_GetDryrun", + "description": "get a dryrun job", + "x-ms-examples": { + "GetDryrun": { + "$ref": "./examples/GetDryrun.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_CreateDryrun", + "description": "create a dryrun job to do necessary check before actual creation", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PutDryrun": { + "$ref": "./examples/PutDryrun.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + }, + { + "name": "parameters", + "description": "dryrun resource.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DryrunResource" + } + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "201": { + "description": "Long running operation", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "patch": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_UpdateDryrun", + "description": "add a dryrun job to do necessary check before actual creation", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PatchDryrun": { + "$ref": "./examples/PatchDryrun.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + }, + { + "name": "parameters", + "description": "dryrun resource.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DryrunPatch" + } + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "202": { + "description": "Accepted - Returns this status until the asynchronous operation has completed." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "delete": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_DeleteDryrun", + "description": "delete a dryrun job", + "x-ms-examples": { + "DeleteDryrun": { + "$ref": "./examples/DeleteDryrun.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + } + ], + "responses": { + "200": { + "description": "OK. The job is deleted." + }, + "204": { + "description": "Deleted. The job is not found." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers/{linkerName}/generateConfigurations": { + "post": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_GenerateConfigurations", + "description": "Generate configurations for a Linker.", + "x-ms-examples": { + "GenerateConfiguration": { + "$ref": "./examples/LinkerGenerateConfigurations.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + }, + { + "name": "parameters", + "description": "Connection Info, including format, secret store, etc", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/ConfigurationInfo" + } + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ConfigurationResult" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/providers/Microsoft.ServiceLinker/operations": { + "get": { + "tags": [ + "Operations" + ], + "operationId": "Operations_List", + "description": "Lists the available ServiceLinker REST API operations.", + "x-ms-examples": { + "GetConfiguration": { + "$ref": "./examples/OperationsList.json" + } + }, + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/OperationListResult" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/providers/Microsoft.ServiceLinker/configurationNames": { + "get": { + "tags": [ + "ConfigurationNames" + ], + "operationId": "ConfigurationNames_List", + "description": "Lists the configuration names generated by Service Connector for all target, client types, auth types.", + "x-ms-examples": { + "GetConfigurationNames": { + "$ref": "./examples/ConfigurationNamesList.json" + } + }, + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "$filter", + "in": "query", + "required": false, + "type": "string", + "description": "OData filter options." + }, + { + "name": "$skipToken", + "in": "query", + "required": false, + "type": "string", + "description": "OData skipToken option for pagination." + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ConfigurationNameResult" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/daprConfigurations": { + "get": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_ListDaprConfigurations", + "description": "List the dapr configuration supported by Service Connector.", + "x-ms-examples": { + "GetDaprConfigurations": { + "$ref": "./examples/GetDaprConfigurations.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DaprConfigurationList" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + } + }, + "definitions": { + "TargetServiceType": { + "description": "The target service type.", + "type": "string", + "enum": [ + "AzureResource", + "ConfluentBootstrapServer", + "ConfluentSchemaRegistry", + "SelfHostedServer" + ], + "x-ms-enum": { + "name": "targetServiceType", + "modelAsString": true + } + }, + "TargetServiceBase": { + "description": "The target service properties", + "discriminator": "type", + "type": "object", + "properties": { + "type": { + "description": "The target service type.", + "$ref": "#/definitions/TargetServiceType" + } + }, + "required": [ + "type" + ] + }, + "AzureResourceType": { + "description": "The azure resource type.", + "type": "string", + "enum": [ + "KeyVault" + ], + "x-ms-enum": { + "name": "azureResourceType", + "modelAsString": true + } + }, + "AzureResourcePropertiesBase": { + "description": "The azure resource properties", + "discriminator": "type", + "type": "object", + "properties": { + "type": { + "description": "The azure resource type.", + "$ref": "#/definitions/AzureResourceType" + } + }, + "required": [ + "type" + ] + }, + "AzureResource": { + "x-ms-discriminator-value": "AzureResource", + "type": "object", + "description": "The azure resource info when target service type is AzureResource", + "allOf": [ + { + "$ref": "#/definitions/TargetServiceBase" + } + ], + "properties": { + "id": { + "description": "The Id of azure resource.", + "type": "string" + }, + "resourceProperties": { + "x-nullable": true, + "description": "The azure resource connection related properties.", + "$ref": "#/definitions/AzureResourcePropertiesBase" + } + } + }, + "AzureKeyVaultProperties": { + "x-ms-discriminator-value": "KeyVault", + "type": "object", + "description": "The resource properties when type is Azure Key Vault", + "allOf": [ + { + "$ref": "#/definitions/AzureResourcePropertiesBase" + } + ], + "properties": { + "connectAsKubernetesCsiDriver": { + "x-nullable": true, + "description": "True if connect via Kubernetes CSI Driver.", + "type": "boolean" + } + } + }, + "ConfluentBootstrapServer": { + "x-ms-discriminator-value": "ConfluentBootstrapServer", + "type": "object", + "description": "The service properties when target service type is ConfluentBootstrapServer", + "allOf": [ + { + "$ref": "#/definitions/TargetServiceBase" + } + ], + "properties": { + "endpoint": { + "description": "The endpoint of service.", + "type": "string" + } + } + }, + "SelfHostedServer": { + "x-ms-discriminator-value": "SelfHostedServer", + "type": "object", + "description": "The service properties when target service type is SelfHostedServer", + "allOf": [ + { + "$ref": "#/definitions/TargetServiceBase" + } + ], + "properties": { + "endpoint": { + "description": "The endpoint of service.", + "type": "string" + } + } + }, + "ConfluentSchemaRegistry": { + "x-ms-discriminator-value": "ConfluentSchemaRegistry", + "type": "object", + "description": "The service properties when target service type is ConfluentSchemaRegistry", + "allOf": [ + { + "$ref": "#/definitions/TargetServiceBase" + } + ], + "properties": { + "endpoint": { + "description": "The endpoint of service.", + "type": "string" + } + } + }, + "DeleteOrUpdateBehavior": { + "description": "The cleanup behavior to indicate whether clean up operation when resource is deleted or updated", + "type": "string", + "enum": [ + "Default", + "ForcedCleanup" + ], + "x-ms-enum": { + "name": "DeleteOrUpdateBehavior", + "modelAsString": true + } + }, + "ClientType": { + "description": "The application client type", + "type": "string", + "enum": [ + "none", + "dotnet", + "java", + "python", + "go", + "php", + "ruby", + "django", + "nodejs", + "springBoot", + "kafka-springBoot", + "jms-springBoot", + "dapr" + ], + "x-ms-enum": { + "name": "clientType", + "modelAsString": true + } + }, + "AuthType": { + "description": "The authentication type.", + "type": "string", + "enum": [ + "systemAssignedIdentity", + "userAssignedIdentity", + "servicePrincipalSecret", + "servicePrincipalCertificate", + "secret", + "accessKey", + "userAccount", + "easyAuthMicrosoftEntraID" + ], + "x-ms-enum": { + "name": "AuthType", + "modelAsString": true + } + }, + "SecretType": { + "description": "The secret type.", + "type": "string", + "enum": [ + "rawValue", + "keyVaultSecretUri", + "keyVaultSecretReference" + ], + "x-ms-enum": { + "name": "SecretType", + "modelAsString": true + } + }, + "SecretSourceType": { + "description": "The type of secret source.", + "type": "string", + "enum": [ + "rawValue", + "keyVaultSecret" + ], + "x-ms-enum": { + "name": "SecretSourceType", + "modelAsString": true + } + }, + "SecretInfoBase": { + "description": "The secret info", + "discriminator": "secretType", + "type": "object", + "properties": { + "secretType": { + "description": "The secret type.", + "$ref": "#/definitions/SecretType" + } + }, + "required": [ + "secretType" + ] + }, + "ValueSecretInfo": { + "x-ms-discriminator-value": "rawValue", + "type": "object", + "description": "The secret info when type is rawValue. It's for scenarios that user input the secret.", + "allOf": [ + { + "$ref": "#/definitions/SecretInfoBase" + } + ], + "properties": { + "value": { + "x-nullable": true, + "description": "The actual value of the secret.", + "type": "string", + "x-ms-secret": true + } + } + }, + "KeyVaultSecretReferenceSecretInfo": { + "x-ms-discriminator-value": "keyVaultSecretReference", + "type": "object", + "description": "The secret info when type is keyVaultSecretReference. It's for scenario that user provides a secret stored in user's keyvault and source is Azure Kubernetes. The key Vault's resource id is linked to secretStore.keyVaultId.", + "allOf": [ + { + "$ref": "#/definitions/SecretInfoBase" + } + ], + "properties": { + "name": { + "description": "Name of the Key Vault secret.", + "type": "string" + }, + "version": { + "x-nullable": true, + "description": "Version of the Key Vault secret.", + "type": "string" + } + } + }, + "KeyVaultSecretUriSecretInfo": { + "x-ms-discriminator-value": "keyVaultSecretUri", + "type": "object", + "description": "The secret info when type is keyVaultSecretUri. It's for scenario that user provides a secret stored in user's keyvault and source is Web App, Spring Cloud or Container App.", + "allOf": [ + { + "$ref": "#/definitions/SecretInfoBase" + } + ], + "properties": { + "value": { + "description": "URI to the keyvault secret", + "type": "string" + } + } + }, + "AuthInfoBase": { + "description": "The authentication info", + "discriminator": "authType", + "type": "object", + "properties": { + "authType": { + "description": "The authentication type.", + "$ref": "#/definitions/AuthType" + }, + "authMode": { + "description": "Optional. Indicates how to configure authentication. If optInAllAuth, service linker configures authentication such as enabling identity on source resource and granting RBAC roles. If optOutAllAuth, opt out authentication setup. Default is optInAllAuth.", + "$ref": "#/definitions/AuthMode" + } + }, + "required": [ + "authType" + ] + }, + "AccessKeyInfoBase": { + "description": "The access key directly from target resource properties, which target service is Azure Resource, such as Microsoft.Storage", + "x-ms-discriminator-value": "accessKey", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + } + ], + "properties": { + "permissions": { + "description": "Permissions of the accessKey. `Read` and `Write` are for Azure Cosmos DB and Azure App Configuration, `Listen`, `Send` and `Manage` are for Azure Event Hub and Azure Service Bus.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "Read", + "Write", + "Listen", + "Send", + "Manage" + ], + "x-ms-enum": { + "name": "accessKeyPermissions", + "modelAsString": true + } + } + } + } + }, + "DatabaseAadAuthInfo": { + "description": "The extra auth info required by Database AAD authentication.", + "type": "object", + "properties": { + "userName": { + "x-nullable": true, + "description": "Username created in the database which is mapped to a user in AAD.", + "type": "string" + } + } + }, + "SecretAuthInfo": { + "x-ms-discriminator-value": "secret", + "type": "object", + "description": "The authentication info when authType is secret", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + } + ], + "properties": { + "name": { + "x-nullable": true, + "description": "Username or account name for secret auth.", + "type": "string" + }, + "secretInfo": { + "x-nullable": true, + "description": "Password or key vault secret for secret auth.", + "$ref": "#/definitions/SecretInfoBase" + } + } + }, + "UserAssignedIdentityAuthInfo": { + "x-ms-discriminator-value": "userAssignedIdentity", + "type": "object", + "description": "The authentication info when authType is userAssignedIdentity", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + }, + { + "$ref": "#/definitions/DatabaseAadAuthInfo" + } + ], + "properties": { + "clientId": { + "description": "Client Id for userAssignedIdentity.", + "type": "string" + }, + "subscriptionId": { + "description": "Subscription id for userAssignedIdentity.", + "type": "string" + }, + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional, this value specifies the Azure role to be assigned" + } + } + }, + "SystemAssignedIdentityAuthInfo": { + "x-ms-discriminator-value": "systemAssignedIdentity", + "type": "object", + "description": "The authentication info when authType is systemAssignedIdentity", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + }, + { + "$ref": "#/definitions/DatabaseAadAuthInfo" + } + ], + "properties": { + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional, this value specifies the Azure role to be assigned" + } + } + }, + "ServicePrincipalSecretAuthInfo": { + "x-ms-discriminator-value": "servicePrincipalSecret", + "type": "object", + "description": "The authentication info when authType is servicePrincipal secret", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + }, + { + "$ref": "#/definitions/DatabaseAadAuthInfo" + } + ], + "properties": { + "clientId": { + "description": "ServicePrincipal application clientId for servicePrincipal auth.", + "type": "string" + }, + "principalId": { + "description": "Principal Id for servicePrincipal auth.", + "type": "string" + }, + "secret": { + "description": "Secret for servicePrincipal auth.", + "type": "string", + "x-ms-secret": true + }, + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional, this value specifies the Azure roles to be assigned. Automatically " + } + }, + "required": [ + "clientId", + "principalId", + "secret" + ] + }, + "ServicePrincipalCertificateAuthInfo": { + "x-ms-discriminator-value": "servicePrincipalCertificate", + "type": "object", + "description": "The authentication info when authType is servicePrincipal certificate", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + } + ], + "properties": { + "clientId": { + "description": "Application clientId for servicePrincipal auth.", + "type": "string" + }, + "principalId": { + "description": "Principal Id for servicePrincipal auth.", + "type": "string" + }, + "certificate": { + "description": "ServicePrincipal certificate for servicePrincipal auth.", + "type": "string", + "x-ms-secret": true + }, + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional, this value specifies the Azure roles to be assigned. Automatically " + } + }, + "required": [ + "clientId", + "principalId", + "certificate" + ] + }, + "UserAccountAuthInfo": { + "x-ms-discriminator-value": "userAccount", + "type": "object", + "description": "The authentication info when authType is user account", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + }, + { + "$ref": "#/definitions/DatabaseAadAuthInfo" + } + ], + "properties": { + "principalId": { + "description": "Principal Id for user account.", + "type": "string" + }, + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional, this value specifies the Azure roles to be assigned. Automatically " + } + } + }, + "EasyAuthMicrosoftEntraIDAuthInfo": { + "x-ms-discriminator-value": "easyAuthMicrosoftEntraID", + "type": "object", + "description": "The authentication info when authType is EasyAuth Microsoft Entra ID", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + } + ], + "properties": { + "clientId": { + "description": "Application clientId for EasyAuth Microsoft Entra ID.", + "type": "string" + }, + "secret": { + "description": "Application Secret for EasyAuth Microsoft Entra ID.", + "type": "string", + "x-ms-secret": true + }, + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + } + } + }, + "LinkerResource": { + "type": "object", + "description": "Linker of source and target resource", + "allOf": [ + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ProxyResource", + "description": "The resource model definition for an Azure Resource Manager proxy resource." + } + ], + "required": [ + "properties" + ], + "properties": { + "properties": { + "description": "The properties of the Linker.", + "$ref": "#/definitions/LinkerProperties", + "x-ms-client-flatten": true + }, + "systemData": { + "x-nullable": true, + "readOnly": true, + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/systemData", + "description": "The system data." + } + } + }, + "LinkerPatch": { + "description": "A Linker to be updated.", + "type": "object", + "properties": { + "properties": { + "description": "Linker properties", + "type": "object", + "x-ms-client-flatten": true, + "$ref": "#/definitions/LinkerProperties" + } + } + }, + "ResourceList": { + "description": "The list of Linker.", + "type": "object", + "properties": { + "nextLink": { + "x-nullable": true, + "description": "The Linker used to get the next page of Linker list.", + "type": "string" + }, + "value": { + "description": "The list of Linkers.", + "type": "array", + "items": { + "$ref": "#/definitions/LinkerResource" + } + } + } + }, + "LinkerProperties": { + "description": "The properties of the Linker.", + "type": "object", + "properties": { + "targetService": { + "$ref": "#/definitions/TargetServiceBase", + "description": "The target service properties" + }, + "authInfo": { + "description": "The authentication type.", + "$ref": "#/definitions/AuthInfoBase" + }, + "clientType": { + "description": "The application client type", + "$ref": "#/definitions/ClientType" + }, + "provisioningState": { + "readOnly": true, + "type": "string", + "description": "The provisioning state. " + }, + "vNetSolution": { + "x-nullable": true, + "description": "The VNet solution.", + "$ref": "#/definitions/VNetSolution" + }, + "secretStore": { + "x-nullable": true, + "description": "An option to store secret value in secure place", + "$ref": "#/definitions/SecretStore" + }, + "scope": { + "x-nullable": true, + "type": "string", + "description": "connection scope in source service." + }, + "publicNetworkSolution": { + "x-nullable": true, + "description": "The network solution.", + "$ref": "#/definitions/PublicNetworkSolution" + }, + "configurationInfo": { + "x-nullable": true, + "description": "The connection information consumed by applications, including secrets, connection strings.", + "$ref": "#/definitions/ConfigurationInfo" + } + } + }, + "LinkerConfigurationType": { + "description": "Type of configuration to determine whether the configuration can be modified after creation. KeyvaultSecret means the configuration references a key vault secret, such as App Service/ACA key vault reference. Default means the configuration is real value, such as user name, raw secret, etc.", + "type": "string", + "enum": [ + "Default", + "KeyVaultSecret" + ], + "x-ms-enum": { + "name": "LinkerConfigurationType", + "modelAsString": true + } + }, + "SourceConfiguration": { + "description": "A configuration item for source resource", + "type": "object", + "properties": { + "name": { + "description": "The name of setting.", + "type": "string" + }, + "value": { + "x-nullable": true, + "description": "The value of setting", + "type": "string" + }, + "configType": { + "description": "The type of setting", + "readOnly": true, + "$ref": "#/definitions/LinkerConfigurationType" + }, + "keyVaultReferenceIdentity": { + "x-nullable": true, + "description": "The identity for key vault reference, system or user-assigned managed identity ID", + "type": "string" + }, + "description": { + "x-nullable": true, + "description": "Descriptive information for the configuration", + "type": "string" + } + } + }, + "ConfigurationNameItem": { + "type": "object", + "properties": { + "properties": { + "x-nullable": true, + "description": "The result detail.", + "x-ms-client-flatten": true, + "$ref": "#/definitions/ConfigurationNames" + } + } + }, + "ConfigurationNames": { + "type": "object", + "description": "The configuration names which will be set based on specific target resource, client type, auth type.", + "properties": { + "targetService": { + "type": "string", + "description": "The target service provider name and resource name." + }, + "clientType": { + "$ref": "#/definitions/ClientType", + "description": "The client type for configuration names." + }, + "authType": { + "$ref": "#/definitions/AuthType", + "description": "The auth type." + }, + "secretType": { + "$ref": "#/definitions/SecretSourceType", + "description": "Indicates where the secrets in configuration from. Used when secrets are from Keyvault." + }, + "daprProperties": { + "description": "Deprecated, please use #/definitions/DaprConfigurationList instead", + "$ref": "#/definitions/DaprProperties" + }, + "names": { + "type": "array", + "description": "The configuration names to be set in compute service environment.", + "items": { + "$ref": "#/definitions/ConfigurationName" + } + } + } + }, + "ConfigurationName": { + "type": "object", + "description": "The configuration names.", + "properties": { + "value": { + "type": "string" + }, + "description": { + "type": "string", + "description": "Description for the configuration name." + }, + "required": { + "type": "boolean", + "description": "Represent the configuration is required or not" + } + } + }, + "ConfigurationNameResult": { + "description": "Configuration Name list which will be set based on different target resource, client type, auth type.", + "type": "object", + "properties": { + "value": { + "description": "Expected configuration names for each target service.", + "type": "array", + "items": { + "$ref": "#/definitions/ConfigurationNameItem" + }, + "x-ms-identifiers": [] + }, + "nextLink": { + "description": "Link to next page of resources.", + "type": "string", + "readOnly": true + } + } + }, + "ConfigurationResult": { + "description": "Configurations for source resource, include appSettings, connectionString and serviceBindings", + "type": "object", + "properties": { + "configurations": { + "description": "The configuration properties for source resource.", + "type": "array", + "items": { + "$ref": "#/definitions/SourceConfiguration" + }, + "x-ms-identifiers": [ + "name" + ] + } + } + }, + "ValidateOperationResult": { + "description": "The validation operation result for a Linker.", + "type": "object", + "properties": { + "properties": { + "x-nullable": true, + "description": "The validation result detail.", + "x-ms-client-flatten": true, + "$ref": "#/definitions/ValidateResult" + }, + "resourceId": { + "x-nullable": true, + "description": "Validated Linker id.", + "type": "string" + }, + "status": { + "x-nullable": true, + "description": "Validation operation status.", + "type": "string" + } + } + }, + "ValidateResult": { + "description": "The validation result for a Linker.", + "type": "object", + "properties": { + "linkerName": { + "x-nullable": true, + "description": "The linker name.", + "type": "string" + }, + "isConnectionAvailable": { + "x-nullable": true, + "description": "A boolean value indicating whether the connection is available or not", + "type": "boolean" + }, + "reportStartTimeUtc": { + "x-nullable": true, + "type": "string", + "format": "date-time", + "description": "The start time of the validation report." + }, + "reportEndTimeUtc": { + "x-nullable": true, + "type": "string", + "format": "date-time", + "description": "The end time of the validation report." + }, + "sourceId": { + "x-nullable": true, + "description": "The resource id of the Linker source application.", + "type": "string" + }, + "targetId": { + "x-nullable": true, + "description": "The resource Id of target service.", + "type": "string" + }, + "authType": { + "x-nullable": true, + "description": "The authentication type.", + "$ref": "#/definitions/AuthType" + }, + "validationDetail": { + "description": "The detail of validation result", + "type": "array", + "items": { + "$ref": "#/definitions/ValidationResultItem" + }, + "x-ms-identifiers": [ + "name" + ] + } + } + }, + "ValidationResultItem": { + "description": "The validation item for a Linker.", + "type": "object", + "properties": { + "name": { + "description": "The validation item name.", + "type": "string" + }, + "description": { + "x-nullable": true, + "description": "The display name of validation item", + "type": "string" + }, + "result": { + "x-nullable": true, + "description": "The result of validation", + "type": "string", + "enum": [ + "success", + "failure", + "warning" + ], + "x-ms-enum": { + "name": "ValidationResultStatus", + "modelAsString": true + } + }, + "errorMessage": { + "x-nullable": true, + "description": "The error message of validation result", + "type": "string" + }, + "errorCode": { + "x-nullable": true, + "description": "The error code of validation result", + "type": "string" + } + } + }, + "VNetSolution": { + "type": "object", + "description": "The VNet solution for linker", + "properties": { + "type": { + "x-nullable": true, + "description": "Type of VNet solution.", + "type": "string", + "enum": [ + "serviceEndpoint", + "privateLink" + ], + "x-ms-enum": { + "name": "vNetSolutionType", + "modelAsString": true + } + }, + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + } + } + }, + "PublicNetworkSolution": { + "type": "object", + "description": "Indicates public network solution, include firewall rules", + "properties": { + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation(such as firewall rules) when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "action": { + "description": "Optional. Indicates public network solution. If enable, enable public network access of target service with best try. Default is enable. If optOut, opt out public network access configuration.", + "$ref": "#/definitions/ActionType" + }, + "firewallRules": { + "description": "Describe firewall rules of target service to make sure source application could connect to the target.", + "$ref": "#/definitions/FirewallRules" + } + } + }, + "FirewallRules": { + "type": "object", + "description": "Target service's firewall rules. to allow connections from source service.", + "properties": { + "ipRanges": { + "type": "array", + "items": { + "type": "string" + }, + "description": "This value specifies the set of IP addresses or IP address ranges in CIDR form to be included as the allowed list of client IPs for a given database account." + }, + "azureServices": { + "description": "Allow Azure services to access the target service if true.", + "$ref": "#/definitions/AllowType" + }, + "callerClientIP": { + "description": "Allow caller client IP to access the target service if true. the property is used when connecting local application to target service.", + "$ref": "#/definitions/AllowType" + } + } + }, + "ConfigurationInfo": { + "type": "object", + "description": "The configuration information, used to generate configurations or save to applications", + "properties": { + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "action": { + "description": "Optional, indicate whether to apply configurations on source application. If enable, generate configurations and applied to the source application. Default is enable. If optOut, no configuration change will be made on source.", + "$ref": "#/definitions/ActionType" + }, + "customizedKeys": { + "description": "Optional. A dictionary of default key name and customized key name mapping. If not specified, default key name will be used for generate configurations", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "daprProperties": { + "description": "Indicates some additional properties for dapr client type", + "$ref": "#/definitions/DaprProperties" + }, + "additionalConfigurations": { + "description": "A dictionary of additional configurations to be added. Service will auto generate a set of basic configurations and this property is to full fill more customized configurations", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "additionalConnectionStringProperties": { + "description": "A dictionary of additional properties to be added in the end of connection string.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "configurationStore": { + "x-nullable": true, + "description": "An option to store configuration into different place", + "$ref": "#/definitions/ConfigurationStore" + } + } + }, + "ConfigurationStore": { + "type": "object", + "description": "An option to store configuration into different place", + "properties": { + "appConfigurationId": { + "x-nullable": true, + "type": "string", + "description": "The app configuration id to store configuration" + } + } + }, + "DaprProperties": { + "type": "object", + "description": "Indicates some additional properties for dapr client type", + "properties": { + "version": { + "x-nullable": true, + "type": "string", + "description": "The dapr component version" + }, + "componentType": { + "x-nullable": true, + "type": "string", + "description": "The dapr component type" + }, + "secretStoreComponent": { + "x-nullable": true, + "type": "string", + "description": "The name of a secret store dapr to retrieve secret" + }, + "metadata": { + "description": "Additional dapr metadata", + "type": "array", + "items": { + "$ref": "#/definitions/DaprMetadata" + }, + "x-ms-identifiers": [ + "name" + ] + }, + "scopes": { + "description": "The dapr component scopes", + "type": "array", + "items": { + "type": "string" + } + }, + "runtimeVersion": { + "x-nullable": true, + "type": "string", + "readOnly": true, + "description": "The runtime version supported by the properties" + }, + "bindingComponentDirection": { + "x-nullable": true, + "type": "string", + "enum": [ + "input", + "output" + ], + "x-ms-enum": { + "name": "DaprBindingComponentDirection", + "modelAsString": true + }, + "readOnly": true, + "description": "The direction supported by the dapr binding component" + } + } + }, + "DaprMetadata": { + "description": "The dapr component metadata.", + "type": "object", + "properties": { + "name": { + "description": "Metadata property name.", + "type": "string" + }, + "value": { + "description": "Metadata property value.", + "type": "string" + }, + "secretRef": { + "description": "The secret name where dapr could get value", + "type": "string" + }, + "description": { + "description": "The description of the metadata, returned from configuration api", + "type": "string" + }, + "required": { + "description": "The value indicating whether the metadata is required or not", + "type": "string", + "enum": [ + "true", + "false" + ], + "x-ms-enum": { + "name": "DaprMetadataRequired", + "modelAsString": true + } + } + } + }, + "SecretStore": { + "type": "object", + "description": "An option to store secret value in secure place", + "properties": { + "keyVaultId": { + "x-nullable": true, + "type": "string", + "description": "The key vault id to store secret" + }, + "keyVaultSecretName": { + "x-nullable": true, + "type": "string", + "description": "The key vault secret name to store secret, only valid when storing one secret" + } + } + }, + "DryrunList": { + "description": "The list of dryrun.", + "type": "object", + "properties": { + "nextLink": { + "x-nullable": true, + "description": "The link used to get the next page of dryrun list.", + "type": "string" + }, + "value": { + "description": "The list of dryrun.", + "type": "array", + "items": { + "$ref": "#/definitions/DryrunResource" + } + } + } + }, + "DryrunResource": { + "type": "object", + "description": "a dryrun job resource", + "allOf": [ + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ProxyResource", + "description": "The resource model definition for an Azure Resource Manager proxy resource." + } + ], + "properties": { + "properties": { + "description": "The properties of the dryrun job.", + "$ref": "#/definitions/DryrunProperties", + "x-ms-client-flatten": true + } + } + }, + "DryrunPatch": { + "type": "object", + "description": "a dryrun job to be updated.", + "properties": { + "properties": { + "description": "The properties of the dryrun job.", + "$ref": "#/definitions/DryrunProperties", + "x-ms-client-flatten": true + } + } + }, + "DryrunProperties": { + "description": "The properties of the dryrun job", + "type": "object", + "properties": { + "parameters": { + "description": "The parameters of the dryrun", + "$ref": "#/definitions/DryrunParameters" + }, + "prerequisiteResults": { + "readOnly": true, + "description": "the result of the dryrun", + "type": "array", + "items": { + "$ref": "#/definitions/DryrunPrerequisiteResult" + }, + "x-ms-identifiers": [] + }, + "operationPreviews": { + "readOnly": true, + "description": "the preview of the operations for creation", + "type": "array", + "items": { + "$ref": "#/definitions/DryrunOperationPreview" + }, + "x-ms-identifiers": [] + }, + "provisioningState": { + "readOnly": true, + "type": "string", + "description": "The provisioning state. " + } + } + }, + "DryrunActionName": { + "description": "The name of action for you dryrun job.", + "type": "string", + "enum": [ + "createOrUpdate" + ], + "x-ms-enum": { + "name": "DryrunActionName", + "modelAsString": true + } + }, + "DryrunParameters": { + "description": "The parameters of the dryrun", + "discriminator": "actionName", + "type": "object", + "properties": { + "actionName": { + "$ref": "#/definitions/DryrunActionName" + } + }, + "required": [ + "actionName" + ] + }, + "CreateOrUpdateDryrunParameters": { + "x-ms-discriminator-value": "createOrUpdate", + "type": "object", + "description": "The dryrun parameters for creation or update a linker", + "allOf": [ + { + "$ref": "#/definitions/DryrunParameters" + }, + { + "$ref": "#/definitions/LinkerProperties" + } + ] + }, + "DryrunPrerequisiteResultType": { + "description": "The type of dryrun result.", + "type": "string", + "enum": [ + "basicError", + "permissionsMissing" + ], + "x-ms-enum": { + "name": "DryrunPrerequisiteResultType", + "modelAsString": true + } + }, + "DryrunPrerequisiteResult": { + "description": "A result of dryrun", + "discriminator": "type", + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/DryrunPrerequisiteResultType" + } + }, + "required": [ + "type" + ] + }, + "BasicErrorDryrunPrerequisiteResult": { + "x-ms-discriminator-value": "basicError", + "description": "The represent of basic error", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/DryrunPrerequisiteResult" + } + ], + "properties": { + "code": { + "type": "string", + "description": "The error code." + }, + "message": { + "type": "string", + "description": "The error message." + } + } + }, + "PermissionsMissingDryrunPrerequisiteResult": { + "x-ms-discriminator-value": "permissionsMissing", + "description": "The represent of missing permissions", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/DryrunPrerequisiteResult" + } + ], + "properties": { + "scope": { + "description": "The permission scope", + "type": "string" + }, + "permissions": { + "description": "The permission list", + "type": "array", + "items": { + "type": "string" + } + }, + "recommendedRole": { + "description": "The recommended role to resolve permissions missing", + "type": "string" + } + } + }, + "DryrunOperationPreview": { + "description": "The preview of the operations for creation", + "type": "object", + "properties": { + "name": { + "description": "The operation name", + "type": "string" + }, + "operationType": { + "description": "The operation type", + "type": "string", + "enum": [ + "configConnection", + "configNetwork", + "configAuth" + ], + "x-ms-enum": { + "name": "DryrunPreviewOperationType", + "modelAsString": true + } + }, + "description": { + "description": "The description of the operation", + "type": "string" + }, + "action": { + "description": "The action defined by RBAC, refer https://docs.microsoft.com/azure/role-based-access-control/role-definitions#actions-format", + "type": "string" + }, + "scope": { + "description": "The scope of the operation, refer https://docs.microsoft.com/azure/role-based-access-control/scope-overview", + "type": "string" + } + } + }, + "ActionType": { + "description": "Indicates how to apply the connector operations, such as opt out network configuration, opt in configuration.", + "type": "string", + "enum": [ + "enable", + "optOut" + ], + "x-ms-enum": { + "name": "actionType", + "modelAsString": true + } + }, + "AuthMode": { + "description": "Indicates how to apply the authentication configuration operations.", + "type": "string", + "enum": [ + "optInAllAuth", + "optOutAllAuth" + ], + "x-ms-enum": { + "name": "authMode", + "modelAsString": true, + "values": [ + { + "value": "optInAllAuth", + "description": "Default authentication configuration according to the authentication type." + }, + { + "value": "optOutAllAuth", + "description": "Skip all authentication configuration such as enabling managed identity and granting RBAC roles" + } + ] + } + }, + "AllowType": { + "description": "Whether to allow firewall rules.", + "type": "string", + "enum": [ + "true", + "false" + ], + "x-ms-enum": { + "name": "allowType", + "modelAsString": true + } + }, + "DaprConfigurationList": { + "description": "Dapr configuration list supported by Service Connector", + "type": "object", + "properties": { + "value": { + "description": "The list of dapr configurations", + "type": "array", + "items": { + "$ref": "#/definitions/DaprConfigurationResource" + }, + "x-ms-identifiers": [] + }, + "nextLink": { + "description": "Link to next page of resources.", + "type": "string", + "readOnly": true + } + } + }, + "DaprConfigurationResource": { + "description": "Represent one resource of the dapr configuration list", + "type": "object", + "properties": { + "properties": { + "description": "The properties of the dapr configuration.", + "$ref": "#/definitions/DaprConfigurationProperties", + "x-ms-client-flatten": true + } + } + }, + "DaprConfigurationProperties": { + "type": "object", + "properties": { + "targetType": { + "type": "string", + "description": "Supported target resource type, extract from resource id, uppercase" + }, + "authType": { + "$ref": "#/definitions/AuthType" + }, + "daprProperties": { + "$ref": "#/definitions/DaprProperties" + } + } + } + }, + "parameters": { + "LinkerNameParameter": { + "name": "linkerName", + "in": "path", + "required": true, + "type": "string", + "description": "The name Linker resource.", + "x-ms-parameter-location": "method" + }, + "ConnectorNameParameter": { + "name": "connectorName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of resource.", + "x-ms-parameter-location": "method" + }, + "ResourceUriParameter": { + "name": "resourceUri", + "in": "path", + "required": true, + "type": "string", + "description": "The fully qualified Azure Resource manager identifier of the resource to be connected.", + "x-ms-skip-url-encoding": true, + "x-ms-parameter-location": "method" + }, + "SubscriptionIdParameter": { + "name": "subscriptionId", + "in": "path", + "required": true, + "type": "string", + "description": "The ID of the target subscription.", + "minLength": 1, + "x-ms-parameter-location": "method" + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConfigurationNamesList.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConfigurationNamesList.json new file mode 100644 index 000000000000..57c406a4b875 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConfigurationNamesList.json @@ -0,0 +1,52 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "properties": { + "targetService": "MICROSOFT.APPCONFIGURATION/CONFIGURATIONSTORES", + "clientType": "none", + "authType": "systemAssignedIdentity", + "names": [ + { + "value": "AZURE_APPCONFIGURATION_ENDPOINT", + "description": "App configuration endpoint" + }, + { + "value": "AZURE_APPCONFIGURATION_SCOPE", + "description": "The scopes required for the token." + } + ] + } + }, + { + "properties": { + "targetService": "MICROSOFT.APPCONFIGURATION/CONFIGURATIONSTORES", + "clientType": "none", + "authType": "userAssignedIdentity", + "names": [ + { + "value": "AZURE_APPCONFIGURATION_ENDPOINT", + "description": "App configuration endpoint" + }, + { + "value": "AZURE_APPCONFIGURATION_CLIENTID", + "description": "The client(application) ID of the user identity." + }, + { + "value": "AZURE_APPCONFIGURATION_SCOPE", + "description": "The scopes required for getting token." + } + ] + } + } + ], + "nextLink": null + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConnectorDryrunCreate.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConnectorDryrunCreate.json new file mode 100644 index 000000000000..d4103f9d88e4 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConnectorDryrunCreate.json @@ -0,0 +1,116 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "dryrunName": "dryrunName", + "parameters": { + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name", + "secretInfo": { + "secretType": "rawValue", + "value": "secret" + } + } + } + } + } + }, + "responses": { + "200": { + "body": { + "name": "dryrunName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/locations/westus/dryruns/dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name" + } + }, + "prerequisiteResults": [ + { + "type": "basicError", + "code": "ResourceNotFound", + "message": "Target resource is not found" + }, + { + "type": "permissionsMissing", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc", + "permissions": [ + "Microsoft.DocumentDb/databaseAccounts/write" + ] + } + ], + "operationPreviews": [ + { + "name": "configFirewallRule", + "operationType": "configNetwork", + "description": "Config firewall rule for target service to allow source service access", + "action": "Microsoft.DocumentDb/databaseAccounts/write", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc" + } + ], + "provisioningState": "Succeeded" + } + } + }, + "201": { + "body": { + "name": "dryrunName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/locations/westus/dryruns/dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name" + } + }, + "prerequisiteResults": [ + { + "type": "basicError", + "code": "ResourceNotFound", + "message": "Target resource is not found" + }, + { + "type": "permissionsMissing", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc", + "permissions": [ + "Microsoft.DocumentDb/databaseAccounts/write" + ] + } + ], + "operationPreviews": [ + { + "name": "configFirewallRule", + "operationType": "configNetwork", + "description": "Config firewall rule for target service to allow source service access", + "action": "Microsoft.DocumentDb/databaseAccounts/write", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc" + } + ], + "provisioningState": "Accepted" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConnectorDryrunDelete.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConnectorDryrunDelete.json new file mode 100644 index 000000000000..d1cd053dfdf2 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConnectorDryrunDelete.json @@ -0,0 +1,13 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "dryrunName": "dryrunName" + }, + "responses": { + "200": {}, + "204": {} + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConnectorDryrunGet.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConnectorDryrunGet.json new file mode 100644 index 000000000000..e9fab0f0d0fb --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConnectorDryrunGet.json @@ -0,0 +1,33 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "dryrunName": "dryrunName" + }, + "responses": { + "200": { + "body": { + "name": "dryrunName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/locations/westus/dryruns/dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConnectorDryrunList.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConnectorDryrunList.json new file mode 100644 index 000000000000..b0d6cb39937d --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConnectorDryrunList.json @@ -0,0 +1,36 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "name": "dryrunName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/locations/westus/dryruns/dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConnectorDryrunUpdate.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConnectorDryrunUpdate.json new file mode 100644 index 000000000000..3f87be091230 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConnectorDryrunUpdate.json @@ -0,0 +1,78 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "dryrunName": "dryrunName", + "parameters": { + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name", + "secretInfo": { + "secretType": "rawValue", + "value": "secret" + } + } + } + } + } + }, + "responses": { + "200": { + "body": { + "name": "dryrunName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/locations/westus/dryruns/dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name" + } + }, + "prerequisiteResults": [ + { + "type": "basicError", + "code": "ResourceNotFound", + "message": "Target resource is not found" + }, + { + "type": "permissionsMissing", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc", + "permissions": [ + "Microsoft.DocumentDb/databaseAccounts/write" + ] + } + ], + "operationPreviews": [ + { + "name": "configFirewallRule", + "operationType": "configNetwork", + "description": "Config firewall rule for target service to allow source service access", + "action": "Microsoft.DocumentDb/databaseAccounts/write", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc" + } + ], + "provisioningState": "Succeeded" + } + } + }, + "202": { + "headers": { + "azure-AsyncOperation": "http://azure.async.operation/status" + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConnectorList.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConnectorList.json new file mode 100644 index 000000000000..ac02963e6953 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ConnectorList.json @@ -0,0 +1,34 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/linkers/linkName", + "name": "linkName", + "type": "Microsoft.ServiceLinker/devConnectors", + "properties": { + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/Connectors.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/Connectors.json new file mode 100644 index 000000000000..b774d9142b09 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/Connectors.json @@ -0,0 +1,43 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "connectorName": "connectorName" + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/devConnnectors/linkName", + "name": "linkName", + "type": "Microsoft.ServiceLinker/devConnectors", + "properties": { + "authInfo": { + "authType": "systemAssignedIdentity", + "roles": [ + "customizedOwner" + ] + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "publicNetworkSolution": { + "firewallRules": { + "ipRanges": [ + "182.22.120" + ], + "callerClientIP": "true" + }, + "action": "enable", + "deleteOrUpdateBehavior": "ForcedCleanup" + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/DeleteConnector.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/DeleteConnector.json new file mode 100644 index 000000000000..1dc02aed0953 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/DeleteConnector.json @@ -0,0 +1,18 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "connectorName": "connectorName" + }, + "responses": { + "200": {}, + "204": {}, + "202": { + "headers": { + "azure-AsyncOperation": "http://azure.async.operation/status" + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/DeleteDryrun.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/DeleteDryrun.json new file mode 100644 index 000000000000..b6d84517cba6 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/DeleteDryrun.json @@ -0,0 +1,11 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "dryrunName": "dryrunName" + }, + "responses": { + "200": {}, + "204": {} + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/DeleteLinker.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/DeleteLinker.json new file mode 100644 index 000000000000..a2eb35ce6361 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/DeleteLinker.json @@ -0,0 +1,16 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName" + }, + "responses": { + "200": {}, + "204": {}, + "202": { + "headers": { + "azure-AsyncOperation": "http://azure.async.operation/status" + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/GenerateConfigurations.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/GenerateConfigurations.json new file mode 100644 index 000000000000..b51f80725ab6 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/GenerateConfigurations.json @@ -0,0 +1,26 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "connectorName": "connectorName", + "parameters": { + "customizedKeys": { + "ASL_DocumentDb_ConnectionString": "MyConnectionstring" + } + } + }, + "responses": { + "200": { + "body": { + "configurations": [ + { + "name": "MyConnectionstring", + "value": "ConnectionString" + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/GetConfigurations.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/GetConfigurations.json new file mode 100644 index 000000000000..39d787a2211d --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/GetConfigurations.json @@ -0,0 +1,41 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.App/containerApps/test-app", + "linkerName": "linkName" + }, + "responses": { + "200": { + "body": { + "configurations": [ + { + "name": "AZURE_POSTGRESQL_HOST", + "value": "Host", + "configType": "Default" + }, + { + "name": "AZURE_POSTGRESQL_USER", + "value": "Username", + "configType": "Default" + }, + { + "name": "AZURE_POSTGRESQL_DATABASE", + "value": "DatabaseName", + "configType": "Default" + }, + { + "name": "AZURE_POSTGRESQL_PORT", + "value": "Port", + "configType": "Default" + }, + { + "name": "AZURE_POSTGRESQL_PASSWORD", + "value": "SecretUri", + "configType": "KeyVaultSecret", + "keyVaultReferenceIdentity": "system" + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/GetDaprConfigurations.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/GetDaprConfigurations.json new file mode 100644 index 000000000000..cdbc03e325fc --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/GetDaprConfigurations.json @@ -0,0 +1,33 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "properties": { + "targetType": "MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES", + "authType": "secret", + "daprProperties": { + "version": "v1", + "componentType": "bindings", + "runtimeVersion": "1.10", + "bindingComponentDirection": "input", + "metadata": [ + { + "name": "containerName", + "description": "The name of the container to be used for Dapr state.", + "required": "true" + } + ] + } + } + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/GetDryrun.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/GetDryrun.json new file mode 100644 index 000000000000..6545c44b1163 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/GetDryrun.json @@ -0,0 +1,32 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "dryrunName": "dryrunName" + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/dryruns/dryrunName", + "name": "dryrunName", + "type": "Microsoft.ServiceLinker/dryruns", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/Linker.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/Linker.json new file mode 100644 index 000000000000..eabc04aa76e7 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/Linker.json @@ -0,0 +1,47 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName" + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "name": "linkName", + "type": "Microsoft.ServiceLinker/links", + "properties": { + "authInfo": { + "authType": "secret", + "name": "name" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "secretStore": { + "keyVaultId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/kvname" + }, + "scope": "AKS-Namespace", + "clientType": "dotnet", + "publicNetworkSolution": { + "action": "enable" + }, + "configurationInfo": { + "deleteOrUpdateBehavior": "ForcedCleanup", + "customizedKeys": { + "AZURE_MYSQL_CONNECTIONSTRING": "myConnectionstring", + "AZURE_MYSQL_SSLMODE": "mySslmode" + }, + "additionalConfigurations": { + "throttlingLimit": "100" + } + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/LinkerGenerateConfigurations.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/LinkerGenerateConfigurations.json new file mode 100644 index 000000000000..20077dc3ba0b --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/LinkerGenerateConfigurations.json @@ -0,0 +1,24 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName", + "parameters": { + "customizedKeys": { + "ASL_DocumentDb_ConnectionString": "MyConnectionstring" + } + } + }, + "responses": { + "200": { + "body": { + "configurations": [ + { + "name": "MyConnectionstring", + "value": "ConnectionString" + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/LinkerList.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/LinkerList.json new file mode 100644 index 000000000000..99a8f52427c4 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/LinkerList.json @@ -0,0 +1,32 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/links/linkName", + "name": "linkName", + "type": "Microsoft.ServiceLinker/links", + "properties": { + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ListDryrun.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ListDryrun.json new file mode 100644 index 000000000000..4a8dde8c63f0 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ListDryrun.json @@ -0,0 +1,35 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/dryruns/dryrunName", + "name": "dryrunName", + "type": "Microsoft.ServiceLinker/dryruns", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/OperationsList.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/OperationsList.json new file mode 100644 index 000000000000..905bfe616da0 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/OperationsList.json @@ -0,0 +1,184 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "display": { + "description": "Register the subscription for Microsoft.ServiceLinker", + "operation": "Register the Microsoft.ServiceLinker", + "provider": "Microsoft.ServiceLinker", + "resource": "Microsoft.ServiceLinker" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/register/action" + }, + { + "display": { + "description": "Unregister the subscription for Microsoft.ServiceLinker", + "operation": "Unregister the Microsoft.ServiceLinker", + "provider": "Microsoft.ServiceLinker", + "resource": "Microsoft.ServiceLinker" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/unregister/action" + }, + { + "display": { + "description": "read operations", + "operation": "read_operations", + "provider": "Microsoft.ServiceLinker", + "resource": "operations" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/operations/read" + }, + { + "display": { + "description": "list dryrun jobs", + "operation": "Dryrun_List", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/read" + }, + { + "display": { + "description": "get a dryrun job", + "operation": "Dryrun_Get", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/read" + }, + { + "display": { + "description": "create a dryrun job to do necessary check before actual creation", + "operation": "Dryrun_Create", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/write" + }, + { + "display": { + "description": "delete a dryrun job", + "operation": "Dryrun_Delete", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/delete" + }, + { + "display": { + "description": "add a dryrun job to do necessary check before actual creation", + "operation": "Dryrun_Update", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/write" + }, + { + "display": { + "description": "read operationStatuses", + "operation": "read_operationStatuses", + "provider": "Microsoft.ServiceLinker", + "resource": "locations/operationStatuses" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/locations/operationStatuses/read" + }, + { + "display": { + "description": "write operationStatuses", + "operation": "write_operationStatuses", + "provider": "Microsoft.ServiceLinker", + "resource": "locations/operationStatuses" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/locations/operationStatuses/write" + }, + { + "display": { + "description": "Returns list of Linkers which connects to the resource.", + "operation": "Linker_List", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/read" + }, + { + "display": { + "description": "Returns Linker resource for a given name.", + "operation": "Linker_Get", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/read" + }, + { + "display": { + "description": "Create or update linker resource.", + "operation": "Linker_CreateOrUpdate", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/write" + }, + { + "display": { + "description": "Delete a link.", + "operation": "Linker_Delete", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/delete" + }, + { + "display": { + "description": "Operation to update an existing link.", + "operation": "Linker_Update", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/write" + }, + { + "display": { + "description": "Validate a link.", + "operation": "Linker_Validate", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/validateLinker/action" + }, + { + "display": { + "description": "list source configurations for a linker.", + "operation": "Linker_ListConfigurations", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/listConfigurations/action" + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/PatchConnector.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/PatchConnector.json new file mode 100644 index 000000000000..a302ac667d38 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/PatchConnector.json @@ -0,0 +1,64 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "connectorName": "connectorName", + "parameters": { + "properties": { + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id", + "secret": "secret" + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + }, + "202": { + "headers": { + "azure-asyncoperation": "http://azure.async.operation/status" + }, + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/PatchDryrun.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/PatchDryrun.json new file mode 100644 index 000000000000..c7da10d3eac5 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/PatchDryrun.json @@ -0,0 +1,77 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "dryrunName": "dryrunName", + "parameters": { + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name", + "secretInfo": { + "secretType": "rawValue", + "value": "secret" + } + } + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/dryruns/dryrunName", + "type": "Microsoft.ServiceLinker/dryruns", + "name": "dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name" + } + }, + "prerequisiteResults": [ + { + "type": "basicError", + "code": "ResourceNotFound", + "message": "Target resource is not found" + }, + { + "type": "permissionsMissing", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc", + "permissions": [ + "Microsoft.DocumentDb/databaseAccounts/write" + ] + } + ], + "operationPreviews": [ + { + "name": "configFirewallRule", + "operationType": "configNetwork", + "description": "Config firewall rule for target service to allow source service access", + "action": "Microsoft.DocumentDb/databaseAccounts/write", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc" + } + ], + "provisioningState": "Succeeded" + } + } + }, + "202": { + "headers": { + "azure-AsyncOperation": "http://azure.async.operation/status" + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/PatchLinker.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/PatchLinker.json new file mode 100644 index 000000000000..6a85c703e63b --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/PatchLinker.json @@ -0,0 +1,59 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName", + "parameters": { + "properties": { + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id", + "secret": "secret" + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + }, + "201": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/PutConnector.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/PutConnector.json new file mode 100644 index 000000000000..b85bfd26ae2e --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/PutConnector.json @@ -0,0 +1,63 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "connectorName": "connectorName", + "parameters": { + "properties": { + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret" + }, + "secretStore": { + "keyVaultId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-kv" + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "secret" + }, + "secretStore": { + "keyVaultId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-kv" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + }, + "201": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "secret" + }, + "secretStore": { + "keyVaultId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-kv" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/PutDryrun.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/PutDryrun.json new file mode 100644 index 000000000000..65e6713bb9c6 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/PutDryrun.json @@ -0,0 +1,116 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "dryrunName": "dryrunName", + "parameters": { + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name", + "secretInfo": { + "secretType": "rawValue", + "value": "secret" + } + } + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/dryruns/dryrunName", + "type": "Microsoft.ServiceLinker/dryruns", + "name": "dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name" + } + }, + "prerequisiteResults": [ + { + "type": "basicError", + "code": "ResourceNotFound", + "message": "Target resource is not found" + }, + { + "type": "permissionsMissing", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc", + "permissions": [ + "Microsoft.DocumentDb/databaseAccounts/write" + ] + } + ], + "operationPreviews": [ + { + "name": "configFirewallRule", + "operationType": "configNetwork", + "description": "Config firewall rule for target service to allow source service access", + "action": "Microsoft.DocumentDb/databaseAccounts/write", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc" + } + ], + "provisioningState": "Succeeded" + } + } + }, + "201": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/dryruns/dryrunName", + "type": "Microsoft.ServiceLinker/dryruns", + "name": "dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name" + } + }, + "prerequisiteResults": [ + { + "type": "basicError", + "code": "ResourceNotFound", + "message": "Target resource is not found" + }, + { + "type": "permissionsMissing", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc", + "permissions": [ + "Microsoft.DocumentDb/databaseAccounts/write" + ] + } + ], + "operationPreviews": [ + { + "name": "configFirewallRule", + "operationType": "configNetwork", + "description": "Config firewall rule for target service to allow source service access", + "action": "Microsoft.DocumentDb/databaseAccounts/write", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc" + } + ], + "provisioningState": "Updating" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/PutLinker.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/PutLinker.json new file mode 100644 index 000000000000..f678acb875be --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/PutLinker.json @@ -0,0 +1,68 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName", + "parameters": { + "properties": { + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/servers/test-pg/databases/test-db" + }, + "vNetSolution": { + "type": "serviceEndpoint" + }, + "authInfo": { + "authType": "secret", + "name": "name", + "secretInfo": { + "secretType": "rawValue", + "value": "secret" + } + } + } + } + }, + "responses": { + "200": { + "body": { + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "properties": { + "authInfo": { + "authType": "secret", + "name": "name" + }, + "vNetSolution": { + "type": "serviceEndpoint" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/servers/test-pg/databases/test-db" + } + } + } + }, + "201": { + "body": { + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "properties": { + "authInfo": { + "authType": "secret", + "name": "name" + }, + "vNetSolution": { + "type": "serviceEndpoint" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/servers/test-pg/databases/test-db" + } + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ValidateConnectorSuccess.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ValidateConnectorSuccess.json new file mode 100644 index 000000000000..a2e570267b75 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ValidateConnectorSuccess.json @@ -0,0 +1,38 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "connectorName": "connectorName" + }, + "responses": { + "200": { + "body": { + "properties": { + "isConnectionAvailable": true, + "reportStartTimeUtc": "2020-07-12T22:05:09Z", + "reportEndTimeUtc": "2020-07-12T22:06:09Z", + "authType": "secret", + "validationDetail": [ + { + "name": "TargetExistence", + "description": "The target existence is validated", + "result": "success" + }, + { + "name": "TargetNetworkAccess", + "description": "Deny public network access is set to yes. Please confirm you are using private endpoint connection to access target resource.", + "result": "warning" + } + ] + } + } + }, + "202": { + "headers": { + "azure-AsyncOperation": "http://azure.async.operation/status" + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ValidateLinkerSuccess.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ValidateLinkerSuccess.json new file mode 100644 index 000000000000..3bb0f31ce67f --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/examples/ValidateLinkerSuccess.json @@ -0,0 +1,38 @@ +{ + "parameters": { + "api-version": "2024-07-01-preview", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName" + }, + "responses": { + "200": { + "body": { + "properties": { + "isConnectionAvailable": true, + "reportStartTimeUtc": "2020-07-12T22:05:09Z", + "reportEndTimeUtc": "2020-07-12T22:06:09Z", + "sourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db", + "targetId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db", + "authType": "secret", + "validationDetail": [ + { + "name": "TargetExistence", + "description": "The target existence is validated", + "result": "success" + }, + { + "name": "TargetNetworkAccess", + "description": "Deny public network access is set to yes. Please confirm you are using private endpoint connection to access target resource.", + "result": "warning" + } + ] + } + } + }, + "202": { + "headers": { + "azure-AsyncOperation": "http://azure.async.operation/status" + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/servicelinker.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/servicelinker.json new file mode 100644 index 000000000000..3be7908389ae --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/preview/2024-07-01-preview/servicelinker.json @@ -0,0 +1,2937 @@ +{ + "swagger": "2.0", + "info": { + "title": "Microsoft.ServiceLinker", + "description": "Microsoft.ServiceLinker provider", + "version": "2024-07-01-preview" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "security": [ + { + "azure_auth": [ + "user_impersonation" + ] + } + ], + "securityDefinitions": { + "azure_auth": { + "type": "oauth2", + "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize", + "flow": "implicit", + "description": "Azure Active Directory OAuth2 Flow.", + "scopes": { + "user_impersonation": "impersonate your user account" + } + } + }, + "paths": { + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/dryruns": { + "get": { + "tags": [ + "Connector" + ], + "operationId": "Connector_ListDryrun", + "description": "list dryrun jobs", + "x-ms-examples": { + "ConnectorDryrunList": { + "$ref": "./examples/ConnectorDryrunList.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunList" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/dryruns/{dryrunName}": { + "get": { + "tags": [ + "Connector" + ], + "operationId": "Connector_GetDryrun", + "description": "get a dryrun job", + "x-ms-examples": { + "ConnectorDryrunGet": { + "$ref": "./examples/ConnectorDryrunGet.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "tags": [ + "Connector" + ], + "operationId": "Connector_CreateDryrun", + "description": "create a dryrun job to do necessary check before actual creation", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "ConnectorDryrunCreate": { + "$ref": "./examples/ConnectorDryrunCreate.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + }, + { + "name": "parameters", + "description": "dryrun resource.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DryrunResource" + } + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "201": { + "description": "Long running operation", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "patch": { + "tags": [ + "Connector" + ], + "operationId": "Connector_UpdateDryrun", + "description": "update a dryrun job to do necessary check before actual creation", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "ConnectorDryrunUpdate": { + "$ref": "./examples/ConnectorDryrunUpdate.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + }, + { + "name": "parameters", + "description": "dryrun resource.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DryrunPatch" + } + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "202": { + "description": "Accepted - Returns this status until the asynchronous operation has completed." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "delete": { + "tags": [ + "Connector" + ], + "operationId": "Connector_DeleteDryrun", + "description": "delete a dryrun job", + "x-ms-examples": { + "ConnectorDryrunDelete": { + "$ref": "./examples/ConnectorDryrunDelete.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + } + ], + "responses": { + "200": { + "description": "OK. The job is deleted." + }, + "204": { + "description": "Deleted. The job is not found." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/connectors": { + "get": { + "deprecated": false, + "description": "Returns list of connector which connects to the resource, which supports to config the target service during the resource provision.", + "operationId": "Connector_List", + "x-ms-examples": { + "ConnectorList": { + "$ref": "./examples/ConnectorList.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "Connector details.", + "schema": { + "$ref": "#/definitions/ResourceList" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/connectors/{connectorName}": { + "get": { + "description": "Returns Connector resource for a given name.", + "operationId": "Connector_Get", + "x-ms-examples": { + "Connector": { + "$ref": "./examples/Connectors.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "#/parameters/ConnectorNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "Connector details.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "description": "Create or update Connector resource.", + "operationId": "Connector_CreateOrUpdate", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PutConnector": { + "$ref": "./examples/PutConnector.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "#/parameters/ConnectorNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "parameters", + "description": "Connector details.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LinkerResource" + } + } + ], + "responses": { + "200": { + "description": "Successful.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "201": { + "description": "Long running operation.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "delete": { + "tags": [ + "Connector" + ], + "operationId": "Connector_Delete", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "description": "Delete a Connector.", + "x-ms-examples": { + "DeleteConnector": { + "$ref": "./examples/DeleteConnector.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "#/parameters/ConnectorNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The Connector is deleted." + }, + "202": { + "description": "Long running operation." + }, + "204": { + "description": "Deleted. The Connector is not found." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "patch": { + "tags": [ + "Connector" + ], + "operationId": "Connector_Update", + "description": "Operation to update an existing Connector.", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PatchConnector": { + "$ref": "./examples/PatchConnector.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "#/parameters/ConnectorNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "parameters", + "description": "Connector details.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LinkerPatch" + } + } + ], + "responses": { + "200": { + "description": "Success. The response describes a Connector.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "202": { + "description": "Long running operation.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/connectors/{connectorName}/validate": { + "post": { + "tags": [ + "Connector" + ], + "operationId": "Connector_Validate", + "description": "Validate a Connector.", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "location" + }, + "x-ms-examples": { + "ValidateConnectorSuccess": { + "$ref": "./examples/ValidateConnectorSuccess.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "#/parameters/ConnectorNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ValidateOperationResult" + } + }, + "202": { + "description": "Accepted - Returns this status until the asynchronous operation has completed." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/connectors/{connectorName}/generateConfigurations": { + "post": { + "tags": [ + "Connector" + ], + "operationId": "Connector_GenerateConfigurations", + "description": "Generate configurations for a Connector.", + "x-ms-examples": { + "GenerateConfiguration": { + "$ref": "./examples/GenerateConfigurations.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "#/parameters/ConnectorNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "parameters", + "description": "Connection Info, including format, secret store, etc", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/ConfigurationInfo" + } + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ConfigurationResult" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers": { + "get": { + "deprecated": false, + "description": "Returns list of Linkers which connects to the resource. which supports to config both application and target service during the resource provision.", + "operationId": "Linker_List", + "x-ms-examples": { + "LinkerList": { + "$ref": "./examples/LinkerList.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "Linker details.", + "schema": { + "$ref": "#/definitions/ResourceList" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers/{linkerName}": { + "get": { + "description": "Returns Linker resource for a given name.", + "operationId": "Linker_Get", + "x-ms-examples": { + "Linker": { + "$ref": "./examples/Linker.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + } + ], + "responses": { + "200": { + "description": "Linker details.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "description": "Create or update Linker resource.", + "operationId": "Linker_CreateOrUpdate", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PutLinker": { + "$ref": "./examples/PutLinker.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + }, + { + "name": "parameters", + "description": "Linker details.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LinkerResource" + } + } + ], + "responses": { + "200": { + "description": "Successful.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "201": { + "description": "Long running operation.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "delete": { + "tags": [ + "Linkers" + ], + "operationId": "Linker_Delete", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "description": "Delete a Linker.", + "x-ms-examples": { + "DeleteLinker": { + "$ref": "./examples/DeleteLinker.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + } + ], + "responses": { + "200": { + "description": "OK. The Linker is deleted." + }, + "202": { + "description": "Long running operation." + }, + "204": { + "description": "Deleted. The Linker is not found." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "patch": { + "tags": [ + "Linkers" + ], + "operationId": "Linker_Update", + "description": "Operation to update an existing Linker.", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PatchLinker": { + "$ref": "./examples/PatchLinker.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + }, + { + "name": "parameters", + "description": "Linker details.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LinkerPatch" + } + } + ], + "responses": { + "200": { + "description": "Success. The response describes a Linker.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "201": { + "description": "Long running operation.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers/{linkerName}/validateLinker": { + "post": { + "tags": [ + "Linkers" + ], + "operationId": "Linker_Validate", + "description": "Validate a Linker.", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "location" + }, + "x-ms-examples": { + "ValidateLinkerSuccess": { + "$ref": "./examples/ValidateLinkerSuccess.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ValidateOperationResult" + } + }, + "202": { + "description": "Accepted - Returns this status until the asynchronous operation has completed." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers/{linkerName}/listConfigurations": { + "post": { + "tags": [ + "Linkers" + ], + "operationId": "Linker_ListConfigurations", + "description": "list source configurations for a Linker.", + "x-ms-examples": { + "GetConfiguration": { + "$ref": "./examples/GetConfigurations.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ConfigurationResult" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/dryruns": { + "get": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_ListDryrun", + "description": "list dryrun jobs", + "x-ms-examples": { + "ListDryrun": { + "$ref": "./examples/ListDryrun.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunList" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/dryruns/{dryrunName}": { + "get": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_GetDryrun", + "description": "get a dryrun job", + "x-ms-examples": { + "GetDryrun": { + "$ref": "./examples/GetDryrun.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_CreateDryrun", + "description": "create a dryrun job to do necessary check before actual creation", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PutDryrun": { + "$ref": "./examples/PutDryrun.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + }, + { + "name": "parameters", + "description": "dryrun resource.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DryrunResource" + } + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "201": { + "description": "Long running operation", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "patch": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_UpdateDryrun", + "description": "add a dryrun job to do necessary check before actual creation", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PatchDryrun": { + "$ref": "./examples/PatchDryrun.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + }, + { + "name": "parameters", + "description": "dryrun resource.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DryrunPatch" + } + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "202": { + "description": "Accepted - Returns this status until the asynchronous operation has completed." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "delete": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_DeleteDryrun", + "description": "delete a dryrun job", + "x-ms-examples": { + "DeleteDryrun": { + "$ref": "./examples/DeleteDryrun.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + } + ], + "responses": { + "200": { + "description": "OK. The job is deleted." + }, + "204": { + "description": "Deleted. The job is not found." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers/{linkerName}/generateConfigurations": { + "post": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_GenerateConfigurations", + "description": "Generate configurations for a Linker.", + "x-ms-examples": { + "GenerateConfiguration": { + "$ref": "./examples/LinkerGenerateConfigurations.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + }, + { + "name": "parameters", + "description": "Connection Info, including format, secret store, etc", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/ConfigurationInfo" + } + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ConfigurationResult" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/providers/Microsoft.ServiceLinker/operations": { + "get": { + "tags": [ + "Operations" + ], + "operationId": "Operations_List", + "description": "Lists the available ServiceLinker REST API operations.", + "x-ms-examples": { + "GetConfiguration": { + "$ref": "./examples/OperationsList.json" + } + }, + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/OperationListResult" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/providers/Microsoft.ServiceLinker/configurationNames": { + "get": { + "tags": [ + "ConfigurationNames" + ], + "operationId": "ConfigurationNames_List", + "description": "Lists the configuration names generated by Service Connector for all target, client types, auth types.", + "x-ms-examples": { + "GetConfigurationNames": { + "$ref": "./examples/ConfigurationNamesList.json" + } + }, + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "$filter", + "in": "query", + "required": false, + "type": "string", + "description": "OData filter options." + }, + { + "name": "$skipToken", + "in": "query", + "required": false, + "type": "string", + "description": "OData skipToken option for pagination." + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ConfigurationNameResult" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/daprConfigurations": { + "get": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_ListDaprConfigurations", + "description": "List the dapr configuration supported by Service Connector.", + "x-ms-examples": { + "GetDaprConfigurations": { + "$ref": "./examples/GetDaprConfigurations.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DaprConfigurationList" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + } + }, + "definitions": { + "TargetServiceType": { + "description": "The target service type.", + "type": "string", + "enum": [ + "AzureResource", + "ConfluentBootstrapServer", + "ConfluentSchemaRegistry", + "SelfHostedServer", + "FabricPlatform" + ], + "x-ms-enum": { + "name": "targetServiceType", + "modelAsString": true + } + }, + "TargetServiceBase": { + "description": "The target service properties", + "discriminator": "type", + "type": "object", + "properties": { + "type": { + "description": "The target service type.", + "$ref": "#/definitions/TargetServiceType" + } + }, + "required": [ + "type" + ] + }, + "AzureResourceType": { + "description": "The azure resource type.", + "type": "string", + "enum": [ + "KeyVault", + "AppConfig" + ], + "x-ms-enum": { + "name": "azureResourceType", + "modelAsString": true + } + }, + "AzureResourcePropertiesBase": { + "description": "The azure resource properties", + "discriminator": "type", + "type": "object", + "properties": { + "type": { + "description": "The azure resource type.", + "$ref": "#/definitions/AzureResourceType" + } + }, + "required": [ + "type" + ] + }, + "AzureResource": { + "x-ms-discriminator-value": "AzureResource", + "type": "object", + "description": "The azure resource info when target service type is AzureResource", + "allOf": [ + { + "$ref": "#/definitions/TargetServiceBase" + } + ], + "properties": { + "id": { + "description": "The Id of azure resource.", + "type": "string" + }, + "resourceProperties": { + "x-nullable": true, + "description": "The azure resource connection related properties.", + "$ref": "#/definitions/AzureResourcePropertiesBase" + } + } + }, + "AzureKeyVaultProperties": { + "x-ms-discriminator-value": "KeyVault", + "type": "object", + "description": "The resource properties when type is Azure Key Vault", + "allOf": [ + { + "$ref": "#/definitions/AzureResourcePropertiesBase" + } + ], + "properties": { + "connectAsKubernetesCsiDriver": { + "x-nullable": true, + "description": "True if connect via Kubernetes CSI Driver.", + "type": "boolean" + } + } + }, + "AzureAppConfigProperties": { + "x-ms-discriminator-value": "AppConfig", + "type": "object", + "description": "The resource properties when type is Azure App Configuration", + "allOf": [ + { + "$ref": "#/definitions/AzureResourcePropertiesBase" + } + ], + "properties": { + "connectWithKubernetesExtension": { + "x-nullable": true, + "description": "True if connection enables app configuration kubernetes extension.", + "type": "boolean" + } + } + }, + "ConfluentBootstrapServer": { + "x-ms-discriminator-value": "ConfluentBootstrapServer", + "type": "object", + "description": "The service properties when target service type is ConfluentBootstrapServer", + "allOf": [ + { + "$ref": "#/definitions/TargetServiceBase" + } + ], + "properties": { + "endpoint": { + "description": "The endpoint of service.", + "type": "string" + } + } + }, + "FabricPlatform": { + "x-ms-discriminator-value": "FabricPlatform", + "type": "object", + "description": "The service properties when target service type is FabricPlatform", + "allOf": [ + { + "$ref": "#/definitions/TargetServiceBase" + } + ], + "properties": { + "endpoint": { + "description": "The endpoint of service.", + "type": "string" + } + } + }, + "SelfHostedServer": { + "x-ms-discriminator-value": "SelfHostedServer", + "type": "object", + "description": "The service properties when target service type is SelfHostedServer", + "allOf": [ + { + "$ref": "#/definitions/TargetServiceBase" + } + ], + "properties": { + "endpoint": { + "description": "The endpoint of service.", + "type": "string" + } + } + }, + "ConfluentSchemaRegistry": { + "x-ms-discriminator-value": "ConfluentSchemaRegistry", + "type": "object", + "description": "The service properties when target service type is ConfluentSchemaRegistry", + "allOf": [ + { + "$ref": "#/definitions/TargetServiceBase" + } + ], + "properties": { + "endpoint": { + "description": "The endpoint of service.", + "type": "string" + } + } + }, + "DeleteOrUpdateBehavior": { + "description": "The cleanup behavior to indicate whether clean up operation when resource is deleted or updated", + "type": "string", + "enum": [ + "Default", + "ForcedCleanup" + ], + "x-ms-enum": { + "name": "DeleteOrUpdateBehavior", + "modelAsString": true + } + }, + "ClientType": { + "description": "The application client type", + "type": "string", + "enum": [ + "none", + "dotnet", + "java", + "python", + "go", + "php", + "ruby", + "django", + "nodejs", + "springBoot", + "kafka-springBoot", + "jms-springBoot", + "dapr" + ], + "x-ms-enum": { + "name": "clientType", + "modelAsString": true + } + }, + "AuthType": { + "description": "The authentication type.", + "type": "string", + "enum": [ + "systemAssignedIdentity", + "userAssignedIdentity", + "servicePrincipalSecret", + "servicePrincipalCertificate", + "secret", + "accessKey", + "userAccount", + "easyAuthMicrosoftEntraID" + ], + "x-ms-enum": { + "name": "AuthType", + "modelAsString": true + } + }, + "SecretType": { + "description": "The secret type.", + "type": "string", + "enum": [ + "rawValue", + "keyVaultSecretUri", + "keyVaultSecretReference" + ], + "x-ms-enum": { + "name": "SecretType", + "modelAsString": true + } + }, + "SecretSourceType": { + "description": "The type of secret source.", + "type": "string", + "enum": [ + "rawValue", + "keyVaultSecret" + ], + "x-ms-enum": { + "name": "SecretSourceType", + "modelAsString": true + } + }, + "SecretInfoBase": { + "description": "The secret info", + "discriminator": "secretType", + "type": "object", + "properties": { + "secretType": { + "description": "The secret type.", + "$ref": "#/definitions/SecretType" + } + }, + "required": [ + "secretType" + ] + }, + "ValueSecretInfo": { + "x-ms-discriminator-value": "rawValue", + "type": "object", + "description": "The secret info when type is rawValue. It's for scenarios that user input the secret.", + "allOf": [ + { + "$ref": "#/definitions/SecretInfoBase" + } + ], + "properties": { + "value": { + "x-nullable": true, + "description": "The actual value of the secret.", + "type": "string", + "x-ms-secret": true + } + } + }, + "KeyVaultSecretReferenceSecretInfo": { + "x-ms-discriminator-value": "keyVaultSecretReference", + "type": "object", + "description": "The secret info when type is keyVaultSecretReference. It's for scenario that user provides a secret stored in user's keyvault and source is Azure Kubernetes. The key Vault's resource id is linked to secretStore.keyVaultId.", + "allOf": [ + { + "$ref": "#/definitions/SecretInfoBase" + } + ], + "properties": { + "name": { + "description": "Name of the Key Vault secret.", + "type": "string" + }, + "version": { + "x-nullable": true, + "description": "Version of the Key Vault secret.", + "type": "string" + } + } + }, + "KeyVaultSecretUriSecretInfo": { + "x-ms-discriminator-value": "keyVaultSecretUri", + "type": "object", + "description": "The secret info when type is keyVaultSecretUri. It's for scenario that user provides a secret stored in user's keyvault and source is Web App, Spring Cloud or Container App.", + "allOf": [ + { + "$ref": "#/definitions/SecretInfoBase" + } + ], + "properties": { + "value": { + "description": "URI to the keyvault secret", + "type": "string" + } + } + }, + "AuthInfoBase": { + "description": "The authentication info", + "discriminator": "authType", + "type": "object", + "properties": { + "authType": { + "description": "The authentication type.", + "$ref": "#/definitions/AuthType" + }, + "authMode": { + "description": "Optional. Indicates how to configure authentication. If optInAllAuth, service linker configures authentication such as enabling identity on source resource and granting RBAC roles. If optOutAllAuth, opt out authentication setup. Default is optInAllAuth.", + "$ref": "#/definitions/AuthMode" + } + }, + "required": [ + "authType" + ] + }, + "AccessKeyInfoBase": { + "description": "The access key directly from target resource properties, which target service is Azure Resource, such as Microsoft.Storage", + "x-ms-discriminator-value": "accessKey", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + } + ], + "properties": { + "permissions": { + "description": "Permissions of the accessKey. `Read` and `Write` are for Azure Cosmos DB and Azure App Configuration, `Listen`, `Send` and `Manage` are for Azure Event Hub and Azure Service Bus.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "Read", + "Write", + "Listen", + "Send", + "Manage" + ], + "x-ms-enum": { + "name": "accessKeyPermissions", + "modelAsString": true + } + } + } + } + }, + "DatabaseAadAuthInfo": { + "description": "The extra auth info required by Database AAD authentication.", + "type": "object", + "properties": { + "userName": { + "x-nullable": true, + "description": "Username created in the database which is mapped to a user in AAD.", + "type": "string" + } + } + }, + "SecretAuthInfo": { + "x-ms-discriminator-value": "secret", + "type": "object", + "description": "The authentication info when authType is secret", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + } + ], + "properties": { + "name": { + "x-nullable": true, + "description": "Username or account name for secret auth.", + "type": "string" + }, + "secretInfo": { + "x-nullable": true, + "description": "Password or key vault secret for secret auth.", + "$ref": "#/definitions/SecretInfoBase" + } + } + }, + "UserAssignedIdentityAuthInfo": { + "x-ms-discriminator-value": "userAssignedIdentity", + "type": "object", + "description": "The authentication info when authType is userAssignedIdentity", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + }, + { + "$ref": "#/definitions/DatabaseAadAuthInfo" + } + ], + "properties": { + "clientId": { + "description": "Client Id for userAssignedIdentity.", + "type": "string" + }, + "subscriptionId": { + "description": "Subscription id for userAssignedIdentity.", + "type": "string" + }, + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional, this value specifies the Azure role to be assigned" + } + } + }, + "SystemAssignedIdentityAuthInfo": { + "x-ms-discriminator-value": "systemAssignedIdentity", + "type": "object", + "description": "The authentication info when authType is systemAssignedIdentity", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + }, + { + "$ref": "#/definitions/DatabaseAadAuthInfo" + } + ], + "properties": { + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional, this value specifies the Azure role to be assigned" + } + } + }, + "ServicePrincipalSecretAuthInfo": { + "x-ms-discriminator-value": "servicePrincipalSecret", + "type": "object", + "description": "The authentication info when authType is servicePrincipal secret", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + }, + { + "$ref": "#/definitions/DatabaseAadAuthInfo" + } + ], + "properties": { + "clientId": { + "description": "ServicePrincipal application clientId for servicePrincipal auth.", + "type": "string" + }, + "principalId": { + "description": "Principal Id for servicePrincipal auth.", + "type": "string" + }, + "secret": { + "description": "Secret for servicePrincipal auth.", + "type": "string", + "x-ms-secret": true + }, + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional, this value specifies the Azure roles to be assigned. Automatically " + } + }, + "required": [ + "clientId", + "principalId", + "secret" + ] + }, + "ServicePrincipalCertificateAuthInfo": { + "x-ms-discriminator-value": "servicePrincipalCertificate", + "type": "object", + "description": "The authentication info when authType is servicePrincipal certificate", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + } + ], + "properties": { + "clientId": { + "description": "Application clientId for servicePrincipal auth.", + "type": "string" + }, + "principalId": { + "description": "Principal Id for servicePrincipal auth.", + "type": "string" + }, + "certificate": { + "description": "ServicePrincipal certificate for servicePrincipal auth.", + "type": "string", + "x-ms-secret": true + }, + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional, this value specifies the Azure roles to be assigned. Automatically " + } + }, + "required": [ + "clientId", + "principalId", + "certificate" + ] + }, + "UserAccountAuthInfo": { + "x-ms-discriminator-value": "userAccount", + "type": "object", + "description": "The authentication info when authType is user account", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + }, + { + "$ref": "#/definitions/DatabaseAadAuthInfo" + } + ], + "properties": { + "principalId": { + "description": "Principal Id for user account.", + "type": "string" + }, + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional, this value specifies the Azure roles to be assigned. Automatically " + } + } + }, + "EasyAuthMicrosoftEntraIDAuthInfo": { + "x-ms-discriminator-value": "easyAuthMicrosoftEntraID", + "type": "object", + "description": "The authentication info when authType is EasyAuth Microsoft Entra ID", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + } + ], + "properties": { + "clientId": { + "description": "Application clientId for EasyAuth Microsoft Entra ID.", + "type": "string" + }, + "secret": { + "description": "Application Secret for EasyAuth Microsoft Entra ID.", + "type": "string", + "x-ms-secret": true + }, + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + } + } + }, + "LinkerResource": { + "type": "object", + "description": "Linker of source and target resource", + "allOf": [ + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ProxyResource", + "description": "The resource model definition for an Azure Resource Manager proxy resource." + } + ], + "required": [ + "properties" + ], + "properties": { + "properties": { + "description": "The properties of the Linker.", + "$ref": "#/definitions/LinkerProperties", + "x-ms-client-flatten": true + }, + "systemData": { + "x-nullable": true, + "readOnly": true, + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/systemData", + "description": "The system data." + } + } + }, + "LinkerPatch": { + "description": "A Linker to be updated.", + "type": "object", + "properties": { + "properties": { + "description": "Linker properties", + "type": "object", + "x-ms-client-flatten": true, + "$ref": "#/definitions/LinkerProperties" + } + } + }, + "ResourceList": { + "description": "The list of Linker.", + "type": "object", + "properties": { + "nextLink": { + "x-nullable": true, + "description": "The Linker used to get the next page of Linker list.", + "type": "string" + }, + "value": { + "description": "The list of Linkers.", + "type": "array", + "items": { + "$ref": "#/definitions/LinkerResource" + } + } + } + }, + "LinkerProperties": { + "description": "The properties of the Linker.", + "type": "object", + "properties": { + "targetService": { + "$ref": "#/definitions/TargetServiceBase", + "description": "The target service properties" + }, + "authInfo": { + "description": "The authentication type.", + "$ref": "#/definitions/AuthInfoBase" + }, + "clientType": { + "description": "The application client type", + "$ref": "#/definitions/ClientType" + }, + "provisioningState": { + "readOnly": true, + "type": "string", + "description": "The provisioning state. " + }, + "vNetSolution": { + "x-nullable": true, + "description": "The VNet solution.", + "$ref": "#/definitions/VNetSolution" + }, + "secretStore": { + "x-nullable": true, + "description": "An option to store secret value in secure place", + "$ref": "#/definitions/SecretStore" + }, + "scope": { + "x-nullable": true, + "type": "string", + "description": "connection scope in source service." + }, + "publicNetworkSolution": { + "x-nullable": true, + "description": "The network solution.", + "$ref": "#/definitions/PublicNetworkSolution" + }, + "configurationInfo": { + "x-nullable": true, + "description": "The connection information consumed by applications, including secrets, connection strings.", + "$ref": "#/definitions/ConfigurationInfo" + } + } + }, + "LinkerConfigurationType": { + "description": "Type of configuration to determine whether the configuration can be modified after creation. KeyvaultSecret means the configuration references a key vault secret, such as App Service/ACA key vault reference. Default means the configuration is real value, such as user name, raw secret, etc.", + "type": "string", + "enum": [ + "Default", + "KeyVaultSecret" + ], + "x-ms-enum": { + "name": "LinkerConfigurationType", + "modelAsString": true + } + }, + "SourceConfiguration": { + "description": "A configuration item for source resource", + "type": "object", + "properties": { + "name": { + "description": "The name of setting.", + "type": "string" + }, + "value": { + "x-nullable": true, + "description": "The value of setting", + "type": "string" + }, + "configType": { + "description": "The type of setting", + "readOnly": true, + "$ref": "#/definitions/LinkerConfigurationType" + }, + "keyVaultReferenceIdentity": { + "x-nullable": true, + "description": "The identity for key vault reference, system or user-assigned managed identity ID", + "type": "string" + }, + "description": { + "x-nullable": true, + "description": "Descriptive information for the configuration", + "type": "string" + } + } + }, + "ConfigurationNameItem": { + "type": "object", + "properties": { + "properties": { + "x-nullable": true, + "description": "The result detail.", + "x-ms-client-flatten": true, + "$ref": "#/definitions/ConfigurationNames" + } + } + }, + "ConfigurationNames": { + "type": "object", + "description": "The configuration names which will be set based on specific target resource, client type, auth type.", + "properties": { + "targetService": { + "type": "string", + "description": "The target service provider name and resource name." + }, + "clientType": { + "$ref": "#/definitions/ClientType", + "description": "The client type for configuration names." + }, + "authType": { + "$ref": "#/definitions/AuthType", + "description": "The auth type." + }, + "secretType": { + "$ref": "#/definitions/SecretSourceType", + "description": "Indicates where the secrets in configuration from. Used when secrets are from Keyvault." + }, + "daprProperties": { + "description": "Deprecated, please use #/definitions/DaprConfigurationList instead", + "$ref": "#/definitions/DaprProperties" + }, + "names": { + "type": "array", + "description": "The configuration names to be set in compute service environment.", + "items": { + "$ref": "#/definitions/ConfigurationName" + } + } + } + }, + "ConfigurationName": { + "type": "object", + "description": "The configuration names.", + "properties": { + "value": { + "type": "string" + }, + "description": { + "type": "string", + "description": "Description for the configuration name." + }, + "required": { + "type": "boolean", + "description": "Represent the configuration is required or not" + } + } + }, + "ConfigurationNameResult": { + "description": "Configuration Name list which will be set based on different target resource, client type, auth type.", + "type": "object", + "properties": { + "value": { + "description": "Expected configuration names for each target service.", + "type": "array", + "items": { + "$ref": "#/definitions/ConfigurationNameItem" + }, + "x-ms-identifiers": [] + }, + "nextLink": { + "description": "Link to next page of resources.", + "type": "string", + "readOnly": true + } + } + }, + "ConfigurationResult": { + "description": "Configurations for source resource, include appSettings, connectionString and serviceBindings", + "type": "object", + "properties": { + "configurations": { + "description": "The configuration properties for source resource.", + "type": "array", + "items": { + "$ref": "#/definitions/SourceConfiguration" + }, + "x-ms-identifiers": [ + "name" + ] + } + } + }, + "ValidateOperationResult": { + "description": "The validation operation result for a Linker.", + "type": "object", + "properties": { + "properties": { + "x-nullable": true, + "description": "The validation result detail.", + "x-ms-client-flatten": true, + "$ref": "#/definitions/ValidateResult" + }, + "resourceId": { + "x-nullable": true, + "description": "Validated Linker id.", + "type": "string" + }, + "status": { + "x-nullable": true, + "description": "Validation operation status.", + "type": "string" + } + } + }, + "ValidateResult": { + "description": "The validation result for a Linker.", + "type": "object", + "properties": { + "linkerName": { + "x-nullable": true, + "description": "The linker name.", + "type": "string" + }, + "isConnectionAvailable": { + "x-nullable": true, + "description": "A boolean value indicating whether the connection is available or not", + "type": "boolean" + }, + "reportStartTimeUtc": { + "x-nullable": true, + "type": "string", + "format": "date-time", + "description": "The start time of the validation report." + }, + "reportEndTimeUtc": { + "x-nullable": true, + "type": "string", + "format": "date-time", + "description": "The end time of the validation report." + }, + "sourceId": { + "x-nullable": true, + "description": "The resource id of the Linker source application.", + "type": "string" + }, + "targetId": { + "x-nullable": true, + "description": "The resource Id of target service.", + "type": "string" + }, + "authType": { + "x-nullable": true, + "description": "The authentication type.", + "$ref": "#/definitions/AuthType" + }, + "validationDetail": { + "description": "The detail of validation result", + "type": "array", + "items": { + "$ref": "#/definitions/ValidationResultItem" + }, + "x-ms-identifiers": [ + "name" + ] + } + } + }, + "ValidationResultItem": { + "description": "The validation item for a Linker.", + "type": "object", + "properties": { + "name": { + "description": "The validation item name.", + "type": "string" + }, + "description": { + "x-nullable": true, + "description": "The display name of validation item", + "type": "string" + }, + "result": { + "x-nullable": true, + "description": "The result of validation", + "type": "string", + "enum": [ + "success", + "failure", + "warning" + ], + "x-ms-enum": { + "name": "ValidationResultStatus", + "modelAsString": true + } + }, + "errorMessage": { + "x-nullable": true, + "description": "The error message of validation result", + "type": "string" + }, + "errorCode": { + "x-nullable": true, + "description": "The error code of validation result", + "type": "string" + } + } + }, + "VNetSolution": { + "type": "object", + "description": "The VNet solution for linker", + "properties": { + "type": { + "x-nullable": true, + "description": "Type of VNet solution.", + "type": "string", + "enum": [ + "serviceEndpoint", + "privateLink" + ], + "x-ms-enum": { + "name": "vNetSolutionType", + "modelAsString": true + } + }, + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + } + } + }, + "PublicNetworkSolution": { + "type": "object", + "description": "Indicates public network solution, include firewall rules", + "properties": { + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation(such as firewall rules) when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "action": { + "description": "Optional. Indicates public network solution. If enable, enable public network access of target service with best try. Default is enable. If optOut, opt out public network access configuration.", + "$ref": "#/definitions/ActionType" + }, + "firewallRules": { + "description": "Describe firewall rules of target service to make sure source application could connect to the target.", + "$ref": "#/definitions/FirewallRules" + } + } + }, + "FirewallRules": { + "type": "object", + "description": "Target service's firewall rules. to allow connections from source service.", + "properties": { + "ipRanges": { + "type": "array", + "items": { + "type": "string" + }, + "description": "This value specifies the set of IP addresses or IP address ranges in CIDR form to be included as the allowed list of client IPs for a given database account." + }, + "azureServices": { + "description": "Allow Azure services to access the target service if true.", + "$ref": "#/definitions/AllowType" + }, + "callerClientIP": { + "description": "Allow caller client IP to access the target service if true. the property is used when connecting local application to target service.", + "$ref": "#/definitions/AllowType" + } + } + }, + "ConfigurationInfo": { + "type": "object", + "description": "The configuration information, used to generate configurations or save to applications", + "properties": { + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "action": { + "description": "Optional, indicate whether to apply configurations on source application. If enable, generate configurations and applied to the source application. Default is enable. If optOut, no configuration change will be made on source.", + "$ref": "#/definitions/ActionType" + }, + "customizedKeys": { + "description": "Optional. A dictionary of default key name and customized key name mapping. If not specified, default key name will be used for generate configurations", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "daprProperties": { + "description": "Indicates some additional properties for dapr client type", + "$ref": "#/definitions/DaprProperties" + }, + "additionalConfigurations": { + "description": "A dictionary of additional configurations to be added. Service will auto generate a set of basic configurations and this property is to full fill more customized configurations", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "additionalConnectionStringProperties": { + "description": "A dictionary of additional properties to be added in the end of connection string.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "configurationStore": { + "x-nullable": true, + "description": "An option to store configuration into different place", + "$ref": "#/definitions/ConfigurationStore" + } + } + }, + "ConfigurationStore": { + "type": "object", + "description": "An option to store configuration into different place", + "properties": { + "appConfigurationId": { + "x-nullable": true, + "type": "string", + "description": "The app configuration id to store configuration" + } + } + }, + "DaprProperties": { + "type": "object", + "description": "Indicates some additional properties for dapr client type", + "properties": { + "version": { + "x-nullable": true, + "type": "string", + "description": "The dapr component version" + }, + "componentType": { + "x-nullable": true, + "type": "string", + "description": "The dapr component type" + }, + "secretStoreComponent": { + "x-nullable": true, + "type": "string", + "description": "The name of a secret store dapr to retrieve secret" + }, + "metadata": { + "description": "Additional dapr metadata", + "type": "array", + "items": { + "$ref": "#/definitions/DaprMetadata" + }, + "x-ms-identifiers": [ + "name" + ] + }, + "scopes": { + "description": "The dapr component scopes", + "type": "array", + "items": { + "type": "string" + } + }, + "runtimeVersion": { + "x-nullable": true, + "type": "string", + "readOnly": true, + "description": "The runtime version supported by the properties" + }, + "bindingComponentDirection": { + "x-nullable": true, + "type": "string", + "enum": [ + "input", + "output" + ], + "x-ms-enum": { + "name": "DaprBindingComponentDirection", + "modelAsString": true + }, + "readOnly": true, + "description": "The direction supported by the dapr binding component" + } + } + }, + "DaprMetadata": { + "description": "The dapr component metadata.", + "type": "object", + "properties": { + "name": { + "description": "Metadata property name.", + "type": "string" + }, + "value": { + "description": "Metadata property value.", + "type": "string" + }, + "secretRef": { + "description": "The secret name where dapr could get value", + "type": "string" + }, + "description": { + "description": "The description of the metadata, returned from configuration api", + "type": "string" + }, + "required": { + "description": "The value indicating whether the metadata is required or not", + "type": "string", + "enum": [ + "true", + "false" + ], + "x-ms-enum": { + "name": "DaprMetadataRequired", + "modelAsString": true + } + } + } + }, + "SecretStore": { + "type": "object", + "description": "An option to store secret value in secure place", + "properties": { + "keyVaultId": { + "x-nullable": true, + "type": "string", + "description": "The key vault id to store secret" + }, + "keyVaultSecretName": { + "x-nullable": true, + "type": "string", + "description": "The key vault secret name to store secret, only valid when storing one secret" + } + } + }, + "DryrunList": { + "description": "The list of dryrun.", + "type": "object", + "properties": { + "nextLink": { + "x-nullable": true, + "description": "The link used to get the next page of dryrun list.", + "type": "string" + }, + "value": { + "description": "The list of dryrun.", + "type": "array", + "items": { + "$ref": "#/definitions/DryrunResource" + } + } + } + }, + "DryrunResource": { + "type": "object", + "description": "a dryrun job resource", + "allOf": [ + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ProxyResource", + "description": "The resource model definition for an Azure Resource Manager proxy resource." + } + ], + "properties": { + "properties": { + "description": "The properties of the dryrun job.", + "$ref": "#/definitions/DryrunProperties", + "x-ms-client-flatten": true + } + } + }, + "DryrunPatch": { + "type": "object", + "description": "a dryrun job to be updated.", + "properties": { + "properties": { + "description": "The properties of the dryrun job.", + "$ref": "#/definitions/DryrunProperties", + "x-ms-client-flatten": true + } + } + }, + "DryrunProperties": { + "description": "The properties of the dryrun job", + "type": "object", + "properties": { + "parameters": { + "description": "The parameters of the dryrun", + "$ref": "#/definitions/DryrunParameters" + }, + "prerequisiteResults": { + "readOnly": true, + "description": "the result of the dryrun", + "type": "array", + "items": { + "$ref": "#/definitions/DryrunPrerequisiteResult" + }, + "x-ms-identifiers": [] + }, + "operationPreviews": { + "readOnly": true, + "description": "the preview of the operations for creation", + "type": "array", + "items": { + "$ref": "#/definitions/DryrunOperationPreview" + }, + "x-ms-identifiers": [] + }, + "provisioningState": { + "readOnly": true, + "type": "string", + "description": "The provisioning state. " + } + } + }, + "DryrunActionName": { + "description": "The name of action for you dryrun job.", + "type": "string", + "enum": [ + "createOrUpdate" + ], + "x-ms-enum": { + "name": "DryrunActionName", + "modelAsString": true + } + }, + "DryrunParameters": { + "description": "The parameters of the dryrun", + "discriminator": "actionName", + "type": "object", + "properties": { + "actionName": { + "$ref": "#/definitions/DryrunActionName" + } + }, + "required": [ + "actionName" + ] + }, + "CreateOrUpdateDryrunParameters": { + "x-ms-discriminator-value": "createOrUpdate", + "type": "object", + "description": "The dryrun parameters for creation or update a linker", + "allOf": [ + { + "$ref": "#/definitions/DryrunParameters" + }, + { + "$ref": "#/definitions/LinkerProperties" + } + ] + }, + "DryrunPrerequisiteResultType": { + "description": "The type of dryrun result.", + "type": "string", + "enum": [ + "basicError", + "permissionsMissing" + ], + "x-ms-enum": { + "name": "DryrunPrerequisiteResultType", + "modelAsString": true + } + }, + "DryrunPrerequisiteResult": { + "description": "A result of dryrun", + "discriminator": "type", + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/DryrunPrerequisiteResultType" + } + }, + "required": [ + "type" + ] + }, + "BasicErrorDryrunPrerequisiteResult": { + "x-ms-discriminator-value": "basicError", + "description": "The represent of basic error", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/DryrunPrerequisiteResult" + } + ], + "properties": { + "code": { + "type": "string", + "description": "The error code." + }, + "message": { + "type": "string", + "description": "The error message." + } + } + }, + "PermissionsMissingDryrunPrerequisiteResult": { + "x-ms-discriminator-value": "permissionsMissing", + "description": "The represent of missing permissions", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/DryrunPrerequisiteResult" + } + ], + "properties": { + "scope": { + "description": "The permission scope", + "type": "string" + }, + "permissions": { + "description": "The permission list", + "type": "array", + "items": { + "type": "string" + } + }, + "recommendedRole": { + "description": "The recommended role to resolve permissions missing", + "type": "string" + } + } + }, + "DryrunOperationPreview": { + "description": "The preview of the operations for creation", + "type": "object", + "properties": { + "name": { + "description": "The operation name", + "type": "string" + }, + "operationType": { + "description": "The operation type", + "type": "string", + "enum": [ + "configConnection", + "configNetwork", + "configAuth" + ], + "x-ms-enum": { + "name": "DryrunPreviewOperationType", + "modelAsString": true + } + }, + "description": { + "description": "The description of the operation", + "type": "string" + }, + "action": { + "description": "The action defined by RBAC, refer https://docs.microsoft.com/azure/role-based-access-control/role-definitions#actions-format", + "type": "string" + }, + "scope": { + "description": "The scope of the operation, refer https://docs.microsoft.com/azure/role-based-access-control/scope-overview", + "type": "string" + } + } + }, + "ActionType": { + "description": "Indicates how to apply the connector operations, such as opt out network configuration, opt in configuration.", + "type": "string", + "enum": [ + "enable", + "optOut" + ], + "x-ms-enum": { + "name": "actionType", + "modelAsString": true + } + }, + "AuthMode": { + "description": "Indicates how to apply the authentication configuration operations.", + "type": "string", + "enum": [ + "optInAllAuth", + "optOutAllAuth" + ], + "x-ms-enum": { + "name": "authMode", + "modelAsString": true, + "values": [ + { + "value": "optInAllAuth", + "description": "Default authentication configuration according to the authentication type." + }, + { + "value": "optOutAllAuth", + "description": "Skip all authentication configuration such as enabling managed identity and granting RBAC roles" + } + ] + } + }, + "AllowType": { + "description": "Whether to allow firewall rules.", + "type": "string", + "enum": [ + "true", + "false" + ], + "x-ms-enum": { + "name": "allowType", + "modelAsString": true + } + }, + "DaprConfigurationList": { + "description": "Dapr configuration list supported by Service Connector", + "type": "object", + "properties": { + "value": { + "description": "The list of dapr configurations", + "type": "array", + "items": { + "$ref": "#/definitions/DaprConfigurationResource" + }, + "x-ms-identifiers": [] + }, + "nextLink": { + "description": "Link to next page of resources.", + "type": "string", + "readOnly": true + } + } + }, + "DaprConfigurationResource": { + "description": "Represent one resource of the dapr configuration list", + "type": "object", + "properties": { + "properties": { + "description": "The properties of the dapr configuration.", + "$ref": "#/definitions/DaprConfigurationProperties", + "x-ms-client-flatten": true + } + } + }, + "DaprConfigurationProperties": { + "type": "object", + "properties": { + "targetType": { + "type": "string", + "description": "Supported target resource type, extract from resource id, uppercase" + }, + "authType": { + "$ref": "#/definitions/AuthType" + }, + "daprProperties": { + "$ref": "#/definitions/DaprProperties" + } + } + } + }, + "parameters": { + "LinkerNameParameter": { + "name": "linkerName", + "in": "path", + "required": true, + "type": "string", + "description": "The name Linker resource.", + "x-ms-parameter-location": "method" + }, + "ConnectorNameParameter": { + "name": "connectorName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of resource.", + "x-ms-parameter-location": "method" + }, + "ResourceUriParameter": { + "name": "resourceUri", + "in": "path", + "required": true, + "type": "string", + "description": "The fully qualified Azure Resource manager identifier of the resource to be connected.", + "x-ms-skip-url-encoding": true, + "x-ms-parameter-location": "method" + }, + "SubscriptionIdParameter": { + "name": "subscriptionId", + "in": "path", + "required": true, + "type": "string", + "description": "The ID of the target subscription.", + "minLength": 1, + "x-ms-parameter-location": "method" + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/DeleteLink.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/DeleteLink.json new file mode 100644 index 000000000000..1665cb62d617 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/DeleteLink.json @@ -0,0 +1,12 @@ +{ + "parameters": { + "api-version": "2022-05-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName" + }, + "responses": { + "200": {}, + "204": {}, + "202": {} + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/GetConfigurations.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/GetConfigurations.json new file mode 100644 index 000000000000..b9ac679ce37e --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/GetConfigurations.json @@ -0,0 +1,19 @@ +{ + "parameters": { + "api-version": "2022-05-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName" + }, + "responses": { + "200": { + "body": { + "configurations": [ + { + "name": "ASL_DocumentDb_ConnectionString", + "value": "ConnectionString" + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/Link.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/Link.json new file mode 100644 index 000000000000..f6fc96a81031 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/Link.json @@ -0,0 +1,29 @@ +{ + "parameters": { + "api-version": "2022-05-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName" + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "name": "linkName", + "type": "Microsoft.ServiceLinker/links", + "properties": { + "authInfo": { + "authType": "secret", + "name": "name" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/LinkList.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/LinkList.json new file mode 100644 index 000000000000..b0156db7285b --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/LinkList.json @@ -0,0 +1,32 @@ +{ + "parameters": { + "api-version": "2022-05-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/links/linkName", + "name": "linkName", + "type": "Microsoft.ServiceLinker/links", + "properties": { + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/OperationsList.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/OperationsList.json new file mode 100644 index 000000000000..8f67473f5c42 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/OperationsList.json @@ -0,0 +1,185 @@ +{ + "parameters": { + "api-version": "2022-05-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "display": { + "description": "Register the subscription for Microsoft.ServiceLinker", + "operation": "Register the Microsoft.ServiceLinker", + "provider": "Microsoft.ServiceLinker", + "resource": "Microsoft.ServiceLinker" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/register/action" + }, + { + "display": { + "description": "Unregister the subscription for Microsoft.ServiceLinker", + "operation": "Unregister the Microsoft.ServiceLinker", + "provider": "Microsoft.ServiceLinker", + "resource": "Microsoft.ServiceLinker" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/unregister/action" + }, + { + "display": { + "description": "read operations", + "operation": "read_operations", + "provider": "Microsoft.ServiceLinker", + "resource": "operations" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/operations/read" + }, + { + "display": { + "description": "list dryrun jobs", + "operation": "Dryrun_List", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/read" + }, + { + "display": { + "description": "get a dryrun job", + "operation": "Dryrun_Get", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/read" + }, + { + "display": { + "description": "create a dryrun job to do necessary check before actual creation", + "operation": "Dryrun_Create", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/write" + }, + { + "display": { + "description": "delete a dryrun job", + "operation": "Dryrun_Delete", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/delete" + }, + { + "display": { + "description": "add a dryrun job to do necessary check before actual creation", + "operation": "Dryrun_Update", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/write" + }, + { + "display": { + "description": "read operationStatuses", + "operation": "read_operationStatuses", + "provider": "Microsoft.ServiceLinker", + "resource": "locations/operationStatuses" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/locations/operationStatuses/read" + }, + { + "display": { + "description": "write operationStatuses", + "operation": "write_operationStatuses", + "provider": "Microsoft.ServiceLinker", + "resource": "locations/operationStatuses" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/locations/operationStatuses/write" + }, + { + "display": { + "description": "Returns list of Linkers which connects to the resource.", + "operation": "Linker_List", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/read" + }, + { + "display": { + "description": "Returns Linker resource for a given name.", + "operation": "Linker_Get", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/read" + }, + { + "display": { + "description": "Create or update linker resource.", + "operation": "Linker_CreateOrUpdate", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/write" + }, + { + "display": { + "description": "Delete a link.", + "operation": "Linker_Delete", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/delete" + }, + { + "display": { + "description": "Operation to update an existing link.", + "operation": "Linker_Update", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/write" + }, + { + "display": { + "description": "Validate a link.", + "operation": "Linker_Validate", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/validateLinker/action" + }, + { + "display": { + "description": "list source configurations for a linker.", + "operation": "Linker_ListConfigurations", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/listConfigurations/action" + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/PatchLink.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/PatchLink.json new file mode 100644 index 000000000000..eabeb61953cb --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/PatchLink.json @@ -0,0 +1,59 @@ +{ + "parameters": { + "api-version": "2022-05-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName", + "parameters": { + "properties": { + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id", + "secret": "secret" + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + }, + "201": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/PutLink.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/PutLink.json new file mode 100644 index 000000000000..0cf5efec9cf7 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/PutLink.json @@ -0,0 +1,57 @@ +{ + "parameters": { + "api-version": "2022-05-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName", + "parameters": { + "properties": { + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/servers/test-pg/databases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name", + "secretInfo": { + "secretType": "rawValue", + "value": "secret" + } + } + } + } + }, + "responses": { + "200": { + "body": { + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "secret", + "name": "name" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/servers/test-pg/databases/test-db" + } + } + } + }, + "201": { + "body": { + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "secret", + "name": "name" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/servers/test-pg/databases/test-db" + } + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/PutLinkWithSecretStore.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/PutLinkWithSecretStore.json new file mode 100644 index 000000000000..b3b37c417811 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/PutLinkWithSecretStore.json @@ -0,0 +1,61 @@ +{ + "parameters": { + "api-version": "2022-05-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName", + "parameters": { + "properties": { + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret" + }, + "secretStore": { + "keyVaultId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-kv" + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "secret" + }, + "secretStore": { + "keyVaultId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-kv" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + }, + "201": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "secret" + }, + "secretStore": { + "keyVaultId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-kv" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/PutLinkWithServiceEndpoint.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/PutLinkWithServiceEndpoint.json new file mode 100644 index 000000000000..33b69d5fcb15 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/PutLinkWithServiceEndpoint.json @@ -0,0 +1,68 @@ +{ + "parameters": { + "api-version": "2022-05-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName", + "parameters": { + "properties": { + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/servers/test-pg/databases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name", + "secretInfo": { + "secretType": "keyVaultSecretUri", + "value": "https://vault-name.vault.azure.net/secrets/secret-name/00000000000000000000000000000000" + } + }, + "vNetSolution": { + "type": "serviceEndpoint" + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "secret", + "name": "name" + }, + "vNetSolution": { + "type": "serviceEndpoint" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/servers/test-pg/databases/test-db" + } + } + } + }, + "201": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "secret", + "name": "name" + }, + "vNetSolution": { + "type": "serviceEndpoint" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/servers/test-pg/databases/test-db" + } + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/ValidateLinkSuccess.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/ValidateLinkSuccess.json new file mode 100644 index 000000000000..5704a4b9f312 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/examples/ValidateLinkSuccess.json @@ -0,0 +1,35 @@ +{ + "parameters": { + "api-version": "2022-05-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName" + }, + "responses": { + "200": { + "body": { + "properties": { + "linkerName": "linkName", + "isConnectionAvailable": true, + "reportStartTimeUtc": "2020-07-12T22:05:09Z", + "reportEndTimeUtc": "2020-07-12T22:06:09Z", + "sourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db", + "targetId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db", + "authType": "secret", + "validationDetail": [ + { + "name": "TargetExistence", + "description": "The target existence is validated", + "result": "success" + }, + { + "name": "TargetNetworkAccess", + "description": "Deny public network access is set to yes. Please confirm you are using private endpoint connection to access target resource.", + "result": "warning" + } + ] + } + } + }, + "202": {} + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/servicelinker.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/servicelinker.json new file mode 100644 index 000000000000..36c50fd40b7a --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2022-05-01/servicelinker.json @@ -0,0 +1,1043 @@ +{ + "swagger": "2.0", + "info": { + "title": "Microsoft.ServiceLinker", + "description": "Microsoft.ServiceLinker provider", + "version": "2022-05-01" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "security": [ + { + "azure_auth": [ + "user_impersonation" + ] + } + ], + "securityDefinitions": { + "azure_auth": { + "type": "oauth2", + "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize", + "flow": "implicit", + "description": "Azure Active Directory OAuth2 Flow.", + "scopes": { + "user_impersonation": "impersonate your user account" + } + } + }, + "paths": { + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers": { + "get": { + "deprecated": false, + "description": "Returns list of Linkers which connects to the resource.", + "operationId": "Linker_List", + "x-ms-examples": { + "LinkList": { + "$ref": "./examples/LinkList.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "linker details.", + "schema": { + "$ref": "#/definitions/LinkerList" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v2/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers/{linkerName}": { + "get": { + "description": "Returns Linker resource for a given name.", + "operationId": "Linker_Get", + "x-ms-examples": { + "Link": { + "$ref": "./examples/Link.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + } + ], + "responses": { + "200": { + "description": "Linker details.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v2/types.json#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "description": "Create or update linker resource.", + "operationId": "Linker_CreateOrUpdate", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PutLink": { + "$ref": "./examples/PutLink.json" + }, + "PutLinkWithServiceEndpoint": { + "$ref": "./examples/PutLinkWithServiceEndpoint.json" + }, + "PutLinkWithSecretStore": { + "$ref": "./examples/PutLinkWithSecretStore.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + }, + { + "name": "parameters", + "description": "Linker details.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LinkerResource" + } + } + ], + "responses": { + "200": { + "description": "Successful.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "201": { + "description": "Long running operation.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v2/types.json#/definitions/ErrorResponse" + } + } + } + }, + "delete": { + "tags": [ + "Links" + ], + "operationId": "Linker_Delete", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "description": "Delete a link.", + "x-ms-examples": { + "DeleteLink": { + "$ref": "./examples/DeleteLink.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + } + ], + "responses": { + "200": { + "description": "OK. The link is deleted." + }, + "204": { + "description": "Deleted. The link is not found." + }, + "202": { + "description": "Long running operation." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v2/types.json#/definitions/ErrorResponse" + } + } + } + }, + "patch": { + "tags": [ + "Links" + ], + "operationId": "Linker_Update", + "description": "Operation to update an existing link.", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PatchLink": { + "$ref": "./examples/PatchLink.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + }, + { + "name": "parameters", + "description": "Linker details.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LinkerPatch" + } + } + ], + "responses": { + "200": { + "description": "Success. The response describes a link.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "201": { + "description": "Long running operation.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v2/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers/{linkerName}/validateLinker": { + "post": { + "tags": [ + "Links" + ], + "operationId": "Linker_Validate", + "description": "Validate a link.", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "ValidateLinkSuccess": { + "$ref": "./examples/ValidateLinkSuccess.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ValidateOperationResult" + } + }, + "202": { + "description": "Accepted - Returns this status until the asynchronous operation has completed." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v2/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers/{linkerName}/listConfigurations": { + "post": { + "tags": [ + "Links" + ], + "operationId": "Linker_ListConfigurations", + "description": "list source configurations for a linker.", + "x-ms-examples": { + "GetConfiguration": { + "$ref": "./examples/GetConfigurations.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/SourceConfigurationResult" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v2/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/providers/Microsoft.ServiceLinker/operations": { + "get": { + "tags": [ + "Operations" + ], + "operationId": "Operations_List", + "description": "Lists the available ServiceLinker REST API operations.", + "x-ms-examples": { + "GetConfiguration": { + "$ref": "./examples/OperationsList.json" + } + }, + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v2/types.json#/definitions/OperationListResult" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v2/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + } + }, + "definitions": { + "TargetServiceType": { + "description": "The target service type.", + "type": "string", + "enum": [ + "AzureResource", + "ConfluentBootstrapServer", + "ConfluentSchemaRegistry" + ], + "x-ms-enum": { + "name": "targetServiceType", + "modelAsString": true + } + }, + "TargetServiceBase": { + "description": "The target service properties", + "discriminator": "type", + "type": "object", + "properties": { + "type": { + "description": "The target service type.", + "$ref": "#/definitions/TargetServiceType" + } + }, + "required": [ + "type" + ] + }, + "AzureResourceType": { + "description": "The azure resource type.", + "type": "string", + "enum": [ + "KeyVault" + ], + "x-ms-enum": { + "name": "azureResourceType", + "modelAsString": true + } + }, + "AzureResourcePropertiesBase": { + "description": "The azure resource properties", + "discriminator": "type", + "type": "object", + "properties": { + "type": { + "description": "The azure resource type.", + "$ref": "#/definitions/AzureResourceType" + } + }, + "required": [ + "type" + ] + }, + "AzureResource": { + "x-ms-discriminator-value": "AzureResource", + "type": "object", + "description": "The azure resource info when target service type is AzureResource", + "allOf": [ + { + "$ref": "#/definitions/TargetServiceBase" + } + ], + "properties": { + "id": { + "description": "The Id of azure resource.", + "type": "string" + }, + "resourceProperties": { + "x-nullable": true, + "description": "The azure resource connection related properties.", + "$ref": "#/definitions/AzureResourcePropertiesBase" + } + } + }, + "AzureKeyVaultProperties": { + "x-ms-discriminator-value": "KeyVault", + "type": "object", + "description": "The resource properties when type is Azure Key Vault", + "allOf": [ + { + "$ref": "#/definitions/AzureResourcePropertiesBase" + } + ], + "properties": { + "connectAsKubernetesCsiDriver": { + "x-nullable": true, + "description": "True if connect via Kubernetes CSI Driver.", + "type": "boolean" + } + } + }, + "ConfluentBootstrapServer": { + "x-ms-discriminator-value": "ConfluentBootstrapServer", + "type": "object", + "description": "The service properties when target service type is ConfluentBootstrapServer", + "allOf": [ + { + "$ref": "#/definitions/TargetServiceBase" + } + ], + "properties": { + "endpoint": { + "description": "The endpoint of service.", + "type": "string" + } + } + }, + "ConfluentSchemaRegistry": { + "x-ms-discriminator-value": "ConfluentSchemaRegistry", + "type": "object", + "description": "The service properties when target service type is ConfluentSchemaRegistry", + "allOf": [ + { + "$ref": "#/definitions/TargetServiceBase" + } + ], + "properties": { + "endpoint": { + "description": "The endpoint of service.", + "type": "string" + } + } + }, + "AuthType": { + "description": "The authentication type.", + "type": "string", + "enum": [ + "systemAssignedIdentity", + "userAssignedIdentity", + "servicePrincipalSecret", + "servicePrincipalCertificate", + "secret" + ], + "x-ms-enum": { + "name": "AuthType", + "modelAsString": true + } + }, + "SecretType": { + "description": "The secret type.", + "type": "string", + "enum": [ + "rawValue", + "keyVaultSecretUri", + "keyVaultSecretReference" + ], + "x-ms-enum": { + "name": "SecretType", + "modelAsString": true + } + }, + "SecretInfoBase": { + "description": "The secret info", + "discriminator": "secretType", + "type": "object", + "properties": { + "secretType": { + "description": "The secret type.", + "$ref": "#/definitions/SecretType" + } + }, + "required": [ + "secretType" + ] + }, + "ValueSecretInfo": { + "x-ms-discriminator-value": "rawValue", + "type": "object", + "description": "The secret info when type is rawValue. It's for scenarios that user input the secret.", + "allOf": [ + { + "$ref": "#/definitions/SecretInfoBase" + } + ], + "properties": { + "value": { + "x-nullable": true, + "description": "The actual value of the secret.", + "type": "string", + "x-ms-secret": true + } + } + }, + "KeyVaultSecretReferenceSecretInfo": { + "x-ms-discriminator-value": "keyVaultSecretReference", + "type": "object", + "description": "The secret info when type is keyVaultSecretReference. It's for scenario that user provides a secret stored in user's keyvault and source is Azure Kubernetes. The key Vault's resource id is linked to secretStore.keyVaultId.", + "allOf": [ + { + "$ref": "#/definitions/SecretInfoBase" + } + ], + "properties": { + "name": { + "description": "Name of the Key Vault secret.", + "type": "string" + }, + "version": { + "x-nullable": true, + "description": "Version of the Key Vault secret.", + "type": "string" + } + } + }, + "KeyVaultSecretUriSecretInfo": { + "x-ms-discriminator-value": "keyVaultSecretUri", + "type": "object", + "description": "The secret info when type is keyVaultSecretUri. It's for scenario that user provides a secret stored in user's keyvault and source is Web App, Spring Cloud or Container App.", + "allOf": [ + { + "$ref": "#/definitions/SecretInfoBase" + } + ], + "properties": { + "value": { + "description": "URI to the keyvault secret", + "type": "string" + } + } + }, + "AuthInfoBase": { + "description": "The authentication info", + "discriminator": "authType", + "type": "object", + "properties": { + "authType": { + "description": "The authentication type.", + "$ref": "#/definitions/AuthType" + } + }, + "required": [ + "authType" + ] + }, + "SecretAuthInfo": { + "x-ms-discriminator-value": "secret", + "type": "object", + "description": "The authentication info when authType is secret", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + } + ], + "properties": { + "name": { + "x-nullable": true, + "description": "Username or account name for secret auth.", + "type": "string" + }, + "secretInfo": { + "x-nullable": true, + "description": "Password or key vault secret for secret auth.", + "$ref": "#/definitions/SecretInfoBase" + } + } + }, + "UserAssignedIdentityAuthInfo": { + "x-ms-discriminator-value": "userAssignedIdentity", + "type": "object", + "description": "The authentication info when authType is userAssignedIdentity", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + } + ], + "properties": { + "clientId": { + "description": "Client Id for userAssignedIdentity.", + "type": "string" + }, + "subscriptionId": { + "description": "Subscription id for userAssignedIdentity.", + "type": "string" + } + } + }, + "SystemAssignedIdentityAuthInfo": { + "x-ms-discriminator-value": "systemAssignedIdentity", + "type": "object", + "description": "The authentication info when authType is systemAssignedIdentity", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + } + ] + }, + "ServicePrincipalSecretAuthInfo": { + "x-ms-discriminator-value": "servicePrincipalSecret", + "type": "object", + "description": "The authentication info when authType is servicePrincipal secret", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + } + ], + "properties": { + "clientId": { + "description": "ServicePrincipal application clientId for servicePrincipal auth.", + "type": "string" + }, + "principalId": { + "description": "Principal Id for servicePrincipal auth.", + "type": "string" + }, + "secret": { + "description": "Secret for servicePrincipal auth.", + "type": "string", + "x-ms-secret": true + } + }, + "required": [ + "clientId", + "principalId", + "secret" + ] + }, + "ServicePrincipalCertificateAuthInfo": { + "x-ms-discriminator-value": "servicePrincipalCertificate", + "type": "object", + "description": "The authentication info when authType is servicePrincipal certificate", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + } + ], + "properties": { + "clientId": { + "description": "Application clientId for servicePrincipal auth.", + "type": "string" + }, + "principalId": { + "description": "Principal Id for servicePrincipal auth.", + "type": "string" + }, + "certificate": { + "description": "ServicePrincipal certificate for servicePrincipal auth.", + "type": "string", + "x-ms-secret": true + } + }, + "required": [ + "clientId", + "principalId", + "certificate" + ] + }, + "LinkerResource": { + "type": "object", + "description": "Linker of source and target resource", + "allOf": [ + { + "$ref": "../../../../../common-types/resource-management/v2/types.json#/definitions/ProxyResource", + "description": "The resource model definition for an Azure Resource Manager proxy resource." + } + ], + "required": [ + "properties" + ], + "properties": { + "properties": { + "description": "The properties of the linker.", + "$ref": "#/definitions/LinkerProperties", + "x-ms-client-flatten": true + }, + "systemData": { + "x-nullable": true, + "readOnly": true, + "$ref": "../../../../../common-types/resource-management/v2/types.json#/definitions/systemData", + "description": "The system data." + } + } + }, + "LinkerPatch": { + "description": "A linker to be updated.", + "type": "object", + "properties": { + "properties": { + "description": "Linker properties", + "type": "object", + "x-ms-client-flatten": true, + "$ref": "#/definitions/LinkerProperties" + } + } + }, + "LinkerList": { + "description": "The list of Linker.", + "type": "object", + "properties": { + "nextLink": { + "x-nullable": true, + "description": "The link used to get the next page of Linker list.", + "type": "string" + }, + "value": { + "description": "The list of Linkers.", + "type": "array", + "items": { + "$ref": "#/definitions/LinkerResource" + } + } + } + }, + "LinkerProperties": { + "description": "The properties of the linker.", + "type": "object", + "properties": { + "targetService": { + "$ref": "#/definitions/TargetServiceBase", + "description": "The target service properties" + }, + "authInfo": { + "description": "The authentication type.", + "$ref": "#/definitions/AuthInfoBase" + }, + "clientType": { + "description": "The application client type", + "type": "string", + "enum": [ + "none", + "dotnet", + "java", + "python", + "go", + "php", + "ruby", + "django", + "nodejs", + "springBoot", + "kafka-springBoot" + ], + "x-ms-enum": { + "name": "clientType", + "modelAsString": true + } + }, + "provisioningState": { + "readOnly": true, + "type": "string", + "description": "The provisioning state. " + }, + "vNetSolution": { + "x-nullable": true, + "description": "The VNet solution.", + "$ref": "#/definitions/VNetSolution" + }, + "secretStore": { + "x-nullable": true, + "description": "An option to store secret value in secure place", + "$ref": "#/definitions/SecretStore" + }, + "scope": { + "x-nullable": true, + "type": "string", + "description": "connection scope in source service." + } + } + }, + "SourceConfiguration": { + "description": "A configuration item for source resource", + "type": "object", + "properties": { + "name": { + "description": "The name of setting.", + "type": "string" + }, + "value": { + "x-nullable": true, + "description": "The value of setting", + "type": "string" + } + } + }, + "SourceConfigurationResult": { + "description": "Configurations for source resource, include appSettings, connectionString and serviceBindings", + "type": "object", + "properties": { + "configurations": { + "description": "The configuration properties for source resource.", + "type": "array", + "items": { + "$ref": "#/definitions/SourceConfiguration" + }, + "x-ms-identifiers": [ + "name" + ] + } + } + }, + "ValidateOperationResult": { + "description": "The validation operation result for a linker.", + "type": "object", + "properties": { + "properties": { + "x-nullable": true, + "description": "The validation result detail.", + "x-ms-client-flatten": true, + "$ref": "#/definitions/ValidateResult" + }, + "resourceId": { + "x-nullable": true, + "description": "Validated linker id.", + "type": "string" + }, + "status": { + "x-nullable": true, + "description": "Validation operation status.", + "type": "string" + } + } + }, + "ValidateResult": { + "description": "The validation result for a linker.", + "type": "object", + "properties": { + "linkerName": { + "x-nullable": true, + "description": "The linker name.", + "type": "string" + }, + "isConnectionAvailable": { + "x-nullable": true, + "description": "A boolean value indicating whether the connection is available or not", + "type": "boolean" + }, + "reportStartTimeUtc": { + "x-nullable": true, + "type": "string", + "format": "date-time", + "description": "The start time of the validation report." + }, + "reportEndTimeUtc": { + "x-nullable": true, + "type": "string", + "format": "date-time", + "description": "The end time of the validation report." + }, + "sourceId": { + "x-nullable": true, + "description": "The resource id of the linker source application.", + "type": "string" + }, + "targetId": { + "x-nullable": true, + "description": "The resource Id of target service.", + "type": "string" + }, + "authType": { + "x-nullable": true, + "description": "The authentication type.", + "$ref": "#/definitions/AuthType" + }, + "validationDetail": { + "description": "The detail of validation result", + "type": "array", + "items": { + "$ref": "#/definitions/ValidationResultItem" + }, + "x-ms-identifiers": [ + "name" + ] + } + } + }, + "ValidationResultItem": { + "description": "The validation item for a linker.", + "type": "object", + "properties": { + "name": { + "description": "The validation item name.", + "type": "string" + }, + "description": { + "x-nullable": true, + "description": "The display name of validation item", + "type": "string" + }, + "result": { + "x-nullable": true, + "description": "The result of validation", + "type": "string", + "enum": [ + "success", + "failure", + "warning" + ], + "x-ms-enum": { + "name": "ValidationResultStatus", + "modelAsString": true + } + }, + "errorMessage": { + "x-nullable": true, + "description": "The error message of validation result", + "type": "string" + }, + "errorCode": { + "x-nullable": true, + "description": "The error code of validation result", + "type": "string" + } + } + }, + "VNetSolution": { + "type": "object", + "description": "The VNet solution for linker", + "properties": { + "type": { + "x-nullable": true, + "description": "Type of VNet solution.", + "type": "string", + "enum": [ + "serviceEndpoint", + "privateLink" + ], + "x-ms-enum": { + "name": "vNetSolutionType", + "modelAsString": true + } + } + } + }, + "SecretStore": { + "type": "object", + "description": "An option to store secret value in secure place", + "properties": { + "keyVaultId": { + "x-nullable": true, + "type": "string", + "description": "The key vault id to store secret" + } + } + } + }, + "parameters": { + "LinkerNameParameter": { + "name": "linkerName", + "in": "path", + "required": true, + "type": "string", + "description": "The name Linker resource.", + "x-ms-parameter-location": "method" + }, + "ResourceUriParameter": { + "name": "resourceUri", + "in": "path", + "required": true, + "type": "string", + "description": "The fully qualified Azure Resource manager identifier of the resource to be connected.", + "x-ms-skip-url-encoding": true, + "x-ms-parameter-location": "method" + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConfigurationNamesList.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConfigurationNamesList.json new file mode 100644 index 000000000000..e78bcd89e157 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConfigurationNamesList.json @@ -0,0 +1,52 @@ +{ + "parameters": { + "api-version": "2024-04-01" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "properties": { + "targetService": "MICROSOFT.APPCONFIGURATION/CONFIGURATIONSTORES", + "clientType": "none", + "authType": "systemAssignedIdentity", + "names": [ + { + "value": "AZURE_APPCONFIGURATION_ENDPOINT", + "description": "App configuration endpoint" + }, + { + "value": "AZURE_APPCONFIGURATION_SCOPE", + "description": "The scopes required for the token." + } + ] + } + }, + { + "properties": { + "targetService": "MICROSOFT.APPCONFIGURATION/CONFIGURATIONSTORES", + "clientType": "none", + "authType": "userAssignedIdentity", + "names": [ + { + "value": "AZURE_APPCONFIGURATION_ENDPOINT", + "description": "App configuration endpoint" + }, + { + "value": "AZURE_APPCONFIGURATION_CLIENTID", + "description": "The client(application) ID of the user identity." + }, + { + "value": "AZURE_APPCONFIGURATION_SCOPE", + "description": "The scopes required for getting token." + } + ] + } + } + ], + "nextLink": null + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConnectorDryrunCreate.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConnectorDryrunCreate.json new file mode 100644 index 000000000000..f5f690730a3c --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConnectorDryrunCreate.json @@ -0,0 +1,116 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "dryrunName": "dryrunName", + "parameters": { + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name", + "secretInfo": { + "secretType": "rawValue", + "value": "secret" + } + } + } + } + } + }, + "responses": { + "200": { + "body": { + "name": "dryrunName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/locations/westus/dryruns/dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name" + } + }, + "prerequisiteResults": [ + { + "type": "basicError", + "code": "ResourceNotFound", + "message": "Target resource is not found" + }, + { + "type": "permissionsMissing", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc", + "permissions": [ + "Microsoft.DocumentDb/databaseAccounts/write" + ] + } + ], + "operationPreviews": [ + { + "name": "configFirewallRule", + "operationType": "configNetwork", + "description": "Config firewall rule for target service to allow source service access", + "action": "Microsoft.DocumentDb/databaseAccounts/write", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc" + } + ], + "provisioningState": "Succeeded" + } + } + }, + "201": { + "body": { + "name": "dryrunName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/locations/westus/dryruns/dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name" + } + }, + "prerequisiteResults": [ + { + "type": "basicError", + "code": "ResourceNotFound", + "message": "Target resource is not found" + }, + { + "type": "permissionsMissing", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc", + "permissions": [ + "Microsoft.DocumentDb/databaseAccounts/write" + ] + } + ], + "operationPreviews": [ + { + "name": "configFirewallRule", + "operationType": "configNetwork", + "description": "Config firewall rule for target service to allow source service access", + "action": "Microsoft.DocumentDb/databaseAccounts/write", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc" + } + ], + "provisioningState": "Accepted" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConnectorDryrunDelete.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConnectorDryrunDelete.json new file mode 100644 index 000000000000..a2bae803c32a --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConnectorDryrunDelete.json @@ -0,0 +1,13 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "dryrunName": "dryrunName" + }, + "responses": { + "200": {}, + "204": {} + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConnectorDryrunGet.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConnectorDryrunGet.json new file mode 100644 index 000000000000..e4aa9704d7e2 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConnectorDryrunGet.json @@ -0,0 +1,33 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "dryrunName": "dryrunName" + }, + "responses": { + "200": { + "body": { + "name": "dryrunName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/locations/westus/dryruns/dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConnectorDryrunList.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConnectorDryrunList.json new file mode 100644 index 000000000000..ef3f43ad4e14 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConnectorDryrunList.json @@ -0,0 +1,36 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "name": "dryrunName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/locations/westus/dryruns/dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConnectorDryrunUpdate.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConnectorDryrunUpdate.json new file mode 100644 index 000000000000..adc1ea615588 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConnectorDryrunUpdate.json @@ -0,0 +1,78 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "dryrunName": "dryrunName", + "parameters": { + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name", + "secretInfo": { + "secretType": "rawValue", + "value": "secret" + } + } + } + } + } + }, + "responses": { + "200": { + "body": { + "name": "dryrunName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/locations/westus/dryruns/dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name" + } + }, + "prerequisiteResults": [ + { + "type": "basicError", + "code": "ResourceNotFound", + "message": "Target resource is not found" + }, + { + "type": "permissionsMissing", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc", + "permissions": [ + "Microsoft.DocumentDb/databaseAccounts/write" + ] + } + ], + "operationPreviews": [ + { + "name": "configFirewallRule", + "operationType": "configNetwork", + "description": "Config firewall rule for target service to allow source service access", + "action": "Microsoft.DocumentDb/databaseAccounts/write", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc" + } + ], + "provisioningState": "Succeeded" + } + } + }, + "202": { + "headers": { + "azure-AsyncOperation": "http://azure.async.operation/status" + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConnectorList.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConnectorList.json new file mode 100644 index 000000000000..dc6561c8b804 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ConnectorList.json @@ -0,0 +1,34 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/linkers/linkName", + "name": "linkName", + "type": "Microsoft.ServiceLinker/devConnectors", + "properties": { + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/Connectors.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/Connectors.json new file mode 100644 index 000000000000..c75b7998a450 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/Connectors.json @@ -0,0 +1,43 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "connectorName": "connectorName" + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/devConnnectors/linkName", + "name": "linkName", + "type": "Microsoft.ServiceLinker/devConnectors", + "properties": { + "authInfo": { + "authType": "systemAssignedIdentity", + "roles": [ + "customizedOwner" + ] + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "publicNetworkSolution": { + "firewallRules": { + "ipRanges": [ + "182.22.120" + ], + "callerClientIP": "true" + }, + "action": "enable", + "deleteOrUpdateBehavior": "ForcedCleanup" + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/DeleteConnector.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/DeleteConnector.json new file mode 100644 index 000000000000..ee18d68b35c3 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/DeleteConnector.json @@ -0,0 +1,18 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "connectorName": "connectorName" + }, + "responses": { + "200": {}, + "204": {}, + "202": { + "headers": { + "azure-AsyncOperation": "http://azure.async.operation/status" + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/DeleteDryrun.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/DeleteDryrun.json new file mode 100644 index 000000000000..6170545eefdf --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/DeleteDryrun.json @@ -0,0 +1,11 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "dryrunName": "dryrunName" + }, + "responses": { + "200": {}, + "204": {} + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/DeleteLinker.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/DeleteLinker.json new file mode 100644 index 000000000000..9d03d6e82f6e --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/DeleteLinker.json @@ -0,0 +1,16 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName" + }, + "responses": { + "200": {}, + "204": {}, + "202": { + "headers": { + "azure-AsyncOperation": "http://azure.async.operation/status" + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/GenerateConfigurations.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/GenerateConfigurations.json new file mode 100644 index 000000000000..ce8d8ca14863 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/GenerateConfigurations.json @@ -0,0 +1,26 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "connectorName": "connectorName", + "parameters": { + "customizedKeys": { + "ASL_DocumentDb_ConnectionString": "MyConnectionstring" + } + } + }, + "responses": { + "200": { + "body": { + "configurations": [ + { + "name": "MyConnectionstring", + "value": "ConnectionString" + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/GetConfigurations.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/GetConfigurations.json new file mode 100644 index 000000000000..54118985b95a --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/GetConfigurations.json @@ -0,0 +1,41 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.App/containerApps/test-app", + "linkerName": "linkName" + }, + "responses": { + "200": { + "body": { + "configurations": [ + { + "name": "AZURE_POSTGRESQL_HOST", + "value": "Host", + "configType": "Default" + }, + { + "name": "AZURE_POSTGRESQL_USER", + "value": "Username", + "configType": "Default" + }, + { + "name": "AZURE_POSTGRESQL_DATABASE", + "value": "DatabaseName", + "configType": "Default" + }, + { + "name": "AZURE_POSTGRESQL_PORT", + "value": "Port", + "configType": "Default" + }, + { + "name": "AZURE_POSTGRESQL_PASSWORD", + "value": "SecretUri", + "configType": "KeyVaultSecret", + "keyVaultReferenceIdentity": "system" + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/GetDaprConfigurations.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/GetDaprConfigurations.json new file mode 100644 index 000000000000..4154317a74c1 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/GetDaprConfigurations.json @@ -0,0 +1,33 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "properties": { + "targetType": "MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES", + "authType": "secret", + "daprProperties": { + "version": "v1", + "componentType": "bindings", + "runtimeVersion": "1.10", + "bindingComponentDirection": "input", + "metadata": [ + { + "name": "containerName", + "description": "The name of the container to be used for Dapr state.", + "required": "true" + } + ] + } + } + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/GetDryrun.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/GetDryrun.json new file mode 100644 index 000000000000..ecdc7df7acda --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/GetDryrun.json @@ -0,0 +1,32 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "dryrunName": "dryrunName" + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/dryruns/dryrunName", + "name": "dryrunName", + "type": "Microsoft.ServiceLinker/dryruns", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/Linker.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/Linker.json new file mode 100644 index 000000000000..d09f31330555 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/Linker.json @@ -0,0 +1,47 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName" + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "name": "linkName", + "type": "Microsoft.ServiceLinker/links", + "properties": { + "authInfo": { + "authType": "secret", + "name": "name" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "secretStore": { + "keyVaultId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/kvname" + }, + "scope": "AKS-Namespace", + "clientType": "dotnet", + "publicNetworkSolution": { + "action": "enable" + }, + "configurationInfo": { + "deleteOrUpdateBehavior": "ForcedCleanup", + "customizedKeys": { + "AZURE_MYSQL_CONNECTIONSTRING": "myConnectionstring", + "AZURE_MYSQL_SSLMODE": "mySslmode" + }, + "additionalConfigurations": { + "throttlingLimit": "100" + } + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/LinkerGenerateConfigurations.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/LinkerGenerateConfigurations.json new file mode 100644 index 000000000000..17261ebabd97 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/LinkerGenerateConfigurations.json @@ -0,0 +1,24 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName", + "parameters": { + "customizedKeys": { + "ASL_DocumentDb_ConnectionString": "MyConnectionstring" + } + } + }, + "responses": { + "200": { + "body": { + "configurations": [ + { + "name": "MyConnectionstring", + "value": "ConnectionString" + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/LinkerList.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/LinkerList.json new file mode 100644 index 000000000000..ac851789d850 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/LinkerList.json @@ -0,0 +1,32 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.ServiceLinker/links/linkName", + "name": "linkName", + "type": "Microsoft.ServiceLinker/links", + "properties": { + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ListDryrun.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ListDryrun.json new file mode 100644 index 000000000000..7425c56a7c5e --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ListDryrun.json @@ -0,0 +1,35 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/dryruns/dryrunName", + "name": "dryrunName", + "type": "Microsoft.ServiceLinker/dryruns", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "authInfo": { + "authType": "secret", + "name": "username" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + }, + "systemData": { + "createdAt": "2020-07-12T22:05:09Z" + } + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/OperationsList.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/OperationsList.json new file mode 100644 index 000000000000..4db0c00028ba --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/OperationsList.json @@ -0,0 +1,184 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "display": { + "description": "Register the subscription for Microsoft.ServiceLinker", + "operation": "Register the Microsoft.ServiceLinker", + "provider": "Microsoft.ServiceLinker", + "resource": "Microsoft.ServiceLinker" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/register/action" + }, + { + "display": { + "description": "Unregister the subscription for Microsoft.ServiceLinker", + "operation": "Unregister the Microsoft.ServiceLinker", + "provider": "Microsoft.ServiceLinker", + "resource": "Microsoft.ServiceLinker" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/unregister/action" + }, + { + "display": { + "description": "read operations", + "operation": "read_operations", + "provider": "Microsoft.ServiceLinker", + "resource": "operations" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/operations/read" + }, + { + "display": { + "description": "list dryrun jobs", + "operation": "Dryrun_List", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/read" + }, + { + "display": { + "description": "get a dryrun job", + "operation": "Dryrun_Get", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/read" + }, + { + "display": { + "description": "create a dryrun job to do necessary check before actual creation", + "operation": "Dryrun_Create", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/write" + }, + { + "display": { + "description": "delete a dryrun job", + "operation": "Dryrun_Delete", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/delete" + }, + { + "display": { + "description": "add a dryrun job to do necessary check before actual creation", + "operation": "Dryrun_Update", + "provider": "Microsoft.ServiceLinker", + "resource": "dryruns" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/dryruns/write" + }, + { + "display": { + "description": "read operationStatuses", + "operation": "read_operationStatuses", + "provider": "Microsoft.ServiceLinker", + "resource": "locations/operationStatuses" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/locations/operationStatuses/read" + }, + { + "display": { + "description": "write operationStatuses", + "operation": "write_operationStatuses", + "provider": "Microsoft.ServiceLinker", + "resource": "locations/operationStatuses" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/locations/operationStatuses/write" + }, + { + "display": { + "description": "Returns list of Linkers which connects to the resource.", + "operation": "Linker_List", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/read" + }, + { + "display": { + "description": "Returns Linker resource for a given name.", + "operation": "Linker_Get", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/read" + }, + { + "display": { + "description": "Create or update linker resource.", + "operation": "Linker_CreateOrUpdate", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/write" + }, + { + "display": { + "description": "Delete a link.", + "operation": "Linker_Delete", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/delete" + }, + { + "display": { + "description": "Operation to update an existing link.", + "operation": "Linker_Update", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/write" + }, + { + "display": { + "description": "Validate a link.", + "operation": "Linker_Validate", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/validateLinker/action" + }, + { + "display": { + "description": "list source configurations for a linker.", + "operation": "Linker_ListConfigurations", + "provider": "Microsoft.ServiceLinker", + "resource": "linkers" + }, + "isDataAction": false, + "name": "Microsoft.ServiceLinker/linkers/listConfigurations/action" + } + ] + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/PatchConnector.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/PatchConnector.json new file mode 100644 index 000000000000..2d33a6ac50b8 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/PatchConnector.json @@ -0,0 +1,64 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "connectorName": "connectorName", + "parameters": { + "properties": { + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id", + "secret": "secret" + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + }, + "202": { + "headers": { + "azure-asyncoperation": "http://azure.async.operation/status" + }, + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/PatchDryrun.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/PatchDryrun.json new file mode 100644 index 000000000000..fe0fbc9e9342 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/PatchDryrun.json @@ -0,0 +1,77 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "dryrunName": "dryrunName", + "parameters": { + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name", + "secretInfo": { + "secretType": "rawValue", + "value": "secret" + } + } + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/dryruns/dryrunName", + "type": "Microsoft.ServiceLinker/dryruns", + "name": "dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name" + } + }, + "prerequisiteResults": [ + { + "type": "basicError", + "code": "ResourceNotFound", + "message": "Target resource is not found" + }, + { + "type": "permissionsMissing", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc", + "permissions": [ + "Microsoft.DocumentDb/databaseAccounts/write" + ] + } + ], + "operationPreviews": [ + { + "name": "configFirewallRule", + "operationType": "configNetwork", + "description": "Config firewall rule for target service to allow source service access", + "action": "Microsoft.DocumentDb/databaseAccounts/write", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc" + } + ], + "provisioningState": "Succeeded" + } + } + }, + "202": { + "headers": { + "azure-AsyncOperation": "http://azure.async.operation/status" + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/PatchLinker.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/PatchLinker.json new file mode 100644 index 000000000000..f43d2a751af7 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/PatchLinker.json @@ -0,0 +1,59 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName", + "parameters": { + "properties": { + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id", + "secret": "secret" + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + }, + "201": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "servicePrincipalSecret", + "clientId": "name", + "principalId": "id" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/PutConnector.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/PutConnector.json new file mode 100644 index 000000000000..e59f28e633dd --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/PutConnector.json @@ -0,0 +1,63 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "connectorName": "connectorName", + "parameters": { + "properties": { + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret" + }, + "secretStore": { + "keyVaultId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-kv" + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "secret" + }, + "secretStore": { + "keyVaultId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-kv" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + }, + "201": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "properties": { + "authInfo": { + "authType": "secret" + }, + "secretStore": { + "keyVaultId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-kv" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + } + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/PutDryrun.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/PutDryrun.json new file mode 100644 index 000000000000..91619e5257a6 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/PutDryrun.json @@ -0,0 +1,116 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "dryrunName": "dryrunName", + "parameters": { + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name", + "secretInfo": { + "secretType": "rawValue", + "value": "secret" + } + } + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/dryruns/dryrunName", + "type": "Microsoft.ServiceLinker/dryruns", + "name": "dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name" + } + }, + "prerequisiteResults": [ + { + "type": "basicError", + "code": "ResourceNotFound", + "message": "Target resource is not found" + }, + { + "type": "permissionsMissing", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc", + "permissions": [ + "Microsoft.DocumentDb/databaseAccounts/write" + ] + } + ], + "operationPreviews": [ + { + "name": "configFirewallRule", + "operationType": "configNetwork", + "description": "Config firewall rule for target service to allow source service access", + "action": "Microsoft.DocumentDb/databaseAccounts/write", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc" + } + ], + "provisioningState": "Succeeded" + } + } + }, + "201": { + "body": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/dryruns/dryrunName", + "type": "Microsoft.ServiceLinker/dryruns", + "name": "dryrunName", + "properties": { + "parameters": { + "actionName": "createOrUpdate", + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db" + }, + "authInfo": { + "authType": "secret", + "name": "name" + } + }, + "prerequisiteResults": [ + { + "type": "basicError", + "code": "ResourceNotFound", + "message": "Target resource is not found" + }, + { + "type": "permissionsMissing", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc", + "permissions": [ + "Microsoft.DocumentDb/databaseAccounts/write" + ] + } + ], + "operationPreviews": [ + { + "name": "configFirewallRule", + "operationType": "configNetwork", + "description": "Config firewall rule for target service to allow source service access", + "action": "Microsoft.DocumentDb/databaseAccounts/write", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc" + } + ], + "provisioningState": "Updating" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/PutLinker.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/PutLinker.json new file mode 100644 index 000000000000..679db2cb61f2 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/PutLinker.json @@ -0,0 +1,68 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName", + "parameters": { + "properties": { + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/servers/test-pg/databases/test-db" + }, + "vNetSolution": { + "type": "serviceEndpoint" + }, + "authInfo": { + "authType": "secret", + "name": "name", + "secretInfo": { + "secretType": "rawValue", + "value": "secret" + } + } + } + } + }, + "responses": { + "200": { + "body": { + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "properties": { + "authInfo": { + "authType": "secret", + "name": "name" + }, + "vNetSolution": { + "type": "serviceEndpoint" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/servers/test-pg/databases/test-db" + } + } + } + }, + "201": { + "body": { + "type": "Microsoft.ServiceLinker/links", + "name": "linkName", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app/providers/Microsoft.ServiceLinker/links/linkName", + "properties": { + "authInfo": { + "authType": "secret", + "name": "name" + }, + "vNetSolution": { + "type": "serviceEndpoint" + }, + "targetService": { + "type": "AzureResource", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/servers/test-pg/databases/test-db" + } + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ValidateConnectorSuccess.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ValidateConnectorSuccess.json new file mode 100644 index 000000000000..ca8ab2093d98 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ValidateConnectorSuccess.json @@ -0,0 +1,38 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "test-rg", + "location": "westus", + "connectorName": "connectorName" + }, + "responses": { + "200": { + "body": { + "properties": { + "isConnectionAvailable": true, + "reportStartTimeUtc": "2020-07-12T22:05:09Z", + "reportEndTimeUtc": "2020-07-12T22:06:09Z", + "authType": "secret", + "validationDetail": [ + { + "name": "TargetExistence", + "description": "The target existence is validated", + "result": "success" + }, + { + "name": "TargetNetworkAccess", + "description": "Deny public network access is set to yes. Please confirm you are using private endpoint connection to access target resource.", + "result": "warning" + } + ] + } + } + }, + "202": { + "headers": { + "azure-AsyncOperation": "http://azure.async.operation/status" + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ValidateLinkerSuccess.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ValidateLinkerSuccess.json new file mode 100644 index 000000000000..9bbe684cf2dd --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/examples/ValidateLinkerSuccess.json @@ -0,0 +1,38 @@ +{ + "parameters": { + "api-version": "2024-04-01", + "resourceUri": "subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app", + "linkerName": "linkName" + }, + "responses": { + "200": { + "body": { + "properties": { + "isConnectionAvailable": true, + "reportStartTimeUtc": "2020-07-12T22:05:09Z", + "reportEndTimeUtc": "2020-07-12T22:06:09Z", + "sourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db", + "targetId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/test-acc/mongodbDatabases/test-db", + "authType": "secret", + "validationDetail": [ + { + "name": "TargetExistence", + "description": "The target existence is validated", + "result": "success" + }, + { + "name": "TargetNetworkAccess", + "description": "Deny public network access is set to yes. Please confirm you are using private endpoint connection to access target resource.", + "result": "warning" + } + ] + } + } + }, + "202": { + "headers": { + "azure-AsyncOperation": "http://azure.async.operation/status" + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/servicelinker.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/servicelinker.json new file mode 100644 index 000000000000..011ae9cf4964 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/servicelinker.json @@ -0,0 +1,2902 @@ +{ + "swagger": "2.0", + "info": { + "title": "Microsoft.ServiceLinker", + "description": "Microsoft.ServiceLinker provider", + "version": "2024-04-01" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "security": [ + { + "azure_auth": [ + "user_impersonation" + ] + } + ], + "securityDefinitions": { + "azure_auth": { + "type": "oauth2", + "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize", + "flow": "implicit", + "description": "Azure Active Directory OAuth2 Flow.", + "scopes": { + "user_impersonation": "impersonate your user account" + } + } + }, + "paths": { + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/dryruns": { + "get": { + "tags": [ + "Connector" + ], + "operationId": "Connector_ListDryrun", + "description": "list dryrun jobs", + "x-ms-examples": { + "ConnectorDryrunList": { + "$ref": "./examples/ConnectorDryrunList.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunList" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/dryruns/{dryrunName}": { + "get": { + "tags": [ + "Connector" + ], + "operationId": "Connector_GetDryrun", + "description": "get a dryrun job", + "x-ms-examples": { + "ConnectorDryrunGet": { + "$ref": "./examples/ConnectorDryrunGet.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "tags": [ + "Connector" + ], + "operationId": "Connector_CreateDryrun", + "description": "create a dryrun job to do necessary check before actual creation", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "ConnectorDryrunCreate": { + "$ref": "./examples/ConnectorDryrunCreate.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + }, + { + "name": "parameters", + "description": "dryrun resource.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DryrunResource" + } + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "201": { + "description": "Long running operation", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "patch": { + "tags": [ + "Connector" + ], + "operationId": "Connector_UpdateDryrun", + "description": "update a dryrun job to do necessary check before actual creation", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "ConnectorDryrunUpdate": { + "$ref": "./examples/ConnectorDryrunUpdate.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + }, + { + "name": "parameters", + "description": "dryrun resource.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DryrunPatch" + } + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "202": { + "description": "Accepted - Returns this status until the asynchronous operation has completed." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "delete": { + "tags": [ + "Connector" + ], + "operationId": "Connector_DeleteDryrun", + "description": "delete a dryrun job", + "x-ms-examples": { + "ConnectorDryrunDelete": { + "$ref": "./examples/ConnectorDryrunDelete.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + } + ], + "responses": { + "200": { + "description": "OK. The job is deleted." + }, + "204": { + "description": "Deleted. The job is not found." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/connectors": { + "get": { + "deprecated": false, + "description": "Returns list of connector which connects to the resource, which supports to config the target service during the resource provision.", + "operationId": "Connector_List", + "x-ms-examples": { + "ConnectorList": { + "$ref": "./examples/ConnectorList.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "Connector details.", + "schema": { + "$ref": "#/definitions/ResourceList" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/connectors/{connectorName}": { + "get": { + "description": "Returns Connector resource for a given name.", + "operationId": "Connector_Get", + "x-ms-examples": { + "Connector": { + "$ref": "./examples/Connectors.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "#/parameters/ConnectorNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "Connector details.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "description": "Create or update Connector resource.", + "operationId": "Connector_CreateOrUpdate", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PutConnector": { + "$ref": "./examples/PutConnector.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "#/parameters/ConnectorNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "parameters", + "description": "Connector details.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LinkerResource" + } + } + ], + "responses": { + "200": { + "description": "Successful.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "201": { + "description": "Long running operation.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "delete": { + "tags": [ + "Connector" + ], + "operationId": "Connector_Delete", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "description": "Delete a Connector.", + "x-ms-examples": { + "DeleteConnector": { + "$ref": "./examples/DeleteConnector.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "#/parameters/ConnectorNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The Connector is deleted." + }, + "202": { + "description": "Long running operation." + }, + "204": { + "description": "Deleted. The Connector is not found." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "patch": { + "tags": [ + "Connector" + ], + "operationId": "Connector_Update", + "description": "Operation to update an existing Connector.", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PatchConnector": { + "$ref": "./examples/PatchConnector.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "#/parameters/ConnectorNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "parameters", + "description": "Connector details.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LinkerPatch" + } + } + ], + "responses": { + "200": { + "description": "Success. The response describes a Connector.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "202": { + "description": "Long running operation.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/connectors/{connectorName}/validate": { + "post": { + "tags": [ + "Connector" + ], + "operationId": "Connector_Validate", + "description": "Validate a Connector.", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "location" + }, + "x-ms-examples": { + "ValidateConnectorSuccess": { + "$ref": "./examples/ValidateConnectorSuccess.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "#/parameters/ConnectorNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ValidateOperationResult" + } + }, + "202": { + "description": "Accepted - Returns this status until the asynchronous operation has completed." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/connectors/{connectorName}/generateConfigurations": { + "post": { + "tags": [ + "Connector" + ], + "operationId": "Connector_GenerateConfigurations", + "description": "Generate configurations for a Connector.", + "x-ms-examples": { + "GenerateConfiguration": { + "$ref": "./examples/GenerateConfigurations.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/LocationParameter" + }, + { + "$ref": "#/parameters/ConnectorNameParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "parameters", + "description": "Connection Info, including format, secret store, etc", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/ConfigurationInfo" + } + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ConfigurationResult" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers": { + "get": { + "deprecated": false, + "description": "Returns list of Linkers which connects to the resource. which supports to config both application and target service during the resource provision.", + "operationId": "Linker_List", + "x-ms-examples": { + "LinkerList": { + "$ref": "./examples/LinkerList.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "Linker details.", + "schema": { + "$ref": "#/definitions/ResourceList" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers/{linkerName}": { + "get": { + "description": "Returns Linker resource for a given name.", + "operationId": "Linker_Get", + "x-ms-examples": { + "Linker": { + "$ref": "./examples/Linker.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + } + ], + "responses": { + "200": { + "description": "Linker details.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "description": "Create or update Linker resource.", + "operationId": "Linker_CreateOrUpdate", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PutLinker": { + "$ref": "./examples/PutLinker.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + }, + { + "name": "parameters", + "description": "Linker details.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LinkerResource" + } + } + ], + "responses": { + "200": { + "description": "Successful.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "201": { + "description": "Long running operation.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "delete": { + "tags": [ + "Linkers" + ], + "operationId": "Linker_Delete", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "description": "Delete a Linker.", + "x-ms-examples": { + "DeleteLinker": { + "$ref": "./examples/DeleteLinker.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + } + ], + "responses": { + "200": { + "description": "OK. The Linker is deleted." + }, + "202": { + "description": "Long running operation." + }, + "204": { + "description": "Deleted. The Linker is not found." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "patch": { + "tags": [ + "Linkers" + ], + "operationId": "Linker_Update", + "description": "Operation to update an existing Linker.", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PatchLinker": { + "$ref": "./examples/PatchLinker.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + }, + { + "name": "parameters", + "description": "Linker details.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LinkerPatch" + } + } + ], + "responses": { + "200": { + "description": "Success. The response describes a Linker.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "201": { + "description": "Long running operation.", + "schema": { + "$ref": "#/definitions/LinkerResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers/{linkerName}/validateLinker": { + "post": { + "tags": [ + "Linkers" + ], + "operationId": "Linker_Validate", + "description": "Validate a Linker.", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "location" + }, + "x-ms-examples": { + "ValidateLinkerSuccess": { + "$ref": "./examples/ValidateLinkerSuccess.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ValidateOperationResult" + } + }, + "202": { + "description": "Accepted - Returns this status until the asynchronous operation has completed." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers/{linkerName}/listConfigurations": { + "post": { + "tags": [ + "Linkers" + ], + "operationId": "Linker_ListConfigurations", + "description": "list source configurations for a Linker.", + "x-ms-examples": { + "GetConfiguration": { + "$ref": "./examples/GetConfigurations.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ConfigurationResult" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/dryruns": { + "get": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_ListDryrun", + "description": "list dryrun jobs", + "x-ms-examples": { + "ListDryrun": { + "$ref": "./examples/ListDryrun.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunList" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/dryruns/{dryrunName}": { + "get": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_GetDryrun", + "description": "get a dryrun job", + "x-ms-examples": { + "GetDryrun": { + "$ref": "./examples/GetDryrun.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_CreateDryrun", + "description": "create a dryrun job to do necessary check before actual creation", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PutDryrun": { + "$ref": "./examples/PutDryrun.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + }, + { + "name": "parameters", + "description": "dryrun resource.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DryrunResource" + } + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "201": { + "description": "Long running operation", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "patch": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_UpdateDryrun", + "description": "add a dryrun job to do necessary check before actual creation", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "PatchDryrun": { + "$ref": "./examples/PatchDryrun.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + }, + { + "name": "parameters", + "description": "dryrun resource.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DryrunPatch" + } + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + }, + "202": { + "description": "Accepted - Returns this status until the asynchronous operation has completed." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + }, + "delete": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_DeleteDryrun", + "description": "delete a dryrun job", + "x-ms-examples": { + "DeleteDryrun": { + "$ref": "./examples/DeleteDryrun.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "dryrunName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of dryrun.", + "x-ms-parameter-location": "method" + } + ], + "responses": { + "200": { + "description": "OK. The job is deleted." + }, + "204": { + "description": "Deleted. The job is not found." + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/linkers/{linkerName}/generateConfigurations": { + "post": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_GenerateConfigurations", + "description": "Generate configurations for a Linker.", + "x-ms-examples": { + "GenerateConfiguration": { + "$ref": "./examples/LinkerGenerateConfigurations.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/LinkerNameParameter" + }, + { + "name": "parameters", + "description": "Connection Info, including format, secret store, etc", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/ConfigurationInfo" + } + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ConfigurationResult" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + } + } + }, + "/providers/Microsoft.ServiceLinker/operations": { + "get": { + "tags": [ + "Operations" + ], + "operationId": "Operations_List", + "description": "Lists the available ServiceLinker REST API operations.", + "x-ms-examples": { + "GetConfiguration": { + "$ref": "./examples/OperationsList.json" + } + }, + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/OperationListResult" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/providers/Microsoft.ServiceLinker/configurationNames": { + "get": { + "tags": [ + "ConfigurationNames" + ], + "operationId": "ConfigurationNames_List", + "description": "Lists the configuration names generated by Service Connector for all target, client types, auth types.", + "x-ms-examples": { + "GetConfigurationNames": { + "$ref": "./examples/ConfigurationNamesList.json" + } + }, + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + }, + { + "name": "$filter", + "in": "query", + "required": false, + "type": "string", + "description": "OData filter options." + }, + { + "name": "$skipToken", + "in": "query", + "required": false, + "type": "string", + "description": "OData skipToken option for pagination." + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/ConfigurationNameResult" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/{resourceUri}/providers/Microsoft.ServiceLinker/daprConfigurations": { + "get": { + "tags": [ + "Linkers" + ], + "operationId": "Linkers_ListDaprConfigurations", + "description": "List the dapr configuration supported by Service Connector.", + "x-ms-examples": { + "GetDaprConfigurations": { + "$ref": "./examples/GetDaprConfigurations.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ResourceUriParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DaprConfigurationList" + } + }, + "default": { + "description": "Error response describing why the operation failed.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + } + }, + "definitions": { + "TargetServiceType": { + "description": "The target service type.", + "type": "string", + "enum": [ + "AzureResource", + "ConfluentBootstrapServer", + "ConfluentSchemaRegistry", + "SelfHostedServer" + ], + "x-ms-enum": { + "name": "targetServiceType", + "modelAsString": true + } + }, + "TargetServiceBase": { + "description": "The target service properties", + "discriminator": "type", + "type": "object", + "properties": { + "type": { + "description": "The target service type.", + "$ref": "#/definitions/TargetServiceType" + } + }, + "required": [ + "type" + ] + }, + "AzureResourceType": { + "description": "The azure resource type.", + "type": "string", + "enum": [ + "KeyVault" + ], + "x-ms-enum": { + "name": "azureResourceType", + "modelAsString": true + } + }, + "AzureResourcePropertiesBase": { + "description": "The azure resource properties", + "discriminator": "type", + "type": "object", + "properties": { + "type": { + "description": "The azure resource type.", + "$ref": "#/definitions/AzureResourceType" + } + }, + "required": [ + "type" + ] + }, + "AzureResource": { + "x-ms-discriminator-value": "AzureResource", + "type": "object", + "description": "The azure resource info when target service type is AzureResource", + "allOf": [ + { + "$ref": "#/definitions/TargetServiceBase" + } + ], + "properties": { + "id": { + "description": "The Id of azure resource.", + "type": "string" + }, + "resourceProperties": { + "x-nullable": true, + "description": "The azure resource connection related properties.", + "$ref": "#/definitions/AzureResourcePropertiesBase" + } + } + }, + "AzureKeyVaultProperties": { + "x-ms-discriminator-value": "KeyVault", + "type": "object", + "description": "The resource properties when type is Azure Key Vault", + "allOf": [ + { + "$ref": "#/definitions/AzureResourcePropertiesBase" + } + ], + "properties": { + "connectAsKubernetesCsiDriver": { + "x-nullable": true, + "description": "True if connect via Kubernetes CSI Driver.", + "type": "boolean" + } + } + }, + "ConfluentBootstrapServer": { + "x-ms-discriminator-value": "ConfluentBootstrapServer", + "type": "object", + "description": "The service properties when target service type is ConfluentBootstrapServer", + "allOf": [ + { + "$ref": "#/definitions/TargetServiceBase" + } + ], + "properties": { + "endpoint": { + "description": "The endpoint of service.", + "type": "string" + } + } + }, + "SelfHostedServer": { + "x-ms-discriminator-value": "SelfHostedServer", + "type": "object", + "description": "The service properties when target service type is SelfHostedServer", + "allOf": [ + { + "$ref": "#/definitions/TargetServiceBase" + } + ], + "properties": { + "endpoint": { + "description": "The endpoint of service.", + "type": "string" + } + } + }, + "ConfluentSchemaRegistry": { + "x-ms-discriminator-value": "ConfluentSchemaRegistry", + "type": "object", + "description": "The service properties when target service type is ConfluentSchemaRegistry", + "allOf": [ + { + "$ref": "#/definitions/TargetServiceBase" + } + ], + "properties": { + "endpoint": { + "description": "The endpoint of service.", + "type": "string" + } + } + }, + "DeleteOrUpdateBehavior": { + "description": "The cleanup behavior to indicate whether clean up operation when resource is deleted or updated", + "type": "string", + "enum": [ + "Default", + "ForcedCleanup" + ], + "x-ms-enum": { + "name": "DeleteOrUpdateBehavior", + "modelAsString": true + } + }, + "ClientType": { + "description": "The application client type", + "type": "string", + "enum": [ + "none", + "dotnet", + "java", + "python", + "go", + "php", + "ruby", + "django", + "nodejs", + "springBoot", + "kafka-springBoot", + "jms-springBoot", + "dapr" + ], + "x-ms-enum": { + "name": "clientType", + "modelAsString": true + } + }, + "AuthType": { + "description": "The authentication type.", + "type": "string", + "enum": [ + "systemAssignedIdentity", + "userAssignedIdentity", + "servicePrincipalSecret", + "servicePrincipalCertificate", + "secret", + "accessKey", + "userAccount", + "easyAuthMicrosoftEntraID" + ], + "x-ms-enum": { + "name": "AuthType", + "modelAsString": true + } + }, + "SecretType": { + "description": "The secret type.", + "type": "string", + "enum": [ + "rawValue", + "keyVaultSecretUri", + "keyVaultSecretReference" + ], + "x-ms-enum": { + "name": "SecretType", + "modelAsString": true + } + }, + "SecretSourceType": { + "description": "The type of secret source.", + "type": "string", + "enum": [ + "rawValue", + "keyVaultSecret" + ], + "x-ms-enum": { + "name": "SecretSourceType", + "modelAsString": true + } + }, + "SecretInfoBase": { + "description": "The secret info", + "discriminator": "secretType", + "type": "object", + "properties": { + "secretType": { + "description": "The secret type.", + "$ref": "#/definitions/SecretType" + } + }, + "required": [ + "secretType" + ] + }, + "ValueSecretInfo": { + "x-ms-discriminator-value": "rawValue", + "type": "object", + "description": "The secret info when type is rawValue. It's for scenarios that user input the secret.", + "allOf": [ + { + "$ref": "#/definitions/SecretInfoBase" + } + ], + "properties": { + "value": { + "x-nullable": true, + "description": "The actual value of the secret.", + "type": "string", + "x-ms-secret": true + } + } + }, + "KeyVaultSecretReferenceSecretInfo": { + "x-ms-discriminator-value": "keyVaultSecretReference", + "type": "object", + "description": "The secret info when type is keyVaultSecretReference. It's for scenario that user provides a secret stored in user's keyvault and source is Azure Kubernetes. The key Vault's resource id is linked to secretStore.keyVaultId.", + "allOf": [ + { + "$ref": "#/definitions/SecretInfoBase" + } + ], + "properties": { + "name": { + "description": "Name of the Key Vault secret.", + "type": "string" + }, + "version": { + "x-nullable": true, + "description": "Version of the Key Vault secret.", + "type": "string" + } + } + }, + "KeyVaultSecretUriSecretInfo": { + "x-ms-discriminator-value": "keyVaultSecretUri", + "type": "object", + "description": "The secret info when type is keyVaultSecretUri. It's for scenario that user provides a secret stored in user's keyvault and source is Web App, Spring Cloud or Container App.", + "allOf": [ + { + "$ref": "#/definitions/SecretInfoBase" + } + ], + "properties": { + "value": { + "description": "URI to the keyvault secret", + "type": "string" + } + } + }, + "AuthInfoBase": { + "description": "The authentication info", + "discriminator": "authType", + "type": "object", + "properties": { + "authType": { + "description": "The authentication type.", + "$ref": "#/definitions/AuthType" + }, + "authMode": { + "description": "Optional. Indicates how to configure authentication. If optInAllAuth, service linker configures authentication such as enabling identity on source resource and granting RBAC roles. If optOutAllAuth, opt out authentication setup. Default is optInAllAuth.", + "$ref": "#/definitions/AuthMode" + } + }, + "required": [ + "authType" + ] + }, + "AccessKeyInfoBase": { + "description": "The access key directly from target resource properties, which target service is Azure Resource, such as Microsoft.Storage", + "x-ms-discriminator-value": "accessKey", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + } + ], + "properties": { + "permissions": { + "description": "Permissions of the accessKey. `Read` and `Write` are for Azure Cosmos DB and Azure App Configuration, `Listen`, `Send` and `Manage` are for Azure Event Hub and Azure Service Bus.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "Read", + "Write", + "Listen", + "Send", + "Manage" + ], + "x-ms-enum": { + "name": "accessKeyPermissions", + "modelAsString": true + } + } + } + } + }, + "DatabaseAadAuthInfo": { + "description": "The extra auth info required by Database AAD authentication.", + "type": "object", + "properties": { + "userName": { + "x-nullable": true, + "description": "Username created in the database which is mapped to a user in AAD.", + "type": "string" + } + } + }, + "SecretAuthInfo": { + "x-ms-discriminator-value": "secret", + "type": "object", + "description": "The authentication info when authType is secret", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + } + ], + "properties": { + "name": { + "x-nullable": true, + "description": "Username or account name for secret auth.", + "type": "string" + }, + "secretInfo": { + "x-nullable": true, + "description": "Password or key vault secret for secret auth.", + "$ref": "#/definitions/SecretInfoBase" + } + } + }, + "UserAssignedIdentityAuthInfo": { + "x-ms-discriminator-value": "userAssignedIdentity", + "type": "object", + "description": "The authentication info when authType is userAssignedIdentity", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + }, + { + "$ref": "#/definitions/DatabaseAadAuthInfo" + } + ], + "properties": { + "clientId": { + "description": "Client Id for userAssignedIdentity.", + "type": "string" + }, + "subscriptionId": { + "description": "Subscription id for userAssignedIdentity.", + "type": "string" + }, + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional, this value specifies the Azure role to be assigned" + } + } + }, + "SystemAssignedIdentityAuthInfo": { + "x-ms-discriminator-value": "systemAssignedIdentity", + "type": "object", + "description": "The authentication info when authType is systemAssignedIdentity", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + }, + { + "$ref": "#/definitions/DatabaseAadAuthInfo" + } + ], + "properties": { + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional, this value specifies the Azure role to be assigned" + } + } + }, + "ServicePrincipalSecretAuthInfo": { + "x-ms-discriminator-value": "servicePrincipalSecret", + "type": "object", + "description": "The authentication info when authType is servicePrincipal secret", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + }, + { + "$ref": "#/definitions/DatabaseAadAuthInfo" + } + ], + "properties": { + "clientId": { + "description": "ServicePrincipal application clientId for servicePrincipal auth.", + "type": "string" + }, + "principalId": { + "description": "Principal Id for servicePrincipal auth.", + "type": "string" + }, + "secret": { + "description": "Secret for servicePrincipal auth.", + "type": "string", + "x-ms-secret": true + }, + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional, this value specifies the Azure roles to be assigned. Automatically " + } + }, + "required": [ + "clientId", + "principalId", + "secret" + ] + }, + "ServicePrincipalCertificateAuthInfo": { + "x-ms-discriminator-value": "servicePrincipalCertificate", + "type": "object", + "description": "The authentication info when authType is servicePrincipal certificate", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + } + ], + "properties": { + "clientId": { + "description": "Application clientId for servicePrincipal auth.", + "type": "string" + }, + "principalId": { + "description": "Principal Id for servicePrincipal auth.", + "type": "string" + }, + "certificate": { + "description": "ServicePrincipal certificate for servicePrincipal auth.", + "type": "string", + "x-ms-secret": true + }, + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional, this value specifies the Azure roles to be assigned. Automatically " + } + }, + "required": [ + "clientId", + "principalId", + "certificate" + ] + }, + "UserAccountAuthInfo": { + "x-ms-discriminator-value": "userAccount", + "type": "object", + "description": "The authentication info when authType is user account", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + }, + { + "$ref": "#/definitions/DatabaseAadAuthInfo" + } + ], + "properties": { + "principalId": { + "description": "Principal Id for user account.", + "type": "string" + }, + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional, this value specifies the Azure roles to be assigned. Automatically " + } + } + }, + "EasyAuthMicrosoftEntraIDAuthInfo": { + "x-ms-discriminator-value": "easyAuthMicrosoftEntraID", + "type": "object", + "description": "The authentication info when authType is EasyAuth Microsoft Entra ID", + "allOf": [ + { + "$ref": "#/definitions/AuthInfoBase" + } + ], + "properties": { + "clientId": { + "description": "Application clientId for EasyAuth Microsoft Entra ID.", + "type": "string" + }, + "secret": { + "description": "Application Secret for EasyAuth Microsoft Entra ID.", + "type": "string", + "x-ms-secret": true + }, + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + } + } + }, + "LinkerResource": { + "type": "object", + "description": "Linker of source and target resource", + "allOf": [ + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ProxyResource", + "description": "The resource model definition for an Azure Resource Manager proxy resource." + } + ], + "required": [ + "properties" + ], + "properties": { + "properties": { + "description": "The properties of the Linker.", + "$ref": "#/definitions/LinkerProperties", + "x-ms-client-flatten": true + }, + "systemData": { + "x-nullable": true, + "readOnly": true, + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/systemData", + "description": "The system data." + } + } + }, + "LinkerPatch": { + "description": "A Linker to be updated.", + "type": "object", + "properties": { + "properties": { + "description": "Linker properties", + "type": "object", + "x-ms-client-flatten": true, + "$ref": "#/definitions/LinkerProperties" + } + } + }, + "ResourceList": { + "description": "The list of Linker.", + "type": "object", + "properties": { + "nextLink": { + "x-nullable": true, + "description": "The Linker used to get the next page of Linker list.", + "type": "string" + }, + "value": { + "description": "The list of Linkers.", + "type": "array", + "items": { + "$ref": "#/definitions/LinkerResource" + } + } + } + }, + "LinkerProperties": { + "description": "The properties of the Linker.", + "type": "object", + "properties": { + "targetService": { + "$ref": "#/definitions/TargetServiceBase", + "description": "The target service properties" + }, + "authInfo": { + "description": "The authentication type.", + "$ref": "#/definitions/AuthInfoBase" + }, + "clientType": { + "description": "The application client type", + "$ref": "#/definitions/ClientType" + }, + "provisioningState": { + "readOnly": true, + "type": "string", + "description": "The provisioning state. " + }, + "vNetSolution": { + "x-nullable": true, + "description": "The VNet solution.", + "$ref": "#/definitions/VNetSolution" + }, + "secretStore": { + "x-nullable": true, + "description": "An option to store secret value in secure place", + "$ref": "#/definitions/SecretStore" + }, + "scope": { + "x-nullable": true, + "type": "string", + "description": "connection scope in source service." + }, + "publicNetworkSolution": { + "x-nullable": true, + "description": "The network solution.", + "$ref": "#/definitions/PublicNetworkSolution" + }, + "configurationInfo": { + "x-nullable": true, + "description": "The connection information consumed by applications, including secrets, connection strings.", + "$ref": "#/definitions/ConfigurationInfo" + } + } + }, + "LinkerConfigurationType": { + "description": "Type of configuration to determine whether the configuration can be modified after creation. KeyvaultSecret means the configuration references a key vault secret, such as App Service/ACA key vault reference. Default means the configuration is real value, such as user name, raw secret, etc.", + "type": "string", + "enum": [ + "Default", + "KeyVaultSecret" + ], + "x-ms-enum": { + "name": "LinkerConfigurationType", + "modelAsString": true + } + }, + "SourceConfiguration": { + "description": "A configuration item for source resource", + "type": "object", + "properties": { + "name": { + "description": "The name of setting.", + "type": "string" + }, + "value": { + "x-nullable": true, + "description": "The value of setting", + "type": "string" + }, + "configType": { + "description": "The type of setting", + "readOnly": true, + "$ref": "#/definitions/LinkerConfigurationType" + }, + "keyVaultReferenceIdentity": { + "x-nullable": true, + "description": "The identity for key vault reference, system or user-assigned managed identity ID", + "type": "string" + }, + "description": { + "x-nullable": true, + "description": "Descriptive information for the configuration", + "type": "string" + } + } + }, + "ConfigurationNameItem": { + "type": "object", + "properties": { + "properties": { + "x-nullable": true, + "description": "The result detail.", + "x-ms-client-flatten": true, + "$ref": "#/definitions/ConfigurationNames" + } + } + }, + "ConfigurationNames": { + "type": "object", + "description": "The configuration names which will be set based on specific target resource, client type, auth type.", + "properties": { + "targetService": { + "type": "string", + "description": "The target service provider name and resource name." + }, + "clientType": { + "$ref": "#/definitions/ClientType", + "description": "The client type for configuration names." + }, + "authType": { + "$ref": "#/definitions/AuthType", + "description": "The auth type." + }, + "secretType": { + "$ref": "#/definitions/SecretSourceType", + "description": "Indicates where the secrets in configuration from. Used when secrets are from Keyvault." + }, + "daprProperties": { + "description": "Deprecated, please use #/definitions/DaprConfigurationList instead", + "$ref": "#/definitions/DaprProperties" + }, + "names": { + "type": "array", + "description": "The configuration names to be set in compute service environment.", + "items": { + "$ref": "#/definitions/ConfigurationName" + } + } + } + }, + "ConfigurationName": { + "type": "object", + "description": "The configuration names.", + "properties": { + "value": { + "type": "string" + }, + "description": { + "type": "string", + "description": "Description for the configuration name." + }, + "required": { + "type": "boolean", + "description": "Represent the configuration is required or not" + } + } + }, + "ConfigurationNameResult": { + "description": "Configuration Name list which will be set based on different target resource, client type, auth type.", + "type": "object", + "properties": { + "value": { + "description": "Expected configuration names for each target service.", + "type": "array", + "items": { + "$ref": "#/definitions/ConfigurationNameItem" + }, + "x-ms-identifiers": [] + }, + "nextLink": { + "description": "Link to next page of resources.", + "type": "string", + "readOnly": true + } + } + }, + "ConfigurationResult": { + "description": "Configurations for source resource, include appSettings, connectionString and serviceBindings", + "type": "object", + "properties": { + "configurations": { + "description": "The configuration properties for source resource.", + "type": "array", + "items": { + "$ref": "#/definitions/SourceConfiguration" + }, + "x-ms-identifiers": [ + "name" + ] + } + } + }, + "ValidateOperationResult": { + "description": "The validation operation result for a Linker.", + "type": "object", + "properties": { + "properties": { + "x-nullable": true, + "description": "The validation result detail.", + "x-ms-client-flatten": true, + "$ref": "#/definitions/ValidateResult" + }, + "resourceId": { + "x-nullable": true, + "description": "Validated Linker id.", + "type": "string" + }, + "status": { + "x-nullable": true, + "description": "Validation operation status.", + "type": "string" + } + } + }, + "ValidateResult": { + "description": "The validation result for a Linker.", + "type": "object", + "properties": { + "linkerName": { + "x-nullable": true, + "description": "The linker name.", + "type": "string" + }, + "isConnectionAvailable": { + "x-nullable": true, + "description": "A boolean value indicating whether the connection is available or not", + "type": "boolean" + }, + "reportStartTimeUtc": { + "x-nullable": true, + "type": "string", + "format": "date-time", + "description": "The start time of the validation report." + }, + "reportEndTimeUtc": { + "x-nullable": true, + "type": "string", + "format": "date-time", + "description": "The end time of the validation report." + }, + "sourceId": { + "x-nullable": true, + "description": "The resource id of the Linker source application.", + "type": "string" + }, + "targetId": { + "x-nullable": true, + "description": "The resource Id of target service.", + "type": "string" + }, + "authType": { + "x-nullable": true, + "description": "The authentication type.", + "$ref": "#/definitions/AuthType" + }, + "validationDetail": { + "description": "The detail of validation result", + "type": "array", + "items": { + "$ref": "#/definitions/ValidationResultItem" + }, + "x-ms-identifiers": [ + "name" + ] + } + } + }, + "ValidationResultItem": { + "description": "The validation item for a Linker.", + "type": "object", + "properties": { + "name": { + "description": "The validation item name.", + "type": "string" + }, + "description": { + "x-nullable": true, + "description": "The display name of validation item", + "type": "string" + }, + "result": { + "x-nullable": true, + "description": "The result of validation", + "type": "string", + "enum": [ + "success", + "failure", + "warning" + ], + "x-ms-enum": { + "name": "ValidationResultStatus", + "modelAsString": true + } + }, + "errorMessage": { + "x-nullable": true, + "description": "The error message of validation result", + "type": "string" + }, + "errorCode": { + "x-nullable": true, + "description": "The error code of validation result", + "type": "string" + } + } + }, + "VNetSolution": { + "type": "object", + "description": "The VNet solution for linker", + "properties": { + "type": { + "x-nullable": true, + "description": "Type of VNet solution.", + "type": "string", + "enum": [ + "serviceEndpoint", + "privateLink" + ], + "x-ms-enum": { + "name": "vNetSolutionType", + "modelAsString": true + } + }, + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + } + } + }, + "PublicNetworkSolution": { + "type": "object", + "description": "Indicates public network solution, include firewall rules", + "properties": { + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation(such as firewall rules) when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "action": { + "description": "Optional. Indicates public network solution. If enable, enable public network access of target service with best try. Default is enable. If optOut, opt out public network access configuration.", + "$ref": "#/definitions/ActionType" + }, + "firewallRules": { + "description": "Describe firewall rules of target service to make sure source application could connect to the target.", + "$ref": "#/definitions/FirewallRules" + } + } + }, + "FirewallRules": { + "type": "object", + "description": "Target service's firewall rules. to allow connections from source service.", + "properties": { + "ipRanges": { + "type": "array", + "items": { + "type": "string" + }, + "description": "This value specifies the set of IP addresses or IP address ranges in CIDR form to be included as the allowed list of client IPs for a given database account." + }, + "azureServices": { + "description": "Allow Azure services to access the target service if true.", + "$ref": "#/definitions/AllowType" + }, + "callerClientIP": { + "description": "Allow caller client IP to access the target service if true. the property is used when connecting local application to target service.", + "$ref": "#/definitions/AllowType" + } + } + }, + "ConfigurationInfo": { + "type": "object", + "description": "The configuration information, used to generate configurations or save to applications", + "properties": { + "deleteOrUpdateBehavior": { + "description": "Indicates whether to clean up previous operation when Linker is updating or deleting", + "$ref": "#/definitions/DeleteOrUpdateBehavior" + }, + "action": { + "description": "Optional, indicate whether to apply configurations on source application. If enable, generate configurations and applied to the source application. Default is enable. If optOut, no configuration change will be made on source.", + "$ref": "#/definitions/ActionType" + }, + "customizedKeys": { + "description": "Optional. A dictionary of default key name and customized key name mapping. If not specified, default key name will be used for generate configurations", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "daprProperties": { + "description": "Indicates some additional properties for dapr client type", + "$ref": "#/definitions/DaprProperties" + }, + "additionalConfigurations": { + "description": "A dictionary of additional configurations to be added. Service will auto generate a set of basic configurations and this property is to full fill more customized configurations", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "additionalConnectionStringProperties": { + "description": "A dictionary of additional properties to be added in the end of connection string.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "configurationStore": { + "x-nullable": true, + "description": "An option to store configuration into different place", + "$ref": "#/definitions/ConfigurationStore" + } + } + }, + "ConfigurationStore": { + "type": "object", + "description": "An option to store configuration into different place", + "properties": { + "appConfigurationId": { + "x-nullable": true, + "type": "string", + "description": "The app configuration id to store configuration" + } + } + }, + "DaprProperties": { + "type": "object", + "description": "Indicates some additional properties for dapr client type", + "properties": { + "version": { + "x-nullable": true, + "type": "string", + "description": "The dapr component version" + }, + "componentType": { + "x-nullable": true, + "type": "string", + "description": "The dapr component type" + }, + "secretStoreComponent": { + "x-nullable": true, + "type": "string", + "description": "The name of a secret store dapr to retrieve secret" + }, + "metadata": { + "description": "Additional dapr metadata", + "type": "array", + "items": { + "$ref": "#/definitions/DaprMetadata" + }, + "x-ms-identifiers": [ + "name" + ] + }, + "scopes": { + "description": "The dapr component scopes", + "type": "array", + "items": { + "type": "string" + } + }, + "runtimeVersion": { + "x-nullable": true, + "type": "string", + "readOnly": true, + "description": "The runtime version supported by the properties" + }, + "bindingComponentDirection": { + "x-nullable": true, + "type": "string", + "enum": [ + "input", + "output" + ], + "x-ms-enum": { + "name": "DaprBindingComponentDirection", + "modelAsString": true + }, + "readOnly": true, + "description": "The direction supported by the dapr binding component" + } + } + }, + "DaprMetadata": { + "description": "The dapr component metadata.", + "type": "object", + "properties": { + "name": { + "description": "Metadata property name.", + "type": "string" + }, + "value": { + "description": "Metadata property value.", + "type": "string" + }, + "secretRef": { + "description": "The secret name where dapr could get value", + "type": "string" + }, + "description": { + "description": "The description of the metadata, returned from configuration api", + "type": "string" + }, + "required": { + "description": "The value indicating whether the metadata is required or not", + "type": "string", + "enum": [ + "true", + "false" + ], + "x-ms-enum": { + "name": "DaprMetadataRequired", + "modelAsString": true + } + } + } + }, + "SecretStore": { + "type": "object", + "description": "An option to store secret value in secure place", + "properties": { + "keyVaultId": { + "x-nullable": true, + "type": "string", + "description": "The key vault id to store secret" + }, + "keyVaultSecretName": { + "x-nullable": true, + "type": "string", + "description": "The key vault secret name to store secret, only valid when storing one secret" + } + } + }, + "DryrunList": { + "description": "The list of dryrun.", + "type": "object", + "properties": { + "nextLink": { + "x-nullable": true, + "description": "The link used to get the next page of dryrun list.", + "type": "string" + }, + "value": { + "description": "The list of dryrun.", + "type": "array", + "items": { + "$ref": "#/definitions/DryrunResource" + } + } + } + }, + "DryrunResource": { + "type": "object", + "description": "a dryrun job resource", + "allOf": [ + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ProxyResource", + "description": "The resource model definition for an Azure Resource Manager proxy resource." + } + ], + "properties": { + "properties": { + "description": "The properties of the dryrun job.", + "$ref": "#/definitions/DryrunProperties", + "x-ms-client-flatten": true + } + } + }, + "DryrunPatch": { + "type": "object", + "description": "a dryrun job to be updated.", + "properties": { + "properties": { + "description": "The properties of the dryrun job.", + "$ref": "#/definitions/DryrunProperties", + "x-ms-client-flatten": true + } + } + }, + "DryrunProperties": { + "description": "The properties of the dryrun job", + "type": "object", + "properties": { + "parameters": { + "description": "The parameters of the dryrun", + "$ref": "#/definitions/DryrunParameters" + }, + "prerequisiteResults": { + "readOnly": true, + "description": "the result of the dryrun", + "type": "array", + "items": { + "$ref": "#/definitions/DryrunPrerequisiteResult" + }, + "x-ms-identifiers": [] + }, + "operationPreviews": { + "readOnly": true, + "description": "the preview of the operations for creation", + "type": "array", + "items": { + "$ref": "#/definitions/DryrunOperationPreview" + }, + "x-ms-identifiers": [] + }, + "provisioningState": { + "readOnly": true, + "type": "string", + "description": "The provisioning state. " + } + } + }, + "DryrunActionName": { + "description": "The name of action for you dryrun job.", + "type": "string", + "enum": [ + "createOrUpdate" + ], + "x-ms-enum": { + "name": "DryrunActionName", + "modelAsString": true + } + }, + "DryrunParameters": { + "description": "The parameters of the dryrun", + "discriminator": "actionName", + "type": "object", + "properties": { + "actionName": { + "$ref": "#/definitions/DryrunActionName" + } + }, + "required": [ + "actionName" + ] + }, + "CreateOrUpdateDryrunParameters": { + "x-ms-discriminator-value": "createOrUpdate", + "type": "object", + "description": "The dryrun parameters for creation or update a linker", + "allOf": [ + { + "$ref": "#/definitions/DryrunParameters" + }, + { + "$ref": "#/definitions/LinkerProperties" + } + ] + }, + "DryrunPrerequisiteResultType": { + "description": "The type of dryrun result.", + "type": "string", + "enum": [ + "basicError", + "permissionsMissing" + ], + "x-ms-enum": { + "name": "DryrunPrerequisiteResultType", + "modelAsString": true + } + }, + "DryrunPrerequisiteResult": { + "description": "A result of dryrun", + "discriminator": "type", + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/DryrunPrerequisiteResultType" + } + }, + "required": [ + "type" + ] + }, + "BasicErrorDryrunPrerequisiteResult": { + "x-ms-discriminator-value": "basicError", + "description": "The represent of basic error", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/DryrunPrerequisiteResult" + } + ], + "properties": { + "code": { + "type": "string", + "description": "The error code." + }, + "message": { + "type": "string", + "description": "The error message." + } + } + }, + "PermissionsMissingDryrunPrerequisiteResult": { + "x-ms-discriminator-value": "permissionsMissing", + "description": "The represent of missing permissions", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/DryrunPrerequisiteResult" + } + ], + "properties": { + "scope": { + "description": "The permission scope", + "type": "string" + }, + "permissions": { + "description": "The permission list", + "type": "array", + "items": { + "type": "string" + } + }, + "recommendedRole": { + "description": "The recommended role to resolve permissions missing", + "type": "string" + } + } + }, + "DryrunOperationPreview": { + "description": "The preview of the operations for creation", + "type": "object", + "properties": { + "name": { + "description": "The operation name", + "type": "string" + }, + "operationType": { + "description": "The operation type", + "type": "string", + "enum": [ + "configConnection", + "configNetwork", + "configAuth" + ], + "x-ms-enum": { + "name": "DryrunPreviewOperationType", + "modelAsString": true + } + }, + "description": { + "description": "The description of the operation", + "type": "string" + }, + "action": { + "description": "The action defined by RBAC, refer https://docs.microsoft.com/azure/role-based-access-control/role-definitions#actions-format", + "type": "string" + }, + "scope": { + "description": "The scope of the operation, refer https://docs.microsoft.com/azure/role-based-access-control/scope-overview", + "type": "string" + } + } + }, + "ActionType": { + "description": "Indicates how to apply the connector operations, such as opt out network configuration, opt in configuration.", + "type": "string", + "enum": [ + "enable", + "optOut" + ], + "x-ms-enum": { + "name": "actionType", + "modelAsString": true + } + }, + "AuthMode": { + "description": "Indicates how to apply the authentication configuration operations.", + "type": "string", + "enum": [ + "optInAllAuth", + "optOutAllAuth" + ], + "x-ms-enum": { + "name": "authMode", + "modelAsString": true, + "values": [ + { + "value": "optInAllAuth", + "description": "Default authentication configuration according to the authentication type." + }, + { + "value": "optOutAllAuth", + "description": "Skip all authentication configuration such as enabling managed identity and granting RBAC roles" + } + ] + } + }, + "AllowType": { + "description": "Whether to allow firewall rules.", + "type": "string", + "enum": [ + "true", + "false" + ], + "x-ms-enum": { + "name": "allowType", + "modelAsString": true + } + }, + "DaprConfigurationList": { + "description": "Dapr configuration list supported by Service Connector", + "type": "object", + "properties": { + "value": { + "description": "The list of dapr configurations", + "type": "array", + "items": { + "$ref": "#/definitions/DaprConfigurationResource" + }, + "x-ms-identifiers": [] + }, + "nextLink": { + "description": "Link to next page of resources.", + "type": "string", + "readOnly": true + } + } + }, + "DaprConfigurationResource": { + "description": "Represent one resource of the dapr configuration list", + "type": "object", + "properties": { + "properties": { + "description": "The properties of the dapr configuration.", + "$ref": "#/definitions/DaprConfigurationProperties", + "x-ms-client-flatten": true + } + } + }, + "DaprConfigurationProperties": { + "type": "object", + "properties": { + "targetType": { + "type": "string", + "description": "Supported target resource type, extract from resource id, uppercase" + }, + "authType": { + "$ref": "#/definitions/AuthType" + }, + "daprProperties": { + "$ref": "#/definitions/DaprProperties" + } + } + } + }, + "parameters": { + "LinkerNameParameter": { + "name": "linkerName", + "in": "path", + "required": true, + "type": "string", + "description": "The name Linker resource.", + "x-ms-parameter-location": "method" + }, + "ConnectorNameParameter": { + "name": "connectorName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of resource.", + "x-ms-parameter-location": "method" + }, + "ResourceUriParameter": { + "name": "resourceUri", + "in": "path", + "required": true, + "type": "string", + "description": "The fully qualified Azure Resource manager identifier of the resource to be connected.", + "x-ms-skip-url-encoding": true, + "x-ms-parameter-location": "method" + }, + "SubscriptionIdParameter": { + "name": "subscriptionId", + "in": "path", + "required": true, + "type": "string", + "description": "The ID of the target subscription.", + "minLength": 1, + "x-ms-parameter-location": "method" + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/test.json b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/test.json new file mode 100644 index 000000000000..ec387afa516d --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/test.json @@ -0,0 +1,144 @@ +{ + "swagger": "2.0", + "info": { + "title": "Microsoft.ServiceLinker", + "description": "Microsoft.ServiceLinker provider", + "version": "2024-04-01" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "security": [ + { + "azure_auth": [ + "user_impersonation" + ] + } + ], + "securityDefinitions": { + "azure_auth": { + "type": "oauth2", + "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize", + "flow": "implicit", + "description": "Azure Active Directory OAuth2 Flow.", + "scopes": { + "user_impersonation": "impersonate your user account" + } + } + }, + "paths": { + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/dryruns": { + "get": { + "tags": [ + "Connector" + ], + "operationId": "Connector_ListDryrun", + "description": "list dryrun jobs", + "x-ms-examples": { + "ConnectorDryrunList": { + "$ref": "./examples/ConnectorDryrunList.json" + } + }, + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunList" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + } + }, + "x-ms-paths": { + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/dryruns/{dryrunName}": { + "get": { + "tags": [ + "Connector" + ], + "operationId": "Connector_GetDryrun", + "description": "get a dryrun job", + "x-ms-examples": { + "ConnectorDryrunGet": { + "$ref": "./examples/ConnectorDryrunGet.json" + } + }, + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + } + } + }, + "put": { + "tags": [ + "Connector" + ], + "operationId": "Connector_CreateDryrun", + "description": "create a dryrun job to do necessary check before actual creation", + "x-ms-long-running-operation": true, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-examples": { + "ConnectorDryrunCreate": { + "$ref": "./examples/ConnectorDryrunCreate.json" + } + }, + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "schema": { + "$ref": "#/definitions/DryrunResource" + } + } + } + } + } + }, + "definitions": { + "DryrunList": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "$ref": "#/definitions/DryrunResource" + } + }, + "nextLink": { + "type": "string" + } + } + }, + "DryrunResource": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "location": { + "type": "string" + } + } + } + } +} diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/readme.csharp.md b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/readme.csharp.md new file mode 100644 index 000000000000..cec7ea43ab74 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/readme.csharp.md @@ -0,0 +1,15 @@ +## C + +These settings apply only when `--csharp` is specified on the command line. +Please also specify `--csharp-sdks-folder=`. + +```yaml $(csharp) +csharp: + azure-arm: true + license-header: MICROSOFT_MIT_NO_VERSION + payload-flattening-threshold: 1 + clear-output-folder: true + client-side-validation: false + namespace: Microsoft.ServiceLinker + output-folder: $(csharp-sdks-folder)/servicelinker/management/Microsoft.ServiceLinker/GeneratedProtocol +``` diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/readme.go.md b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/readme.go.md new file mode 100644 index 000000000000..5a6d630e721c --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/readme.go.md @@ -0,0 +1,11 @@ +## Go + +These settings apply only when `--go` is specified on the command line. + +``` yaml $(go) && $(track2) +license-header: MICROSOFT_MIT_NO_VERSION +module-name: sdk/resourcemanager/servicelinker/armservicelinker +module: github.com/Azure/azure-sdk-for-go/$(module-name) +output-folder: $(go-sdk-folder)/$(module-name) +azure-arm: true +``` diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/readme.java.md b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/readme.java.md new file mode 100644 index 000000000000..d548eb7e399c --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/readme.java.md @@ -0,0 +1,9 @@ +## Java + +These settings apply only when `--java` is specified on the command line. + +```yaml $(java) +resource-collection-associations: +- resource: LinkerResource + collection: Linkers +``` diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/readme.md b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/readme.md new file mode 100644 index 000000000000..6079f7701073 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/readme.md @@ -0,0 +1,121 @@ +# ServiceLinker + +> see https://aka.ms/autorest + +This is the AutoRest configuration file for ServiceLinker. + +## Getting Started + +To build the SDKs for My API, simply install AutoRest via `npm` (`npm install -g autorest`) and then run: + +> `autorest readme.md` + +To see additional help and options, run: + +> `autorest --help` + +For other options on installation see [Installing AutoRest](https://aka.ms/autorest/install) on the AutoRest github page. + +--- + +## Configuration + +### Basic Information + +These are the global settings for the ServiceLinker. + +```yaml +title: ServiceLinkerManagementClient +openapi-type: arm +openapi-subtype : rpaas +tag: package-2024-07-01-preview +``` + +### Tag: package-2022-05-01 + +These settings apply only when `--tag=package-2022-05-01` is specified on the command line. + +```yaml $(tag) == 'package-2022-05-01' +input-file: + - Microsoft.ServiceLinker/stable/2022-05-01/servicelinker.json +``` + +### Tag: package-2023-04-01-preview + +These settings apply only when `--tag=package-2023-04-01-preview` is specified on the command line. + +```yaml $(tag) == 'package-2023-04-01-preview' +input-file: + - Microsoft.ServiceLinker/preview/2023-04-01-preview/servicelinker.json +``` + +### Tag: package-2024-04-01 + +These settings apply only when `--tag=package-2024-04-01` is specified on the command line. + +```yaml $(tag) == 'package-2024-04-01' +input-file: + - Microsoft.ServiceLinker/stable/2024-04-01/servicelinker.json + - Microsoft.ServiceLinker/stable/2024-04-01/test.json +``` + +### Tag: package-2024-07-01-preview + +These settings apply only when `--tag=package-2024-07-01-preview` is specified on the command line. + +```yaml $(tag) == 'package-2024-07-01-preview' +input-file: + - Microsoft.ServiceLinker/preview/2024-07-01-preview/servicelinker.json +``` + +## Suppression + +``` yaml +directive: + - suppress: TopLevelResourcesListBySubscription + from: servicelinker.json + where: $.definitions.LinkerResource + reason: This is an extension resource +``` + +--- + +# Code Generation + +## Swagger to SDK + +This section describes what SDK should be generated by the automatic system. +This is not used by Autorest itself. + +```yaml $(swagger-to-sdk) +swagger-to-sdk: + - repo: azure-sdk-for-python + - repo: azure-sdk-for-java + - repo: azure-sdk-for-go-track2 + - repo: azure-sdk-for-js + - repo: azure-sdk-for-ruby + after_scripts: + - bundle install && rake arm:regen_all_profiles['azure_mgmt_servicelinker'] + - repo: azure-resource-manager-schemas + - repo: azure-powershell +``` + +## Go + +See configuration in [readme.go.md](./readme.go.md) + +## Python + +See configuration in [readme.python.md](./readme.python.md) + +## Ruby + +See configuration in [readme.ruby.md](./readme.ruby.md) + +## TypeScript + +See configuration in [readme.typescript.md](./readme.typescript.md) + +## CSharp + +See configuration in [readme.csharp.md](./readme.csharp.md) diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/readme.python.md b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/readme.python.md new file mode 100644 index 000000000000..07d5dae67222 --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/readme.python.md @@ -0,0 +1,18 @@ +## Python + +These settings apply only when `--python` is specified on the command line. +Please also specify `--python-sdks-folder=`. + +``` yaml $(python) +azure-arm: true +license-header: MICROSOFT_MIT_NO_VERSION +package-name: azure-mgmt-servicelinker +namespace: azure.mgmt.servicelinker +package-version: 1.0.0b1 +clear-output-folder: true +``` + +``` yaml $(python) +no-namespace-folders: true +output-folder: $(python-sdks-folder)/servicelinker/azure-mgmt-servicelinker/azure/mgmt/servicelinker +``` \ No newline at end of file diff --git a/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/readme.typescript.md b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/readme.typescript.md new file mode 100644 index 000000000000..f10dd7331a2e --- /dev/null +++ b/.github/shared/test/fixtures/swagger/specification/servicelinker/resource-manager/readme.typescript.md @@ -0,0 +1,13 @@ +## TypeScript + +These settings apply only when `--typescript` is specified on the command line. +Please also specify `--typescript-sdks-folder=`. + +```yaml $(typescript) +typescript: + azure-arm: true + package-name: "@azure/arm-servicelinker" + output-folder: "$(typescript-sdks-folder)/sdk/servicelinker/arm-servicelinker" + payload-flattening-threshold: 1 + generate-metadata: true +``` diff --git a/.github/shared/test/path.test.js b/.github/shared/test/path.test.js new file mode 100644 index 000000000000..2f7e29aa040d --- /dev/null +++ b/.github/shared/test/path.test.js @@ -0,0 +1,46 @@ +// @ts-check + +import { strict as assert } from "assert"; +import { existsSync, mkdirSync, rmSync } from "fs"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; +import { afterEach, beforeEach, describe, it } from "vitest"; +import { includesFolder } from "../src/path.js"; + +// Get the directory of this test file +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +/** + * Unit tests for path.js utility functions + */ +describe("Path utilities", () => { + let tempTestDir; + + beforeEach(() => { + // Create a temporary directory for test files + tempTestDir = join(__dirname, "temp-path-tests"); + if (existsSync(tempTestDir)) { + rmSync(tempTestDir, { recursive: true, force: true }); + } + mkdirSync(tempTestDir, { recursive: true }); + }); + + afterEach(() => { + // Clean up temporary directory + if (existsSync(tempTestDir)) { + rmSync(tempTestDir, { recursive: true, force: true }); + } + }); + + describe("includesFolder", () => { + it("should return true when path contains the specified folder", () => { + assert.equal(includesFolder("/path/to/examples/file.json", "examples"), true); + assert.equal(includesFolder("/path/to/examples", "examples"), true); + }); + + it("should return false when path does not contain the specified folder", () => { + assert.equal(includesFolder("/path/to/swagger/file.json", "examples"), false); + }); + }); +}); diff --git a/.github/shared/test/readme.test.js b/.github/shared/test/readme.test.js index 3f356a53f033..4602546d189b 100644 --- a/.github/shared/test/readme.test.js +++ b/.github/shared/test/readme.test.js @@ -3,7 +3,7 @@ import { resolve } from "path"; import { describe, expect, it } from "vitest"; import { ConsoleLogger } from "../src/logger.js"; -import { Readme } from "../src/readme.js"; +import { Readme, TagMatchRegex } from "../src/readme.js"; import { SpecModel } from "../src/spec-model.js"; import { contosoReadme } from "./examples.js"; @@ -14,9 +14,7 @@ describe("readme", () => { const readme = new Readme("bar"); expect(readme.path).toBe(resolve("bar")); - await expect(readme.getTags()).rejects.toThrowError( - /no such file or directory/i, - ); + await expect(readme.getTags()).rejects.toThrowError(/no such file or directory/i); expect(readme.specModel).toBeUndefined(); }); @@ -39,23 +37,15 @@ describe("readme", () => { const tags = await readme.getTags(); const tagNames = [...tags.keys()]; - const expectedTagNames = [ - "package-2021-11-01", - "package-2021-10-01-preview", - ]; + const expectedTagNames = ["package-2021-11-01", "package-2021-10-01-preview"]; expect(tagNames.sort()).toEqual(expectedTagNames.sort()); - const swaggerPaths = [...tags.values()].flatMap((t) => [ - ...t.inputFiles.keys(), - ]); + const swaggerPaths = [...tags.values()].flatMap((t) => [...t.inputFiles.keys()]); const expectedPaths = [ resolve(folder, "Microsoft.Contoso/stable/2021-11-01/contoso.json"), - resolve( - folder, - "Microsoft.Contoso/preview/2021-10-01-preview/contoso.json", - ), + resolve(folder, "Microsoft.Contoso/preview/2021-10-01-preview/contoso.json"), ]; expect(swaggerPaths.sort()).toEqual(expectedPaths.sort()); @@ -75,3 +65,19 @@ describe("readme", () => { expect(tags.size).toBe(0); }); }); + +describe("TagMatchRegex", () => { + it.each([ + ["```yaml $(package-A-tag) == 'package-A-[[Version]]'", false, undefined], + ["``` yaml $(tag)=='package-2017-03' && $(go)", true, "package-2017-03"], + ["``` yaml $(csharp) && $(tag) == 'release_4_0'", true, "release_4_0"], + ["``` yaml $(tag) == 'package-2021-12-01-preview'", true, "package-2021-12-01-preview"], + ['``` yaml $(tag) == "package-2025-06-05"', true, "package-2025-06-05"], + ["``` yaml $(tag) == 'package-2025-06-05\"", false, undefined], + ["``` yaml $(tag) == \"package-2025-06-05'", false, undefined], + ])("matches tags and extracts tag names properly: %s", (example, expectedMatch, expectedTag) => { + const match = example.match(TagMatchRegex); + expect(TagMatchRegex.test(example)).toEqual(expectedMatch); + expect(match?.[2]).toEqual(expectedTag); + }); +}); diff --git a/.github/shared/test/simple-git.test.js b/.github/shared/test/simple-git.test.js new file mode 100644 index 000000000000..706ab6407ce8 --- /dev/null +++ b/.github/shared/test/simple-git.test.js @@ -0,0 +1,20 @@ +import { mkdtemp, rm } from "fs/promises"; +import os from "os"; +import path from "path"; +import { describe, expect, it } from "vitest"; +import { getRootFolder } from "../src/simple-git.js"; + +describe("getRootFolder", () => { + it("resolves to repo root from a nested folder", async () => { + const testDir = __dirname; + const calculatedRoot = await getRootFolder(testDir); + const expectedRoot = path.resolve(path.join(__dirname, "..", "..", "..")); + expect(calculatedRoot).toBe(expectedRoot); + }); + + it("throws when directory is not a git repository", async () => { + const tempDir = await mkdtemp(path.join(os.tmpdir(), "non-git-")); + await expect(getRootFolder(tempDir)).rejects.toThrow(); + await rm(tempDir, { recursive: true, force: true }); + }); +}); diff --git a/.github/shared/test/sort.test.js b/.github/shared/test/sort.test.js new file mode 100644 index 000000000000..17db2445643a --- /dev/null +++ b/.github/shared/test/sort.test.js @@ -0,0 +1,35 @@ +// @ts-check + +import { describe, expect, it } from "vitest"; +import { byDate, invert } from "../src/sort.js"; + +describe("byDate", () => { + const input = [{ foo: "2025-01-01" }, { foo: "2023-01-01" }, { foo: "2024-01-01" }]; + + it("ascending by default", () => { + input.sort(byDate((s) => s.foo)); + + // Value `undefined` always sorts to the end + expect(input).toEqual([{ foo: "2023-01-01" }, { foo: "2024-01-01" }, { foo: "2025-01-01" }]); + }); + + it("descending with invert()", () => { + input.sort(invert(byDate((s) => s.foo))); + + // Value `undefined` always sorts to the end + expect(input).toEqual([{ foo: "2025-01-01" }, { foo: "2024-01-01" }, { foo: "2023-01-01" }]); + }); + + it.each([null, undefined, "invalid"])("invalid input: %s", (i) => { + /** @type {{foo: string | null | undefined}[]} */ + const input = [{ foo: "2025-01-01" }, { foo: "2024-01-01" }]; + const comparator = byDate((i) => i.foo); + + // Ensure base case doesn't throw + input.sort(comparator); + expect(input).toEqual([{ foo: "2024-01-01" }, { foo: "2025-01-01" }]); + + input[0].foo = i; + expect(() => input.sort(comparator)).toThrowError(`Unable to parse '${i}' to a valid date`); + }); +}); diff --git a/.github/shared/test/spec-model-error.test.js b/.github/shared/test/spec-model-error.test.js index 7639841bb204..3afe37a030bd 100644 --- a/.github/shared/test/spec-model-error.test.js +++ b/.github/shared/test/spec-model-error.test.js @@ -6,9 +6,7 @@ import { SpecModelError } from "../src/spec-model-error.js"; describe("SpecModelError", () => { it("toString`", () => { let error = new SpecModelError("test message"); - expect(error.toString()).toMatchInlineSnapshot( - `"SpecModelError: test message"`, - ); + expect(error.toString()).toMatchInlineSnapshot(`"SpecModelError: test message"`); error.source = "/test/source.json"; expect(error.toString()).toMatchInlineSnapshot(` diff --git a/.github/shared/test/spec-model.test.js b/.github/shared/test/spec-model.test.js index 9c400c0137b0..e159719055a9 100644 --- a/.github/shared/test/spec-model.test.js +++ b/.github/shared/test/spec-model.test.js @@ -1,5 +1,6 @@ // @ts-check +import { randomUUID } from "crypto"; import { readdir } from "fs/promises"; import { dirname, isAbsolute, join, resolve } from "path"; import { describe, expect, it } from "vitest"; @@ -15,9 +16,16 @@ describe("SpecModel", () => { const specModel = new SpecModel("foo"); expect(specModel.folder).toBe(resolve("foo")); - await expect(specModel.getReadmes()).rejects.toThrowError( - /no such file or directory/i, - ); + await expect(specModel.getReadmes()).rejects.toThrowError(/no such file or directory/i); + }); + + it("returns cached spec model", async () => { + const path = randomUUID(); + + const specModel1 = new SpecModel(path); + const specModel2 = new SpecModel(path); + + expect(specModel1).toBe(specModel2); }); it("returns spec model", async () => { @@ -58,10 +66,7 @@ describe("SpecModel", () => { expect(inputFiles0.length).toBe(1); expect(inputFiles0[0].toString()).toContain("Swagger"); expect(inputFiles0[0].path).toBe( - resolve( - folder, - "Microsoft.Contoso/preview/2021-10-01-preview/contoso.json", - ), + resolve(folder, "Microsoft.Contoso/preview/2021-10-01-preview/contoso.json"), ); expect(inputFiles0[0].tag).toBe(tags[0]); @@ -98,16 +103,11 @@ describe("SpecModel", () => { }); const readmePathRelative = jsonRefsRelative.readmes[0].path; expect(isAbsolute(readmePathRelative)).toBe(false); - expect( - jsonRefsRelative.readmes[0].tags[0].inputFiles[0].refs, - ).toBeDefined(); + expect(jsonRefsRelative.readmes[0].tags[0].inputFiles[0].refs).toBeDefined(); }); it("uses strings for tag names and doesn't parse Date object", async () => { - const folder = resolve( - __dirname, - "fixtures/getSpecModel/specification/yaml-date-parsing", - ); + const folder = resolve(__dirname, "fixtures/getSpecModel/specification/yaml-date-parsing"); const specModel = new SpecModel(folder, options); @@ -117,7 +117,7 @@ describe("SpecModel", () => { const tag = globalConfig["tag"]; - // @ts-ignore + // @ts-expect-error testing runtime behavior of invalid types expect(tag).not.toBeTypeOf(Date); expect(tag).toBeTypeOf("string"); @@ -154,8 +154,7 @@ describe("SpecModel", () => { "resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso.json", ); - const affectedReadmeTags = - await specModel.getAffectedReadmeTags(swaggerPath); + const affectedReadmeTags = await specModel.getAffectedReadmeTags(swaggerPath); expect(affectedReadmeTags.size).toBe(1); @@ -169,17 +168,13 @@ describe("SpecModel", () => { }); it("returns affected readme tags for multiple tags", async () => { - const folder = resolve( - __dirname, - "fixtures/getAffectedSwaggers/specification/1", - ); + const folder = resolve(__dirname, "fixtures/getAffectedSwaggers/specification/1"); const specModel = new SpecModel(folder, options); const swaggerPath = resolve(folder, "data-plane/shared/shared.json"); - const affectedReadmeTags = - await specModel.getAffectedReadmeTags(swaggerPath); + const affectedReadmeTags = await specModel.getAffectedReadmeTags(swaggerPath); expect(affectedReadmeTags.size).toBe(1); @@ -219,23 +214,16 @@ describe("SpecModel", () => { }); describe("getAffectedSwaggers", async () => { - const folder = resolve( - __dirname, - "fixtures/getAffectedSwaggers/specification/1", - ); + const folder = resolve(__dirname, "fixtures/getAffectedSwaggers/specification/1"); const specModel = new SpecModel(folder, options); it("returns directly referenced swagger", async () => { const swaggerPath = resolve(folder, "data-plane/a.json"); - const actual = [ - ...(await specModel.getAffectedSwaggers(swaggerPath)).keys(), - ].sort(); + const actual = [...(await specModel.getAffectedSwaggers(swaggerPath)).keys()].sort(); - const expected = ["data-plane/a.json"] - .map((p) => resolve(folder, p)) - .sort(); + const expected = ["data-plane/a.json"].map((p) => resolve(folder, p)).sort(); expect(actual).toEqual(expected); }); @@ -243,17 +231,15 @@ describe("SpecModel", () => { it("throws when swagger file is not found", async () => { const swaggerPath = resolve(folder, "data-plane/not-found.json"); - await expect( - specModel.getAffectedSwaggers(swaggerPath), - ).rejects.toThrowError(/no affected swaggers/i); + await expect(specModel.getAffectedSwaggers(swaggerPath)).rejects.toThrowError( + /no affected swaggers/i, + ); }); it("returns correct swaggers for one layer of dependencies", async () => { const swaggerPath = resolve(folder, "data-plane/nesting/b.json"); - const actual = [ - ...(await specModel.getAffectedSwaggers(swaggerPath)).keys(), - ].sort(); + const actual = [...(await specModel.getAffectedSwaggers(swaggerPath)).keys()].sort(); const expected = ["data-plane/a.json", "data-plane/nesting/b.json"] .map((p) => resolve(folder, p)) @@ -265,15 +251,9 @@ describe("SpecModel", () => { it("returns correct swaggers for two layers of dependencies", async () => { const swaggerPath = resolve(folder, "data-plane/c.json"); - const actual = [ - ...(await specModel.getAffectedSwaggers(swaggerPath)).keys(), - ].sort(); + const actual = [...(await specModel.getAffectedSwaggers(swaggerPath)).keys()].sort(); - const expected = [ - "data-plane/a.json", - "data-plane/nesting/b.json", - "data-plane/c.json", - ] + const expected = ["data-plane/a.json", "data-plane/nesting/b.json", "data-plane/c.json"] .map((p) => resolve(folder, p)) .sort(); @@ -283,9 +263,7 @@ describe("SpecModel", () => { it("returns correct swaggers for three layers of dependencies", async () => { const swaggerPath = resolve(folder, "data-plane/d.json"); - const actual = [ - ...(await specModel.getAffectedSwaggers(swaggerPath)).keys(), - ].sort(); + const actual = [...(await specModel.getAffectedSwaggers(swaggerPath)).keys()].sort(); const expected = [ "data-plane/a.json", @@ -302,9 +280,7 @@ describe("SpecModel", () => { it("returns correctly for multiple shared dependencies", async () => { const swaggerPath = resolve(folder, "data-plane/shared/shared.json"); - const actual = [ - ...(await specModel.getAffectedSwaggers(swaggerPath)).keys(), - ].sort(); + const actual = [...(await specModel.getAffectedSwaggers(swaggerPath)).keys()].sort(); const expected = [ "data-plane/a.json", @@ -322,18 +298,6 @@ describe("SpecModel", () => { }); }); -describe("getReadme regex", () => { - it.each([ - ["```yaml $(package-A-tag) == 'package-A-[[Version]]'", false], - ["``` yaml $(tag)=='package-2017-03' && $(go)", true], - ["``` yaml $(csharp) && $(tag) == 'release_4_0'", true], - ["``` yaml $(tag) == 'package-2021-12-01-preview'", true], // Typical case - ])("ignores tags that don't match the regex: %s", (example, expected) => { - const regex = /yaml.*\$\(tag\) ?== ?'([^']*)'/; - expect(regex.test(example)).toEqual(expected); - }); -}); - // TODO: Update tests for new object-oriented API // Stress test the parser against all specs in the specification/ folder. This @@ -400,3 +364,99 @@ describe.skip("Parse readmes", () => { }, ); }); + +describe("getSwaggers", () => { + it("should return all swagger files from tags", async () => { + const folder = resolve( + __dirname, + "fixtures/getSpecModel/specification/contosowidgetmanager/resource-manager", + ); + + const specModel = new SpecModel(folder, options); + const swaggers = await specModel.getSwaggers(); + + expect(swaggers.length).toBeGreaterThan(0); + + // Verify that all returned items are Swagger instances + expect(swaggers.every((s) => s.constructor.name === "Swagger")).toBe(true); + + // Verify that swagger files have the expected properties + const swagger = swaggers[0]; + expect(swagger.path).toBeDefined(); + expect(swagger.versionKind).toBeDefined(); + }); + + it("should return swaggers from multiple readmes and tags", async () => { + // Using a fixture that has multiple readme files + const folder = resolve( + __dirname, + "fixtures/getSpecModel/specification/contosowidgetmanager/resource-manager", + ); + const specModel = new SpecModel(folder, options); + + const swaggers = await specModel.getSwaggers(); + + // Should find swaggers from all readmes + expect(swaggers.length).toBeGreaterThan(0); + + // Each swagger should have a valid path + swaggers.forEach((swagger) => { + expect(swagger.path).toBeDefined(); + expect(typeof swagger.path).toBe("string"); + expect(swagger.path.length).toBeGreaterThan(0); + }); + }); + + it("should handle empty directories gracefully", async () => { + // Test with a minimal or empty spec model + const tempFolder = resolve( + __dirname, + "fixtures/getSpecModel/specification/contosowidgetmanager/resource-manager", + ); + const specModel = new SpecModel(tempFolder, options); + + const swaggers = await specModel.getSwaggers(); + + // Should return an array even if empty + expect(Array.isArray(swaggers)).toBe(true); + }); + + it("should preserve tag relationships", async () => { + const folder = resolve( + __dirname, + "fixtures/getSpecModel/specification/contosowidgetmanager/resource-manager", + ); + + const specModel = new SpecModel(folder, options); + const swaggers = await specModel.getSwaggers(); + + // Each swagger should have a tag reference + swaggers.forEach((swagger) => { + expect(swagger.tag).toBeDefined(); + if (swagger.tag) { + expect(swagger.tag.name).toBeDefined(); + expect(typeof swagger.tag.name).toBe("string"); + } + }); + }); + + it("should work with swagger fixtures", async () => { + const folder = resolve( + __dirname, + "fixtures/swagger/specification/servicelinker/resource-manager", + ); + + const specModel = new SpecModel(folder, options); + const swaggers = await specModel.getSwaggers(); + // Should return an array (may be empty if no valid readmes in this fixture) + expect(swaggers.length).toBe(9); + + // If swaggers are found, they should have the expected structure + for (const swagger of swaggers) { + expect(swagger.path).toBeDefined(); + expect(swagger.versionKind).toBeDefined(); + } + expect(swaggers[0].path).contains(folder); + expect(swaggers[0].versionKind).toBe("stable"); + }); +}); diff --git a/.github/shared/test/swagger.test.js b/.github/shared/test/swagger.test.js index b946d74ac885..abca25fe9ffb 100644 --- a/.github/shared/test/swagger.test.js +++ b/.github/shared/test/swagger.test.js @@ -1,12 +1,15 @@ // @ts-check -import { dirname, resolve } from "path"; +import { dirname, join, resolve } from "path"; import { describe, expect, it } from "vitest"; import { Swagger } from "../src/swagger.js"; import { fileURLToPath } from "url"; +import { ConsoleLogger } from "../src/logger.js"; import { Readme } from "../src/readme.js"; +import { SpecModel } from "../src/spec-model.js"; import { Tag } from "../src/tag.js"; + const __dirname = dirname(fileURLToPath(import.meta.url)); describe("Swagger", () => { @@ -15,9 +18,7 @@ describe("Swagger", () => { expect(swagger.path).toBe(resolve("bar")); expect(swagger.tag).toBeUndefined(); - await expect(swagger.getRefs()).rejects.toThrowError( - /Failed to resolve file for swagger/i, - ); + await expect(swagger.getRefs()).rejects.toThrowError(/Failed to resolve file for swagger/i); }); it("resolves path against Tag.readme", async () => { @@ -31,14 +32,12 @@ describe("Swagger", () => { // TODO: Test that path is resolved against backpointer it("excludes example files", async () => { - const swagger = new Swagger( - resolve(__dirname, "fixtures/Swagger/ignoreExamples/swagger.json"), - ); + const swagger = new Swagger(resolve(__dirname, "fixtures/swagger/ignoreExamples/swagger.json")); const refs = await swagger.getRefs(); const expectedIncludedPath = resolve( __dirname, - "fixtures/Swagger/ignoreExamples/included.json", + "fixtures/swagger/ignoreExamples/included.json", ); expect(refs).toMatchObject( new Map([ @@ -51,4 +50,74 @@ describe("Swagger", () => { ]), ); }); + + it("returns examples", async () => { + const swagger = new Swagger(resolve(__dirname, "fixtures/swagger/ignoreExamples/swagger.json")); + const examples = await swagger.getExamples(); + + const expectedExamplePath = resolve( + __dirname, + "fixtures/swagger/ignoreExamples/examples/example.json", + ); + expect(examples).toMatchObject( + new Map([ + [ + expectedExamplePath, + expect.objectContaining({ + path: expect.stringContaining(expectedExamplePath), + }), + ], + ]), + ); + }); + + describe("getOperations", () => { + it("should return normal operations", async () => { + const testFixturePath = join(__dirname, "fixtures", "swagger", "specification"); + const targetPath = join( + testFixturePath, + "servicelinker/resource-manager/Microsoft.ServiceLinker/stable/2024-04-01/test.json", + ); + const specModel = new SpecModel(testFixturePath, { + logger: new ConsoleLogger(/*debug*/ true), + }); + const result = await specModel.getSwaggers(); + const swagger = result.find((s) => s.path === targetPath); + + if (!swagger) throw new Error("Swagger not found for the given path"); + const operationsMap = await swagger.getOperations(); + expect(operationsMap.size).toBe(3); + + let expectedApiPath = + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/dryruns/{dryrunName}"; + + // Test specific operations by ID + const createDryrun = operationsMap.get("Connector_CreateDryrun"); + const getDryrun = operationsMap.get("Connector_GetDryrun"); + const listDryruns = operationsMap.get("Connector_ListDryrun"); + + expect(createDryrun).toBeDefined(); + if (createDryrun) { + expect(createDryrun.id).toBe("Connector_CreateDryrun"); + expect(createDryrun.httpMethod).toBe("PUT"); + expect(createDryrun.path).toBe(expectedApiPath); + } + + expect(getDryrun).toBeDefined(); + if (getDryrun) { + expect(getDryrun.id).toBe("Connector_GetDryrun"); + expect(getDryrun.httpMethod).toBe("GET"); + expect(getDryrun.path).toBe(expectedApiPath); + } + + expectedApiPath = + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.ServiceLinker/locations/{location}/dryruns"; + expect(listDryruns).toBeDefined(); + if (listDryruns) { + expect(listDryruns.id).toBe("Connector_ListDryrun"); + expect(listDryruns.httpMethod).toBe("GET"); + expect(listDryruns.path).toBe(expectedApiPath); + } + }); + }); }); diff --git a/.github/shared/test/tag.test.js b/.github/shared/test/tag.test.js index 0e5b5b0a4cef..ec1d02b363c6 100644 --- a/.github/shared/test/tag.test.js +++ b/.github/shared/test/tag.test.js @@ -16,8 +16,6 @@ describe("Tag", () => { const swagger = [...tag.inputFiles.values()][0]; expect(swagger.path).toBe(resolve("swagger")); - await expect(swagger.getRefs()).rejects.toThrowError( - /Failed to resolve file for swagger/i, - ); + await expect(swagger.getRefs()).rejects.toThrowError(/Failed to resolve file for swagger/i); }); }); diff --git a/.github/shared/tsconfig.json b/.github/shared/tsconfig.json index 429bb29460da..fd8d4c43ea86 100644 --- a/.github/shared/tsconfig.json +++ b/.github/shared/tsconfig.json @@ -10,6 +10,6 @@ }, "include": [ // Only check runtime sources. Tests currently have too many errors. - "**/src/**/*.js" - ] + "**/src/**/*.js", + ], } diff --git a/.github/tsconfig.json b/.github/tsconfig.json index 429bb29460da..fd8d4c43ea86 100644 --- a/.github/tsconfig.json +++ b/.github/tsconfig.json @@ -10,6 +10,6 @@ }, "include": [ // Only check runtime sources. Tests currently have too many errors. - "**/src/**/*.js" - ] + "**/src/**/*.js", + ], } diff --git a/.github/workflows/SDK-Suppressions-Label.yaml b/.github/workflows/SDK-Suppressions-Label.yaml index 88f5511d1c6f..920b5bffe4cf 100644 --- a/.github/workflows/SDK-Suppressions-Label.yaml +++ b/.github/workflows/SDK-Suppressions-Label.yaml @@ -45,20 +45,20 @@ jobs: run: | node eng/tools/sdk-suppressions/cmd/sdk-suppressions-label.js HEAD^ HEAD "$GITHUB_PULL_REQUEST_LABELS" - OUTPUT=$(cat $OUTPUT_FILE) + OUTPUT=$(cat "$OUTPUT_FILE") echo "Script output labels: $OUTPUT" labelsToAdd=$(echo "$OUTPUT" | sed -n 's/.*"labelsToAdd":\[\([^]]*\)\].*/\1/p' | tr -d '" ') labelsToRemove=$(echo "$OUTPUT" | sed -n 's/.*"labelsToRemove":\[\([^]]*\)\].*/\1/p' | tr -d '" ') - for label in $(echo $labelsToAdd | tr ',' '\n'); do + for label in $(echo "$labelsToAdd" | tr ',' '\n'); do echo "Label to add: $label" - echo "$label=true" >> $GITHUB_OUTPUT + echo "$label=true" >> "$GITHUB_OUTPUT" done - for label in $(echo $labelsToRemove | tr ',' '\n'); do + for label in $(echo "$labelsToRemove" | tr ',' '\n'); do echo "Label to remove: $label" - echo "$label=false" >> $GITHUB_OUTPUT + echo "$label=false" >> "$GITHUB_OUTPUT" done # No Action or Add/Remove label ​​according to step run-suppressions-script output diff --git a/.github/workflows/_reusable-eng-tools-test.yaml b/.github/workflows/_reusable-eng-tools-test.yaml index c87bd60c1dad..02c135d7b1ec 100644 --- a/.github/workflows/_reusable-eng-tools-test.yaml +++ b/.github/workflows/_reusable-eng-tools-test.yaml @@ -15,10 +15,6 @@ on: description: Run 'npm run lint' if true required: false type: boolean - prettier: - description: Run 'npm run prettier' if true - required: false - type: boolean permissions: contents: read @@ -54,21 +50,24 @@ jobs: with: node-version: ${{ matrix.node-version }}.x - - run: npm run build + - name: Build + run: npm run build shell: pwsh working-directory: ./eng/tools/${{ inputs.package }} - - run: npm run lint + - name: Lint + run: npm run lint if: inputs.lint == true shell: pwsh working-directory: ./eng/tools/${{ inputs.package }} - - run: npm run prettier - if: inputs.prettier == true + - name: Check Formatting + run: npm run format:check:ci shell: pwsh working-directory: ./eng/tools/${{ inputs.package }} - - run: npm run test:ci + - name: Test + run: npm run test:ci shell: pwsh working-directory: ./eng/tools/${{ inputs.package }} diff --git a/.github/workflows/_reusable-set-check-status.yml b/.github/workflows/_reusable-set-check-status.yaml similarity index 79% rename from .github/workflows/_reusable-set-check-status.yml rename to .github/workflows/_reusable-set-check-status.yaml index 5b0af4d972ea..9798dce2a0a2 100644 --- a/.github/workflows/_reusable-set-check-status.yml +++ b/.github/workflows/_reusable-set-check-status.yaml @@ -13,7 +13,7 @@ on: required: true type: string overriding_label: - description: Name of the label that, when set, causes the check to always pass + description: Comma-separated list of labels that, when any is set, causes the check to always pass required: true type: string @@ -35,7 +35,7 @@ jobs: github.event.action == 'synchronize' || github.event.action == 'reopened') || ((github.event.action == 'labeled' || github.event.action == 'unlabeled') && - (github.event.label.name == inputs.overriding_label)))) + inputs.overriding_label && contains(inputs.overriding_label, github.event.label.name)))) runs-on: ubuntu-24.04 @@ -56,13 +56,21 @@ jobs: - name: "Set Status" uses: actions/github-script@v7 + id: set-status with: script: | const { default: setStatus } = await import('${{ github.workspace }}/.github/workflows/src/set-status.js'); return await setStatus( - { github, context, core }, + { github, context, core }, '${{ inputs.monitored_workflow_name }}', '${{ inputs.required_check_name }}', '${{ inputs.overriding_label }}' ); + + - if: ${{ always() && steps.set-status.outputs.issue_number }} + name: Upload artifact with issue number + uses: ./.github/actions/add-empty-artifact + with: + name: issue-number + value: ${{ steps.set-status.outputs.issue_number }} diff --git a/.github/workflows/_reusable-verify-run-status.yaml b/.github/workflows/_reusable-verify-run-status.yaml index 4a1dba02cdf7..1c76ce45721f 100644 --- a/.github/workflows/_reusable-verify-run-status.yaml +++ b/.github/workflows/_reusable-verify-run-status.yaml @@ -8,14 +8,18 @@ on: description: Name of the check run to verify required: true type: string + commit_status_name: + description: Name of the commit status to verify + type: string workflow_name: description: Name of the workflow to verify - required: true type: string permissions: + actions: read checks: read contents: read + statuses: read jobs: check-run-status: @@ -27,7 +31,6 @@ jobs: (github.event_name == 'check_suite' && github.event.check_suite.app.name == 'openapi-pipeline-app') || (github.event_name == 'check_run' && github.event.check_run.name == inputs.check_run_name) runs-on: ubuntu-24.04 - steps: - uses: actions/checkout@v4 with: @@ -42,4 +45,5 @@ jobs: return await verifyRunStatus({ github, context, core }); env: CHECK_RUN_NAME: ${{ inputs.check_run_name }} + COMMIT_STATUS_NAME: ${{ inputs.commit_status_name }} WORKFLOW_NAME: ${{ inputs.workflow_name }} diff --git a/.github/workflows/arm-auto-signoff.yaml b/.github/workflows/arm-auto-signoff.yaml index 0f3cfcbc1032..ca099c309749 100644 --- a/.github/workflows/arm-auto-signoff.yaml +++ b/.github/workflows/arm-auto-signoff.yaml @@ -16,25 +16,6 @@ on: workflow_run: workflows: ["ARM Incremental TypeSpec"] types: [completed] - # For manual testing - workflow_dispatch: - inputs: - owner: - description: The account owner of the repository. The name is not case sensitive. - required: true - type: string - repo: - description: The name of the repository without the .git extension. The name is not case sensitive. - required: true - type: string - issue_number: - description: The number of the pull request. - required: true - type: string - head_sha: - description: The SHA of the commit. - required: true - type: string permissions: actions: read @@ -51,7 +32,6 @@ jobs: # issue_comment:edited - filter to only PR comments containing "next steps to merge", # a signal that checks like "Swagger LintDiff" or "Swagger Avocado" status may have changed if: | - github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_run' || (github.event_name == 'pull_request_target' && (github.event.action == 'labeled' || @@ -96,11 +76,6 @@ jobs: const { default: getLabelAction } = await import('${{ github.workspace }}/.github/workflows/src/arm-auto-signoff.js'); return await getLabelAction({ github, context, core }); - env: - OWNER: ${{ inputs.owner }} - REPO: ${{ inputs.repo }} - ISSUE_NUMBER: ${{ inputs.issue_number }} - HEAD_SHA: ${{ inputs.head_sha }} - if: | fromJson(steps.get-label-action.outputs.result).labelAction == 'add' || diff --git a/.github/workflows/arm-incremental-typespec.yaml b/.github/workflows/arm-incremental-typespec.yaml index 6a1650308b34..a0055862fc99 100644 --- a/.github/workflows/arm-incremental-typespec.yaml +++ b/.github/workflows/arm-incremental-typespec.yaml @@ -34,7 +34,7 @@ jobs: # actions/github-script@v7 uses Node 20 node-version: 20.x # "--no-audit": improves performance - # "--omit dev": not needed at dev time, improves performance + # "--omit dev": not needed at runtime, improves performance install-command: "npm ci --no-audit --omit dev" working-directory: ./.github diff --git a/.github/workflows/avocado-code.yaml b/.github/workflows/avocado-code.yaml new file mode 100644 index 000000000000..b46dbf169349 --- /dev/null +++ b/.github/workflows/avocado-code.yaml @@ -0,0 +1,92 @@ +name: "Swagger Avocado - Analyze Code" + +on: pull_request + +permissions: + contents: read + +jobs: + avocado-code: + name: "Swagger Avocado - Analyze Code" + + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v4 + with: + # Must include all branches, for git branch logic in Avocado to work correctly + fetch-depth: 0 + + - name: Setup Node and install deps + uses: ./.github/actions/setup-node-install-deps + + - name: Run Avocado + id: run-avocado + run: | + set -x + + AVOCADO_OUTPUT_FILE=$RUNNER_TEMP/avocado.ndjson + + echo "output-file=$AVOCADO_OUTPUT_FILE" >> "$GITHUB_OUTPUT" + + npm exec --no -- avocado \ + --excludePaths \ + "/common-types/" \ + "/scenarios/" \ + "/package.json" \ + "/package-lock.json" \ + "/cadl/examples/" \ + '(?=/examples/)(?!(?:/stable/|/preview/))' \ + "/\\.github/" \ + "/eng/" \ + --includePaths \ + "data-plane" \ + "resource-manager" \ + --file \ + "$AVOCADO_OUTPUT_FILE" + + TIME=$(node -p 'JSON.stringify(new Date())') + + # Avocado doesn't write any output if it was successful, so we add some to simplify later processing + [[ -e $AVOCADO_OUTPUT_FILE ]] || \ + echo "{\"type\":\"Raw\",\"level\":\"Info\",\"message\":\"success\",\"time\":$TIME}" > "$AVOCADO_OUTPUT_FILE" + env: + # Tells Avocado to analyze the files changed between the PR head (default checkout) + # and the PR base branch. + SYSTEM_PULLREQUEST_TARGETBRANCH: ${{ github.event.pull_request.base.ref }} + # Avocado hardcodes these env vars to get repo and SHA + TRAVIS_REPO_SLUG: ${{ github.repository }} + TRAVIS_PULL_REQUEST_SHA: ${{ github.sha }} + + - name: Setup Node 20 and install deps (under .github) + if: ${{ always() && (steps.run-avocado.outputs.output-file) }} + uses: ./.github/actions/setup-node-install-deps + with: + # actions/github-script@v7 uses Node 20 + node-version: 20.x + # "--no-audit": improves performance + # "--omit dev": not needed at runtime, improves performance + install-command: "npm ci --no-audit --omit dev" + working-directory: ./.github + + - name: Generate job summary + if: ${{ always() && (steps.run-avocado.outputs.output-file) }} + id: generate-job-summary + uses: actions/github-script@v7 + with: + script: | + const { default: generateJobSummary } = + await import('${{ github.workspace }}/.github/workflows/src/avocado-code.js'); + return await generateJobSummary({ core }); + env: + AVOCADO_OUTPUT_FILE: ${{ steps.run-avocado.outputs.output-file }} + + # Used by other workflows like set-status + - name: Set job-summary artifact + if: ${{ always() && steps.generate-job-summary.outputs.summary }} + uses: actions/upload-artifact@v4 + with: + name: job-summary + path: ${{ steps.generate-job-summary.outputs.summary }} + # If the file doesn't exist, just don't add the artifact + if-no-files-found: ignore diff --git a/.github/workflows/avocado-code.yml b/.github/workflows/avocado-code.yml deleted file mode 100644 index 30d4f42cb35e..000000000000 --- a/.github/workflows/avocado-code.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: "[TEST-IGNORE] Swagger Avocado - Analyze Code" - -on: pull_request - -permissions: - contents: read - -jobs: - avocado-code: - name: "[TEST-IGNORE] Swagger Avocado - Analyze Code" - - runs-on: ubuntu-24.04 - - steps: - - uses: actions/checkout@v4 - with: - # Must include all branches, for git branch logic in Avocado to work correctly - fetch-depth: 0 - - - name: Setup Node and install deps - uses: ./.github/actions/setup-node-install-deps - - - name: Run Avocado - run: | - npm exec --no -- avocado \ - --excludePaths \ - "/common-types/" \ - "/scenarios/" \ - "/package.json" \ - "/package-lock.json" \ - "/cadl/examples/" \ - --includePaths \ - "data-plane" \ - "resource-manager" - env: - # Tells Avocado to analyze the files changed between the PR head (default checkout) - # and the PR base branch. - SYSTEM_PULLREQUEST_TARGETBRANCH: ${{ github.event.pull_request.base.ref }} diff --git a/.github/workflows/avocado-status.yaml b/.github/workflows/avocado-status.yaml index 6a0d00913f38..80942c1c2544 100644 --- a/.github/workflows/avocado-status.yaml +++ b/.github/workflows/avocado-status.yaml @@ -1,4 +1,4 @@ -name: "[TEST-IGNORE] Swagger Avocado - Set Status" +name: "Swagger Avocado - Set Status" on: # Must run on pull_request_target instead of pull_request, since the latter cannot trigger on @@ -15,7 +15,7 @@ on: - labeled - unlabeled workflow_run: - workflows: ["\\[TEST-IGNORE\\] Swagger Avocado - Analyze Code"] + workflows: ["Swagger Avocado - Analyze Code"] types: [completed] permissions: @@ -28,8 +28,8 @@ permissions: jobs: avocado-status: name: Set Avocado Status - uses: ./.github/workflows/_reusable-set-check-status.yml + uses: ./.github/workflows/_reusable-set-check-status.yaml with: - monitored_workflow_name: "[TEST-IGNORE] Swagger Avocado - Analyze Code" - required_check_name: "[TEST-IGNORE] Swagger Avocado" + monitored_workflow_name: "Swagger Avocado - Analyze Code" + required_check_name: "Swagger Avocado" overriding_label: "Approved-Avocado" diff --git a/.github/workflows/breaking-change-code.yaml b/.github/workflows/breaking-change-code.yaml new file mode 100644 index 000000000000..d3fe1d9b5522 --- /dev/null +++ b/.github/workflows/breaking-change-code.yaml @@ -0,0 +1,70 @@ +name: "[TEST-IGNORE] Swagger BreakingChange - Analyze Code" + +on: pull_request + +permissions: + contents: read + +jobs: + validateBreakingChange: + name: "[TEST-IGNORE] Swagger BreakingChange - Analyze Code" + runs-on: ubuntu-24.04 + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node and install deps + uses: ./.github/actions/setup-node-install-deps + + - name: Setup .NET 6 SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "6.0.x" + + - name: Swagger Breaking Change - Analyze Code + id: swagger-breaking-change-analyze-code + run: | + npm exec --no -- openapi-diff-runner \ + --spec-repo-path "$GITHUB_WORKSPACE" \ + --pr-source-branch "$GITHUB_HEAD_REF" \ + --pr-target-branch "$GITHUB_BASE_REF" \ + --head-commit "${{ github.event.pull_request.head.sha }}" \ + --pr-number "${{ github.event.pull_request.number }}" \ + --source-repo "${{ github.event.pull_request.head.repo.full_name }}" \ + --target-repo "$GITHUB_REPOSITORY" + + # Upload artifact for 'BreakingChangeReviewRequired' label + - if: | + always() && + (steps.swagger-breaking-change-analyze-code.outputs.breakingChangeReviewLabelName != '') + name: Upload artifact with BreakingChangeReviewRequiredLabel label + uses: ./.github/actions/add-label-artifact + with: + name: "${{ steps.swagger-breaking-change-analyze-code.outputs.breakingChangeReviewLabelName }}" + value: "${{ steps.swagger-breaking-change-analyze-code.outputs.breakingChangeReviewLabelValue == 'true' }}" + + # Upload artifact for 'VersioningReviewRequired' label + - if: | + always() && + (steps.swagger-breaking-change-analyze-code.outputs.versioningReviewLabelName != '') + name: Upload artifact with VersioningReviewRequiredLabel label + uses: ./.github/actions/add-label-artifact + with: + name: "${{ steps.swagger-breaking-change-analyze-code.outputs.versioningReviewLabelName }}" + # Convert "add/remove" to "true/false" + value: "${{ steps.swagger-breaking-change-analyze-code.outputs.versioningReviewLabelValue == 'true' }}" + + # Upload artifact with issue number if labels are present and PR number is valid + - if: | + always() && + (steps.swagger-breaking-change-analyze-code.outputs.breakingChangeReviewLabelName != '' || + steps.swagger-breaking-change-analyze-code.outputs.versioningReviewLabelName != '') && + github.event.pull_request.number > 0 + name: Upload artifact with issue number + uses: ./.github/actions/add-empty-artifact + with: + name: "issue-number" + value: "${{ github.event.pull_request.number }}" diff --git a/.github/workflows/breaking-change-cross-version-code.yaml b/.github/workflows/breaking-change-cross-version-code.yaml new file mode 100644 index 000000000000..3c44f1ad3184 --- /dev/null +++ b/.github/workflows/breaking-change-cross-version-code.yaml @@ -0,0 +1,70 @@ +name: "[TEST-IGNORE] Breaking Change(Cross-Version) - Analyze Code" + +on: pull_request + +permissions: + contents: read + +jobs: + validateBreakingChange: + name: "[TEST-IGNORE] Breaking Change(Cross-Version) - Analyze Code" + runs-on: ubuntu-24.04 + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node and install deps + uses: ./.github/actions/setup-node-install-deps + + - name: Setup .NET 6 SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "6.0.x" + + - name: Breaking Change(Cross-Version) - Analyze Code + id: breaking-change-cross-version-analyze-code + run: | + npm exec --no -- openapi-diff-runner \ + --run-type "CrossVersion" \ + --spec-repo-path "$GITHUB_WORKSPACE" \ + --pr-source-branch "$GITHUB_HEAD_REF" \ + --pr-target-branch "$GITHUB_BASE_REF" \ + --head-commit "${{ github.event.pull_request.head.sha }}" \ + --pr-number "${{ github.event.pull_request.number }}" \ + --source-repo "${{ github.event.pull_request.head.repo.full_name }}" \ + --target-repo "$GITHUB_REPOSITORY" + + # Upload artifact for 'BreakingChangeReviewRequired' label + - if: | + always() && + (steps.breaking-change-cross-version-analyze-code.outputs.breakingChangeReviewLabelName != '') + name: Upload artifact with BreakingChangeReviewRequiredLabel label + uses: ./.github/actions/add-label-artifact + with: + name: "${{ steps.breaking-change-cross-version-analyze-code.outputs.breakingChangeReviewLabelName }}" + value: "${{ steps.breaking-change-cross-version-analyze-code.outputs.breakingChangeReviewLabelValue == 'true' }}" + + # Upload artifact for 'VersioningReviewRequired' label + - if: | + always() && + (steps.breaking-change-cross-version-analyze-code.outputs.versioningReviewLabelName != '') + name: Upload artifact with VersioningReviewRequiredLabel label + uses: ./.github/actions/add-label-artifact + with: + name: "${{ steps.breaking-change-cross-version-analyze-code.outputs.versioningReviewLabelName }}" + value: "${{ steps.breaking-change-cross-version-analyze-code.outputs.versioningReviewLabelValue == 'true' }}" + + # Upload artifact with issue number if labels are present and PR number is valid + - if: | + always() && + (steps.breaking-change-cross-version-analyze-code.outputs.breakingChangeReviewLabelName != '' || + steps.breaking-change-cross-version-analyze-code.outputs.versioningReviewLabelName != '') && + github.event.pull_request.number > 0 + name: Upload artifact with issue number + uses: ./.github/actions/add-empty-artifact + with: + name: "issue-number" + value: "${{ github.event.pull_request.number }}" diff --git a/.github/workflows/breaking-change-cross-version-status.yaml b/.github/workflows/breaking-change-cross-version-status.yaml new file mode 100644 index 000000000000..fda112c1ef41 --- /dev/null +++ b/.github/workflows/breaking-change-cross-version-status.yaml @@ -0,0 +1,35 @@ +name: "[TEST-IGNORE] Breaking Change(Cross-Version) - Set Status" + +on: + # Must run on pull_request_target instead of pull_request, since the latter cannot trigger on + # labels from bot accounts in fork PRs. pull_request_target is also more similar to the other + # trigger "workflow_run" -- they are both privileged and run in the target branch and repo -- + # which simplifies implementation. + pull_request_target: + types: + # Run workflow on default types, to update status as quickly as possible. + - opened + - synchronize + - reopened + # Depends on labels, so must re-evaluate whenever a relevant label is manually added or removed. + - labeled + - unlabeled + workflow_run: + workflows: ["\\[TEST-IGNORE\\] Breaking Change(Cross-Version) - Analyze Code"] + types: [completed] + +permissions: + actions: read + contents: read + issues: read + pull-requests: read + statuses: write + +jobs: + breaking-change-cross-version-status: + name: Set Breaking Change(Cross-Version) Status + uses: ./.github/workflows/_reusable-set-check-status.yaml + with: + monitored_workflow_name: "[TEST-IGNORE] Breaking Change(Cross-Version) - Analyze Code" + required_check_name: "[TEST-IGNORE] Breaking Change(Cross-Version)" + overriding_label: "BreakingChange-Approved-Benign,BreakingChange-Approved-BugFix,BreakingChange-Approved-UserImpact,BreakingChange-Approved-BranchPolicyException,BreakingChange-Approved-Previously,BreakingChange-Approved-Security,Versioning-Approved-Benign,Versioning-Approved-BugFix,Versioning-Approved-PrivatePreview,Versioning-Approved-BranchPolicyException,Versioning-Approved-Previously,Versioning-Approved-Retired" diff --git a/.github/workflows/breaking-change-status.yaml b/.github/workflows/breaking-change-status.yaml new file mode 100644 index 000000000000..3b3d77c4dcce --- /dev/null +++ b/.github/workflows/breaking-change-status.yaml @@ -0,0 +1,35 @@ +name: "[TEST-IGNORE] Swagger BreakingChange - Set Status" + +on: + # Must run on pull_request_target instead of pull_request, since the latter cannot trigger on + # labels from bot accounts in fork PRs. pull_request_target is also more similar to the other + # trigger "workflow_run" -- they are both privileged and run in the target branch and repo -- + # which simplifies implementation. + pull_request_target: + types: + # Run workflow on default types, to update status as quickly as possible. + - opened + - synchronize + - reopened + # Depends on labels, so must re-evaluate whenever a relevant label is manually added or removed. + - labeled + - unlabeled + workflow_run: + workflows: ["\\[TEST-IGNORE\\] Swagger BreakingChange - Analyze Code"] + types: [completed] + +permissions: + actions: read + contents: read + issues: read + pull-requests: read + statuses: write + +jobs: + breaking-change-status: + name: Set BreakingChange Status + uses: ./.github/workflows/_reusable-set-check-status.yaml + with: + monitored_workflow_name: "[TEST-IGNORE] Swagger BreakingChange - Analyze Code" + required_check_name: "[TEST-IGNORE] Swagger BreakingChange" + overriding_label: "BreakingChange-Approved-Benign,BreakingChange-Approved-BugFix,BreakingChange-Approved-UserImpact,BreakingChange-Approved-BranchPolicyException,BreakingChange-Approved-Previously,BreakingChange-Approved-Security,Versioning-Approved-Benign,Versioning-Approved-BugFix,Versioning-Approved-PrivatePreview,Versioning-Approved-BranchPolicyException,Versioning-Approved-Previously,Versioning-Approved-Retired" diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index c94984173eb7..40e9c913f851 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -13,17 +13,17 @@ name: "CodeQL Advanced" on: push: - branches: [ "main", "release-*" ] + branches: ["main", "release-*"] paths: - - .github/** - - eng/tools/** + - .github/** + - eng/tools/** pull_request: - branches: [ "main", "release-*" ] + branches: ["main", "release-*"] paths: - - .github/** - - eng/tools/** + - .github/** + - eng/tools/** schedule: - - cron: '27 4 * * 1' + - cron: "27 4 * * 1" jobs: analyze: @@ -49,21 +49,21 @@ jobs: fail-fast: false matrix: include: - - language: actions - config: | - paths: - - .github - sparse-checkout: | - .github - - language: javascript-typescript - build-mode: none - config: | - paths: - - .github - - eng/tools - sparse-checkout: | - .github - eng/tools + - language: actions + config: | + paths: + - .github + sparse-checkout: | + .github + - language: javascript-typescript + build-mode: none + config: | + paths: + - .github + - eng/tools + sparse-checkout: | + .github + eng/tools # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both @@ -74,48 +74,48 @@ jobs: # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - sparse-checkout: ${{ matrix.sparse-checkout }} + - name: Checkout repository + uses: actions/checkout@v4 + with: + sparse-checkout: ${{ matrix.sparse-checkout }} - # Add any setup steps before running the `github/codeql-action/init` action. - # This includes steps like installing compilers or runtimes (`actions/setup-node` - # or others). This is typically only required for manual builds. - # - name: Setup runtime (example) - # uses: actions/setup-example@v1 + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - config: ${{ matrix.config }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + config: ${{ matrix.config }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 000000000000..840384194b83 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,24 @@ +name: Copilot Setup Steps + +on: workflow_dispatch + +jobs: + copilot-setup-steps: + runs-on: ubuntu-latest + + permissions: + contents: read + checks: read + statuses: write + id-token: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Azure Login with Workload Identity Federation + uses: azure/login@v2 + with: + client-id: "936c56f0-298b-467f-b702-3ad5bf4b15c1" + tenant-id: "72f988bf-86f1-41af-91ab-2d7cd011db47" + allow-no-subscriptions: true diff --git a/.github/workflows/github-test.yaml b/.github/workflows/github-test.yaml index cced40a4adac..9cea7c2bc60a 100644 --- a/.github/workflows/github-test.yaml +++ b/.github/workflows/github-test.yaml @@ -34,6 +34,17 @@ jobs: sparse-checkout: | .github + # Content copied from https://raw.githubusercontent.com/rhysd/actionlint/2ab3a12c7848f6c15faca9a92612ef4261d0e370/.github/actionlint-matcher.json + - if: ${{ matrix.folder == '.github' && matrix.os == 'ubuntu'}} + name: Add ActionLint Problem Matcher + run: echo "::add-matcher::.github/matchers/actionlint.json" + + - if: ${{ matrix.folder == '.github' && matrix.os == 'ubuntu'}} + name: Lint workflows + uses: docker://rhysd/actionlint:1.7.7 + with: + args: -color -verbose + - if: ${{ matrix.folder == '.github' }} name: Setup Node 20 and install runtime deps uses: ./.github/actions/setup-node-install-deps @@ -63,7 +74,7 @@ jobs: - run: npm run lint - - run: npm run prettier + - run: npm run format:check:ci - run: npm run test:ci diff --git a/.github/workflows/lintdiff-code.yaml b/.github/workflows/lintdiff-code.yaml index 8d3110e50b90..11e0e9c187df 100644 --- a/.github/workflows/lintdiff-code.yaml +++ b/.github/workflows/lintdiff-code.yaml @@ -1,4 +1,4 @@ -name: "[TEST-IGNORE] Swagger LintDiff - Analyze Code" +name: "Swagger LintDiff - Analyze Code" on: pull_request @@ -7,7 +7,7 @@ permissions: jobs: lintdiff: - name: "[TEST-IGNORE] Swagger LintDiff - Analyze Code" + name: "Swagger LintDiff - Analyze Code" runs-on: ubuntu-24.04 steps: @@ -42,34 +42,39 @@ jobs: const { join } = await import('path'); // TODO: Logger - const changedFiles = await getChangedFiles({ cwd: 'after'}); + const changedFiles = await getChangedFiles({ cwd: 'after', paths: ['specification'] }); console.log('Changed files:', changedFiles); - + const filePath = join(process.cwd(), 'changed-files.txt'); writeFileSync(filePath, changedFiles.join('\n'), 'utf8'); console.log(`Changed files written to ${filePath}`); - - name: Prepend test notice to summary - if: always() - run: | - temp_file=$(mktemp) - echo -e "> [!IMPORTANT]\n> This check is testing a new version of 'Swagger LintDiff'.\n> Failures are expected, and should be completely ignored by spec authors and reviewers.\n> Meaningful results for this PR are are in required check 'Swagger LintDiff'.\n" > $GITHUB_STEP_SUMMARY - # TODO: Could be github.sha for merge commit - name: Run LintDiff + id: run-lintdiff run: | + set -x + + echo "summary=$GITHUB_STEP_SUMMARY" >> "$GITHUB_OUTPUT" + npm exec --no -- lint-diff \ --before before \ --after after \ --changed-files-path changed-files.txt \ --base-branch ${{ github.event.pull_request.base.ref }} \ --compare-sha ${{ github.event.pull_request.head.sha }} \ - --out-file $GITHUB_STEP_SUMMARY - - echo "⚠️ This check is testing a new version of 'Swagger LintDiff'." - echo "⚠️ Failures are expected, and should be completely ignored by spec authors and reviewers." - echo "⚠️ Meaningful results for this PR are are in required check 'Swagger LintDiff'." + --out-file "$GITHUB_STEP_SUMMARY" env: # Some LintDiff runs are memory intensive and require more than the # default. - NODE_OPTIONS: '--max-old-space-size=8192' + NODE_OPTIONS: "--max-old-space-size=8192" + + # Used by other workflows like set-status + - name: Set job-summary artifact + if: ${{ always() && steps.run-lintdiff.outputs.summary }} + uses: actions/upload-artifact@v4 + with: + name: job-summary + path: ${{ steps.run-lintdiff.outputs.summary }} + # If the file doesn't exist, just don't add the artifact + if-no-files-found: ignore diff --git a/.github/workflows/lintdiff-status.yaml b/.github/workflows/lintdiff-status.yaml index a7f651a7641a..a938b9c8d534 100644 --- a/.github/workflows/lintdiff-status.yaml +++ b/.github/workflows/lintdiff-status.yaml @@ -1,4 +1,4 @@ -name: "[TEST-IGNORE] Swagger LintDiff - Set Status" +name: "Swagger LintDiff - Set Status" on: # Must run on pull_request_target instead of pull_request, since the latter cannot trigger on @@ -15,7 +15,7 @@ on: - labeled - unlabeled workflow_run: - workflows: ["\\[TEST-IGNORE\\] Swagger LintDiff - Analyze Code"] + workflows: ["Swagger LintDiff - Analyze Code"] types: [completed] permissions: @@ -28,8 +28,8 @@ permissions: jobs: lintdiff-status: name: Set LintDiff Status - uses: ./.github/workflows/_reusable-set-check-status.yml + uses: ./.github/workflows/_reusable-set-check-status.yaml with: - monitored_workflow_name: "[TEST-IGNORE] Swagger LintDiff - Analyze Code" - required_check_name: "[TEST-IGNORE] Swagger LintDiff" + monitored_workflow_name: "Swagger LintDiff - Analyze Code" + required_check_name: "Swagger LintDiff" overriding_label: "Approved-LintDiff" diff --git a/.github/workflows/lintdiff-test.yaml b/.github/workflows/lintdiff-test.yaml index e7ca5f1380c3..fa0a09658b4a 100644 --- a/.github/workflows/lintdiff-test.yaml +++ b/.github/workflows/lintdiff-test.yaml @@ -32,4 +32,3 @@ jobs: with: package: lint-diff lint: false - prettier: false diff --git a/.github/workflows/oav-runner-tests.yaml b/.github/workflows/oav-runner-tests.yaml new file mode 100644 index 000000000000..21c02ef9d49e --- /dev/null +++ b/.github/workflows/oav-runner-tests.yaml @@ -0,0 +1,29 @@ +name: OAV Runner - Tests + +on: + push: + branches: + - main + pull_request: + paths: + - package-lock.json + - package.json + - tsconfig.json + - .github/shared + - .github/workflows/_reusable-eng-tools-test.yaml + - .github/workflows/oav-runner-test.yaml + - eng/tools/package.json + - eng/tools/tsconfig.json + - eng/tools/oav-runner/** + workflow_dispatch: + +permissions: + contents: read + +jobs: + oavrunnertests: + name: Check OAV Runner + uses: ./.github/workflows/_reusable-eng-tools-test.yaml + with: + package: oav-runner + lint: false diff --git a/.github/workflows/openapi-diff-runner-test.yaml b/.github/workflows/openapi-diff-runner-test.yaml new file mode 100644 index 000000000000..f78a78f72414 --- /dev/null +++ b/.github/workflows/openapi-diff-runner-test.yaml @@ -0,0 +1,28 @@ +name: openapi-diff-runner - Test + +on: + push: + branches: + - main + pull_request: + paths: + - package-lock.json + - package.json + - tsconfig.json + - .github/workflows/_reusable-eng-tools-test.yaml + - .github/workflows/openapi-diff-runner-test.yaml + - eng/tools/package.json + - eng/tools/tsconfig.json + - eng/tools/openapi-diff-runner/** + workflow_dispatch: + +permissions: + contents: read + +jobs: + openapiDiffRunner: + name: openapi-diff-runner + uses: ./.github/workflows/_reusable-eng-tools-test.yaml + with: + package: openapi-diff-runner + lint: false diff --git a/.github/workflows/post-apiview.yml b/.github/workflows/post-apiview.yaml similarity index 93% rename from .github/workflows/post-apiview.yml rename to .github/workflows/post-apiview.yaml index 84def2778802..1f08b8c4bf12 100644 --- a/.github/workflows/post-apiview.yml +++ b/.github/workflows/post-apiview.yaml @@ -3,7 +3,7 @@ name: After APIView on: check_run: types: [completed] - + permissions: pull-requests: write contents: read @@ -20,8 +20,8 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - sparse-checkout: 'eng/common' - + sparse-checkout: "eng/common" + - name: Create APIView Comment on PR run: | . "eng/common/scripts/Helpers/ApiView-Helpers.ps1" diff --git a/.github/workflows/sdk-breaking-change-labels.yaml b/.github/workflows/sdk-breaking-change-labels.yaml index 013a43ee8dab..cd8c226aa196 100644 --- a/.github/workflows/sdk-breaking-change-labels.yaml +++ b/.github/workflows/sdk-breaking-change-labels.yaml @@ -37,7 +37,7 @@ jobs: run: | # Get token for Azure DevOps resource ADO_TOKEN=$(az account get-access-token --resource "499b84ac-1321-427f-aa17-267ca6975798" --query "accessToken" -o tsv) - echo "ADO_TOKEN=$ADO_TOKEN" >> $GITHUB_ENV + echo "ADO_TOKEN=$ADO_TOKEN" >> "$GITHUB_ENV" - name: Get label and action id: get-label-and-action diff --git a/.github/workflows/spec-gen-sdk-status.yml b/.github/workflows/spec-gen-sdk-status.yml index cc409df325e7..e1bb91ab37ae 100644 --- a/.github/workflows/spec-gen-sdk-status.yml +++ b/.github/workflows/spec-gen-sdk-status.yml @@ -37,7 +37,7 @@ jobs: run: | # Get token for Azure DevOps resource ADO_TOKEN=$(az account get-access-token --resource "499b84ac-1321-427f-aa17-267ca6975798" --query "accessToken" -o tsv) - echo "ADO_TOKEN=$ADO_TOKEN" >> $GITHUB_ENV + echo "ADO_TOKEN=$ADO_TOKEN" >> "$GITHUB_ENV" - name: "SDK Validation Set Status" id: sdk-validation-status @@ -47,3 +47,10 @@ jobs: const setStatus = (await import('${{ github.workspace }}/.github/workflows/src/spec-gen-sdk-status.js')).default; return await setStatus({ github, context, core }); + + - if: ${{ always() && steps.sdk-validation-status.outputs.issue_number }} + name: Upload artifact with issue number + uses: ./.github/actions/add-empty-artifact + with: + name: issue-number + value: ${{ steps.sdk-validation-status.outputs.issue_number }} diff --git a/.github/workflows/src/arm-auto-signoff.js b/.github/workflows/src/arm-auto-signoff.js index c1c2a57c3914..05f825557223 100644 --- a/.github/workflows/src/arm-auto-signoff.js +++ b/.github/workflows/src/arm-auto-signoff.js @@ -8,22 +8,11 @@ import { LabelAction } from "./label.js"; // TODO: Add tests /* v8 ignore start */ /** - * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments + * @param {import('@actions/github-script').AsyncFunctionArguments} AsyncFunctionArguments * @returns {Promise<{labelAction: LabelAction, issueNumber: number}>} */ export default async function getLabelAction({ github, context, core }) { - let owner = process.env.OWNER || ""; - let repo = process.env.REPO || ""; - let issue_number = parseInt(process.env.ISSUE_NUMBER || ""); - let head_sha = process.env.HEAD_SHA || ""; - - if (!owner || !repo || !issue_number || !head_sha) { - let inputs = await extractInputs(github, context, core); - owner = owner || inputs.owner; - repo = repo || inputs.repo; - issue_number = issue_number || inputs.issue_number; - head_sha = head_sha || inputs.head_sha; - } + const { owner, repo, issue_number, head_sha } = await extractInputs(github, context, core); return await getLabelActionImpl({ owner, @@ -46,14 +35,7 @@ export default async function getLabelAction({ github, context, core }) { * @param {typeof import("@actions/core")} params.core * @returns {Promise<{labelAction: LabelAction, issueNumber: number}>} */ -export async function getLabelActionImpl({ - owner, - repo, - issue_number, - head_sha, - github, - core, -}) { +export async function getLabelActionImpl({ owner, repo, issue_number, head_sha, github, core }) { const labelActions = { [LabelAction.None]: { labelAction: LabelAction.None, @@ -86,16 +68,13 @@ export async function getLabelActionImpl({ core.info(`Labels: ${labelNames}`); - const workflowRuns = await github.paginate( - github.rest.actions.listWorkflowRunsForRepo, - { - owner, - repo, - event: "pull_request", - head_sha, - per_page: PER_PAGE_MAX, - }, - ); + const workflowRuns = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, { + owner, + repo, + event: "pull_request", + head_sha, + per_page: PER_PAGE_MAX, + }); core.info("Workflow Runs:"); workflowRuns.forEach((wf) => { @@ -106,10 +85,7 @@ export async function getLabelActionImpl({ const incrementalTspRuns = workflowRuns .filter((wf) => wf.name == wfName) // Sort by "updated_at" descending - .sort( - (a, b) => - new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(), - ); + .sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()); if (incrementalTspRuns.length == 0) { core.info( @@ -122,29 +98,22 @@ export async function getLabelActionImpl({ if (run.status == "completed") { if (run.conclusion != "success") { - core.info( - `Run for workflow '${wfName}' did not succeed: '${run.conclusion}'`, - ); + core.info(`Run for workflow '${wfName}' did not succeed: '${run.conclusion}'`); return removeAction; } - const artifacts = await github.paginate( - github.rest.actions.listWorkflowRunArtifacts, - { - owner, - repo, - run_id: run.id, - per_page: PER_PAGE_MAX, - }, - ); + const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, { + owner, + repo, + run_id: run.id, + per_page: PER_PAGE_MAX, + }); const artifactNames = artifacts.map((a) => a.name); core.info(`artifactNames: ${JSON.stringify(artifactNames)}`); if (artifactNames.includes("incremental-typespec=false")) { - core.info( - "Spec is not an incremental change to an existing TypeSpec RP", - ); + core.info("Spec is not an incremental change to an existing TypeSpec RP"); return removeAction; } else if (artifactNames.includes("incremental-typespec=true")) { core.info("Spec is an incremental change to an existing TypeSpec RP"); @@ -154,9 +123,7 @@ export async function getLabelActionImpl({ throw `Workflow artifacts did not contain 'incremental-typespec': ${JSON.stringify(artifactNames)}`; } } else { - core.info( - `Workflow '${wfName}' is still in-progress: status='${run.status}'`, - ); + core.info(`Workflow '${wfName}' is still in-progress: status='${run.status}'`); return labelActions[LabelAction.None]; } } @@ -190,9 +157,7 @@ export async function getLabelActionImpl({ const matchingRuns = checkRuns.filter((run) => run.name === checkName); if (matchingRuns.length > 1) { - throw new Error( - `Unexpected number of checks named '${checkName}': ${matchingRuns.length}`, - ); + throw new Error(`Unexpected number of checks named '${checkName}': ${matchingRuns.length}`); } const matchingRun = matchingRuns.length === 1 ? matchingRuns[0] : undefined; @@ -201,11 +166,7 @@ export async function getLabelActionImpl({ `${checkName}: Status='${matchingRun?.status}', Conclusion='${matchingRun?.conclusion}'`, ); - if ( - matchingRun && - matchingRun.status === "completed" && - matchingRun.conclusion !== "success" - ) { + if (matchingRun && matchingRun.status === "completed" && matchingRun.conclusion !== "success") { core.info(`Check '${checkName}' did not succeed`); return removeAction; } @@ -216,13 +177,8 @@ export async function getLabelActionImpl({ } if ( - setEquals( - new Set(requiredCheckRuns.map((run) => run.name)), - new Set(requiredCheckNames), - ) && - requiredCheckRuns.every( - (run) => run.status === "completed" && run.conclusion === "success", - ) + setEquals(new Set(requiredCheckRuns.map((run) => run.name)), new Set(requiredCheckNames)) && + requiredCheckRuns.every((run) => run.status === "completed" && run.conclusion === "success") ) { core.info("All requirements met for auto-signoff"); return labelActions[LabelAction.Add]; diff --git a/.github/workflows/src/arm-incremental-typespec.js b/.github/workflows/src/arm-incremental-typespec.js index 44f562134056..14cd9173090a 100644 --- a/.github/workflows/src/arm-incremental-typespec.js +++ b/.github/workflows/src/arm-incremental-typespec.js @@ -18,12 +18,13 @@ import { CoreLogger } from "./core-logger.js"; debug.enable("simple-git"); /** - * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments + * @param {import('@actions/github-script').AsyncFunctionArguments} AsyncFunctionArguments * @returns {Promise} */ export default async function incrementalTypeSpec({ core }) { const options = { cwd: process.env.GITHUB_WORKSPACE, + paths: ["specification"], logger: new CoreLogger(core), }; @@ -108,9 +109,7 @@ export default async function incrementalTypeSpec({ core }) { const changedSpecDirs = new Set([ ...changedRmFiles.filter(swagger).map((f) => dirname(dirname(dirname(f)))), - ...changedRmFiles - .filter(example) - .map((f) => dirname(dirname(dirname(dirname(f))))), + ...changedRmFiles.filter(example).map((f) => dirname(dirname(dirname(dirname(f))))), // Readme input files should use the same path format as changed swagger files ...[...changedReadmeInputFiles].map((f) => dirname(dirname(dirname(f)))), ]); @@ -164,8 +163,6 @@ export default async function incrementalTypeSpec({ core }) { } } - core.info( - "Appears to contain only incremental changes to existing TypeSpec RP(s)", - ); + core.info("Appears to contain only incremental changes to existing TypeSpec RP(s)"); return true; } diff --git a/.github/workflows/src/artifacts.js b/.github/workflows/src/artifacts.js index 97c678c6c675..035eb210f601 100644 --- a/.github/workflows/src/artifacts.js +++ b/.github/workflows/src/artifacts.js @@ -86,9 +86,7 @@ export async function getAzurePipelineArtifact({ const artifacts = /** @type {Artifacts} */ (await response.json()); core.info(`Artifacts found: ${JSON.stringify(artifacts)}`); if (!artifacts.resource || !artifacts.resource.downloadUrl) { - throw new Error( - `Download URL not found for the artifact ${artifactName}`, - ); + throw new Error(`Download URL not found for the artifact ${artifactName}`); } let downloadUrl = artifacts.resource.downloadUrl; @@ -110,16 +108,12 @@ export async function getAzurePipelineArtifact({ retryOptions, ); if (!artifactResponse.ok) { - throw new Error( - `Failed to fetch artifact: ${artifactResponse.statusText}`, - ); + throw new Error(`Failed to fetch artifact: ${artifactResponse.statusText}`); } artifactData = await artifactResponse.text(); } else { - core.error( - `Failed to fetch artifacts: ${response.status}, ${response.statusText}`, - ); + core.error(`Failed to fetch artifacts: ${response.status}, ${response.statusText}`); const errorText = await response.text(); core.error(`Error details: ${errorText}`); } @@ -137,9 +131,7 @@ export function getAdoBuildInfoFromUrl(buildUrl) { const buildUrlRegex = /^(.*?)(?=\/_build\/).*?[?&]buildId=(\d+)/; const match = buildUrl.match(buildUrlRegex); if (!match) { - throw new Error( - `Could not extract build ID or project URL from the URL: ${buildUrl}`, - ); + throw new Error(`Could not extract build ID or project URL from the URL: ${buildUrl}`); } return { projectUrl: match[1], buildId: match[2] }; } @@ -174,14 +166,10 @@ export async function fetchFailedArtifact({ retryOptions, ); if (!response.ok) { - throw new Error( - `Failed to fetch artifacts: ${response.status}, ${response.statusText}`, - ); + throw new Error(`Failed to fetch artifacts: ${response.status}, ${response.statusText}`); } /** @type {ListArtifactsResponse} */ - const listArtifactResponse = /** @type {ListArtifactsResponse} */ ( - await response.json() - ); + const listArtifactResponse = /** @type {ListArtifactsResponse} */ (await response.json()); core.info(`Artifacts found: ${JSON.stringify(listArtifactResponse)}`); // Use filter to get matching artifacts and sort them in descending alphabetical order const artifactsList = listArtifactResponse.value diff --git a/.github/workflows/src/avocado-code.js b/.github/workflows/src/avocado-code.js new file mode 100644 index 000000000000..395a3c0ad07d --- /dev/null +++ b/.github/workflows/src/avocado-code.js @@ -0,0 +1,58 @@ +// @ts-check + +import { readFile } from "fs/promises"; +import { + generateMarkdownTable, + MessageLevel, + MessageRecordSchema, + MessageType, +} from "./message.js"; +import { parse } from "./ndjson.js"; + +/** + * @param {import('@actions/github-script').AsyncFunctionArguments} AsyncFunctionArguments + * @returns {Promise} + */ +export default async function generateJobSummary({ core }) { + const avocadoOutputFile = process.env.AVOCADO_OUTPUT_FILE; + core.info(`avocadoOutputFile: ${avocadoOutputFile}`); + + if (!avocadoOutputFile) { + throw new Error("Env var AVOCADO_OUTPUT_FILE must be set"); + } + + /** @type {string} */ + let content; + + try { + core.info(`readfile(${avocadoOutputFile})`); + content = await readFile(avocadoOutputFile, { encoding: "utf-8" }); + core.info(`content:\n${content}`); + } catch (error) { + // If we can't read the file, the previous step must have failed catastrophically. + // generateJobSummary() should never fail, so just log the error and return + core.info(`Error reading '${avocadoOutputFile}': ${error}`); + return; + } + + const messages = parse(content).map((obj) => MessageRecordSchema.parse(obj)); + + if (messages.length === 0) { + // Should never happen, but if it does, just log the error and return. + core.notice(`No messages in '${avocadoOutputFile}'`); + return; + } else if ( + messages.length === 1 && + messages[0].type === MessageType.Raw && + messages[0].level === MessageLevel.Info && + messages[0].message.toLowerCase() === "success" + ) { + // Special-case marker message for success + core.summary.addRaw("Success"); + } else { + core.summary.addRaw(generateMarkdownTable(messages)); + } + + core.summary.write(); + core.setOutput("summary", process.env.GITHUB_STEP_SUMMARY); +} diff --git a/.github/workflows/src/comment.js b/.github/workflows/src/comment.js new file mode 100644 index 000000000000..e6f8ae63b73d --- /dev/null +++ b/.github/workflows/src/comment.js @@ -0,0 +1,112 @@ +import { PER_PAGE_MAX } from "./github.js"; + +/** + * @typedef {Object} IssueComment + * @property {number} id + * @property {string} [body] + * @property {{ login: string } | null} [user] + */ + +/** + * Given a set of comments from an issue (or PR) for a specific user, grab the one that contains the specified comment group name. + * + * If the comment group name is not found, this function should return undefined. + * + * @param {IssueComment[]} comments - The list of comments to search through. + * @param {string} commentGroupName - The name of the comment group to search for in the comments. This is merely a "category" that + * will allow the comment to be identified and updated later in a context where there are MULTIPLE github.amrom.workers.devments being left by token. + * This is a bit more future proofed than merely assuming the first comment by the GITHUB_TOKEN user is the one we want to update. + * @returns {[number | undefined, string | undefined]} Resolves when the comment is created or updated. + */ +export function parseExistingComments(comments, commentGroupName) { + /** @type {number | undefined} */ + let commentId = undefined; + /** @type {string | undefined} */ + let commentBody = undefined; + + if (comments) { + comments.map((comment) => { + // When we create or update this comment, we will always leave behind + // in the body of the comment. + // This allows us to identify the comment later on. We need to return the body + // so we can know if we need to update it or not. If it's the same body content + // we will no-op on the update. + if (comment.body?.includes(commentGroupName)) { + commentId = comment.id; + commentBody = comment.body; + } + }); + } + + return [commentId, commentBody]; +} + +/** + * Creates a new issue comment or updates an existing one. + * + * + * @param {import('@actions/github-script').AsyncFunctionArguments['github']} github + * @param {typeof import("@actions/core")} core + * @param {string} owner - The repository owner. + * @param {string} repo - The repository name. + * @param {number} issue_number - The issue or pull request number. + * @param {string} body - The markdown content of the comment. + * @param {string} commentIdentifier - The value that will be stored in an html comment so we can retrieve this comment later + * @returns {Promise} Resolves when the comment is created or updated. + */ +export async function commentOrUpdate( + github, + core, + owner, + repo, + issue_number, + body, + commentIdentifier, +) { + try { + // Get the authenticated user to know who we are + const { data: user } = await github.rest.users.getAuthenticated(); + const authenticatedUsername = user.login; + const computedBody = body + `\n`; + + /** @type {IssueComment[]} */ + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number, + per_page: PER_PAGE_MAX, + }); + + // only examine the comments from user in our current GITHUB_TOKEN context + const existingComments = comments.filter( + (comment) => comment.user?.login === authenticatedUsername, + ); + + const [commentId, commentBody] = parseExistingComments(existingComments, commentIdentifier); + + if (commentId) { + if (commentBody === computedBody) { + core.info(`No update needed for comment ${commentId} by ${authenticatedUsername}`); + return; // No-op if the body is the same + } + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: commentId, + body: computedBody, + }); + core.info(`Updated existing comment ${commentId} by ${authenticatedUsername}`); + } else { + // Create a new comment + const { data: newComment } = await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body: computedBody, + }); + core.info(`Created new comment #${newComment.id}`); + } + } catch (/** @type {any} */ error) { + core.error(`Failed to comment or update: ${error.message}`); + } +} diff --git a/.github/workflows/src/context.js b/.github/workflows/src/context.js index 75314d1426db..aa3d7dbb4aa9 100644 --- a/.github/workflows/src/context.js +++ b/.github/workflows/src/context.js @@ -3,26 +3,31 @@ import { PER_PAGE_MAX } from "./github.js"; import { getIssueNumber } from "./issues.js"; +/** + * @typedef {import('@octokit/plugin-rest-endpoint-methods').RestEndpointMethodTypes} RestEndpointMethodTypes + * @typedef {RestEndpointMethodTypes["repos"]["listPullRequestsAssociatedWithCommit"]["response"]["data"][number]} PullRequest + */ + /** * Extracts inputs from context based on event name and properties. * run_id is only defined for "workflow_run:completed" events. * - * @param {import('github-script').AsyncFunctionArguments['github']} github - * @param {import('github-script').AsyncFunctionArguments['context']} context - * @param {import('github-script').AsyncFunctionArguments['core']} core + * @param {import('@actions/github-script').AsyncFunctionArguments['github']} github + * @param {import('@actions/github-script').AsyncFunctionArguments['context']} context + * @param {import('@actions/github-script').AsyncFunctionArguments['core']} core * @returns {Promise<{owner: string, repo: string, head_sha: string, issue_number: number, run_id: number, details_url?: string }>} */ export async function extractInputs(github, context, core) { core.info("extractInputs()"); core.info(` eventName: ${context.eventName}`); core.info(` payload.action: ${context.eventName}`); - core.info( - ` payload.workflow_run.event: ${context.payload.workflow_run?.event || "undefined"}`, - ); + core.info(` payload.workflow_run.event: ${context.payload.workflow_run?.event || "undefined"}`); // Log full context when debug is enabled. Most workflows should be idempotent and can be re-run // with debug enabled to replay the previous context. - core.isDebug() && core.debug(`context: ${JSON.stringify(context)}`); + if (core.isDebug()) { + core.debug(`context: ${JSON.stringify(context)}`); + } /** @type {{ owner: string, repo: string, head_sha: string, issue_number: number, run_id: number, details_url?: string }} */ let inputs; @@ -40,10 +45,9 @@ export async function extractInputs(github, context, core) { ) { // Most properties on payload should be the same for both pull_request and pull_request_target - const payload = - /** @type {import("@octokit/webhooks-types").PullRequestEvent} */ ( - context.payload - ); + const payload = /** @type {import("@octokit/webhooks-types").PullRequestEvent} */ ( + context.payload + ); inputs = { owner: payload.repository.owner.login, @@ -52,14 +56,10 @@ export async function extractInputs(github, context, core) { issue_number: payload.pull_request.number, run_id: NaN, }; - } else if ( - context.eventName === "issue_comment" && - context.payload.action === "edited" - ) { - const payload = - /** @type {import("@octokit/webhooks-types").IssueCommentEditedEvent} */ ( - context.payload - ); + } else if (context.eventName === "issue_comment" && context.payload.action === "edited") { + const payload = /** @type {import("@octokit/webhooks-types").IssueCommentEditedEvent} */ ( + context.payload + ); const owner = payload.repository.owner.login; const repo = payload.repository.name; @@ -79,10 +79,9 @@ export async function extractInputs(github, context, core) { run_id: NaN, }; } else if (context.eventName === "workflow_dispatch") { - const payload = - /** @type {import("@octokit/webhooks-types").WorkflowDispatchEvent} */ ( - context.payload - ); + const payload = /** @type {import("@octokit/webhooks-types").WorkflowDispatchEvent} */ ( + context.payload + ); inputs = { owner: payload.repository.owner.login, repo: payload.repository.name, @@ -90,14 +89,10 @@ export async function extractInputs(github, context, core) { issue_number: NaN, run_id: NaN, }; - } else if ( - context.eventName === "workflow_run" && - context.payload.action === "completed" - ) { - const payload = - /** @type {import("@octokit/webhooks-types").WorkflowRunCompletedEvent} */ ( - context.payload - ); + } else if (context.eventName === "workflow_run" && context.payload.action === "completed") { + const payload = /** @type {import("@octokit/webhooks-types").WorkflowRunCompletedEvent} */ ( + context.payload + ); let issue_number = NaN; @@ -130,26 +125,34 @@ export async function extractInputs(github, context, core) { const head_repo = payload.workflow_run.head_repository.name; const head_sha = payload.workflow_run.head_sha; - core.info( - `listPullRequestsAssociatedWithCommit(${head_owner}, ${head_repo}, ${head_sha})`, - ); - const pullRequests = ( - await github.paginate( - github.rest.repos.listPullRequestsAssociatedWithCommit, - { + /** @type {PullRequest[]} */ + let pullRequests = []; + + try { + core.info( + `listPullRequestsAssociatedWithCommit(${head_owner}, ${head_repo}, ${head_sha})`, + ); + pullRequests = ( + await github.paginate(github.rest.repos.listPullRequestsAssociatedWithCommit, { owner: head_owner, repo: head_repo, commit_sha: head_sha, per_page: PER_PAGE_MAX, - }, - ) - ).filter( - // Only include PRs to the same repo as the triggering workflow. - // - // Other unique keys like "full_name" should also work, but "id" is the safest since it's - // supposed to be guaranteed unique and never change (repos can be renamed or change owners). - (pr) => pr.base.repo.id === payload.workflow_run.repository.id, - ); + }) + ).filter( + // Only include PRs to the same repo as the triggering workflow. + // + // Other unique keys like "full_name" should also work, but "id" is the safest since it's + // supposed to be guaranteed unique and never change (repos can be renamed or change owners). + (pr) => pr.base.repo.id === payload.workflow_run.repository.id, + ); + } catch (error) { + // Short message always + core.info(`Error: ${error instanceof Error ? error.message : "unknown"}`); + + // Long message only in debug + core.debug(`Error: ${error}`); + } if (pullRequests.length === 0) { // There are three cases where the "commits" REST API called above can return @@ -162,8 +165,7 @@ export async function extractInputs(github, context, core) { // // In any case, the solution is to fall back to the (lower-rate-limit) search API. // The search API is confirmed to work in case #1, but has not been tested in #2 or #3. - issue_number = (await getIssueNumber({ head_sha, github, core })) - .issueNumber; + issue_number = (await getIssueNumber({ head_sha, github, core })).issueNumber; } else if (pullRequests.length === 1) { issue_number = pullRequests[0].number; } else { @@ -184,15 +186,12 @@ export async function extractInputs(github, context, core) { ) { // Attempt to extract issue number from artifact. This can be trusted, because it was uploaded from a workflow that is trusted, // because "issue_comment" and "workflow_run" only trigger on workflows in the default branch. - const artifacts = await github.paginate( - github.rest.actions.listWorkflowRunArtifacts, - { - owner: payload.workflow_run.repository.owner.login, - repo: payload.workflow_run.repository.name, - run_id: payload.workflow_run.id, - per_page: PER_PAGE_MAX, - }, - ); + const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, { + owner: payload.workflow_run.repository.owner.login, + repo: payload.workflow_run.repository.name, + run_id: payload.workflow_run.id, + per_page: PER_PAGE_MAX, + }); const artifactNames = artifacts.map((a) => a.name); @@ -213,9 +212,7 @@ export async function extractInputs(github, context, core) { issue_number = parsedValue; continue; } else { - throw new Error( - `Invalid issue-number: '${value}' parsed to '${parsedValue}'`, - ); + throw new Error(`Invalid issue-number: '${value}' parsed to '${parsedValue}'`); } } } @@ -240,10 +237,9 @@ export async function extractInputs(github, context, core) { }; } else if (context.eventName === "check_run") { let checkRun = context.payload.check_run; - const payload = - /** @type {import("@octokit/webhooks-types").CheckRunEvent} */ ( - context.payload - ); + const payload = /** @type {import("@octokit/webhooks-types").CheckRunEvent} */ ( + context.payload + ); const repositoryInfo = getRepositoryInfo(payload.repository); inputs = { owner: repositoryInfo.owner, @@ -253,14 +249,10 @@ export async function extractInputs(github, context, core) { issue_number: NaN, run_id: NaN, }; - } else if ( - context.eventName === "check_suite" && - context.payload.action === "completed" - ) { - const payload = - /** @type {import("@octokit/webhooks-types").CheckSuiteCompletedEvent} */ ( - context.payload - ); + } else if (context.eventName === "check_suite" && context.payload.action === "completed") { + const payload = /** @type {import("@octokit/webhooks-types").CheckSuiteCompletedEvent} */ ( + context.payload + ); const repositoryInfo = getRepositoryInfo(payload.repository); inputs = { @@ -288,12 +280,7 @@ export async function extractInputs(github, context, core) { * @returns {{ owner: string, repo: string }} */ function getRepositoryInfo(repository) { - if ( - !repository || - !repository.owner || - !repository.owner.login || - !repository.name - ) { + if (!repository || !repository.owner || !repository.owner.login || !repository.name) { throw new Error( `Could not extract repository owner or name from context payload: ${JSON.stringify(repository)}`, ); diff --git a/.github/workflows/src/core-logger.js b/.github/workflows/src/core-logger.js index a0c6f7d03212..47de0bf70a96 100644 --- a/.github/workflows/src/core-logger.js +++ b/.github/workflows/src/core-logger.js @@ -8,11 +8,11 @@ * @implements {ILogger} */ export class CoreLogger { - /** @type {import('github-script').AsyncFunctionArguments['core']} */ + /** @type {import('@actions/github-script').AsyncFunctionArguments['core']} */ #core; /** - * @param {import('github-script').AsyncFunctionArguments['core']} core + * @param {import('@actions/github-script').AsyncFunctionArguments['core']} core */ constructor(core) { this.#core = core; diff --git a/.github/workflows/src/github-test.js b/.github/workflows/src/github-test.js index 06bea6d1bcab..f89044ee6bf1 100644 --- a/.github/workflows/src/github-test.js +++ b/.github/workflows/src/github-test.js @@ -6,7 +6,7 @@ import { pathToFileURL } from "url"; import { inspect } from "util"; /** - * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments + * @param {import('@actions/github-script').AsyncFunctionArguments} AsyncFunctionArguments */ export default async function importAllModules({ core }) { const workspace = process.env.GITHUB_WORKSPACE; @@ -18,10 +18,7 @@ export default async function importAllModules({ core }) { // find all files matching "**/src/**/*.js", sorted for readability const scriptFiles = (await readdir(githubDir, { recursive: true })) - .filter( - (f) => - normalize(f).split(sep).includes("src") && basename(f).endsWith(".js"), - ) + .filter((f) => normalize(f).split(sep).includes("src") && basename(f).endsWith(".js")) .sort(); core.info("Script Files:"); diff --git a/.github/workflows/src/github.js b/.github/workflows/src/github.js index a5fa001defe3..503b2ce66d32 100644 --- a/.github/workflows/src/github.js +++ b/.github/workflows/src/github.js @@ -1,4 +1,14 @@ // @ts-check + +import { byDate, invert } from "../../shared/src/sort.js"; + +/** + * @typedef {import('@octokit/plugin-rest-endpoint-methods').RestEndpointMethodTypes} RestEndpointMethodTypes + * @typedef {RestEndpointMethodTypes["checks"]["listForRef"]["response"]["data"]["check_runs"]} CheckRuns + * @typedef {RestEndpointMethodTypes["actions"]["listWorkflowRunsForRepo"]["response"]["data"]["workflow_runs"]} WorkflowRuns + * @typedef {RestEndpointMethodTypes["repos"]["listCommitStatusesForRef"]["response"]["data"]} CommitStatuses + */ + export const PER_PAGE_MAX = 100; /** @@ -133,3 +143,80 @@ export async function writeToActionsSummary(content, core) { throw new Error(`Failed to write to the GitHub Actions summary: ${error}`); } } + +/** + * Returns the check with the given checkRunName for the given ref. + * @param {import('@actions/github-script').AsyncFunctionArguments['github']} github + * @param {import('@actions/github-script').AsyncFunctionArguments['context']} context + * @param {string} checkRunName + * @param {string} ref + * @returns {Promise} + */ +export async function getCheckRuns(github, context, checkRunName, ref) { + const result = await github.paginate(github.rest.checks.listForRef, { + ...context.repo, + ref: ref, + check_name: checkRunName, + status: "completed", + per_page: PER_PAGE_MAX, + }); + + /* v8 ignore next */ + return result.sort( + invert( + byDate((run) => { + if (run.completed_at === null) { + // completed_at should never be null because status is "completed" + throw new Error(`Unexpected value of run.completed_at: '${run.completed_at}'`); + } else { + return run.completed_at; + } + }), + ), + ); +} + +/** + * Returns the check with the given checkRunName for the given ref. + * @param {import('@actions/github-script').AsyncFunctionArguments['github']} github + * @param {import('@actions/github-script').AsyncFunctionArguments['context']} context + * @param {string} commitStatusName + * @param {string} ref + * @returns {Promise} + */ +export async function getCommitStatuses(github, context, commitStatusName, ref) { + const result = await github.paginate(github.rest.repos.listCommitStatusesForRef, { + ...context.repo, + ref: ref, + per_page: PER_PAGE_MAX, + }); + + return result + .filter( + (status) => + // Property "context" is case-insensitive + status.context.toLowerCase() === commitStatusName.toLowerCase(), + ) + .sort(invert(byDate((status) => status.updated_at))); +} + +/** + * Returns the workflow run with the given workflowName for the given ref. + * @param {import('@actions/github-script').AsyncFunctionArguments['github']} github + * @param {import('@actions/github-script').AsyncFunctionArguments['context']} context + * @param {string} workflowName + * @param {string} ref + * @returns {Promise} + */ +export async function getWorkflowRuns(github, context, workflowName, ref) { + const result = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, { + ...context.repo, + head_sha: ref, + status: "completed", + per_page: PER_PAGE_MAX, + }); + + return result + .filter((run) => run.name === workflowName) + .sort(invert(byDate((run) => run.updated_at))); +} diff --git a/.github/workflows/src/issues.js b/.github/workflows/src/issues.js index 66642ae55b93..98ae32234616 100644 --- a/.github/workflows/src/issues.js +++ b/.github/workflows/src/issues.js @@ -25,16 +25,12 @@ export async function getIssueNumber({ head_sha, core, github }) { const totalCount = searchResponse.data.total_count; const itemsCount = searchResponse.data.items.length; - core.info( - `Search results: ${totalCount} total matches, ${itemsCount} items returned`, - ); + core.info(`Search results: ${totalCount} total matches, ${itemsCount} items returned`); if (itemsCount > 0) { const firstItem = searchResponse.data.items[0]; issueNumber = firstItem.number; - core.info( - `Found the first matched PR #${issueNumber}: ${firstItem.html_url}`, - ); + core.info(`Found the first matched PR #${issueNumber}: ${firstItem.html_url}`); if (itemsCount > 1) { core.warning( diff --git a/.github/workflows/src/label.js b/.github/workflows/src/label.js index 03d879e00594..cb8a6a2b189d 100644 --- a/.github/workflows/src/label.js +++ b/.github/workflows/src/label.js @@ -1,20 +1,14 @@ // @ts-check -// @ts-nocheck Prevent false positive for duplicate typedef and const names -/** - * @typedef {"none" | "add" | "remove"} LabelAction - */ - /** * @readonly - * @enum {LabelAction} + * @enum {"none" | "add" | "remove"} */ export const LabelAction = Object.freeze({ None: "none", Add: "add", Remove: "remove", }); -// @ts-check export const Label = { /** diff --git a/.github/workflows/src/message.js b/.github/workflows/src/message.js new file mode 100644 index 000000000000..207a1f0cbf9d --- /dev/null +++ b/.github/workflows/src/message.js @@ -0,0 +1,188 @@ +// @ts-check + +// Ported from @azure/swagger-validation-common:src/types/message.ts + +import { markdownTable } from "markdown-table"; +import * as z from "zod"; + +/** + * @readonly + * @enum {"Info" | "Warning" | "Error"} + */ +export const MessageLevel = Object.freeze({ + Info: "Info", + Warning: "Warning", + Error: "Error", +}); +/** @type {import("zod").ZodType} */ +export const MessageLevelSchema = z.enum(Object.values(MessageLevel)); + +export const MessageContextSchema = z.object({ + toolVersion: z.string(), +}); +/** + * @typedef {import("zod").infer} MessageContext + */ + +export const ExtraSchema = z.record(z.string(), z.any()); +/** + * @typedef {import("zod").infer} Extra + */ + +export const BaseMessageRecordSchema = z.object({ + level: MessageLevelSchema, + message: z.string(), + time: z.iso.datetime(), + context: z.optional(MessageContextSchema), + group: z.optional(z.string()), + extra: z.optional(ExtraSchema), + groupName: z.optional(z.string()), +}); +/** + * @typedef {import("zod").infer} BaseMessageRecord + */ + +/** + * @readonly + * @enum {"Raw" | "Result"} + */ +export const MessageType = Object.freeze({ + Raw: "Raw", + Result: "Result", +}); +/** @type {import("zod").ZodType} */ +export const MessageTypeSchema = z.enum(Object.values(MessageType)); + +export const RawMessageRecordSchema = BaseMessageRecordSchema.extend({ + type: z.literal(MessageType.Raw), +}); +/** + * @typedef {import("zod").infer} RawMessageRecord + */ + +export const JsonPathSchema = z.object({ + tag: z.string(), + path: z.string(), + jsonPath: z.optional(z.string()), +}); +/** + * @typedef {import("zod").infer} JsonPathSchema + */ + +export const ResultMessageRecordSchema = BaseMessageRecordSchema.extend({ + type: z.literal(MessageType.Result), + id: z.optional(z.string()), + code: z.optional(z.string()), + docUrl: z.optional(z.string()), + paths: z.array(JsonPathSchema), +}); +/** + * @typedef {import("zod").infer} ResultMessageRecord + */ + +export const MessageRecordSchema = z.discriminatedUnion("type", [ + RawMessageRecordSchema, + ResultMessageRecordSchema, +]); +/** + * @typedef {import("zod").infer} MessageRecord + */ + +/** + * Adds table of messages to core.summary + * + * @param {MessageRecord[]} messages + */ +export function generateMarkdownTable(messages) { + const header = ["Rule", "Message"]; + const rows = messages.map((m) => getMarkdownRow(m)); + return markdownTable([header, ...rows]); +} + +/** + * @param {MessageRecord} record + * @returns {string[]} + */ +function getMarkdownRow(record) { + if (record.type === MessageType.Result) { + return [ + getLevelMarkdown(record) + " " + getRuleMarkdown(record), + getMessageMarkdown(record) + "
" + getLocationMarkdown(record), + ]; + } else { + return [getLevelMarkdown(record) + " " + getMessageMarkdown(record), getExtraMarkdown(record)]; + } +} + +// Following ported from openapi-alps/reportGenerator.ts + +/** + * @param {MessageRecord} record + * @returns {string} + */ +function getLevelMarkdown(record) { + switch (record.level) { + case "Error": + return "❌"; + case "Info": + return "ℹ️"; + case "Warning": + return "⚠️"; + } +} + +/** + * @param {ResultMessageRecord} result + * @returns {string} + */ +function getRuleMarkdown(result) { + let ruleName = [result.id, result.code].filter((s) => s).join(" - "); + return `[${ruleName}](${result.docUrl})`; +} + +/** + * @param {ResultMessageRecord} result + * @returns {string} + */ +function getLocationMarkdown(result) { + return result.paths + .filter((p) => p.path) + .map((p) => `${p.tag}: [${getPathSegment(p.path)}](${p.path})`) + .join("
"); +} + +/** + * @param {string} path + * @returns {string} + */ +function getPathSegment(path) { + const idx = path.indexOf("path="); + if (idx !== -1) { + path = decodeURIComponent(path.substr(idx + 5).split("&")[0]); + } + // for github url + return path.split("/").slice(-4).join("/").split("#")[0]; +} + +/** + * @param {MessageRecord} record + * @returns {string} + */ +function getMessageMarkdown(record) { + if (record.type === MessageType.Raw) { + return record.message.replace(/\\n\\n/g, "\n").split("\n")[0]; + } else { + // record.type === MessageType.Raw + const re = /(\n|\t|\r)/gi; + return record.message.replace(re, " "); + } +} +/** + * @param {MessageRecord} record + * @returns {string} + */ +function getExtraMarkdown(record) { + return JSON.stringify(record.extra || {}) + .replace(/[{}]/g, "") + .replace(/,/g, ",
"); +} diff --git a/.github/workflows/src/ndjson.js b/.github/workflows/src/ndjson.js new file mode 100644 index 000000000000..b45eb7b2cba9 --- /dev/null +++ b/.github/workflows/src/ndjson.js @@ -0,0 +1,27 @@ +// @ts-check + +// Functions for processing newline-delimited JSON (aka "ndjson") +// Cannot round-trip "undefined", but neither can JSON.parse()/JSON.stringify() + +/** + * @param {string} text + * @returns {any[]} + */ +export function parse(text) { + return ( + text + .split("\n") + // Skip empty lines, since JSON.parse("") throws "unexpected end of JSON input" + .filter((line) => line.trim() !== "") + .map((line) => JSON.parse(line)) + ); +} + +/** + * @param {any[]} values + * @returns {string} + */ +export function stringify(values) { + // stringify(undefined) returns "undefined", which is ignored in string[].join() + return values.map((v) => JSON.stringify(v)).join("\n"); +} diff --git a/.github/workflows/src/retries.js b/.github/workflows/src/retries.js index d4a51b4b23e5..9fc20a2988df 100644 --- a/.github/workflows/src/retries.js +++ b/.github/workflows/src/retries.js @@ -31,13 +31,8 @@ export async function retry(fn, options = {}) { lastError = error; if (attempt < maxRetries) { - const delayMs = Math.min( - initialDelayMs * Math.pow(2, attempt), - maxDelayMs, - ); - logger( - `Request failed, retrying in ${delayMs}ms... (${attempt + 1}/${maxRetries})`, - ); + const delayMs = Math.min(initialDelayMs * Math.pow(2, attempt), maxDelayMs); + logger(`Request failed, retrying in ${delayMs}ms... (${attempt + 1}/${maxRetries})`); if (error instanceof Error) { logger(`Error: ${error.message}`); } diff --git a/.github/workflows/src/sdk-breaking-change-labels.js b/.github/workflows/src/sdk-breaking-change-labels.js index a5e1ac4694e2..f62b79b3d634 100644 --- a/.github/workflows/src/sdk-breaking-change-labels.js +++ b/.github/workflows/src/sdk-breaking-change-labels.js @@ -1,9 +1,6 @@ // @ts-check import { sdkLabels } from "../../shared/src/sdk-types.js"; -import { - getAdoBuildInfoFromUrl, - getAzurePipelineArtifact, -} from "./artifacts.js"; +import { getAdoBuildInfoFromUrl, getAzurePipelineArtifact } from "./artifacts.js"; import { extractInputs } from "./context.js"; import { LabelAction } from "./label.js"; @@ -22,16 +19,14 @@ import { LabelAction } from "./label.js"; */ /** - * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments + * @param {import('@actions/github-script').AsyncFunctionArguments} AsyncFunctionArguments * @returns {Promise<{labelName: string | undefined, labelAction: LabelAction, issueNumber: number}>} */ export async function getLabelAndAction({ github, context, core }) { const inputs = await extractInputs(github, context, core); const details_url = inputs.details_url; if (!details_url) { - throw new Error( - `Required inputs are not valid: details_url:${details_url}`, - ); + throw new Error(`Required inputs are not valid: details_url:${details_url}`); } return await getLabelAndActionImpl({ details_url, @@ -46,11 +41,7 @@ export async function getLabelAndAction({ github, context, core }) { * @param {import('./retries.js').RetryOptions} [params.retryOptions] * @returns {Promise<{labelName: string | undefined, labelAction: LabelAction, issueNumber: number}>} */ -export async function getLabelAndActionImpl({ - details_url, - core, - retryOptions = {}, -}) { +export async function getLabelAndActionImpl({ details_url, core, retryOptions = {} }) { // Override default logger from console.log to core.info retryOptions = { logger: core.info, ...retryOptions }; diff --git a/.github/workflows/src/set-status.js b/.github/workflows/src/set-status.js index 166f9879c3f1..7fad49b6153f 100644 --- a/.github/workflows/src/set-status.js +++ b/.github/workflows/src/set-status.js @@ -1,17 +1,12 @@ // @ts-check import { extractInputs } from "./context.js"; -import { - CheckConclusion, - CheckStatus, - CommitStatusState, - PER_PAGE_MAX, -} from "./github.js"; +import { CheckConclusion, CheckStatus, CommitStatusState, PER_PAGE_MAX } from "./github.js"; // TODO: Add tests /* v8 ignore start */ /** - * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments + * @param {import('@actions/github-script').AsyncFunctionArguments} AsyncFunctionArguments * @param {string} monitoredWorkflowName * @param {string} requiredStatusName * @param {string} overridingLabel @@ -23,11 +18,7 @@ export default async function setStatus( requiredStatusName, overridingLabel, ) { - const { owner, repo, head_sha, issue_number } = await extractInputs( - github, - context, - core, - ); + const { owner, repo, head_sha, issue_number } = await extractInputs(github, context, core); // Default target is this run itself let target_url = @@ -75,6 +66,8 @@ export async function setStatusImpl({ requiredStatusName, overridingLabel, }) { + core.setOutput("issue_number", issue_number); + // TODO: Try to extract labels from context (when available) to avoid unnecessary API call const labels = await github.paginate(github.rest.issues.listLabelsOnIssue, { owner: owner, @@ -82,12 +75,23 @@ export async function setStatusImpl({ issue_number: issue_number, per_page: PER_PAGE_MAX, }); - const overridingLabels = labels.map((label) => label.name); + const prLabels = labels.map((label) => label.name); + + core.info(`Labels: ${prLabels}`); - core.info(`Labels: ${overridingLabels}`); + // Parse overriding labels (comma-separated string to array) + const overridingLabelsArray = overridingLabel + ? overridingLabel + .split(",") + .map((label) => label.trim()) + .filter((label) => label) // Filter out empty labels + : []; - if (overridingLabels.includes(overridingLabel)) { - const description = `Found label '${overridingLabel}'`; + // Check if any overriding label is present + const foundOverridingLabel = overridingLabelsArray.find((label) => prLabels.includes(label)); + + if (foundOverridingLabel) { + const description = `Found label '${foundOverridingLabel}'`; core.info(description); const state = CheckConclusion.SUCCESS; @@ -106,16 +110,13 @@ export async function setStatusImpl({ return; } - const workflowRuns = await github.paginate( - github.rest.actions.listWorkflowRunsForRepo, - { - owner, - repo, - event: "pull_request", - head_sha, - per_page: PER_PAGE_MAX, - }, - ); + const workflowRuns = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, { + owner, + repo, + event: "pull_request", + head_sha, + per_page: PER_PAGE_MAX, + }); core.info("Workflow Runs:"); workflowRuns.forEach((wf) => { @@ -125,10 +126,7 @@ export async function setStatusImpl({ const targetRuns = workflowRuns .filter((wf) => wf.name == monitoredWorkflowName) // Sort by "updated_at" descending - .sort( - (a, b) => - new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(), - ); + .sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()); // Sorted by "updated_at" descending, so most recent run is at index 0. // If "targetRuns.length === 0", run will be "undefined", which the following @@ -143,32 +141,49 @@ export async function setStatusImpl({ /** * Update target to the "Analyze Code" run, which contains the meaningful output. * - * @example https://github.com/mikeharder/azure-rest-api-specs/actions/runs/14509047569 + * @example https://github.com/Azure/azure-rest-api-specs/actions/runs/14509047569 */ target_url = run.html_url; if (run.conclusion === CheckConclusion.FAILURE) { - /** - * Update target to point directly to the first failed job - * - * @example https://github.com/mikeharder/azure-rest-api-specs/actions/runs/14509047569/job/40703679014?pr=18 - */ - - const jobs = await github.paginate( - github.rest.actions.listJobsForWorkflowRun, + const jobSummaryArtifactName = "job-summary"; + + // Check if run has a custom job summary + core.info( + `listWorkflowRunArtifacts(${owner}, ${repo}, ${run.id}, ${jobSummaryArtifactName})`, + ); + const jobSummaryArtifacts = await github.paginate( + github.rest.actions.listWorkflowRunArtifacts, { - owner, - repo, + owner: owner, + repo: repo, run_id: run.id, + name: jobSummaryArtifactName, per_page: PER_PAGE_MAX, }, ); - const failedJobs = jobs.filter( - (job) => job.conclusion === CheckConclusion.FAILURE, - ); - const failedJob = failedJobs[0]; - if (failedJob?.html_url) { - target_url = `${failedJob.html_url}?pr=${issue_number}`; + + const hasJobSummary = jobSummaryArtifacts.length > 0; + core.info(`hasJobSummary: ${hasJobSummary}`); + + if (!hasJobSummary) { + /** + * Update target to point directly to the first failed job + * + * @example https://github.com/Azure/azure-rest-api-specs/actions/runs/14509047569/job/40703679014?pr=18 + */ + + const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { + owner, + repo, + run_id: run.id, + per_page: PER_PAGE_MAX, + }); + const failedJobs = jobs.filter((job) => job.conclusion === CheckConclusion.FAILURE); + const failedJob = failedJobs[0]; + if (failedJob?.html_url) { + target_url = `${failedJob.html_url}?pr=${issue_number}`; + } } } } diff --git a/.github/workflows/src/spec-gen-sdk-status.js b/.github/workflows/src/spec-gen-sdk-status.js index 34cffbeb155d..90632d2b03d1 100644 --- a/.github/workflows/src/spec-gen-sdk-status.js +++ b/.github/workflows/src/spec-gen-sdk-status.js @@ -1,24 +1,17 @@ // @ts-check +import { getAdoBuildInfoFromUrl, getAzurePipelineArtifact } from "./artifacts.js"; import { extractInputs } from "./context.js"; -import { - getAdoBuildInfoFromUrl, - getAzurePipelineArtifact, -} from "./artifacts.js"; -import { - CheckStatus, - CommitStatusState, - PER_PAGE_MAX, - writeToActionsSummary, -} from "./github.js"; +import { CheckStatus, CommitStatusState, PER_PAGE_MAX, writeToActionsSummary } from "./github.js"; /** - * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments + * @param {import('@actions/github-script').AsyncFunctionArguments} AsyncFunctionArguments * @returns {Promise} */ export default async function setSpecGenSdkStatus({ github, context, core }) { const inputs = await extractInputs(github, context, core); const head_sha = inputs.head_sha; const details_url = inputs.details_url; + const issue_number = inputs.issue_number; if (!details_url || !head_sha) { throw new Error( `Required inputs are not valid: details_url:${details_url}, head_sha:${head_sha}`, @@ -38,6 +31,7 @@ export default async function setSpecGenSdkStatus({ github, context, core }) { target_url, github, core, + issue_number, }); } @@ -47,6 +41,7 @@ export default async function setSpecGenSdkStatus({ github, context, core }) { * @param {string} params.repo * @param {string} params.head_sha * @param {string} params.target_url + * @param {number} params.issue_number * @param {(import("@octokit/core").Octokit & import("@octokit/plugin-rest-endpoint-methods/dist-types/types.js").Api & { paginate: import("@octokit/plugin-paginate-rest").PaginateInterface; })} params.github * @param {typeof import("@actions/core")} params.core * @returns {Promise} @@ -58,24 +53,23 @@ export async function setSpecGenSdkStatusImpl({ target_url, github, core, + issue_number, }) { const statusName = "SDK Validation Status"; + core.setOutput("issue_number", issue_number); const checks = await github.paginate(github.rest.checks.listForRef, { owner, repo, ref: head_sha, per_page: PER_PAGE_MAX, }); + // Filter sdk generation check runs const specGenSdkChecks = checks.filter( - (check) => - check.app?.name === "Azure Pipelines" && - check.name.includes("SDK Validation"), + (check) => check.app?.name === "Azure Pipelines" && check.name.includes("SDK Validation"), ); - core.info( - `Found ${specGenSdkChecks.length} check runs from Azure Pipelines:`, - ); + core.info(`Found ${specGenSdkChecks.length} check runs from Azure Pipelines:`); for (const check of specGenSdkChecks) { core.info(`- ${check.name}: ${check.status} (${check.conclusion})`); } @@ -88,16 +82,12 @@ export async function setSpecGenSdkStatusImpl({ (check) => check.status !== CheckStatus.COMPLETED, ); for (const check of allIncompletedChecks) { - core.info( - `incompleted check runs: ${check.name}: ${check.status} (${check.conclusion})`, - ); + core.info(`incompleted check runs: ${check.name}: ${check.status} (${check.conclusion})`); } if (!allCompleted) { // At least one check is still running or none found yet, set status to pending - core.info( - "Some SDK Validation checks are not completed. Setting status to pending.", - ); + core.info("Some SDK Validation checks are not completed. Setting status to pending."); await github.rest.repos.createCommitStatus({ owner, @@ -115,9 +105,7 @@ export async function setSpecGenSdkStatusImpl({ core, }); - core.info( - `All SDK Validation checks completed. Setting status to ${result.state}.`, - ); + core.info(`All SDK Validation checks completed. Setting status to ${result.state}.`); await github.rest.repos.createCommitStatus({ owner, @@ -149,9 +137,7 @@ async function processResult({ checkRuns, core }) { summaryContent += "|----------|--------|---------------|\n"; for (const checkRun of checkRuns) { - core.info( - `Processing check run: ${checkRun.name} (${checkRun.conclusion})`, - ); + core.info(`Processing check run: ${checkRun.name} (${checkRun.conclusion})`); const buildInfo = getAdoBuildInfoFromUrl(checkRun.details_url); const ado_project_url = buildInfo.projectUrl; const ado_build_id = buildInfo.buildId; @@ -198,8 +184,7 @@ async function processResult({ checkRuns, core }) { } if (state === CommitStatusState.FAILURE) { - specGenSdkFailedRequiredLanguages = - specGenSdkFailedRequiredLanguages.replace(/,\s*$/, ""); + specGenSdkFailedRequiredLanguages = specGenSdkFailedRequiredLanguages.replace(/,\s*$/, ""); description = `SDK Validation failed for ${specGenSdkFailedRequiredLanguages} languages`; } diff --git a/.github/workflows/src/summarize-checks/labelling.js b/.github/workflows/src/summarize-checks/labelling.js new file mode 100644 index 000000000000..efe97a9ef30c --- /dev/null +++ b/.github/workflows/src/summarize-checks/labelling.js @@ -0,0 +1,1255 @@ +/* + This file covers two areas of enforcement: + 1. It calculates what set of label rules has been violated by the current PR, for the purposes of updating next steps to merge. + 2. It calculates what set of labels should be added or removed from the PR. +*/ + +import { includesEvery, includesNone } from "../../../shared/src/array.js"; +import { + brchTsg, + diagramTsg, + href, + notReadyForArmReviewReason, + wrapInArmReviewMessage, +} from "./tsgs.js"; + +// #region typedefs +/** + * The LabelContext is used by the updateLabels() to determine which labels to add or remove to the PR. + * + * The "present" set represents the set of labels that are currently present on the PR and should be populated + * ONCE at the beginning of the summarize-checks action script. + * + * The "toAdd" set is the set of labels to be added to the PR at the end of invocation of updateLabels(). + * This is to be done by calling GitHub Octokit API to add the labels. + * + * The "toRemove" set is analogous to "toAdd" set, but instead it is the set of labels to be removed. + * + * The general pattern used in the code to populate "toAdd" or "toRemove" sets to be ready for + * Octokit invocation is as follows: + * + * - the summary() function passes the context through its invocation chain. + * - given function responsible for given label, like e.g. for label "ARMReview", + * creates a new instance of Label: const armReviewLabel = new Label("ARMReview", labelContext.present) + * - the function then processes the label to determine if armReviewLabel.shouldBePresent is to be set to true or false. + * - the function at the end of its invocation calls armReviewLabel.applyStateChanges(labelContext.toAdd, labelContext.toRemove) + * to update the sets. + * - the function may optionally return { armReviewLabel.shouldBePresent } to allow the caller to pass this value + * further to downstream business logic that depends on it. + * - at the end of invocation summary() calls Octokit passing it as input labelContext.toAdd and labelContext.toRemove. + */ +/** + * @typedef {Object} LabelContext + * @property {Set} present - The current set of labels + * @property {Set} toAdd - The set of labels to add + * @property {Set} toRemove - The set of labels to remove + */ + +/** + * @typedef {Object} ImpactAssessment + * @property {boolean} resourceManagerRequired - Whether a resource manager review is required. + * @property {boolean} suppressionReviewRequired - Whether a suppression review is required. + * @property {boolean} isNewApiVersion - Whether this PR introduces a new API version. + * @property {boolean} rpaasExceptionRequired - Whether an RPaaS exception is required. + * @property {boolean} rpaasRpNotInPrivateRepo - Whether the RPaaS RP is not present in the private repo. + * @property {boolean} rpaasChange - Whether this PR includes RPaaS changes. + * @property {boolean} newRP - Whether this PR introduces a new resource provider. + * @property {boolean} rpaasRPMissing - Whether the RPaaS RP label is missing. + * @property {boolean} typeSpecChanged - Whether a TypeSpec file has changed. + * @property {boolean} isDraft - Whether the PR is a draft. + * @property {LabelContext} labelContext - The context containing present, to-add, and to-remove labels. + * @property {string} targetBranch - The name of the target branch for the PR. + */ + +/** + * This file is the single source of truth for the labels used by the SDK generation tooling + * in the Azure/azure-rest-api-specs and Azure/azure-rest-api-specs-pr repositories. + * + * For additional context, see: + * - https://gist.github.com/raych1/353949d19371b69fb82a10dd70032a51 + * - https://github.com/Azure/azure-sdk-tools/issues/6327 + * - https://microsoftapc-my.sharepoint.com/:w:/g/personal/raychen_microsoft_com/EbOAA9SkhQhGlgxtf7mc0kUB-25bFue0EFbXKXS3TFLTQA + */ + +/** + * RequiredLabelRule: + * IF ((any of the anyPrerequisiteLabels is present + * OR all of the allPrerequisiteLabels are present) + * AND none of the allPrerequisiteAbsentLabels is present) + * THEN any of the anyRequiredLabels is required. + * + * IF any of the anyRequiredLabels is required + * THEN display the troubleshootingGuide in "Next Steps to Merge" comment. + * + * @typedef {Object} RequiredLabelRule + * @property {number} precedence - If multiple RequiredLabelRules are violated, the one with lowest + * precedence should be displayed to the user first. If multiple rules have + * the same precedence, and one of them should be displayed, + * then all of them should be displayed. + * + * Note this independent of the CheckMetadata.precedence. That is, + * if there are failing checks, and failing required label rules, + * both of them will be shown, both taking appropriate lowest precedence. + * @property {string[]} [branches] - Branches, in format "repo/branch", e.g. "azure-rest-api-specs/main", + * to which this required label rule applies. + * + * To be exact: + * - If there is at least one branch defined, and the evaluated PR is + * not targeting any of the branches defined, then the rule is not applicable (it implicitly passes). + * - If "branches" is empty or undefined, then the rule applies to all branches in all repos. + * @property {string[]} [anyPrerequisiteLabels] - If any of anyPrerequisiteLabels is present, the requiredLabel is required. + * This condition is ORed with allPrerequisiteLabels. + * + * If the anyRequiredLabels collection is empty or undefined, anyPrerequisiteLabels must have exactly one entry. + * @property {string[]} [allPrerequisiteLabels] - If all of allPrerequisiteLabels are present, the requiredLabel is required. + * This condition is ORed with anyPrerequisiteLabels. + * + * If the anyRequiredLabels collection is empty or undefined, allPrerequisiteLabels must be empty or undefined. + * @property {string[]} [allPrerequisiteAbsentLabels] - If any of the allPrerequisiteAbsentLabels is present, + * the requiredLabel is not required. + * @property {string[]} anyRequiredLabels - If any of the labels in anyRequiredLabels is present, + * then the rule prerequisites, as expressed by anyPrerequisiteLabels and allPrerequisiteLabels, are met. + * Conversely, if none of anyRequiredLabels are present, then the rule is violated. + * + * For the purposes of determining which label to display as required in the 'automated merging requirements met' check and + * 'Next Steps to Merge" comments, the first label in anyRequiredLabels is used. + * The assumption here is that anyRequiredLabels[0] is the 'current' label, + * while the remaining required label options are 'legacy' labels. + * + * If given required label string ends with an asterisk, it is treated as a prefix substring match. + * For example, requiredLabel of 'Foo-Approved-*' means that any label with prefix 'Foo-Approved-' will satisfy the rule. + * For example, 'Foo-Approved-Bar' or 'Foo-Approved-Qux', but not 'Foo-Approved' nor 'Foo-Approved-'. + * + * If anyRequiredLabels is an empty array, then if the rule is violated, there is no way to meet the rule prerequisites. + * @property {string} troubleshootingGuide - The doc to display to the user if the required label is required, but missing. + */ + +/** + * This file is the single source of truth for types used by the OpenAPI specification breaking change checks + * in the Azure/azure-rest-api-specs and Azure/azure-rest-api-specs-pr repositories. + * + * For additional context, see: + * + * - "Deep-dive into breaking changes on spec PRs" + * https://aka.ms/azsdk/pr-brch-deep + * + * - "[Breaking Change][PR Workflow] Use more granular labels for Breaking Changes approvals" + * https://github.com/Azure/azure-sdk-tools/issues/6374 + */ +/** + * @typedef {"SameVersion" | "CrossVersion"} BreakingChangesCheckType + * @typedef {"VersioningReviewRequired" | "BreakingChangeReviewRequired"} ReviewRequiredLabel + * @typedef {"Versioning-Approved-*" | "BreakingChange-Approved-*"} ReviewApprovalPrefixLabel + * @typedef {"Versioning-Approved-Benign" | "Versioning-Approved-BugFix" | "Versioning-Approved-PrivatePreview" | "Versioning-Approved-BranchPolicyException" | "Versioning-Approved-Previously" | "Versioning-Approved-Retired"} ValidVersioningApproval + * @typedef {"BreakingChange-Approved-Benign" | "BreakingChange-Approved-BugFix" | "BreakingChange-Approved-UserImpact" | "BreakingChange-Approved-BranchPolicyException" | "BreakingChange-Approved-Previously" | "BreakingChange-Approved-Security"} ValidBreakingChangeApproval + * @typedef {ReviewRequiredLabel | ReviewApprovalPrefixLabel | ValidBreakingChangeApproval | ValidVersioningApproval} SpecsBreakingChangesLabel + */ +/** + * @typedef {Object} BreakingChangesCheckConfig + * @property {ReviewRequiredLabel} reviewRequiredLabel + * @property {ReviewApprovalPrefixLabel} approvalPrefixLabel + * @property {(ValidVersioningApproval | ValidBreakingChangeApproval)[]} approvalLabels + * @property {string} [deprecatedReviewRequiredLabel] + */ + +/** + * @typedef {"SwaggerFile" | "TypeSpecFile" | "ExampleFile" | "ReadmeFile"} FileTypes + */ + +/** + * @typedef {"Addition" | "Deletion" | "Update"} ChangeTypes + */ + +/** + * @typedef {Object} PRChange + * @property {FileTypes} fileType + * @property {ChangeTypes} changeType + * @property {string} filePath + * @property {any} [additionalInfo] + */ + +/** + * @typedef {Object} ChangeHandler + * @property {function(PRChange): void} [SwaggerFile] + * @property {function(PRChange): void} [TypeSpecFile] + * @property {function(PRChange): void} [ExampleFile] + * @property {function(PRChange): void} [ReadmeFile] + */ + +/** + * @typedef {"resource-manager" | "data-plane"} PRType + */ + +/** + * Represents a GitHub label. + * Currently used in the context of processing + * labels related to the review workflow of PRs submitted to + * Azure/azure-rest-api-specs and Azure/azure-rest-api-specs-pr repositories. + * This processing happens in the prSummary.ts / summary() function. + * + * See also: https://aka.ms/SpecPRReviewARMInvariants + */ +// todo: inject `core` for access to logging +export class Label { + /** + * @param {string} name + * @param {Set} [presentLabels] + */ + constructor(name, presentLabels) { + /** @type {string} */ + this.name = name; + + /** + * Is the label currently present on the pull request? + * This is determined at the time of construction of this object. + * @type {boolean | undefined} + */ + this.present = presentLabels?.has(this.name) ?? undefined; + + /** + * Should this label be present on the pull request? + * Must be defined before applyStateChange is called. + * Not set at the construction time to facilitate determining desired presence + * of multiple labels in single code block, without intermixing it with + * label construction logic. + * @type {boolean | undefined} + */ + this.shouldBePresent = undefined; + } + + /** + * If the label should be added, add its name to labelsToAdd. + * If the label should be removed, add its name to labelsToRemove. + * Otherwise, do nothing. + * + * Precondition: this.shouldBePresent has been defined. + * @param {Set} labelsToAdd + * @param {Set} labelsToRemove + */ + applyStateChange(labelsToAdd, labelsToRemove) { + if (this.shouldBePresent === undefined) { + console.warn( + "ASSERTION VIOLATION! " + + `Cannot applyStateChange for label '${this.name}' ` + + "as its desired presence hasn't been defined. Returning early.", + ); + throw new Error( + `Label '${this.name}' has not been properly initialized with shouldBePresent before being applied.`, + ); + } + + if (!this.present && this.shouldBePresent) { + if (!labelsToAdd.has(this.name)) { + console.log( + `Label.applyStateChange: '${this.name}' was not present and should be present. Scheduling addition.`, + ); + labelsToAdd.add(this.name); + } else { + console.log( + `Label.applyStateChange: '${this.name}' was not present and should be present. It is already scheduled for addition.`, + ); + } + } else if (this.present && !this.shouldBePresent) { + if (!labelsToRemove.has(this.name)) { + console.log( + `Label.applyStateChange: '${this.name}' was present and should not be present. Scheduling removal.`, + ); + labelsToRemove.add(this.name); + } else { + console.log( + `Label.applyStateChange: '${this.name}' was present and should not be present. It is already scheduled for removal.`, + ); + } + } else if (this.present === this.shouldBePresent) { + console.log( + `Label.applyStateChange: '${this.name}' is ${this.present ? "present" : "not present"}. This is the desired state.`, + ); + } else { + console.warn( + "ASSERTION VIOLATION! " + + `Label.applyStateChange: '${this.name}' is ${this.present ? "present" : "not present"} while it should be ${this.shouldBePresent ? "present" : "not present"}. ` + + `At this point of execution this should not happen.`, + ); + } + } + + /** + * @param {string} label + * @returns {boolean} + */ + isEqualToOrPrefixOf(label) { + return this.name.endsWith("*") ? label.startsWith(this.name.slice(0, -1)) : this.name === label; + } + + /** + * @returns {string} + */ + logString() { + return ( + `Label: name: ${this.name}, ` + + `present: ${this.present}, ` + + `shouldBePresent: ${this.shouldBePresent}. ` + ); + } +} + +// #endregion typedefs +// #region constants +export const sdkLabels = { + "azure-cli-extensions": { + breakingChange: undefined, + breakingChangeApproved: undefined, + breakingChangeSuppression: undefined, + breakingChangeSuppressionApproved: undefined, + deprecatedBreakingChange: undefined, + deprecatedBreakingChangeApproved: undefined, + }, + "azure-resource-manager-schemas": { + breakingChange: undefined, + breakingChangeApproved: undefined, + breakingChangeSuppression: undefined, + breakingChangeSuppressionApproved: undefined, + deprecatedBreakingChange: undefined, + deprecatedBreakingChangeApproved: undefined, + }, + "azure-sdk-for-go": { + breakingChange: "BreakingChange-Go-Sdk", + breakingChangeApproved: "BreakingChange-Go-Sdk-Approved", + breakingChangeSuppression: "BreakingChange-Go-Sdk-Suppression", + breakingChangeSuppressionApproved: "BreakingChange-Go-Sdk-Suppression-Approved", + deprecatedBreakingChange: "CI-BreakingChange-Go", + deprecatedBreakingChangeApproved: "Approved-SdkBreakingChange-Go", + }, + "azure-sdk-for-java": { + breakingChange: "BreakingChange-Java-Sdk", + breakingChangeApproved: "BreakingChange-Java-Sdk-Approved", + breakingChangeSuppression: "BreakingChange-Java-Sdk-Suppression", + breakingChangeSuppressionApproved: "BreakingChange-Java-Sdk-Suppression-Approved", + deprecatedBreakingChange: "CI-BreakingChange-Java", + deprecatedBreakingChangeApproved: "Approved-SdkBreakingChange-Java", + }, + "azure-sdk-for-js": { + breakingChange: "BreakingChange-JavaScript-Sdk", + breakingChangeApproved: "BreakingChange-JavaScript-Sdk-Approved", + breakingChangeSuppression: "BreakingChange-JavaScript-Sdk-Suppression", + breakingChangeSuppressionApproved: "BreakingChange-JavaScript-Sdk-Suppression-Approved", + deprecatedBreakingChange: "CI-BreakingChange-JavaScript", + deprecatedBreakingChangeApproved: "Approved-SdkBreakingChange-JavaScript", + }, + "azure-sdk-for-net": { + breakingChange: undefined, + breakingChangeApproved: undefined, + breakingChangeSuppression: undefined, + breakingChangeSuppressionApproved: undefined, + deprecatedBreakingChange: undefined, + deprecatedBreakingChangeApproved: undefined, + }, + "azure-sdk-for-python": { + breakingChange: "BreakingChange-Python-Sdk", + breakingChangeApproved: "BreakingChange-Python-Sdk-Approved", + breakingChangeSuppression: "BreakingChange-Python-Sdk-Suppression", + breakingChangeSuppressionApproved: "BreakingChange-Python-Sdk-Suppression-Approved", + deprecatedBreakingChange: "CI-BreakingChange-Python", + deprecatedBreakingChangeApproved: "Approved-SdkBreakingChange-Python", + }, + "azure-sdk-for-python-track2": { + breakingChange: "BreakingChange-Python-Track2-Sdk", + breakingChangeApproved: "BreakingChange-Python-Track2-Sdk-Approved", + breakingChangeSuppression: "BreakingChange-Python-Sdk-Suppression", + breakingChangeSuppressionApproved: "BreakingChange-Python-Sdk-Suppression-Approved", + deprecatedBreakingChange: "CI-BreakingChange-Python-Track2", + // Note that deprecatedBreakingChangeApproved is the same for python and python-track2 + deprecatedBreakingChangeApproved: "Approved-SdkBreakingChange-Python", + }, + "azure-sdk-for-trenton": { + breakingChange: undefined, + breakingChangeApproved: undefined, + breakingChangeSuppression: undefined, + breakingChangeSuppressionApproved: undefined, + deprecatedBreakingChange: undefined, + deprecatedBreakingChangeApproved: undefined, + }, +}; + +/** + * @type {Record} + */ +// todo: pull values from eng/tools/openapi-diff-runner/src/types/breaking-change.ts +export const breakingChangesCheckType = { + SameVersion: { + reviewRequiredLabel: "VersioningReviewRequired", + approvalPrefixLabel: "Versioning-Approved-*", + approvalLabels: [ + "Versioning-Approved-Benign", + "Versioning-Approved-BugFix", + "Versioning-Approved-PrivatePreview", + "Versioning-Approved-BranchPolicyException", + "Versioning-Approved-Previously", + "Versioning-Approved-Retired", + ], + }, + CrossVersion: { + reviewRequiredLabel: "BreakingChangeReviewRequired", + approvalPrefixLabel: "BreakingChange-Approved-*", + approvalLabels: [ + "BreakingChange-Approved-Benign", + "BreakingChange-Approved-BugFix", + "BreakingChange-Approved-UserImpact", + "BreakingChange-Approved-BranchPolicyException", + "BreakingChange-Approved-Previously", + "BreakingChange-Approved-Security", + ], + }, +}; + +// #endregion constants +// #region Required Labels + +/** + * @param {LabelContext} context + * @param {string[]} existingLabels + * @returns {void} + */ +export function processArmReviewLabels(context, existingLabels) { + // the important part about how this will work depends how the users use it + // EG: if they add the "ARMSignedOff" label, we will remove the "ARMChangesRequested" and "WaitForARMFeedback" labels. + // if they add the "ARMChangesRequested" label, we will remove the "WaitForARMFeedback" label. + // if they remove the "ARMChangesRequested" label, we will add the "WaitForARMFeedback" label. + // so if the user or ARM team actually unlabels `ARMChangesRequested`, then we're actually ok + // if we are signed off, we should remove the "ARMChangesRequested" and "WaitForARMFeedback" labels + if (includesEvery(existingLabels, ["ARMSignedOff"])) { + if (existingLabels.includes("ARMChangesRequested")) { + context.toRemove.add("ARMChangesRequested"); + } + if (existingLabels.includes("WaitForARMFeedback")) { + context.toRemove.add("WaitForARMFeedback"); + } + } + // if there are ARM changes requested, we should remove the "WaitForARMFeedback" label as the presence indicates that ARM has reviewed + else if ( + includesEvery(existingLabels, ["ARMChangesRequested"]) && + includesNone(existingLabels, ["ARMSignedOff"]) + ) { + if (existingLabels.includes("WaitForARMFeedback")) { + context.toRemove.add("WaitForARMFeedback"); + } + } + // finally, if ARMChangesRequested are not present, and we've gotten here by lac;k of signoff, we should add the "WaitForARMFeedback" label + else if (includesNone(existingLabels, ["ARMChangesRequested"])) { + if (!existingLabels.includes("WaitForARMFeedback")) { + context.toAdd.add("WaitForARMFeedback"); + } + } +} + +/** +This function determines which labels of the ARM review should +be applied to given PR. It adds and removes the labels as appropriate. It used to be called +ProcessARMReview, but was renamed to processImpactAssessment to better reflect its purpose given that is +merely evaluating the impact assessment of the PR and returning a final set of labels to be applied/removed +from the PR. + +This function does the following, **among other things**: + +- Adds the "ARMReview" label if all of the following conditions hold: + - The processed PR "isReleaseBranch" or "isShiftLeftPRWithRPSaaSDev" + - The PR is not a draft, as determined by "isDraftPR" + - The PR is labelled with "resource-manager" label, meaning it pertains + to ARM, as previously determined by the "isManagementPR" function, + called from the "getPRType" function. + +- Calls the "processARMReviewWorkflowLabels" function if "ARMReview" label applies. +*/ +// todo: refactor to take context: PRContext as input instead of IValidatorContext. +// All downstream usage appears to be using "context.contextConfig() as PRContext". +/** + * @param {string} targetBranch + * @param {LabelContext} labelContext + * @param {boolean} resourceManagerLabelShouldBePresent + * @param {boolean} ciNewRPNamespaceWithoutRpaaSLabelShouldBePresent + * @param {boolean} rpaasExceptionLabelShouldBePresent + * @param {boolean} ciRpaasRPNotInPrivateRepoLabelShouldBePresent + * @param {boolean} isNewApiVersion + * @param {boolean} isDraft + * @returns {Promise<{armReviewLabelShouldBePresent: boolean}>} + */ +export async function processImpactAssessment( + targetBranch, + labelContext, + resourceManagerLabelShouldBePresent, + ciNewRPNamespaceWithoutRpaaSLabelShouldBePresent, + rpaasExceptionLabelShouldBePresent, + ciRpaasRPNotInPrivateRepoLabelShouldBePresent, + isNewApiVersion, + isDraft, +) { + console.log("ENTER definition processARMReview"); + + const armReviewLabel = new Label("ARMReview", labelContext.present); + // By default this label should not be present. We may determine later in this function that it should be present after all. + armReviewLabel.shouldBePresent = false; + + const newApiVersionLabel = new Label("new-api-version", labelContext.present); + // By default this label should not be present. We may determine later in this function that it should be present after all. + newApiVersionLabel.shouldBePresent = false; + + const branch = targetBranch; + const isReleaseBranchVal = isReleaseBranch(branch); + + // we used to also calculate if the branch name was from ShiftLeft, in which case we would or that + // with isReleaseBranchVal to see if it's in scope of ARM review. ShiftLeft is not supported anymore, + // so we only check if the branch is a release branch. + // const prTitle = await getPrTitle(owner, repo, prNumber) + // const isShiftLeftPRWithRPSaaSDevVal = isShiftLeftPRWithRPSaaSDev(prTitle, branch) + const isBranchInScopeOfSpecReview = isReleaseBranchVal; // || isShiftLeftPRWithRPSaaSDevVal + + // 'specReviewApplies' means that either ARM or data-plane review applies. Downstream logic + // determines which kind of review exactly we need. + let specReviewApplies = !isDraft && isBranchInScopeOfSpecReview; + if (specReviewApplies) { + if (isNewApiVersion) { + // Note that in case of data-plane PRs, the addition of this label will result + // in API stewardship board review being required. + // See requiredLabelsRules.ts. + newApiVersionLabel.shouldBePresent = true; + } + + armReviewLabel.shouldBePresent = resourceManagerLabelShouldBePresent; + await processARMReviewWorkflowLabels( + labelContext, + armReviewLabel.shouldBePresent, + ciNewRPNamespaceWithoutRpaaSLabelShouldBePresent, + rpaasExceptionLabelShouldBePresent, + ciRpaasRPNotInPrivateRepoLabelShouldBePresent, + ); + } + + newApiVersionLabel.applyStateChange(labelContext.toAdd, labelContext.toRemove); + armReviewLabel.applyStateChange(labelContext.toAdd, labelContext.toRemove); + + console.log( + `RETURN definition processARMReview. ` + + `isReleaseBranch: ${isReleaseBranchVal}, ` + + `isBranchInScopeOfArmReview: ${isBranchInScopeOfSpecReview}, ` + + `isNewApiVersion: ${isNewApiVersion}, ` + + `isDraft: ${isDraft}, ` + + `newApiVersionLabel.shouldBePresent: ${newApiVersionLabel.shouldBePresent}, ` + + `armReviewLabel.shouldBePresent: ${armReviewLabel.shouldBePresent}.`, + ); + + return { armReviewLabelShouldBePresent: armReviewLabel.shouldBePresent }; +} + +/** + * @param {string} branchName + * @returns {boolean} + */ +function isReleaseBranch(branchName) { + const branchRegex = [/main/, /RPSaaSMaster/, /release*/, /ARMCoreRPDev/]; + return branchRegex.some((b) => b.test(branchName)); +} + +/** +CODESYNC: +- requiredLabelsRules.ts / requiredLabelsRules +- https://github.com/Azure/azure-rest-api-specs/blob/main/.github.amrom.workers.devment.yml + +This function determines which label from the ARM review workflow labels +should be present on the PR. It adds and removes the labels as appropriate. + +In other words, this function captures the +ARM review workflow label processing logic. + +To be exact, this function executes if and only if the PR in question +has been determined to have the "ARMReview" label, denoting given PR +is in scope for ARM review. + +The implementation of this function is the source of truth specifying the +desired behavior. + +To understand this implementation, the most important constraint to keep in mind +is that if "ARMReview" label is present, then exactly one of the following +labels must be present: + +- NotReadyForARMReview +- WaitForARMFeedback +- ARMChangesRequested +- ARMSignedOff + +Note that another important place in this codebase where ARM review workflow +labels are being removed or added to a PR is pipelineBotOnPRLabelEvent.ts. +*/ +/** + * @param {LabelContext} labelContext + * @param {boolean} armReviewLabelShouldBePresent + * @param {boolean} ciNewRPNamespaceWithoutRpaaSLabelShouldBePresent + * @param {boolean} rpaasExceptionLabelShouldBePresent + * @param {boolean} ciRpaasRPNotInPrivateRepoLabelShouldBePresent + * @returns {Promise} + */ +async function processARMReviewWorkflowLabels( + labelContext, + armReviewLabelShouldBePresent, + ciNewRPNamespaceWithoutRpaaSLabelShouldBePresent, + rpaasExceptionLabelShouldBePresent, + ciRpaasRPNotInPrivateRepoLabelShouldBePresent, +) { + console.log("ENTER definition processARMReviewWorkflowLabels"); + + const notReadyForArmReviewLabel = new Label("NotReadyForARMReview", labelContext.present); + + const waitForArmFeedbackLabel = new Label("WaitForARMFeedback", labelContext.present); + + const armChangesRequestedLabel = new Label("ARMChangesRequested", labelContext.present); + + const armSignedOffLabel = new Label("ARMSignedOff", labelContext.present); + + const blockedOnRpaas = getBlockedOnRpaas( + ciNewRPNamespaceWithoutRpaaSLabelShouldBePresent, + rpaasExceptionLabelShouldBePresent, + ciRpaasRPNotInPrivateRepoLabelShouldBePresent, + ); + + const blocked = blockedOnRpaas; + + // If given PR is in scope of ARM review and it is blocked for any reason, + // the "NotReadyForARMReview" label should be present, to the exclusion + // of all other ARM review workflow labels. + notReadyForArmReviewLabel.shouldBePresent = armReviewLabelShouldBePresent && blocked; + + // If given PR is in scope of ARM review and the review is not blocked, + // then "ARMSignedOff" label should remain present on the PR if it was + // already present. This means that labels "ARMChangesRequested" + // and "WaitForARMFeedback" are invalid and will be removed by automation + // in presence of "ARMSignedOff". + armSignedOffLabel.shouldBePresent = + armReviewLabelShouldBePresent && !blocked && armSignedOffLabel.present; + + // If given PR is in scope of ARM review and the review is not blocked and + // not signed-off, then the label "ARMChangesRequested" should remain present + // if it was already present. This means that labels "WaitForARMFeedback" + // is invalid and will be removed by automation in presence of + // "WaitForARMFeedback". + armChangesRequestedLabel.shouldBePresent = + armReviewLabelShouldBePresent && + !blocked && + !armSignedOffLabel.shouldBePresent && + armChangesRequestedLabel.present; + + // If given PR is in scope of ARM review and the review is not blocked and + // not signed-off, and ARM reviewer didn't request any changes, + // then the label "WaitForARMFeedback" should be present on the PR, whether + // it was present before or not. + waitForArmFeedbackLabel.shouldBePresent = + armReviewLabelShouldBePresent && + !blocked && + !armSignedOffLabel.shouldBePresent && + !armChangesRequestedLabel.shouldBePresent && + (waitForArmFeedbackLabel.present || true); + + const exactlyOneArmReviewWorkflowLabelShouldBePresent = + Number(notReadyForArmReviewLabel.shouldBePresent) + + Number(armSignedOffLabel.shouldBePresent) + + Number(armChangesRequestedLabel.shouldBePresent) + + Number(waitForArmFeedbackLabel.shouldBePresent) === + 1 || !armReviewLabelShouldBePresent; + + if (!exactlyOneArmReviewWorkflowLabelShouldBePresent) { + console.warn("ASSERTION VIOLATION! exactlyOneArmReviewWorkflowLabelShouldBePresent is false"); + } + + notReadyForArmReviewLabel.applyStateChange(labelContext.toAdd, labelContext.toRemove); + armSignedOffLabel.applyStateChange(labelContext.toAdd, labelContext.toRemove); + armChangesRequestedLabel.applyStateChange(labelContext.toAdd, labelContext.toRemove); + waitForArmFeedbackLabel.applyStateChange(labelContext.toAdd, labelContext.toRemove); + + console.log( + `RETURN definition processARMReviewWorkflowLabels. ` + + `presentLabels: ${[...labelContext.present].join(",")}, ` + + `blockedOnRpaas: ${blockedOnRpaas}. ` + + `exactlyOneArmReviewWorkflowLabelShouldBePresent: ${exactlyOneArmReviewWorkflowLabelShouldBePresent}. `, + ); + return; +} + +/** + * @param {boolean} ciNewRPNamespaceWithoutRpaaSLabelShouldBePresent + * @param {boolean} rpaasExceptionLabelShouldBePresent + * @param {boolean} ciRpaasRPNotInPrivateRepoLabelShouldBePresent + * @returns {boolean} + */ +function getBlockedOnRpaas( + ciNewRPNamespaceWithoutRpaaSLabelShouldBePresent, + rpaasExceptionLabelShouldBePresent, + ciRpaasRPNotInPrivateRepoLabelShouldBePresent, +) { + return ( + (ciNewRPNamespaceWithoutRpaaSLabelShouldBePresent && !rpaasExceptionLabelShouldBePresent) || + ciRpaasRPNotInPrivateRepoLabelShouldBePresent + ); +} + +// #endregion +// #region LabelRules +/** + * @param {BreakingChangesCheckType} approvalType + * @param {string[]} labels + * @returns {boolean} + */ +export function anyApprovalLabelPresent(approvalType, labels) { + // todo: confirm property versus map syntax for accessing this. + const labelsToMatchAgainst = [breakingChangesCheckType[approvalType].approvalPrefixLabel]; + return anyLabelMatches(labelsToMatchAgainst, labels); +} + +/** + * @param {RequiredLabelRule} rule + * @returns {boolean} + */ +export function isAnyPrerequisiteLabelsNonempty(rule) { + return rule.anyPrerequisiteLabels !== undefined && rule.anyPrerequisiteLabels.length > 0; +} + +/** + * @param {RequiredLabelRule} rule + * @returns {boolean} + */ +export function isAllPrerequisiteLabelsNonempty(rule) { + return rule.allPrerequisiteLabels !== undefined && rule.allPrerequisiteLabels.length > 0; +} + +/** + * @param {RequiredLabelRule} rule + * @returns {boolean} + */ +export function isAllPrerequisiteAbsentLabelsNonempty(rule) { + return ( + rule.allPrerequisiteAbsentLabels !== undefined && rule.allPrerequisiteAbsentLabels.length > 0 + ); +} + +export const verRev = breakingChangesCheckType.SameVersion.reviewRequiredLabel; +export const verRevApproval = breakingChangesCheckType.SameVersion.approvalPrefixLabel; +export const brChRev = breakingChangesCheckType.CrossVersion.reviewRequiredLabel; +export const brChRevApproval = breakingChangesCheckType.CrossVersion.approvalPrefixLabel; + +/** @type {RequiredLabelRule[]} */ +const rulesPri0dataPlane = [ + // For context on this rule see https://github.com/Azure/azure-sdk-tools/issues/7648. + { + precedence: 0, + branches: ["azure-rest-api-specs/main", "azure-rest-api-specs-pr/RPSaaSMaster"], + anyPrerequisiteLabels: ["data-plane", "resource-manager"], + anyRequiredLabels: ["PublishToCustomers"], + troubleshootingGuide: + `This PR targets either the main branch of the public ` + + `specs repo or the RPSaaSMaster branch of the private specs repo. These ` + + `branches are not intended for iterative development. Therefore, you must acknowledge ` + + `you understand that after this PR is merged, the APIs are considered shipped to ` + + `Azure customers. Any further attempts at in-place modifications to the APIs will be ` + + `subject to Azure's versioning and breaking change policies. Additionally, for control ` + + `plane APIs, you must acknowledge that you are following all the best practices ` + + `documented by ARM at ` + + `${href("aka.ms/armapibestpractices", "https://aka.ms/armapibestpractices")}. ` + + `If you do intend to release the APIs to your customers by merging this PR, ` + + `add the PublishToCustomers label to your PR in acknowledgement ` + + `of the above. ` + + `Otherwise, retarget this PR onto a feature branch, i.e. with prefix release- ` + + `(see ${href("aka.ms/azsdk/api-versions#release--branches", "https://aka.ms/azsdk/api-versions#release--branches")}).`, + }, + // For context on this rule see https://github.com/Azure/azure-sdk-tools/issues/6184 + // and https://github.com/Azure/azure-sdk-tools/issues/6612 + // This rule says: + // + // IF (the label APIStewardshipBoard-ReviewRequested is present) + // OR (both labels data-plane AND new-api-version are present), + // THEN (require label APIStewardshipBoard-SignedOff) + // + // TODO: need to implement, in the prSummary.ts, addition of the APIStewardshipBoard-ReviewRequested label + // Once done, remove "data-plane" and "new-api-version" from here. + { + precedence: 0, + anyPrerequisiteLabels: ["APIStewardshipBoard-ReviewRequested"], + allPrerequisiteLabels: ["data-plane", "new-api-version"], + anyRequiredLabels: ["APIStewardshipBoard-SignedOff"], + troubleshootingGuide: + `Your PR requires an API stewardship board review as it introduces a new API version (label: new-api-version). ` + + `Schedule the review by following ` + + `${href("aka.ms/azsdk/onboarding/restapischedule", "https://aka.ms/azsdk/onboarding/restapischedule")}.`, + }, +]; + +/** @type {RequiredLabelRule[]} */ +const rulesPri0NotReadyForArmReview = [ + // Note this rule can be circumvented e.g. by adding approval mismatching the actual problem. + // E.g. PR may not be ready for ARM review due to pending breaking changes review, but this rule + // can be fulfilled by adding RPaaSException instead of breaking change approval. + // + // This rule provides a message that sets context for why PR is not ready for ARM review. + // But the rule doesn't know the exact reason - it is provided by other, more specific rules. + // This "general context" rule must be considered fulfilled once all the applicable fine-grained rules + // for NotReadyForARMReview are fulfilled. But because this rule doesn't know the exact problem, + // it considers itself fulfilled if any one of the fine-grained rules is fulfilled. + { + precedence: 0, + anyPrerequisiteLabels: ["NotReadyForARMReview"], + allPrerequisiteAbsentLabels: [verRevApproval, brChRevApproval, "RPaaSException"], + anyRequiredLabels: ["ARMSignedOff"], + troubleshootingGuide: wrapInArmReviewMessage( + "This PR is not ready for ARM review (label: NotReadyForARMReview). " + + "This PR will not be reviewed by ARM until relevant problems are fixed. " + + "Consult the rest of this Next Steps to Merge comment for details.
" + + "Once the blocking problems are addressed, add to the PR a comment with contents /azp run. " + + "Automation will re-evaluate this PR and if everything looks good, it will add WaitForARMFeedback label " + + "which will put this PR on the ARM review queue.", + ), + }, + { + precedence: 0, + allPrerequisiteLabels: ["NotReadyForARMReview", "CI-NewRPNamespaceWithoutRPaaS"], + anyRequiredLabels: ["RPaaSException"], + troubleshootingGuide: notReadyForArmReviewReason("CI-NewRPNamespaceWithoutRPaaS"), + }, + { + precedence: 0, + allPrerequisiteLabels: ["NotReadyForARMReview", "CI-RpaaSRPNotInPrivateRepo"], + anyRequiredLabels: ["ARMSignedOff"], + troubleshootingGuide: notReadyForArmReviewReason("CI-RpaaSRPNotInPrivateRepo"), + }, + { + precedence: 0, + allPrerequisiteLabels: ["NotReadyForARMReview", verRev], + anyRequiredLabels: [verRevApproval], + troubleshootingGuide: notReadyForArmReviewReason(verRev), + }, + { + precedence: 0, + allPrerequisiteLabels: ["NotReadyForARMReview", brChRev], + anyRequiredLabels: [brChRevApproval], + troubleshootingGuide: notReadyForArmReviewReason(brChRev), + }, +]; + +/** @type {RequiredLabelRule[]} */ +const rulesPri0ArmRpaas = [ + { + precedence: 0, + anyPrerequisiteLabels: ["CI-NewRPNamespaceWithoutRPaaS"], + anyRequiredLabels: ["RPaaSException"], + troubleshootingGuide: + "This PR has CI-NewRPNamespaceWithoutRPaaS label. " + + "This means it is introducing a new RP (Resource Provider) namespace that has not been onboarded with " + + `${href("RPaaS", "https://armwiki.azurewebsites.net/rpaas/overview.html")}. ` + + "Merging this PR to the main branch is blocked as RPaaS is required for new RPs.
" + + `To resolve, ${href( + "use RPaaS to onboard the new RP", + "https://armwiki.azurewebsites.net/rpaas/gettingstarted.html", + )}. ` + + `To apply for exception, see ${href("aka.ms/RPaaSException", "https://aka.ms/RPaaSException")}.`, + }, + { + precedence: 0, + anyPrerequisiteLabels: ["CI-RpaaSRPNotInPrivateRepo"], + anyRequiredLabels: ["ARMSignedOff"], + troubleshootingGuide: + "This PR has CI-RpaaSRPNotInPrivateRepo label. " + + "This means it is introducing a new RP (Resource Provider) namespace to the main branch " + + "without first merging the new RP to the RPSaaSMaster branch. " + + "To resolve, first submit and merge a PR with the new RP namespace to the RPSaaSMasterbranch." + + "This PR will remain blocked until then.", + }, +]; + +/** @type {RequiredLabelRule[]} */ +const rulesPri0Changes = [ + { + precedence: 0, + allPrerequisiteLabels: [verRev], + anyRequiredLabels: [verRevApproval], + troubleshootingGuide: + `This PR has at least one change violating Azure versioning policy (label: ${verRev}).
` + + `To unblock this PR, either a) introduce a new API version with these changes instead of modifying an existing API version, ` + + `or b) ${brchTsg.replace("To unblock this PR, ", "")}`, + }, + { + precedence: 0, + allPrerequisiteLabels: [brChRev], + anyRequiredLabels: [brChRevApproval], + troubleshootingGuide: + `This PR has at least one breaking change (label: ${brChRev}).
` + brchTsg, + }, +]; + +/** @type {RequiredLabelRule[]} */ +const rulesPri0ArmRev = [ + { + precedence: 0, + anyPrerequisiteLabels: ["WaitForARMFeedback"], + anyRequiredLabels: ["ARMSignedOff"], + troubleshootingGuide: wrapInArmReviewMessage( + `This PR is awaiting ARM reviewer feedback (label: WaitForARMFeedback).
` + + `To learn when this PR will get reviewed, see ARM review queue at ` + + `${href("aka.ms/azsdk/pr-arm-review", "https://aka.ms/azsdk/pr-arm-review")}`, + ), + }, + { + precedence: 0, + anyPrerequisiteLabels: ["ARMChangesRequested"], + anyRequiredLabels: ["ARMSignedOff"], + troubleshootingGuide: wrapInArmReviewMessage( + "This PR has ARMChangesRequested label. Please address or respond to feedback from the ARM API reviewer.
" + + "When you are ready to continue the ARM API review, please remove the ARMChangesRequested label.
" + + "Automation should then add WaitForARMFeedback label.
" + + "❗If you don't have permissions to remove the label, request write access per " + + `${href("aka.ms/azsdk/access#request-access-to-rest-api-or-sdk-repositories", "https://aka.ms/azsdk/access#request-access-to-rest-api-or-sdk-repositories")}` + + ".", + ), + }, +]; + +/** @type {RequiredLabelRule[]} */ +const rulesPri1ArmRev = [ + // This rule should never kick-in in normal flow, as there should always be one of rules for the ARM review workflow + // labels active, and all of them have higher precedence than this rule. + // However, we may still end up in this situation if someone manually tampered with the labels. + { + precedence: 1, + anyPrerequisiteLabels: ["ARMReview"], + anyRequiredLabels: ["ARMSignedOff"], + troubleshootingGuide: wrapInArmReviewMessage( + "❗❗❗ ARM review workflow label is missing! ❗❗❗" + + "TO UNBLOCK THIS PR, add /azp run comment to this PR to allow automation to determine where this PR is in the ARM review workflow.", + ), + }, +]; + +/** @type {RequiredLabelRule[]} */ +const rulesPri1Suppressions = [ + { + precedence: 1, + anyPrerequisiteLabels: ["SuppressionReviewRequired"], + anyRequiredLabels: ["Approved-Suppression"], + troubleshootingGuide: + `The suppressions added to the AutoRest config files (README.mds) require review. ${diagramTsg(1, true)}, ` + + `or to step 3, depending on the kind of suppression you did.`, + }, +]; + +/** @type {RequiredLabelRule[]} */ +const rulesPri1ArcReview = [ + { + precedence: 1, + anyPrerequisiteLabels: ["ArcReview"], + anyRequiredLabels: ["ArcSignedOff"], + troubleshootingGuide: + "This PR is labelled with ArcReview. " + + "For this PR to be merged, it must pass an ARC review and be labelled ArcSignedOff.
" + + "Email the ARC board to request review per " + + `${href( + "this Contact section", + "https://msazure.visualstudio.com/One/_wiki/wikis/One.wiki/377428/Consistency-in-ARM-Modeling?anchor=contact", + )}.`, + }, +]; + +/** + * This collection has "SDK breaking changes" labels rules introduced in February 2024. + * For description of the "SDK breaking changes" labels, see: + * - https://gist.github.com/raych1/353949d19371b69fb82a10dd70032a51 + * - https://github.com/Azure/azure-sdk-tools/issues/6374#issuecomment-1897379880 + */ + +/** @type {RequiredLabelRule[]} */ +const rulesPri2Sdk = [ + { + precedence: 2, + anyPrerequisiteLabels: [sdkLabels["azure-sdk-for-go"].breakingChange], + anyRequiredLabels: [sdkLabels["azure-sdk-for-go"].breakingChangeApproved], + troubleshootingGuide: + `Your PR has breaking changes in the generated SDK for Go (label: ${ + sdkLabels["azure-sdk-for-go"].breakingChange + }). ` + `${diagramTsg(3, true)}.`, + }, + { + precedence: 2, + anyPrerequisiteLabels: [sdkLabels["azure-sdk-for-js"].breakingChange], + anyRequiredLabels: [sdkLabels["azure-sdk-for-js"].breakingChangeApproved], + troubleshootingGuide: + `Your PR has breaking changes in the generated SDK for JavaScript (label: ${ + sdkLabels["azure-sdk-for-js"].breakingChange + }). ` + `${diagramTsg(3, true)}.`, + }, + { + precedence: 2, + anyPrerequisiteLabels: [sdkLabels["azure-sdk-for-python"].breakingChange], + anyRequiredLabels: [sdkLabels["azure-sdk-for-python"].breakingChangeApproved], + troubleshootingGuide: + `Your PR has breaking changes in the generated SDK for Python (label: ${ + sdkLabels["azure-sdk-for-python"].breakingChange + }). ` + `${diagramTsg(3, true)}.`, + }, + { + precedence: 2, + anyPrerequisiteLabels: [sdkLabels["azure-sdk-for-python-track2"].breakingChange], + anyRequiredLabels: [sdkLabels["azure-sdk-for-python-track2"].breakingChangeApproved], + troubleshootingGuide: + `Your PR has breaking changes in the generated SDK for Python track-2 (label: ${ + sdkLabels["azure-sdk-for-python-track2"].breakingChange + }). ` + `${diagramTsg(3, true)}.`, + }, + { + precedence: 2, + anyPrerequisiteLabels: [sdkLabels["azure-sdk-for-go"].breakingChangeSuppression], + anyRequiredLabels: [sdkLabels["azure-sdk-for-go"].breakingChangeSuppressionApproved], + troubleshootingGuide: + `Your PR modified the suppressions for Go SDK breaking changes (label: ${ + sdkLabels["azure-sdk-for-go"].breakingChangeSuppression + }). ` + `${diagramTsg(3, true)}.`, + }, + { + precedence: 2, + anyPrerequisiteLabels: [sdkLabels["azure-sdk-for-js"].breakingChangeSuppression], + anyRequiredLabels: [sdkLabels["azure-sdk-for-js"].breakingChangeSuppressionApproved], + troubleshootingGuide: + `Your PR modified the suppressions for JavaScript SDK breaking changes (label: ${ + sdkLabels["azure-sdk-for-js"].breakingChangeSuppression + }). ` + `${diagramTsg(3, true)}.`, + }, + { + precedence: 2, + anyPrerequisiteLabels: [sdkLabels["azure-sdk-for-python"].breakingChangeSuppression], + anyRequiredLabels: [sdkLabels["azure-sdk-for-python"].breakingChangeSuppressionApproved], + troubleshootingGuide: + `Your PR modified the suppressions for Python SDK breaking changes (label: ${ + sdkLabels["azure-sdk-for-python"].breakingChangeSuppression + }). ` + `${diagramTsg(3, true)}.`, + }, + { + precedence: 2, + anyPrerequisiteLabels: [sdkLabels["azure-sdk-for-python-track2"].breakingChangeSuppression], + anyRequiredLabels: [sdkLabels["azure-sdk-for-python-track2"].breakingChangeSuppressionApproved], + troubleshootingGuide: + `Your PR modified the suppressions for Python track-2 SDK breaking changes (label: ${ + sdkLabels["azure-sdk-for-python-track2"].breakingChangeSuppression + }). ` + `${diagramTsg(3, true)}.`, + }, +]; + +/** + * This collection has "legacy" rules that are to be removed once migration to new rules is done. + * These rules have been marked as "legacy" in February 2024. + */ +/** @type {RequiredLabelRule[]} */ +const rulesPri2LegacySdk = [ + // -------------------- + // LEGACY / OBSOLETE RULES + // To be removed once migration to new rules is done + // -------------------- + + // CODESYNC These CI-BreakingChange-* rules must be in sync with + // \private\azure-rest-api-specs-pipeline\config\production\Azure\azure-rest-api-specs.yml + // \private\azure-rest-api-specs-pipeline\config\production\Azure\azure-rest-api-specs-pr.yml + { + precedence: 2, + anyPrerequisiteLabels: ["CI-BreakingChange-Go"], + anyRequiredLabels: ["Approved-SdkBreakingChange-Go"], + troubleshootingGuide: + `Your PR has breaking changes in the generated SDK for go (label: CI-BreakingChange-Go). ` + + `${diagramTsg(3, true)}.`, + }, + { + precedence: 2, + anyPrerequisiteLabels: ["CI-BreakingChange-Python-Track2"], + anyRequiredLabels: ["Approved-SdkBreakingChange-Python"], + troubleshootingGuide: + `Your PR has breaking changes in the generated SDK for python (label: CI-BreakingChange-Python-Track2). ` + + `${diagramTsg(3, true)}.`, + }, + { + precedence: 2, + anyPrerequisiteLabels: ["CI-BreakingChange-JavaScript"], + anyRequiredLabels: ["Approved-SdkBreakingChange-JavaScript"], + troubleshootingGuide: + `Your PR has breaking changes in the generated SDK for javascript (label: CI-BreakingChange-JavaScript). ` + + `${diagramTsg(3, true)}.`, + }, +]; + +/** @type {RequiredLabelRule[]} */ +const rulesPri3Blockers = [ + { + precedence: 3, + anyPrerequisiteLabels: ["Approved-OkToMerge"], + anyRequiredLabels: [], + troubleshootingGuide: + "This PR has been approved with Approved-OkToMerge label.
" + + "This label should be used only to approve PRs targeting private specs repo main branch.
" + + "PRs targeting this branch cannot be merged, as this branch is a mirror of the public specs repo main branch.
" + + "If you want to publish changes in this PR to the public repo, see: " + + `${href("aka.ms/azsdk/move-pr", "https://aka.ms/azsdk/move-pr")}.
` + + "If this label was applied erroneously to a different branch than private specs repo main branch, remove the label.", + }, +]; + +export const requiredLabelsRules = rulesPri0dataPlane + .concat(rulesPri0NotReadyForArmReview) + .concat(rulesPri0ArmRpaas) + .concat(rulesPri0Changes) + .concat(rulesPri0ArmRev) + .concat(rulesPri1ArmRev) + .concat(rulesPri1Suppressions) + .concat(rulesPri1ArcReview) + .concat(rulesPri2Sdk) + .concat(rulesPri2LegacySdk) + .concat(rulesPri3Blockers); + +/** + * @param {typeof import("@actions/core")} core + * @param {string} repo + * @param {string} owner + * @param {string[]} existingLabels + * @param {string} targetBranch + * + * @returns {Promise<{presentBlockingLabels: string[], missingRequiredLabels: string[]}>} + */ +export async function getPresentBlockingLabelsAndMissingRequiredLabels( + core, + repo, + owner, + existingLabels, + targetBranch, +) { + // this is a little bit of a strange looking branch format, but not going to touch this for now. + const repoTargetBranch = `${repo}/${targetBranch}`; + + const violatedReqLabelsRules = await getViolatedRequiredLabelsRules( + core, + existingLabels, + repoTargetBranch, + ); + const presentBlockingLabels = getPresentBlockingLabels(violatedReqLabelsRules); + + const requiredLabelsFromRules = ( + await getViolatedRequiredLabelsRules(core, existingLabels, repoTargetBranch) + ) + .filter((rule) => rule.anyRequiredLabels.length > 0) + // See comment on RequiredLabelRule.anyRequiredLabels to understand why we pick [0] from rule.anyRequiredLabels here. + .map((rule) => rule.anyRequiredLabels[0]); + + // Multiple rules may result in the same label being required, e.g. BreakingChange-Approved-* + // Deduplicate the array + const missingRequiredLabels = [...new Set(requiredLabelsFromRules)]; + + return { presentBlockingLabels, missingRequiredLabels }; +} + +/** + * @param {typeof import("@actions/core")} core + * @param {string[]} labels + * @param {string} targetBranch This function uses a special format {repo/branch}, e.g. "azure-rest-api-specs/main". + * @returns {Promise} + */ +export async function getViolatedRequiredLabelsRules(core, labels, targetBranch) { + const violatedRules = []; + for (const rule of requiredLabelsRules) { + if (await requiredLabelRuleViolated(core, labels, targetBranch, rule)) { + violatedRules.push(rule); + } + } + return violatedRules; +} + +/** + * @param {typeof import("@actions/core")} core + * @param {string[]} presentLabels + * @param {string} targetBranch + * @param {RequiredLabelRule} rule + * @returns {Promise} + */ +export async function requiredLabelRuleViolated(core, presentLabels, targetBranch, rule) { + const branchIsApplicable = rule.branches === undefined || rule.branches.includes(targetBranch); + + const anyPrerequisiteLabelPresent = + isAnyPrerequisiteLabelsNonempty(rule) && + anyLabelMatches(rule.anyPrerequisiteLabels || [], presentLabels); + + const allPrerequisiteLabelsPresent = + isAllPrerequisiteLabelsNonempty(rule) && + (rule.allPrerequisiteLabels || []).every((label) => presentLabels.includes(label)); + + const anyPrerequisiteAbsentLabelPresent = + isAllPrerequisiteAbsentLabelsNonempty(rule) && + anyLabelMatches(rule.allPrerequisiteAbsentLabels || [], presentLabels); + + const ruleIsApplicable = + branchIsApplicable && + (anyPrerequisiteLabelPresent || allPrerequisiteLabelsPresent) && + !anyPrerequisiteAbsentLabelPresent; + + const anyRequiredLabelPresent = anyLabelMatches(rule.anyRequiredLabels, presentLabels); + + const ruleIsViolated = ruleIsApplicable && !anyRequiredLabelPresent; + + core.debug( + `RETURN definition requiredLabelRuleViolated: ` + + `presentLabels: ${[...presentLabels].join(", ")}, ` + + `targetBranch: ${targetBranch}, ` + + `rule.precedence: ${rule.precedence}, ` + + `rule.branches: ${[...(rule.branches ?? [])].join(", ")}, ` + + `rule.anyPrerequisiteLabels: ${[...(rule.anyPrerequisiteLabels ?? [])].join(", ")}, ` + + `rule.allPrerequisiteLabels: ${[...(rule.allPrerequisiteLabels ?? [])].join(", ")}, ` + + `rule.allPrerequisiteAbsentLabels: ${[...(rule.allPrerequisiteAbsentLabels ?? [])].join(", ")}: ` + + `ruleIsViolated: ${ruleIsViolated}, branchIsApplicable: ${branchIsApplicable}, ` + + `ruleIsApplicable: ${ruleIsApplicable}, anyRequiredLabelPresent: ${anyRequiredLabelPresent}`, + ); + + return ruleIsViolated; +} + +/** + * @param {RequiredLabelRule[]} violatedReqLabelsRules + * @returns {string[]} + */ +export function getPresentBlockingLabels(violatedReqLabelsRules) { + return violatedReqLabelsRules + .filter((rule) => rule.anyRequiredLabels.length === 0) + .flatMap((rule) => rule.anyPrerequisiteLabels || []); +} + +/** + * Checks if any label in labelsToMatchAgainst matches any label in inputLabels + * @param {string[]} labelsToMatchAgainst - Labels to match against (can include prefix patterns ending with *) + * @param {string[]} inputLabels - Input labels to check + * @returns {boolean} True if any match is found + */ +export function anyLabelMatches(labelsToMatchAgainst, inputLabels) { + return getMatchingLabelIfAny(labelsToMatchAgainst, inputLabels) !== undefined; +} + +/** + * Gets the first matching label from labelsToMatchAgainst that matches any label in inputLabels + * @param {string[]} labelsToMatchAgainst - Labels to match against (can include prefix patterns ending with *) + * @param {string[]} inputLabels - Input labels to check + * @returns {string | undefined} The first matching label or undefined if no match + */ +export function getMatchingLabelIfAny(labelsToMatchAgainst, inputLabels) { + const matchingLabel = labelsToMatchAgainst.find((labelToMatchAgainstStr) => { + return inputLabels.some((inputLabel) => + isEqualToOrPrefixOf(labelToMatchAgainstStr, inputLabel), + ); + }); + + return matchingLabel; +} +/** + * + * @param {string} inputstring + * @param {string} label + * @returns {boolean} + */ +export function isEqualToOrPrefixOf(inputstring, label) { + return inputstring.endsWith("*") + ? label.startsWith(inputstring.slice(0, -1)) + : inputstring === label; +} +// #endregion diff --git a/.github/workflows/src/summarize-checks/summarize-checks.js b/.github/workflows/src/summarize-checks/summarize-checks.js new file mode 100644 index 000000000000..c04829a20e65 --- /dev/null +++ b/.github/workflows/src/summarize-checks/summarize-checks.js @@ -0,0 +1,1018 @@ +// @ts-check + +/* + This file is a github script. It will be called directly from a github-script action. This code is a simplified + amalgamation of logic that previously resided in the `PR Summary` check and various events within the `pipelinebot`. + Both from openapi-alps repo. + + It will trigger on: + + - label addition / removal to a PR + - when one of a set of required workflows configured in .github/workflows/summarize-checks.yaml completes + + While handling the incoming trigger, it will: + + - Apply or remove labels from the PR based on the status of the checks and other labels + - Create or update a comment that summarizes the user's "next steps to merge" on the PR. + + This script is a replacement for the old pipelinebot infrastructure from open-api-alps repository. +*/ + +// #region imports/constants +import { extractInputs } from "../context.js"; +// import { commentOrUpdate } from "../comment.js"; +import { execFile } from "../../../shared/src/exec.js"; +import { PER_PAGE_MAX } from "../github.js"; +import { + brChRevApproval, + getViolatedRequiredLabelsRules, + processArmReviewLabels, + processImpactAssessment, + verRevApproval, +} from "./labelling.js"; + +import { + brchTsg, + checkAndDiagramTsg, + defaultTsg, + diagramTsg, + reqMetCheckTsg, + typeSpecRequirementArmTsg, + typeSpecRequirementDataPlaneTsg, +} from "./tsgs.js"; + +import fs from "fs/promises"; +import os from "os"; +import path from "path"; + +/** + * @typedef {Object} CheckMetadata + * @property {number} precedence + * @property {string} name + * @property {string[]} suppressionLabels + * @property {string} troubleshootingGuide + */ + +/** + * @typedef {Object} CheckRunData + * @property {string} name + * @property {string} status + * @property {string} conclusion + * @property {CheckMetadata} checkInfo + */ + +/** + * @typedef {Object} WorkflowRunArtifact + * @property {string} name + * @property {number} id + * @property {string} url + * @property {string} archive_download_url + */ + +/** + * @typedef {Object} WorkflowRunInfo + * @property {string} name + * @property {number} id + * @property {number} databaseId + * @property {string} url + * @property {number} workflowId + * @property {string} status + * @property {string} conclusion + * @property {string} createdAt + * @property {string} updatedAt + */ + +/** + * @typedef {Object} GraphQLCheckRun + * @property {string} name + * @property {string} status + * @property {string} conclusion + * @property {boolean} isRequired + */ + +/** + * @typedef {Object} GraphQLCheckSuite + * @property {GraphQLCheckRun[]} nodes + */ + +/** + * @typedef {Object} GraphQLCheckSuites + * @property {GraphQLCheckSuite[]} nodes + */ + +/** + * @typedef {Object} GraphQLCommit + * @property {GraphQLCheckSuites} checkSuites + */ + +/** + * @typedef {Object} GraphQLResource + * @property {GraphQLCheckSuites} checkSuites + */ + +/** + * @typedef {Object} GraphQLResponse + * @property {GraphQLResource} resource + * @property {Object} rateLimit + * @property {number} rateLimit.limit + * @property {number} rateLimit.cost + * @property {number} rateLimit.used + * @property {number} rateLimit.remaining + * @property {string} rateLimit.resetAt + */ + +/** + * @typedef {import("./labelling.js").RequiredLabelRule} RequiredLabelRule + */ + +// Placing these configuration items here until we decide another way to pull them in. +const FYI_CHECK_NAMES = [ + "Swagger LintDiff", + "SDK Validation Status", + "Swagger BreakingChange", + "Swagger PrettierCheck", +]; +const AUTOMATED_CHECK_NAME = "Automated merging requirements met"; +const NEXT_STEPS_COMMENT_ID = "NextStepsToMerge"; + +/** @type {CheckMetadata[]} */ +const CHECK_METADATA = [ + { + precedence: 0, + name: "TypeSpec Requirement (resource-manager)", + suppressionLabels: [], + troubleshootingGuide: typeSpecRequirementArmTsg, + }, + { + precedence: 0, + name: "TypeSpec Requirement (data-plane)", + suppressionLabels: [], + troubleshootingGuide: typeSpecRequirementDataPlaneTsg, + }, + { + precedence: 0, + name: "TypeSpec Validation", + suppressionLabels: [], + troubleshootingGuide: defaultTsg, + }, + { + precedence: 0, + name: "license/cla", + suppressionLabels: [], + troubleshootingGuide: defaultTsg, + }, + { + precedence: 1, + name: "Swagger Avocado", + suppressionLabels: [], + troubleshootingGuide: defaultTsg, + }, + { + precedence: 1, + name: "Swagger SpellCheck", + suppressionLabels: [], + troubleshootingGuide: defaultTsg, + }, + { + precedence: 1, + name: "Swagger PrettierCheck", + suppressionLabels: [], + troubleshootingGuide: defaultTsg, + }, + { + precedence: 2, + name: "Swagger SemanticValidation", + suppressionLabels: [], + troubleshootingGuide: defaultTsg, + }, + { + precedence: 3, + name: "Swagger ModelValidation", + suppressionLabels: [], + troubleshootingGuide: defaultTsg, + }, + { + precedence: 4, + name: "Swagger BreakingChange", + suppressionLabels: [verRevApproval, brChRevApproval], + troubleshootingGuide: brchTsg, + }, + { + precedence: 4, + name: "Breaking Change(Cross-Version)", + suppressionLabels: [verRevApproval, brChRevApproval], + troubleshootingGuide: brchTsg, + }, + { + precedence: 5, + name: "Swagger LintDiff", + suppressionLabels: [], + troubleshootingGuide: defaultTsg, + }, + { + precedence: 5, + name: "Swagger Lint(RPaaS)", + suppressionLabels: [], + troubleshootingGuide: defaultTsg, + }, + { + precedence: 6, + name: "SDK azure-sdk-for-net", + suppressionLabels: [], + troubleshootingGuide: checkAndDiagramTsg(3), + }, + { + precedence: 6, + name: "SDK azure-sdk-for-net-track2", + suppressionLabels: [], + troubleshootingGuide: checkAndDiagramTsg(3), + }, + { + precedence: 6, + name: "SDK azure-sdk-for-go", + suppressionLabels: [], + troubleshootingGuide: checkAndDiagramTsg(3), + }, + { + precedence: 6, + name: "SDK azure-sdk-for-java", + suppressionLabels: [], + troubleshootingGuide: checkAndDiagramTsg(3), + }, + { + precedence: 6, + name: "SDK azure-sdk-for-js", + suppressionLabels: [], + troubleshootingGuide: checkAndDiagramTsg(3), + }, + { + precedence: 6, + name: "SDK azure-sdk-for-python", + suppressionLabels: [], + troubleshootingGuide: checkAndDiagramTsg(3), + }, + { + precedence: 6, + name: "SDK azure-sdk-for-python-track2", + suppressionLabels: [], + troubleshootingGuide: checkAndDiagramTsg(3), + }, + { + precedence: 10, + name: AUTOMATED_CHECK_NAME, + suppressionLabels: [], + troubleshootingGuide: reqMetCheckTsg, + }, +]; + +// during renderAutomatedMergingRequirementsMetCheck we resolve the result of +// automated merge requirements met by from the result of and(requiredChecks). +// if any are pending, automated merging requirements is pending. This is ripe for complete removal +// in favor of just honoring the `required` checks results directly. +/** @type {string[]} */ +const EXCLUDED_CHECK_NAMES = []; + +// #endregion +// #region core +/** + * @param {import('@actions/github-script').AsyncFunctionArguments} AsyncFunctionArguments + * @returns {Promise} + */ +export default async function summarizeChecks({ github, context, core }) { + let { owner, repo, issue_number, head_sha } = await extractInputs(github, context, core); + const targetBranch = context.payload.pull_request?.base?.ref; + core.info(`PR target branch: ${targetBranch}`); + + if (!issue_number) { + core.info( + "This summarize-checks was triggered off a workflow that doesn't provide the issue-number artifact, early exiting.", + ); + return; + } + + await summarizeChecksImpl( + github, + context, + core, + owner, + repo, + issue_number, + head_sha, + context.eventName, + targetBranch, + ); +} + +/** + * @param {import('@actions/github-script').AsyncFunctionArguments['github']} github + * @param {import('@actions/github').context } context + * @param {typeof import("@actions/core")} core + * @param {string} owner + * @param {string} repo + * @param {number} issue_number + * @param {string} head_sha + * @param {string} event_name + * @param {string} targetBranch + * @returns {Promise} + */ +export async function summarizeChecksImpl( + github, + context, + core, + owner, + repo, + issue_number, + head_sha, + event_name, + targetBranch, +) { + core.info( + `Handling ${event_name} event for PR #${issue_number} in ${owner}/${repo}@${head_sha}.`, + ); + + // retrieve latest labels state + const labels = await github.paginate(github.rest.issues.listLabelsOnIssue, { + owner, + repo, + issue_number: issue_number, + per_page: PER_PAGE_MAX, + }); + + /** @type {[CheckRunData[], CheckRunData[], import("./labelling.js").ImpactAssessment | undefined]} */ + const [requiredCheckRuns, fyiCheckRuns, impactAssessment] = await getCheckRunTuple( + github, + core, + owner, + repo, + head_sha, + issue_number, + EXCLUDED_CHECK_NAMES, + ); + + /** @type {string[]} */ + let labelNames = labels.map((/** @type {{ name: string; }} */ label) => label.name); + + let labelContext = await updateLabels(labelNames, impactAssessment); + + core.info( + `Summarize checks label actions against ${owner}/${repo}#${issue_number}: \n` + + `The following labels were present: [${Array.from(labelContext.present).join(", ")}] \n` + + `Removing labels: [${Array.from(labelContext.toRemove).join(", ")}] \n` + + `Adding labels: [${Array.from(labelContext.toAdd).join(", ")}]`, + ); + + // for (const label of labelContext.toRemove) { + // core.info(`Removing label: ${label} from ${owner}/${repo}#${issue_number}.`); + // await github.rest.issues.removeLabel({ + // owner: owner, + // repo: repo, + // issue_number: issue_number, + // name: label, + // }); + // } + + // if (labelContext.toAdd.size > 0) { + // core.info( + // `Adding labels: ${Array.from(labelContext.toAdd).join(", ")} to ${owner}/${repo}#${issue_number}.`, + // ); + // await github.rest.issues.addLabels({ + // owner: owner, + // repo: repo, + // issue_number: issue_number, + // labels: Array.from(labelContext.toAdd), + // }); + // } + + // adjust labelNames based on labelsToAdd/labelsToRemove + labelNames = labelNames.filter((name) => !labelContext.toRemove.has(name)); + for (const label of labelContext.toAdd) { + if (!labelNames.includes(label)) { + labelNames.push(label); + } + } + + const [commentBody, automatedChecksMet] = await createNextStepsComment( + core, + repo, + labelNames, + targetBranch, + requiredCheckRuns, + fyiCheckRuns, + ); + + core.info( + `Updating comment '${NEXT_STEPS_COMMENT_ID}' on ${owner}/${repo}#${issue_number} with body: ${commentBody}`, + ); + // this will remain commented until we're comfortable with the change. + // await commentOrUpdate( + // { github, context, core }, + // owner, + // repo, + // issue_number, + // commentName, + // commentBody + // ) + + core.info( + `Summarize checks has identified that status of "Automated merging requirements met" check should be updated to: ${automatedChecksMet}.`, + ); +} + +/** + * A GraphQL query to GitHub API that returns all check runs for given commit, with "isRequired" field for given PR. + * + * If you want to see example response, copy the query body into this: + * https://docs.github.com/en/graphql/overview/explorer + * Example inputs: + * resourceUrl: "https://github.com/test-repo-billy/azure-rest-api-specs/commit/c2789c5bde1b3f4fa34f76a8eeaaed479df23c4d" + * prNumber: 2996 + * + * Reference: + * https://docs.github.com/en/graphql/reference/queries#resource + * https://docs.github.com/en/graphql/guides/using-global-node-ids#3-do-a-direct-node-lookup-in-graphql + * https://docs.github.com/en/graphql/reference/objects#checkrun + * Rate limit: + * https://docs.github.com/en/graphql/overview/resource-limitations#rate-limit + * https://docs.github.com/en/graphql/reference/objects#ratelimit + * + * Note: here, for "checkRuns(first: ..)", maybe we should add a filter that filters to LATEST, per: + * https://docs.github.com/en/graphql/reference/input-objects#checkrunfilter + * https://docs.github.com/en/graphql/reference/enums#checkruntype + **/ +/** + * Returns a GraphQL query string for the given resource URL and PR number. + * + * @param {string} owner - The URL of the GitHub resource (commit). + * @param {string} repo - The URL of the GitHub resource (commit). + * @param {string} sha - targeted commit. context.pr!.headInfo.sha + * @param {number} prNumber - The pull request number. + * @returns {string} The GraphQL query string. + */ +function getGraphQLQuery(owner, repo, sha, prNumber) { + const resourceUrl = `https://github.com/${owner}/${repo}/commit/${sha}`; + + return ` + { + resource(url: "${resourceUrl}") { + ... on Commit { + checkSuites(first: 20) { + nodes { + workflowRun { + id + databaseId + workflow { + name + } + } + checkRuns(first: 30) { + nodes { + name + status + conclusion + isRequired(pullRequestNumber: ${prNumber}) + } + } + } + } + } + } + rateLimit { + limit + cost + used + remaining + resetAt + } + } + `; +} + +// #endregion +// #region label update +/** + * @param {Set} labelsToAdd + * @param {Set} labelsToRemove + */ +function warnIfLabelSetsIntersect(labelsToAdd, labelsToRemove) { + const intersection = Array.from(labelsToAdd).filter((label) => labelsToRemove.has(label)); + if (intersection.length > 0) { + console.warn( + "ASSERTION VIOLATION! The intersection of labelsToRemove and labelsToAdd is non-empty! " + + `labelsToAdd: [${[...labelsToAdd].join(", ")}]. ` + + `labelsToRemove: [${[...labelsToRemove].join(", ")}]. ` + + `intersection: [${intersection.join(", ")}].`, + ); + } +} + +// * @param {string} eventName +// * @param {string | undefined } changedLabel +/** + * @param {string[]} existingLabels + * @param {import("./labelling.js").ImpactAssessment | undefined} impactAssessment + * @returns {import("./labelling.js").LabelContext} + */ +export function updateLabels(existingLabels, impactAssessment) { + // logic for this function originally present in: + // - private/openapi-kebab/src/bots/pipeline/pipelineBotOnPRLabelEvent.ts + // - public/rest-api-specs-scripts/src/prSummary.ts + // it has since been simplified and moved here to handle all label addition and subtraction given a PR context + + /** @type {import("./labelling.js").LabelContext} */ + const labelContext = { + present: new Set(existingLabels), + toAdd: new Set(), + toRemove: new Set(), + }; + + if (impactAssessment) { + console.log(`Downloaded impact assessment: ${JSON.stringify(impactAssessment)}`); + // Merge impact assessment labels into the main labelContext + impactAssessment.labelContext.toAdd.forEach((label) => { + labelContext.toAdd.add(label); + }); + impactAssessment.labelContext.toRemove.forEach((label) => { + labelContext.toRemove.add(label); + }); + } + + // this is the only labelling that was part of original pipelinebot logic + processArmReviewLabels(labelContext, existingLabels); + + if (impactAssessment) { + // will further update the label context if necessary + processImpactAssessment( + impactAssessment.targetBranch, + labelContext, + impactAssessment.resourceManagerRequired, + impactAssessment.rpaasRPMissing, + impactAssessment.rpaasExceptionRequired, + impactAssessment.rpaasRpNotInPrivateRepo, + impactAssessment.isNewApiVersion, + impactAssessment.isDraft, + ); + } + + warnIfLabelSetsIntersect(labelContext.toAdd, labelContext.toRemove); + return labelContext; +} + +// #endregion +// #region checks +/** + * @param {import('@actions/github-script').AsyncFunctionArguments['github']} github + * @param {typeof import("@actions/core")} core + * @param {string} owner - The repository owner. + * @param {string} repo - The repository name. + * @param {string} head_sha - The commit SHA to check. + * @param {number} prNumber - The pull request number. + * @param {string[]} excludedCheckNames + * @returns {Promise<[CheckRunData[], CheckRunData[], import("./labelling.js").ImpactAssessment | undefined]>} + */ +export async function getCheckRunTuple( + github, + core, + owner, + repo, + head_sha, + prNumber, + excludedCheckNames, +) { + // This function was originally a version of getRequiredAndFyiAndAutomatedMergingRequirementsMetCheckRuns + // but has been simplified for clarity and purpose. + /** @type {CheckRunData[]} */ + let reqCheckRuns = []; + /** @type {CheckRunData[]} */ + let fyiCheckRuns = []; + + /** @type {number | undefined} */ + let impactAssessmentWorkflowRun = undefined; + + /** @type { import("./labelling.js").ImpactAssessment | undefined } */ + let impactAssessment = undefined; + + const response = await github.graphql(getGraphQLQuery(owner, repo, head_sha, prNumber)); + core.info(`GraphQL Rate Limit Information: ${JSON.stringify(response.rateLimit)}`); + + [reqCheckRuns, fyiCheckRuns, impactAssessmentWorkflowRun] = + extractRunsFromGraphQLResponse(response); + + if (impactAssessmentWorkflowRun) { + core.info( + `Impact Assessment Workflow Run ID is present: ${impactAssessmentWorkflowRun}. Downloading job summary artifact`, + ); + impactAssessment = await getImpactAssessment( + github, + core, + owner, + repo, + impactAssessmentWorkflowRun, + ); + } + + core.info( + `RequiredCheckRuns: ${JSON.stringify(reqCheckRuns)}, ` + + `FyiCheckRuns: ${JSON.stringify(fyiCheckRuns)}, ` + + `ImpactAssessment: ${JSON.stringify(impactAssessment)}`, + ); + const filteredReqCheckRuns = reqCheckRuns.filter( + /** + * @param {CheckRunData} checkRun + */ + (checkRun) => !excludedCheckNames.includes(checkRun.name), + ); + const filteredFyiCheckRuns = fyiCheckRuns.filter( + /** + * @param {CheckRunData} checkRun + */ + (checkRun) => !excludedCheckNames.includes(checkRun.name), + ); + + return [filteredReqCheckRuns, filteredFyiCheckRuns, impactAssessment]; +} + +/** + * @param {CheckRunData} checkRun + * @returns {boolean | undefined} + */ +export function checkRunIsSuccessful(checkRun) { + // If the check is still queued or in progress, return undefined + const status = checkRun.status.toLowerCase(); + if (status === "queued" || status === "in_progress") { + return undefined; + } + + // At this point we expect a completed run, so conclusion should be defined + const conclusion = checkRun.conclusion?.toLowerCase(); + if (conclusion == null) { + return undefined; + } + + // Return true for success or neutral, false for any other conclusion + return conclusion === "success" || conclusion === "neutral"; +} + +/** + * @param {any} response - GraphQL response data + * @returns {[CheckRunData[], CheckRunData[], number | undefined]} + */ +function extractRunsFromGraphQLResponse(response) { + /** @type {CheckRunData[]} */ + const reqCheckRuns = []; + /** @type {CheckRunData[]} */ + const fyiCheckRuns = []; + + /** @type {number | undefined} */ + let impactAssessmentWorkflowRun = undefined; + + // Define the automated merging requirements check name + + if (response.resource?.checkSuites?.nodes) { + response.resource.checkSuites.nodes.forEach( + /** @param {{ workflowRun?: WorkflowRunInfo, checkRuns?: { nodes?: any[] } }} checkSuiteNode */ + (checkSuiteNode) => { + if (checkSuiteNode.checkRuns?.nodes) { + checkSuiteNode.checkRuns.nodes.forEach((checkRunNode) => { + // We have some specific guidance for some of the required checks. + const checkInfo = + CHECK_METADATA.find((metadata) => metadata.name === checkRunNode.name) || + /** @type {CheckMetadata} */ ({ + precedence: 1000, + name: checkRunNode.name, + suppressionLabels: [], + troubleshootingGuide: defaultTsg, + }); + + if (checkRunNode.isRequired) { + reqCheckRuns.push({ + name: checkRunNode.name, + status: checkRunNode.status, + conclusion: checkRunNode.conclusion, + checkInfo: checkInfo, + }); + } + // Note the "else" here. It means that: + // A GH check will be bucketed into "failing FYI check run" if: + // - It is failing + // - AND it is is NOT marked as 'required' in GitHub branch policy + // - AND it is marked as 'FYI' in this file's FYI_CHECK_NAMES array + else if (FYI_CHECK_NAMES.includes(checkRunNode.name)) { + fyiCheckRuns.push({ + name: checkRunNode.name, + status: checkRunNode.status, + conclusion: checkRunNode.conclusion, + checkInfo: checkInfo, + }); + } + }); + } + }, + ); + } + + // extract the ImpactAssessment check run if it is completed and successful + if (response.resource?.checkSuites?.nodes) { + response.resource.checkSuites.nodes.forEach( + /** @param {{ workflowRun?: WorkflowRunInfo, checkRuns?: { nodes?: any[] } }} checkSuiteNode */ + (checkSuiteNode) => { + if (checkSuiteNode.checkRuns?.nodes) { + checkSuiteNode.checkRuns.nodes.forEach((checkRunNode) => { + if ( + checkRunNode.name === "[TEST-IGNORE] Summarize PR Impact" && + checkRunNode.status?.toLowerCase() === "completed" && + checkRunNode.conclusion?.toLowerCase() === "success" + ) { + // Assign numeric databaseId, not the string node ID + impactAssessmentWorkflowRun = checkSuiteNode.workflowRun?.databaseId; + } + }); + } + }, + ); + } + + return [reqCheckRuns, fyiCheckRuns, impactAssessmentWorkflowRun]; +} +// #endregion +// #region next steps +/** + * + * @param {typeof import("@actions/core")} core + * @param {string} repo + * @param {string[]} labels + * @param {string} targetBranch + * @param {CheckRunData[]} requiredRuns + * @param {CheckRunData[]} fyiRuns + * @returns {Promise<[string, string]>} + */ +export async function createNextStepsComment( + core, + repo, + labels, + targetBranch, + requiredRuns, + fyiRuns, +) { + // select just the metadata that we need about the runs. + const requiredCheckInfos = requiredRuns + .filter((run) => checkRunIsSuccessful(run) === false) + .map((run) => run.checkInfo); + + // determine if required runs have any successful runs. + const requiredCheckInfosPresent = requiredRuns.some((run) => { + const status = run.status.toLowerCase(); + return status !== "queued" && status !== "in_progress"; + }); + const fyiCheckInfos = fyiRuns + .filter((run) => checkRunIsSuccessful(run) === false) + .map((run) => run.checkInfo); + + const [commentBody, automatedChecksMet] = await buildNextStepsToMergeCommentBody( + core, + labels, + `${repo}/${targetBranch}`, + requiredCheckInfosPresent, + requiredCheckInfos, + fyiCheckInfos, + ); + + return [commentBody, automatedChecksMet]; +} + +/** + * @param {import("@actions/core")} core + * @param {string[]} labels + * @param {string} targetBranch // this is in the format of "repo/branch" + * @param {boolean} requiredCheckInfosPresent + * @param {CheckMetadata[]} failingReqChecksInfo + * @param {CheckMetadata[]} failingFyiChecksInfo + * @returns {Promise<[string, string]>} + */ +async function buildNextStepsToMergeCommentBody( + core, + labels, + targetBranch, + requiredCheckInfosPresent, + failingReqChecksInfo, + failingFyiChecksInfo, +) { + // Build the comment header + const commentTitle = `

Next Steps to Merge

`; + + const violatedReqLabelsRules = await getViolatedRequiredLabelsRules(core, labels, targetBranch); + + // this is the first place of adjusted logic. I am treating `requirementsMet` as `no failed required checks`. + // I do this because the `automatedMergingRequirementsMetCheckRun` WILL NOT BE PRESENT in the new world. + // The new world we will simply pull all the required checks and if any are failing then we are blocked. If there are + // no failed checks we can't yet say that everything is met, because a check MIGHT run in the future. To prevent + // this "no checks run" accidentally evaluating as success, we need to ensure that we have at least one failing check + // in the required checks to consider the requirements met + const anyBlockerPresent = failingReqChecksInfo.length > 0 || violatedReqLabelsRules.length > 0; + const anyFyiPresent = failingFyiChecksInfo.length > 0; + const requirementsMet = !anyBlockerPresent && requiredCheckInfosPresent; + + // Compose the body based on the current state + const [commentBody, automatedChecksMet] = getCommentBody( + requirementsMet, + anyBlockerPresent, + anyFyiPresent, + failingReqChecksInfo, + failingFyiChecksInfo, + violatedReqLabelsRules, + ); + + return [commentTitle + commentBody, automatedChecksMet]; +} + +/** + * Gets the proper body content based on requirements status + * @param {boolean} requirementsMet - Whether all requirements are met + * @param {boolean} anyBlockerPresent - Whether any blockers are present + * @param {boolean} anyFyiPresent - Whether any FYI issues are present + * @param {CheckMetadata[]} failingReqChecksInfo - Failing required checks info + * @param {CheckMetadata[]} failingFyiChecksInfo - Failing FYI checks info + * @param {RequiredLabelRule[]} violatedRequiredLabelsRules - Violated required label rules + * @returns {[string, string]} The body content HTML and the status that automated checks met should be set to. + */ +function getCommentBody( + requirementsMet, + anyBlockerPresent, + anyFyiPresent, + failingReqChecksInfo, + failingFyiChecksInfo, + violatedRequiredLabelsRules, +) { + let bodyProper = ""; + let automatedChecksMet = "pending"; + + if (anyBlockerPresent || anyFyiPresent) { + if (anyBlockerPresent) { + bodyProper += getBlockerPresentBody(failingReqChecksInfo, violatedRequiredLabelsRules); + automatedChecksMet = "blocked"; + } + + if (anyBlockerPresent && anyFyiPresent) { + bodyProper += "
"; + } + + if (anyFyiPresent) { + bodyProper += getFyiPresentBody(failingFyiChecksInfo); + if (!anyBlockerPresent) { + bodyProper += `If you still want to proceed merging this PR without addressing the above failures, ${diagramTsg(4, false)}.`; + } + } + } else if (requirementsMet) { + automatedChecksMet = "success"; + bodyProper = + `✅ All automated merging requirements have been met! ` + + `To get your PR merged, see aka.ms/azsdk/specreview/merge.`; + } else { + bodyProper = + "⌛ Please wait. Next steps to merge this PR are being evaluated by automation. ⌛"; + } + return [bodyProper, automatedChecksMet]; +} + +/** + * Gets the body content when blockers are present + * @param {CheckMetadata[]} failingRequiredChecks - Failing required checks + * @param {RequiredLabelRule[]} violatedRequiredLabelsRules - Violated required label rules + * @returns {string} The blocker present body HTML + */ +function getBlockerPresentBody(failingRequiredChecks, violatedRequiredLabelsRules) { + const failingRequiredChecksNextStepsText = buildFailingChecksNextStepsText( + failingRequiredChecks, + "required", + ); + const violatedReqLabelsRulesNextStepsText = buildViolatedLabelRulesNextStepsText( + violatedRequiredLabelsRules, + ); + return ( + "Next steps that must be taken to merge this PR:
" + + "
    " + + violatedReqLabelsRulesNextStepsText + + failingRequiredChecksNextStepsText + + "
" + ); +} + +/** + * Gets the body content when FYI issues are present + * @param {CheckMetadata[]} failingFyiChecksInfo - Failing FYI checks info + * @returns {string} The FYI present body HTML + */ +function getFyiPresentBody(failingFyiChecksInfo) { + return ( + "Important checks have failed. As of today they are not blocking this PR, but in near future they may.
" + + "Addressing the following failures is highly recommended:
" + + "
    " + + buildFailingChecksNextStepsText(failingFyiChecksInfo, "FYI") + + "
" + ); +} + +/** + * Builds next steps text for failing checks + * @param {CheckMetadata[]} failingChecks - Array of failing checks + * @param {"required" | "FYI"} checkKind - Kind of check (required or FYI) + * @returns {string} The failing checks next steps HTML + */ +function buildFailingChecksNextStepsText(failingChecks, checkKind) { + let failingChecksNextStepsText = ""; + if (failingChecks.length > 0) { + const minPrecedence = Math.min(...failingChecks.map((check) => check.precedence)); + const checksToDisplay = failingChecks.filter((check) => check.precedence === minPrecedence); + + // assert: checksToDisplay.length > 0 + failingChecksNextStepsText = checksToDisplay + .map((check) => + checkKind === "required" + ? `
  • ❌ The required check named ${check.name} has failed. ${check.troubleshootingGuide}
  • ` + : `
  • ⚠️ The check named ${check.name} has failed. ${check.troubleshootingGuide}
  • `, + ) + .join(""); + } + return failingChecksNextStepsText; +} + +/** + * Builds next steps text for violated required label rules + * @param {RequiredLabelRule[]} violatedRequiredLabelsRules - Array of violated required label rules + * @returns {string} The violated label rules next steps HTML + */ +function buildViolatedLabelRulesNextStepsText(violatedRequiredLabelsRules) { + let violatedReqLabelsNextStepsText = ""; + if (violatedRequiredLabelsRules.length > 0) { + const minPrecedence = Math.min(...violatedRequiredLabelsRules.map((rule) => rule.precedence)); + const rulesToDisplay = violatedRequiredLabelsRules.filter( + (rule) => rule.precedence == minPrecedence, + ); + // assert: rulesToDisplay.length > 0 + violatedReqLabelsNextStepsText = rulesToDisplay + .map((rule) => `
  • ❌ ${rule.troubleshootingGuide}
  • `) + .join(""); + } + return violatedReqLabelsNextStepsText; +} +// #endregion + +// #region artifact downloading +/** + * Downloads the job-summary artifact for a given workflow run. + * @param {import('@actions/github-script').AsyncFunctionArguments['github']} github + * @param {typeof import("@actions/core")} core + * @param {string} owner + * @param {string} repo + * @param {number} runId - The workflow run databaseId + * @returns {Promise} The parsed job summary data + */ +export async function getImpactAssessment(github, core, owner, repo, runId) { + try { + // List artifacts for provided workflow run + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner, + repo, + run_id: runId, + }); + + // Find the job-summary artifact + const jobSummaryArtifact = artifacts.data.artifacts.find( + (artifact) => artifact.name === "job-summary", + ); + + if (!jobSummaryArtifact) { + core.info("No job-summary artifact found"); + return undefined; + } + + // Download the artifact as a zip archive + const download = await github.rest.actions.downloadArtifact({ + owner, + repo, + artifact_id: jobSummaryArtifact.id, + archive_format: "zip", + }); + + core.info(`Successfully downloaded job-summary artifact ID: ${jobSummaryArtifact.id}`); + + // Write zip buffer to temp file and extract JSON + const tmpZip = path.join(process.env.RUNNER_TEMP || os.tmpdir(), `job-summary-${runId}.zip`); + // Convert ArrayBuffer to Buffer + // Convert ArrayBuffer (download.data) to Node Buffer + const arrayBuffer = /** @type {ArrayBuffer} */ (download.data); + const zipBuffer = Buffer.from(new Uint8Array(arrayBuffer)); + await fs.writeFile(tmpZip, zipBuffer); + // Extract JSON content from zip archive + const { stdout: jsonContent } = await execFile("unzip", ["-p", tmpZip]); + await fs.unlink(tmpZip); + + /** @type {import("./labelling.js").ImpactAssessment} */ + // todo: we need to zod this to ensure the structure is correct, however we do not have zod installed at time of run + const impact = JSON.parse(jsonContent); + return impact; + } catch (/** @type {any} */ error) { + core.error(`Failed to download job summary artifact: ${error.message}`); + return undefined; + } +} +// #endregion diff --git a/.github/workflows/src/summarize-checks/tsgs.js b/.github/workflows/src/summarize-checks/tsgs.js new file mode 100644 index 000000000000..57604a529dd2 --- /dev/null +++ b/.github/workflows/src/summarize-checks/tsgs.js @@ -0,0 +1,78 @@ +/** + * tsgs.ts defines TroubleShooting Guides. These typically end up being presented to the PR reader + * inside the 'Next Steps to Merge' comment. + **/ + +/** + * @param {number} step + * @returns {string} + */ +export function checkAndDiagramTsg(step) { + return defaultTsg + `. In addition, ` + diagramTsg(step); +} + +/** + * @param {number} step + * @param {boolean} [capitalizeFirstLetter=false] + * @returns {string} + */ +export function diagramTsg(step, capitalizeFirstLetter = false) { + const firstLetter = capitalizeFirstLetter ? "R" : "r"; + return `${firstLetter}efer to step ${step} in the ${diagramHtmlAnchor}`; +} + +const diagramUrl = "https://aka.ms/azsdk/pr-diagram"; + +const diagramHtmlAnchor = `PR workflow diagram`; + +/** @type {string} */ +export const defaultTsg = `Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide`; + +export const reqMetCheckTsg = `This is the final check that must pass. ${checkAndDiagramTsg(4)}`; + +export const typeSpecRequirementArmTsg = + `TypeSpec usage is required for all new (aka "greenfield") services. ` + + `The automation detected this pull request adds at least one new service that is violating this requirement. ` + + `For information on converting from OpenAPI specs to TypeSpec specs or on data-plane (DP) policies, ` + + `refer to aka.ms/azsdk/typespec. ` + + `If you have general questions on resource provider (RP) policies, ` + + `refer to aka.ms/rphelp`; + +export const typeSpecRequirementDataPlaneTsg = typeSpecRequirementArmTsg; + +export const brchHref = href("aka.ms/brch", "https://aka.ms/brch"); + +export const brchTsg = `To unblock this PR, follow the process at ${brchHref}.`; + +/** + * @param {string} message + * @returns {string} + */ +export function wrapInArmReviewMessage(message) { + const armReviewPrefix = + "This PR is in purview of the ARM review (label: ARMReview). " + + "This PR must get ARMSignedOff label from an ARM reviewer.
    "; + + const armReviewSuffix = + `
    For details of the ARM review, see ` + + `${href("aka.ms/azsdk/pr-arm-review", "https://aka.ms/azsdk/pr-arm-review")}
    `; + + return armReviewPrefix + message + armReviewSuffix; +} + +/** + * @param {string} label + * @returns {string} + */ +export function notReadyForArmReviewReason(label) { + return `This PR is NotReadyForARMReview because it has the ${label} label.
    `; +} + +/** + * @param {string} text + * @param {string} url + * @returns {string} + */ +export function href(text, url) { + return `${text}`; +} diff --git a/.github/workflows/src/update-labels.js b/.github/workflows/src/update-labels.js index 1ec70e2136ca..a7664d21fb6f 100644 --- a/.github/workflows/src/update-labels.js +++ b/.github/workflows/src/update-labels.js @@ -4,22 +4,10 @@ import { extractInputs } from "../src/context.js"; import { PER_PAGE_MAX } from "./github.js"; /** - * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments + * @param {import('@actions/github-script').AsyncFunctionArguments} AsyncFunctionArguments */ export default async function updateLabels({ github, context, core }) { - let owner = process.env.OWNER; - let repo = process.env.REPO; - let issue_number = parseInt(process.env.ISSUE_NUMBER || ""); - let run_id = parseInt(process.env.RUN_ID || ""); - - if (!owner || !repo || !(issue_number || run_id)) { - let inputs = await extractInputs(github, context, core); - owner = owner || inputs.owner; - repo = repo || inputs.repo; - issue_number = issue_number || inputs.issue_number; - run_id = run_id || inputs.run_id; - } - + const { owner, repo, issue_number, run_id } = await extractInputs(github, context, core); await updateLabelsImpl({ owner, repo, issue_number, run_id, github, core }); } @@ -32,29 +20,19 @@ export default async function updateLabels({ github, context, core }) { * @param {(import("@octokit/core").Octokit & import("@octokit/plugin-rest-endpoint-methods/dist-types/types.js").Api & { paginate: import("@octokit/plugin-paginate-rest").PaginateInterface; })} params.github * @param {typeof import("@actions/core")} params.core */ -export async function updateLabelsImpl({ - owner, - repo, - issue_number, - run_id, - github, - core, -}) { +export async function updateLabelsImpl({ owner, repo, issue_number, run_id, github, core }) { /** @type {string[]} */ let artifactNames = []; if (run_id) { // List artifacts from a single run_id core.info(`listWorkflowRunArtifacts(${owner}, ${repo}, ${run_id})`); - const artifacts = await github.paginate( - github.rest.actions.listWorkflowRunArtifacts, - { - owner: owner, - repo: repo, - run_id: run_id, - per_page: PER_PAGE_MAX, - }, - ); + const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, { + owner: owner, + repo: repo, + run_id: run_id, + per_page: PER_PAGE_MAX, + }); artifactNames = artifacts.map((a) => a.name); } else { @@ -97,10 +75,7 @@ export async function updateLabelsImpl({ core.info(`labelsToAdd: ${JSON.stringify(labelsToAdd)}`); core.info(`labelsToRemove: ${JSON.stringify(labelsToRemove)}`); - if ( - (labelsToAdd.length > 0 || labelsToRemove.length > 0) && - Number.isNaN(issue_number) - ) { + if ((labelsToAdd.length > 0 || labelsToRemove.length > 0) && Number.isNaN(issue_number)) { throw new Error( `Invalid value for 'issue_number':${issue_number}. Expected an 'issue-number' artifact created by the workflow run.`, ); @@ -129,11 +104,7 @@ export async function updateLabelsImpl({ name: name, }); } catch (error) { - if ( - error instanceof Error && - "status" in error && - error.status === 404 - ) { + if (error instanceof Error && "status" in error && error.status === 404) { core.info(`Ignoring error: ${error.status} - ${error.message}`); } else { throw error; diff --git a/.github/workflows/src/verify-run-status.js b/.github/workflows/src/verify-run-status.js index bff5e3a5f7dc..1108472d6b11 100644 --- a/.github/workflows/src/verify-run-status.js +++ b/.github/workflows/src/verify-run-status.js @@ -1,20 +1,19 @@ import { extractInputs } from "./context.js"; -import { PER_PAGE_MAX } from "./github.js"; - -const SUPPORTED_EVENTS = ["workflow_run", "check_run", "check_suite"]; +import { getCheckRuns, getCommitStatuses, getWorkflowRuns } from "./github.js"; /** * @typedef {import('@octokit/plugin-rest-endpoint-methods').RestEndpointMethodTypes} RestEndpointMethodTypes - * @typedef {RestEndpointMethodTypes["checks"]["listForRef"]["response"]["data"]["check_runs"]} CheckRuns - * @typedef {RestEndpointMethodTypes["actions"]["listWorkflowRunsForRepo"]["response"]["data"]["workflow_runs"]} WorkflowRuns + * @typedef {RestEndpointMethodTypes["repos"]["listCommitStatusesForRef"]["response"]["data"]} CommitStatuses */ +const SUPPORTED_EVENTS = ["workflow_run", "check_run", "check_suite"]; + /* v8 ignore start */ /** * Given the name of a completed check run name and a completed workflow, verify * that both have the same conclusion. If conclusions are different, fail the * action. - * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments + * @param {import('@actions/github-script').AsyncFunctionArguments} AsyncFunctionArguments */ export async function verifyRunStatus({ github, context, core }) { const checkRunName = process.env.CHECK_RUN_NAME; @@ -22,9 +21,10 @@ export async function verifyRunStatus({ github, context, core }) { throw new Error("CHECK_RUN_NAME is not set"); } + const commitStatusName = process.env.COMMIT_STATUS_NAME; const workflowName = process.env.WORKFLOW_NAME; - if (!workflowName) { - throw new Error("WORKFLOW_NAME is not set"); + if (!commitStatusName && !workflowName) { + throw new Error("Neither COMMIT_STATUS nor WORKFLOW_NAME is not set"); } if (!SUPPORTED_EVENTS.some((e) => e === context.eventName)) { @@ -33,10 +33,7 @@ export async function verifyRunStatus({ github, context, core }) { ); } - if ( - context.eventName === "check_suite" && - context.payload.check_suite.status !== "completed" - ) { + if (context.eventName === "check_suite" && context.payload.check_suite.status !== "completed") { core.setFailed( `Check suite ${context.payload.check_suite.app.name} is not completed. Cannot evaluate incomplete check suite.`, ); @@ -48,6 +45,7 @@ export async function verifyRunStatus({ github, context, core }) { context, core, checkRunName, + commitStatusName, workflowName, }); } @@ -55,17 +53,19 @@ export async function verifyRunStatus({ github, context, core }) { /** * @param {Object} params - * @param {import('github-script').AsyncFunctionArguments["github"]} params.github - * @param {import('github-script').AsyncFunctionArguments["context"]} params.context - * @param {import('github-script').AsyncFunctionArguments["core"]} params.core + * @param {import('@actions/github-script').AsyncFunctionArguments["github"]} params.github + * @param {import('@actions/github-script').AsyncFunctionArguments["context"]} params.context + * @param {import('@actions/github-script').AsyncFunctionArguments["core"]} params.core * @param {string} params.checkRunName - * @param {string} params.workflowName + * @param {string} [params.commitStatusName] + * @param {string} [params.workflowName] */ export async function verifyRunStatusImpl({ github, context, core, checkRunName, + commitStatusName, workflowName, }) { if (context.eventName == "check_run") { @@ -84,12 +84,7 @@ export async function verifyRunStatusImpl({ if (context.eventName == "check_run") { checkRun = context.payload.check_run; } else { - const checkRuns = await getCheckRuns( - github, - context, - checkRunName, - head_sha, - ); + const checkRuns = await getCheckRuns(github, context, checkRunName, head_sha); if (checkRuns.length === 0) { if (context.eventName === "check_suite") { const message = `Could not locate check run ${checkRunName} in check suite ${context.payload.check_suite.app.name}. Ensure job is filtering by github.event.check_suite.app.name.`; @@ -112,98 +107,93 @@ export async function verifyRunStatusImpl({ ); core.debug(`Check run: ${JSON.stringify(checkRun)}`); - let workflowRun; - if (context.eventName == "workflow_run") { - workflowRun = context.payload.workflow_run; - } else { - const workflowRuns = await getWorkflowRuns( - github, - context, - workflowName, - head_sha, + if (commitStatusName) { + core.info(`commitStatusName: ${commitStatusName}`); + + // Get the commit status + let commitStatusContext, commitStatusState, commitStatusTargetUrl; + + // Fetch the commit status from the API + try { + const commitStatuses = await getCommitStatuses(github, context, commitStatusName, head_sha); + if (commitStatuses && commitStatuses.length > 0) { + commitStatusContext = commitStatuses[0].context; + commitStatusState = commitStatuses[0].state; + commitStatusTargetUrl = commitStatuses[0].target_url; + } else { + // Count the commit status as pending if not found and return with no-op + core.notice( + `Commit status is in pending state. Skipping comparison with check run conclusion.`, + ); + return; + } + } catch (error) { + core.setFailed( + `Failed to fetch commit status: ${error instanceof Error ? error.message : String(error)}`, + ); + return; + } + + core.info( + `Commit status context: ${commitStatusContext}, state: ${commitStatusState}, URL: ${commitStatusTargetUrl}`, ); - if (workflowRuns.length === 0) { + + if (commitStatusState === "pending") { core.notice( - `No completed workflow run with name: ${workflowName}. Not enough information to judge success or failure. Ending with success status.`, + `Commit status is in pending state. Skipping comparison with check run conclusion.`, ); return; } - // Use the most recent workflow run - workflowRun = workflowRuns[0]; - } + // Normalize check run conclusion: treat 'neutral' as 'success' + const normalizedCheckRunConclusion = + checkRun.conclusion === "neutral" ? "success" : checkRun.conclusion; - core.info( - `Workflow run name: ${workflowRun.name}, conclusion: ${workflowRun.conclusion}, URL: ${workflowRun.html_url}`, - ); - core.debug(`Workflow run: ${JSON.stringify(workflowRun)}`); + if (normalizedCheckRunConclusion !== commitStatusState) { + core.setFailed( + `Check run conclusion (${checkRun.conclusion}) does not match commit status state (${commitStatusState})`, + ); + return; + } - if (checkRun.conclusion !== workflowRun.conclusion) { - core.setFailed( - `Check run conclusion (${checkRun.conclusion}) does not match workflow run conclusion (${workflowRun.conclusion})`, + core.notice( + `Conclusions match for check run ${checkRunName} and commit status ${commitStatusName}`, ); - return; } - core.notice( - `Conclusions match for check run ${checkRunName} and workflow run ${workflowName}`, - ); -} + if (workflowName) { + let workflowRun; + if (context.eventName == "workflow_run") { + workflowRun = context.payload.workflow_run; + } else { + const workflowRuns = await getWorkflowRuns(github, context, workflowName, head_sha); + if (workflowRuns.length === 0) { + core.notice( + `No completed workflow run with name: ${workflowName}. Not enough information to judge success or failure. Ending with success status.`, + ); + return; + } -/** - * Returns the check with the given checkRunName for the given ref. - * @param {import('github-script').AsyncFunctionArguments['github']} github - * @param {import('github-script').AsyncFunctionArguments['context']} context - * @param {string} checkRunName - * @param {string} ref - * @returns {Promise} - */ -export async function getCheckRuns(github, context, checkRunName, ref) { - const result = await github.paginate(github.rest.checks.listForRef, { - ...context.repo, - ref: ref, - check_name: checkRunName, - status: "completed", - per_page: PER_PAGE_MAX, - }); + // Use the most recent workflow run + workflowRun = workflowRuns[0]; + } - // a and b will never be null because status is "completed" - /* v8 ignore next */ - return result.sort((a, b) => - compareDatesDescending(a.completed_at || "", b.completed_at || ""), - ); -} + core.info( + `Workflow run name: ${workflowRun.name}, conclusion: ${workflowRun.conclusion}, URL: ${workflowRun.html_url}`, + ); + core.debug(`Workflow run: ${JSON.stringify(workflowRun)}`); -/** - * Returns the workflow run with the given workflowName for the given ref. - * @param {import('github-script').AsyncFunctionArguments['github']} github - * @param {import('github-script').AsyncFunctionArguments['context']} context - * @param {string} workflowName - * @param {string} ref - * @returns {Promise} - */ -export async function getWorkflowRuns(github, context, workflowName, ref) { - const result = await github.paginate( - github.rest.actions.listWorkflowRunsForRepo, - { - ...context.repo, - head_sha: ref, - status: "completed", - per_page: PER_PAGE_MAX, - }, - ); + // Normalize check run conclusion: treat 'neutral' as 'success' + const normalizedCheckRunConclusion = + checkRun.conclusion === "neutral" ? "success" : checkRun.conclusion; - return result - .filter((run) => run.name === workflowName) - .sort((a, b) => compareDatesDescending(a.updated_at, b.updated_at)); -} + if (normalizedCheckRunConclusion !== workflowRun.conclusion) { + core.setFailed( + `Check run conclusion (${checkRun.conclusion}) does not match workflow run conclusion (${workflowRun.conclusion})`, + ); + return; + } -/** - * Compares two date strings in descending order. - * @param {string} a date string of the form "YYYY-MM-DDTHH:mm:ssZ" - * @param {string} b date string of the form "YYYY-MM-DDTHH:mm:ssZ" - * @returns - */ -export function compareDatesDescending(a, b) { - return new Date(b).getTime() - new Date(a).getTime(); + core.notice(`Conclusions match for check run ${checkRunName} and workflow run ${workflowName}`); + } } diff --git a/.github/workflows/summarize-checks.yaml b/.github/workflows/summarize-checks.yaml new file mode 100644 index 000000000000..4e39bd8fb5a4 --- /dev/null +++ b/.github/workflows/summarize-checks.yaml @@ -0,0 +1,60 @@ +name: "[TEST-IGNORE] Summarize Checks" + +on: + workflow_run: + # todo, any others need to be added here? + workflows: + - "\\[TEST-IGNORE\\] Swagger SemanticValidation - Set Status" + - "\\[TEST-IGNORE\\] Swagger ModelValidation - Set Status" + - "\\[TEST-IGNORE\\] Summarize PR Impact" + - "Swagger Avocado - Set Status" + - "Swagger LintDiff - Set Status" + - "SDK Validation Status" + types: + - completed + pull_request_target: # when a PR is labeled. NOT pull_request, because that would run on the PR branch, not the base branch. + types: + - labeled + - unlabeled + - opened + - synchronize + - reopened + +permissions: + actions: read # to inspect workflow_run metadata + contents: read # for actions/checkout + checks: read # for octokit.rest.checks.listForRef + statuses: read # for octokit.rest.repos.getCombinedStatusForRef + issues: write # to post comments via the Issues API + pull-requests: write # to post comments via the Pull Requests API + +jobs: + run-summarize-checks: + name: "[TEST-IGNORE] Summarize Checks" + runs-on: ubuntu-24.04 + + steps: + # *** IMPORTANT *** + # For workflows that are triggered by the pull_request_target event, the workflow runs in the + # context of the base of the pull request. You should make sure that you do not check out, + # build, or run untrusted code from the head of the pull request. + - uses: actions/checkout@v4 + with: + # Only needs .github folder for automation, not the files in the PR (analyzed in a + # separate workflow). + # + # Uses the .github folder from the PR base branch (pull_request_target trigger), + # or the repo default branch (other triggers). + sparse-checkout: | + .github + + # todo: add the npm install ci here for the zod usage in summarize-checks (not currently there) + + - id: summarize-checks + name: Summarize Checks + uses: actions/github-script@v7 + with: + script: | + const { default: summarizeChecks } = + await import('${{ github.workspace }}/.github/workflows/src/summarize-checks/summarize-checks.js'); + return await summarizeChecks({ github, context, core }); diff --git a/.github/workflows/summarize-impact.yaml b/.github/workflows/summarize-impact.yaml new file mode 100644 index 000000000000..373f091ed470 --- /dev/null +++ b/.github/workflows/summarize-impact.yaml @@ -0,0 +1,66 @@ +name: "[TEST-IGNORE] Summarize PR Impact" + +on: pull_request + +permissions: + contents: read + pull-requests: read + +jobs: + impact: + name: "[TEST-IGNORE] Summarize PR Impact" + runs-on: ubuntu-24.04 + + steps: + - name: Checkout 'after' state + uses: actions/checkout@v4 + with: + fetch-depth: 2 + path: after + + - name: Checkout 'before' state + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha }} + path: before + + - name: Setup Node and install deps + uses: ./after/.github/actions/setup-node-install-deps + with: + working-directory: after + + - name: Run Summarize Impact + id: summarize-impact + working-directory: after + run: | + npm exec --no -- summarize-impact --sourceDirectory . --targetDirectory ../before \ + --number "${{ github.event.pull_request.number }}" \ + --sourceBranch "$HEAD_REF" \ + --targetBranch "${{ github.event.pull_request.base.ref }}" \ + --sha "${{ github.event.pull_request.head.sha }}" \ + --repo "${{ github.event.repository.name }}" \ + --owner "${{ github.repository_owner }}" \ + --isDraft ${{ github.event.pull_request.draft }} + env: + # We absolutely need to avoid OOM errors due to certain inherited types from openapi-alps + NODE_OPTIONS: "--max-old-space-size=8192" + HEAD_REF: ${{ github.event.pull_request.head.ref }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Set job-summary artifact + if: ${{ always() && steps.summarize-impact.outputs.summary }} + uses: actions/upload-artifact@v4 + with: + name: job-summary + path: ${{ steps.summarize-impact.outputs.summary }} + # If the file doesn't exist, just don't add the artifact + if-no-files-found: ignore + + - if: | + always() && + github.event.pull_request.number > 0 + name: Upload artifact with issue number + uses: ./after/.github/actions/add-empty-artifact + with: + name: "issue-number" + value: "${{ github.event.pull_request.number }}" diff --git a/.github/workflows/swagger-modelvalidation-code.yaml b/.github/workflows/swagger-modelvalidation-code.yaml new file mode 100644 index 000000000000..7ee3c19b8571 --- /dev/null +++ b/.github/workflows/swagger-modelvalidation-code.yaml @@ -0,0 +1,24 @@ +name: "[TEST-IGNORE] Swagger ModelValidation" + +on: pull_request + +permissions: + contents: read + +jobs: + oav: + name: "[TEST-IGNORE] Swagger ModelValidation" + runs-on: ubuntu-24.04 + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Setup Node and install deps + uses: ./.github/actions/setup-node-install-deps + + - name: Swagger Model Validation + run: | + npm exec --no -- oav-runner examples diff --git a/.github/workflows/swagger-modelvalidation-status.yaml b/.github/workflows/swagger-modelvalidation-status.yaml new file mode 100644 index 000000000000..5dc54b4caa8c --- /dev/null +++ b/.github/workflows/swagger-modelvalidation-status.yaml @@ -0,0 +1,35 @@ +name: "[TEST-IGNORE] Swagger ModelValidation - Set Status" + +on: + # Must run on pull_request_target instead of pull_request, since the latter cannot trigger on + # labels from bot accounts in fork PRs. pull_request_target is also more similar to the other + # trigger "workflow_run" -- they are both privileged and run in the target branch and repo -- + # which simplifies implementation. + pull_request_target: + types: + # Run workflow on default types, to update status as quickly as possible. + - opened + - synchronize + - reopened + # Depends on labels, so must re-evaluate whenever a relevant label is manually added or removed. + - labeled + - unlabeled + workflow_run: + workflows: ["\\[TEST-IGNORE\\] Swagger ModelValidation"] + types: [completed] + +permissions: + actions: read + contents: read + issues: read + pull-requests: read + statuses: write + +jobs: + model-validation-status: + name: Set ModelValidation Status + uses: ./.github/workflows/_reusable-set-check-status.yaml + with: + monitored_workflow_name: "[TEST-IGNORE] Swagger ModelValidation" + required_check_name: "[TEST-IGNORE] Swagger ModelValidation" + overriding_label: "Approved-ModelValidation" diff --git a/.github/workflows/swagger-semanticvalidation-code.yaml b/.github/workflows/swagger-semanticvalidation-code.yaml new file mode 100644 index 000000000000..532de1554d88 --- /dev/null +++ b/.github/workflows/swagger-semanticvalidation-code.yaml @@ -0,0 +1,24 @@ +name: "[TEST-IGNORE] Swagger SemanticValidation" + +on: pull_request + +permissions: + contents: read + +jobs: + oav: + name: "[TEST-IGNORE] Swagger SemanticValidation" + runs-on: ubuntu-24.04 + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Setup Node and install deps + uses: ./.github/actions/setup-node-install-deps + + - name: Swagger Semantic Validation + run: | + npm exec --no -- oav-runner specs diff --git a/.github/workflows/swagger-semanticvalidation-status.yaml b/.github/workflows/swagger-semanticvalidation-status.yaml new file mode 100644 index 000000000000..637866d22004 --- /dev/null +++ b/.github/workflows/swagger-semanticvalidation-status.yaml @@ -0,0 +1,35 @@ +name: "[TEST-IGNORE] Swagger SemanticValidation - Set Status" + +on: + # Must run on pull_request_target instead of pull_request, since the latter cannot trigger on + # labels from bot accounts in fork PRs. pull_request_target is also more similar to the other + # trigger "workflow_run" -- they are both privileged and run in the target branch and repo -- + # which simplifies implementation. + pull_request_target: + types: + # Run workflow on default types, to update status as quickly as possible. + - opened + - synchronize + - reopened + # Depends on labels, so must re-evaluate whenever a relevant label is manually added or removed. + - labeled + - unlabeled + workflow_run: + workflows: ["\\[TEST-IGNORE\\] Swagger SemanticValidation"] + types: [completed] + +permissions: + actions: read + contents: read + issues: read + pull-requests: read + statuses: write + +jobs: + spec-validation-status: + name: Set SemanticValidation Status + uses: ./.github/workflows/_reusable-set-check-status.yaml + with: + monitored_workflow_name: "[TEST-IGNORE] Swagger SemanticValidation" + required_check_name: "[TEST-IGNORE] Swagger SemanticValidation" + overriding_label: "Approved-SemanticValidation" diff --git a/.github/workflows/test/arm-auto-signoff.test.js b/.github/workflows/test/arm-auto-signoff.test.js index 9c43de6a1abd..c27801369f36 100644 --- a/.github/workflows/test/arm-auto-signoff.test.js +++ b/.github/workflows/test/arm-auto-signoff.test.js @@ -1,10 +1,7 @@ import { describe, expect, it } from "vitest"; import { getLabelActionImpl } from "../src/arm-auto-signoff.js"; import { LabelAction } from "../src/label.js"; -import { - createMockCore, - createMockGithub as createMockGithubBase, -} from "./mocks.js"; +import { createMockCore, createMockGithub as createMockGithubBase } from "./mocks.js"; const core = createMockCore(); diff --git a/.github/workflows/test/arm-incremental-typespec.test.js b/.github/workflows/test/arm-incremental-typespec.test.js index 48af9d227e88..668613e1acd1 100644 --- a/.github/workflows/test/arm-incremental-typespec.test.js +++ b/.github/workflows/test/arm-incremental-typespec.test.js @@ -42,9 +42,7 @@ describe("incrementalTypeSpec", () => { vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([swaggerPath]); - const showSpy = vi - .mocked(simpleGit.simpleGit().show) - .mockResolvedValue(swaggerHandWritten); + const showSpy = vi.mocked(simpleGit.simpleGit().show).mockResolvedValue(swaggerHandWritten); await expect(incrementalTypeSpec({ core })).resolves.toBe(false); @@ -52,8 +50,7 @@ describe("incrementalTypeSpec", () => { }); it("returns false if changed files add a new RP", async () => { - const specDir = - "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso"; + const specDir = "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso"; const swaggerPath = `${specDir}/preview/2021-10-01-preview/contoso.json`; vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([swaggerPath]); @@ -69,13 +66,7 @@ describe("incrementalTypeSpec", () => { expect(showSpy).toBeCalledWith([`HEAD:${swaggerPath}`]); - expect(rawSpy).toBeCalledWith([ - "ls-tree", - "-r", - "--name-only", - "HEAD^", - specDir, - ]); + expect(rawSpy).toBeCalledWith(["ls-tree", "-r", "--name-only", "HEAD^", specDir]); }); it("returns false if swagger deleted", async () => { @@ -86,9 +77,7 @@ describe("incrementalTypeSpec", () => { const showSpy = vi .mocked(simpleGit.simpleGit().show) - .mockRejectedValue( - new Error("path contoso.json does not exist in 'HEAD'"), - ); + .mockRejectedValue(new Error("path contoso.json does not exist in 'HEAD'")); await expect(incrementalTypeSpec({ core })).resolves.toBe(false); @@ -96,8 +85,7 @@ describe("incrementalTypeSpec", () => { }); it("returns false if readme deleted", async () => { - const readmePath = - "specification/contosowidgetmanager/resource-manager/readme.md"; + const readmePath = "specification/contosowidgetmanager/resource-manager/readme.md"; vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([readmePath]); @@ -111,8 +99,7 @@ describe("incrementalTypeSpec", () => { }); it("returns false if readme contains no input-files", async () => { - const readmePath = - "specification/contosowidgetmanager/resource-manager/readme.md"; + const readmePath = "specification/contosowidgetmanager/resource-manager/readme.md"; vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([readmePath]); @@ -129,9 +116,7 @@ describe("incrementalTypeSpec", () => { vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([swaggerPath]); - const showSpy = vi - .mocked(simpleGit.simpleGit().show) - .mockResolvedValue("not } valid { json"); + const showSpy = vi.mocked(simpleGit.simpleGit().show).mockResolvedValue("not } valid { json"); await expect(incrementalTypeSpec({ core })).resolves.toBe(false); @@ -139,8 +124,7 @@ describe("incrementalTypeSpec", () => { }); it("returns false if tsp conversion", async () => { - const specDir = - "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso"; + const specDir = "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso"; const swaggerPath = `${specDir}/preview/2021-10-01-preview/contoso.json`; vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([swaggerPath]); @@ -148,27 +132,17 @@ describe("incrementalTypeSpec", () => { const showSpy = vi .mocked(simpleGit.simpleGit().show) .mockImplementation(async ([treePath]) => - treePath.split(":")[0] == "HEAD" - ? swaggerTypeSpecGenerated - : swaggerHandWritten, + treePath.split(":")[0] == "HEAD" ? swaggerTypeSpecGenerated : swaggerHandWritten, ); - const lsTreeSpy = vi - .mocked(simpleGit.simpleGit().raw) - .mockResolvedValue(swaggerPath); + const lsTreeSpy = vi.mocked(simpleGit.simpleGit().raw).mockResolvedValue(swaggerPath); await expect(incrementalTypeSpec({ core })).resolves.toBe(false); expect(showSpy).toHaveBeenCalledWith([`HEAD:${swaggerPath}`]); expect(showSpy).toHaveBeenCalledWith([`HEAD^:${swaggerPath}`]); - expect(lsTreeSpy).toBeCalledWith([ - "ls-tree", - "-r", - "--name-only", - "HEAD^", - specDir, - ]); + expect(lsTreeSpy).toBeCalledWith(["ls-tree", "-r", "--name-only", "HEAD^", specDir]); }); it("throws if git show for swagger returns unknown error", async () => { @@ -177,9 +151,7 @@ describe("incrementalTypeSpec", () => { vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([swaggerPath]); - const showSpy = vi - .mocked(simpleGit.simpleGit().show) - .mockRejectedValue("string error"); + const showSpy = vi.mocked(simpleGit.simpleGit().show).mockRejectedValue("string error"); await expect(incrementalTypeSpec({ core })).rejects.toThrowError(); @@ -187,14 +159,11 @@ describe("incrementalTypeSpec", () => { }); it("throws if git show for readme returns unknown error", async () => { - const readmePath = - "specification/contosowidgetmanager/resource-manager/readme.md"; + const readmePath = "specification/contosowidgetmanager/resource-manager/readme.md"; vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([readmePath]); - const showSpy = vi - .mocked(simpleGit.simpleGit().show) - .mockRejectedValue("string error"); + const showSpy = vi.mocked(simpleGit.simpleGit().show).mockRejectedValue("string error"); await expect(incrementalTypeSpec({ core })).rejects.toThrowError(); @@ -202,8 +171,7 @@ describe("incrementalTypeSpec", () => { }); it("returns true if changed files are incremental changes to an existing TypeSpec RP swagger", async () => { - const specDir = - "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso"; + const specDir = "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso"; const swaggerPath = `${specDir}/preview/2021-10-01-preview/contoso.json`; vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([swaggerPath]); @@ -212,51 +180,37 @@ describe("incrementalTypeSpec", () => { .mocked(simpleGit.simpleGit().show) .mockResolvedValue(swaggerTypeSpecGenerated); - const lsTreeSpy = vi - .mocked(simpleGit.simpleGit().raw) - .mockResolvedValue(swaggerPath); + const lsTreeSpy = vi.mocked(simpleGit.simpleGit().raw).mockResolvedValue(swaggerPath); await expect(incrementalTypeSpec({ core })).resolves.toBe(true); expect(showSpy).toBeCalledWith([`HEAD:${swaggerPath}`]); expect(showSpy).toBeCalledWith([`HEAD^:${swaggerPath}`]); - expect(lsTreeSpy).toBeCalledWith([ - "ls-tree", - "-r", - "--name-only", - "HEAD^", - specDir, - ]); + expect(lsTreeSpy).toBeCalledWith(["ls-tree", "-r", "--name-only", "HEAD^", specDir]); }); it("returns true if changed files are incremental changes to an existing TypeSpec RP readme", async () => { - const specDir = - "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso"; + const specDir = "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso"; const swaggerPath = `${specDir}/preview/2021-10-01-preview/contoso.json`; - const readmePath = - "specification/contosowidgetmanager/resource-manager/readme.md"; + const readmePath = "specification/contosowidgetmanager/resource-manager/readme.md"; vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([readmePath]); - const showSpy = vi - .mocked(simpleGit.simpleGit().show) - .mockImplementation(async ([treePath]) => { - const path = treePath.split(":")[1]; - if (path === swaggerPath) { - return swaggerTypeSpecGenerated; - } else if (path === readmePath) { - return contosoReadme; - } else { - throw new Error("does not exist"); - } - }); - - const lsTreeSpy = vi - .mocked(simpleGit.simpleGit().raw) - .mockResolvedValue(swaggerPath); + const showSpy = vi.mocked(simpleGit.simpleGit().show).mockImplementation(async ([treePath]) => { + const path = treePath.split(":")[1]; + if (path === swaggerPath) { + return swaggerTypeSpecGenerated; + } else if (path === readmePath) { + return contosoReadme; + } else { + throw new Error("does not exist"); + } + }); + + const lsTreeSpy = vi.mocked(simpleGit.simpleGit().raw).mockResolvedValue(swaggerPath); await expect(incrementalTypeSpec({ core })).resolves.toBe(true); @@ -273,8 +227,7 @@ describe("incrementalTypeSpec", () => { }); it("returns true if changed files are incremental changes to an existing TypeSpec RP example", async () => { - const specDir = - "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso"; + const specDir = "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso"; const swaggerPath = `${specDir}/preview/2021-10-01-preview/contoso.json`; const examplesPath = "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/preview/2021-10-01-preview/examples/Employees_Get.json"; @@ -285,20 +238,12 @@ describe("incrementalTypeSpec", () => { .mocked(simpleGit.simpleGit().show) .mockResolvedValue(swaggerTypeSpecGenerated); - const lsTreeSpy = vi - .mocked(simpleGit.simpleGit().raw) - .mockResolvedValue(swaggerPath); + const lsTreeSpy = vi.mocked(simpleGit.simpleGit().raw).mockResolvedValue(swaggerPath); await expect(incrementalTypeSpec({ core })).resolves.toBe(true); expect(showSpy).toBeCalledWith([`HEAD^:${swaggerPath}`]); - expect(lsTreeSpy).toHaveBeenCalledWith([ - "ls-tree", - "-r", - "--name-only", - "HEAD^", - specDir, - ]); + expect(lsTreeSpy).toHaveBeenCalledWith(["ls-tree", "-r", "--name-only", "HEAD^", specDir]); }); }); diff --git a/.github/workflows/test/artifacts.test.js b/.github/workflows/test/artifacts.test.js index 489596e06626..ac5810967cf1 100644 --- a/.github/workflows/test/artifacts.test.js +++ b/.github/workflows/test/artifacts.test.js @@ -1,8 +1,8 @@ -import { describe, expect, it, vi, beforeEach } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { - getAzurePipelineArtifact, - getAdoBuildInfoFromUrl, fetchFailedArtifact, + getAdoBuildInfoFromUrl, + getAzurePipelineArtifact, } from "../src/artifacts.js"; import { createMockCore } from "./mocks.js"; @@ -64,8 +64,7 @@ describe("getAzurePipelineArtifact function", () => { ok: true, json: vi.fn().mockResolvedValue({ resource: { - downloadUrl: - "https://example.com/failed-artifact-download?format=zip", + downloadUrl: "https://example.com/failed-artifact-download?format=zip", }, }), status: 200, @@ -86,24 +85,15 @@ describe("getAzurePipelineArtifact function", () => { } // First attempted artifact request with 404 - if ( - url.includes( - `artifacts?artifactName=${inputs.artifactName}&api-version=7.0`, - ) - ) { + if (url.includes(`artifacts?artifactName=${inputs.artifactName}&api-version=7.0`)) { return mockInitialResponse; } // List all artifacts request - else if ( - url.includes("artifacts?api-version=7.0") && - !url.includes("artifactName=") - ) { + else if (url.includes("artifacts?api-version=7.0") && !url.includes("artifactName=")) { return mockListResponse; } // Request for failed artifact - else if ( - url.includes("artifactName=spec-gen-sdk-artifact-FailedAttempt1") - ) { + else if (url.includes("artifactName=spec-gen-sdk-artifact-FailedAttempt1")) { return mockFailedArtifactResponse; } // Content download request @@ -165,17 +155,11 @@ describe("getAzurePipelineArtifact function", () => { global.fetch.mockImplementation((url, options) => { if (url.includes("artifacts?artifactName=")) { // Verify headers contain Authorization - expect(options.headers).toHaveProperty( - "Authorization", - `Bearer ${testToken}`, - ); + expect(options.headers).toHaveProperty("Authorization", `Bearer ${testToken}`); return mockArtifactResponse; } else { // Verify headers contain Authorization for the content download as well - expect(options.headers).toHaveProperty( - "Authorization", - `Bearer ${testToken}`, - ); + expect(options.headers).toHaveProperty("Authorization", `Bearer ${testToken}`); return mockContentResponse; } }); @@ -287,8 +271,7 @@ describe("getAzurePipelineArtifact function", () => { ok: true, json: vi.fn().mockResolvedValue({ resource: { - downloadUrl: - "https://example.com/failed-artifact-download?format=zip", + downloadUrl: "https://example.com/failed-artifact-download?format=zip", }, }), status: 200, @@ -308,24 +291,15 @@ describe("getAzurePipelineArtifact function", () => { // Setup fetch to return different responses based on the URL global.fetch.mockImplementation((url) => { // First attempted artifact request with 404 - if ( - url.includes( - `artifacts?artifactName=${inputs.artifactName}&api-version=7.0`, - ) - ) { + if (url.includes(`artifacts?artifactName=${inputs.artifactName}&api-version=7.0`)) { return mockInitialResponse; } // List all artifacts request - else if ( - url.includes("artifacts?api-version=7.0") && - !url.includes("artifactName=") - ) { + else if (url.includes("artifacts?api-version=7.0") && !url.includes("artifactName=")) { return mockListResponse; } // Request for failed artifact - notice we use the first item from mockListResponse - else if ( - url.includes("artifactName=spec-gen-sdk-artifact-FailedAttempt2") - ) { + else if (url.includes("artifactName=spec-gen-sdk-artifact-FailedAttempt2")) { return mockFailedArtifactResponse; } // Content download request @@ -531,8 +505,7 @@ describe("getAdoBuildInfoFromUrl function", () => { }); it("should extract project URL and build ID from a valid URL", () => { - const buildUrl = - "https://dev.azure.com/azure-sdk/_build/results?buildId=12345&view=logs"; + const buildUrl = "https://dev.azure.com/azure-sdk/_build/results?buildId=12345&view=logs"; const result = getAdoBuildInfoFromUrl(buildUrl); expect(result).toEqual({ @@ -542,8 +515,7 @@ describe("getAdoBuildInfoFromUrl function", () => { }); it("should extract build ID when it's not the first parameter", () => { - const buildUrl = - "https://dev.azure.com/azure-sdk/_build/results?view=logs&buildId=54321"; + const buildUrl = "https://dev.azure.com/azure-sdk/_build/results?view=logs&buildId=54321"; const result = getAdoBuildInfoFromUrl(buildUrl); expect(result).toEqual({ @@ -561,8 +533,7 @@ describe("getAdoBuildInfoFromUrl function", () => { }); it("should throw error when buildId is missing", () => { - const invalidUrl = - "https://dev.azure.com/azure-sdk/_build/results?view=logs"; + const invalidUrl = "https://dev.azure.com/azure-sdk/_build/results?view=logs"; expect(() => { getAdoBuildInfoFromUrl(invalidUrl); @@ -620,10 +591,7 @@ describe("fetchFailedArtifact function", () => { // Verify that the custom headers are included in all requests expect(options.headers).toEqual(customHeaders); - if ( - url.includes("artifacts?api-version") && - !url.includes("artifactName=") - ) { + if (url.includes("artifacts?api-version") && !url.includes("artifactName=")) { return mockListResponse; } else { return mockFetchResponse; @@ -689,9 +657,7 @@ describe("fetchFailedArtifact function", () => { global.fetch.mockImplementation((url) => { if (url.includes("artifacts?api-version")) { return mockListResponse; - } else if ( - url.includes("artifactName=spec-gen-sdk-artifact-FailedAttempt2") - ) { + } else if (url.includes("artifactName=spec-gen-sdk-artifact-FailedAttempt2")) { return mockFetchResponse; } }); @@ -788,9 +754,7 @@ describe("fetchFailedArtifact function", () => { global.fetch.mockImplementation((url) => { if (url.includes("artifacts?api-version")) { return mockListResponse; - } else if ( - url.includes("artifactName=spec-gen-sdk-artifact-FailedAttempt3") - ) { + } else if (url.includes("artifactName=spec-gen-sdk-artifact-FailedAttempt3")) { return mockFetchResponse; } }); diff --git a/.github/workflows/test/avocado-code.test.js b/.github/workflows/test/avocado-code.test.js new file mode 100644 index 000000000000..909c782b21b0 --- /dev/null +++ b/.github/workflows/test/avocado-code.test.js @@ -0,0 +1,136 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +// Mock fs/promises before imports +vi.mock("fs/promises", () => ({ readFile: vi.fn() })); + +import * as fs from "fs/promises"; + +/** @type {import("vitest").Mock} */ +const readFileMock = fs.readFile; + +import generateJobSummary from "../src/avocado-code.js"; +import { MessageLevel, MessageType } from "../src/message.js"; +import { stringify } from "../src/ndjson.js"; +import { createMockCore } from "./mocks.js"; + +const core = createMockCore(); +const outputFile = "avocado.ndjson"; + +describe("generateJobSummary", () => { + beforeEach(() => { + vi.stubEnv("AVOCADO_OUTPUT_FILE", outputFile); + readFileMock.mockReset(); + core.summary.addRaw.mockReset(); + core.summary.write.mockReset(); + }); + + it("throws if env var not set", async () => { + vi.unstubAllEnvs(); + + await expect(generateJobSummary({ core })).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Env var AVOCADO_OUTPUT_FILE must be set]`, + ); + + expect(core.info.mock.calls.at(-1)[0]).toMatchInlineSnapshot(`"avocadoOutputFile: undefined"`); + }); + + it("no-ops if file cannot be read", async () => { + readFileMock.mockRejectedValueOnce( + new Error(`ENOENT: no such file or directory, open '${outputFile}'`), + ); + + await expect(generateJobSummary({ core })).resolves.toBeUndefined(); + + expect(core.info.mock.calls.at(-1)[0]).toMatchInlineSnapshot( + `"Error reading 'avocado.ndjson': Error: ENOENT: no such file or directory, open 'avocado.ndjson'"`, + ); + }); + + it("no-ops with notice if file is empty", async () => { + readFileMock.mockResolvedValueOnce(""); + + await expect(generateJobSummary({ core })).resolves.toBeUndefined(); + + expect(core.notice.mock.calls.at(-1)[0]).toMatchInlineSnapshot( + `"No messages in 'avocado.ndjson'"`, + ); + }); + + it("generates summary for success", async () => { + vi.stubEnv("GITHUB_STEP_SUMMARY", "test-step-summary"); + + /** @type {import("../src/message.js").MessageRecord[]} */ + const messages = [ + { + type: MessageType.Raw, + level: MessageLevel.Info, + message: "success", + time: new Date().toISOString(), + }, + ]; + + readFileMock.mockResolvedValueOnce(stringify(messages)); + + await expect(generateJobSummary({ core })).resolves.toBeUndefined(); + + expect(core.summary.addRaw.mock.calls[0][0]).toMatchInlineSnapshot(`"Success"`); + expect(core.summary.write).toBeCalledTimes(1); + + expect(core.setOutput).toBeCalledWith("summary", "test-step-summary"); + }); + + it("generates summary for failure", async () => { + vi.stubEnv("GITHUB_STEP_SUMMARY", "test-step-summary"); + + /** @type {import("../src/message.js").MessageRecord[]} */ + const messages = [ + { + type: MessageType.Raw, + level: MessageLevel.Info, + message: "test-raw-info", + time: new Date().toISOString(), + }, + { + type: MessageType.Result, + level: MessageLevel.Error, + code: "NO_JSON_FILE_FOUND", + message: "The JSON file is not found but it is referenced from the readme file.", + docUrl: "https://github.com/Azure/avocado/blob/master/README.md#NO_JSON_FILE_FOUND", + time: "2025-07-11T00:01:00.418Z", + paths: [ + { + tag: "readme", + path: "https://github.com/mikeharder/azure-rest-api-specs/blob/4e6083f35e27d8e1e3b78222cf4bd27b87cd1409/specification/contosowidgetmanager/resource-manager/readme.md", + }, + { + tag: "json", + path: "https://github.com/mikeharder/azure-rest-api-specs/blob/4e6083f35e27d8e1e3b78222cf4bd27b87cd1409/specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso2.json", + }, + ], + }, + { + type: MessageType.Raw, + level: MessageLevel.Warning, + message: "test-raw-notice", + time: new Date().toISOString(), + }, + ]; + + const str = stringify(messages); + + readFileMock.mockResolvedValueOnce(str); + + await expect(generateJobSummary({ core })).resolves.toBeUndefined(); + + expect(core.summary.addRaw.mock.calls[0][0]).toMatchInlineSnapshot(` + "| Rule | Message | + | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | ℹ️ test-raw-info | | + | ❌ [NO_JSON_FILE_FOUND](https://github.com/Azure/avocado/blob/master/README.md#NO_JSON_FILE_FOUND) | The JSON file is not found but it is referenced from the readme file.
    readme: [specification/contosowidgetmanager/resource-manager/readme.md](https://github.com/mikeharder/azure-rest-api-specs/blob/4e6083f35e27d8e1e3b78222cf4bd27b87cd1409/specification/contosowidgetmanager/resource-manager/readme.md)
    json: [Microsoft.Contoso/stable/2021-11-01/contoso2.json](https://github.com/mikeharder/azure-rest-api-specs/blob/4e6083f35e27d8e1e3b78222cf4bd27b87cd1409/specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso2.json) | + | ⚠️ test-raw-notice | |" + `); + expect(core.summary.write).toBeCalledTimes(1); + + expect(core.setOutput).toBeCalledWith("summary", "test-step-summary"); + }); +}); diff --git a/.github/workflows/test/context.test.js b/.github/workflows/test/context.test.js index 6dbf0e9599f2..baedbdbcfa8b 100644 --- a/.github/workflows/test/context.test.js +++ b/.github/workflows/test/context.test.js @@ -12,9 +12,7 @@ describe("extractInputs", () => { }, }; - await expect( - extractInputs(null, context, createMockCore()), - ).rejects.toThrow(); + await expect(extractInputs(null, context, createMockCore())).rejects.toThrow(); }); it("pull_request", async () => { @@ -36,9 +34,7 @@ describe("extractInputs", () => { }, }; - await expect( - extractInputs(null, context, createMockCore()), - ).resolves.toEqual({ + await expect(extractInputs(null, context, createMockCore())).resolves.toEqual({ owner: "TestRepoOwnerLogin", repo: "TestRepoName", head_sha: "abc123", @@ -75,35 +71,23 @@ describe("extractInputs", () => { run_id: NaN, }; - await expect( - extractInputs(null, context, createMockCore()), - ).resolves.toEqual(expected); + await expect(extractInputs(null, context, createMockCore())).resolves.toEqual(expected); context.payload.action = "unlabeled"; - await expect( - extractInputs(null, context, createMockCore()), - ).resolves.toEqual(expected); + await expect(extractInputs(null, context, createMockCore())).resolves.toEqual(expected); context.payload.action = "opened"; - await expect( - extractInputs(null, context, createMockCore()), - ).resolves.toEqual(expected); + await expect(extractInputs(null, context, createMockCore())).resolves.toEqual(expected); context.payload.action = "synchronize"; - await expect( - extractInputs(null, context, createMockCore()), - ).resolves.toEqual(expected); + await expect(extractInputs(null, context, createMockCore())).resolves.toEqual(expected); context.payload.action = "reopened"; - await expect( - extractInputs(null, context, createMockCore()), - ).resolves.toEqual(expected); + await expect(extractInputs(null, context, createMockCore())).resolves.toEqual(expected); // Action not yet supported context.payload.action = "assigned"; - await expect( - extractInputs(null, context, createMockCore()), - ).rejects.toThrow(); + await expect(extractInputs(null, context, createMockCore())).rejects.toThrow(); }); it("issue_comment:edited", async () => { @@ -128,9 +112,7 @@ describe("extractInputs", () => { }, }; - await expect( - extractInputs(github, context, createMockCore()), - ).resolves.toEqual({ + await expect(extractInputs(github, context, createMockCore())).resolves.toEqual({ owner: "TestRepoOwnerLogin", repo: "TestRepoName", head_sha: "abc123", @@ -158,9 +140,7 @@ describe("extractInputs", () => { }, }; - await expect( - extractInputs(null, context, createMockCore()), - ).resolves.toEqual({ + await expect(extractInputs(null, context, createMockCore())).resolves.toEqual({ owner: "TestRepoOwnerLogin", repo: "TestRepoName", head_sha: "", @@ -177,9 +157,7 @@ describe("extractInputs", () => { }, }; - await expect( - extractInputs(null, context, createMockCore()), - ).rejects.toThrow(); + await expect(extractInputs(null, context, createMockCore())).rejects.toThrow(); }); it("workflow_run:completed:pull_request (same repo)", async () => { @@ -202,9 +180,7 @@ describe("extractInputs", () => { }, }; - await expect( - extractInputs(null, context, createMockCore()), - ).resolves.toEqual({ + await expect(extractInputs(null, context, createMockCore())).resolves.toEqual({ owner: "TestRepoOwnerLogin", repo: "TestRepoName", head_sha: "abc123", @@ -243,44 +219,40 @@ describe("extractInputs", () => { }; const github = createMockGithub(); - github.rest.repos.listPullRequestsAssociatedWithCommit.mockImplementation( - async (args) => { - console.log(JSON.stringify(args)); - return { - data: [ - { - base: { - repo: { id: 1234 }, - }, - number: 123, + github.rest.repos.listPullRequestsAssociatedWithCommit.mockImplementation(async (args) => { + console.log(JSON.stringify(args)); + return { + data: [ + { + base: { + repo: { id: 1234 }, }, - // Ensure PRs to other repos are excluded - { - base: { - repo: { id: 4567 }, - }, - number: 1, + number: 123, + }, + // Ensure PRs to other repos are excluded + { + base: { + repo: { id: 4567 }, }, - // Multiple PRs to triggering repo still causes an error (TODO: #33418) - { - base: { - repo: { id: 1234 }, - }, - number: 124, + number: 1, + }, + // Multiple PRs to triggering repo still causes an error (TODO: #33418) + { + base: { + repo: { id: 1234 }, }, - ].slice(0, numPullRequests), - }; - }, - ); + number: 124, + }, + ].slice(0, numPullRequests), + }; + }); if (numPullRequests === 0) { github.rest.search.issuesAndPullRequests.mockResolvedValue({ data: { total_count: 0, items: [] }, }); - await expect( - extractInputs(github, context, createMockCore()), - ).resolves.toEqual({ + await expect(extractInputs(github, context, createMockCore())).resolves.toEqual({ owner: "TestRepoOwnerLogin", repo: "TestRepoName", head_sha: "abc123", @@ -292,9 +264,7 @@ describe("extractInputs", () => { data: { total_count: 1, items: [{ number: 789 }] }, }); - await expect( - extractInputs(github, context, createMockCore()), - ).resolves.toEqual({ + await expect(extractInputs(github, context, createMockCore())).resolves.toEqual({ owner: "TestRepoOwnerLogin", repo: "TestRepoName", head_sha: "abc123", @@ -305,9 +275,7 @@ describe("extractInputs", () => { expect(github.rest.search.issuesAndPullRequests).toHaveBeenCalled(); } else if (numPullRequests === 1 || numPullRequests === 2) { // Second PR is to a different repo, so expect same behavior with or without it - await expect( - extractInputs(github, context, createMockCore()), - ).resolves.toEqual({ + await expect(extractInputs(github, context, createMockCore())).resolves.toEqual({ owner: "TestRepoOwnerLogin", repo: "TestRepoName", head_sha: "abc123", @@ -316,14 +284,12 @@ describe("extractInputs", () => { }); } else { // Multiple PRs to triggering repo still causes an error (TODO: #33418) - await expect( - extractInputs(github, context, createMockCore()), - ).rejects.toThrow("Unexpected number of pull requests"); + await expect(extractInputs(github, context, createMockCore())).rejects.toThrow( + "Unexpected number of pull requests", + ); } - expect( - github.rest.repos.listPullRequestsAssociatedWithCommit, - ).toHaveBeenCalledWith({ + expect(github.rest.repos.listPullRequestsAssociatedWithCommit).toHaveBeenCalledWith({ owner: "TestRepoOwnerLoginFork", repo: "TestRepoName", commit_sha: "abc123", @@ -356,9 +322,7 @@ describe("extractInputs", () => { }, }; - await expect( - extractInputs(github, context, createMockCore()), - ).resolves.toEqual({ + await expect(extractInputs(github, context, createMockCore())).resolves.toEqual({ owner: "TestRepoOwnerLogin", repo: "TestRepoName", head_sha: "abc123", @@ -369,16 +333,14 @@ describe("extractInputs", () => { github.rest.actions.listWorkflowRunArtifacts.mockResolvedValue({ data: { artifacts: [{ name: "issue-number=not-a-number" }] }, }); - await expect( - extractInputs(github, context, createMockCore()), - ).rejects.toThrow(/invalid issue-number/i); + await expect(extractInputs(github, context, createMockCore())).rejects.toThrow( + /invalid issue-number/i, + ); github.rest.actions.listWorkflowRunArtifacts.mockResolvedValue({ data: { artifacts: [] }, }); - await expect( - extractInputs(github, context, createMockCore()), - ).resolves.toEqual({ + await expect(extractInputs(github, context, createMockCore())).resolves.toEqual({ owner: "TestRepoOwnerLogin", repo: "TestRepoName", head_sha: "abc123", @@ -398,9 +360,7 @@ describe("extractInputs", () => { }, }; - await expect( - extractInputs(null, context, createMockCore()), - ).rejects.toThrow(); + await expect(extractInputs(null, context, createMockCore())).rejects.toThrow(); }); it("workflow_run:completed:check_run", async () => { @@ -430,9 +390,7 @@ describe("extractInputs", () => { github.rest.actions.listWorkflowRunArtifacts.mockResolvedValue({ data: { artifacts: [] }, }); - await expect( - extractInputs(github, context, createMockCore()), - ).resolves.toEqual({ + await expect(extractInputs(github, context, createMockCore())).resolves.toEqual({ owner: "TestRepoOwnerLogin", repo: "TestRepoName", head_sha: "abc123", @@ -448,8 +406,7 @@ describe("extractInputs", () => { payload: { action: "completed", check_run: { - details_url: - "https://dev.azure.com/abc/123-456/_build/results?buildId=56789", + details_url: "https://dev.azure.com/abc/123-456/_build/results?buildId=56789", head_sha: "abc123", }, repository: { @@ -461,16 +418,13 @@ describe("extractInputs", () => { }, }; - await expect( - extractInputs(github, context, createMockCore()), - ).resolves.toEqual({ + await expect(extractInputs(github, context, createMockCore())).resolves.toEqual({ owner: "TestRepoOwnerLogin", repo: "TestRepoName", issue_number: NaN, head_sha: "abc123", run_id: NaN, - details_url: - "https://dev.azure.com/abc/123-456/_build/results?buildId=56789", + details_url: "https://dev.azure.com/abc/123-456/_build/results?buildId=56789", }); }); @@ -481,8 +435,7 @@ describe("extractInputs", () => { payload: { action: "completed", check_run: { - details_url: - "https://dev.azure.com/abc/123-456/_build/results?buildId=56789", + details_url: "https://dev.azure.com/abc/123-456/_build/results?buildId=56789", head_sha: "abc123", }, repository: { @@ -493,9 +446,9 @@ describe("extractInputs", () => { }, }; - await expect( - extractInputs(github, context, createMockCore()), - ).rejects.toThrow("from context payload"); + await expect(extractInputs(github, context, createMockCore())).rejects.toThrow( + "from context payload", + ); }); }); @@ -516,9 +469,7 @@ it("check_run:completed", async () => { }, }; - await expect( - extractInputs(createMockGithub(), context, createMockCore()), - ).resolves.toEqual({ + await expect(extractInputs(createMockGithub(), context, createMockCore())).resolves.toEqual({ owner: "TestRepoOwnerLogin", repo: "TestRepoName", head_sha: "head_sha", diff --git a/.github/workflows/test/github.test.js b/.github/workflows/test/github.test.js index 74094b2df366..3cc5770e4e60 100644 --- a/.github/workflows/test/github.test.js +++ b/.github/workflows/test/github.test.js @@ -1,16 +1,197 @@ -import { describe, expect, it } from "vitest"; -import { writeToActionsSummary } from "../src/github.js"; -import { createMockCore } from "./mocks.js"; +import { describe, expect, it, vi } from "vitest"; +import { getCheckRuns, getWorkflowRuns, writeToActionsSummary } from "../src/github.js"; +import { createMockContext, createMockCore, createMockGithub } from "./mocks.js"; const mockCore = createMockCore(); +describe("getCheckRuns", () => { + it("returns matching check_run", async () => { + const githubMock = createMockGithub(); + githubMock.rest.checks.listForRef = vi.fn().mockResolvedValue({ + data: { + check_runs: [ + { + name: "checkRunName", + status: "completed", + conclusion: "success", + }, + ], + }, + }); + + const actual = await getCheckRuns( + githubMock, + createMockContext(), + createMockCore(), + "checkRunName", + "head_sha", + ); + + expect(actual).toEqual([ + expect.objectContaining({ + name: "checkRunName", + status: "completed", + conclusion: "success", + }), + ]); + }); + + it("returns null when no check matches", async () => { + const githubMock = createMockGithub(); + githubMock.rest.checks.listForRef = vi.fn().mockResolvedValue({ + data: { + check_runs: [], + }, + }); + + const actual = await getCheckRuns(githubMock, createMockContext(), "checkRunName", "head_sha"); + + expect(actual).toEqual([]); + }); + + it("throws when multiple checks match", async () => { + const githubMock = createMockGithub(); + const earlierDate = "2025-04-01T00:00:00Z"; + const laterDate = "2025-04-02T00:00:00Z"; + githubMock.rest.checks.listForRef = vi.fn().mockResolvedValue({ + data: { + check_runs: [ + { + name: "checkRunName", + status: "completed", + conclusion: "success", + completed_at: earlierDate, + }, + { + name: "checkRunName", + status: "completed", + conclusion: "success", + completed_at: laterDate, + }, + ], + }, + }); + + const actual = await getCheckRuns(githubMock, createMockContext(), "checkRunName", "head_sha"); + + expect(actual).toEqual([ + expect.objectContaining({ + name: "checkRunName", + status: "completed", + conclusion: "success", + completed_at: laterDate, + }), + expect.objectContaining({ + name: "checkRunName", + status: "completed", + conclusion: "success", + completed_at: earlierDate, + }), + ]); + }); +}); + +describe("getWorkflowRuns", () => { + it("returns matching workflow_run", async () => { + const githubMock = createMockGithub(); + githubMock.rest.actions.listWorkflowRunsForRepo = vi.fn().mockResolvedValue({ + data: { + workflow_runs: [ + { + name: "workflowName", + status: "completed", + conclusion: "success", + }, + ], + }, + }); + + const actual = await getWorkflowRuns( + githubMock, + createMockContext(), + "workflowName", + "head_sha", + ); + + expect(actual).toEqual([ + expect.objectContaining({ + name: "workflowName", + status: "completed", + conclusion: "success", + }), + ]); + }); + + it("returns null when no workflow matches", async () => { + const githubMock = createMockGithub(); + githubMock.rest.actions.listWorkflowRunsForRepo = vi.fn().mockResolvedValue({ + data: { + workflow_runs: [ + { + name: "otherWorkflowName", + }, + ], + }, + }); + + const actual = await getWorkflowRuns( + githubMock, + createMockContext(), + "workflowName", + "head_sha", + ); + + expect(actual).toEqual([]); + }); + + it("returns latest when multiple workflows match", async () => { + const githubMock = createMockGithub(); + const earlyDate = "2025-04-01T00:00:00Z"; + const laterDate = "2025-04-02T00:00:00Z"; + githubMock.rest.actions.listWorkflowRunsForRepo = vi.fn().mockResolvedValue({ + data: { + workflow_runs: [ + { + name: "workflowName", + status: "completed", + conclusion: "success", + updated_at: earlyDate, + }, + { + name: "workflowName", + status: "completed", + conclusion: "success", + updated_at: laterDate, + }, + ], + }, + }); + + const actual = await getWorkflowRuns( + githubMock, + createMockContext(), + "workflowName", + "head_sha", + ); + + expect(actual).toEqual([ + expect.objectContaining({ + updated_at: laterDate, + }), + expect.objectContaining({ + updated_at: earlyDate, + }), + ]); + }); +}); + describe("writeToActionsSummary function", () => { it("should add content to the summary and write it", async () => { // Call function const result = await writeToActionsSummary("Test content", mockCore); // Verify result - expect(result).undefined; + expect(result).toBeUndefined(); // Verify summary methods were called expect(mockCore.summary.addRaw).toHaveBeenCalledWith("Test content"); @@ -20,13 +201,11 @@ describe("writeToActionsSummary function", () => { it("should handle exception", async () => { // Create a mock with a write method that throws an error const mockCoreWithError = createMockCore(); - mockCoreWithError.summary.write.mockRejectedValue( - new Error("Mock write error"), - ); + mockCoreWithError.summary.write.mockRejectedValue(new Error("Mock write error")); // Call function and validate it throws - await expect( - writeToActionsSummary("Test content", mockCoreWithError), - ).rejects.toThrow("Failed to write to the GitHub Actions summary"); + await expect(writeToActionsSummary("Test content", mockCoreWithError)).rejects.toThrow( + "Failed to write to the GitHub Actions summary", + ); }); }); diff --git a/.github/workflows/test/issues.test.js b/.github/workflows/test/issues.test.js index e1bb5048ef3d..03facfee03ea 100644 --- a/.github/workflows/test/issues.test.js +++ b/.github/workflows/test/issues.test.js @@ -1,6 +1,6 @@ -import { describe, expect, it, vi, beforeEach } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { getIssueNumber } from "../src/issues.js"; -import { createMockGithub, createMockCore } from "./mocks.js"; +import { createMockCore, createMockGithub } from "./mocks.js"; const mockGithub = createMockGithub(); const mockCore = createMockCore(); diff --git a/.github/workflows/test/message.test.js b/.github/workflows/test/message.test.js new file mode 100644 index 000000000000..8368b5b08e75 --- /dev/null +++ b/.github/workflows/test/message.test.js @@ -0,0 +1,205 @@ +// @ts-check + +import { describe, expect, it } from "vitest"; +import { ZodError } from "zod"; +import { + BaseMessageRecordSchema, + MessageLevel, + MessageLevelSchema, + MessageRecordSchema, + MessageType, + generateMarkdownTable, +} from "../src/message.js"; + +function testSchemaParse(schema, input, expectedError) { + if (expectedError) { + expect(() => schema.parse(input)).toThrowError(expectedError); + } else { + expect(schema.parse(input)).toEqual(input); + } +} + +describe("MessageLevelSchema", () => { + it.each([["foo", ZodError], [MessageLevel.Error], [MessageLevel.Info], [MessageLevel.Warning]])( + "parse(%o)", + (input, expectedError) => { + testSchemaParse(MessageLevelSchema, input, expectedError); + }, + ); +}); + +describe("BaseMessageRecordSchema", () => { + it.each([ + [{}, ZodError], + [{ level: "foo", message: 1, time: "bar" }, ZodError], + [{ level: MessageLevel.Error, message: "test-message", time: new Date().toISOString() }], + [ + { + level: MessageLevel.Warning, + message: "test-message", + time: new Date().toISOString(), + context: { toolVersion: "1.0" }, + group: "test-group", + extra: { extraKey: "extraVal" }, + groupName: "test-group-name", + }, + ], + ])("parse(%o)", (input, expectedError) => { + testSchemaParse(BaseMessageRecordSchema, input, expectedError); + }); +}); + +describe("MessageRecordSchema", () => { + it.each([ + [{}, ZodError], + [ + { + type: "Result", + level: MessageLevel.Error, + message: "test-message", + time: new Date().toISOString(), + // Missing required field "paths" + }, + ZodError, + ], + [ + { + type: "Raw", + level: MessageLevel.Error, + message: "test-message", + time: new Date().toISOString(), + }, + ], + [ + { + type: "Raw", + level: MessageLevel.Warning, + message: "test-message", + time: new Date().toISOString(), + context: { toolVersion: "1.0" }, + group: "test-group", + extra: { extraKey: "extraVal" }, + groupName: "test-group-name", + }, + ], + [ + { + type: "Result", + level: MessageLevel.Warning, + message: "test-message", + time: new Date().toISOString(), + context: { toolVersion: "1.0" }, + group: "test-group", + extra: { extraKey: "extraVal" }, + groupName: "test-group-name", + id: "test-id", + code: "test-code", + docUrl: "test-url", + paths: [ + { tag: "tag1", path: "path1", jsonPath: "jsonPath1" }, + { tag: "tag2", path: "path2" }, + ], + }, + ], + ])("parse(%o)", (input, expectedError) => { + testSchemaParse(MessageRecordSchema, input, expectedError); + }); +}); + +describe("generateMarkdownTable", () => { + it("no messages", () => { + const messages = []; + + expect(generateMarkdownTable(messages)).toMatchInlineSnapshot(` + "| Rule | Message | + | ---- | ------- |" + `); + }); + + it("all message types", () => { + /** @type {import("../src/message.js").MessageRecord[]} */ + const messages = []; + + /** @type {import("../src/message.js").ResultMessageRecord} */ + const resultMessageRecord = { + type: MessageType.Result, + level: MessageLevel.Error, + code: "NO_JSON_FILE_FOUND", + message: "The JSON file is not found but it is referenced from the readme file.", + docUrl: "https://github.com/Azure/avocado/blob/master/README.md#NO_JSON_FILE_FOUND", + time: "2025-07-11T00:01:00.418Z", + paths: [ + { + tag: "readme", + path: "https://github.com/mikeharder/azure-rest-api-specs/blob/4e6083f35e27d8e1e3b78222cf4bd27b87cd1409/specification/contosowidgetmanager/resource-manager/readme.md", + }, + { + tag: "json", + path: "https://github.com/mikeharder/azure-rest-api-specs/blob/4e6083f35e27d8e1e3b78222cf4bd27b87cd1409/specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso2.json", + }, + ], + }; + + messages.push(resultMessageRecord); + messages.push({ ...messages[0], level: MessageLevel.Info }); + messages.push({ ...messages[0], level: MessageLevel.Warning }); + + // URL with "path=" + messages.push({ + ...resultMessageRecord, + paths: [ + { + tag: resultMessageRecord.paths[0].tag, + path: + "https://www.example.org?path=" + encodeURIComponent(resultMessageRecord.paths[0].path), + }, + ], + }); + + // Empty path + messages.push({ + ...resultMessageRecord, + paths: [ + { + tag: resultMessageRecord.paths[0].tag, + path: "", + }, + ], + }); + + // No paths + messages.push({ + ...resultMessageRecord, + paths: [], + }); + + /** @type {import("../src/message.js").RawMessageRecord} */ + const rawMessageRecord = { + type: MessageType.Raw, + level: MessageLevel.Error, + message: "Test raw message", + time: "2025-07-11T00:01:00.418Z", + extra: { + key1: "value1", + key2: "value2", + }, + }; + + messages.push(rawMessageRecord); + + messages.push({ ...rawMessageRecord, extra: undefined }); + + expect(generateMarkdownTable(messages)).toMatchInlineSnapshot(` + "| Rule | Message | + | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | ❌ [NO_JSON_FILE_FOUND](https://github.com/Azure/avocado/blob/master/README.md#NO_JSON_FILE_FOUND) | The JSON file is not found but it is referenced from the readme file.
    readme: [specification/contosowidgetmanager/resource-manager/readme.md](https://github.com/mikeharder/azure-rest-api-specs/blob/4e6083f35e27d8e1e3b78222cf4bd27b87cd1409/specification/contosowidgetmanager/resource-manager/readme.md)
    json: [Microsoft.Contoso/stable/2021-11-01/contoso2.json](https://github.com/mikeharder/azure-rest-api-specs/blob/4e6083f35e27d8e1e3b78222cf4bd27b87cd1409/specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso2.json) | + | ℹ️ [NO_JSON_FILE_FOUND](https://github.com/Azure/avocado/blob/master/README.md#NO_JSON_FILE_FOUND) | The JSON file is not found but it is referenced from the readme file.
    readme: [specification/contosowidgetmanager/resource-manager/readme.md](https://github.com/mikeharder/azure-rest-api-specs/blob/4e6083f35e27d8e1e3b78222cf4bd27b87cd1409/specification/contosowidgetmanager/resource-manager/readme.md)
    json: [Microsoft.Contoso/stable/2021-11-01/contoso2.json](https://github.com/mikeharder/azure-rest-api-specs/blob/4e6083f35e27d8e1e3b78222cf4bd27b87cd1409/specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso2.json) | + | ⚠️ [NO_JSON_FILE_FOUND](https://github.com/Azure/avocado/blob/master/README.md#NO_JSON_FILE_FOUND) | The JSON file is not found but it is referenced from the readme file.
    readme: [specification/contosowidgetmanager/resource-manager/readme.md](https://github.com/mikeharder/azure-rest-api-specs/blob/4e6083f35e27d8e1e3b78222cf4bd27b87cd1409/specification/contosowidgetmanager/resource-manager/readme.md)
    json: [Microsoft.Contoso/stable/2021-11-01/contoso2.json](https://github.com/mikeharder/azure-rest-api-specs/blob/4e6083f35e27d8e1e3b78222cf4bd27b87cd1409/specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso2.json) | + | ❌ [NO_JSON_FILE_FOUND](https://github.com/Azure/avocado/blob/master/README.md#NO_JSON_FILE_FOUND) | The JSON file is not found but it is referenced from the readme file.
    readme: [specification/contosowidgetmanager/resource-manager/readme.md](https://www.example.org?path=https%3A%2F%2Fgithub.amrom.workers.dev%2Fmikeharder%2Fazure-rest-api-specs%2Fblob%2F4e6083f35e27d8e1e3b78222cf4bd27b87cd1409%2Fspecification%2Fcontosowidgetmanager%2Fresource-manager%2Freadme.md) | + | ❌ [NO_JSON_FILE_FOUND](https://github.com/Azure/avocado/blob/master/README.md#NO_JSON_FILE_FOUND) | The JSON file is not found but it is referenced from the readme file.
    | + | ❌ [NO_JSON_FILE_FOUND](https://github.com/Azure/avocado/blob/master/README.md#NO_JSON_FILE_FOUND) | The JSON file is not found but it is referenced from the readme file.
    | + | ❌ Test raw message | "key1":"value1",
    "key2":"value2" | + | ❌ Test raw message | |" + `); + }); +}); diff --git a/.github/workflows/test/mocks.js b/.github/workflows/test/mocks.js index bbbe5b99ad61..555bcb3d295a 100644 --- a/.github/workflows/test/mocks.js +++ b/.github/workflows/test/mocks.js @@ -14,12 +14,8 @@ export function createMockGithub() { rest: { actions: { listJobsForWorkflowRun: vi.fn().mockResolvedValue({ data: [] }), - listWorkflowRunArtifacts: vi - .fn() - .mockResolvedValue({ data: { artifacts: [] } }), - listWorkflowRunsForRepo: vi - .fn() - .mockResolvedValue({ data: { workflow_runs: [] } }), + listWorkflowRunArtifacts: vi.fn().mockResolvedValue({ data: { artifacts: [] } }), + listWorkflowRunsForRepo: vi.fn().mockResolvedValue({ data: { workflow_runs: [] } }), }, checks: { listForRef: vi.fn().mockResolvedValue({ data: { check_runs: [] } }), @@ -54,12 +50,10 @@ export function createMockCore() { error: vi.fn(console.error), warning: vi.fn(console.warn), isDebug: vi.fn().mockReturnValue(true), - setOutput: vi.fn((name, value) => - console.log(`setOutput('${name}', '${value}')`), - ), + setOutput: vi.fn((name, value) => console.log(`setOutput('${name}', '${value}')`)), setFailed: vi.fn((msg) => console.log(`setFailed('${msg}')`)), summary: { - // eslint-disable-next-line no-unused-vars + // eslint-disable-next-line @typescript-eslint/no-unused-vars addRaw: vi.fn(function (content) { return this; // Return 'this' for method chaining }), diff --git a/.github/workflows/test/ndjson.test.js b/.github/workflows/test/ndjson.test.js new file mode 100644 index 000000000000..0937ca3192c1 --- /dev/null +++ b/.github/workflows/test/ndjson.test.js @@ -0,0 +1,34 @@ +// @ts-check + +import { describe, expect, it } from "vitest"; +import { parse, stringify } from "../src/ndjson.js"; + +describe("ndjson", () => { + it("primitives, arrays, objects", () => { + // Cannot round-trip "undefined", but neither can JSON.parse()/JSON.stringify() + let values = [null, true, 1, "foo", [false, 2, "bar"], { a: true, b: 3, c: "qux" }]; + + const stringifiedValues = stringify(values); + expect(stringifiedValues).toMatchInlineSnapshot(` + "null + true + 1 + "foo" + [false,2,"bar"] + {"a":true,"b":3,"c":"qux"}" + `); + + const parsedValues = parse(stringifiedValues); + expect(parsedValues).toEqual(values); + }); + + it("empty array", () => { + let values = []; + + const stringifiedValues = stringify(values); + expect(stringifiedValues).toEqual(""); + + const parsedValues = parse(stringifiedValues); + expect(parsedValues).toEqual(values); + }); +}); diff --git a/.github/workflows/test/retries.test.js b/.github/workflows/test/retries.test.js index 8d7a9725403d..49895789f26b 100644 --- a/.github/workflows/test/retries.test.js +++ b/.github/workflows/test/retries.test.js @@ -1,5 +1,5 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; -import { retry, fetchWithRetry } from "../src/retries.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { fetchWithRetry, retry } from "../src/retries.js"; // Mock console.log to avoid cluttering test output const originalConsoleLog = console.log; @@ -22,10 +22,7 @@ describe("retry function", () => { }); it("should retry when function fails and then succeed", async () => { - const mockFn = vi - .fn() - .mockRejectedValueOnce(new Error("failure")) - .mockResolvedValue("success"); + const mockFn = vi.fn().mockRejectedValueOnce(new Error("failure")).mockResolvedValue("success"); // Use fake timers vi.useFakeTimers(); diff --git a/.github/workflows/test/sdk-breaking-change-labels.test.js b/.github/workflows/test/sdk-breaking-change-labels.test.js index 0385fe4e1585..244eb4dd6b1f 100644 --- a/.github/workflows/test/sdk-breaking-change-labels.test.js +++ b/.github/workflows/test/sdk-breaking-change-labels.test.js @@ -1,15 +1,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { sdkLabels } from "../../shared/src/sdk-types.js"; import { LabelAction } from "../src/label.js"; -import { - getLabelAndAction, - getLabelAndActionImpl, -} from "../src/sdk-breaking-change-labels.js"; -import { - createMockContext, - createMockCore, - createMockGithub, -} from "./mocks.js"; +import { getLabelAndAction, getLabelAndActionImpl } from "../src/sdk-breaking-change-labels.js"; +import { createMockContext, createMockCore, createMockGithub } from "./mocks.js"; // Mock dependencies vi.mock("../src/context.js", () => ({ @@ -33,8 +26,7 @@ describe("sdk-breaking-change-labels", () => { it("should extract inputs and call getLabelAndActionImpl", async () => { // Mock extracted inputs const mockInputs = { - details_url: - "https://dev.azure.com/project/_build/results?buildId=12345", + details_url: "https://dev.azure.com/project/_build/results?buildId=12345", head_sha: "abc123", }; @@ -93,8 +85,7 @@ describe("sdk-breaking-change-labels", () => { it("should correctly set labelAction to Remove", async () => { // Setup inputs const inputs = { - details_url: - "https://dev.azure.com/project/_build/results?buildId=12345", + details_url: "https://dev.azure.com/project/_build/results?buildId=12345", head_sha: "abc123", }; @@ -173,8 +164,7 @@ describe("sdk-breaking-change-labels", () => { it("should handle API failure", async () => { // Setup inputs const inputs = { - details_url: - "https://dev.azure.com/project/_build/results?buildId=12345", + details_url: "https://dev.azure.com/project/_build/results?buildId=12345", head_sha: "abc123", }; @@ -210,8 +200,7 @@ describe("sdk-breaking-change-labels", () => { it("should complete without op when artifact does not exist", async () => { // Setup inputs const inputs = { - details_url: - "https://dev.azure.com/project/_build/results?buildId=12345", + details_url: "https://dev.azure.com/project/_build/results?buildId=12345", head_sha: "abc123", }; @@ -225,10 +214,7 @@ describe("sdk-breaking-change-labels", () => { statusText: "Not Found", text: vi.fn().mockResolvedValue("Artifact not found"), }); - } else if ( - url.includes("artifacts?api-version=7.0") && - !url.includes("artifactName=") - ) { + } else if (url.includes("artifacts?api-version=7.0") && !url.includes("artifactName=")) { // List artifacts API call (used by fetchFailedArtifact) return Promise.resolve({ ok: true, @@ -238,8 +224,7 @@ describe("sdk-breaking-change-labels", () => { name: "spec-gen-sdk-artifact-failed", id: "12345", resource: { - downloadUrl: - "https://dev.azure.com/download-failed?format=zip", + downloadUrl: "https://dev.azure.com/download-failed?format=zip", }, }, ], @@ -280,8 +265,7 @@ describe("sdk-breaking-change-labels", () => { it("should throw error if resource is empty from the artifact api response", async () => { // Setup inputs const inputs = { - details_url: - "https://dev.azure.com/project/_build/results?buildId=12345", + details_url: "https://dev.azure.com/project/_build/results?buildId=12345", head_sha: "abc123", }; @@ -312,8 +296,7 @@ describe("sdk-breaking-change-labels", () => { it("should throw error if missing download url from the artifact api response", async () => { // Setup inputs const inputs = { - details_url: - "https://dev.azure.com/project/_build/results?buildId=12345", + details_url: "https://dev.azure.com/project/_build/results?buildId=12345", head_sha: "abc123", }; @@ -346,8 +329,7 @@ describe("sdk-breaking-change-labels", () => { it("should throw error when fail to fetch artifact content", async () => { // Setup inputs const inputs = { - details_url: - "https://dev.azure.com/project/_build/results?buildId=12345", + details_url: "https://dev.azure.com/project/_build/results?buildId=12345", head_sha: "abc123", }; @@ -394,8 +376,7 @@ describe("sdk-breaking-change-labels", () => { it("should handle exception during processing", async () => { // Setup inputs const inputs = { - details_url: - "https://dev.azure.com/project/_build/results?buildId=12345", + details_url: "https://dev.azure.com/project/_build/results?buildId=12345", head_sha: "abc123", }; diff --git a/.github/workflows/test/set-status.test.js b/.github/workflows/test/set-status.test.js index 823e04dd97bb..b2d22575ae54 100644 --- a/.github/workflows/test/set-status.test.js +++ b/.github/workflows/test/set-status.test.js @@ -1,11 +1,9 @@ +// @ts-check + import { beforeEach, describe, expect, it } from "vitest"; import { setStatusImpl } from "../src/set-status.js"; -import { - CheckConclusion, - CheckStatus, - CommitStatusState, -} from "../src/github.js"; +import { CheckConclusion, CheckStatus, CommitStatusState } from "../src/github.js"; import { createMockCore, createMockGithub } from "./mocks.js"; describe("setStatusImpl", () => { @@ -52,29 +50,204 @@ describe("setStatusImpl", () => { }); }); + it("sets success with multiple comma-separated labels - first label matches", async () => { + github.rest.issues.listLabelsOnIssue.mockResolvedValue({ + data: [{ name: "test" }, { name: "BreakingChange-Approved-Benign" }], + }); + + await expect( + setStatusImpl({ + owner: "test-owner", + repo: "test-repo", + head_sha: "test-head-sha", + issue_number: 123, + target_url: "https://test.com/set_status_url", + github, + core, + monitoredWorkflowName: "[TEST-IGNORE] Swagger BreakingChange - Analyze Code", + requiredStatusName: "[TEST-IGNORE] Swagger BreakingChange", + overridingLabel: + "BreakingChange-Approved-Benign,BreakingChange-Approved-BugFix,BreakingChange-Approved-UserImpact", + }), + ).resolves.toBeUndefined(); + + expect(github.rest.repos.createCommitStatus).toBeCalledWith({ + owner: "test-owner", + repo: "test-repo", + sha: "test-head-sha", + state: CommitStatusState.SUCCESS, + context: "[TEST-IGNORE] Swagger BreakingChange", + description: "Found label 'BreakingChange-Approved-Benign'", + target_url: "https://test.com/set_status_url", + }); + }); + + it("handles comma-separated labels with whitespace", async () => { + github.rest.issues.listLabelsOnIssue.mockResolvedValue({ + data: [{ name: "test" }, { name: "BreakingChange-Approved-UserImpact" }], + }); + + await expect( + setStatusImpl({ + owner: "test-owner", + repo: "test-repo", + head_sha: "test-head-sha", + issue_number: 123, + target_url: "https://test.com/set_status_url", + github, + core, + monitoredWorkflowName: "[TEST-IGNORE] Swagger BreakingChange - Analyze Code", + requiredStatusName: "[TEST-IGNORE] Swagger BreakingChange", + overridingLabel: + "BreakingChange-Approved-Benign, BreakingChange-Approved-BugFix , BreakingChange-Approved-UserImpact", + }), + ).resolves.toBeUndefined(); + + expect(github.rest.repos.createCommitStatus).toBeCalledWith({ + owner: "test-owner", + repo: "test-repo", + sha: "test-head-sha", + state: CommitStatusState.SUCCESS, + context: "[TEST-IGNORE] Swagger BreakingChange", + description: "Found label 'BreakingChange-Approved-UserImpact'", + target_url: "https://test.com/set_status_url", + }); + }); + + it("handles comma-separated labels with empty entries", async () => { + github.rest.issues.listLabelsOnIssue.mockResolvedValue({ + data: [{ name: "test" }, { name: "BreakingChange-Approved-Security" }], + }); + + await expect( + setStatusImpl({ + owner: "test-owner", + repo: "test-repo", + head_sha: "test-head-sha", + issue_number: 123, + target_url: "https://test.com/set_status_url", + github, + core, + monitoredWorkflowName: "[TEST-IGNORE] Swagger BreakingChange - Analyze Code", + requiredStatusName: "[TEST-IGNORE] Swagger BreakingChange", + overridingLabel: "BreakingChange-Approved-Benign,,BreakingChange-Approved-Security,", + }), + ).resolves.toBeUndefined(); + + expect(github.rest.repos.createCommitStatus).toBeCalledWith({ + owner: "test-owner", + repo: "test-repo", + sha: "test-head-sha", + state: CommitStatusState.SUCCESS, + context: "[TEST-IGNORE] Swagger BreakingChange", + description: "Found label 'BreakingChange-Approved-Security'", + target_url: "https://test.com/set_status_url", + }); + }); + + it("does not set success when none of the multiple labels match", async () => { + github.rest.issues.listLabelsOnIssue.mockResolvedValue({ + data: [{ name: "test" }, { name: "SomeOtherLabel" }], + }); + + github.rest.actions.listWorkflowRunsForRepo.mockResolvedValue({ + data: [], + }); + + await expect( + setStatusImpl({ + owner: "test-owner", + repo: "test-repo", + head_sha: "test-head-sha", + issue_number: 123, + target_url: "https://test.com/set_status_url", + github, + core, + monitoredWorkflowName: "[TEST-IGNORE] Swagger BreakingChange - Analyze Code", + requiredStatusName: "[TEST-IGNORE] Swagger BreakingChange", + overridingLabel: + "BreakingChange-Approved-Benign,BreakingChange-Approved-BugFix,BreakingChange-Approved-UserImpact", + }), + ).resolves.toBeUndefined(); + + expect(github.rest.repos.createCommitStatus).toBeCalledWith({ + owner: "test-owner", + repo: "test-repo", + sha: "test-head-sha", + state: CommitStatusState.PENDING, + context: "[TEST-IGNORE] Swagger BreakingChange", + target_url: "https://test.com/set_status_url", + }); + }); + + it("handles empty overriding label", async () => { + github.rest.issues.listLabelsOnIssue.mockResolvedValue({ + data: [{ name: "test" }], + }); + + github.rest.actions.listWorkflowRunsForRepo.mockResolvedValue({ + data: [], + }); + + await expect( + setStatusImpl({ + owner: "test-owner", + repo: "test-repo", + head_sha: "test-head-sha", + issue_number: 123, + target_url: "https://test.com/set_status_url", + github, + core, + monitoredWorkflowName: "[TEST-IGNORE] Swagger BreakingChange - Analyze Code", + requiredStatusName: "[TEST-IGNORE] Swagger BreakingChange", + overridingLabel: "", + }), + ).resolves.toBeUndefined(); + + expect(github.rest.repos.createCommitStatus).toBeCalledWith({ + owner: "test-owner", + repo: "test-repo", + sha: "test-head-sha", + state: CommitStatusState.PENDING, + context: "[TEST-IGNORE] Swagger BreakingChange", + target_url: "https://test.com/set_status_url", + }); + }); + + // TODO: Add tests for "job-summary" artifact it.each([ [ CheckStatus.COMPLETED, CheckConclusion.SUCCESS, CommitStatusState.SUCCESS, + [], "https://test.com/workflow_run_html_url", ], [ CheckStatus.COMPLETED, CheckConclusion.FAILURE, CommitStatusState.FAILURE, + [], "https://test.com/job_html_url?pr=123", ], + [ + CheckStatus.COMPLETED, + CheckConclusion.FAILURE, + CommitStatusState.FAILURE, + ["job-status"], + "https://test.com/workflow_run_html_url", + ], [ CheckStatus.IN_PROGRESS, null, CommitStatusState.PENDING, + [], "https://test.com/workflow_run_html_url", ], - [null, null, CommitStatusState.PENDING, "https://test.com/set_status_url"], + [null, null, CommitStatusState.PENDING, [], "https://test.com/set_status_url"], ])( - "(%s, %s, %s) => %s", - async (checkStatus, checkConclusion, commitStatusState, targetUrl) => { + "(%s, %s, %s, %o) => %s", + async (checkStatus, checkConclusion, commitStatusState, artifactNames, targetUrl) => { if (checkStatus) { github.rest.actions.listWorkflowRunsForRepo.mockResolvedValue({ data: [ @@ -88,6 +261,12 @@ describe("setStatusImpl", () => { ], }); + github.rest.actions.listWorkflowRunArtifacts.mockResolvedValue({ + data: artifactNames.map((n) => ({ + name: n, + })), + }); + if ( checkConclusion === CheckConclusion.SUCCESS || checkConclusion === CheckConclusion.FAILURE diff --git a/.github/workflows/test/spec-gen-sdk-status.test.js b/.github/workflows/test/spec-gen-sdk-status.test.js index da82bbf897fd..6ac0ad303da9 100644 --- a/.github/workflows/test/spec-gen-sdk-status.test.js +++ b/.github/workflows/test/spec-gen-sdk-status.test.js @@ -1,11 +1,10 @@ -/* eslint-disable no-unused-vars */ // @ts-check -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { setSpecGenSdkStatusImpl } from "../src/spec-gen-sdk-status.js"; +import fs from "fs"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import * as artifacts from "../src/artifacts.js"; import * as github from "../src/github.js"; -import { createMockGithub, createMockCore } from "./mocks.js"; -import fs from "fs"; +import { setSpecGenSdkStatusImpl } from "../src/spec-gen-sdk-status.js"; +import { createMockCore, createMockGithub } from "./mocks.js"; describe("spec-gen-sdk-status", () => { let mockGithub; @@ -22,28 +21,26 @@ describe("spec-gen-sdk-status", () => { // Setup specific mocks getAzurePipelineArtifactMock = vi .spyOn(artifacts, "getAzurePipelineArtifact") - .mockImplementation( - async ({ ado_build_id, ado_project_url, artifactName }) => { - return { - artifactData: JSON.stringify({ - language: "test-language", - result: "succeeded", - isSpecGenSdkCheckRequired: true, - }), - }; - }, - ); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementation(async ({ ado_build_id, ado_project_url, artifactName }) => { + return { + artifactData: JSON.stringify({ + language: "test-language", + result: "succeeded", + isSpecGenSdkCheckRequired: true, + }), + }; + }); writeToActionsSummaryMock = vi .spyOn(github, "writeToActionsSummary") + // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementation(async (content, core) => { // Implementation that just returns return; }); - appendFileSyncMock = vi - .spyOn(fs, "appendFileSync") - .mockImplementation(vi.fn()); + appendFileSyncMock = vi.spyOn(fs, "appendFileSync").mockImplementation(vi.fn()); // Reset mock call counts vi.clearAllMocks(); @@ -105,8 +102,7 @@ describe("spec-gen-sdk-status", () => { name: "SDK Validation", status: "completed", conclusion: "success", - details_url: - "https://dev.azure.com/project/_build/results?buildId=123", + details_url: "https://dev.azure.com/project/_build/results?buildId=123", }, ], }, @@ -153,43 +149,39 @@ describe("spec-gen-sdk-status", () => { name: "SDK Validation", status: "completed", conclusion: "success", - details_url: - "https://dev.azure.com/project/_build/results?buildId=123", + details_url: "https://dev.azure.com/project/_build/results?buildId=123", }, { app: { name: "Azure Pipelines" }, name: "SDK Validation", status: "completed", conclusion: "failure", - details_url: - "https://dev.azure.com/project/_build/results?buildId=456", + details_url: "https://dev.azure.com/project/_build/results?buildId=456", }, ], }, }); // Mock getAzurePipelineArtifact to return mixed results - getAzurePipelineArtifactMock.mockImplementation( - async ({ ado_build_id }) => { - if (ado_build_id === "123") { - return { - artifactData: JSON.stringify({ - language: "test-language-1", - result: "succeeded", - isSpecGenSdkCheckRequired: true, - }), - }; - } else { - return { - artifactData: JSON.stringify({ - language: "test-language-2", - result: "failed", - isSpecGenSdkCheckRequired: true, - }), - }; - } - }, - ); + getAzurePipelineArtifactMock.mockImplementation(async ({ ado_build_id }) => { + if (ado_build_id === "123") { + return { + artifactData: JSON.stringify({ + language: "test-language-1", + result: "succeeded", + isSpecGenSdkCheckRequired: true, + }), + }; + } else { + return { + artifactData: JSON.stringify({ + language: "test-language-2", + result: "failed", + isSpecGenSdkCheckRequired: true, + }), + }; + } + }); // Call the function await setSpecGenSdkStatusImpl({ @@ -223,8 +215,7 @@ describe("spec-gen-sdk-status", () => { name: "SDK Validation", status: "completed", conclusion: "success", - details_url: - "https://dev.azure.com/project/_build/results?buildId=123", + details_url: "https://dev.azure.com/project/_build/results?buildId=123", }, ], }, @@ -242,9 +233,7 @@ describe("spec-gen-sdk-status", () => { // Verify summary was written expect(writeToActionsSummaryMock).toHaveBeenCalled(); - expect(writeToActionsSummaryMock.mock.calls[0][0]).toContain( - "SDK Validation CI Checks Result", - ); + expect(writeToActionsSummaryMock.mock.calls[0][0]).toContain("SDK Validation CI Checks Result"); }); it("should handle artifact download failures", async () => { @@ -257,8 +246,7 @@ describe("spec-gen-sdk-status", () => { name: "SDK Validation", status: "completed", conclusion: "success", - details_url: - "https://dev.azure.com/project/_build/results?buildId=123", + details_url: "https://dev.azure.com/project/_build/results?buildId=123", }, ], }, @@ -290,43 +278,39 @@ describe("spec-gen-sdk-status", () => { name: "SDK Validation", status: "completed", conclusion: "success", - details_url: - "https://dev.azure.com/project/_build/results?buildId=123", + details_url: "https://dev.azure.com/project/_build/results?buildId=123", }, { app: { name: "Azure Pipelines" }, name: "SDK Validation", status: "completed", conclusion: "failure", - details_url: - "https://dev.azure.com/project/_build/results?buildId=456", + details_url: "https://dev.azure.com/project/_build/results?buildId=456", }, ], }, }); // Mock getAzurePipelineArtifact to return mixed results - getAzurePipelineArtifactMock.mockImplementation( - async ({ ado_build_id }) => { - if (ado_build_id === "123") { - return { - artifactData: JSON.stringify({ - language: "test-language-1", - result: "succeeded", - isSpecGenSdkCheckRequired: true, - }), - }; - } else { - return { - artifactData: JSON.stringify({ - language: "test-language-2", - result: "failed", - isSpecGenSdkCheckRequired: false, // Not required - }), - }; - } - }, - ); + getAzurePipelineArtifactMock.mockImplementation(async ({ ado_build_id }) => { + if (ado_build_id === "123") { + return { + artifactData: JSON.stringify({ + language: "test-language-1", + result: "succeeded", + isSpecGenSdkCheckRequired: true, + }), + }; + } else { + return { + artifactData: JSON.stringify({ + language: "test-language-2", + result: "failed", + isSpecGenSdkCheckRequired: false, // Not required + }), + }; + } + }); // Call the function await setSpecGenSdkStatusImpl({ diff --git a/.github/workflows/test/summarize-checks.test.js b/.github/workflows/test/summarize-checks.test.js new file mode 100644 index 000000000000..058963c37c28 --- /dev/null +++ b/.github/workflows/test/summarize-checks.test.js @@ -0,0 +1,592 @@ +import { Octokit } from "@octokit/rest"; +import { describe, expect, it } from "vitest"; +import { + createNextStepsComment, + summarizeChecksImpl, + updateLabels, +} from "../src/summarize-checks/summarize-checks.js"; +import { createMockCore } from "./mocks.js"; + +const mockCore = createMockCore(); + +describe("Summarize Checks Tests", () => { + describe("next steps comment rendering", () => { + it("Should generate summary for a mockdata PR scenario", async () => { + const repo = "azure-rest-api-specs"; + const targetBranch = "main"; + const labelNames = [ + "Cognitive Services", + "data-plane", + "TypeSpec", + "VersioningReviewRequired", + ]; + const fyiCheckRuns = []; + const expectedComment = + "

    Next Steps to Merge

    Next steps that must be taken to merge this PR:
      " + + "
    • ❌ This PR targets either the main branch of the public specs repo or the RPSaaSMaster branch of the private specs repo. " + + "These branches are not intended for iterative development. Therefore, you must acknowledge you understand that after this PR is merged, the APIs are considered " + + "shipped to Azure customers. Any further attempts at in-place modifications to the APIs will be subject to Azure's versioning " + + "and breaking change policies. Additionally, for control plane APIs, you must acknowledge that you are following all " + + 'the best practices documented by ARM at aka.ms/armapibestpractices. ' + + "If you do intend to release the APIs to your customers by merging this PR, add the PublishToCustomers label " + + "to your PR in acknowledgement of the above. Otherwise, retarget this PR onto a feature branch, i.e. with prefix release- " + + '(see aka.ms/azsdk/api-versions#release--branches).
    • ' + + "
    • ❌ This PR has at least one change violating Azure versioning policy (label: VersioningReviewRequired).
      To unblock this PR, either a) " + + 'introduce a new API version with these changes instead of modifying an existing API version, or b) follow the process at aka.ms/brch.' + + "
    • ❌ The required check named TypeSpec Validation has failed. Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult " + + 'the aka.ms/ci-fix guide
    '; + const expectedOutput = [expectedComment, "blocked"]; + + const requiredCheckRuns = [ + { + name: "SpellCheck", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 1000, + name: "SpellCheck", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "TypeSpec Requirement", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 1000, + name: "TypeSpec Requirement", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "Protected Files", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 1000, + name: "Protected Files", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "TypeSpec Validation", + status: "COMPLETED", + conclusion: "FAILURE", + checkInfo: { + precedence: 0, + name: "TypeSpec Validation", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "Swagger BreakingChange", + status: "COMPLETED", + conclusion: "FAILURE", + checkInfo: { + precedence: 4, + name: "Swagger BreakingChange", + suppressionLabels: ["Versioning-Approved-*", "BreakingChange-Approved-*"], + troubleshootingGuide: + 'To unblock this PR, follow the process at aka.ms/brch.', + }, + }, + { + name: "Breaking Change(Cross-Version)", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 4, + name: "Breaking Change(Cross-Version)", + suppressionLabels: ["Versioning-Approved-*", "BreakingChange-Approved-*"], + troubleshootingGuide: + 'To unblock this PR, follow the process at aka.ms/brch.', + }, + }, + { + name: "Swagger Avocado", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 1, + name: "Swagger Avocado", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "Swagger ModelValidation", + status: "COMPLETED", + conclusion: "FAILURE", + checkInfo: { + precedence: 3, + name: "Swagger ModelValidation", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "Swagger SemanticValidation", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 2, + name: "Swagger SemanticValidation", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "Swagger Lint(RPaaS)", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 5, + name: "Swagger Lint(RPaaS)", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "Automated merging requirements met", + status: "COMPLETED", + conclusion: "FAILURE", + checkInfo: { + precedence: 10, + name: "Automated merging requirements met", + suppressionLabels: [], + troubleshootingGuide: + 'This is the final check that must pass. Refer to the check in the PR\'s \'Checks\' tab for details on how to fix it and consult the aka.ms/ci-fix guide. In addition, refer to step 4 in the PR workflow diagram', + }, + }, + { + name: "license/cla", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 0, + name: "license/cla", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "Swagger PrettierCheck", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 1, + name: "Swagger PrettierCheck", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + ]; + + const output = await createNextStepsComment( + mockCore, + repo, + labelNames, + targetBranch, + requiredCheckRuns, + fyiCheckRuns, + ); + + expect(output).toEqual(expectedOutput); + }); + + it("should generate pending summary for no matched check suites", async () => { + const repo = "azure-rest-api-specs"; + const targetBranch = "main"; + const labelNames = []; + const fyiCheckRuns = []; + const requiredCheckRuns = []; + const expectedOutput = [ + "

    Next Steps to Merge

    ⌛ Please wait. Next steps to merge this PR are being evaluated by automation. ⌛", + "pending", + ]; + + const output = await createNextStepsComment( + mockCore, + repo, + labelNames, + targetBranch, + requiredCheckRuns, + fyiCheckRuns, + ); + + expect(output).toEqual(expectedOutput); + }); + + it("should generate success summary for all completed check suites", async () => { + const repo = "azure-rest-api-specs"; + const targetBranch = "main"; + const labelNames = []; + const fyiCheckRuns = []; + const expectedOutput = [ + '

    Next Steps to Merge

    ✅ All automated merging requirements have been met! To get your PR merged, see aka.ms/azsdk/specreview/merge.', + "success", + ]; + + const requiredCheckRuns = [ + { + name: "SpellCheck", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 1000, + name: "SpellCheck", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "TypeSpec Requirement", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 1000, + name: "TypeSpec Requirement", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "Protected Files", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 1000, + name: "Protected Files", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "TypeSpec Validation", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 0, + name: "TypeSpec Validation", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "Swagger BreakingChange", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 4, + name: "Swagger BreakingChange", + suppressionLabels: ["Versioning-Approved-*", "BreakingChange-Approved-*"], + troubleshootingGuide: + 'To unblock this PR, follow the process at aka.ms/brch.', + }, + }, + { + name: "Breaking Change(Cross-Version)", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 4, + name: "Breaking Change(Cross-Version)", + suppressionLabels: ["Versioning-Approved-*", "BreakingChange-Approved-*"], + troubleshootingGuide: + 'To unblock this PR, follow the process at aka.ms/brch.', + }, + }, + { + name: "Swagger Avocado", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 1, + name: "Swagger Avocado", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "Swagger ModelValidation", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 3, + name: "Swagger ModelValidation", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "Swagger SemanticValidation", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 2, + name: "Swagger SemanticValidation", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "Swagger Lint(RPaaS)", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 5, + name: "Swagger Lint(RPaaS)", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "Automated merging requirements met", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 10, + name: "Automated merging requirements met", + suppressionLabels: [], + troubleshootingGuide: + 'This is the final check that must pass. Refer to the check in the PR\'s \'Checks\' tab for details on how to fix it and consult the aka.ms/ci-fix guide. In addition, refer to step 4 in the PR workflow diagram', + }, + }, + { + name: "license/cla", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 0, + name: "license/cla", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "Swagger PrettierCheck", + status: "COMPLETED", + conclusion: "SUCCESS", + checkInfo: { + precedence: 1, + name: "Swagger PrettierCheck", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + ]; + + const output = await createNextStepsComment( + mockCore, + repo, + labelNames, + targetBranch, + requiredCheckRuns, + fyiCheckRuns, + ); + + expect(output).toEqual(expectedOutput); + }); + + it("should generate pending summary when checks are in progress", async () => { + const repo = "azure-rest-api-specs"; + const targetBranch = "main"; + const labelNames = []; + const fyiCheckRuns = []; + const expectedOutput = [ + "

    Next Steps to Merge

    ⌛ Please wait. Next steps to merge this PR are being evaluated by automation. ⌛", + "pending", + ]; + + const requiredCheckRuns = [ + { + name: "TypeSpec Validation", + status: "IN_PROGRESS", + conclusion: null, + checkInfo: { + precedence: 0, + name: "TypeSpec Validation", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "Swagger Avocado", + status: "QUEUED", + conclusion: null, + checkInfo: { + precedence: 1, + name: "Swagger Avocado", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + { + name: "license/cla", + status: "IN_PROGRESS", + conclusion: null, + checkInfo: { + precedence: 0, + name: "license/cla", + suppressionLabels: [], + troubleshootingGuide: + "Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide", + }, + }, + ]; + + const output = await createNextStepsComment( + mockCore, + repo, + labelNames, + targetBranch, + requiredCheckRuns, + fyiCheckRuns, + ); + + expect(output).toEqual(expectedOutput); + }); + + it.skipIf(!process.env.GITHUB_TOKEN || !process.env.INTEGRATION_TEST)( + "Should fetch real PR data when GITHUB_TOKEN is available and when integration testing is enabled.", + async () => { + const github = new Octokit({ + auth: process.env.GITHUB_TOKEN, + }); + + const owner = "Azure"; + const repo = "azure-rest-api-specs"; + const issue_number = 35629; + const head_sha = "c12f0191c34212c4e6be88121d132ccb0a7f560c"; + const event_name = "pull_request"; + const mockContext = { + repo: { + owner: owner, + repo: repo, + }, + payload: { + action: "opened", + pull_request: { + number: issue_number, + head: { + sha: head_sha, + }, + }, + }, + eventName: event_name, + }; + + await expect( + summarizeChecksImpl( + github, + mockContext, + mockCore, + owner, + repo, + issue_number, + head_sha, + event_name, + "main", + ), + ).resolves.not.toThrow(); + }, + 600000, + ); + }); + + describe("label add and remove", () => { + const testCases = [ + { + existingLabels: ["WaitForARMFeedback", "ARMChangesRequested", "other-label"], + expectedLabelsToAdd: [], + expectedLabelsToRemove: ["WaitForARMFeedback"], + }, + { + existingLabels: ["other-label", "ARMChangesRequested"], + expectedLabelsToAdd: [], + expectedLabelsToRemove: [], + }, + { + existingLabels: [ + "WaitForARMFeedback", + "ARMSignedOff", + "ARMChangesRequested", + "other-label", + ], + expectedLabelsToAdd: [], + expectedLabelsToRemove: ["WaitForARMFeedback", "ARMChangesRequested"], + }, + { + existingLabels: ["WaitForARMFeedback", "ARMSignedOff", "other-label"], + expectedLabelsToAdd: [], + expectedLabelsToRemove: ["WaitForARMFeedback"], + }, + { + existingLabels: ["ARMChangesRequested", "ARMSignedOff", "other-label"], + expectedLabelsToAdd: [], + expectedLabelsToRemove: ["ARMChangesRequested"], + }, + { + existingLabels: ["other-label", "ARMSignedOff"], + expectedLabelsToAdd: [], + expectedLabelsToRemove: [], + }, + { + existingLabels: ["WaitForARMFeedback", "other-label"], + expectedLabelsToAdd: [], + expectedLabelsToRemove: [], + }, + { + existingLabels: ["other-label"], + expectedLabelsToAdd: ["WaitForARMFeedback"], + expectedLabelsToRemove: [], + }, + { + existingLabels: ["WaitForARMFeedback", "ARMChangesRequested"], + expectedLabelsToAdd: [], + expectedLabelsToRemove: ["WaitForARMFeedback"], + }, + { + existingLabels: ["WaitForARMFeedback", "ARMChangesRequested"], + expectedLabelsToAdd: [], + expectedLabelsToRemove: ["WaitForARMFeedback"], + }, + ]; + + it.each(testCases)( + "$description", + async ({ existingLabels, expectedLabelsToAdd, expectedLabelsToRemove }) => { + const labelContext = await updateLabels(existingLabels, undefined); + + expect([...labelContext.toAdd].sort()).toEqual(expectedLabelsToAdd.sort()); + expect([...labelContext.toRemove].sort()).toEqual(expectedLabelsToRemove.sort()); + }, + ); + }); +}); diff --git a/.github/workflows/test/update-labels.test.js b/.github/workflows/test/update-labels.test.js index 506315f6d173..4c14438888ea 100644 --- a/.github/workflows/test/update-labels.test.js +++ b/.github/workflows/test/update-labels.test.js @@ -1,56 +1,9 @@ import { describe, expect, it } from "vitest"; import { PER_PAGE_MAX } from "../src/github.js"; import updateLabels, { updateLabelsImpl } from "../src/update-labels.js"; -import { - createMockCore, - createMockGithub, - createMockRequestError, -} from "./mocks.js"; +import { createMockCore, createMockGithub, createMockRequestError } from "./mocks.js"; describe("updateLabels", () => { - it("loads inputs from env", async () => { - const github = createMockGithub(); - github.rest.actions.listWorkflowRunArtifacts.mockResolvedValue({ - data: { - artifacts: [{ name: "label-foo=true" }], - }, - }); - - try { - process.env.OWNER = "TestRepoOwnerLoginEnv"; - process.env.REPO = "TestRepoNameEnv"; - process.env.ISSUE_NUMBER = "123"; - process.env.RUN_ID = "456"; - - await expect( - updateLabels({ - github: github, - context: null, - core: createMockCore(), - }), - ).resolves.toBeUndefined(); - } finally { - delete process.env.OWNER; - delete process.env.REPO; - delete process.env.ISSUE_NUMBER; - delete process.env.RUN_ID; - } - - expect(github.rest.actions.listWorkflowRunArtifacts).toBeCalledWith({ - owner: "TestRepoOwnerLoginEnv", - repo: "TestRepoNameEnv", - run_id: 456, - per_page: PER_PAGE_MAX, - }); - expect(github.rest.issues.addLabels).toBeCalledWith({ - owner: "TestRepoOwnerLoginEnv", - repo: "TestRepoNameEnv", - issue_number: 123, - labels: ["foo"], - }); - expect(github.rest.issues.removeLabel).toBeCalledTimes(0); - }); - it("loads inputs from context", async () => { const github = createMockGithub(); github.rest.actions.listWorkflowRunArtifacts.mockResolvedValue({ @@ -100,64 +53,6 @@ describe("updateLabels", () => { }); expect(github.rest.issues.removeLabel).toBeCalledTimes(0); }); - - it("loads inputs from env and context", async () => { - const github = createMockGithub(); - github.rest.actions.listWorkflowRunArtifacts.mockResolvedValue({ - data: { - artifacts: [{ name: "label-foo=true" }], - }, - }); - - const context = { - eventName: "workflow_run", - payload: { - action: "completed", - workflow_run: { - event: "pull_request", - head_sha: "abc123", - id: 456, - repository: { - name: "TestRepoName", - owner: { - login: "TestRepoOwnerLogin", - }, - }, - pull_requests: [{ number: 123 }], - }, - }, - }; - - try { - process.env.OWNER = "TestRepoOwnerLoginEnv"; - process.env.REPO = "TestRepoNameEnv"; - - await expect( - updateLabels({ - github: github, - context: context, - core: createMockCore(), - }), - ).resolves.toBeUndefined(); - } finally { - delete process.env.OWNER; - delete process.env.REPO; - } - - expect(github.rest.actions.listWorkflowRunArtifacts).toBeCalledWith({ - owner: "TestRepoOwnerLoginEnv", - repo: "TestRepoNameEnv", - run_id: 456, - per_page: PER_PAGE_MAX, - }); - expect(github.rest.issues.addLabels).toBeCalledWith({ - owner: "TestRepoOwnerLoginEnv", - repo: "TestRepoNameEnv", - issue_number: 123, - labels: ["foo"], - }); - expect(github.rest.issues.removeLabel).toBeCalledTimes(0); - }); }); describe("updateLabelsImpl", () => { @@ -311,47 +206,42 @@ describe("updateLabelsImpl", () => { expect(github.rest.issues.removeLabel).toBeCalledTimes(0); }); - it.each([404, 500, 501])( - "handles error removing label (%s)", - async (status) => { - const github = createMockGithub(); - github.rest.actions.listWorkflowRunArtifacts.mockResolvedValue({ - data: { - artifacts: [{ name: "label-foo=false" }], - }, - }); - github.rest.issues.removeLabel.mockRejectedValue( - createMockRequestError(status), - ); + it.each([404, 500, 501])("handles error removing label (%s)", async (status) => { + const github = createMockGithub(); + github.rest.actions.listWorkflowRunArtifacts.mockResolvedValue({ + data: { + artifacts: [{ name: "label-foo=false" }], + }, + }); + github.rest.issues.removeLabel.mockRejectedValue(createMockRequestError(status)); - const updateLabelsImplPromise = updateLabelsImpl({ - owner: "owner", - repo: "repo", - issue_number: 123, - run_id: 456, - github: github, - core: createMockCore(), - }); + const updateLabelsImplPromise = updateLabelsImpl({ + owner: "owner", + repo: "repo", + issue_number: 123, + run_id: 456, + github: github, + core: createMockCore(), + }); - if (status == 404) { - await expect(updateLabelsImplPromise).resolves.toBeUndefined(); - } else { - await expect(updateLabelsImplPromise).rejects.toThrow(); - } + if (status == 404) { + await expect(updateLabelsImplPromise).resolves.toBeUndefined(); + } else { + await expect(updateLabelsImplPromise).rejects.toThrow(); + } - expect(github.rest.actions.listWorkflowRunArtifacts).toBeCalledWith({ - owner: "owner", - repo: "repo", - run_id: 456, - per_page: PER_PAGE_MAX, - }); - expect(github.rest.issues.addLabels).toBeCalledTimes(0); - expect(github.rest.issues.removeLabel).toBeCalledWith({ - owner: "owner", - repo: "repo", - issue_number: 123, - name: "foo", - }); - }, - ); + expect(github.rest.actions.listWorkflowRunArtifacts).toBeCalledWith({ + owner: "owner", + repo: "repo", + run_id: 456, + per_page: PER_PAGE_MAX, + }); + expect(github.rest.issues.addLabels).toBeCalledTimes(0); + expect(github.rest.issues.removeLabel).toBeCalledWith({ + owner: "owner", + repo: "repo", + issue_number: 123, + name: "foo", + }); + }); }); diff --git a/.github/workflows/test/verify-run-status.test.js b/.github/workflows/test/verify-run-status.test.js index c7e8e09f801e..7a6280e9d26a 100644 --- a/.github/workflows/test/verify-run-status.test.js +++ b/.github/workflows/test/verify-run-status.test.js @@ -1,14 +1,6 @@ -import { describe, expect, it, vi } from "vitest"; -import { - createMockGithub, - createMockContext, - createMockCore, -} from "./mocks.js"; -import { - getCheckRuns, - getWorkflowRuns, - verifyRunStatusImpl, -} from "../src/verify-run-status.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { verifyRunStatusImpl } from "../src/verify-run-status.js"; +import { createMockCore, createMockGithub } from "./mocks.js"; vi.mock("../src/context.js", () => { return { @@ -18,218 +10,30 @@ vi.mock("../src/context.js", () => { }; }); -describe("getCheckRuns", () => { - it("returns matching check_run", async () => { - const githubMock = createMockGithub(); - githubMock.rest.checks.listForRef = vi.fn().mockResolvedValue({ - data: { - check_runs: [ - { - name: "checkRunName", - status: "completed", - conclusion: "success", - }, - ], - }, - }); - - const actual = await getCheckRuns( - githubMock, - createMockContext(), - createMockCore(), - "checkRunName", - "head_sha", - ); - - expect(actual).toEqual([ - expect.objectContaining({ - name: "checkRunName", - status: "completed", - conclusion: "success", - }), - ]); - }); - - it("returns null when no check matches", async () => { - const githubMock = createMockGithub(); - githubMock.rest.checks.listForRef = vi.fn().mockResolvedValue({ - data: { - check_runs: [], - }, - }); - - const actual = await getCheckRuns( - githubMock, - createMockContext(), - "checkRunName", - "head_sha", - ); - - expect(actual).toEqual([]); - }); - - it("throws when multiple checks match", async () => { - const githubMock = createMockGithub(); - const earlierDate = "2025-04-01T00:00:00Z"; - const laterDate = "2025-04-02T00:00:00Z"; - githubMock.rest.checks.listForRef = vi.fn().mockResolvedValue({ - data: { - check_runs: [ - { - name: "checkRunName", - status: "completed", - conclusion: "success", - completed_at: earlierDate, - }, - { - name: "checkRunName", - status: "completed", - conclusion: "success", - completed_at: laterDate, - }, - ], - }, - }); - - const actual = await await getCheckRuns( - githubMock, - createMockContext(), - "checkRunName", - "head_sha", - ); - - expect(actual).toEqual([ - expect.objectContaining({ - name: "checkRunName", - status: "completed", - conclusion: "success", - completed_at: laterDate, - }), - expect.objectContaining({ - name: "checkRunName", - status: "completed", - conclusion: "success", - completed_at: earlierDate, - }), - ]); - }); +vi.mock("../src/github.js", () => { + return { + getCheckRuns: vi.fn().mockResolvedValue([]), + getWorkflowRuns: vi.fn().mockResolvedValue([]), + getCommitStatuses: vi.fn().mockResolvedValue([]), + }; }); -describe("getWorkflowRuns", () => { - it("returns matching workflow_run", async () => { - const githubMock = createMockGithub(); - githubMock.rest.actions.listWorkflowRunsForRepo = vi - .fn() - .mockResolvedValue({ - data: { - workflow_runs: [ - { - name: "workflowName", - status: "completed", - conclusion: "success", - }, - ], - }, - }); - - const actual = await getWorkflowRuns( - githubMock, - createMockContext(), - "workflowName", - "head_sha", - ); - - expect(actual).toEqual([ - expect.objectContaining({ - name: "workflowName", - status: "completed", - conclusion: "success", - }), - ]); - }); - - it("returns null when no workflow matches", async () => { - const githubMock = createMockGithub(); - githubMock.rest.actions.listWorkflowRunsForRepo = vi - .fn() - .mockResolvedValue({ - data: { - workflow_runs: [ - { - name: "otherWorkflowName", - }, - ], - }, - }); - - const actual = await getWorkflowRuns( - githubMock, - createMockContext(), - "workflowName", - "head_sha", - ); - - expect(actual).toEqual([]); - }); - - it("returns latest when multiple workflows match", async () => { - const githubMock = createMockGithub(); - const earlyDate = "2025-04-01T00:00:00Z"; - const laterDate = "2025-04-02T00:00:00Z"; - githubMock.rest.actions.listWorkflowRunsForRepo = vi - .fn() - .mockResolvedValue({ - data: { - workflow_runs: [ - { - name: "workflowName", - status: "completed", - conclusion: "success", - updated_at: earlyDate, - }, - { - name: "workflowName", - status: "completed", - conclusion: "success", - updated_at: laterDate, - }, - ], - }, - }); - - const actual = await getWorkflowRuns( - githubMock, - createMockContext(), - "workflowName", - "head_sha", - ); - - expect(actual).toEqual([ - expect.objectContaining({ - updated_at: laterDate, - }), - expect.objectContaining({ - updated_at: earlyDate, - }), - ]); +describe("verifyRunStatusImpl", () => { + // Reset mock call history before each test + beforeEach(() => { + vi.clearAllMocks(); }); -}); -describe("verifyRunStatusImpl", () => { it("verifies status when check_run event fires", async () => { const github = createMockGithub(); - github.rest.actions.listWorkflowRunsForRepo = vi.fn().mockResolvedValue({ - data: { - workflow_runs: [ - { - name: "workflowName", - status: "completed", - conclusion: "success", - }, - ], + const { getWorkflowRuns } = await import("../src/github.js"); + getWorkflowRuns.mockResolvedValue([ + { + name: "workflowName", + status: "completed", + conclusion: "success", }, - }); - + ]); const context = { eventName: "check_run", payload: { @@ -329,12 +133,8 @@ describe("verifyRunStatusImpl", () => { }); it("returns early during check_run event when no matching workflow_run is found", async () => { - const github = createMockGithub(); - github.rest.actions.listWorkflowRunsForRepo = vi.fn().mockResolvedValue({ - data: { - workflow_runs: [], - }, - }); + const { getWorkflowRuns } = await import("../src/github.js"); + getWorkflowRuns.mockResolvedValue([]); const context = { eventName: "check_run", @@ -347,7 +147,7 @@ describe("verifyRunStatusImpl", () => { }; const core = createMockCore(); await verifyRunStatusImpl({ - github, + github: createMockGithub(), context, core, checkRunName: "checkRunName", @@ -384,18 +184,14 @@ describe("verifyRunStatusImpl", () => { }); it("throws if check_run conclusion does not match workflow_run conclusion", async () => { - const github = createMockGithub(); - github.rest.actions.listWorkflowRunsForRepo = vi.fn().mockResolvedValue({ - data: { - workflow_runs: [ - { - name: "workflowName", - status: "completed", - conclusion: "failure", - }, - ], + const { getWorkflowRuns } = await import("../src/github.js"); + getWorkflowRuns.mockResolvedValue([ + { + name: "workflowName", + status: "completed", + conclusion: "failure", }, - }); + ]); const context = { eventName: "check_run", @@ -408,7 +204,7 @@ describe("verifyRunStatusImpl", () => { }; const core = createMockCore(); await verifyRunStatusImpl({ - github, + github: createMockGithub(), context, core, checkRunName: "checkRunName", @@ -449,4 +245,119 @@ describe("verifyRunStatusImpl", () => { "Could not locate check run checkRunName in check suite checkRunName. Ensure job is filtering by github.event.check_suite.app.name.", ); }); + + it("fetches commit status from API when not status event", async () => { + const { getCheckRuns, getCommitStatuses } = await import("../src/github.js"); + getCheckRuns.mockResolvedValue([ + { + name: "checkRunName", + conclusion: "success", + html_url: "https://example.com/check", + }, + ]); + getCommitStatuses.mockResolvedValue([ + { + context: "commitStatusName", + state: "success", + target_url: "https://example.com/status", + }, + ]); + + const context = { + eventName: "workflow_run", + payload: { + workflow_run: { + name: "workflowName", + conclusion: "success", + }, + }, + }; + + const core = createMockCore(); + + await verifyRunStatusImpl({ + github: createMockGithub(), + context, + core, + checkRunName: "checkRunName", + commitStatusName: "commitStatusName", + workflowName: "workflowName", + }); + + expect(core.setFailed).not.toHaveBeenCalled(); + expect(core.notice).toHaveBeenCalledWith( + "Conclusions match for check run checkRunName and commit status commitStatusName", + ); + }); + + it("handles API error when fetching commit status", async () => { + const { getCheckRuns, getCommitStatuses } = await import("../src/github.js"); + getCheckRuns.mockResolvedValue([ + { + name: "checkRunName", + conclusion: "success", + html_url: "https://example.com/check", + }, + ]); + getCommitStatuses.mockRejectedValue(new Error("API Error")); + + const context = { + eventName: "workflow_run", + payload: { + workflow_run: { + name: "workflowName", + conclusion: "success", + }, + }, + }; + + const core = createMockCore(); + + await verifyRunStatusImpl({ + github: createMockGithub(), + context, + core, + checkRunName: "checkRunName", + commitStatusName: "commitStatusName", + workflowName: "workflowName", + }); + + expect(core.setFailed).toHaveBeenCalledWith("Failed to fetch commit status: API Error"); + }); + + it("verifies neutral check run matches success workflow run", async () => { + const { getCheckRuns } = await import("../src/github.js"); + getCheckRuns.mockResolvedValue([ + { + name: "checkRunName", + conclusion: "neutral", + html_url: "https://example.com/check", + }, + ]); + + const context = { + eventName: "workflow_run", + payload: { + workflow_run: { + name: "workflowName", + conclusion: "success", + }, + }, + }; + + const core = createMockCore(); + + await verifyRunStatusImpl({ + github: createMockGithub(), + context, + core, + checkRunName: "checkRunName", + workflowName: "workflowName", + }); + + expect(core.setFailed).not.toHaveBeenCalled(); + expect(core.notice).toHaveBeenCalledWith( + "Conclusions match for check run checkRunName and workflow run workflowName", + ); + }); }); diff --git a/.github/workflows/typespec-migration-validation.yaml b/.github/workflows/typespec-migration-validation.yaml index 6f273a1ef5c8..2536989fe082 100644 --- a/.github/workflows/typespec-migration-validation.yaml +++ b/.github/workflows/typespec-migration-validation.yaml @@ -21,7 +21,7 @@ jobs: - name: Setup Node and install deps uses: ./.github/actions/setup-node-install-deps - - name: Run TypeSpec Migration Validation + - name: Run TypeSpec Migration Validation run: | ./eng/tools/typespec-migration-validation/scripts/download-main.ps1 -Verbose -callValidation $true shell: pwsh diff --git a/.github/workflows/typespec-validation.yaml b/.github/workflows/typespec-validation.yaml index 813029a8e8ee..7a38bb20870d 100644 --- a/.github/workflows/typespec-validation.yaml +++ b/.github/workflows/typespec-validation.yaml @@ -24,5 +24,8 @@ jobs: # step as failed. $ErrorActionPreference = 'Continue' - ./eng/scripts/TypeSpec-Validation.ps1 -GitClean -Verbose + # Only "main" and "RPSaaSMaster" should validate all specs if core files change + $ignoreCoreFiles = -not (@('main', 'RPSaaSMaster') -contains $Env:GITHUB_BASE_REF) + + ./eng/scripts/TypeSpec-Validation.ps1 -GitClean -Verbose -IgnoreCoreFiles:$ignoreCoreFiles shell: pwsh diff --git a/.github/workflows/update-labels.yaml b/.github/workflows/update-labels.yaml index 81d386f39c44..54a3c0576a18 100644 --- a/.github/workflows/update-labels.yaml +++ b/.github/workflows/update-labels.yaml @@ -7,33 +7,8 @@ on: # If an upstream workflow if completed, get only the artifacts from that workflow, and update labels workflow_run: workflows: - [ - "ARM Auto SignOff", - "SDK Breaking Change Labels", - "SDK Suppressions", - "TypeSpec Requirement", - ] + ["ARM Auto SignOff", "SDK Breaking Change Labels", "SDK Suppressions", "TypeSpec Requirement"] types: [completed] - workflow_dispatch: - inputs: - owner: - description: The account owner of the repository. The name is not case sensitive. - required: true - type: string - repo: - description: The name of the repository without the .git extension. The name is not case sensitive. - required: true - type: string - # simulate pull_request trigger - issue_number: - description: The number that identifies the issue. - required: false - type: number - # simulate workflow_run trigger - run_id: - description: The unique identifier of the workflow run. - required: false - type: number permissions: actions: read @@ -57,11 +32,6 @@ jobs: - name: Update Labels uses: actions/github-script@v7 - env: - OWNER: ${{ inputs.owner }} - REPO: ${{ inputs.repo }} - ISSUE_NUMBER: ${{ inputs.issue_number }} - RUN_ID: ${{ inputs.run_id }} with: script: | const { default: updateLabels } = diff --git a/.github/workflows/watch-breakingchange-crossversion.yaml b/.github/workflows/watch-breakingchange-crossversion.yaml new file mode 100644 index 000000000000..41fa9752c676 --- /dev/null +++ b/.github/workflows/watch-breakingchange-crossversion.yaml @@ -0,0 +1,27 @@ +# Use ~ to sort the workflow to the bottom of the list +name: "~Watch - Breaking Change(Cross-Version)" + +on: + # check_suite is preferred over check_run to avoid triggering on all check + # runs. In some cases, check_run must be used in testing environments. + check_suite: + types: completed + + workflow_run: + types: completed + workflows: + - "\\[TEST-IGNORE\\] Breaking Change(Cross-Version) - Set Status" + +permissions: + actions: read + checks: read + contents: read + statuses: read + +jobs: + BreakingChangeCrossVersion: + name: Watch Breaking Change(Cross-Version) + uses: ./.github/workflows/_reusable-verify-run-status.yaml + with: + check_run_name: "Breaking Change(Cross-Version)" + commit_status_name: "[TEST-IGNORE] Breaking Change(Cross-Version)" diff --git a/.github/workflows/watch-avocado.yaml b/.github/workflows/watch-breakingchange.yaml similarity index 59% rename from .github/workflows/watch-avocado.yaml rename to .github/workflows/watch-breakingchange.yaml index 63db5aad195c..50978f8a475b 100644 --- a/.github/workflows/watch-avocado.yaml +++ b/.github/workflows/watch-breakingchange.yaml @@ -1,5 +1,5 @@ # Use ~ to sort the workflow to the bottom of the list -name: "~Watch - Avocado" +name: "~Watch - BreakingChange" on: # check_suite is preferred over check_run to avoid triggering on all check @@ -10,16 +10,18 @@ on: workflow_run: types: completed workflows: - - "\\[TEST-IGNORE\\] Swagger Avocado - Analyze Code" + - "\\[TEST-IGNORE\\] Swagger BreakingChange - Set Status" permissions: + actions: read checks: read contents: read + statuses: read jobs: - Avocado: - name: Watch Avocado + BreakingChange: + name: Watch BreakingChange uses: ./.github/workflows/_reusable-verify-run-status.yaml with: - check_run_name: "Swagger Avocado" - workflow_name: "[TEST-IGNORE] Swagger Avocado - Analyze Code" + check_run_name: "Swagger BreakingChange" + commit_status_name: "[TEST-IGNORE] Swagger BreakingChange" diff --git a/.github/workflows/watch-lintdiff.yaml b/.github/workflows/watch-modelvalidation.yaml similarity index 57% rename from .github/workflows/watch-lintdiff.yaml rename to .github/workflows/watch-modelvalidation.yaml index 7eb843df3f4d..ba3ff8165018 100644 --- a/.github/workflows/watch-lintdiff.yaml +++ b/.github/workflows/watch-modelvalidation.yaml @@ -1,5 +1,5 @@ # Use ~ to sort the workflow to the bottom of the list -name: "~Watch - LintDiff" +name: "~Watch - Swagger ModelValidation" on: # check_suite is preferred over check_run to avoid triggering on all check @@ -9,17 +9,19 @@ on: workflow_run: types: completed - workflows: - - "\\[TEST-IGNORE\\] Swagger LintDiff - Analyze Code" + workflows: + - "\\[TEST-IGNORE\\] Swagger ModelValidation" permissions: + actions: read checks: read contents: read + statuses: read jobs: - LintDiff: - name: Watch LintDiff + ModelValidationWatch: + name: Watch ModelValidation uses: ./.github/workflows/_reusable-verify-run-status.yaml with: - check_run_name: "Swagger LintDiff" - workflow_name: "[TEST-IGNORE] Swagger LintDiff - Analyze Code" + check_run_name: "Swagger ModelValidation" + workflow_name: "[TEST-IGNORE] Swagger ModelValidation" diff --git a/.github/workflows/watch-semanticvalidation.yaml b/.github/workflows/watch-semanticvalidation.yaml new file mode 100644 index 000000000000..757e55e367d6 --- /dev/null +++ b/.github/workflows/watch-semanticvalidation.yaml @@ -0,0 +1,27 @@ +# Use ~ to sort the workflow to the bottom of the list +name: "~Watch - Swagger SemanticValidation" + +on: + # check_suite is preferred over check_run to avoid triggering on all check + # runs. In some cases, check_run must be used in testing environments. + check_suite: + types: completed + + workflow_run: + types: completed + workflows: + - "\\[TEST-IGNORE\\] Swagger SemanticValidation" + +permissions: + actions: read + checks: read + contents: read + statuses: read + +jobs: + SemanticValidationWatch: + name: Watch SemanticValidation + uses: ./.github/workflows/_reusable-verify-run-status.yaml + with: + check_run_name: "Swagger SemanticValidation" + workflow_name: "[TEST-IGNORE] Swagger SemanticValidation" diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 47635eb64764..a2562e2ab494 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -3,7 +3,7 @@ "azure-sdk-mcp": { "type": "stdio", "command": "pwsh", - "args": ["${workspaceFolder}/eng/common/mcp/azure-sdk-mcp.ps1", "-Run", "-Version", "1.0.0-dev.20250527.1"] + "args": ["${workspaceFolder}/eng/common/mcp/azure-sdk-mcp.ps1", "-Run"] }, } } \ No newline at end of file diff --git a/documentation/samplefiles/readme.java.md b/documentation/samplefiles/readme.java.md index 1abdd25dc681..70c57e50aa07 100644 --- a/documentation/samplefiles/readme.java.md +++ b/documentation/samplefiles/readme.java.md @@ -3,6 +3,8 @@ These settings apply only when `--java` is specified on the command line. ``` yaml $(java) +service-name: [[ServiceName]] # human-readable service name, whitespace allowed client-flattened-annotation-target: disabled uuid-as-string: true +output-model-immutable: true ``` diff --git a/documentation/typespec-rest-api-dev-process.md b/documentation/typespec-rest-api-dev-process.md index afd7d2ed8141..b20b12a28530 100644 --- a/documentation/typespec-rest-api-dev-process.md +++ b/documentation/typespec-rest-api-dev-process.md @@ -95,7 +95,7 @@ Please first review recommended folder structure detailed in [this document](htt npx tsp compile . ``` - The generated swagger files should be correctly placed in `data-plane` or `resource-manager` folders follwoing the + The generated swagger files should be correctly placed in `data-plane` or `resource-manager` folders following the naming conventions. 6. Now the project has been set up. You can modify the sample and develop your own APIs with TypeSpec. diff --git a/eng/common/TestResources/New-TestResources.ps1 b/eng/common/TestResources/New-TestResources.ps1 index 500bd89d0dfc..078991250e64 100755 --- a/eng/common/TestResources/New-TestResources.ps1 +++ b/eng/common/TestResources/New-TestResources.ps1 @@ -163,7 +163,7 @@ try { $root = $repositoryRoot = "$PSScriptRoot/../../.." | Resolve-Path if($ServiceDirectory) { - $root = "$repositoryRoot/sdk/$ServiceDirectory" | Resolve-Path + $root = [System.IO.Path]::Combine($repositoryRoot, "sdk", $ServiceDirectory) | Resolve-Path } if ($TestResourcesDirectory) { diff --git a/eng/common/TestResources/deploy-test-resources.yml b/eng/common/TestResources/deploy-test-resources.yml index e64404e7c5b4..30efe36e231c 100644 --- a/eng/common/TestResources/deploy-test-resources.yml +++ b/eng/common/TestResources/deploy-test-resources.yml @@ -1,5 +1,6 @@ parameters: ServiceDirectory: '' + TestResourcesDirectory: '' ArmTemplateParameters: '@{}' DeleteAfterHours: 8 Location: '' @@ -98,6 +99,7 @@ steps: eng/common/TestResources/New-TestResources.ps1 ` -ResourceType '${{ parameters.ResourceType }}' ` -ServiceDirectory '${{ parameters.ServiceDirectory }}' ` + -TestResourcesDirectory '${{ parameters.TestResourcesDirectory }}' ` -Location '${{ parameters.Location }}' ` -DeleteAfterHours '${{ parameters.DeleteAfterHours }}' ` @subscriptionConfiguration ` @@ -142,6 +144,7 @@ steps: eng/common/TestResources/New-TestResources.ps1 ` -ResourceType '${{ parameters.ResourceType }}' ` -ServiceDirectory '${{ parameters.ServiceDirectory }}' ` + -TestResourcesDirectory '${{ parameters.TestResourcesDirectory }}' ` -Location '${{ parameters.Location }}' ` -DeleteAfterHours '${{ parameters.DeleteAfterHours }}' ` @subscriptionConfiguration ` diff --git a/eng/common/mcp/azure-sdk-mcp.ps1 b/eng/common/mcp/azure-sdk-mcp.ps1 index d78c61a751f2..b56fb4e96a28 100755 --- a/eng/common/mcp/azure-sdk-mcp.ps1 +++ b/eng/common/mcp/azure-sdk-mcp.ps1 @@ -4,7 +4,7 @@ param( [string]$FileName = 'Azure.Sdk.Tools.Cli', [string]$Package = 'azsdk', [string]$Version, # Default to latest - [string]$InstallDirectory = (Join-Path $HOME ".azure-sdk-mcp" "azsdk"), + [string]$InstallDirectory = '', [string]$Repository = 'Azure/azure-sdk-tools', [string]$RunDirectory = (Resolve-Path (Join-Path $PSScriptRoot .. .. ..)), [switch]$Run, @@ -14,6 +14,11 @@ param( $ErrorActionPreference = "Stop" +if (-not $InstallDirectory) +{ + $homeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE } + $InstallDirectory = (Join-Path $homeDir ".azure-sdk-mcp" "azsdk") +} . (Join-Path $PSScriptRoot '..' 'scripts' 'Helpers' 'AzSdkTool-Helpers.ps1') if ($Clean) { @@ -21,9 +26,9 @@ if ($Clean) { } if ($UpdateVsCodeConfig) { - $vscodeConfigPath = $PSScriptRoot + "../../../.vscode/mcp.json" + $vscodeConfigPath = Join-Path $PSScriptRoot ".." ".." ".." ".vscode" "mcp.json" if (Test-Path $vscodeConfigPath) { - $vscodeConfig = Get-Content -Raw $vscodeConfig | ConvertFrom-Json -AsHashtable + $vscodeConfig = Get-Content -Raw $vscodeConfigPath | ConvertFrom-Json -AsHashtable } else { $vscodeConfig = @{} @@ -31,7 +36,7 @@ if ($UpdateVsCodeConfig) { $serverKey = "azure-sdk-mcp" $serverConfig = @{ "type" = "stdio" - "command" = "/home/ben/azs/azure-sdk-tools/eng/common/mcp/azure-sdk-mcp.ps1" + "command" = "$PSCommandPath" } $orderedServers = [ordered]@{ $serverKey = $serverConfig @@ -46,7 +51,7 @@ if ($UpdateVsCodeConfig) { } $vscodeConfig.servers = $orderedServers Write-Host "Updating vscode mcp config at $vscodeConfigPath" - $vscodeConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $vscodeConfig -Force + $vscodeConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $vscodeConfigPath -Force } $exe = Install-Standalone-Tool ` diff --git a/eng/common/pipelines/codeowners-linter.yml b/eng/common/pipelines/codeowners-linter.yml index 821b0ea8b5a7..a77bcf8ff0bf 100644 --- a/eng/common/pipelines/codeowners-linter.yml +++ b/eng/common/pipelines/codeowners-linter.yml @@ -19,6 +19,7 @@ pr: stages: - stage: Run variables: + Codeql.SkipTaskAutoInjection: true skipComponentGovernanceDetection: true nugetMultiFeedWarnLevel: 'none' baseBranchBaseline: '' @@ -27,8 +28,8 @@ stages: - job: Run timeoutInMinutes: 120 pool: - name: azsdk-pool-mms-ubuntu-2204-general - vmImage: ubuntu-22.04 + name: azsdk-pool + demands: ImageOverride -equals ubuntu-24.04 variables: CodeownersLinterVersion: '1.0.0-dev.20240926.2' @@ -38,13 +39,10 @@ stages: UserOrgUri: "https://azuresdkartifacts.blob.core.windows.net/azure-sdk-write-teams/user-org-visibility-blob" steps: - - task: DotNetCoreCLI@2 - displayName: 'Install CodeownersLinter' - inputs: - command: custom - custom: 'tool' - arguments: 'install --global --add-source "$(DotNetDevOpsFeed)" --version "$(CodeownersLinterVersion)" "Azure.Sdk.Tools.CodeownersLinter"' - workingDirectory: '$(Build.SourcesDirectory)/eng/common' + - pwsh: | + dotnet tool install --global --add-source "$(DotNetDevOpsFeed)" --version "$(CodeownersLinterVersion)" "Azure.Sdk.Tools.CodeownersLinter" + displayName: Install CodeownersLinter + workingDirectory: '$(Agent.WorkFolder)' # Some directory outside of the source clone to avoid hitting global.json files when any version of dotnet will work for this install - ${{ if eq(variables['Build.Reason'], 'PullRequest') }}: - pwsh: | diff --git a/eng/common/pipelines/templates/archetype-typespec-emitter.yml b/eng/common/pipelines/templates/archetype-typespec-emitter.yml index 772726f31156..a92ad9d9935e 100644 --- a/eng/common/pipelines/templates/archetype-typespec-emitter.yml +++ b/eng/common/pipelines/templates/archetype-typespec-emitter.yml @@ -164,63 +164,75 @@ extends: - Build - ${{ if and(parameters.PublishDependsOnTest, ne(length(parameters.TestMatrix), 0)) }}: - Test + variables: buildArtifactsPath: $(Pipeline.Workspace)/build_artifacts - pool: ${{ parameters.Pool }} - jobs: - - job: Publish - steps: - - template: /eng/common/pipelines/templates/steps/sparse-checkout.yml - - download: current - artifact: build_artifacts - displayName: Download build artifacts - - # Create authenticated .npmrc file for publishing to devops - - template: /eng/common/pipelines/templates/steps/create-authenticated-npmrc.yml - parameters: - npmrcPath: $(buildArtifactsPath)/packages/.npmrc - registryUrl: https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js-test-autorest/npm/registry/ - - # publish to devops feed - - pwsh: | - $packageFiles = Get-ChildItem -Path . -Filter '*.tgz' - foreach ($file in $packageFiles.Name) { - Write-Host "npm publish $file --verbose --access public" - npm publish $file --verbose --access public - } - displayName: Publish to DevOps feed - workingDirectory: $(buildArtifactsPath)/packages - - # Publish to https://dev.azure.com/azure-sdk/public/_packaging?_a=feed&feed=azure-sdk-for-net - - ${{ if parameters.HasNugetPackages }}: - - task: 1ES.PublishNuget@1 - displayName: Publish NuGet Packages to DevOps feed - inputs: - packagesToPush: $(buildArtifactsPath)/packages/*.nupkg;!$(buildArtifactsPath)/packages/*.symbols.nupkg - packageParentPath: $(buildArtifactsPath)/packages - publishVstsFeed: "29ec6040-b234-4e31-b139-33dc4287b756/fa8c16a3-dbe0-4de2-a297-03065ec1ba3f" - nuGetFeedType: internal - - - ${{ if parameters.PublishPublic }}: - # publish to npmjs.org using ESRP - - task: EsrpRelease@9 - inputs: - displayName: Publish to npmjs.org - ConnectedServiceName: Azure SDK PME Managed Identity - ClientId: 5f81938c-2544-4f1f-9251-dd9de5b8a81b - DomainTenantId: 975f013f-7f24-47e8-a7d3-abc4752bf346 - UseManagedIdentity: true - KeyVaultName: kv-azuresdk-codesign - SignCertName: azure-sdk-esrp-release-certificate - Intent: PackageDistribution - ContentType: npm - FolderLocation: $(buildArtifactsPath)/packages - Owners: ${{ coalesce(variables['Build.RequestedForEmail'], 'azuresdk@microsoft.com') }} - Approvers: ${{ coalesce(variables['Build.RequestedForEmail'], 'azuresdk@microsoft.com') }} - ServiceEndpointUrl: https://api.esrp.microsoft.com - MainPublisher: ESRPRELPACMANTEST + jobs: + - deployment: Publish + environment: none + + pool: + name: azsdk-pool + image: windows-2022 # Nuget publish requires .NET framework on windows to handle the auth + os: windows + + templateContext: + type: releaseJob + isProduction: true + inputs: # All input build artifacts must be declared here + - input: pipelineArtifact # Required, type of the input artifact + artifactName: build_artifacts + targetPath: $(buildArtifactsPath) + strategy: + runOnce: + deploy: + steps: + # Create authenticated .npmrc file for publishing to devops + - template: /eng/common/pipelines/templates/steps/create-authenticated-npmrc.yml + parameters: + npmrcPath: $(buildArtifactsPath)/packages/.npmrc + registryUrl: https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js-test-autorest/npm/registry/ + + # publish to devops feed + - pwsh: | + $packageFiles = Get-ChildItem -Path . -Filter '*.tgz' + foreach ($file in $packageFiles.Name) { + Write-Host "npm publish $file --verbose --access public" + npm publish $file --verbose --access public + } + displayName: Publish to DevOps feed + workingDirectory: $(buildArtifactsPath)/packages + + # Publish to https://dev.azure.com/azure-sdk/public/_packaging?_a=feed&feed=azure-sdk-for-net + - ${{ if parameters.HasNugetPackages }}: + - task: 1ES.PublishNuget@1 + displayName: Publish NuGet Packages to DevOps feed + inputs: + packagesToPush: $(buildArtifactsPath)/packages/*.nupkg;!$(buildArtifactsPath)/packages/*.symbols.nupkg + packageParentPath: $(buildArtifactsPath)/packages + publishVstsFeed: "public/azure-sdk-for-net" + + - ${{ if parameters.PublishPublic }}: + # publish to npmjs.org using ESRP + - task: EsrpRelease@9 + inputs: + displayName: Publish to npmjs.org + ConnectedServiceName: Azure SDK PME Managed Identity + ClientId: 5f81938c-2544-4f1f-9251-dd9de5b8a81b + DomainTenantId: 975f013f-7f24-47e8-a7d3-abc4752bf346 + UseManagedIdentity: true + KeyVaultName: kv-azuresdk-codesign + SignCertName: azure-sdk-esrp-release-certificate + Intent: PackageDistribution + ContentType: npm + FolderLocation: $(buildArtifactsPath)/packages + Owners: ${{ coalesce(variables['Build.RequestedForEmail'], 'azuresdk@microsoft.com') }} + Approvers: ${{ coalesce(variables['Build.RequestedForEmail'], 'azuresdk@microsoft.com') }} + ServiceEndpointUrl: https://api.esrp.microsoft.com + MainPublisher: ESRPRELPACMANTEST + # Regenerate stage # Responsible for regenerating the SDK code using the emitter package and the generation matrix. - ${{ if and(parameters.ShouldPublish, parameters.ShouldRegenerate) }}: @@ -506,4 +518,4 @@ extends: scriptType: "bash" scriptLocation: "inlineScript" inlineScript: npx tsp-spector upload-coverage --coverageFile $(Build.ArtifactStagingDirectory)/tsp-spector-coverage-azure.json --generatorName @azure-typespec/$(SpectorName) --storageAccountName typespec --containerName coverages --generatorVersion $(node -p -e "require('./package.json').version") --generatorMode azure - workingDirectory: $(Build.SourcesDirectory)/eng/packages/$(SpectorName) \ No newline at end of file + workingDirectory: $(Build.SourcesDirectory)/eng/packages/$(SpectorName) diff --git a/eng/common/pipelines/templates/jobs/archetype-sdk-tests-generate.yml b/eng/common/pipelines/templates/jobs/archetype-sdk-tests-generate.yml index afc5de87ce1f..00aaec7c6a6e 100644 --- a/eng/common/pipelines/templates/jobs/archetype-sdk-tests-generate.yml +++ b/eng/common/pipelines/templates/jobs/archetype-sdk-tests-generate.yml @@ -27,10 +27,10 @@ parameters: default: [] - name: Pool type: string - default: azsdk-pool-mms-ubuntu-2204-general + default: azsdk-pool - name: OsVmImage type: string - default: ubuntu-22.04 + default: ubuntu-24.04 # This parameter is only necessary if there are multiple invocations of this template within the SAME STAGE. # When that occurs, provide a name other than the default value. - name: GenerateJobName diff --git a/eng/common/pipelines/templates/jobs/docindex.yml b/eng/common/pipelines/templates/jobs/docindex.yml index 45c19dc21000..077d07ad16de 100644 --- a/eng/common/pipelines/templates/jobs/docindex.yml +++ b/eng/common/pipelines/templates/jobs/docindex.yml @@ -1,7 +1,8 @@ jobs: - job: CreateDocIndex pool: - name: azsdk-pool-mms-win-2022-general + name: azsdk-pool + demands: ImageOverride -equals windows-2022 steps: - task: UsePythonVersion@0 displayName: 'Use Python 3.11' @@ -36,14 +37,14 @@ jobs: Copy-Item -Path $(Build.SourcesDirectory)/eng/* -Destination ./ -Recurse -Force echo "##vso[task.setvariable variable=toolPath]$(Build.BinariesDirectory)" workingDirectory: $(Build.BinariesDirectory) - displayName: Move eng/common to Tool Directory + displayName: Move eng/common to Tool Directory - task: PublishPipelineArtifact@0 condition: succeeded() inputs: artifactName: "Doc.Index" targetPath: $(Build.ArtifactStagingDirectory)/docfx_project/_site - + - pwsh: | git checkout -b gh-pages-local --track origin/gh-pages-root -f workingDirectory: $(Build.SourcesDirectory) diff --git a/eng/common/pipelines/templates/jobs/npm-publish.yml b/eng/common/pipelines/templates/jobs/npm-publish.yml index 4821ea9b3b4a..9909ace96e9f 100644 --- a/eng/common/pipelines/templates/jobs/npm-publish.yml +++ b/eng/common/pipelines/templates/jobs/npm-publish.yml @@ -1,18 +1,27 @@ parameters: - Tag: 'latest' + Tag: 'auto' ArtifactName: 'packages' + ArtifactSubPath: '' + DeploymentName: 'PublishPackage' DependsOn: [] Environment: 'package-publish' Registry: 'https://registry.npmjs.org/' + Pool: # hardcoding the pool and image name because deployment jobs do not support variable expansion in pool names + name: azsdk-pool + image: ubuntu-24.04 + os: linux + CustomCondition: succeeded() + FailOnMissingPackages: true jobs: -- deployment: PublishPackage_${{ parameters.ArtifactName }} - displayName: 'Publish ${{ parameters.ArtifactName }} to ${{ parameters.Registry }}' +- deployment: ${{ parameters.DeploymentName }} + displayName: 'Publish ${{ parameters.ArtifactName }} to ${{ parameters.Registry }}' + condition: ${{ parameters.CustomCondition }} environment: ${{ parameters.Environment }} dependsOn: ${{ parameters.DependsOn }} variables: - name: ArtifactPath - value: $(Pipeline.Workspace)/${{ parameters.ArtifactName }} + value: $(Pipeline.Workspace)/${{ parameters.ArtifactName }}/${{ parameters.ArtifactSubPath }} templateContext: type: releaseJob @@ -21,26 +30,48 @@ jobs: - input: pipelineArtifact artifactName: ${{ parameters.ArtifactName }} itemPattern: '**/*.tgz' - targetPath: $(ArtifactPath) + targetPath: $(Pipeline.Workspace)/${{ parameters.ArtifactName }}/ - pool: - name: azsdk-pool - image: ubuntu-24.04 - os: linux + pool: ${{ parameters.Pool }} strategy: runOnce: deploy: steps: - pwsh: | + $containsBeta = $false + $containsPackages = $false foreach ($package in (dir $(ArtifactPath) *.tgz -Recurse)) { - Write-Host "Publishing $package to ${{ parameters.Registry }} with tag ${{ parameters.Tag }}" + if ($package.Name -match "[\d\.]+-[a-zA-Z]+") { $containsBeta = $true } + Write-Host "Publishing $package to ${{ parameters.Registry }}" + $containsPackages = $true + } + if (!$containsPackages) { + Write-Host "##vso[task.setvariable variable=SkipPublishing]true" + if ("${{ parameters.FailOnMissingPackages }}" -eq 'true') { + Write-Error "No packages found to publish, but FailOnMissingPackages is set to true. Failing the job." + exit 1 + } + else { + Write-Host "No packages found to publish in $(ArtifactPath), so skipping publishing." + exit 0 + } } - displayName: 'Display packages to be published' + + $tag = '${{ parameters.Tag }}' + if ($tag -eq '' -or $tag -eq 'auto') { + $tag = 'latest' + # If the package is prerelease publish it under 'beta' tag + if ($containsBeta) { $tag = 'beta'} + } + Write-Host "##vso[task.setvariable variable=TagName]$tag" + Write-Host "Publishing packages with tag: $tag" + displayName: 'Packages to be published' - ${{ if eq(parameters.Registry, 'https://registry.npmjs.org/') }}: - task: EsrpRelease@9 displayName: 'Publish ${{ parameters.ArtifactName }} via ESRP' + condition: and(succeeded(), ne(variables['SkipPublishing'], 'true')) inputs: ConnectedServiceName: 'Azure SDK PME Managed Identity' ClientId: '5f81938c-2544-4f1f-9251-dd9de5b8a81b' @@ -55,23 +86,47 @@ jobs: Approvers: ${{ coalesce(variables['Build.RequestedForEmail'], 'azuresdk@microsoft.com') }} ServiceEndpointUrl: 'https://api.esrp.microsoft.com' MainPublisher: 'ESRPRELPACMANTEST' - productstate: ${{ parameters.Tag }} + productstate: $(TagName) + + - pwsh: | + foreach ($package in (dir $(ArtifactPath) *.tgz -Recurse)) { + $packageJson = tar -xOf $package "package/package.json" | ConvertFrom-Json + if (!$packageJson) { + Write-Warning "Could not read package.json from $package" + continue; + } + Write-Host "Verifying tag '$(TagName)' is set for '$($packageJson.name)' version '$($packageJson.version)'" + $packageTags = npm view $packageJson.name "dist-tags" -json -silent | ConvertFrom-Json + if ($LASTEXITCODE -ne 0 -or !$packageTags) { + Write-Warning "Failed to retrieve dist-tags for $packageJson.name. It is possible the package hasn't been indexed yet so ignoring." + continue + } + + if ($packageTags."$(TagName)" -ne $packageJson.version) { + Write-Error "The dist-tag '$(TagName)' for package '$($packageJson.name)' is not correctly set something must have gone wrong during the ESRP release process." + exit 1 + } + } + displayName: 'Verify tag after ESRP release' + condition: and(succeeded(), ne(variables['SkipPublishing'], 'true')) - ${{ else }}: - template: /eng/common/pipelines/templates/steps/create-authenticated-npmrc.yml parameters: npmrcPath: $(ArtifactPath)/.npmrc registryUrl: ${{ parameters.Registry }} + CustomCondition: and(succeeded(), ne(variables['SkipPublishing'], 'true')) - pwsh: | foreach ($package in (dir $(ArtifactPath) *.tgz -Recurse)) { - Write-Host "npm publish $package --verbose --access public --tag ${{ parameters.Tag }} --registry ${{ parameters.Registry }}" - npm publish $package --verbose --access public --tag ${{ parameters.Tag }} --registry ${{ parameters.Registry }} + Write-Host "npm publish $package --verbose --access public --tag $(TagName) --registry ${{ parameters.Registry }}" + npm publish $package --verbose --access public --tag $(TagName) --registry ${{ parameters.Registry }} if ($LASTEXITCODE -ne 0) { Write-Error "Failed to publish $package to ${{ parameters.Registry }}" exit $LASTEXITCODE } } displayName: 'Publish ${{ parameters.ArtifactName }}' + condition: and(succeeded(), ne(variables['SkipPublishing'], 'true')) workingDirectory: $(ArtifactPath) diff --git a/eng/common/pipelines/templates/jobs/perf.yml b/eng/common/pipelines/templates/jobs/perf.yml index bd53833282c2..67c4accf3800 100644 --- a/eng/common/pipelines/templates/jobs/perf.yml +++ b/eng/common/pipelines/templates/jobs/perf.yml @@ -112,7 +112,30 @@ jobs: - template: /eng/common/pipelines/templates/steps/verify-agent-os.yml parameters: AgentImage: $(OSVmImage) - + + # Copied from eng/pipelines/templates/steps/install-dotnet.yml, but changed workingDirectory + - task: UseDotNet@2 # About UseDotNet@2 task: https://learn.microsoft.com/azure/devops/pipelines/tasks/reference/use-dotnet-v2?view=azure-pipelines + displayName: "Use .NET SDK from global.json" + retryCountOnTaskFailure: 3 + inputs: + useGlobalJson: true + workingDirectory: azure-sdk-tools + + # Copied from eng/pipelines/templates/steps/install-dotnet.yml, but changed workingDirectory + - task: UseDotNet@2 + displayName: "Use .NET SDK 8.0.x" + retryCountOnTaskFailure: 3 + inputs: + # We must install sdk, not just runtime, as it is required by some of our tools, like test-proxy. + # Specifically, test-proxy requires asp.net core runtime, which is installed only when sdk option + # is selected, per: https://github.com/microsoft/azure-pipelines-tasks/issues/14405 + packageType: sdk + version: 8.0.x + # performMultiLevelLookup comes into play when given .NET executable target runtime is different + # than the installed .NET SDK. Without this, such runtime would not be found. + performMultiLevelLookup: true + workingDirectory: azure-sdk-tools + - ${{ parameters.InstallLanguageSteps }} - template: /eng/common/TestResources/deploy-test-resources.yml diff --git a/eng/common/pipelines/templates/jobs/prepare-pipelines.yml b/eng/common/pipelines/templates/jobs/prepare-pipelines.yml index 6e692ad0b338..0e54b11a1e87 100644 --- a/eng/common/pipelines/templates/jobs/prepare-pipelines.yml +++ b/eng/common/pipelines/templates/jobs/prepare-pipelines.yml @@ -88,6 +88,7 @@ jobs: switch ($lang) { "java" { + $generatePublicCIPipeline = 'false' $internalVariableGroups = '$(AzureSDK_Maven_Release_Pipeline_Secrets) $(Release_Secrets_for_GitHub) $(APIReview_AutoCreate_Configurations)' $testVariableGroups = '$(Secrets_for_Resource_Provisioner)' } diff --git a/eng/common/pipelines/templates/stages/archetype-sdk-tool-pwsh.yml b/eng/common/pipelines/templates/stages/archetype-sdk-tool-pwsh.yml index 357b6d3360ed..d76b0392df98 100644 --- a/eng/common/pipelines/templates/stages/archetype-sdk-tool-pwsh.yml +++ b/eng/common/pipelines/templates/stages/archetype-sdk-tool-pwsh.yml @@ -25,18 +25,15 @@ stages: strategy: matrix: Windows: - Pool: azsdk-pool-mms-win-2022-general + Pool: azsdk-pool Image: windows-2022 Linux: - Pool: azsdk-pool-mms-ubuntu-2204-general - Image: ubuntu-22.04 - Mac: - Pool: Azure Pipelines - Image: macos-latest + Pool: azsdk-pool + Image: ubuntu-24.04 pool: name: $(Pool) - vmImage: $(Image) + demands: ImageOverride -equals $(Image) steps: - ${{ parameters.PreTestSteps }} diff --git a/eng/common/pipelines/templates/steps/create-authenticated-npmrc.yml b/eng/common/pipelines/templates/steps/create-authenticated-npmrc.yml index 4b6f08359bd9..52379684c4ea 100644 --- a/eng/common/pipelines/templates/steps/create-authenticated-npmrc.yml +++ b/eng/common/pipelines/templates/steps/create-authenticated-npmrc.yml @@ -3,6 +3,9 @@ parameters: type: string - name: registryUrl type: string + - name: CustomCondition + type: string + default: succeeded() steps: - pwsh: | @@ -17,7 +20,9 @@ steps: $content = "registry=${{ parameters.registryUrl }}`n`nalways-auth=true" $content | Out-File '${{ parameters.npmrcPath }}' displayName: 'Create .npmrc' + condition: ${{ parameters.CustomCondition }} - task: npmAuthenticate@0 displayName: Authenticate .npmrc + condition: ${{ parameters.CustomCondition }} inputs: workingFile: ${{ parameters.npmrcPath }} diff --git a/eng/common/pipelines/templates/steps/detect-api-changes.yml b/eng/common/pipelines/templates/steps/detect-api-changes.yml index b14f209cb12c..d997caa84915 100644 --- a/eng/common/pipelines/templates/steps/detect-api-changes.yml +++ b/eng/common/pipelines/templates/steps/detect-api-changes.yml @@ -5,24 +5,25 @@ parameters: Condition: true steps: - - pwsh: | - $apiChangeDetectRequestUrl = "https://apiview.dev/api/PullRequests/CreateAPIRevisionIfAPIHasChanges" - echo "##vso[task.setvariable variable=ApiChangeDetectRequestUrl]$apiChangeDetectRequestUrl" - displayName: "Set API change detect request URL" - condition: and(${{ parameters.Condition}}, eq(variables['ApiChangeDetectRequestUrl'], '')) + - ${{ if eq(variables['Build.Reason'],'PullRequest') }}: + - pwsh: | + $apiChangeDetectRequestUrl = "https://apiview.dev/api/PullRequests/CreateAPIRevisionIfAPIHasChanges" + echo "##vso[task.setvariable variable=ApiChangeDetectRequestUrl]$apiChangeDetectRequestUrl" + displayName: "Set API change detect request URL" + condition: and(${{ parameters.Condition}}, eq(variables['ApiChangeDetectRequestUrl'], '')) - - task: Powershell@2 - inputs: - filePath: ${{ parameters.RepoRoot }}/eng/common/scripts/Detect-Api-Changes.ps1 - arguments: > - -ArtifactPath ${{parameters.ArtifactPath}} - -CommitSha '$(System.PullRequest.SourceCommitId)' - -BuildId $(Build.BuildId) - -PullRequestNumber $(System.PullRequest.PullRequestNumber) - -RepoFullName $(Build.Repository.Name) - -APIViewUri $(ApiChangeDetectRequestUrl) - -ArtifactName ${{ parameters.ArtifactName }} - -DevopsProject $(System.TeamProject) - pwsh: true - displayName: Create APIView if API has changes - condition: and(${{ parameters.Condition }}, succeededOrFailed(), eq(variables['Build.Reason'],'PullRequest')) + - task: Powershell@2 + inputs: + filePath: ${{ parameters.RepoRoot }}/eng/common/scripts/Detect-Api-Changes.ps1 + arguments: > + -ArtifactPath ${{parameters.ArtifactPath}} + -CommitSha '$(System.PullRequest.SourceCommitId)' + -BuildId $(Build.BuildId) + -PullRequestNumber $(System.PullRequest.PullRequestNumber) + -RepoFullName $(Build.Repository.Name) + -APIViewUri $(ApiChangeDetectRequestUrl) + -ArtifactName ${{ parameters.ArtifactName }} + -DevopsProject $(System.TeamProject) + pwsh: true + displayName: Create APIView if API has changes + condition: and(${{ parameters.Condition }}, succeededOrFailed()) diff --git a/eng/common/pipelines/templates/steps/publish-1es-artifact.yml b/eng/common/pipelines/templates/steps/publish-1es-artifact.yml index 81ade0f1a441..30d38828e720 100644 --- a/eng/common/pipelines/templates/steps/publish-1es-artifact.yml +++ b/eng/common/pipelines/templates/steps/publish-1es-artifact.yml @@ -31,8 +31,8 @@ steps: inputs: artifactName: '$(PublishArtifactName)' targetPath: '${{ parameters.ArtifactPath }}' - # Disable sbom generation by default for our public validation builds to avoid unnecessary work - ${{ if eq(variables['System.TeamProject'], 'public') }}: + # Disable sbom generation by default for our public or pull request validation builds to avoid unnecessary work + ${{ if or(eq(variables['System.TeamProject'], 'public'), eq(variables['Build.Reason'], 'PullRequest')) }}: sbomEnabled: false ${{ else }}: sbomEnabled: ${{ parameters.SbomEnabled }} diff --git a/eng/common/pipelines/templates/steps/verify-links.yml b/eng/common/pipelines/templates/steps/verify-links.yml index 9811b8a83c5e..896b30d0fe38 100644 --- a/eng/common/pipelines/templates/steps/verify-links.yml +++ b/eng/common/pipelines/templates/steps/verify-links.yml @@ -28,7 +28,6 @@ steps: -ignoreLinksFile ${{ parameters.IgnoreLinksFile }} -branchReplaceRegex "${{ parameters.BranchReplaceRegex }}" -branchReplacementName ${{ parameters.BranchReplacementName }} - -devOpsLogging: $true -checkLinkGuidance: ${{ parameters.CheckLinkGuidance }} -localBuildRepoName "$env:BUILD_REPOSITORY_NAME" -localBuildRepoPath $(Build.SourcesDirectory) diff --git a/eng/common/scripts/Helpers/ApiView-Helpers.ps1 b/eng/common/scripts/Helpers/ApiView-Helpers.ps1 index d03c205ed568..e8d867db9e95 100644 --- a/eng/common/scripts/Helpers/ApiView-Helpers.ps1 +++ b/eng/common/scripts/Helpers/ApiView-Helpers.ps1 @@ -132,7 +132,7 @@ function Set-ApiViewCommentForRelatedIssues { . ${PSScriptRoot}\..\common.ps1 $issuesForCommit = $null try { - $issuesForCommit = Search-GitHubIssues -CommitHash $HeadCommitish + $issuesForCommit = Search-GitHubIssues -CommitHash $HeadCommitish -AuthToken $AuthToken if ($issuesForCommit.items.Count -eq 0) { LogInfo "No issues found for commit: $HeadCommitish" Write-Host "##vso[task.complete result=SucceededWithIssues;]DONE" diff --git a/eng/common/scripts/Invoke-GitHubAPI.ps1 b/eng/common/scripts/Invoke-GitHubAPI.ps1 index 556efd64a9b4..0e5bace3e48c 100644 --- a/eng/common/scripts/Invoke-GitHubAPI.ps1 +++ b/eng/common/scripts/Invoke-GitHubAPI.ps1 @@ -560,12 +560,17 @@ function Search-GitHubIssues { [ValidateNotNullOrEmpty()] [Parameter(Mandatory = $true)] $CommitHash, - $State="open" + $State="open", + $AuthToken ) $uri = "https://api.github.com/search/issues?q=sha:$CommitHash+state:$State" - - return Invoke-RestMethod ` - -Method GET ` - -Uri $uri ` - -MaximumRetryCount 3 + $params = @{ + Method = 'GET' + Uri = $uri + MaximumRetryCount = 3 + } + if ($AuthToken) { + $params.Headers = Get-GitHubApiHeaders -token $AuthToken + } + return Invoke-RestMethod @params } diff --git a/eng/common/scripts/Verify-Links.ps1 b/eng/common/scripts/Verify-Links.ps1 index 7bba07fe1ae1..d4406c609d0e 100644 --- a/eng/common/scripts/Verify-Links.ps1 +++ b/eng/common/scripts/Verify-Links.ps1 @@ -11,9 +11,6 @@ .PARAMETER ignoreLinksFile Specifies the file that contains a set of links to ignore when verifying. - .PARAMETER devOpsLogging - Switch that will enable devops specific logging for warnings. - .PARAMETER recursive Check the links recurisvely. Applies to links starting with 'baseUrl' parameter. Defaults to true. @@ -70,7 +67,6 @@ param ( [string[]] $urls, [string] $ignoreLinksFile = "$PSScriptRoot/ignore-links.txt", - [switch] $devOpsLogging = $false, [switch] $recursive = $true, [string] $baseUrl = "", [string] $rootUrl = "", @@ -89,6 +85,8 @@ param ( Set-StrictMode -Version 3.0 +. "$PSScriptRoot/logging.ps1" + $ProgressPreference = "SilentlyContinue"; # Disable invoke-webrequest progress dialog function ProcessLink([System.Uri]$linkUri) { @@ -211,30 +209,6 @@ function NormalizeUrl([string]$url) { return $uri } -function LogWarning -{ - if ($devOpsLogging) - { - Write-Host "##vso[task.LogIssue type=warning;]$args" - } - else - { - Write-Warning "$args" - } -} - -function LogError -{ - if ($devOpsLogging) - { - Write-Host "##vso[task.logissue type=error]$args" - } - else - { - Write-Error "$args" - } -} - function ResolveUri ([System.Uri]$referralUri, [string]$link) { # If the link is mailto, skip it. @@ -554,9 +528,8 @@ foreach ($url in $urls) { $pageUrisToCheck.Enqueue($uri); } -if ($devOpsLogging) { - Write-Host "##[group]Link checking details" -} +LogGroupStart "Link checking details" + while ($pageUrisToCheck.Count -ne 0) { $pageUri = $pageUrisToCheck.Dequeue(); @@ -592,9 +565,7 @@ while ($pageUrisToCheck.Count -ne 0) } try { - if ($devOpsLogging) { - Write-Host "##[endgroup]" - } + LogGroupEnd if ($badLinks.Count -gt 0) { Write-Host "Summary of broken links:" diff --git a/eng/common/scripts/copy-docs-to-blobstorage.ps1 b/eng/common/scripts/copy-docs-to-blobstorage.ps1 index bfcae988b875..852945338e9d 100644 --- a/eng/common/scripts/copy-docs-to-blobstorage.ps1 +++ b/eng/common/scripts/copy-docs-to-blobstorage.ps1 @@ -61,7 +61,7 @@ function ToSemVer($version){ function SortSemVersions($versions) { - return $versions | Sort -Property Major, Minor, Patch, PrereleaseLabel, PrereleaseNumber -Descending + return $versions | Sort-Object -Property Major, Minor, Patch, PrereleaseLabel, PrereleaseNumber -Descending } function Sort-Versions diff --git a/eng/common/scripts/job-matrix/Create-JobMatrix.ps1 b/eng/common/scripts/job-matrix/Create-JobMatrix.ps1 index 3fc4a3255dd0..d35b3c923a6d 100644 --- a/eng/common/scripts/job-matrix/Create-JobMatrix.ps1 +++ b/eng/common/scripts/job-matrix/Create-JobMatrix.ps1 @@ -21,15 +21,27 @@ param ( ) . $PSScriptRoot/job-matrix-functions.ps1 +. $PSScriptRoot/../logging.ps1 if (!(Test-Path $ConfigPath)) { Write-Error "ConfigPath '$ConfigPath' does not exist." exit 1 } -$config = GetMatrixConfigFromFile (Get-Content $ConfigPath -Raw) +$rawConfig = Get-Content $ConfigPath -Raw +$config = GetMatrixConfigFromFile $rawConfig # Strip empty string filters in order to be able to use azure pipelines yaml join() $Filters = $Filters | Where-Object { $_ } +LogGroupStart "Matrix generation configuration" +Write-Host "Configuration File: $ConfigPath" +Write-Host $rawConfig +Write-Host "SelectionType: $Selection" +Write-Host "DisplayNameFilter: $DisplayNameFilter" +Write-Host "Filters: $Filters" +Write-Host "Replace: $Replace" +Write-Host "NonSparseParameters: $NonSparseParameters" +LogGroupEnd + [array]$matrix = GenerateMatrix ` -config $config ` -selectFromMatrixType $Selection ` @@ -41,7 +53,8 @@ $Filters = $Filters | Where-Object { $_ } $serialized = SerializePipelineMatrix $matrix -Write-Output $serialized.pretty +Write-Host "Generated matrix:" +Write-Host $serialized.pretty if ($CI) { Write-Output "##vso[task.setVariable variable=matrix;isOutput=true]$($serialized.compressed)" diff --git a/eng/common/spelling/package-lock.json b/eng/common/spelling/package-lock.json index a87e13d2c52b..8f3d520f4a8c 100644 --- a/eng/common/spelling/package-lock.json +++ b/eng/common/spelling/package-lock.json @@ -509,9 +509,10 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2021,9 +2022,9 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" diff --git a/eng/common/testproxy/dotnet-devcert.crt b/eng/common/testproxy/dotnet-devcert.crt index 764cf84c6c0d..931b6e739722 100644 --- a/eng/common/testproxy/dotnet-devcert.crt +++ b/eng/common/testproxy/dotnet-devcert.crt @@ -1,21 +1,21 @@ -----BEGIN CERTIFICATE----- -MIIDZzCCAk+gAwIBAgIUUGsnTw0cjASpYrNVxo7S41oMGtUwDQYJKoZIhvcNAQEL -BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDcyODA2NTAzOFoXDTI1MDcy -ODA2NTAzOFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAs9w2heE1VvnZcD3aR3jLgI/tiwjJScf+ljOMor9lxcIA -80NvcnfH3T7eH4Gsk0xcQ+J94FpFC5sDcsumcuEV1W6flvqj9k8vASoBpvpkoUnA -tr6aChLEp5hpwz59NAhdLrzd1eU+PNtB2CS0Blb6OEKUsVvoscmP5BwOWe+fuAdW -rQisHYrYzT9K5oWQA+AntrRJPbow7LCGQtT1viIlqxKaI8mTor+aRB0AmrAMJlPZ -4scmrpNs7+ertRVTz+uZ6HWHN5CJsVKnFKgTTuYYustRV2Oz9tF4W0+j1fBKCkYM -WQdga3md/9t51mG0naezJNDsMlT45IFOadqOozqsswIDAQABo4GwMIGtMA8GA1Ud +MIIDZzCCAk+gAwIBAgIUXUCvBB5U6rYQyAEu5rApzjp3RQYwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDcyMzE3NTUyM1oXDTI2MDcy +MzE3NTUyM1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA67HYsBPB865zJS8TDwUBgFSKqWgJ7dGTQh3aTlvamFED +5/kHnL7oTr3x/6Hylnajf0v4vGWmSok0/3SAcbpr/9l19/7zbpA1LLGzu8G909o5 +38Wl5sNbGvQIMK91KxbHRXlVMKoYTIL38cNdZvhzfb9m9Tew6vPmz4ABMPiwYS9T +R19lAPwmQYwce00NkKaQE5+6pzsPhnG/o/Ww9rBE370fidXn8jhqLSOEk+hbp3ju +KlxeSrVHAqlvTzlvSTZGRyxioRLDEMFT3ka1cyLo6HP3U7lj76mlJBibahE+ylL+ +z594fzHnfYPQaN5g13G9H2oxTg+VwwNtL1U737FNiwIDAQABo4GwMIGtMA8GA1Ud EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGmMBYGA1UdJQEB/wQMMAoGCCsGAQUF BwMBMBcGA1UdEQEB/wQNMAuCCWxvY2FsaG9zdDA6BgorBgEEAYI3VAEBBCwMKkFT UC5ORVQgQ29yZSBIVFRQUyBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTAdBgNVHQ4E -FgQUhYGoJsA6zisErkL5EcbKAO3LoQ8wDQYJKoZIhvcNAQELBQADggEBAHOBgMVZ -a27y41edktHaLC4kayKHe3bLBtiXUd1vjWEZaPrcvQH6FItJdB5Z3xbi/4LA6QuU -dZ33z44Lt4AzOiyvR7AGojytM2UYimtBBOFvi9CZuyrMC6ls4SLy6c/Rq1ATmyet -QfkT1a7awC1cJUn7jeuHfly7hnYqrw+67HFY7Ajugv+Uwu6wCessKloWM127AZj/ -V0Gw0rblOmX+uX7fvH3BrYOkrNiHQdzCYZ1P1WU7eWm55JgBfaCrheNZ4L5TVeB2 -uTZPnlsBKQ23qgZ0dtcJzL0scC7zSuYI+qm1QNNyz13K+1ORXPQGQJXULlt7UeHB -ZYN060pO6IO4Ti8= +FgQUg5xH/y5mh7lGgF+y+1sJC7ZMN9owDQYJKoZIhvcNAQELBQADggEBAK/gRk/L +5q4/xXXYW77WawygxpGmTgBLDHkiJViMnJ4VDk6q97/DMABIqCp3sv3FNP9sOssG +ypgmX4jYbyrLNwfvdtpVaiHAHvrTfAtrfHgG0EVM1DRUJ1e0/SRfYf1QTdVxEZv+ +3IM+0roYa8EtQq8BxSsAZz+zhNMoav8nXQVhR7+v5NI5vzT0TVncfXIYYYfLhllb +wcmh9iQuXMifj7WohOFE1XK4O6Bats/6V85ZSGDl3npEpYcgBwyxNQE5hKn/lG5b +3DDkpCTeoMZxAHLo39RzAy0WJTF1KPHQ2EzUa+MfoTNWNrhYSW3IqI3xfPzevSDx +BTBk4gS9MSt2Jj8= -----END CERTIFICATE----- diff --git a/eng/common/testproxy/dotnet-devcert.pfx b/eng/common/testproxy/dotnet-devcert.pfx new file mode 100644 index 000000000000..93a5617aea60 Binary files /dev/null and b/eng/common/testproxy/dotnet-devcert.pfx differ diff --git a/eng/pipelines/swagger-api-doc-preview.yml b/eng/pipelines/swagger-api-doc-preview.yml new file mode 100644 index 000000000000..8174542fd87f --- /dev/null +++ b/eng/pipelines/swagger-api-doc-preview.yml @@ -0,0 +1,227 @@ +trigger: none +pr: + paths: + include: + # Trigger for files that will result in a doc preview build + - specification/** + + # Smoke test on changed files + - eng/pipelines/swagger-api-doc-preview.yml + - eng/pipelines/templates/steps/set-sha-check.yml + - .github/shared/src/doc-preview.js + - .github/shared/cmd/api-doc-preview.js + +jobs: + - job: SwaggerApiDocPreview + + pool: + name: $(LINUXPOOL) + vmImage: $(LINUXVMIMAGE) + + variables: + - template: /eng/pipelines/templates/variables/globals.yml + - template: /eng/pipelines/templates/variables/image.yml + + - name: BranchName + value: preview/$(Build.Repository.Name)/pr/$(System.PullRequest.PullRequestNumber)/build/$(Build.BuildId)/attempt/$(System.JobAttempt) + + - name: CurrentBuildUrl + value: $(System.CollectionUri)$(System.TeamProject)/_build/results?buildId=$(Build.BuildId) + + - name: StatusName + value: 'Swagger ApiDocPreview' + + steps: + - template: /eng/pipelines/templates/steps/set-sha-check.yml + parameters: + State: pending + TargetUrl: $(CurrentBuildUrl) + Description: 'Starting docs build' + Context: $(StatusName) + + - checkout: self + # Fetch depth required to get list of changed files + fetchDepth: 2 + + - template: /eng/common/pipelines/templates/steps/sparse-checkout.yml + parameters: + SkipCheckoutNone: true + TokenToUseForAuth: $(azuresdk-github-pat) + # Path does not need to be set because sparse-checkout.yml already + # checks out files in the repo root + Repositories: + - Name: MicrosoftDocs/AzureRestPreview + Commitish: main + WorkingDirectory: AzureRestPreview + + - template: /eng/pipelines/templates/steps/npm-install.yml + parameters: + WorkingDirectory: .github/shared + + - script: cmd/api-doc-preview.js --output ../../AzureRestPreview + displayName: Generate Swagger API documentation preview + workingDirectory: .github/shared + + - template: /eng/common/pipelines/templates/steps/git-push-changes.yml + parameters: + WorkingDirectory: AzureRestPreview + BaseRepoOwner: MicrosoftDocs + TargetRepoOwner: MicrosoftDocs + TargetRepoName: AzureRestPreview + BaseRepoBranch: $(BranchName) + CommitMsg: | + Update API Doc Preview + Build: $(System.CollectionUri)$(System.TeamProject)/_build/results?buildId=$(Build.BuildId) + PR: $(System.PullRequest.SourceRepositoryURI)/pull/$(System.PullRequest.PullRequestId) + + - task: AzureCLI@2 + displayName: Start docs build + condition: and(succeeded(), eq(variables['HasChanges'], 'true')) + inputs: + azureSubscription: msdocs-apidrop-connection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + $buildStartRaw = az pipelines build queue ` + --organization "https://dev.azure.com/apidrop/" ` + --project "Content CI" ` + --definition-id "8157" ` + --variables 'params={"target_repo":{"url":"https://github.com/MicrosoftDocs/AzureRestPreview","branch":"$(BranchName)"}, "source_of_truth": "code"}' + $buildStartRaw | Set-Content buildstart.json + $buildStart = $buildStartRaw | ConvertFrom-Json + Write-Host "Build started at https://dev.azure.com/apidrop/Content%20CI/_build/results?buildId=$($buildStart.id)" + + - template: /eng/pipelines/templates/steps/set-sha-check.yml + parameters: + Condition: and(succeeded(), eq(variables['HasChanges'], 'true')) + State: pending + TargetUrl: $(CurrentBuildUrl) + Description: 'Waiting for docs build to finish' + Context: $(StatusName) + + - task: AzureCLI@2 + displayName: Wait for docs build to finish + condition: and(succeeded(), eq(variables['HasChanges'], 'true')) + # Retry on failure to handle transient issues or builds that run longer + # than the token used by az CLI is valid + retryCountOnTaskFailure: 3 + inputs: + azureSubscription: msdocs-apidrop-connection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + $buildStart = Get-Content buildstart.json | ConvertFrom-Json + + Write-Host "Waiting for build to finish: https://dev.azure.com/apidrop/Content%20CI/_build/results?buildId=$($buildStart.id)" + + # Timeout in 10 minutes to avoid infinite waiting + $start = Get-Date + while (((Get-Date) - $start).TotalMinutes -lt 10) { + $runStatusRaw = az pipelines runs show ` + --organization "https://dev.azure.com/apidrop/" ` + --project "Content CI" ` + --id "$($buildStart.id)" + + if ($LASTEXITCODE) { + Write-Host "Failed to get run status" + Write-Host "Exit code: $LASTEXITCODE" + Write-Host "Output: $runStatusRaw" + exit 1 + } + + $runStatus = $runStatusRaw | ConvertFrom-Json + Write-Host "Run status: $($runStatus.status)" + + if ($runStatus.status -eq "completed") { + break; + } + Start-Sleep -Seconds 10 + } + + Write-Host "Build completed with status: $($runStatus.result)" + Write-Host "Build logs: https://dev.azure.com/apidrop/Content%20CI/_build/results?buildId=$($buildStart.id)" + + Write-Host "Downloading artifact..." + $artifactDownloadRaw = az pipelines runs artifact download ` + --organization "https://dev.azure.com/apidrop/" ` + --project "Content CI" ` + --run-id "$($buildStart.id)" ` + --artifact-name "report" ` + --path "./" + + if ($LASTEXITCODE) { + Write-Host "Failed to download artifact" + Write-Host "Exit code: $LASTEXITCODE" + Write-Host "Output: $artifactDownloadRaw" + exit 1 + } + + Write-Host "Artifact downloaded successfully" + + - pwsh: | + # Read the report.json file downloaded from the docs build artifact + $reportRaw = Get-Content -Path "./report.json" -Raw + $report = $reportRaw | ConvertFrom-Json -AsHashtable + + # Get build ID from buildstart.json (set during the "Start docs build" step) + $buildStart = Get-Content buildstart.json | ConvertFrom-Json + $buildLink = "https://dev.azure.com/apidrop/Content%20CI/_build/results?buildId=$($buildStart.id)" + + if ($report.status -ne "Succeeded") { + Write-Host "Docs build failed with status: $($report.status)" + Write-Host "Report:" + Write-Host $reportRaw + + Write-Host "##vso[task.setvariable variable=CheckUrl]${buildLink}" + Write-Host "##vso[task.setvariable variable=CheckDescription]Docs build failed (click to see pipeline logs)" + Write-Host "##vso[task.setvariable variable=CheckState]failure" + + # Docs build failed, but this job should not fail unless it + # encounters an unexpected error. The check status will be set in + # the next task. + exit 0 + } + + Write-Host "Docs build succeeded with status: $($report.status)" + + $docsPreviewUrl = "https://review.learn.microsoft.com/en-us/rest/api/azure-rest-preview/?branch=$([System.Web.HttpUtility]::UrlEncode($report.branch))&view=azure-rest-preview" + Write-Host "##vso[task.setvariable variable=CheckUrl]$docsPreviewUrl" + Write-Host "##vso[task.setvariable variable=CheckDescription]Docs build succeeded (click to see preview)" + Write-Host "##vso[task.setvariable variable=CheckState]success" + Write-Host "Docs preview URL: $docsPreviewUrl" + + exit 0 + displayName: Interpret docs build results + condition: and(succeeded(), eq(variables['HasChanges'], 'true')) + + # Sets check status from docs build using $(CheckUrl), $(CheckState), and + # $(CheckDescription) variables set by api-doc-preview-interpret.js. + - template: /eng/pipelines/templates/steps/set-sha-check.yml + parameters: + DisplayName: Set PR status from docs build + Condition: and(succeeded(), eq(variables['HasChanges'], 'true')) + State: $(CheckState) + TargetUrl: $(CheckUrl) + Description: $(CheckDescription) + Context: $(StatusName) + + - template: /eng/pipelines/templates/steps/set-sha-check.yml + parameters: + DisplayName: Set PR status for no-op + Condition: and(succeeded(), ne(variables['HasChanges'], 'true')) + State: success + TargetUrl: $(CurrentBuildUrl) + Description: No files changed require docs build + Context: $(StatusName) + + # In the event of a failure in this job (not the docs build job), set the + # PR status to failed and link to the current build. + - template: /eng/pipelines/templates/steps/set-sha-check.yml + parameters: + DisplayName: Set PR status for job failure + # Only run if a previous step in the job failed + Condition: failed() + State: failure + TargetUrl: $(CurrentBuildUrl) + Description: 'Orchestration build failed (click to see logs)' + Context: $(StatusName) diff --git a/eng/pipelines/templates/steps/npm-install.yml b/eng/pipelines/templates/steps/npm-install.yml index c64de780bc12..399d58b558fa 100644 --- a/eng/pipelines/templates/steps/npm-install.yml +++ b/eng/pipelines/templates/steps/npm-install.yml @@ -2,6 +2,9 @@ parameters: - name: NodeVersion type: string default: $(NodeVersion) + - name: WorkingDirectory + type: string + default: $(Build.SourcesDirectory) steps: - template: /eng/pipelines/templates/steps/use-node-version.yml @@ -10,7 +13,9 @@ steps: - script: npm ci displayName: npm ci + workingDirectory: ${{ parameters.WorkingDirectory }} - script: npm ls -a || true displayName: npm ls -a condition: succeededOrFailed() + workingDirectory: ${{ parameters.WorkingDirectory }} diff --git a/eng/pipelines/templates/steps/set-sha-check.yml b/eng/pipelines/templates/steps/set-sha-check.yml new file mode 100644 index 000000000000..ae5845922de0 --- /dev/null +++ b/eng/pipelines/templates/steps/set-sha-check.yml @@ -0,0 +1,64 @@ +# Create a "status" check for a SHA (inferred from PR by default) in a GitHub +# repository. By default this uses the azure-sdk account. This might not work +# for required checks where a source must be configured. + +parameters: + - name: Sha + type: string + default: $(System.PullRequest.SourceCommitId) + + - name: RepositoryName + type: string + default: $(Build.Repository.Name) + + - name: State + type: string + default: 'pending' + # Valid values: + # - error + # - failure + # - pending + # - success + + - name: TargetUrl + type: string + default: '' + + - name: Description + type: string + default: '' + + - name: Context + type: string + default: default context + + - name: Condition + type: string + default: succeeded() + + - name: DisplayName + type: string + default: 'Set PR status' + + - name: GitHubToken + type: string + default: $(azuresdk-github-pat) + +steps: + - bash: | + echo "Repository Name: ${{ parameters.RepositoryName }}" + echo "Commit ID: ${{ parameters.Sha }}" + echo "State: ${{ parameters.State }}" + echo "Target URL: ${{ parameters.TargetUrl }}" + echo "Description: ${{ parameters.Description }}" + echo "Context: ${{ parameters.Context }}" + + gh api repos/${{ parameters.RepositoryName }}/statuses/${{ parameters.Sha }} \ + -f state='${{ parameters.State }}' \ + -f target_url='${{ parameters.TargetUrl }}' \ + -f description='${{ parameters.Description }}' \ + -f context='${{ parameters.Context }}' + displayName: ${{ parameters.DisplayName }} + condition: ${{ parameters.Condition }} + env: + GH_TOKEN: ${{ parameters.GitHubToken }} diff --git a/eng/scripts/Create-APIView.ps1 b/eng/scripts/Create-APIView.ps1 index e855e93765e6..999129e68946 100644 --- a/eng/scripts/Create-APIView.ps1 +++ b/eng/scripts/Create-APIView.ps1 @@ -464,7 +464,7 @@ function New-TypeSpecAPIViewTokens { LogGroupEnd foreach ($typeSpecProject in $typeSpecProjects) { # Skip Baseline APIView Token for new projects - if (!(Test-Path -Path $typeSpecProject)) { + if (!(Test-Path -Path (Join-Path $typeSpecProject "tspconfig.yaml"))) { Write-Host "TypeSpec project $typeSpecProject is not found in pull request target branch. API review will not have a baseline revision." } else { diff --git a/eng/scripts/TypeSpec-Requirement.ps1 b/eng/scripts/TypeSpec-Requirement.ps1 index 4b2628348cbb..ab7682e11742 100644 --- a/eng/scripts/TypeSpec-Requirement.ps1 +++ b/eng/scripts/TypeSpec-Requirement.ps1 @@ -169,7 +169,7 @@ else { if ($responseStatus -eq 200) { LogInfo " Branch 'main' contains path '$servicePath/stable', so spec already exists and is not required to use TypeSpec" - $notice = "Brownfield services will soon be required to convert from OpenAPI to TypeSpec. See https://aka.ms/azsdk/typespec." + $notice = "Your service description will soon be required to convert from OpenAPI to TypeSpec. See https://aka.ms/azsdk/typespec." LogNoticeForFile $file $notice if ($env:GITHUB_OUTPUT) { diff --git a/eng/scripts/TypeSpec-Validation.ps1 b/eng/scripts/TypeSpec-Validation.ps1 index 0ea5255aa788..84d95d468cf1 100644 --- a/eng/scripts/TypeSpec-Validation.ps1 +++ b/eng/scripts/TypeSpec-Validation.ps1 @@ -1,5 +1,6 @@ [CmdletBinding()] param ( + [switch]$IgnoreCoreFiles = $false, [switch]$CheckAll = $false, [int]$Shard = 0, [int]$TotalShards = 1, @@ -17,10 +18,11 @@ if ($TotalShards -gt 0 -and $Shard -ge $TotalShards) { . $PSScriptRoot/Suppressions-Functions.ps1 . $PSScriptRoot/Array-Functions.ps1 -$typespecFolders, $checkedAll = &"$PSScriptRoot/Get-TypeSpec-Folders.ps1" ` +$typespecFolders, $checkingAllSpecs = &"$PSScriptRoot/Get-TypeSpec-Folders.ps1" ` -BaseCommitish:$BaseCommitish ` -HeadCommitish:$HeadCommitish ` - -CheckAll:$CheckAll + -CheckAll:$CheckAll ` + -IgnoreCoreFiles:$IgnoreCoreFiles if ($TotalShards -gt 1 -and $TotalShards -le $typespecFolders.Count) { $typespecFolders = shardArray $typespecFolders $Shard $TotalShards @@ -33,11 +35,11 @@ foreach ($typespecFolder in $typespecFolders) { $typespecFoldersWithFailures = @() if ($typespecFolders) { - $typespecFolders = $typespecFolders.Split('',[System.StringSplitOptions]::RemoveEmptyEntries) + $typespecFolders = $typespecFolders.Split('', [System.StringSplitOptions]::RemoveEmptyEntries) foreach ($typespecFolder in $typespecFolders) { LogGroupStart "Validating $typespecFolder" - if ($checkedAll) { + if ($checkingAllSpecs) { $suppression = Get-Suppression "TypeSpecValidationAll" $typespecFolder if ($suppression) { $reason = $suppression["reason"] ?? "" @@ -47,14 +49,17 @@ if ($typespecFolders) { } } - LogInfo "npm exec --no -- tsv $typespecFolder" + # Example: '{"checkingAllSpecs"=true}' + $context = @{ checkingAllSpecs = $checkingAllSpecs } | ConvertTo-Json -Compress + + LogInfo "npm exec --no -- tsv $typespecFolder ""$context""" if ($DryRun) { LogGroupEnd continue } - npm exec --no -- tsv $typespecFolder 2>&1 | Write-Host + npm exec --no -- tsv $typespecFolder "$context" 2>&1 | Write-Host if ($LASTEXITCODE) { $typespecFoldersWithFailures += $typespecFolder $errorString = "TypeSpec Validation failed for project $typespecFolder run the following command locally to validate." @@ -69,7 +74,8 @@ if ($typespecFolders) { } LogGroupEnd } -} else { +} +else { if ($CheckAll) { LogError "TypeSpec Validation - All did not validate any specs" LogJobFailure diff --git a/eng/tools/.prettierignore b/eng/tools/.prettierignore new file mode 100644 index 000000000000..788bffb16216 --- /dev/null +++ b/eng/tools/.prettierignore @@ -0,0 +1,10 @@ +# this file +.prettierignore + +# build artifacts +coverage +dist + +# specs in test folders +fixtures +specification diff --git a/eng/tools/.prettierrc.yaml b/eng/tools/.prettierrc.yaml new file mode 100644 index 000000000000..69cc825aaf04 --- /dev/null +++ b/eng/tools/.prettierrc.yaml @@ -0,0 +1,15 @@ +# Keep in sync with .github/.prettierrc.yaml + +plugins: + - prettier-plugin-organize-imports + +# Aligned with microsoft/typespec +printWidth: 100 + +overrides: + # tsconfig.json is actually parsed as JSONC + - files: + - tsconfig.json + - tsconfig.*.json + options: + parser: jsonc diff --git a/eng/tools/lint-diff/README.md b/eng/tools/lint-diff/README.md index cd7e80add688..67556c42f4d4 100644 --- a/eng/tools/lint-diff/README.md +++ b/eng/tools/lint-diff/README.md @@ -1,30 +1,51 @@ -# TODO: REVERIFY THESE DOCS BEFORE MERGE # LintDiff LintDiff looks at files changed in a commit, runs `autorest` linting over those -files, and looks for "new" errors in the results. +files, and looks for "new" errors in the results. -Altered files examined include `readme.md` files and swagger `.json` files. In -the case of `readme.md` files the tool will examine tags and other files that -could be impacted by the change (e.g. `readme.md` files in folders higher in the -directory structure). +Interesting changed files are swagger `.json` files and `readme.md` files. +Changes to swagger JSON files are mapped to `readme.md` files and tags which +include the swagger file. The entire tree of swagger dependencies is traversed +so changes to a file will result in scanning of readme.md and swagger files +which depend on the changed file. + +## Correlation + +LintDiff attempts to correlate errors found to files with the same base name +and JSON path to the error site. Array position matters, so if an object's +position in an array changes, the error will not correlate and may be reported. + +## Prerequisites + +- Node.JS 20 or later ## Run Locally -1. Setup `before` - Clone the specs repo at the *base commit* for the changes you want to test. (generally cloned to main and in a temp location like `/tmp/rest-api-specs-before`) -1. Setup `after` - Clone the specs repo at the commit for the changes you want to test. -1. Build a list of changed files from `after` generally by running `git diff --name-only > changed-files.txt` -1. From the root of the repo run `npm i` to install dependencies. -1. Run `npm exec --no -- lint-diff --before --after --changed-files ` +1. Setup `before` folder state - Clone the specs repo at the _base commit_ for the changes you want to test. (generally cloned to main and in a temp location like `/tmp/rest-api-specs-before`) +1. Setup `after` folder state - Clone the specs repo at the commit for the changes you want to test in a different folder. +1. Build a list of changed files from `after` generally by running `git diff --name-only > changed-files.txt` in the `after` folder. +1. From the root of the repo run `npm ci` to install dependencies. +1. Run `npm exec --no -- lint-diff --before --after --changed-files ` -### Example +### Example In most common cases, changes to LintDiff are in the current state of the repo and you'll need to set up a `before` to run against. +At the root of the repo with changes you want to test, run the following commands: + ```bash +# Clone the specs repo into a temporary location at the base commit git clone https://github.com/Azure/azure-rest-api-specs.git /tmp/rest-api-specs-before + +# Diff the current repo (after state) against the base commit git diff --name-only main > changed-files.txt -npm i +npm ci npm exec --no -- lint-diff --before /tmp/rest-api-specs-before --after . --changed-files changed-files.txt ``` + +## Output + +Output is written by default to `lint-diff.md` in the current working +directory. The file contains a table of errors. LintDiff itself also +formats error output to the console. diff --git a/eng/tools/lint-diff/package.json b/eng/tools/lint-diff/package.json index 7e2cdb82c12a..7b2136b5f2f4 100644 --- a/eng/tools/lint-diff/package.json +++ b/eng/tools/lint-diff/package.json @@ -8,25 +8,25 @@ }, "scripts": { "build": "tsc --build", - "test": "vitest", - "test:ci": "vitest run --coverage --reporter=verbose", "clean": "rm -rf dist && rm -rf node_modules", - "prettier": "prettier \"**/*.js\" --check", - "prettier:debug": "prettier \"**/*.js\" --check ---log-level debug", - "prettier:write": "prettier \"**/*.js\" --write" + "format": "prettier . --ignore-path ../.prettierignore --write", + "format:check": "prettier . --ignore-path ../.prettierignore --check", + "format:check:ci": "prettier . --ignore-path ../.prettierignore --check --log-level debug", + "test": "vitest", + "test:ci": "vitest run --coverage --reporter=verbose" }, "engines": { "node": ">= 20.0.0" }, "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.9.3", + "@apidevtools/json-schema-ref-parser": "^14.1.1", "@azure-tools/specs-shared": "file:../../../.github/shared", - "@microsoft.azure/openapi-validator": "2.2.4", - "autorest": "3.6.1", + "@microsoft.azure/openapi-validator": "^2.2.4", + "autorest": "^3.7.2", "axios": "^1.8.3", "change-case": "^5.4.4", "deep-eql": "^5.0.2", - "marked": "^15.0.7" + "marked": "^16.1.1" }, "devDependencies": { "@types/deep-eql": "^4.0.2", @@ -35,6 +35,7 @@ "execa": "^9.5.2", "memfs": "^4.17.0", "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "typescript": "~5.8.2", "vitest": "^3.0.2" } diff --git a/eng/tools/lint-diff/src/correlateResults.ts b/eng/tools/lint-diff/src/correlateResults.ts index c48f1c614782..f69ed1eb5ece 100644 --- a/eng/tools/lint-diff/src/correlateResults.ts +++ b/eng/tools/lint-diff/src/correlateResults.ts @@ -1,8 +1,8 @@ +import { Readme } from "@azure-tools/specs-shared/readme"; import { basename, join, relative } from "path"; -import { relativizePath, pathExists, isFailure, isWarning } from "./util.js"; import { AutorestRunResult, BeforeAfter, LintDiffViolation, Source } from "./lintdiff-types.js"; import { getDefaultTag } from "./markdown-utils.js"; -import { Readme } from "@azure-tools/specs-shared/readme"; +import { isFailure, isWarning, pathExists, relativizePath } from "./util.js"; export async function correlateRuns( beforePath: string, @@ -150,7 +150,7 @@ export function getLintDiffViolations(runResult: AutorestRunResult): LintDiffVio const result = JSON.parse(line.trim()); if (result.code == undefined) { - // Results without a code can be assumed to be fatal errors. Set the code + // Results without a code can be assumed to be fatal errors. Set the code // to "FATAL" result.code = "FATAL"; } diff --git a/eng/tools/lint-diff/src/generateReport.ts b/eng/tools/lint-diff/src/generateReport.ts index f672a93f59b7..bb2fab806137 100644 --- a/eng/tools/lint-diff/src/generateReport.ts +++ b/eng/tools/lint-diff/src/generateReport.ts @@ -1,16 +1,21 @@ +import { kebabCase } from "change-case"; import { writeFile } from "node:fs/promises"; import { relative } from "node:path"; -import { kebabCase } from "change-case"; -import { getRelatedArmRpcFromDoc } from "./markdown-utils.js"; -import { getPathToDependency, getDependencyVersion, relativizePath } from "./util.js"; import { getViolations } from "./correlateResults.js"; -import { isFailure, isWarning } from "./util.js"; import { - AutorestRunResult, AutoRestMessage, + AutorestRunResult, BeforeAfter, LintDiffViolation, } from "./lintdiff-types.js"; +import { getRelatedArmRpcFromDoc } from "./markdown-utils.js"; +import { + getDependencyVersion, + getPathToDependency, + isFailure, + isWarning, + relativizePath, +} from "./util.js"; const LIMIT_50_MESSAGE = "Only 50 items are listed, please refer to log for more details."; @@ -20,6 +25,7 @@ export async function generateLintDiffReport( outFile: string, baseBranch: string, compareSha: string, + githubRepoPath: string, ): Promise { console.log("Generating LintDiff report..."); @@ -43,11 +49,18 @@ export async function generateLintDiffReport( const afterPath = getPath(after); const beforePath = before ? getPath(before) : ""; - outputMarkdown += `| ${afterName} | [${afterName}](${getFileLink(compareSha, afterPath)}) | [${beforeName}](${getFileLink(baseBranch, beforePath)}) |\n`; + outputMarkdown += `| ${afterName} | [${afterName}](${getFileLink(githubRepoPath, compareSha, afterPath)}) | [${beforeName}](${getFileLink(githubRepoPath, baseBranch, beforePath)}) ${getAutoRestFailedMessage(before)}|\n`; } outputMarkdown += `\n\n`; + for (const [, { before }] of runCorrelations.entries()) { + if (before && before.error) { + outputMarkdown += `> [!WARNING]\n`; + outputMarkdown += `> Autorest failed checking before state of ${relative(before.rootPath, before.readme.path)} ${before.tag}\n\n`; + } + } + const [newViolations, existingViolations] = getViolations(runCorrelations, affectedSwaggers); for (const newItem of newViolations) { @@ -70,7 +83,7 @@ export async function generateLintDiffReport( outputMarkdown += "| ---- | ------- | ------------------------------- |\n"; for (const violation of newViolations.slice(0, 50)) { - outputMarkdown += getNewViolationReportRow(violation, compareSha); + outputMarkdown += getNewViolationReportRow(violation, githubRepoPath, compareSha); } if (newViolations.some((v) => isFailure(v.level))) { @@ -99,7 +112,7 @@ export async function generateLintDiffReport( for (const violation of existingViolations.slice(0, 50)) { const { level, code, message } = violation; - outputMarkdown += `| ${iconFor(level)} [${code}](${getDocUrl(code)}) | ${message}
    Location: [${getPathSegment(relativizePath(getFile(violation)))}#L${getLine(violation)}](${getFileLink(compareSha, relativizePath(getFile(violation)), getLine(violation))}) |\n`; + outputMarkdown += `| ${iconFor(level)} [${code}](${getDocUrl(code)}) | ${message}
    Location: [${getPathSegment(relativizePath(getFile(violation)))}#L${getLine(violation)}](${getFileLink(githubRepoPath, compareSha, relativizePath(getFile(violation)), getLine(violation))}) |\n`; } LogViolations("Existing violations list", existingViolations); @@ -208,16 +221,21 @@ export function getPathSegment(path: string): string { return path.split("/").slice(-4).join("/"); } -export function getFileLink(sha: string, path: string, line: number | null = null) { +export function getFileLink( + repoPath: string, + sha: string, + path: string, + line: number | null = null, +) { // Paths can sometimes contain a preceeding slash if coming from a nomralized // filesystem path. In this case, remove it so the link doesn't contain two // forward slashes. const urlPath = path.startsWith("/") ? path.slice(1) : path; if (line === null) { - return `https://github.com/Azure/azure-rest-api-specs/blob/${sha}/${urlPath}`; + return `https://github.com/${repoPath}/blob/${sha}/${urlPath}`; } - return `https://github.com/Azure/azure-rest-api-specs/blob/${sha}/${urlPath}#L${line}`; + return `https://github.com/${repoPath}/blob/${sha}/${urlPath}#L${line}`; } export function getDocUrl(id: string) { @@ -241,14 +259,18 @@ export function getLine(lintDiffViolation: LintDiffViolation): number | undefine return undefined; } -function getNewViolationReportRow(violation: LintDiffViolation, compareSha: string): string { +function getNewViolationReportRow( + violation: LintDiffViolation, + githubRepoPath: string, + compareSha: string, +): string { const { level, code, message } = violation; if (level.toLowerCase() == "fatal") { // Fatal errors have fewer details and don't need to be formatted return `| ${iconFor(level)} ${code} | ${message} | ${violation.armRpcs?.join(", ")} |\n`; } - return `| ${iconFor(level)} [${code}](${getDocUrl(code)}) | ${message}
    Location: [${getPathSegment(relativizePath(getFile(violation)))}#L${getLine(violation)}](${getFileLink(compareSha, relativizePath(getFile(violation)), getLine(violation))}) | ${violation.armRpcs?.join(", ")} |\n`; + return `| ${iconFor(level)} [${code}](${getDocUrl(code)}) | ${message}
    Location: [${getPathSegment(relativizePath(getFile(violation)))}#L${getLine(violation)}](${getFileLink(githubRepoPath, compareSha, relativizePath(getFile(violation)), getLine(violation))}) | ${violation.armRpcs?.join(", ")} |\n`; } export function iconFor(type: string) { @@ -268,3 +290,10 @@ export function getPath(result: AutorestRunResult) { const readmePathRelative = relative(rootPath, readme.path); return tag ? `${readmePathRelative}#tag-${tag}` : readmePathRelative; } + +export function getAutoRestFailedMessage(result: AutorestRunResult | null): string { + if (result?.error) { + return "Autorest Failed"; + } + return ""; +} diff --git a/eng/tools/lint-diff/src/lint-diff.ts b/eng/tools/lint-diff/src/lint-diff.ts index 47a877e75263..f9af9cc72198 100644 --- a/eng/tools/lint-diff/src/lint-diff.ts +++ b/eng/tools/lint-diff/src/lint-diff.ts @@ -1,11 +1,11 @@ +import { SpecModelError } from "@azure-tools/specs-shared/spec-model-error"; +import { writeFile } from "node:fs/promises"; import { parseArgs, ParseArgsConfig } from "node:util"; -import { pathExists, getDependencyVersion, getPathToDependency } from "./util.js"; -import { getRunList } from "./processChanges.js"; -import { runChecks, getAutorestErrors } from "./runChecks.js"; import { correlateRuns } from "./correlateResults.js"; import { generateAutoRestErrorReport, generateLintDiffReport } from "./generateReport.js"; -import { writeFile } from "node:fs/promises"; -import { SpecModelError } from "@azure-tools/specs-shared/spec-model-error"; +import { getRunList } from "./processChanges.js"; +import { getAutorestErrors, runChecks } from "./runChecks.js"; +import { getDependencyVersion, getPathToDependency, pathExists } from "./util.js"; function usage() { console.log("TODO: Write up usage"); @@ -43,6 +43,11 @@ export async function main() { short: "m", default: "main", }, + "github-repo-path": { + type: "string", + short: "r", + default: process.env.GITHUB_REPOSITORY || "Azure/azure-rest-api-specs", + }, }, strict: true, }; @@ -55,6 +60,7 @@ export async function main() { "out-file": outFile, "base-branch": baseBranch, "compare-sha": compareSha, + "github-repo-path": githubRepoPath, }, } = parseArgs(config); @@ -92,6 +98,7 @@ export async function main() { outFile as string, baseBranch as string, compareSha as string, + githubRepoPath as string, ); } @@ -102,6 +109,7 @@ async function runLintDiff( outFile: string, baseBranch: string, compareSha: string, + githubRepoPath: string, ) { let beforeList, afterList, affectedSwaggers; try { @@ -111,7 +119,7 @@ async function runLintDiff( changedFilesPath, ); } catch (error) { - if (error instanceof SpecModelError) { + if (error instanceof SpecModelError) { console.log("\n\n"); console.log("❌ Error building Spec Model from changed file list:"); console.log(`${error}`); @@ -130,7 +138,7 @@ async function runLintDiff( return; } - if (afterList.size === 0) { + if (afterList.size === 0) { await writeFile(outFile, "No applicable files found in after. Exiting."); console.log("No applicable files found in after. Exiting."); return; @@ -138,9 +146,11 @@ async function runLintDiff( // It may be possible to run these in parallel as they're running against // different directories. + console.log("Running checks on before state..."); const beforeChecks = await runChecks(beforePath, beforeList); - const afterChecks = await runChecks(afterPath, afterList); + console.log("Running checks on after state..."); + const afterChecks = await runChecks(afterPath, afterList); // If afterChecks has AutoRest errors, fail the run. const autoRestErrors = afterChecks @@ -165,6 +175,7 @@ async function runLintDiff( outFile, baseBranch, compareSha, + githubRepoPath, ); if (!pass) { diff --git a/eng/tools/lint-diff/src/markdown-utils.ts b/eng/tools/lint-diff/src/markdown-utils.ts index 95e1913ec90a..b9a2d94d757a 100644 --- a/eng/tools/lint-diff/src/markdown-utils.ts +++ b/eng/tools/lint-diff/src/markdown-utils.ts @@ -1,7 +1,7 @@ -import { marked } from "marked"; -import { kebabCase } from "change-case"; -import axios from "axios"; import { Readme } from "@azure-tools/specs-shared/readme"; +import axios from "axios"; +import { kebabCase } from "change-case"; +import { marked } from "marked"; export enum MarkdownType { Arm = "arm", @@ -16,12 +16,13 @@ export enum MarkdownType { */ // TODO: Should this be placed in the Readme class? export async function getOpenapiType(readme: Readme): Promise { - const openapiType = (await readme.getGlobalConfig() as { "openapi-type"?: string })["openapi-type"]; + const openapiType = ((await readme.getGlobalConfig()) as { "openapi-type"?: string })[ + "openapi-type" + ]; if (openapiType && Object.values(MarkdownType).includes(openapiType as MarkdownType)) { return openapiType as MarkdownType; } - // Fallback, no openapi-type found in the file. Look at path to determine type // resource-manager: Arm // data-plane: DataPlane @@ -119,7 +120,7 @@ export async function getRelatedArmRpcFromDoc(ruleName: string): Promise { - const tag = (await readme.getGlobalConfig() as { tag?: string }).tag; +export async function getDefaultTag(readme: Readme): Promise { + const tag = ((await readme.getGlobalConfig()) as { tag?: string }).tag; return tag ? tag : ""; } diff --git a/eng/tools/lint-diff/src/processChanges.ts b/eng/tools/lint-diff/src/processChanges.ts index 6824683d5776..ea2400474d46 100644 --- a/eng/tools/lint-diff/src/processChanges.ts +++ b/eng/tools/lint-diff/src/processChanges.ts @@ -1,13 +1,13 @@ -import { join, relative, resolve, sep } from "path"; -import { readFile } from "fs/promises"; -import { pathExists } from "./util.js"; -import { specification, readme, swagger } from "@azure-tools/specs-shared/changed-files"; +import { readme, swagger } from "@azure-tools/specs-shared/changed-files"; import { SpecModel } from "@azure-tools/specs-shared/spec-model"; -import { ReadmeAffectedTags } from "./lintdiff-types.js"; import deepEqual from "deep-eql"; +import { readFile } from "fs/promises"; +import { join, relative, resolve, sep } from "path"; +import { ReadmeAffectedTags } from "./lintdiff-types.js"; +import { pathExists } from "./util.js"; -import { deduplicateTags } from "./markdown-utils.js"; import $RefParser from "@apidevtools/json-schema-ref-parser"; +import { deduplicateTags } from "./markdown-utils.js"; export async function getRunList( beforePath: string, @@ -19,12 +19,9 @@ export async function getRunList( // Read changed files, exclude any files that should be ignored const ignoreFilesWith = ["/examples/", "/quickstart-templates/", "/scenarios/"]; - const changedSpecFiles = (await readFileList(changedFilesPath)).filter((file) => { - // File is in specification/ folder - if (!specification(file)) { - return false; - } + // Changed files should already be filtered to the top-level "specification" folder (see lintdiff-code.yaml) + const changedSpecFiles = (await readFileList(changedFilesPath)).filter((file) => { // File is not ignored for (const ignore of ignoreFilesWith) { if (file.includes(ignore)) { @@ -107,7 +104,7 @@ export async function buildState( readme: (await specModel.getReadmes()).get(readmePath)!, changedTags: new Set(), }; - for (const [tagName,] of tags) { + for (const [tagName] of tags) { affectedTags.changedTags.add(tagName); } readmeTags.set(readmePath, affectedTags); @@ -118,11 +115,11 @@ export async function buildState( const changedFileAndTagsMap = new Map(); for (const [readmeFile, tags] of readmeTags.entries()) { const tagMap = await tags.readme.getTags(); - const tagsAndInputFiles = [...tags.changedTags].map(changedTag => { - return { + const tagsAndInputFiles = [...tags.changedTags].map((changedTag) => { + return { tagName: changedTag, inputFiles: [...tagMap.get(changedTag)!.inputFiles.keys()], - } + }; }); const dedupedTags = deduplicateTags(tagsAndInputFiles); @@ -135,9 +132,18 @@ export async function buildState( // For readme files that have changed but there are no affected swaggers, // add them to the map with no tags for (const changedReadme of existingChangedFiles.filter(readme)) { + const readmePath = resolve(rootPath, changedReadme); + + // Skip readme.md files that don't have "input-file:" as autorest cannot + // scan them. + const readmeContent = await readFile(readmePath, { encoding: "utf-8" }); + if (!readmeContent.includes("input-file:")) { + continue; + } + const service = specModels.get(getService(changedReadme))!; const readmes = await service.getReadmes(); - const readmeObject = readmes.get(resolve(rootPath, changedReadme))!; + const readmeObject = readmes.get(readmePath)!; if (!changedFileAndTagsMap.has(changedReadme)) { changedFileAndTagsMap.set(changedReadme, { diff --git a/eng/tools/lint-diff/src/runChecks.ts b/eng/tools/lint-diff/src/runChecks.ts index 887ec894c124..ec7cc6807197 100644 --- a/eng/tools/lint-diff/src/runChecks.ts +++ b/eng/tools/lint-diff/src/runChecks.ts @@ -1,11 +1,10 @@ -import { join } from "path"; -import { execNpmExec, isExecError, ExecError } from "@azure-tools/specs-shared/exec"; +import { ExecError, execNpmExec, isExecError } from "@azure-tools/specs-shared/exec"; import { debugLogger } from "@azure-tools/specs-shared/logger"; +import { join } from "path"; -import { getPathToDependency, isFailure } from "./util.js"; -import { AutoRestMessage, AutorestRunResult } from "./lintdiff-types.js"; -import { ReadmeAffectedTags } from "./lintdiff-types.js"; +import { AutoRestMessage, AutorestRunResult, ReadmeAffectedTags } from "./lintdiff-types.js"; import { getOpenapiType } from "./markdown-utils.js"; +import { getPathToDependency, isFailure } from "./util.js"; const MAX_EXEC_BUFFER = 64 * 1024 * 1024; diff --git a/eng/tools/lint-diff/test/correlateResults.test.ts b/eng/tools/lint-diff/test/correlateResults.test.ts index d5655c8cbcee..7f06982fe783 100644 --- a/eng/tools/lint-diff/test/correlateResults.test.ts +++ b/eng/tools/lint-diff/test/correlateResults.test.ts @@ -1,28 +1,28 @@ -import { test, describe, expect } from "vitest"; +import { describe, expect, test } from "vitest"; +import { Readme } from "@azure-tools/specs-shared/readme"; +import { resolve } from "path"; import { - AutorestRunResult, - LintDiffViolation, - Source, - BeforeAfter, -} from "../src/lintdiff-types.js"; -import { + arrayIsEqual, correlateRuns, - getViolations, getLintDiffViolations, - arrayIsEqual, getNewItems, + getViolations, isSameSources, } from "../src/correlateResults.js"; +import { + AutorestRunResult, + BeforeAfter, + LintDiffViolation, + Source, +} from "../src/lintdiff-types.js"; import { relativizePath } from "../src/util.js"; import { isWindows } from "./test-util.js"; -import { Readme } from "@azure-tools/specs-shared/readme"; -import { resolve } from "path"; -const __dirname = new URL('.', import.meta.url).pathname; +const __dirname = new URL(".", import.meta.url).pathname; describe.skipIf(isWindows())("correlateRuns", () => { - test("correlates before and after runs with matching readme and tag", async() => { + test("correlates before and after runs with matching readme and tag", async () => { const fixtureRoot = resolve(__dirname, "fixtures/correlateRuns"); const beforePath = resolve(fixtureRoot, "before"); const afterPath = resolve(fixtureRoot, "after"); @@ -31,7 +31,7 @@ describe.skipIf(isWindows())("correlateRuns", () => { { rootPath: beforePath, readme: new Readme( - resolve(beforePath, "specification/service1/resource-manager/readme.md") + resolve(beforePath, "specification/service1/resource-manager/readme.md"), ), tag: "tag1", stdout: "stdout", @@ -43,9 +43,7 @@ describe.skipIf(isWindows())("correlateRuns", () => { const afterChecks: AutorestRunResult[] = [ { rootPath: afterPath, - readme: new Readme( - resolve(afterPath, "specification/service1/resource-manager/readme.md") - ), + readme: new Readme(resolve(afterPath, "specification/service1/resource-manager/readme.md")), tag: "tag1", stdout: "stdout", stderr: "stderr", @@ -61,7 +59,7 @@ describe.skipIf(isWindows())("correlateRuns", () => { }); }); - test("correlates before and after runs with matching readme and a default tag", async() => { + test("correlates before and after runs with matching readme and a default tag", async () => { const fixtureRoot = resolve(__dirname, "fixtures/correlateRuns"); const beforePath = resolve(fixtureRoot, "before"); const afterPath = resolve(fixtureRoot, "after"); @@ -70,7 +68,7 @@ describe.skipIf(isWindows())("correlateRuns", () => { { rootPath: beforePath, readme: new Readme( - resolve(beforePath, "specification/service1/resource-manager/readme.md") + resolve(beforePath, "specification/service1/resource-manager/readme.md"), ), tag: "default-tag", stdout: "stdout", @@ -82,9 +80,7 @@ describe.skipIf(isWindows())("correlateRuns", () => { const afterChecks: AutorestRunResult[] = [ { rootPath: afterPath, - readme: new Readme( - resolve(afterPath, "specification/service1/resource-manager/readme.md") - ), + readme: new Readme(resolve(afterPath, "specification/service1/resource-manager/readme.md")), tag: "tag1", stdout: "stdout", stderr: "stderr", @@ -100,7 +96,7 @@ describe.skipIf(isWindows())("correlateRuns", () => { }); }); - test("correlates before and after runs with matching readme but no tag", async() => { + test("correlates before and after runs with matching readme but no tag", async () => { const fixtureRoot = resolve(__dirname, "fixtures/correlateRuns"); const beforePath = resolve(fixtureRoot, "before"); const afterPath = resolve(fixtureRoot, "after"); @@ -108,9 +104,7 @@ describe.skipIf(isWindows())("correlateRuns", () => { const afterChecks: AutorestRunResult[] = [ { rootPath: afterPath, - readme: new Readme( - resolve(afterPath, "specification/service1/resource-manager/readme.md") - ), + readme: new Readme(resolve(afterPath, "specification/service1/resource-manager/readme.md")), tag: "tag2", stdout: "stdout", stderr: "stderr", @@ -125,8 +119,8 @@ describe.skipIf(isWindows())("correlateRuns", () => { after: afterChecks[0], }); }); - - test("uses no baseline if there are no matching before checks", async() => { + + test("uses no baseline if there are no matching before checks", async () => { const fixtureRoot = resolve(__dirname, "fixtures/correlateRuns"); const beforePath = resolve(fixtureRoot, "before"); const afterPath = resolve(fixtureRoot, "after"); @@ -135,7 +129,7 @@ describe.skipIf(isWindows())("correlateRuns", () => { { rootPath: beforePath, readme: new Readme( - resolve(beforePath, "specification/service1/resource-manager/readme.md") + resolve(beforePath, "specification/service1/resource-manager/readme.md"), ), tag: "", stdout: "stdout", @@ -147,9 +141,7 @@ describe.skipIf(isWindows())("correlateRuns", () => { const afterChecks: AutorestRunResult[] = [ { rootPath: afterPath, - readme: new Readme( - resolve(afterPath, "specification/service1/resource-manager/readme.md") - ), + readme: new Readme(resolve(afterPath, "specification/service1/resource-manager/readme.md")), tag: "tag2", stdout: "stdout", stderr: "stderr", @@ -318,7 +310,9 @@ describe("getLintDiffViolations", async () => { } test("treats fatal errors as errors", () => { - const runResult = createRunResult(`{"pluginName":"spectral","extensionName":"@microsoft.azure/openapi-validator","level":"fatal","message":"openapiValidatorPluginFunc: Failed validating: TypeError: azure-openapi-validator/core/src/runner.ts/LintRunner.runRules/processRule error. ruleName: RequiredPropertiesMissingInResourceModel, specFilePath: file:///mnt/vss/_work/1/azure-rest-api-specs/specification/monitor/resource-manager/Microsoft.Insights/stable/2018-01-01/metrics_API.json, jsonPath: , errorName: TypeError, errorMessage: Cannot read properties of undefined (reading 'readOnly')"}`); + const runResult = createRunResult( + `{"pluginName":"spectral","extensionName":"@microsoft.azure/openapi-validator","level":"fatal","message":"openapiValidatorPluginFunc: Failed validating: TypeError: azure-openapi-validator/core/src/runner.ts/LintRunner.runRules/processRule error. ruleName: RequiredPropertiesMissingInResourceModel, specFilePath: file:///mnt/vss/_work/1/azure-rest-api-specs/specification/monitor/resource-manager/Microsoft.Insights/stable/2018-01-01/metrics_API.json, jsonPath: , errorName: TypeError, errorMessage: Cannot read properties of undefined (reading 'readOnly')"}`, + ); const violations = getLintDiffViolations(runResult); expect(violations.length).toEqual(1); @@ -345,18 +339,15 @@ describe("getLintDiffViolations", async () => { expect(violations[0].code).toEqual("ArmResourcePropertiesBag"); }); - test( - "returns an empty array on violations that don't have extensionname @microsoft.azure/openapi-validator", - () => { - const runResult = - createRunResult(`{"pluginName":"spectral","extensionName":"@microsoft.azure/openapi-validator","level":"information","message":"spectralPluginFunc: Validating OpenAPI spec. TypeSpec-generated: true. Path: 'file:///home/djurek/azure-rest-api-specs/specification/codesigning/resource-manager/Microsoft.CodeSigning/stable/2025-03-30/codeSigningAccount.json'"} + test("returns an empty array on violations that don't have extensionname @microsoft.azure/openapi-validator", () => { + const runResult = + createRunResult(`{"pluginName":"spectral","extensionName":"@microsoft.azure/openapi-validator","level":"information","message":"spectralPluginFunc: Validating OpenAPI spec. TypeSpec-generated: true. Path: 'file:///home/djurek/azure-rest-api-specs/specification/codesigning/resource-manager/Microsoft.CodeSigning/stable/2025-03-30/codeSigningAccount.json'"} {"pluginName":"spectral","extensionName":"THIS IS FILTERED OUT","level":"error","message":"Top level property names should not be repeated inside the properties bag for ARM resource 'CodeSigningAccount'. Properties [properties.sku] conflict with ARM top level properties. Please rename these.","code":"ArmResourcePropertiesBag","details":{"jsonpath":["definitions","CodeSigningAccount"],"validationCategory":"ARMViolation","providerNamespace":false,"resourceType":false,"range":{"start":{"line":1036,"column":27},"end":{"line":1051,"column":6}}},"source":[{"document":"file:///home/djurek/azure-rest-api-specs/specification/codesigning/resource-manager/Microsoft.CodeSigning/stable/2025-03-30/codeSigningAccount.json","position":{"line":1036,"column":5}}]} {"pluginName":"spectral","extensionName":"@microsoft.azure/openapi-validator","level":"information","message":"openapiValidatorPluginFunc: Return"}`); - const violations = getLintDiffViolations(runResult); - expect(violations).toEqual([]); - }, - ); + const violations = getLintDiffViolations(runResult); + expect(violations).toEqual([]); + }); test("returns a violation with code FATAL if the result.code is undefined", () => { const runResult = createRunResult( @@ -409,7 +400,6 @@ describe("arrayIsEqual", () => { }); }); - describe("getNewItems", () => { test("returns empty array when no before or after", () => { const before: LintDiffViolation[] = []; diff --git a/eng/tools/lint-diff/test/fixtures/buildState/specification/no-input-file/readme.md b/eng/tools/lint-diff/test/fixtures/buildState/specification/no-input-file/readme.md new file mode 100644 index 000000000000..38902d5bade1 --- /dev/null +++ b/eng/tools/lint-diff/test/fixtures/buildState/specification/no-input-file/readme.md @@ -0,0 +1,13 @@ +# Widget + +> see https://aka.ms/autorest +> This is the AutoRest configuration file for Widget. + +## Configuration + +Required if any services under this folder are RPaaS. + +```yaml +openapi-type: arm +openapi-subtype: rpaas +``` \ No newline at end of file diff --git a/eng/tools/lint-diff/test/generateReport.test.ts b/eng/tools/lint-diff/test/generateReport.test.ts index b552ce09500e..cb0f56a32394 100644 --- a/eng/tools/lint-diff/test/generateReport.test.ts +++ b/eng/tools/lint-diff/test/generateReport.test.ts @@ -1,8 +1,9 @@ -import { beforeEach, test, describe, expect, vi } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import { compareLintDiffViolations, generateAutoRestErrorReport, generateLintDiffReport, + getAutoRestFailedMessage, getDocUrl, getFile, getFileLink, @@ -11,11 +12,11 @@ import { iconFor, } from "../src/generateReport.js"; import { - Source, - LintDiffViolation, - BeforeAfter, - AutorestRunResult, AutoRestMessage, + AutorestRunResult, + BeforeAfter, + LintDiffViolation, + Source, } from "../src/lintdiff-types.js"; import { isWindows } from "./test-util.js"; @@ -28,8 +29,8 @@ vi.mock("node:fs/promises", async () => { }; }); -import { readFile } from "fs/promises"; import { Readme } from "@azure-tools/specs-shared/readme"; +import { readFile } from "fs/promises"; vi.mock("../src/util.js", async () => { const original = await vi.importActual("../src/util.js"); @@ -51,6 +52,36 @@ describe("iconFor", () => { }); }); +describe("getAutoRestFailedMessage", () => { + test("returns empty string when result is null", () => { + expect(getAutoRestFailedMessage(null)).toEqual(""); + }); + + test("returns empty string when result has no error", () => { + const result: AutorestRunResult = { + error: null, + rootPath: "", + readme: new Readme("file.md"), + tag: "default", + stdout: "", + stderr: "", + }; + expect(getAutoRestFailedMessage(result)).toEqual(""); + }); + + test("returns 'Autorest Failed' when result has an error", () => { + const result: AutorestRunResult = { + error: new Error("Autorest failed"), + rootPath: "", + readme: new Readme("file.md"), + tag: "default", + stdout: "", + stderr: "", + }; + expect(getAutoRestFailedMessage(result)).toEqual("Autorest Failed"); + }); +}); + describe("getLine", () => { test("returns the line number", () => { const violation = { @@ -147,16 +178,16 @@ describe("getDocUrl", () => { describe("getFileLink", () => { test("does not include #L if line is null", () => { - expect(getFileLink("abc123", "file.json", null)).not.toContain("#L"); + expect(getFileLink("repo/path", "abc123", "file.json", null)).not.toContain("#L"); }); test("includes #L if line is not null", () => { - expect(getFileLink("abc123", "file.json", 1)).toContain("#L1"); + expect(getFileLink("repo/path", "abc123", "file.json", 1)).toContain("#L1"); }); test("returns the correct link with preceeding forward slash", () => { - expect(getFileLink("abc123", "/file.json", 1)).toEqual( - "https://github.com/Azure/azure-rest-api-specs/blob/abc123/file.json#L1", + expect(getFileLink("repo/path", "abc123", "/file.json", 1)).toEqual( + "https://github.com/repo/path/blob/abc123/file.json#L1", ); }); }); @@ -387,19 +418,20 @@ describe("generateLintDiffReport", () => { outFile, "baseBranch", "compareSha", + "repo/path", ); expect(actual).toBe(false); expect(await readFile(outFile, { encoding: "utf-8" })).toMatchInlineSnapshot(` "| Compared specs ([v1.0.0](https://www.npmjs.com/package/@microsoft.azure/openapi-validator/v/1.0.0)) | new version | base version | | --- | --- | --- | - | default | [default](https://github.com/Azure/azure-rest-api-specs/blob/compareSha/file1.md) | [default](https://github.com/Azure/azure-rest-api-specs/blob/baseBranch/file1.md) | + | default | [default](https://github.com/repo/path/blob/compareSha/file1.md) | [default](https://github.com/repo/path/blob/baseBranch/file1.md) | **[must fix]The following errors/warnings are intorduced by current PR:** | Rule | Message | Related RPC [For API reviewers] | | ---- | ------- | ------------------------------- | - | :x: [SomeCode](https://github.com/Azure/azure-openapi-validator/blob/main/docs/some-code.md) | Some Message
    Location: [Azure.Contoso.WidgetManager/stable/2022-12-01/widgets.json#L1](https://github.com/Azure/azure-rest-api-specs/blob/compareSha/specification/contosowidgetmanager/data-plane/Azure.Contoso.WidgetManager/stable/2022-12-01/widgets.json#L1) | | + | :x: [SomeCode](https://github.com/Azure/azure-openapi-validator/blob/main/docs/some-code.md) | Some Message
    Location: [Azure.Contoso.WidgetManager/stable/2022-12-01/widgets.json#L1](https://github.com/repo/path/blob/compareSha/specification/contosowidgetmanager/data-plane/Azure.Contoso.WidgetManager/stable/2022-12-01/widgets.json#L1) | | " `); @@ -445,12 +477,13 @@ describe("generateLintDiffReport", () => { outFile, "baseBranch", "compareSha", + "repo/path", ); expect(actual).toBe(false); expect(await readFile(outFile, { encoding: "utf-8" })).toMatchInlineSnapshot(` "| Compared specs ([v1.0.0](https://www.npmjs.com/package/@microsoft.azure/openapi-validator/v/1.0.0)) | new version | base version | | --- | --- | --- | - | default | [default](https://github.com/Azure/azure-rest-api-specs/blob/compareSha/file1.md) | [default](https://github.com/Azure/azure-rest-api-specs/blob/baseBranch/file1.md) | + | default | [default](https://github.com/repo/path/blob/compareSha/file1.md) | [default](https://github.com/repo/path/blob/baseBranch/file1.md) | **[must fix]The following errors/warnings are intorduced by current PR:** @@ -463,6 +496,65 @@ describe("generateLintDiffReport", () => { `); }); + test.skipIf(isWindows())( + "passes and displays warning if before has errors", + async ({ expect }) => { + const afterViolation = { + extensionName: "@microsoft.azure/openapi-validator", + level: "warning", + code: "SomeCode", + message: "A warning occurred", + source: [], + details: {}, + }; + + const beforeResult = { + error: new Error("Autorest failed"), + stdout: "", + stderr: "", + rootPath: "", + readme: new Readme("file1.md"), + tag: "", + } as AutorestRunResult; + const afterResult = { + error: null, + stdout: JSON.stringify(afterViolation), + stderr: "", + rootPath: "", + readme: new Readme("file1.md"), + tag: "", + } as AutorestRunResult; + + const runCorrelations = new Map([ + ["file1.md", { before: beforeResult, after: afterResult }], + ]); + + const outFile = "test-output-fatal.md"; + const actual = await generateLintDiffReport( + runCorrelations, + new Set([ + "specification/contosowidgetmanager/data-plane/Azure.Contoso.WidgetManager/stable/2022-12-01/widgets.json", + ]), + outFile, + "baseBranch", + "compareSha", + "repo/path", + ); + expect(actual).toBe(true); + expect(await readFile(outFile, { encoding: "utf-8" })).toMatchInlineSnapshot(` + "| Compared specs ([v1.0.0](https://www.npmjs.com/package/@microsoft.azure/openapi-validator/v/1.0.0)) | new version | base version | + | --- | --- | --- | + | default | [default](https://github.com/repo/path/blob/compareSha/file1.md) | [default](https://github.com/repo/path/blob/baseBranch/file1.md) Autorest Failed| + + + > [!WARNING] + > Autorest failed checking before state of file1.md + + " + `); + }, + ); + test.skipIf(isWindows())( "passes if new violations do not include an error (warnings only)", async ({ expect }) => { @@ -511,20 +603,21 @@ describe("generateLintDiffReport", () => { outFile, "baseBranch", "compareSha", + "repo/path", ); expect(actual).toBe(true); expect(await readFile(outFile, { encoding: "utf-8" })).toMatchInlineSnapshot(` "| Compared specs ([v1.0.0](https://www.npmjs.com/package/@microsoft.azure/openapi-validator/v/1.0.0)) | new version | base version | | --- | --- | --- | - | default | [default](https://github.com/Azure/azure-rest-api-specs/blob/compareSha/file1.md) | [default](https://github.com/Azure/azure-rest-api-specs/blob/baseBranch/file1.md) | + | default | [default](https://github.com/repo/path/blob/compareSha/file1.md) | [default](https://github.com/repo/path/blob/baseBranch/file1.md) | **[must fix]The following errors/warnings are intorduced by current PR:** | Rule | Message | Related RPC [For API reviewers] | | ---- | ------- | ------------------------------- | - | :warning: [SomeCode](https://github.com/Azure/azure-openapi-validator/blob/main/docs/some-code.md) | Some Message
    Location: [Azure.Contoso.WidgetManager/stable/2022-12-01/widgets.json#L1](https://github.com/Azure/azure-rest-api-specs/blob/compareSha/specification/contosowidgetmanager/data-plane/Azure.Contoso.WidgetManager/stable/2022-12-01/widgets.json#L1) | | + | :warning: [SomeCode](https://github.com/Azure/azure-openapi-validator/blob/main/docs/some-code.md) | Some Message
    Location: [Azure.Contoso.WidgetManager/stable/2022-12-01/widgets.json#L1](https://github.com/repo/path/blob/compareSha/specification/contosowidgetmanager/data-plane/Azure.Contoso.WidgetManager/stable/2022-12-01/widgets.json#L1) | | " `); diff --git a/eng/tools/lint-diff/test/lint-diff.test.ts b/eng/tools/lint-diff/test/lint-diff.test.ts index 8a11bd01418f..c1c3ceca88c1 100644 --- a/eng/tools/lint-diff/test/lint-diff.test.ts +++ b/eng/tools/lint-diff/test/lint-diff.test.ts @@ -1,5 +1,5 @@ import { execa } from "execa"; -import { test, describe, expect } from "vitest"; +import { describe, expect, test } from "vitest"; // TODO: Actual tests describe("e2e", () => { diff --git a/eng/tools/lint-diff/test/markdown-utils.test.ts b/eng/tools/lint-diff/test/markdown-utils.test.ts index 2ab72ce31635..0e296f7d34da 100644 --- a/eng/tools/lint-diff/test/markdown-utils.test.ts +++ b/eng/tools/lint-diff/test/markdown-utils.test.ts @@ -1,17 +1,17 @@ -import { beforeEach, test, describe, vi, Mock, expect } from "vitest"; import { readFile } from "fs/promises"; import { join } from "node:path"; +import { Mock, beforeEach, describe, expect, test, vi } from "vitest"; import axios from "axios"; +import { Readme } from "@azure-tools/specs-shared/readme"; import { deduplicateTags, - getDocRawUrl, getDefaultTag, + getDocRawUrl, getOpenapiType, getRelatedArmRpcFromDoc, } from "../src/markdown-utils.js"; -import { Readme } from "@azure-tools/specs-shared/readme"; vi.mock("axios"); @@ -55,7 +55,7 @@ describe("getDocRawUrl", () => { describe("getDefaultTag", () => { test("returns default tag when there is a Basic Information header", async () => { const defaultTag = await getDefaultTag( - new Readme(join(__dirname, "fixtures/getDefaultTag/hasBasicInformation.md")) + new Readme(join(__dirname, "fixtures/getDefaultTag/hasBasicInformation.md")), ); expect(defaultTag).toEqual("package-2022-12-01"); @@ -63,7 +63,7 @@ describe("getDefaultTag", () => { test("returns default tag when there is no Basic Information header", async () => { const defaultTag = await getDefaultTag( - new Readme(join(__dirname, "fixtures/getDefaultTag/noBasicInformation.md")) + new Readme(join(__dirname, "fixtures/getDefaultTag/noBasicInformation.md")), ); expect(defaultTag).toEqual("package-2023-07-preview"); @@ -71,7 +71,7 @@ describe("getDefaultTag", () => { test("returns empty string when there is no default tag", async () => { const defaultTag = await getDefaultTag( - new Readme(join(__dirname, "fixtures/getDefaultTag/noDefaultTag.md")) + new Readme(join(__dirname, "fixtures/getDefaultTag/noDefaultTag.md")), ); expect(defaultTag).toEqual(""); diff --git a/eng/tools/lint-diff/test/processChanges.fs.test.ts b/eng/tools/lint-diff/test/processChanges.fs.test.ts index eab24dcc9027..67426b4399ef 100644 --- a/eng/tools/lint-diff/test/processChanges.fs.test.ts +++ b/eng/tools/lint-diff/test/processChanges.fs.test.ts @@ -1,12 +1,12 @@ -import { afterEach, vi, test, describe, expect } from "vitest"; import { vol } from "memfs"; +import { afterEach, describe, expect, test, vi } from "vitest"; import { readFileList } from "../src/processChanges.js"; // These tests are in a separate module because fs mocking is difficult to undo vi.mock("node:fs/promises", async () => { - const memfs = await vi.importActual("memfs") as typeof import("memfs"); + const memfs = (await vi.importActual("memfs")) as typeof import("memfs"); return { ...memfs.fs.promises, }; diff --git a/eng/tools/lint-diff/test/processChanges.test.ts b/eng/tools/lint-diff/test/processChanges.test.ts index 5fad4a37997f..7224e4978994 100644 --- a/eng/tools/lint-diff/test/processChanges.test.ts +++ b/eng/tools/lint-diff/test/processChanges.test.ts @@ -1,17 +1,17 @@ -import { test, describe, expect } from "vitest"; +import { describe, expect, test } from "vitest"; +import { ReadmeAffectedTags } from "../src/lintdiff-types.js"; import { + buildState, getAffectedServices, + getChangedSwaggers, getService, reconcileChangedFilesAndTags, - getChangedSwaggers, - buildState, } from "../src/processChanges.js"; -import { ReadmeAffectedTags } from "../src/lintdiff-types.js"; -import { isWindows } from "./test-util.js"; import { Readme } from "@azure-tools/specs-shared/readme"; import { resolve } from "node:path"; +import { isWindows } from "./test-util.js"; describe("getAffectedServices", () => { test.skipIf(isWindows())("returns single service with multiple files", async () => { @@ -249,4 +249,13 @@ describe("buildState", () => { ), ).not.toThrow(); }); + + test.skipIf(isWindows())("does not include readme files that has no input-file:", async () => { + const actual = await buildState( + ["specification/no-input-file/readme.md"], + "test/fixtures/buildState/", + ); + + expect(actual).toEqual([new Map(), []]); + }); }); diff --git a/eng/tools/lint-diff/test/runChecks.test.ts b/eng/tools/lint-diff/test/runChecks.test.ts index c46ae153ddfc..5dd16b881dfc 100644 --- a/eng/tools/lint-diff/test/runChecks.test.ts +++ b/eng/tools/lint-diff/test/runChecks.test.ts @@ -1,4 +1,4 @@ -import { vi, test, describe, beforeEach, expect, Mock } from "vitest"; +import { beforeEach, describe, expect, Mock, test, vi } from "vitest"; vi.mock(import("@azure-tools/specs-shared/exec"), async (importOriginal) => { const actual = await importOriginal(); @@ -24,11 +24,10 @@ vi.mock(import("../src/markdown-utils.js"), async (importOriginal) => { }; }); -import { runChecks, getAutorestErrors } from "../src/runChecks.js"; -import { AutorestRunResult } from "../src/lintdiff-types.js"; import { execNpmExec } from "@azure-tools/specs-shared/exec"; -import { ReadmeAffectedTags } from "../src/lintdiff-types.js"; import { Readme } from "@azure-tools/specs-shared/readme"; +import { AutorestRunResult, ReadmeAffectedTags } from "../src/lintdiff-types.js"; +import { getAutorestErrors, runChecks } from "../src/runChecks.js"; describe("runChecks", () => { beforeEach(() => { diff --git a/eng/tools/lint-diff/test/test-util.ts b/eng/tools/lint-diff/test/test-util.ts index 885288066313..e2a0313f3371 100644 --- a/eng/tools/lint-diff/test/test-util.ts +++ b/eng/tools/lint-diff/test/test-util.ts @@ -1,3 +1,3 @@ export function isWindows(): boolean { return process.platform === "win32"; -} \ No newline at end of file +} diff --git a/eng/tools/lint-diff/test/util.test.ts b/eng/tools/lint-diff/test/util.test.ts index 80e6fde73fff..8cda2857a072 100644 --- a/eng/tools/lint-diff/test/util.test.ts +++ b/eng/tools/lint-diff/test/util.test.ts @@ -1,7 +1,7 @@ -import { test, describe, vi, expect } from "vitest"; import { vol } from "memfs"; -import { pathExists, isFailure, isWarning } from "../src/util.js"; import { beforeEach } from "node:test"; +import { describe, expect, test, vi } from "vitest"; +import { isFailure, isWarning, pathExists } from "../src/util.js"; vi.mock("fs/promises", () => { const memfs = require("memfs"); diff --git a/eng/tools/lint-diff/tsconfig.json b/eng/tools/lint-diff/tsconfig.json index 4f763a74bf87..585f66d32d76 100644 --- a/eng/tools/lint-diff/tsconfig.json +++ b/eng/tools/lint-diff/tsconfig.json @@ -6,10 +6,6 @@ "noImplicitReturns": true, "allowJs": true, }, - "include": [ - "*.ts", - "src/**/*.ts", - "test/**/*.ts", - ], - "exclude": [ "*.test.ts" ] + "include": ["*.ts", "src/**/*.ts", "test/**/*.ts"], + "exclude": ["*.test.ts"], } diff --git a/eng/tools/oav-runner/README.md b/eng/tools/oav-runner/README.md new file mode 100644 index 000000000000..88f0a3f0e70a --- /dev/null +++ b/eng/tools/oav-runner/README.md @@ -0,0 +1,12 @@ +# `oav-runner` + +This is a simple wrapper script around the `oav` tool. It utilizes shared js code code modules from `.github/shared` to +determine a list of swagger specs that should be processed, processes them, then outputs necessary detailed run +information. + +## Invocation shortcuts + +``` +cd +npm ci && npm exec --no -- oav-runner <"specs"/"examples"> +``` diff --git a/eng/tools/oav-runner/cmd/oav-runner.js b/eng/tools/oav-runner/cmd/oav-runner.js new file mode 100755 index 000000000000..abdd7016b6cf --- /dev/null +++ b/eng/tools/oav-runner/cmd/oav-runner.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { main } from "../dist/src/cli.js"; + +await main(); diff --git a/eng/tools/oav-runner/package.json b/eng/tools/oav-runner/package.json new file mode 100644 index 000000000000..f5b18888189e --- /dev/null +++ b/eng/tools/oav-runner/package.json @@ -0,0 +1,33 @@ +{ + "name": "@azure-tools/oav-runner", + "private": true, + "type": "module", + "main": "dist/src/main.js", + "bin": { + "oav-runner": "cmd/oav-runner.js" + }, + "scripts": { + "build": "tsc --build", + "format": "prettier . --ignore-path ../.prettierignore --write", + "format:check": "prettier . --ignore-path ../.prettierignore --check", + "format:check:ci": "prettier . --ignore-path ../.prettierignore --check --log-level debug", + "test": "vitest", + "test:ci": "vitest run --coverage --reporter=verbose" + }, + "dependencies": { + "@azure-tools/specs-shared": "file:../../../.github/shared", + "js-yaml": "^4.1.0", + "oav": "^3.5.1", + "simple-git": "^3.27.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", + "typescript": "~5.8.2", + "vitest": "^3.0.7" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/eng/tools/oav-runner/src/cli.ts b/eng/tools/oav-runner/src/cli.ts new file mode 100644 index 000000000000..9e92b08867f9 --- /dev/null +++ b/eng/tools/oav-runner/src/cli.ts @@ -0,0 +1,111 @@ +#!/usr/bin/env node + +import { + outputAnnotatedErrors, + outputErrorSummary, + outputSuccessSummary, + ReportableOavError, +} from "./formatting.js"; +import { checkExamples, checkSpecs } from "./runner.js"; + +import fs from "node:fs/promises"; +import { parseArgs, ParseArgsConfig } from "node:util"; +import { resolve } from "path"; +import { simpleGit } from "simple-git"; + +export async function getRootFolder(inputPath: string): Promise { + try { + const gitRoot = await simpleGit(inputPath).revparse("--show-toplevel"); + return resolve(gitRoot.trim()); + } catch (error) { + console.error( + `Error: Unable to determine the root folder of the git repository.`, + `Please ensure you are running this command within a git repository OR providing a targeted directory that is within a git repo.`, + ); + process.exit(1); + } +} + +export async function main() { + const config: ParseArgsConfig = { + options: { + targetDirectory: { + type: "string", + short: "d", + multiple: false, + default: process.cwd(), + }, + fileList: { + type: "string", + short: "f", + multiple: false, + default: undefined, + }, + }, + allowPositionals: true, + }; + + const { values: opts, positionals } = parseArgs(config); + // this option has a default value of process.cwd(), so we can assume it is always defined + // just need to resolve that here to make ts aware of it + const targetDirectory = opts.targetDirectory as string; + + const resolvedGitRoot = await getRootFolder(targetDirectory); + + let fileList: string[] | undefined = undefined; + if (opts.fileList !== undefined) { + const fileListPath = resolve(opts.fileList as string); + try { + const fileContent = await fs.readFile(fileListPath, { encoding: "utf-8" }); + fileList = fileContent + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0); + console.log(`Loaded ${fileList.length} files from ${opts.fileList}`); + } catch (error) { + console.error( + `Error reading file list from ${opts.fileList}: ${error instanceof Error ? error.message : String(error)}`, + ); + console.error("User provided file list that is not found."); + console.error( + "Please ensure the file exists and is readable, or do not provide the option 'fileList'", + ); + process.exit(1); + } + } + + // first positional is runType + const [runType] = positionals; + + if (runType !== "specs" && runType !== "examples") { + console.error("Error: must be either 'specs' or 'examples'."); + process.exit(1); + } + + console.log(`Running oav-runner against ${runType} within ${resolvedGitRoot}.`); + + let exitCode = 0; + let scannedSwaggerFiles: string[] = []; + let errorList: ReportableOavError[] = []; + let reportName = ""; + + if (runType === "specs") { + [exitCode, scannedSwaggerFiles, errorList] = await checkSpecs(resolvedGitRoot, fileList); + reportName = "Swagger SemanticValidation"; + } else if (runType === "examples") { + [exitCode, scannedSwaggerFiles, errorList] = await checkExamples(resolvedGitRoot, fileList); + reportName = "Swagger ModelValidation"; + } + + if (errorList.length > 0) { + // print the errors so that they will annotate the files on github UI interface + outputAnnotatedErrors(errorList); + + // print the errors in a summary report that we can later output to + outputErrorSummary(errorList, reportName); + } else { + outputSuccessSummary(scannedSwaggerFiles, reportName); + } + + process.exit(exitCode); +} diff --git a/eng/tools/oav-runner/src/formatting.ts b/eng/tools/oav-runner/src/formatting.ts new file mode 100644 index 000000000000..49816944a3f4 --- /dev/null +++ b/eng/tools/oav-runner/src/formatting.ts @@ -0,0 +1,93 @@ +import { annotateFileError, setSummary } from "@azure-tools/specs-shared/error-reporting"; + +export interface ReportableOavError { + message: string; + file: string; + errorCode?: string; + line?: number; + column?: number; +} + +export function outputAnnotatedErrors(errors: ReportableOavError[]) { + errors.forEach((error) => { + let msg: string = `${error.message}`; + + if (error.errorCode) { + msg = `${error.errorCode}: ${msg}`; + } + + // we only attempt an in-place annotation if we have the line and column associated with the error + // otherwise we just depend upon the summary report to show the error + if (error.line && error.column) { + annotateFileError(error.file, msg, error.line, error.column); + } + }); +} + +export function outputSuccessSummary(swaggerFiles: string[], reportName: string) { + let builtLines: string[] = []; + + builtLines.push(`## All specifications passed ${reportName}`); + builtLines.push("| File | Status |"); + builtLines.push("| --- | --- |"); + for (const swaggerFile of swaggerFiles) { + builtLines.push(`| ${swaggerFile} | ✅ |`); + } + + const summaryResult = builtLines.join("\n"); + + if (process.env.GITHUB_STEP_SUMMARY) { + setSummary(summaryResult); + } else { + console.log(summaryResult); + } +} + +export function outputErrorSummary(errors: ReportableOavError[], reportName: string) { + let builtLines: string[] = []; + let checkName: string = ""; + + builtLines.push(`## Error Summary - ${reportName}`); + + // just mapping the report names we want to migrate to the old names here, so we don't have to pull it through everywhere when we want to change it + if (reportName === "Swagger SemanticValidation") { + checkName = "validate-spec"; + } else if (reportName === "Swagger ModelValidation") { + checkName = "validate-example"; + } + + builtLines.push(`⚠️ This check is testing a new version of '${reportName}'. ⚠️`); + builtLines.push( + "Failures are expected, and should be completely ignored by spec authors and reviewers.", + ); + builtLines.push(`Meaningful results for this PR are in required check '${reportName}'.`); + builtLines.push("| File | Line#Column | Code | Message |"); + builtLines.push("| --- | --- | --- | --- |"); + + // sort the errors by file name then by error code + errors.sort((a, b) => { + const nameCompare = a.file.localeCompare(b.file); + if (nameCompare !== 0) { + return nameCompare; + } + return (a.errorCode || "").localeCompare(b.errorCode || ""); + }); + + errors.forEach((error) => { + const fmtLineCol = error.line && error.column ? `${error.line}#${error.column}` : "N/A"; + builtLines.push(`| ${error.file} | ${fmtLineCol} | ${error.errorCode} | ${error.message} |`); + }); + + builtLines.push("\n"); + builtLines.push( + `> [!IMPORTANT]\n> Repro any individual file's worth of errors by invoking \`npx oav ${checkName} \` from the root of the rest-api-specs repo.`, + ); + + const summaryResult = builtLines.join("\n"); + + if (process.env.GITHUB_STEP_SUMMARY) { + setSummary(summaryResult); + } else { + console.log(summaryResult); + } +} diff --git a/eng/tools/oav-runner/src/runner.ts b/eng/tools/oav-runner/src/runner.ts new file mode 100644 index 000000000000..65286af5887c --- /dev/null +++ b/eng/tools/oav-runner/src/runner.ts @@ -0,0 +1,191 @@ +#!/usr/bin/env node + +import * as fs from "fs"; +import * as oav from "oav"; +import * as path from "path"; + +import { example, getChangedFiles, swagger } from "@azure-tools/specs-shared/changed-files"; //getChangedFiles, +import { Swagger } from "@azure-tools/specs-shared/swagger"; +import { ReportableOavError } from "./formatting.js"; + +export async function preCheckFiltering( + rootDirectory: string, + fileList?: string[], +): Promise { + const changedFiles = + fileList ?? (await getChangedFiles({ cwd: rootDirectory, paths: ["specification"] })); + + const swaggerFiles = await processFilesToSpecificationList(rootDirectory, changedFiles); + + console.log("oav-runner is checking the following specification rooted files:"); + swaggerFiles.forEach((file) => console.log(`- ${file}`)); + + return swaggerFiles; +} + +export async function checkExamples( + rootDirectory: string, + fileList?: string[], +): Promise<[number, string[], ReportableOavError[]]> { + let errors: ReportableOavError[] = []; + + const swaggerFiles = await preCheckFiltering(rootDirectory, fileList); + + for (const swaggerFile of swaggerFiles) { + try { + const errorResults = await oav.validateExamples(swaggerFile, undefined); + + for (const error of errorResults || []) { + errors.push({ + message: error.message, + errorCode: error.code, + file: error.exampleUrl, + line: error.examplePosition?.line, + column: error.examplePosition?.column, + } as ReportableOavError); + } + } catch (e) { + if (e instanceof Error) { + console.log(`Error validating examples for ${swaggerFile}: ${e.message}`); + errors.push({ + message: e.message, + file: swaggerFile, + } as ReportableOavError); + } else { + console.log(`Error validating examples for ${swaggerFile}: ${e}`); + errors.push({ + message: `Unhandled error validating ${swaggerFile}: ${e}`, + file: swaggerFile, + } as ReportableOavError); + } + } + } + + if (errors.length > 0) { + return [1, swaggerFiles, errors]; + } + return [0, swaggerFiles, []]; +} + +export async function checkSpecs( + rootDirectory: string, + fileList?: string[], +): Promise<[number, string[], ReportableOavError[]]> { + let errors: ReportableOavError[] = []; + + const swaggerFiles = await preCheckFiltering(rootDirectory, fileList); + + for (const swaggerFile of swaggerFiles) { + try { + const errorResults = await oav.validateSpec(swaggerFile, undefined); + if (errorResults.validateSpec && errorResults.validateSpec.errors) { + for (const error of errorResults.validateSpec.errors) { + errors.push({ + message: error.message, + errorCode: error.code, + file: swaggerFile, + line: error.position?.line, + column: error.position?.column, + } as ReportableOavError); + } + } + } catch (e) { + if (e instanceof Error) { + console.log(`Error validating ${swaggerFile}: ${e.message}`); + errors.push({ + message: e.message, + file: swaggerFile, + } as ReportableOavError); + } else { + console.log(`Error validating ${swaggerFile}: ${e}`); + errors.push({ + message: `Unhandled error validating ${swaggerFile}: ${e}`, + file: swaggerFile, + } as ReportableOavError); + } + } + } + + if (errors.length > 0) { + return [1, swaggerFiles, errors]; + } + return [0, swaggerFiles, []]; +} + +async function getFiles(rootDirectory: string, directory: string): Promise { + const target = path.join(rootDirectory, directory); + const items = await fs.promises.readdir(target, { + withFileTypes: true, + }); + + return items + .filter((d) => d.isFile() && d.name.endsWith(".json")) + .map((d) => path.join(target, d.name)) + .map((d) => d.replace(/^.*?(specification[\/\\].*)$/, "$1")) + .filter((d) => d.includes("specification" + path.sep)); +} + +export async function processFilesToSpecificationList( + rootDirectory: string, + files: string[], +): Promise { + const cachedSwaggerSpecs = new Map(); + const resultFiles: string[] = []; + const additionalSwaggerFiles: string[] = []; + + // files from get-changed-files are relative to the root of the repo, + // though that context is passed into this from cli arguments. + for (const file of files) { + const absoluteFilePath = path.join(rootDirectory, file); + + // if the file is an example, we need to find the swagger file that references it + if (example(file)) { + /* + examples exist in the same directory as the swagger file that references them: + + path/to/swagger/2024-01-01/examples/example.json <-- this is an example file path + path/to/swagger/2024-01-01/swagger.json <-- we need to identify this file if it references the example + path/to/swagger/2024-01-01/swagger2.json <-- and do nothing with this one + */ + const swaggerDir = path.dirname(path.dirname(file)); + + const visibleSwaggerFiles = await getFiles(rootDirectory, swaggerDir); + + for (const swaggerFile of visibleSwaggerFiles) { + if (!cachedSwaggerSpecs.has(swaggerFile)) { + const swaggerModel = new Swagger(path.join(rootDirectory, swaggerFile)); + try { + const exampleSwaggers = await swaggerModel.getExamples(); + const examples = [...exampleSwaggers.keys()]; + cachedSwaggerSpecs.set(swaggerFile, examples); + } catch (e) { + console.log( + `Error getting examples for ${swaggerFile}: ${e instanceof Error ? e.message : String(e)}`, + ); + // if we can't get the examples, we just skip this file + continue; + } + } + const referencedExamples = cachedSwaggerSpecs.get(swaggerFile); + + // the resolved files are absolute paths, so to compare them to the file we're looking at, we need + // to use the absolute path version of the example file. + if (referencedExamples?.indexOf(absoluteFilePath) !== -1) { + // unfortunately, we get lists of files in posix format from get-changed-files. because of this, when are are grabbing a + // resolved swagger file, we need to ensure we are using the posix version of the path as well. If we do not do this, + // if we change an example and a spec, we will end up resolving the changed spec twice, one with the posix path (from changed-files) + // and one with the windows path (resolved from the swagger model which we pulled refs from to determine which example belonged to which swagger) + additionalSwaggerFiles.push(swaggerFile.replace(/\\/g, "/")); + } + } + } + + // finally handle our base case where the file we're examining is itself a swagger file + if (swagger(file) && fs.existsSync(absoluteFilePath)) { + resultFiles.push(file); + } + } + + // combine and make the results unique + return Array.from(new Set([...resultFiles, ...additionalSwaggerFiles])); +} diff --git a/eng/tools/oav-runner/test/cli.test.ts b/eng/tools/oav-runner/test/cli.test.ts new file mode 100644 index 000000000000..710fd7a05e18 --- /dev/null +++ b/eng/tools/oav-runner/test/cli.test.ts @@ -0,0 +1,31 @@ +import path from "path"; +import { describe, expect, it, vi } from "vitest"; +import { getRootFolder } from "../src/cli.js"; + +const REPOROOT = path.resolve(__dirname, "..", "..", "..", ".."); + +describe("invocation directory checks", () => { + it("Should return the same path when invoked from the root of a git repo.", async () => { + const result = await getRootFolder(REPOROOT); + expect(result).toBe(REPOROOT); + }); + + it("Should return a higher path when invoked from a path deep in a git repo.", async () => { + const result = await getRootFolder(path.join(REPOROOT, "eng", "tools", "oav-runner")); + expect(result).toBe(REPOROOT); + }); + + it("Should exit with error when invoked outside of a git directory.", async () => { + const pathOutsideRepo = path.resolve(path.join(REPOROOT, "..")); + + const exitMock = vi + .spyOn(process, "exit") + .mockImplementation((code?: string | number | null | undefined) => { + throw new Error(`Exit ${code}`); + }); + + await expect(getRootFolder(pathOutsideRepo)).rejects.toThrow("Exit 1"); + + exitMock.mockRestore(); + }); +}); diff --git a/eng/tools/oav-runner/test/fixtures/specification/serviceA/resource-manager/service.A/readme.md b/eng/tools/oav-runner/test/fixtures/specification/serviceA/resource-manager/service.A/readme.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/eng/tools/oav-runner/test/fixtures/specification/serviceA/resource-manager/service.A/stable/2025-06-01/serviceAspec.json b/eng/tools/oav-runner/test/fixtures/specification/serviceA/resource-manager/service.A/stable/2025-06-01/serviceAspec.json new file mode 100644 index 000000000000..13b5261fe80e --- /dev/null +++ b/eng/tools/oav-runner/test/fixtures/specification/serviceA/resource-manager/service.A/stable/2025-06-01/serviceAspec.json @@ -0,0 +1,35 @@ +{ + "swagger": "2.0", + "info": { + "title": "Service A", + "version": "1.0.0" + }, + "paths": { + "/c": { + "get": { + "summary": "Get A", + "responses": { + "200": { + "description": "Successful response", + "schema": { + "$ref": "#/definitions/C" + } + } + } + } + } + }, + "definitions": { + "C": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + } + } +} diff --git a/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/readme.md b/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/readme.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/CreateResource.json b/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/CreateResource.json new file mode 100644 index 000000000000..e1388a21c597 --- /dev/null +++ b/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/CreateResource.json @@ -0,0 +1,27 @@ +{ + "parameters": { + "api-version": "2025-06-01", + "resource": { + "name": "New Resource", + "properties": { + "description": "A new resource created via the API", + "tags": ["test", "new", "sample"] + } + } + }, + "responses": { + "201": { + "body": { + "id": "resource-456", + "name": "New Resource", + "type": "ServiceB/Resource", + "properties": { + "description": "A new resource created via the API", + "tags": ["test", "new", "sample"], + "status": "Provisioning", + "createdAt": "2025-06-02T10:30:00Z" + } + } + } + } +} diff --git a/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/DeleteResource.json b/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/DeleteResource.json new file mode 100644 index 000000000000..2d92d40d1c19 --- /dev/null +++ b/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/DeleteResource.json @@ -0,0 +1,9 @@ +{ + "parameters": { + "api-version": "2025-06-01", + "resourceId": "resource-123" + }, + "responses": { + "204": {} + } +} diff --git a/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/GetResource.json b/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/GetResource.json new file mode 100644 index 000000000000..9187a08b00a6 --- /dev/null +++ b/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/GetResource.json @@ -0,0 +1,20 @@ +{ + "parameters": { + "api-version": "2025-06-01", + "resourceId": "resource-123" + }, + "responses": { + "200": { + "body": { + "id": "resource-123", + "name": "Example Resource", + "type": "ServiceB/Resource", + "properties": { + "status": "Active", + "createdAt": "2025-05-30T15:30:45Z", + "lastModifiedAt": "2025-06-01T09:15:22Z" + } + } + } + } +} diff --git a/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/GetRoot.json b/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/GetRoot.json new file mode 100644 index 000000000000..c0dcb5926703 --- /dev/null +++ b/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/GetRoot.json @@ -0,0 +1,14 @@ +{ + "parameters": { + "api-version": "2025-06-01" + }, + "responses": { + "200": { + "body": { + "status": "OK", + "message": "Service is running", + "version": "1.0.0" + } + } + } +} diff --git a/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/ListResources.json b/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/ListResources.json new file mode 100644 index 000000000000..d0d3cfbc0a5d --- /dev/null +++ b/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/ListResources.json @@ -0,0 +1,34 @@ +{ + "parameters": { + "api-version": "2025-06-01", + "$skip": 0, + "$top": 10 + }, + "responses": { + "200": { + "body": { + "value": [ + { + "id": "resource-123", + "name": "Example Resource", + "type": "ServiceB/Resource", + "properties": { + "status": "Active", + "createdAt": "2025-05-30T15:30:45Z" + } + }, + { + "id": "resource-456", + "name": "New Resource", + "type": "ServiceB/Resource", + "properties": { + "status": "Provisioning", + "createdAt": "2025-06-02T10:30:00Z" + } + } + ], + "nextLink": "https://service.b/api/resources?api-version=2025-06-01&$skip=10&$top=10" + } + } + } +} diff --git a/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/stable/2025-06-01/serviceBspec.json b/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/stable/2025-06-01/serviceBspec.json new file mode 100644 index 000000000000..f354591eaba6 --- /dev/null +++ b/eng/tools/oav-runner/test/fixtures/specification/serviceB/data-plane/service.B/stable/2025-06-01/serviceBspec.json @@ -0,0 +1,409 @@ +{ + "swagger": "2.0", + "info": { + "title": "Service B", + "version": "1.0.0", + "description": "API for Service B data plane operations" + }, + "host": "service.b", + "schemes": ["https"], + "consumes": ["application/json"], + "produces": ["application/json"], + "paths": { + "/": { + "get": { + "tags": ["Status"], + "summary": "Get Service Status", + "description": "Returns the current status of the service.", + "operationId": "Service_GetStatus", + "parameters": [ + { + "$ref": "#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "OK - Returns service status information.", + "schema": { + "$ref": "#/definitions/ServiceStatus" + } + }, + "default": { + "description": "Error response", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "Get service status": { + "$ref": "./examples/GetRoot.json" + } + } + } + }, + "/resources": { + "get": { + "tags": ["Resources"], + "summary": "List Resources", + "description": "Lists all resources in the service.", + "operationId": "Resources_List", + "parameters": [ + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "name": "$skip", + "in": "query", + "description": "Skip the first n items", + "type": "integer", + "default": 0, + "minimum": 0 + }, + { + "name": "$top", + "in": "query", + "description": "Return only the first n items", + "type": "integer", + "default": 10, + "minimum": 1, + "maximum": 100 + } + ], + "responses": { + "200": { + "description": "OK - Returns a list of resources", + "schema": { + "$ref": "#/definitions/ResourceList" + } + }, + "default": { + "description": "Error response", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "List resources": { + "$ref": "./examples/ListResources.json" + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + }, + "post": { + "tags": ["Resources"], + "summary": "Create a Resource", + "description": "Creates a new resource in the service.", + "operationId": "Resources_Create", + "parameters": [ + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "name": "resource", + "in": "body", + "description": "Resource to create", + "required": true, + "schema": { + "$ref": "#/definitions/ResourceCreateRequest" + } + } + ], + "responses": { + "201": { + "description": "Created - Returns the created resource", + "schema": { + "$ref": "#/definitions/Resource" + } + }, + "default": { + "description": "Error response", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "Create resource": { + "$ref": "./examples/CreateResource.json" + } + } + } + }, + "/resources/{resourceId}": { + "get": { + "tags": ["Resources"], + "summary": "Get Resource", + "description": "Gets a specific resource by its ID.", + "operationId": "Resources_Get", + "parameters": [ + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "name": "resourceId", + "in": "path", + "description": "ID of the resource to retrieve", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK - Returns the requested resource", + "schema": { + "$ref": "#/definitions/Resource" + } + }, + "404": { + "description": "Not Found - The resource does not exist", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "default": { + "description": "Error response", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "Get resource": { + "$ref": "./examples/GetResource.json" + } + } + }, + "delete": { + "tags": ["Resources"], + "summary": "Delete Resource", + "description": "Deletes a specific resource by its ID.", + "operationId": "Resources_Delete", + "parameters": [ + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "name": "resourceId", + "in": "path", + "description": "ID of the resource to delete", + "required": true, + "type": "string" + } + ], + "responses": { + "204": { + "description": "No Content - The resource was successfully deleted" + }, + "404": { + "description": "Not Found - The resource does not exist", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "default": { + "description": "Error response", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "Delete resource": { + "$ref": "./examples/DeleteResource.json" + } + } + } + } + }, + "definitions": { + "ServiceStatus": { + "description": "Represents the status of the service", + "type": "object", + "properties": { + "status": { + "description": "The status of the service", + "type": "string", + "enum": ["OK", "Degraded", "Unavailable"], + "x-ms-enum": { + "name": "ServiceStatusEnum", + "modelAsString": true + } + }, + "message": { + "description": "A message providing details about the service status", + "type": "string" + }, + "version": { + "description": "The version of the service", + "type": "string" + } + } + }, + "Resource": { + "description": "Represents a resource in Service B", + "type": "object", + "properties": { + "id": { + "description": "The unique identifier of the resource", + "type": "string", + "readOnly": true + }, + "name": { + "description": "The name of the resource", + "type": "string" + }, + "type": { + "description": "The type of the resource", + "type": "string", + "readOnly": true + }, + "properties": { + "description": "The properties of the resource", + "type": "object", + "properties": { + "description": { + "description": "Description of the resource", + "type": "string" + }, + "status": { + "description": "The status of the resource", + "type": "string", + "enum": ["Active", "Inactive", "Provisioning", "Failed"], + "x-ms-enum": { + "name": "ResourceStatusEnum", + "modelAsString": true + } + }, + "tags": { + "description": "Tags associated with the resource", + "type": "array", + "items": { + "type": "string" + } + }, + "createdAt": { + "description": "The timestamp when the resource was created", + "type": "string", + "format": "date-time", + "readOnly": true + }, + "lastModifiedAt": { + "description": "The timestamp when the resource was last modified", + "type": "string", + "format": "date-time", + "readOnly": true + } + } + } + } + }, + "ResourceCreateRequest": { + "description": "Request body for creating a resource", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "description": "The name of the resource", + "type": "string" + }, + "properties": { + "description": "The properties of the resource", + "type": "object", + "properties": { + "description": { + "description": "Description of the resource", + "type": "string" + }, + "tags": { + "description": "Tags associated with the resource", + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "ResourceList": { + "description": "A paged list of resources", + "type": "object", + "properties": { + "value": { + "description": "The list of resources", + "type": "array", + "items": { + "$ref": "#/definitions/Resource" + } + }, + "nextLink": { + "description": "The URL to get the next set of results, if there are any", + "type": "string" + } + } + }, + "ErrorResponse": { + "description": "Error response", + "type": "object", + "properties": { + "error": { + "description": "The error details", + "type": "object", + "properties": { + "code": { + "description": "Error code", + "type": "string" + }, + "message": { + "description": "Error message", + "type": "string" + }, + "target": { + "description": "Error target", + "type": "string" + }, + "details": { + "description": "Error details", + "type": "array", + "items": { + "$ref": "#/definitions/ErrorDetail" + } + } + } + } + } + }, + "ErrorDetail": { + "description": "Error detail", + "type": "object", + "properties": { + "code": { + "description": "Error code", + "type": "string" + }, + "message": { + "description": "Error message", + "type": "string" + }, + "target": { + "description": "Error target", + "type": "string" + } + } + } + }, + "parameters": { + "ApiVersionParameter": { + "name": "api-version", + "in": "query", + "description": "The API version to use for this operation", + "required": true, + "type": "string", + "default": "2025-06-01" + } + } +} diff --git a/eng/tools/oav-runner/test/runner.test.ts b/eng/tools/oav-runner/test/runner.test.ts new file mode 100644 index 000000000000..13c1067c059b --- /dev/null +++ b/eng/tools/oav-runner/test/runner.test.ts @@ -0,0 +1,73 @@ +import path from "path"; +import { describe, expect, it } from "vitest"; +import { processFilesToSpecificationList } from "../src/runner.js"; + +const ROOT = path.resolve(__dirname, "..", "test", "fixtures"); + +describe("file processing", () => { + it("should process a basic set of files and return a list of swagger files only", async () => { + const changedFiles = [ + "specification/serviceB/data-plane/service.B/stable/2025-06-01/serviceBspec.json", + "specification/serviceB/data-plane/service.B/readme.md", + ]; + const expected = [ + "specification/serviceB/data-plane/service.B/stable/2025-06-01/serviceBspec.json", + ]; + + const result = await processFilesToSpecificationList(ROOT, changedFiles); + expect(result).toEqual(expected); + }); + + it("should process a larger set of files and return a list of expected resolved swagger files", async () => { + const changedFiles = [ + "specification/serviceA/resource-manager/service.A/stable/2025-06-01/serviceAspec.json", + "specification/serviceB/data-plane/service.B/stable/2025-06-01/serviceBspec.json", + "specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/CreateResource.json", + "specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/DeleteResource.json", + "specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/GetResource.json", + "specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/GetRoot.json", + "specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/ListResources.json", + ]; + const expected = [ + "specification/serviceA/resource-manager/service.A/stable/2025-06-01/serviceAspec.json", + "specification/serviceB/data-plane/service.B/stable/2025-06-01/serviceBspec.json", + ]; + + const result = await processFilesToSpecificationList(ROOT, changedFiles); + expect(result).toEqual(expected); + }); + + it("should process the correct swagger file given only changed example files", async () => { + const changedFiles = [ + "specification/serviceB/data-plane/service.B/stable/2025-06-01/examples/CreateResource.json", + ]; + const expected = [ + "specification/serviceB/data-plane/service.B/stable/2025-06-01/serviceBspec.json", + ]; + + const result = await processFilesToSpecificationList(ROOT, changedFiles); + expect(result).toEqual(expected); + }); + + it("should process the correct swagger file given only changed readme file", async () => { + const changedFiles = ["specification/serviceB/data-plane/service.B/readme.md"]; + const expected: string[] = []; + + const result = await processFilesToSpecificationList(ROOT, changedFiles); + expect(result).toEqual(expected); + }); + + it("should handle deleted files without error", async () => { + const changedFiles = [ + "specification/serviceB/data-plane/service.B/stable/2025-06-01/serviceBspec.json", + // non-existent file. Should not throw and quietly omit + "specification/serviceB/data-plane/service.B/stable/2025-06-01/serviceBspecDeleted.json", + ]; + const expected = [ + "specification/serviceB/data-plane/service.B/stable/2025-06-01/serviceBspec.json", + ]; + + const result = await processFilesToSpecificationList(ROOT, changedFiles); + expect(result).toEqual(expected); + }); +}); diff --git a/eng/tools/oav-runner/tsconfig.json b/eng/tools/oav-runner/tsconfig.json new file mode 100644 index 000000000000..5f48d4c6a5b5 --- /dev/null +++ b/eng/tools/oav-runner/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": ".", + "allowJs": true, + }, + "include": ["*.ts", "src/**/*.ts", "test/**/*.ts"], +} diff --git a/eng/tools/openapi-diff-runner/README.md b/eng/tools/openapi-diff-runner/README.md new file mode 100644 index 000000000000..6e08744f8640 --- /dev/null +++ b/eng/tools/openapi-diff-runner/README.md @@ -0,0 +1,98 @@ +# OpenAPI Diff Runner + +A tool for detecting breaking changes in OpenAPI specifications by comparing different versions and analyzing the +differences using @azure/oad library. + +## Overview + +The OpenAPI Diff Runner is designed to: + +- Compare OpenAPI specifications between different versions +- Generate detailed reports of comparing result +- Support both same-version and cross-version breaking change detection +- Integrate with GitHub workflow for automated validation + +## Installation + +```bash +# Install dependencies +npm ci + +# Build the project +npm run build +``` + +## Usage + +### Command Line Interface + +```bash +# Basic usage +npx openapi-diff-runner --srp --repo --number + +# Example +npx openapi-diff-runner \ + --srp /path/to/azure-rest-api-specs \ + --repo Azure/azure-rest-api-specs \ + --number 12345 \ + --bb main \ + --rt SameVersion +``` + +### Command Line Options + +| Option | Description | Default | +| ---------- | ----------------------------------- | ---------------------------- | +| `--srp` | Spec repository path | `../` | +| `--repo` | GitHub repository | `azure/azure-rest-api-specs` | +| `--number` | Pull request number | Required | +| `--bb` | Base branch | `main` | +| `--rt` | Run type (SameVersion/CrossVersion) | `SameVersion` | +| `--hc` | Head commit | `HEAD` | +| `--sb` | Source branch | From PR | +| `--tb` | Target branch | From PR | + +## Breaking Change Types + +### Same Version Breaking Changes + +- Changes within the same API version that break backward compatibility +- Examples: Removing properties, changing required fields, modifying response schemas + +### Cross Version Breaking Changes + +- Changes between different API versions +- Helps ensure proper versioning and migration paths + +### Workflow + +1. **Initialize Context**: Parse command line arguments and setup environment +2. **Setup PR Info**: Fetch pull request details and prepare Git workspace +3. **Detect Changes**: Use OAD (OpenAPI Analysis) to compare specifications +4. **Apply Rules**: Process detected changes through rule engine +5. **Generate Report**: Create detailed output with violations and recommendations + +## Development + +### Prerequisites + +- Node.js >= 20.0.0 +- npm +- .NET 6 +- Git + +### Building + +```bash +# Build TypeScript files +npm run build + +# Run tests +npm test + +# Run tests with coverage +npm run test:ci + +# Lint code +npm run prettier +``` diff --git a/eng/tools/openapi-diff-runner/cmd/openapi-diff-runner.js b/eng/tools/openapi-diff-runner/cmd/openapi-diff-runner.js new file mode 100755 index 000000000000..e2a37e0b5491 --- /dev/null +++ b/eng/tools/openapi-diff-runner/cmd/openapi-diff-runner.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { main } from "../dist/src/index.js"; + +await main(); diff --git a/eng/tools/openapi-diff-runner/package.json b/eng/tools/openapi-diff-runner/package.json new file mode 100644 index 000000000000..e12cb0555d0a --- /dev/null +++ b/eng/tools/openapi-diff-runner/package.json @@ -0,0 +1,32 @@ +{ + "name": "@azure-tools/openapi-diff-runner", + "private": true, + "type": "module", + "main": "dist/src/index.js", + "bin": { + "openapi-diff-runner": "cmd/openapi-diff-runner.js" + }, + "scripts": { + "build": "tsc --build", + "format": "prettier . --ignore-path ../.prettierignore --write", + "format:check": "prettier . --ignore-path ../.prettierignore --check", + "format:check:ci": "prettier . --ignore-path ../.prettierignore --check --log-level debug", + "test": "vitest", + "test:ci": "vitest --coverage --reporter=verbose" + }, + "engines": { + "node": ">=20.0.0" + }, + "dependencies": { + "@azure-tools/specs-shared": "file:../../../.github/shared", + "@azure/oad": "0.10.14" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@vitest/coverage-v8": "^3.0.7", + "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", + "typescript": "~5.8.2", + "vitest": "^3.0.7" + } +} diff --git a/eng/tools/openapi-diff-runner/src/command-helpers.ts b/eng/tools/openapi-diff-runner/src/command-helpers.ts new file mode 100644 index 000000000000..b913b72f300d --- /dev/null +++ b/eng/tools/openapi-diff-runner/src/command-helpers.ts @@ -0,0 +1,270 @@ +import { BREAKING_CHANGES_CHECK_TYPES } from "@azure-tools/specs-shared/breaking-change"; +import { getChangedFilesStatuses, swagger } from "@azure-tools/specs-shared/changed-files"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { logError, LogLevel, logMessage, setOutput } from "./log.js"; +import { + BreakingChangeReviewRequiredLabel, + BreakingChangesCheckType, + Context, + VersioningReviewRequiredLabel, +} from "./types/breaking-change.js"; +import { ResultMessageRecord } from "./types/message.js"; +import { createOadMessageProcessor } from "./utils/oad-message-processor.js"; +import { createPullRequestProperties } from "./utils/pull-request.js"; + +/** + * Interface for parsed CLI arguments + */ +export interface ParsedCliArguments { + localSpecRepoPath: string; + targetRepo: string; + sourceRepo: string; + prNumber: string; + runType: BreakingChangesCheckType; + baseBranch: string; + headCommit: string; + prSourceBranch: string; + prTargetBranch: string; +} + +/** + * Create context from parsed CLI arguments + */ +export function createContextFromParsedArgs( + parsedArgs: ParsedCliArguments, + workingFolder: string, + logFileFolder: string, +): Context { + const swaggerDirs: string[] = ["specification", "dev"]; + const prUrl = `https://github.com/${parsedArgs.targetRepo}/pull/${parsedArgs.prNumber}`; + const oadMessageProcessorContext = createOadMessageProcessor(logFileFolder, prUrl); + + return { + localSpecRepoPath: parsedArgs.localSpecRepoPath, + workingFolder, + swaggerDirs, + logFileFolder, + baseBranch: parsedArgs.baseBranch, + runType: parsedArgs.runType, + checkName: getBreakingChangeCheckName(parsedArgs.runType), + headCommit: parsedArgs.headCommit, + targetRepo: parsedArgs.targetRepo, + sourceRepo: parsedArgs.sourceRepo, + prNumber: parsedArgs.prNumber, + prSourceBranch: parsedArgs.prSourceBranch, + prTargetBranch: parsedArgs.prTargetBranch, + oadMessageProcessorContext, + prUrl, + }; +} + +/** + * This set contains labels denoting which kind of review is required. + * + * Appropriate labels are added to this set by applyRules() function. + */ +export const BreakingChangeLabelsToBeAdded = new Set(); +function getBreakingChangeCheckName(runType: BreakingChangesCheckType): string { + return runType === BREAKING_CHANGES_CHECK_TYPES.SAME_VERSION + ? "Swagger BreakingChange" + : "BreakingChange(Cross-Version)"; +} + +/** + * Output the breaking change labels as GitHub Actions environment variables. + * This function checks the BreakingChangeLabelsToBeAdded set and sets the appropriate outputs. + */ +export function outputBreakingChangeLabelVariables(): void { + // Output the breaking change labels as GitHub Actions environment variables + if (BreakingChangeLabelsToBeAdded.size === 0) { + logMessage("None of the breaking change review labels need to be added."); + logMessage("Setting default breaking change labels to false."); + setOutput("breakingChangeReviewLabelName", BreakingChangeReviewRequiredLabel); + setOutput("breakingChangeReviewLabelValue", "false"); + setOutput("versioningReviewLabelName", VersioningReviewRequiredLabel); + setOutput("versioningReviewLabelValue", "false"); + } else { + if (BreakingChangeLabelsToBeAdded.has(BreakingChangeReviewRequiredLabel)) { + logMessage("'BreakingChangeReviewRequired' label needs to be added."); + setOutput("breakingChangeReviewLabelName", BreakingChangeReviewRequiredLabel); + setOutput("breakingChangeReviewLabelValue", "true"); + } else { + logMessage("'BreakingChangeReviewRequired' label needs to be deleted."); + setOutput("breakingChangeReviewLabelName", BreakingChangeReviewRequiredLabel); + setOutput("breakingChangeReviewLabelValue", "false"); + } + if (BreakingChangeLabelsToBeAdded.has(VersioningReviewRequiredLabel)) { + logMessage("'VersioningReviewRequired' label needs to be added."); + setOutput("versioningReviewLabelName", VersioningReviewRequiredLabel); + setOutput("versioningReviewLabelValue", "true"); + } else { + logMessage("'VersioningReviewRequired' label needs to be deleted."); + setOutput("versioningReviewLabelName", VersioningReviewRequiredLabel); + setOutput("versioningReviewLabelValue", "false"); + } + } +} + +/** + * Get categorized changed files by calling the shared getCategorizedChangedFiles function. + * Filters results to only include Swagger/OpenAPI files using the swagger filter from changed-files.js + * @param options - Options for getting changed files + * @param options.baseCommitish - Base commit to compare from (default: "HEAD^") + * @param options.cwd - Current working directory (default: process.cwd()) + * @param options.headCommitish - Head commit to compare to (default: "HEAD") + * @returns Promise resolving to categorized changed files filtered for Swagger files only + */ +export async function getSwaggerDiffs( + options: { + baseCommitish?: string; + cwd?: string; + headCommitish?: string; + } = {}, +): Promise<{ + additions: string[]; + modifications: string[]; + deletions: string[]; + total: number; +}> { + try { + // Call the function with compatible options + const result = await getChangedFilesStatuses({ + baseCommitish: options.baseCommitish, + cwd: options.cwd, + headCommitish: options.headCommitish, + paths: ["specification"], + }); + + // Filter each array to only include Swagger files using the swagger filter from changed-files.js + const filteredAdditions = result.additions.filter(swagger); + const filteredModifications = result.modifications.filter(swagger); + const filteredDeletions = result.deletions.filter(swagger); + const filteredRenames = result.renames.filter( + (rename) => swagger(rename.from) && swagger(rename.to), + ); + + // Add renamed files to the additions array and deletions array + filteredAdditions.push(...filteredRenames.map((rename) => rename.to)); + filteredDeletions.push(...filteredRenames.map((rename) => rename.from)); + + return { + additions: filteredAdditions, + modifications: filteredModifications, + deletions: filteredDeletions, + total: filteredAdditions.length + filteredModifications.length + filteredDeletions.length, + }; + } catch (error) { + logError(`Error getting categorized changed files: ${error}`); + // Return empty result on error + return { + additions: [], + modifications: [], + deletions: [], + total: 0, + }; + } +} + +/** + * NOTE: For base branch which not in targetBranches, the breaking change tool compare head branch with master branch. + * TargetBranches is a set of branches and treat each of them like a service team master branch. + */ +export async function buildPrInfo(context: Context): Promise { + const prInfo = await createPullRequestProperties( + context, + context.runType === "CrossVersion" ? "cross-version" : "same-version", + ); + if (!prInfo || !prInfo.targetBranch) { + throw new Error("create PR failed!"); + } + context.prInfo = prInfo; +} + +// Constants and state for dummy swagger management +const whitelistsBranches = ["ARMCoreRPDev", "rpsaasmaster"]; +const createdDummySwagger: string[] = []; + +/** + * Change the base branch for comparison based on context and whitelist rules + */ +export function changeBaseBranch(context: Context): void { + /* + * always compare against main + * we still use the changed files got from the PR, because the main branch may quite different with the PR target branch + */ + function isBreakingChangeWhiteListBranch() { + return ( + isSameVersionBreakingType(context.runType) && + whitelistsBranches.some((b) => context.prTargetBranch.toLowerCase() === b.toLowerCase()) + ); + } + // same version breaking change for PR targets to rpaas or armCoreRpDev, will compare with the original target branch. + if (context.baseBranch !== context.prTargetBranch && !isBreakingChangeWhiteListBranch()) { + context.prInfo!.baseBranch = context.baseBranch; + logMessage(`switch target branch to ${context.baseBranch}`); + } +} + +/** + * Log the full list of OAD messages to console + */ +export function logFullOadMessagesList(msgs: ResultMessageRecord[]): void { + logMessage("---- Full list of messages ----", LogLevel.Group); + logMessage("["); + // Printing the messages one by one because the console.log appears to elide the messages with "... X more items" + // after approximately 292 messages. + for (const msg of msgs) { + logMessage(JSON.stringify(msg, null, 4) + ","); + } + logMessage("]"); + logMessage("---- End of full list of messages ----", LogLevel.EndGroup); +} + +/** + * Create a dummy swagger file for comparison purposes + */ +export function createDummySwagger(fromSwagger: string, toSwagger: string): void { + if (!existsSync(path.dirname(toSwagger))) { + mkdirSync(path.dirname(toSwagger), { recursive: true }); + } + const content = readFileSync(fromSwagger).toString(); + const swaggerJson = JSON.parse(content); + swaggerJson.paths = {}; + if (swaggerJson["x-ms-paths"]) { + swaggerJson["x-ms-paths"] = {}; + } + if (swaggerJson["x-ms-parameterized-host"]) { + delete swaggerJson["x-ms-parameterized-host"]; + } + swaggerJson.parameters = {}; + swaggerJson.definitions = {}; + writeFileSync(toSwagger, JSON.stringify(swaggerJson, null, 2)); + createdDummySwagger.push(toSwagger); + logMessage(`created a dummy swagger: ${toSwagger} from ${fromSwagger}`); +} + +/** + * Clean up all created dummy swagger files + */ +export function cleanDummySwagger(): void { + for (const swagger of createdDummySwagger) { + rmSync(swagger, { recursive: true, force: true }); + } + // Clear the array after removing files + createdDummySwagger.length = 0; +} + +/** + * Return true if the type indicates the same version breaking change + */ +export function isSameVersionBreakingType(type: BreakingChangesCheckType): boolean { + return type === BREAKING_CHANGES_CHECK_TYPES.SAME_VERSION; +} + +/** + * Get the count of created dummy swagger files + */ +export function getCreatedDummySwaggerCount(): number { + return createdDummySwagger.length; +} diff --git a/eng/tools/openapi-diff-runner/src/commands.ts b/eng/tools/openapi-diff-runner/src/commands.ts new file mode 100644 index 000000000000..54ab85822025 --- /dev/null +++ b/eng/tools/openapi-diff-runner/src/commands.ts @@ -0,0 +1,247 @@ +/** + * In the "breakingChanges directory invocation depth" this file has depth 1, + * i.e. it is invoked by files with depth 0 and invokes files with depth 2. + */ + +import { BREAKING_CHANGES_CHECK_TYPES } from "@azure-tools/specs-shared/breaking-change"; +import { existsSync } from "node:fs"; +import * as path from "node:path"; +import { + changeBaseBranch, + cleanDummySwagger, + createDummySwagger, + getCreatedDummySwaggerCount, + getSwaggerDiffs, + isSameVersionBreakingType, + logFullOadMessagesList, + outputBreakingChangeLabelVariables, +} from "./command-helpers.js"; +import { + checkBreakingChangeOnSameVersion, + checkCrossVersionBreakingChange, + createBreakingChangeDetectionContext, +} from "./detect-breaking-change.js"; +import { generateBreakingChangeResultSummary } from "./generate-report.js"; +import { LOG_PREFIX, logMessage } from "./log.js"; +import { Context } from "./types/breaking-change.js"; +import { RawMessageRecord, ResultMessageRecord } from "./types/message.js"; +import { createOadTrace, generateOadMarkdown, setOadBaseBranch } from "./types/oad-types.js"; +import { appendMarkdownToLog } from "./utils/oad-message-processor.js"; + +/** + * The function validateBreakingChange() is executed with type SameVersion or CrossVersion + * + * Most importantly, this function does the following: + * + * 1. Invokes + * detect-breaking-change.checkBreakingChangeOnSameVersion() + * or + * detect-breaking-change.checkCrossVersionBreakingChange(), + * depending on the input type. + * + * 2. Gernerate markdown report + * + * 3. Compute "review required" labels to be added in the PR in call to: + * ruleManager.addBreakingChangeLabelsToBeAdded(comparisonType); + * + * 4. Outputs full list of the OAD messages to build log for human review, + */ +export async function validateBreakingChange(context: Context): Promise { + let statusCode: number = 0; + let oadTracer = createOadTrace(context); + logMessage("ENTER definition validateBreakingChange"); + + logMessage(`PR target branch is ${context.prInfo ? context.prTargetBranch : ""}`); + + const diffs = await getSwaggerDiffs(); + + logMessage("Found PR changes:"); + logMessage(JSON.stringify(diffs, null, 2)); + + let swaggersToProcess = diffs.modifications?.concat(diffs.additions || []) as Array; + + logMessage("Processing swaggers:"); + logMessage(JSON.stringify(swaggersToProcess, null, 2)); + + // switch pr to base branch + changeBaseBranch(context); + await context.prInfo?.checkout(context.prInfo.baseBranch); + oadTracer = setOadBaseBranch(oadTracer, context.prInfo?.baseBranch || context.baseBranch); + + const newSwaggers = diffs.additions || []; + + const changedSwaggers = diffs.modifications || []; + + const deletedSwaggers = diffs.deletions || []; + + const newExistingVersionDirs: string[] = []; + + const addedVersionDirs = [...newSwaggers.map((f: string) => path.dirname(f))]; + + for (const f of addedVersionDirs) { + if (existsSync(path.join(context.prInfo!.tempRepoFolder, f))) { + newExistingVersionDirs.push(f); + } + } + // new swaggers in the existing version folder + const newExistingVersionSwaggers = newSwaggers.filter((f: string) => + newExistingVersionDirs.includes(path.dirname(f)), + ); + const needCompareDeletedSwaggers: string[] = deletedSwaggers.filter((f: string) => + existsSync(path.join(context.prInfo!.tempRepoFolder, f)), + ); + + // new swaggers in the new version folder + const newVersionSwaggers = newSwaggers.filter( + (f: string) => !newExistingVersionDirs.includes(path.dirname(f)), + ); + // new swaggers in the new version folder that have changed + const newVersionChangedSwaggers = changedSwaggers.filter( + (f: string) => !existsSync(path.join(context.prInfo!.tempRepoFolder, f)), + ); + // existing swaggers that have changed + const existingChangedSwaggers = changedSwaggers.filter( + (f: string) => !newVersionChangedSwaggers.includes(f), + ); + // swaggers that are in the existing version directories that have changed or deleted or + // newly added in the existing version directories + const needCompareOldSwaggers = existingChangedSwaggers + .concat(newExistingVersionSwaggers) + .concat(needCompareDeletedSwaggers); + + logMessage("Found new version swaggers:"); + logMessage(JSON.stringify(newVersionSwaggers, null, 2)); + + logMessage("Found new existing version swaggers:"); + logMessage(JSON.stringify(newExistingVersionSwaggers, null, 2)); + + logMessage("Found changed existing swaggers:"); + logMessage(JSON.stringify(existingChangedSwaggers, null, 2)); + + logMessage("The following changed swaggers are not existed in base branch:"); + logMessage(JSON.stringify(newVersionChangedSwaggers, null, 2)); + + logMessage("The following are deleted swaggers that need to do the comparison: "); + logMessage(JSON.stringify(needCompareDeletedSwaggers, null, 2)); + + logMessage( + `Creating dummy files to compare for deleted Swagger files. Count: ${needCompareDeletedSwaggers.length}`, + ); + + // create a dummy file to compare. if the deleted file exists in base branch + for (const f of needCompareDeletedSwaggers) { + const baseFilePath = path.join(context.prInfo!.tempRepoFolder, f); + if (isSameVersionBreakingType(context.runType)) { + createDummySwagger(baseFilePath, path.resolve(f)); + } + } + + logMessage( + `Creating dummy files to compare for new Swagger files in existing API version folders. ` + + `Count: ${newExistingVersionSwaggers.length}`, + ); + + // create dummy swagger for new swaggers whose api version already existed before the PR. + newExistingVersionSwaggers.forEach((f: string) => { + const oldSwagger = path.join(context.prInfo!.tempRepoFolder, f); + if (isSameVersionBreakingType(context.runType)) { + createDummySwagger(path.resolve(f), oldSwagger); + } + }); + + if (context.prInfo) { + const detectionContext = createBreakingChangeDetectionContext( + context, + needCompareOldSwaggers, + newVersionSwaggers, + newVersionChangedSwaggers, + oadTracer, + ); + + let msgs: ResultMessageRecord[] = []; + let runtimeErrors: RawMessageRecord[] = []; + let oadViolationsCnt: number = 0; + let errorCnt: number = 0; + + if (context.runType === BREAKING_CHANGES_CHECK_TYPES.SAME_VERSION) { + ({ msgs, runtimeErrors, oadViolationsCnt, errorCnt } = + await checkBreakingChangeOnSameVersion(detectionContext)); + } else { + ({ msgs, runtimeErrors, oadViolationsCnt, errorCnt } = + await checkCrossVersionBreakingChange(detectionContext)); + } + const comparedSpecsTableContent = generateOadMarkdown(detectionContext.oadTracer); + + // Log the markdown content to the pipeline log file + if (comparedSpecsTableContent) { + appendMarkdownToLog(context.oadMessageProcessorContext, comparedSpecsTableContent); + } + + // process breaking change labels + outputBreakingChangeLabelVariables(); + + // If exitCode is already defined and non-zero, we do not interfere with its value here. + if (process.exitCode === undefined || process.exitCode === 0) { + // This exitCode determines if the relevant GitHub breaking change check + // will fail. We want for it to fail only if: + // + // Case 1: there was at least one label added denoting breaking change issue, as declared by oadMessagesRuleMap.ts + // + // OR + // + // Case 2: there was at least one runtime error that is not a warning. + // + // Notably, we want for the exitCode to remain 0, denoting success, in following cases: + // - If there are messages from OAD (openapi-diff) denoting violations, but none + // of them resulted in adding any breaking changes labels. + // This is why we do not include 'oadViolationsCnt' in this formula at all. + // Instead, we rely on 'labelsAddedCount'. + // See https://github.com/Azure/azure-sdk-tools/issues/6396 + // - If there are errors, but they are only warning-level. This happens when comparing + // to previous preview version. In such cases, these errors are not included in the 'errorCnt' at all. + //process.exitCode = labelsAddedCount > 0 || errorCnt > 0 ? 1 : 0; + process.exitCode = errorCnt > 0 ? 1 : 0; + } + + logMessage( + `${LOG_PREFIX}validateBreakingChange: prUrl: ${context.prUrl}, ` + + `comparisonType: ${context.runType}, labelsAddedCount: , ` + + `errorCnt: ${errorCnt}, oadViolationsCnt: ${oadViolationsCnt}, ` + + `process.exitCode: ${process.exitCode}`, + ); + + if (process.exitCode === 0 && oadViolationsCnt > 0) { + // We are using this log as a metric to track and measure impact of the work on improving "breaking changes" tooling. Log statement added around 2/22/2024. + // See: https://github.com/Azure/azure-sdk-tools/issues/7223#issuecomment-1839830834 + logMessage( + `${LOG_PREFIX}validateBreakingChange: ` + + `Prevented spurious failure of breaking change check. prUrl: ${context.prUrl}, ` + + `comparisonType: ${context.runType}, oadViolationsCnt: ${oadViolationsCnt}, ` + + `process.exitCode: ${process.exitCode}.`, + ); + } + if (oadViolationsCnt > 0 || errorCnt > 0) { + // set statusCode to 1 if there are any OAD violations(errors) or runtime errors occurred. + statusCode = 1; + } + + logFullOadMessagesList(msgs); + await generateBreakingChangeResultSummary( + context, + msgs, + runtimeErrors, + comparedSpecsTableContent, + "", + ); + } else { + logMessage("!pr. Skipping the process of breaking change detection."); + } + + logMessage(`Cleaning up dummy files. Count: ${getCreatedDummySwaggerCount()}`); + + cleanDummySwagger(); + + logMessage("RETURN definition validateBreakingChange"); + logMessage(`${LOG_PREFIX}validateBreakingChange: statusCode: ${statusCode}`); + return statusCode; +} diff --git a/eng/tools/openapi-diff-runner/src/detect-breaking-change.ts b/eng/tools/openapi-diff-runner/src/detect-breaking-change.ts new file mode 100644 index 000000000000..b56dee2903c9 --- /dev/null +++ b/eng/tools/openapi-diff-runner/src/detect-breaking-change.ts @@ -0,0 +1,517 @@ +/** + * By design, the members exported from this file are functional breaking change detection utilities. + * + * In the "breakingChanges directory invocation depth" this fil if (previousSt if (previousPreview) { + const { oadViolationsCnt, errorCnt } = await doBreakingChangeDetection( + detectionContext, + path.resolve(detectionContext.context.prInfo!.tempRepoFolder, swaggerPath), + swaggerPath, + BREAKING_CHANGES_CHECK_TYPES.CROSS_VERSION, + "preview", + ); const { oadViolationsCnt, errorCnt } = await doBreakingChangeDetection( + detectionContext, + path.resolve(detectionContext.context.prInfo!.tempRepoFolder, swaggerPath), + swaggerPath, + BREAKING_CHANGES_CHECK_TYPES.CROSS_VERSION, + "stable", + );pth 2, + * i.e. it is invoked by files with depth 1 and invokes files with depth 3. + */ +import { BREAKING_CHANGES_CHECK_TYPES } from "@azure-tools/specs-shared/breaking-change"; +import { SpecModel } from "@azure-tools/specs-shared/spec-model"; +import { appendFileSync, existsSync } from "node:fs"; +import * as path from "node:path"; +import { logError, LogLevel, logMessage } from "./log.js"; +import { runOad } from "./run-oad.js"; +import { + ApiVersionLifecycleStage, + BreakingChangesCheckType, + Context, + logFileName, +} from "./types/breaking-change.js"; +import { RawMessageRecord, ResultMessageRecord } from "./types/message.js"; +import { addOadTrace, OadMessage, OadTraceData } from "./types/oad-types.js"; +import { applyRules } from "./utils/apply-rules.js"; +import { + blobHref, + branchHref, + convertRawErrorToUnifiedMsg, + getRelativeSwaggerPathToRepo, + processOadRuntimeErrorMessage, + specIsPreview, +} from "./utils/common-utils.js"; +import { processAndAppendOadMessages } from "./utils/oad-message-processor.js"; +import { + deduplicateSwaggers, + getExistedVersionOperations, + getPrecedingSwaggers, +} from "./utils/spec.js"; + +// We want to display some lines as we improved AutoRest v2 error output since March 2024 to provide multi-line error messages, e.g.: +// https://github.com/Azure/autorest/pull/4934 +// For console (diagnostic) logs we want to display the entire stack trace. +// The value here is an arbitrary high number to limit the stack trace in case a bug would cause it to be excessively long. +const stackTraceMaxLength = 500; + +/** + * Context for breaking change detection operations + */ +export interface BreakingChangeDetectionContext { + context: Context; + existingVersionSwaggers: string[]; // Files in existing API version directories + newVersionSwaggers: string[]; // Files in completely new API version directories + newVersionChangedSwaggers: string[]; // Files in existing API version directories that have changed + oadTracer: OadTraceData; + msgs: ResultMessageRecord[]; + runtimeErrors: RawMessageRecord[]; + tempTagName: string; +} + +/** + * Create a new breaking change detection context + */ +export function createBreakingChangeDetectionContext( + context: Context, + existingVersionSwaggers: string[], + newVersionSwaggers: string[], + newVersionChangedSwaggers: string[], + oadTracer: OadTraceData, +): BreakingChangeDetectionContext { + return { + context, + existingVersionSwaggers, + newVersionSwaggers, + newVersionChangedSwaggers, + oadTracer, + msgs: [], + runtimeErrors: [], + tempTagName: "oad-default-tag", + }; +} + +/** + * The entry points for breaking change detection are: + * - checkBreakingChangeOnSameVersion() + * - checkCrossVersionBreakingChange() + * both of which are invoked by the function commands.ts / validateBreakingChange() + */ + +/** The function checkBreakingChangeOnSameVersion() + * maps to the lower "Same-version check" rectangle at: + * https://aka.ms/azsdk/pr-brch-deep#diagram-explaining-breaking-changes-and-versioning-issues + * + * This function is called by the function commands.ts / validateBreakingChange() + * This function calls doBreakingChangeDetection with appropriate "type" and other parameters. + */ +export async function checkBreakingChangeOnSameVersion( + detectionContext: BreakingChangeDetectionContext, +): Promise<{ + msgs: ResultMessageRecord[]; + runtimeErrors: RawMessageRecord[]; + oadViolationsCnt: number; + errorCnt: number; +}> { + logMessage(`ENTER definition checkBreakingChangeOnSameVersion`); + + let aggregateOadViolationsCnt = 0; + let aggregateErrorCnt = 0; + + for (const swaggerPath of detectionContext.existingVersionSwaggers) { + logMessage(`Processing swaggerPath: ${swaggerPath}`, LogLevel.Group); + const { oadViolationsCnt, errorCnt } = await doBreakingChangeDetection( + detectionContext, + path.resolve(detectionContext.context.prInfo!.tempRepoFolder, swaggerPath), + swaggerPath, + BREAKING_CHANGES_CHECK_TYPES.SAME_VERSION, + specIsPreview(swaggerPath) + ? ApiVersionLifecycleStage.PREVIEW + : ApiVersionLifecycleStage.STABLE, + ); + aggregateOadViolationsCnt += oadViolationsCnt; + aggregateErrorCnt += errorCnt; + logMessage("Processing completed", LogLevel.EndGroup); + } + + logMessage( + `RETURN definition checkBreakingChangeOnSameVersion. ` + + `msgs.length: ${detectionContext.msgs.length}, ` + + `aggregateOadViolationsCnt: ${aggregateOadViolationsCnt}, aggregateErrorCnt: ${aggregateErrorCnt}`, + ); + + return { + msgs: detectionContext.msgs, + runtimeErrors: detectionContext.runtimeErrors, + oadViolationsCnt: aggregateOadViolationsCnt, + errorCnt: aggregateErrorCnt, + }; +} + +/** The function checkCrossVersionBreakingChange() + * maps to the upper "Cross-version check" rectangle at: + * https://aka.ms/azsdk/pr-brch-deep#diagram-explaining-breaking-changes-and-versioning-issues + * + * This function is called by the function commands.ts / validateBreakingChange() + * This function calls this.doBreakingChangeDetection with appropriate "type" and other parameters. + */ +export async function checkCrossVersionBreakingChange( + detectionContext: BreakingChangeDetectionContext, +): Promise<{ + msgs: ResultMessageRecord[]; + runtimeErrors: RawMessageRecord[]; + oadViolationsCnt: number; + errorCnt: number; +}> { + logMessage(`ENTER definition checkCrossVersionBreakingChange`); + + let aggregateOadViolationsCnt = 0; + let aggregateErrorCnt = 0; + for (const swaggerPath of detectionContext.newVersionSwaggers + .concat(detectionContext.newVersionChangedSwaggers) + .concat(detectionContext.existingVersionSwaggers.filter(isInDevFolder))) { + logMessage(`Processing swaggerPath: ${swaggerPath}`, LogLevel.Group); + + // use the detectionContext.context.localSpecRepoPath to resolve the absolute path as it's the merge commit working directory + const absoluteSwaggerPath = path.resolve( + detectionContext.context.localSpecRepoPath, + swaggerPath, + ); + logMessage(`checkCrossVersionBreakingChange: absoluteSwaggerPath: ${absoluteSwaggerPath}`); + const specModel = await getSpecModel( + detectionContext.context.prInfo!.tempRepoFolder, + swaggerPath, + ); + + // If the specModel is not found, it means the swaggerPath is a new RP + if (!specModel) { + continue; + } + + const originalReturnedSwaggers = await specModel.getSwaggers(); + const availableSwaggers = deduplicateSwaggers(originalReturnedSwaggers); + logMessage( + `checkCrossVersionBreakingChange: swaggerPath: ${swaggerPath}, availableSwaggers.length: ${availableSwaggers?.length}`, + ); + + // use absoluteSwaggerPath as it will need to it will fall back to use the version info in the swagger content + const previousVersions = await getPrecedingSwaggers(absoluteSwaggerPath, availableSwaggers); + logMessage( + `checkCrossVersionBreakingChange: previousVersions: ${JSON.stringify(previousVersions)}`, + ); + const previousStableSwaggerPath = previousVersions.stable; + const previousPreviewSwaggerPath = previousVersions.preview; + if (previousStableSwaggerPath) { + const { oadViolationsCnt, errorCnt } = await doBreakingChangeDetection( + detectionContext, + path.resolve(detectionContext.context.prInfo!.tempRepoFolder, previousStableSwaggerPath), + swaggerPath, + BREAKING_CHANGES_CHECK_TYPES.CROSS_VERSION, + ApiVersionLifecycleStage.STABLE, + ); + aggregateOadViolationsCnt += oadViolationsCnt; + aggregateErrorCnt += errorCnt; + } + if (previousPreviewSwaggerPath) { + const { oadViolationsCnt, errorCnt } = await doBreakingChangeDetection( + detectionContext, + path.resolve(detectionContext.context.prInfo!.tempRepoFolder, previousPreviewSwaggerPath), + swaggerPath, + BREAKING_CHANGES_CHECK_TYPES.CROSS_VERSION, + ApiVersionLifecycleStage.PREVIEW, + ); + aggregateErrorCnt += errorCnt; + // This code block is commented out because we are purposefully ignoring errorCnt here, + // not adding them to aggregateErrorCnt, + // as they originate from cross-version comparison with a preview version. + // + // Comparison to previous preview version must never cause the breaking change process to report failure, per: + // - https://github.com/Azure/azure-sdk-tools/issues/6396 + // + // Contrast this with same-version API breaking changes detection on a preview version, which still produces + // errors/failures. + // + // aggregateErrorCnt += errorCnt; + + // There is no need to ignore oadViolationsCnt here, as it is expected to be zero, due + // to applyRules.ts / applyRule() function downgrading the severity of all "Error" messages. + aggregateOadViolationsCnt += oadViolationsCnt; + } + if (!previousStableSwaggerPath && !previousPreviewSwaggerPath) { + await checkAPIsBeingMovedToANewSpec(detectionContext.context, swaggerPath, availableSwaggers); + } + logMessage("Processing completed", LogLevel.EndGroup); + } + logMessage( + `RETURN definition checkCrossVersionBreakingChange. ` + + `msgs.length: ${detectionContext.msgs.length}, ` + + `aggregateOadViolationsCnt: ${aggregateOadViolationsCnt}, aggregateErrorCnt: ${aggregateErrorCnt}`, + ); + + return { + msgs: detectionContext.msgs, + runtimeErrors: detectionContext.runtimeErrors, + oadViolationsCnt: aggregateOadViolationsCnt, + errorCnt: aggregateErrorCnt, + }; +} + +/** + * The function doBreakingChangeDetection() + * is called by + * + * - checkBreakingChangeOnSameVersion() + * - or checkCrossVersionBreakingChange() + * + * with appropriate options. + * + * Most importantly, this function does the following: + * + * 1. Invokes "@azure/oad" via call to runOad() to obtain OadMessage[] collection. + * + * 2. It post-processes the OadMessage[] collection by calling + * applyRules() function + * + * which uses the oadMessagesRuleMap.ts config to schedule + * appropriate "review required" labels to be added downstream by doBreakingChangeDetection() calling addBreakingChangeLabelsToBeAdded() + * as well as updates the OAD messages severity. + * + * 3. It saves the OadMessage[] collection to the unified pipeline store ("pipe.log" file) in call to: + * processAndAppendOadMessages() + * + * 4. It saves OAD errors, if any, to the unified pipeline store ("pipe.log" file) in call to: + * appendOadRuntimeErrors() + */ +export async function doBreakingChangeDetection( + detectionContext: BreakingChangeDetectionContext, + oldSpec: string, + newSpec: string, + scenario: BreakingChangesCheckType, + previousApiVersionLifecycleStage: ApiVersionLifecycleStage, +): Promise<{ oadViolationsCnt: number; errorCnt: number }> { + logMessage(`ENTER definition doBreakingChangeDetection oldSpec: ${oldSpec}, newSpec: ${newSpec}`); + + let oadViolationsCnt = 0; + let errorCnt = 0; + + try { + const oadMessages = await runOad(oldSpec, newSpec); + + // Handle tracing separately - no need for a trace of two tags comparison + detectionContext.oadTracer = addOadTrace( + detectionContext.oadTracer, + getRelativeSwaggerPathToRepo(oldSpec), + newSpec, + ); + + const modifiedOadMessages: OadMessage[] = applyRules( + oadMessages, + scenario, + previousApiVersionLifecycleStage, + ); + + oadViolationsCnt += modifiedOadMessages.filter( + (oadMessage) => oadMessage.type === "Error", + ).length; + + const msgs: ResultMessageRecord[] = processAndAppendOadMessages( + detectionContext.context, + modifiedOadMessages, + detectionContext.context.baseBranch, + ); + detectionContext.msgs = detectionContext.msgs.concat(msgs); + } catch (e) { + const error = e instanceof Error ? e : new Error(String(e)); + const runtimeError: RawMessageRecord = { + type: "Raw", + level: "Error", + message: "Runtime Exception", + time: new Date(), + groupName: previousApiVersionLifecycleStage, + extra: { + new: blobHref( + detectionContext.context.sourceRepo, + detectionContext.context.headCommit, + getRelativeSwaggerPathToRepo(newSpec), + ), + old: branchHref( + detectionContext.context.targetRepo, + getRelativeSwaggerPathToRepo(oldSpec), + detectionContext.context.baseBranch, + ), + details: processOadRuntimeErrorMessage(error.message, stackTraceMaxLength), + }, + }; + detectionContext.runtimeErrors.push(runtimeError); + errorCnt += 1; + appendFileSync(logFileName, JSON.stringify(runtimeError) + "\n"); + logError(`appendOadRuntimeErrors: ${JSON.stringify(runtimeError)}`); + } + + logMessage( + `RETURN definition doBreakingChangeDetection ` + + `scenario: ${scenario}, ` + + `previousApiVersionLifecycleStage: ${previousApiVersionLifecycleStage}, ` + + `oldSpec: ${oldSpec}, newSpec: ${newSpec}, ` + + `oadViolationsCnt: ${oadViolationsCnt}, errorCnt: ${errorCnt}`, + ); + + return { oadViolationsCnt, errorCnt }; +} + +export function isInDevFolder(swaggerPath: string) { + return swaggerPath.startsWith("dev/"); +} + +/** + * Find the path to the closest readme.md file based on the input file path, + * searching upward from the file's directory up to the 'resource-manager' or 'data-plane' sub path. + * Example: + * input: specification/network/resource-manager/Microsoft.Network/stable/2019-11-01/network.json + * returns: path to the directory containing the closest readme.md file, up to specification/network/resource-manager + */ +export function getReadmeFolder(swaggerFile: string) { + const segments = swaggerFile.split(/\\|\//); + + if (!segments || segments.length < 3) { + return undefined; + } + + // Handle dev folder conversion + if (segments[0] === "dev") { + segments[0] = "specification"; + } + + // Find the boundary index (resource-manager or data-plane) + const resourceManagerIndex = segments.findIndex((segment) => segment === "resource-manager"); + const dataPlaneIndex = segments.findIndex((segment) => segment === "data-plane"); + const boundaryIndex = resourceManagerIndex !== -1 ? resourceManagerIndex : dataPlaneIndex; + + // Determine search range: from file's parent directory up to boundary (or root if no boundary found) + const startIndex = segments.length - 2; // Start from parent directory of the file + const minIndex = boundaryIndex !== -1 ? boundaryIndex : 2; // Stop at boundary or at least keep first 3 segments + + // Search upward for readme.md + for (let i = startIndex; i >= minIndex; i--) { + const currentPath = segments.slice(0, i + 1).join(path.sep); + const readmePath = path.join(currentPath, "readme.md"); + + if (existsSync(readmePath)) { + return currentPath; + } + } + + // Fallback: return up to boundary if found, otherwise first 3 segments + if (boundaryIndex !== -1) { + return segments.slice(0, boundaryIndex + 1).join(path.sep); + } + + return segments.slice(0, 3).join(path.sep); +} + +/** + * Get or create a SpecModel for the given swagger path. + * Uses caching to avoid redundant SpecModel initialization for the same folder. + * @param specRepoFolder - The root folder of the spec repository + * @param swaggerPath - Path to the swagger file + * @returns SpecModel instance for the folder containing the swagger file or undefined if the folder does not exist + */ +export function getSpecModel(specRepoFolder: string, swaggerPath: string): SpecModel | undefined { + const folder = getReadmeFolder(swaggerPath); + + if (!folder) { + throw new Error(`Could not determine readme folder for swagger path: ${swaggerPath}`); + } + + let isNewRp = false; + let fullFolderPath = path.join(specRepoFolder, folder); + if (!existsSync(fullFolderPath)) { + isNewRp = true; + } + + // If the initial folder doesn't exist, search upward for a folder with readme.md + while ( + isNewRp && + !fullFolderPath.endsWith("resource-manager") && + !fullFolderPath.endsWith("data-plane") + ) { + const parent = path.dirname(fullFolderPath); + + // Prevent infinite loop if we reach the root + if (parent === fullFolderPath) { + break; + } + + fullFolderPath = parent; + const readmePath = path.join(fullFolderPath, "readme.md"); + + // If we find a readme.md, use this folder + if (existsSync(readmePath)) { + isNewRp = false; + break; + } + } + + // Return if the readme folder is not found which means it's a new RP + if (isNewRp) { + logMessage( + `getSpecModel: this is a new RP as ${fullFolderPath} folder does not exist in the base branch of spec repo.`, + ); + return undefined; + } + logMessage(`getSpecModel: folder: ${fullFolderPath}, swaggerPath: ${swaggerPath}`); + + // Create new SpecModel and cache it + const specModel = new SpecModel(fullFolderPath); + + return specModel; +} + +export async function checkAPIsBeingMovedToANewSpec( + context: Context, + swaggerPath: string, + availableSwaggers: any[], +) { + const absoluteSwaggerPath = path.resolve(context.localSpecRepoPath, swaggerPath); + logMessage(`checkAPIsBeingMovedToANewSpec: absoluteSwaggerPath: ${absoluteSwaggerPath}`); + const specModel = getSpecModel(context.localSpecRepoPath, swaggerPath); + if (!specModel) { + return; + } + const swaggersFromOriginalClonedRepo = await specModel.getSwaggers(); + const targetSwagger = swaggersFromOriginalClonedRepo.find((s) => s.path === absoluteSwaggerPath); + if (!targetSwagger) { + logError( + `checkAPIsBeingMovedToANewSpec: targetSwagger not found for swaggerPath: ${swaggerPath}`, + ); + return; + } + const targetOperations = await targetSwagger.getOperations(); + + // use absoluteSwaggerPath as it will need to it will fall back to use the version info in the swagger content + const movedApis = await getExistedVersionOperations(absoluteSwaggerPath, availableSwaggers, [ + ...targetOperations.values(), + ]); + logMessage( + `checkAPIsBeingMovedToANewSpec: swaggerPath: ${swaggerPath}, movedApis.size: ${movedApis.size}`, + ); + if (movedApis.size > 0) { + logMessage( + `The swagger ${swaggerPath} has no previous version being found, but its APIs were found in other swaggers. It means that you are moving some APIs to this new swagger file.`, + ); + for (const [swaggerFile, operations] of movedApis) { + const operationIds = operations.map((op: any) => op.id).join(","); + appendFileSync( + logFileName, + convertRawErrorToUnifiedMsg( + context, + "APIsBeingMovedToANewSpec", + "Attention: There are some existing APIs currently documented in a new spec file. The validation may not be able to report breaking changes with these APIs. It is recommended not to rename swagger file or move public APIs to a new file when creating a new API version." + + ` The existing APIs being moved are: ${operationIds};`, + "Warning", + swaggerFile, + ), + ); + logMessage(`The following are details for existing APIs being moved to the new spec:`); + logMessage(` swagger file: ${swaggerFile}, operationIds: ${operationIds}\n`); + } + } +} diff --git a/eng/tools/openapi-diff-runner/src/generate-report.ts b/eng/tools/openapi-diff-runner/src/generate-report.ts new file mode 100644 index 000000000000..eaa4213aa4f6 --- /dev/null +++ b/eng/tools/openapi-diff-runner/src/generate-report.ts @@ -0,0 +1,289 @@ +import { addToSummary, logMessage, logWarning } from "./log.js"; +import { ApiVersionLifecycleStage, Context } from "./types/breaking-change.js"; +import { + BrChMsgRecord, + getKey, + MessageLevel, + RawMessageRecord, + ResultMessageRecord, +} from "./types/message.js"; +import { + BreakingChangeMdReport, + createBreakingChangeMdReport, + reportToString, + sortBreakingChangeMdReports, +} from "./utils/markdown-report.js"; + +// Per the GitHub documentation [1], the length limit of a check pane is 65535 characters. +// While not immediately obvious, it looks like the 65535 limit applies to the total length of the text and summary, +// not separately. +// +// [1] Properties of output / text and output / summary at: +// https://docs.github.com/en/rest/checks/runs?apiVersion=2022-11-28#create-a-check-run +const checkPaneLengthLimit = 65535; + +// GitHub Actions job summary limit is 1MB (much more generous than check runs) +const jobSummaryLengthLimit = 1048576; // 1MB in bytes + +export async function generateBreakingChangeResultSummary( + context: Context, + messages: ResultMessageRecord[], + runtimeErrors: RawMessageRecord[], + comparedSpecsTableContent: string, + summaryDataSuppressionAndDetailsText: string, +): Promise { + const allMessageRecords: BrChMsgRecord[] = [...messages, ...runtimeErrors]; + + const summaryData = getSummaryData( + context.checkName, + allMessageRecords, + summaryDataSuppressionAndDetailsText, + ); + const maxCommentDataLength = checkPaneLengthLimit - summaryData.length; + const commentData = await getCommentData( + context.checkName, + comparedSpecsTableContent, + allMessageRecords, + maxCommentDataLength, + ); + + // Construct complete markdown report for GitHub Actions job summary + const markdownReport = summaryData + commentData; + + // Output to GitHub Actions job summary + await writeToJobSummary(markdownReport); + + logMessage( + `RETURNING. messageRecords# raw/result/all: ` + + `${runtimeErrors.length}/${messages.length}/${runtimeErrors.length + messages.length}, ` + + `length summary/comment/(summary+comment): ` + + `${summaryData.length}/${commentData.length}/${summaryData.length + commentData.length}.`, + ); +} + +async function getCommentData( + checkName: string, + comparedSpecsTableContent: string, + msgs: BrChMsgRecord[], + maxCommentDataLength: number, +): Promise { + // Add blank line before table if table content exists to ensure proper markdown rendering + const markdownMessageRow = comparedSpecsTableContent + ? "\n" + comparedSpecsTableContent + "\n" + : ""; + const textPrefixLength = markdownMessageRow.length; + const reportsString: string = await getReportsAsString( + checkName, + msgs, + textPrefixLength, + maxCommentDataLength, + ); + + let commentData = markdownMessageRow + reportsString; + if (commentData.length > maxCommentDataLength) { + logWarning( + `ASSERTION VIOLATION! commentData.length == ${commentData.length} which is > maxCommentDataLength of ${maxCommentDataLength}.`, + ); + commentData = commentData.substring(0, maxCommentDataLength - 20) + "... ⚠️ TRUNCATED ⚠️"; + } + + return commentData; +} + +async function getReportsAsString( + checkName: string, + msgs: BrChMsgRecord[], + textPrefixLength: number, + maxCommentDataLength: number, +): Promise { + let [stableReports, previewReports, maxRowCountAcrossKeys] = getReports(msgs); + let currentMaxRowCount = maxRowCountAcrossKeys; + + let reportsString: string = getReportsString( + checkName, + stableReports, + previewReports, + currentMaxRowCount, + ); + while ( + currentMaxRowCount > 0 && + !totalTextLengthWithinLimit(reportsString, textPrefixLength, maxCommentDataLength) + ) { + currentMaxRowCount--; + reportsString = getReportsString(checkName, stableReports, previewReports, currentMaxRowCount); + } + + if (!totalTextLengthWithinLimit(reportsString, textPrefixLength, maxCommentDataLength)) { + logWarning( + `ASSERTION VIOLATION! totalTextLengthWithinLimit is false. currentMaxRowCount: ${currentMaxRowCount}.`, + ); + } + + logMessage( + `getReportsAsOneString: RETURNING. ` + + `checkShowName: ${checkName}, ` + + `maxRowCount reduced/current/max: ${maxRowCountAcrossKeys - currentMaxRowCount}/${currentMaxRowCount}/${maxRowCountAcrossKeys}, ` + + `reportsString.length: ${reportsString.length}.`, + ); + + return reportsString; +} + +function getReports( + msgs: BrChMsgRecord[], +): [BreakingChangeMdReport[], BreakingChangeMdReport[], number] { + const msgsByKey: Record = groupMsgsByKey(msgs); + + let maxRowCount = 0; + let stableReports: BreakingChangeMdReport[] = []; + let previewReports: BreakingChangeMdReport[] = []; + + Object.entries(msgsByKey).forEach(([, msgs]) => { + const stableMsgs = msgs.filter( + (msg) => (msg.groupName as ApiVersionLifecycleStage) == ApiVersionLifecycleStage.STABLE, + ); + const previewMsgs = msgs.filter( + (msg) => (msg.groupName as ApiVersionLifecycleStage) == ApiVersionLifecycleStage.PREVIEW, + ); + + if (stableMsgs.length > 0) { + stableReports.push(createBreakingChangeMdReport(stableMsgs)); + } + if (previewMsgs.length > 0) { + previewReports.push(createBreakingChangeMdReport(previewMsgs)); + } + + maxRowCount = Math.max(maxRowCount, stableMsgs.length, previewMsgs.length); + }); + + return [ + sortBreakingChangeMdReports(stableReports), + sortBreakingChangeMdReports(previewReports), + maxRowCount, + ]; +} + +function getReportsString( + checkName: string, + stableReports: BreakingChangeMdReport[], + previewReports: BreakingChangeMdReport[], + maxRowCount: number, +): string { + if (stableReports.length == 0 && previewReports.length == 0) { + return `No breaking changes detected.\n`; + } + + if (checkName === "Swagger BreakingChange") { + return [...stableReports, ...previewReports] + .map((report) => reportToString(report, maxRowCount)) + .join("\n"); + } else { + return getComparedApiVersionsReportsString(stableReports, previewReports, maxRowCount); + } +} + +function getComparedApiVersionsReportsString( + stableReports: BreakingChangeMdReport[], + previewReports: BreakingChangeMdReport[], + maxRowCount: number, +): string { + const stableApiVersionComparisonReportsString: string = getReportsComparedToApiVersionString( + stableReports, + ApiVersionLifecycleStage.STABLE, + maxRowCount, + ); + const previewApiVersionComparisonReportsString: string = getReportsComparedToApiVersionString( + previewReports, + ApiVersionLifecycleStage.PREVIEW, + maxRowCount, + ); + return stableApiVersionComparisonReportsString + previewApiVersionComparisonReportsString; +} + +function getReportsComparedToApiVersionString( + reports: BreakingChangeMdReport[], + comparedApiVersion: ApiVersionLifecycleStage, + maxRowCount: number, +): string { + return ( + `# The following breaking changes have been detected in comparison to the latest ${comparedApiVersion} version\n` + + (reports.length > 0 + ? reports.map((report) => reportToString(report, maxRowCount)).join("\n") + : "No breaking changes detected in this comparison.\n") + ); +} + +function totalTextLengthWithinLimit( + reportsString: string, + textPrefixLength: number, + maxCommentDataLength: number, +): boolean { + return textPrefixLength + reportsString.length <= maxCommentDataLength; +} + +function groupMsgsByKey(msgs: BrChMsgRecord[]): Record { + return msgs.reduce((msgsByKey: Record, msg: BrChMsgRecord) => { + const key = getKey(msg); + if (!msgsByKey[key]) { + msgsByKey[key] = []; + } + msgsByKey[key].push(msg); + return msgsByKey; + }, {}); +} + +function getSummaryData( + checkName: string, + messageRecords: BrChMsgRecord[], + summaryDataSuppressionAndDetailsText: string, +): string { + const errorCount = getMessageLevelCounts(messageRecords, "Error"); + const warningCount = getMessageLevelCounts(messageRecords, "Warning"); + let summaryTitle = checkName; + if (errorCount > 0) { + summaryTitle = + `Detected: ${getMessageLevelCounts(messageRecords, "Error")} Errors, ` + + `${getMessageLevelCounts(messageRecords, "Warning")} Warnings\n`; + } else if (warningCount > 0) { + summaryTitle = `Detected: ${getMessageLevelCounts(messageRecords, "Warning")} Warnings\n`; + } + + return ( + summaryTitle + + summaryDataSuppressionAndDetailsText + + `\n\n> [!IMPORTANT]\n` + + `> Browse to the job logs to see the details.\n\n` + ); +} + +function getMessageLevelCounts(msgs: BrChMsgRecord[], msgLevel: MessageLevel) { + return msgs.filter((msg) => msg.level === msgLevel).length; +} + +/** + * Writes markdown content to GitHub Actions job summary + * Handles the 1MB limit and truncation if necessary + */ +async function writeToJobSummary(markdownContent: string): Promise { + if (!process.env.GITHUB_STEP_SUMMARY) { + logMessage("GitHub Actions job summary not available, skipping summary output."); + return; + } + + let finalContent = markdownContent; + + // Check if content exceeds job summary limit (1MB) + if (markdownContent.length > jobSummaryLengthLimit) { + const truncationMessage = + "\n\n⚠️ **Report truncated due to GitHub Actions job summary size limits (1MB)** ⚠️\n\nFor the complete report, please check the build logs."; + const availableSpace = jobSummaryLengthLimit - truncationMessage.length; + finalContent = markdownContent.substring(0, availableSpace) + truncationMessage; + + logWarning( + `Job summary content truncated. Original length: ${markdownContent.length}, truncated to: ${finalContent.length}`, + ); + } + + addToSummary(finalContent); + logMessage(`Successfully wrote ${finalContent.length} characters to GitHub Actions job summary.`); +} diff --git a/eng/tools/openapi-diff-runner/src/index.ts b/eng/tools/openapi-diff-runner/src/index.ts new file mode 100644 index 000000000000..19ed15cc7555 --- /dev/null +++ b/eng/tools/openapi-diff-runner/src/index.ts @@ -0,0 +1,177 @@ +import { BREAKING_CHANGES_CHECK_TYPES } from "@azure-tools/specs-shared/breaking-change"; +import { existsSync, mkdirSync } from "node:fs"; +import path from "node:path"; +import { exit } from "node:process"; +import { fileURLToPath } from "node:url"; +import { parseArgs, type ParseArgsConfig } from "node:util"; +import { + buildPrInfo, + createContextFromParsedArgs, + type ParsedCliArguments, +} from "./command-helpers.js"; +import { validateBreakingChange } from "./commands.js"; +import { logError, logMessage } from "./log.js"; +import { BreakingChangesCheckType } from "./types/breaking-change.js"; + +const __filename: string = fileURLToPath(import.meta.url); +const __dirname: string = path.dirname(__filename); + +/** + * Parse command line arguments using Node.js parseArgs + */ +function parseCliArguments(): ParsedCliArguments { + const options: ParseArgsConfig = { + options: { + "spec-repo-path": { + type: "string", + short: "p", + default: path.join(__dirname, ".."), + }, + "target-repo": { + type: "string", + short: "r", + default: "azure/azure-rest-api-specs", + }, + "source-repo": { + type: "string", + short: "s", + default: "azure/azure-rest-api-specs", + }, + "pr-number": { + type: "string", + short: "n", + default: "", + }, + "run-type": { + type: "string", + short: "t", + default: BREAKING_CHANGES_CHECK_TYPES.SAME_VERSION, + }, + "base-branch": { + type: "string", + short: "b", + default: "main", + }, + "head-commit": { + type: "string", + short: "c", + default: "HEAD", + }, + "pr-source-branch": { + type: "string", + default: "", + }, + "pr-target-branch": { + type: "string", + default: "", + }, + help: { + type: "boolean", + short: "h", + default: false, + }, + }, + allowPositionals: false, + strict: true, + }; + + try { + const { values } = parseArgs(options); + + // Show help if requested + if (values.help) { + showHelp(); + exit(0); + } + + // Validate required arguments + if (!values["pr-number"]) { + logError("Error: --pr-number (PR number) is required"); + showHelp(); + exit(1); + } + + // Validate run type + const validRunTypes = [ + BREAKING_CHANGES_CHECK_TYPES.SAME_VERSION, + BREAKING_CHANGES_CHECK_TYPES.CROSS_VERSION, + ]; + if (!validRunTypes.includes(values["run-type"] as BreakingChangesCheckType)) { + logError(`Error: --run-type must be one of: ${validRunTypes.join(", ")}`); + exit(1); + } + + return { + localSpecRepoPath: path.resolve(values["spec-repo-path"] as string), + targetRepo: values["target-repo"] as string, + sourceRepo: values["source-repo"] as string, + prNumber: values["pr-number"] as string, + runType: values["run-type"] as BreakingChangesCheckType, + baseBranch: values["base-branch"] as string, + headCommit: values["head-commit"] as string, + prSourceBranch: values["pr-source-branch"] as string, + prTargetBranch: values["pr-target-branch"] as string, + }; + } catch (error) { + logError(`Error parsing arguments: ${error}`); + showHelp(); + exit(1); + } +} + +/** + * Show help message + */ +function showHelp() { + logMessage("OpenAPI Diff Runner - Breaking Change Detection Tool"); + logMessage(""); + logMessage("Usage: node index.js [options]"); + logMessage(""); + logMessage("Options:"); + logMessage(" -h, --help Show help message"); + logMessage(" -p, --spec-repo-path Local spec repository path (default: ..)"); + logMessage( + " -r, --target-repo Target repository (owner/repo format) (default: azure/azure-rest-api-specs)", + ); + logMessage( + " -s, --source-repo Source repository (owner/repo format) (default: azure/azure-rest-api-specs)", + ); + logMessage(" -n, --pr-number Pull request number (required)"); + logMessage( + " -t, --run-type Run type (SameVersion or CrossVersion) (default: SameVersion)", + ); + logMessage(" -b, --base-branch Base branch for comparison (default: main)"); + logMessage(" -c, --head-commit Head commit SHA (default: HEAD)"); + logMessage(" --pr-source-branch PR source branch (default: '')"); + logMessage(" --pr-target-branch PR target branch (default: '')"); + logMessage(""); + logMessage("Examples:"); + logMessage(" node index.js --pr-number 12345 --run-type SameVersion"); + logMessage(" node index.js -n 12345 -t CrossVersion -b main"); + logMessage(" node index.js --pr-number 12345 --target-repo myorg/my-specs"); +} + +export async function main() { + // Parse command line arguments + const parsedArgs = parseCliArguments(); + + // Log the arguments to the console + logMessage(`Arguments: ${JSON.stringify(parsedArgs, null, 2)}`); + logMessage(`Current working directory: ${process.cwd()}`); + + // Create working folder and log file folder + const workingFolder = path.join(parsedArgs.localSpecRepoPath, ".."); + const logFileFolder = path.join(workingFolder, "out/logs"); + + // Create the log file folder if it does not exist + if (!existsSync(logFileFolder)) { + mkdirSync(logFileFolder, { recursive: true }); + } + + // Create context from parsed arguments + const context = createContextFromParsedArgs(parsedArgs, workingFolder, logFileFolder); + await buildPrInfo(context); + let statusCode = 0; + statusCode = await validateBreakingChange(context); + exit(statusCode); +} diff --git a/eng/tools/openapi-diff-runner/src/log.ts b/eng/tools/openapi-diff-runner/src/log.ts new file mode 100644 index 000000000000..d59528e53d1f --- /dev/null +++ b/eng/tools/openapi-diff-runner/src/log.ts @@ -0,0 +1,166 @@ +import { appendFileSync } from "node:fs"; + +/** + * Log prefix for all messages from openapi-diff-runner + */ +export const LOG_PREFIX = "Runner-"; + +export enum LogLevel { + Error = "error", + Warn = "warn", + Info = "info", + Debug = "debug", + Notice = "notice", + Group = "group", + EndGroup = "endgroup", +} + +/** + * Logs a message to the console with GitHub Actions workflow commands. + * Automatically prefixes messages with LOG_PREFIX. + * @param message The message to log. + * @param level The log level (e.g., LogLevel.Group, LogLevel.EndGroup, LogLevel.Debug, LogLevel.Error). + */ +export function logMessage(message: string, level?: LogLevel): void { + switch (level) { + case LogLevel.Group: { + console.log(`::group::${message}`); + break; + } + case LogLevel.EndGroup: { + console.log(`::endgroup::`); + break; + } + case LogLevel.Debug: { + console.log(`::debug::${message}`); + break; + } + case LogLevel.Error: { + console.log(`::error::${message}`); + break; + } + case LogLevel.Warn: { + console.log(`::warning::${message}`); + break; + } + case LogLevel.Notice: { + console.log(`::notice::${message}`); + break; + } + case LogLevel.Info: + default: { + console.log(message); + break; + } + } +} + +/** + * Log an error with file location information for GitHub Actions + * Automatically prefixes messages with LOG_PREFIX. + * @param message Error message + * @param file File path (optional) + * @param line Line number (optional) + * @param col Column number (optional) + */ +export function logError(message: string, file?: string, line?: number, col?: number): void { + if (file) { + const location = line && col ? `line=${line},col=${col}` : line ? `line=${line}` : ""; + const fileLocation = location ? `file=${file},${location}` : `file=${file}`; + console.log(`::error ${fileLocation}::${message}`); + } else { + console.log(`::error::${message}`); + } +} + +/** + * Log a warning with file location information for GitHub Actions + * Automatically prefixes messages with LOG_PREFIX. + * @param message Warning message + * @param file File path (optional) + * @param line Line number (optional) + * @param col Column number (optional) + */ +export function logWarning(message: string, file?: string, line?: number, col?: number): void { + if (file) { + const location = line && col ? `line=${line},col=${col}` : line ? `line=${line}` : ""; + const fileLocation = location ? `file=${file},${location}` : `file=${file}`; + console.log(`::warning ${fileLocation}::${message}`); + } else { + console.log(`::warning::${message}`); + } +} + +/** + * Set an output parameter in GitHub Actions + * @param name Output parameter name + * @param value Output parameter value + */ +export function setOutput(name: string, value: string): void { + if (process.env.GITHUB_OUTPUT) { + appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`); + } else { + // Fallback to older syntax + console.log(`::set-output name=${name}::${value}`); + } +} + +/** + * Add content to the GitHub Actions job summary + * @param content Content to add to summary + */ +export function addToSummary(content: string): void { + if (process.env.GITHUB_STEP_SUMMARY) { + appendFileSync(process.env.GITHUB_STEP_SUMMARY, content); + } + // Do nothing if GITHUB_STEP_SUMMARY is not available +} + +/** + * Create a collapsible group in logs + * @param title Group title + * @param content Function that logs the group content + */ +export async function logGroup(title: string, content: () => Promise | T): Promise { + logMessage(title, LogLevel.Group); + try { + const result = await content(); + return result; + } finally { + logMessage("", LogLevel.EndGroup); + } +} + +/** + * Maximum safe log line length for GitHub Actions (64KB - some buffer) + */ +const MAX_LOG_LINE_LENGTH = 60 * 1024; // 60KB to leave some buffer + +/** + * Truncates a message if it exceeds the maximum GitHub Actions log line length + * @param message The message to potentially truncate + * @param prefix Optional prefix to include in the truncation message + * @returns The original message if under limit, or truncated message with info + */ +export function truncateLogMessage(message: string, prefix?: string): string { + if (message.length <= MAX_LOG_LINE_LENGTH) { + return message; + } + + const prefixText = prefix ? `${prefix}: ` : ""; + const truncationInfo = `... [TRUNCATED: Original length ${message.length} bytes, showing first ${MAX_LOG_LINE_LENGTH} bytes]`; + const availableLength = MAX_LOG_LINE_LENGTH - prefixText.length - truncationInfo.length; + + return prefixText + message.substring(0, availableLength) + truncationInfo; +} + +/** + * Logs a message with automatic truncation if it exceeds GitHub Actions limits + * @param message The message to log + * @param level The log level + * @param prefix Optional prefix for truncation message + */ +export function logMessageSafe(message: string, level?: LogLevel, prefix?: string): void { + const safeMessage = truncateLogMessage(message, prefix); + logMessage(safeMessage, level); +} diff --git a/eng/tools/openapi-diff-runner/src/run-oad.ts b/eng/tools/openapi-diff-runner/src/run-oad.ts new file mode 100644 index 000000000000..7cdf896fd6ba --- /dev/null +++ b/eng/tools/openapi-diff-runner/src/run-oad.ts @@ -0,0 +1,88 @@ +/** + * By design, the only member exported from this file is the runOad function. + * + * In the "breakingChanges directory invocation depth" this file has depth 3, + * i.e. it is invoked by files with depth 2. + */ +import * as oad from "@azure/oad"; +import { logMessage, logMessageSafe } from "./log.js"; +import { OadMessage } from "./types/oad-types.js"; + +/** + * The runOad() function is a wrapper around the "@azure/oad" library whose source is https://github.com/Azure/openapi-diff. + * + * runOad() is invoked by BreakingChangeDetector.doBreakingChangeDetection(). + * + * runOad() eventually invokes openApiDiff.compare() which calls into openapi-diff repo OpenApiDiff class compare() method [1], + * which obtains semantic model of the old / new (before / after) swagger file specs and then compares them + * via the C# OpenApiDiff.Program.Compare() method [2]. + * + * The OAD messages returned from "@azure/oad" invocation are then slightly post-processed and returned as OadMessage[] collection. + * + * See also: OadMessage type comment. + * + * [1] https://github.com/Azure/openapi-diff/blob/4c158308aca2cfd584e556fe8a05ce6967de2912/src/lib/validators/openApiDiff.ts#L128 + * [2] https://github.com/Azure/openapi-diff/blob/4c158308aca2cfd584e556fe8a05ce6967de2912/openapi-diff/src/core/OpenApiDiff/Program.cs#L40 + */ +export async function runOad( + oldSpec: string, + newSpec: string, + oldTag?: string, + newTag?: string, +): Promise { + logMessage( + `ENTER definition runOad oldSpec: ${oldSpec}, newSpec: ${newSpec}, oldTag: ${oldTag}, newTag: ${newTag}`, + ); + + if ( + oldSpec === null || + oldSpec === undefined || + typeof oldSpec.valueOf() !== "string" || + !oldSpec.trim().length + ) { + throw new Error( + 'oldSpec is a required parameter of type "string" and it cannot be an empty string.', + ); + } + + if ( + newSpec === null || + newSpec === undefined || + typeof newSpec.valueOf() !== "string" || + !newSpec.trim().length + ) { + throw new Error( + 'newSpec is a required parameter of type "string" and it cannot be an empty string.', + ); + } + + let oadCompareOutput: string; + if (oldTag && newTag) { + logMessage("oad.CompareTags() when (oldTag && newTag)"); + + oadCompareOutput = await oad.compareTags(oldSpec, oldTag, newSpec, newTag, { + consoleLogLevel: "warn", + }); + } else { + logMessage("oad.CompareTags() when !(oldTag && newTag)"); + + oadCompareOutput = await oad.compare(oldSpec, newSpec, { consoleLogLevel: "warn" }); + } + + logMessageSafe(`oadCompareOutput: ${oadCompareOutput}`); + + // The oadCompareOutput is emitted by this OAD source: + // OpenApiDiff.Program.Main(): + // https://github.com/Azure/openapi-diff/blob/7a3f705224e03de762689eeeb6d4f1b6820dc463/openapi-diff/src/core/OpenApiDiff/Program.cs#L40-L50 + // And each message is of type AutoRest.Swagger.ComparisonMessage: + // https://github.com/Azure/openapi-diff/blob/7a3f705224e03de762689eeeb6d4f1b6820dc463/openapi-diff/src/modeler/AutoRest.Swagger/ComparisonMessage.cs#L17-L17 + // after it was transformed by ComparisonMessage.GetValidationMessagesAsJson(). + const oadMessages: OadMessage[] = JSON.parse(oadCompareOutput); + + logMessage( + `RETURN definition runOad. oadMessages.length: ${oadMessages.length}, ` + + `oldSpec: ${oldSpec}, newSpec: ${newSpec}, oldTag: ${oldTag}, newTag: ${newTag}.`, + ); + + return oadMessages; +} diff --git a/eng/tools/openapi-diff-runner/src/types/breaking-change.ts b/eng/tools/openapi-diff-runner/src/types/breaking-change.ts new file mode 100644 index 000000000000..da3be82f8ded --- /dev/null +++ b/eng/tools/openapi-diff-runner/src/types/breaking-change.ts @@ -0,0 +1,78 @@ +import { + BREAKING_CHANGE_APPROVALS, + BREAKING_CHANGES_CHECK_TYPES, + REVIEW_REQUIRED_LABELS, + VERSIONING_APPROVALS, +} from "@azure-tools/specs-shared/breaking-change"; +import { OadMessageProcessorContext } from "../utils/oad-message-processor.js"; +import { PullRequestProperties } from "../utils/pull-request.js"; + +/** + * This file contains types used by the OpenAPI specification breaking change checks + * in the Azure/azure-rest-api-specs and Azure/azure-rest-api-specs-pr repositories. + * + * For additional context, see: + * + * - "Deep-dive into breaking changes on spec PRs" + * https://aka.ms/azsdk/pr-brch-deep + * + * - "[Breaking Change][PR Workflow] Use more granular labels for Breaking Changes approvals" + * https://github.com/Azure/azure-sdk-tools/issues/6374 + */ + +// Derive TypeScript types from the JavaScript constants +export type BreakingChangesCheckType = + (typeof BREAKING_CHANGES_CHECK_TYPES)[keyof typeof BREAKING_CHANGES_CHECK_TYPES]; + +// Export the constant values for use in TypeScript +export const BreakingChangeReviewRequiredLabel = REVIEW_REQUIRED_LABELS.BREAKING_CHANGE; +export const VersioningReviewRequiredLabel = REVIEW_REQUIRED_LABELS.VERSIONING; + +// Derive types from constants +export type ReviewRequiredLabel = + (typeof REVIEW_REQUIRED_LABELS)[keyof typeof REVIEW_REQUIRED_LABELS]; +export type ValidVersioningApproval = + (typeof VERSIONING_APPROVALS)[keyof typeof VERSIONING_APPROVALS]; +export type ValidBreakingChangeApproval = + (typeof BREAKING_CHANGE_APPROVALS)[keyof typeof BREAKING_CHANGE_APPROVALS]; +export type ReviewApprovalPrefixLabel = "Versioning-Approved-*" | "BreakingChange-Approved-*"; + +export type SpecsBreakingChangesLabel = + | ReviewRequiredLabel + | ReviewApprovalPrefixLabel + | ValidBreakingChangeApproval + | ValidVersioningApproval; + +/** Corresponds to specs in "*\preview\*" or "*\stable\*" directories in the specs repos. + * Scheduled to replace type SwaggerVersionType and type ComparedApiVersion. + * Read more at https://aka.ms/azsdk/spec-dirs + */ +export enum ApiVersionLifecycleStage { + PREVIEW = "preview", + STABLE = "stable", +} + +/** The name of the log file used by the openapi-diff-runner utility. */ +export const logFileName = "openapi-diff-runner.log"; + +/** + * Represents the input parameters for runner execution. + */ +export interface Context { + localSpecRepoPath: string; + workingFolder: string; + logFileFolder: string; + swaggerDirs: string[]; + baseBranch: string; + headCommit: string; + runType: BreakingChangesCheckType; + checkName: string; + targetRepo: string; // The format is: "owner/repoName" + sourceRepo: string; // The format is: "owner/repoName" + prNumber: string; + prSourceBranch: string; + prTargetBranch: string; + oadMessageProcessorContext: OadMessageProcessorContext; + prUrl: string; + prInfo?: PullRequestProperties; +} diff --git a/eng/tools/openapi-diff-runner/src/types/message.ts b/eng/tools/openapi-diff-runner/src/types/message.ts new file mode 100644 index 000000000000..95d222b793cf --- /dev/null +++ b/eng/tools/openapi-diff-runner/src/types/message.ts @@ -0,0 +1,109 @@ +/** + * Note this corresponds to Category enum in openapi-diff. + * For details, see comment on breakingChangeShared.ts / OadMessage.type. + */ +export type MessageLevel = "Info" | "Warning" | "Error"; + +// Instances of this type are created e.g. by the function oadMessagesToResultMessageRecords +export type JsonPath = { + // Example values of tag: "old" or "new" + tag: string; + // Example value of path: + // sourceBranchHref(this.context, oadMessage.new.location || ""), + // where this.context is of type PRContext and oadMessage is of type OadMessage + path: string; + // Example value of jsonPath: + // oadMessage.new?.path + // where oadMessage is of type OadMessage + jsonPath?: string | undefined; +}; + +export type MessageContext = { + toolVersion: string; +}; + +export type Extra = { + [key: string]: any; +}; + +export type BaseMessageRecord = { + level: MessageLevel; + message: string; + time: Date; + context?: MessageContext; + group?: string; + extra?: Extra; + groupName?: string; +}; + +/** See comment on type MessageRecord */ +export type ResultMessageRecord = BaseMessageRecord & { + type: "Result"; + id?: string; + code?: string; + docUrl?: string; + paths: JsonPath[]; +}; + +export type RawMessageRecord = BaseMessageRecord & { + type: "Raw"; +}; + +export type MarkdownMessageRecord = BaseMessageRecord & { + type: "Markdown"; + mode: "replace" | "append"; + detailMessage?: string; +}; + +/** + * Represents a record of detailed information coming out of one of the validation tools, + * like breaking change detector (OAD) or LintDiff. + * + * MessageRecords end up being printed in the contents of tables in relevant validation tool check in GitHub PR. + * These records are transferred from the Azure DevOps Azure.azure-rest-api-specs-pipeline build runs + * to the GitHub via pipe.log file (pipeline.ts / unifiedPipelineResultFileName) and Azure blob. + * + * The pipe.log gets uploaded to the blob via + * publishResult.ts / resultPublisher.uploadLog + * + * The blob contents are read by the pipeline-bot via + * resultComposer.ts / parseCompleteMessageData + * + * Examples: + * Save message record from OAD to pipe.log: + * doBreakingChangeDetection / appendOadMessages + * + * Save exception thrown by OAD to pipe.log, as MessageLine composed of RawMessageRecord[] + * doBreakingChangeDetection / catch block + * + * For details, see: + * https://dev.azure.com/azure-sdk/internal/_wiki/wikis/internal.wiki/1011/How-the-data-in-breaking-change-GH-check-tables-is-getting-populated + */ +export type MessageRecord = ResultMessageRecord | RawMessageRecord | MarkdownMessageRecord; + +/** + * Represents a message record that is either a result of a validation rule violation or a raw message + * (e.g. an AutoRest exception). + * + * These records originate from "runOad" invocation. + */ +export type BrChMsgRecord = ResultMessageRecord | RawMessageRecord; + +export function getKey(msg: BrChMsgRecord): string { + if (msg.type === "Result") { + return [msg.id, msg.code].filter((s) => s).join(" - "); + } else { + // Example value of msgRecord.message here: "Runtime Exception" + return msg.message; + } +} + +/** + * See type MessageRecord + */ +export type MessageLine = MessageRecord | MessageRecord[]; + +export type FilePosition = { + readonly line: number; + readonly column: number; +}; diff --git a/eng/tools/openapi-diff-runner/src/types/oad-types.ts b/eng/tools/openapi-diff-runner/src/types/oad-types.ts new file mode 100644 index 000000000000..0be7ae36c42c --- /dev/null +++ b/eng/tools/openapi-diff-runner/src/types/oad-types.ts @@ -0,0 +1,180 @@ +/** + * The file breakingChangeShared.ts contains members that: + * - are shared across 2 or more files in the "breakingChanges" folder. + * - AND do not depend on any members from beyond this file, except "@azure/swagger-validation-common". + * - "Deep-dive into breaking changes on spec PRs" + * https://aka.ms/azsdk/pr-brch-deep + * + * - "[Breaking Change][PR Workflow] Use more granular labels for Breaking Changes approvals" + * https://github.com/Azure/azure-sdk-tools/issues/6374 + */ +import { readFileSync } from "node:fs"; +import { basename, dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + getVersionFromInputFile, + sourceBranchHref, + specificBranchHref, +} from "../utils/common-utils.js"; +import { ApiVersionLifecycleStage, Context } from "./breaking-change.js"; +import { MessageLevel } from "./message.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const packageJson = JSON.parse(readFileSync(join(__dirname, "../../../package.json"), "utf-8")); +/** + * A type that represents AutoRest.Swagger.ComparisonMessage from OAD + * after being transformed by ComparisonMessage.GetValidationMessagesAsJson(). + * + * An OadMessage gets converted to ResultMessageRecord by the function oadMessagesToResultMessageRecords. + * + * For origin of data of this type and OAD source links, see comments in: + * runOad.ts + */ +export type OadMessage = { + readonly id: string; + readonly code: OadRuleCode; + readonly docUrl: string; + readonly message: string; + readonly mode: string; // "mode" is assigned from MessageType enum in openapi-diff, which is ["Addition", "Update", Removal"] + readonly type: MessageLevel; // "type" is assigned from openapi-diff's "Severity" , which is openapi-diff's Category enum, which is ["Info", "Warning", "Error"] + readonly new: ChangeProperties; + readonly old: ChangeProperties; + readonly groupName?: ApiVersionLifecycleStage; +}; + +export type ChangeProperties = { + readonly location?: string; + readonly path?: string; + readonly ref?: string; +}; + +/** + * Represents the state of OAD tracing with immutable data structure + */ +export type OadTraceData = { + readonly traces: readonly { + readonly old: string; + readonly new: string; + }[]; + readonly baseBranch: string; + readonly context: Context; +}; + +/** + * Creates a new OAD trace data structure + */ +export const createOadTrace = (context: Context): OadTraceData => ({ + traces: [], + baseBranch: context.baseBranch, + context, +}); + +/** + * Adds a new trace entry to the OAD trace data + */ +export const addOadTrace = ( + traceData: OadTraceData, + oldSwagger: string, + newSwagger: string, +): OadTraceData => + ({ + ...traceData, + traces: [...traceData.traces, { old: oldSwagger, new: newSwagger }], + }) as OadTraceData; + +/** + * Sets the base branch for the OAD trace data + */ +export const setOadBaseBranch = (traceData: OadTraceData, branchName: string): OadTraceData => ({ + ...traceData, + baseBranch: branchName, +}); + +/** + * Generates markdown content from OAD trace data + */ +export const generateOadMarkdown = (traceData: OadTraceData): string => { + const oadVersion = packageJson.dependencies?.["@azure/oad"]?.replace(/[\^~]/, "") || "unknown"; + if (traceData.traces.length === 0) { + return ""; + } + + // Create properly formatted markdown table without leading whitespace + let content = `| Compared specs ([v${oadVersion}](https://www.npmjs.com/package/@azure/oad/v/${oadVersion})) | new version | base version | +|-------|-------------|--------------| +`; + + for (const value of traceData.traces) { + // Compose each column for clarity + const newFileName = basename(value.new); + const newVersion = getVersionFromInputFile(value.new, true); + + // Truncate commit hash to first 8 characters for better readability + const shortCommit = traceData.context.headCommit.substring(0, 8); + const newCommitLink = `[${shortCommit}](${sourceBranchHref(traceData.context.sourceRepo, traceData.context.headCommit, value.new)})`; + + const oldVersion = getVersionFromInputFile(value.old, true); + const oldCommitLink = `[${traceData.baseBranch}](${specificBranchHref(traceData.context.targetRepo, value.old, traceData.baseBranch)})`; + + // Add a row to the markdown table with proper spacing + content += `| ${newFileName} | ${newVersion} (${newCommitLink}) | ${oldVersion} (${oldCommitLink}) | +`; + } + + return content; +}; + +// Codes correspond to members of openapi-diff ComparisonMessages: +// https://github.com/Azure/openapi-diff/blob/4c158308aca2cfd584e556fe8a05ce6967de2912/openapi-diff/src/modeler/AutoRest.Swagger/ComparisonMessages.cs +export type OadRuleCode = + | "AddedAdditionalProperties" + | "AddedEnumValue" + | "AddedOperation" + | "AddedOptionalProperty" + | "AddedPath" + | "AddedPropertyInResponse" + | "AddedReadOnlyPropertyInResponse" + | "AddedRequiredProperty" + | "AddedXmsEnum" + | "AddingHeader" + | "AddingOptionalParameter" + | "AddingRequiredParameter" + | "AddingResponseCode" + | "ArrayCollectionFormatChanged" + | "ChangedParameterOrder" + | "ConstantStatusHasChanged" + | "ConstraintChanged" + | "ConstraintIsStronger" + | "ConstraintIsWeaker" + | "DefaultValueChanged" + | "DifferentAllOf" + | "DifferentDiscriminator" + | "DifferentExtends" + | "ModifiedOperationId" + | "NoVersionChange" + | "ParameterInHasChanged" + | "ParameterLocationHasChanged" + | "ProtocolNoLongerSupported" + | "ReadonlyPropertyChanged" + | "ReferenceRedirection" + | "RemovedAdditionalProperties" + | "RemovedClientParameter" + | "RemovedDefinition" + | "RemovedEnumValue" + | "RemovedOperation" + | "RemovedOptionalParameter" + | "RemovedPath" + | "RemovedProperty" + | "RemovedRequiredParameter" + | "RemovedResponseCode" + | "RemovedXmsEnum" + | "RemovingHeader" + | "RequestBodyFormatNoLongerSupported" + | "RequiredStatusChange" + | "ResponseBodyFormatNowSupported" + | "TypeChanged" + | "TypeFormatChanged" + | "VersionsReversed" + | "XmsEnumChanged" + | "XmsLongRunningOperationChanged"; diff --git a/eng/tools/openapi-diff-runner/src/utils/apply-rules.ts b/eng/tools/openapi-diff-runner/src/utils/apply-rules.ts new file mode 100644 index 000000000000..cf44c6e61445 --- /dev/null +++ b/eng/tools/openapi-diff-runner/src/utils/apply-rules.ts @@ -0,0 +1,133 @@ +/** + * By design, the only member exported from this file is the applyRules() function. + * + * In the "breakingChanges directory invocation depth" this file has depth 3 + * i.e. it is invoked by files with depth 2. + */ +import { BreakingChangeLabelsToBeAdded } from "../command-helpers.js"; +import { + ApiVersionLifecycleStage, + BreakingChangesCheckType, + ReviewRequiredLabel, + VersioningReviewRequiredLabel, +} from "../types/breaking-change.js"; +import { OadMessage } from "../types/oad-types.js"; + +import { BREAKING_CHANGES_CHECK_TYPES } from "@azure-tools/specs-shared/breaking-change"; +import { logMessage, logWarning } from "../log.js"; +import { + OadMessageRule, + fallbackLabel, + fallbackRule as fallbackOadMessageRule, + oadMessagesRuleMap, +} from "./oad-rule-map.js"; + +/** + * The function applyRules() applies oadMessagesRuleMap to OAD messages returned by runOad(). + * As a result each OadMessage severity ("type" property) is set and appropriate "review required" labels are + * scheduled to be added. In addition, the API version lifecycle stage is added to the OadMessages so + * they e.g. render correctly in user UI. + * + * This function is invoked by the BreakingChangeDetector.doBreakingChangeDetection() + */ +export function applyRules( + oadMessages: OadMessage[], + scenario: BreakingChangesCheckType, + previousApiVersionLifecycleStage: ApiVersionLifecycleStage, +): OadMessage[] { + logMessage("ENTER definition applyRules"); + let outputOadMessages: OadMessage[] = []; + let outputOadMessage: OadMessage; + + for (const oadMessage of oadMessages) { + const rule: OadMessageRule | undefined = oadMessagesRuleMap.find( + (rule) => rule.scenario == scenario && rule.code == oadMessage.code, + ); + + if (rule !== undefined) { + outputOadMessage = applyRule(oadMessage, rule, previousApiVersionLifecycleStage); + } else { + logWarning( + `ASSERTION VIOLATION! No rule found for scenario: '${scenario}', oadMessage: '${JSON.stringify( + oadMessage, + )}'. Using fallback rule: '${JSON.stringify(fallbackOadMessageRule)}'.`, + ); + outputOadMessage = applyRule( + oadMessage, + { ...fallbackOadMessageRule, scenario }, + previousApiVersionLifecycleStage, + ); + } + + // The groupName is used to render the message in appropriate place in GitHub check page. + // See: buildCompletedBreakingChangeCheckResult.ts / getReports() + outputOadMessage = { + ...outputOadMessage, + groupName: previousApiVersionLifecycleStage, + }; + outputOadMessages.push(outputOadMessage); + } + + logMessage("RETURN definition applyRules"); + return outputOadMessages; +} + +function applyRule( + oadMessage: OadMessage, + rule: Omit, + previousApiVersionLifecycleStage: ApiVersionLifecycleStage, +): OadMessage { + const isSameVersionOnPreview = + previousApiVersionLifecycleStage === "preview" && + rule.scenario === BREAKING_CHANGES_CHECK_TYPES.SAME_VERSION; + + // Comparing against previous previews always decreases failure severity from error to warning. + // The fact we set this to true corresponds to the green "Ignore" rectangle for + // ("Cross-version" check / "Previous Preview" row / "Breaking Change" column) + // at https://aka.ms/azsdk/pr-brch-deep#diagram-explaining-breaking-changes-and-versioning-issues + // See also: + // https://github.com/Azure/azure-sdk-tools/issues/6396 + const isCrossVersionAgainstPreviousPreview = + previousApiVersionLifecycleStage === "preview" && + rule.scenario === BREAKING_CHANGES_CHECK_TYPES.CROSS_VERSION; + + const appliedSeverity = + rule.severity === "Error" && isCrossVersionAgainstPreviousPreview ? "Warning" : rule.severity; + + const addLabel = appliedSeverity === "Error"; + + let labelToAdd: ReviewRequiredLabel = fallbackLabel; + if (addLabel) { + if (rule.label == null) { + logWarning( + `ASSERTION VIOLATION! Missing "label" for 'Error' severity rule '${JSON.stringify( + rule, + )}'. Using fallback label '${labelToAdd}'.`, + ); + } else { + labelToAdd = rule.label; + } + + // This is the "Same-version check / Preview" row at: + // https://aka.ms/azsdk/pr-brch-deep#diagram-explaining-breaking-changes-and-versioning-issues + // Specifically, it overrides the "BreakingChangeReviewRequired" label in the "Breaking change" column. + labelToAdd = isSameVersionOnPreview ? VersioningReviewRequiredLabel : labelToAdd; + + // Note: these labels are processed downstream by addBreakingChangeLabelsToBeAdded(). + BreakingChangeLabelsToBeAdded.add(labelToAdd); + } + + // Here by design the oadMessage.type is ignored and overridden by appliedSeverity. + const outputOadMessage: OadMessage = { + ...oadMessage, + type: appliedSeverity, + }; + + logMessage( + `applyRule: addLabel: ${addLabel}, labelToAdd: ${labelToAdd}, rule: '${JSON.stringify( + rule, + )}', outputOadMessage: '${JSON.stringify(outputOadMessage)}'.`, + ); + + return outputOadMessage; +} diff --git a/eng/tools/openapi-diff-runner/src/utils/common-utils.ts b/eng/tools/openapi-diff-runner/src/utils/common-utils.ts new file mode 100644 index 000000000000..28ddbc8e1dd7 --- /dev/null +++ b/eng/tools/openapi-diff-runner/src/utils/common-utils.ts @@ -0,0 +1,227 @@ +import { existsSync, readFileSync } from "node:fs"; +import { Context } from "../types/breaking-change.js"; +import { FilePosition } from "../types/message.js"; + +export function blobHref(repo: string, sha: string, file: string): string { + return `https://github.com/${repo}/blob/${sha}/${file}`; +} + +/** + * Get the Github url of targeted swagger file. + * @param repo The repository name in the format "owner/repoName" + * @param branch The branch name, e.g. "main" + * @param file Swagger file starts with "specification" + */ +export function targetHref(repo: string, branch: string, file: string) { + return file ? `https://github.com/${repo}/blob/${branch}/${file}` : ""; +} + +export function branchHref(repo: string, file: string, branch: string = "main") { + return file ? `https://github.com/${repo}/blob/${branch}/${file}` : ""; +} + +/** + * For breaking change. Trim file path pattern to github style. + * E.g. Input: specification/redis/resource-manager/Microsoft.Cache/preview/2019-07-01/redis.json:191:5 + * Output: specification/redis/resource-manager/Microsoft.Cache/preview/2019-07-01/redis.json#L191:5 + * @param filePath + * + */ +export function getGithubStyleFilePath(filePath: string, filePos?: FilePosition): string { + if (filePos) { + return `${filePath}#L${filePos.line}:${filePos.column}`; + } + const regex = /(:)/; + return filePath.replace(regex, "#L"); +} + +export function getRelativeSwaggerPathToRepo( + filePath: string, + specDirPatterns: string[] = ["specification"], +): string { + const pattern = specDirPatterns.find((f) => filePath.indexOf(f) !== -1); + if (!pattern) { + return filePath.substring(process.env.BUILD_SOURCEDIRECTORY?.length || 1 - 1); + } + const position = filePath.search(pattern); + return filePath.substring(position, filePath.length); +} + +export function sourceBranchHref( + repo: string, + sha: string, + file: string, + filePos?: FilePosition, +): string { + return blobHref(repo, sha, getGithubStyleFilePath(getRelativeSwaggerPathToRepo(file), filePos)); +} + +export function targetBranchHref( + repo: string, + branch: string, + file: string, + filePos?: FilePosition, +): string { + return targetHref( + repo, + branch, + getGithubStyleFilePath(getRelativeSwaggerPathToRepo(file), filePos), + ); +} + +export function specificBranchHref( + repo: string, + file: string, + branchName: string, + filePos?: FilePosition, +): string { + return branchHref( + repo, + getGithubStyleFilePath(getRelativeSwaggerPathToRepo(file), filePos), + branchName, + ); +} + +export function getVersionFromInputFile(filePath: string, withPreview = false): string { + const apiVersionRegex = /^\d{4}-\d{2}-\d{2}(|-preview|-privatepreview|-alpha|-beta|-rc)$/; + + // Normalize path separators to forward slashes for consistent processing + const normalizedPath = filePath.replace(/\\/g, "/"); + const segments = normalizedPath.split("/"); + + if (normalizedPath.indexOf("data-plane") !== -1) { + if (segments && segments.length > 1) { + for (const s of segments.entries()) { + if (["stable", "preview"].some((v) => v === s[1])) { + const version = segments[s[0] + 1]; + if (version) { + return apiVersionRegex.test(version) && !withPreview + ? version.substring(0, 10) + : version; + } + } + } + } + } else { + if (segments && segments.length > 1) { + for (const s of segments) { + if (apiVersionRegex.test(s)) { + return withPreview ? s : s.substring(0, 10); + } + } + } + } + + // If no regex match found, try to read version from file content + if (existsSync(filePath)) { + const fileContent = readFileSync(filePath, "utf8"); + const parsedContent = JSON.parse(fileContent); + return parsedContent?.info?.version || ""; + } + + return ""; +} + +export function getArgumentValue(args: string[], flag: string, defaultValue: string): string { + const index = args.indexOf(flag); + return index !== -1 && index + 1 < args.length && args[index + 1] + ? args[index + 1] + : defaultValue; +} + +/** + * Truncates the input message to a specified maximum size. + * @param msg The message to be truncated. + * @param size The maximum length of the returned string. Defaults to 1024. + * @returns The truncated message, or an empty string if msg is undefined. + */ +export function cutoffMsg(msg: string | undefined, size: number = 1024): string { + if (!msg || msg.length <= size) { + return msg ? msg : ""; + } + return msg.substring(0, size); +} + +/** + * Post-processes the message of an error coming from OAD. + * Notably, the kind of errors that need post-processing is when OAD + * throws a runtime error because it has invoked AutoRest, it threw, + * and OAD has re-thrown it. + * We want to make such errors more readable, which we do here. + * Issue capturing this work and providing more context: + * https://github.com/Azure/azure-sdk-tools/issues/6998 + */ +export function processOadRuntimeErrorMessage( + message: string, + stackTraceMaxLength: number, +): string { + let outputMsg: string = ""; + + // Example "message" string, truncated with cutoffMsg(): + // + // Command failed: node "/mnt/vss/_work/_tasks/AzureApiValidation_5654d05d-82c1-48da-ad8f-161b817f6d41/0.0.59/common/temp/node_modules/.pnpm/https://github.com/Azure+oad@0.10.4/node_modules/autorest/dist/app.js" --v2 --input-file=specification/servicebus/resource-manager/Microsoft.ServiceBus/preview/2023-01-01-preview/namespace-preview.json --output-artifact=swagger-document.json --output-artifact=swagger-document.map --output-file=new --output-folder=/tmp\nERROR: Schema violation: Data does not match any schemas from 'oneOf'\n - file:///mnt/vss/_work/1/azure-rest-api-specs/specification/servicebus/resource-manager/Microsoft.ServiceBus/preview/2023-01-01-preview/namespace-preview.json:347:10 ($.paths["/subscriptions/subscriptionId/resourceGroups/resourceGroupName/providers/Microsoft.ServiceBus/namespaces/namespaceName/failover"].post["x-ms-examples"].NamespaceFailOver)\nFATAL: swagger-document/individual/schema-validator - FAILED\nFATAL: Error: [OperationAbortedException] Error occurred. Exiting.\nProcess() cancelled due to e + // + const oadAutorestInvocationRuntimeError = + message.startsWith("Command failed: node") && message.includes("autorest/dist/app.js"); + + if (oadAutorestInvocationRuntimeError) { + let lines: string[] = message.split(/[\r\n]+/); + + const introLine: string = + `Breaking change detector (OAD) invoked AutoRest. AutoRest threw a runtime error. ` + + `First ${stackTraceMaxLength} lines of stack trace follow, indexed. ` + + `First line should contain AutoRest command line invocation details. ` + + `Remaining lines should contain the main message reported by AutoRest.`; + + const stackTraceLines: string[] = lines + .filter((line) => line.length > 0) + .slice(0, Math.min(stackTraceMaxLength, lines.length)) + .map((line, i) => `${i + 1}: ${line}`); + + outputMsg = [introLine, "===================="] + .concat(stackTraceLines) + // We join with '
    ' as this will display correctly as a line break inside + // a cell of a table generated in given GitHub check description. + // This '
    ' will be interpreted downstream by + // 'msgInterfaceUtils.ts / commonHelper.renderExtra', + // as called by the 'checker.handlebars' template. + .join("
    "); + } else { + outputMsg = cutoffMsg(message) || ""; + } + return outputMsg; +} + +/** + * Check if a spec path is a preview version + * @param specPath The specification file path to check. + * @returns {boolean} True if the spec path is a preview version, false otherwise. + */ +export function specIsPreview(specPath: string): boolean { + // Example input value: specification/maps/data-plane/Creator/preview/2022-09-01-preview/wayfind.json + return specPath.includes("/preview/") && !specPath.includes("/stable/"); +} + +export function convertRawErrorToUnifiedMsg( + context: Context, + errType: string, + errorMsg: string, + levelType = "Error", + location: string | undefined = undefined, +): string { + const extra: { [index: string]: string } = {}; + extra.details = errorMsg; + if (location) { + extra.location = targetBranchHref(context.targetRepo, context.prTargetBranch, location); + } + const result = { + type: "Raw", + level: levelType, + message: errType, + time: new Date(), + extra: { + ...extra, + }, + }; + return JSON.stringify(result); +} diff --git a/eng/tools/openapi-diff-runner/src/utils/markdown-report-row.ts b/eng/tools/openapi-diff-runner/src/utils/markdown-report-row.ts new file mode 100644 index 000000000000..cf6276e77583 --- /dev/null +++ b/eng/tools/openapi-diff-runner/src/utils/markdown-report-row.ts @@ -0,0 +1,137 @@ +import { BrChMsgRecord, JsonPath, ResultMessageRecord } from "../types/message.js"; + +/** + * Represents a single row in the markdown table for GitHub check pane + */ +export interface BreakingChangeMdRow { + readonly msg: BrChMsgRecord; + readonly index: number; + readonly description: string; +} + +/** + * Creates markdown rows from breaking change messages + */ +export function createBreakingChangeMdRows(msgs: BrChMsgRecord[]): BreakingChangeMdRow[] { + const rows = msgs.map((msg, index) => ({ + msg, + index: index + 1, + description: getDescriptionColumn(msg), + })); + + return sortBreakingChangeMdRows(rows); +} + +/** + * Gets the markdown table header + */ +export function getMdTableHeader(): string { + return "| Index | Description |\n|-|-|\n"; +} + +/** + * Creates a deficit row for omitted occurrences + */ +export function getDeficitRow(deficit: number): string { + return `|| ⚠️ ${deficit} occurrence${deficit > 1 ? "s" : ""} omitted. See the build log.|\n`; +} + +/** + * Converts a row to markdown table row string + */ +export function rowToString(row: BreakingChangeMdRow): string { + return `| ${row.index} | ${row.description} |\n`; +} + +/** + * Gets the description column content for a message + */ +function getDescriptionColumn(msg: BrChMsgRecord): string { + if (msg.type === "Result") { + return getDescription(msg); + } else { + return getExtra(msg); + } +} + +/** + * Gets the full description including message and location + */ +function getDescription(msg: BrChMsgRecord): string { + return `${getMessage(msg)}
    ${getLocation(msg)}`; +} + +/** + * Gets the cleaned message text + */ +function getMessage(msg: BrChMsgRecord): string { + const re = /(\n|\t|\r)/gi; + return msg.message.replace(re, " "); +} + +/** + * Gets the location information for the message + */ +function getLocation(msg: BrChMsgRecord): string { + const paths: JsonPath[] = (msg as ResultMessageRecord).paths; + return paths + .filter((p) => p.path) + .map( + (p) => + `${p.tag}: [${getPathSegment(p.path)}](${p.path})${getJsonPathForNewOrOldTag(paths, p)}`, + ) + .join("
    "); +} + +/** + * Gets the last 4 segments of a path + */ +function getPathSegment(path: string): string { + return path.split("/").slice(-4).join("/"); +} + +/** + * Gets the JSON path for new or old tags based on availability + */ +function getJsonPathForNewOrOldTag(allPaths: JsonPath[], targetPath: JsonPath): string { + const newPath = allPaths.find((p) => p.tag === "New"); + const newJsonPath = newPath?.jsonPath; + const newJsonPathPresent = newJsonPath != null && newJsonPath !== ""; + + const oldPath = allPaths.find((p) => p.tag === "Old"); + const oldJsonPath = oldPath?.jsonPath; + const oldJsonPathPresent = oldJsonPath != null && oldJsonPath !== ""; + + if (targetPath.tag === "New" && newJsonPathPresent) { + return prettyPrintJsonPath(newJsonPath); + } + + if (targetPath.tag === "Old" && !newJsonPathPresent && oldJsonPathPresent) { + return prettyPrintJsonPath(oldJsonPath); + } + + return ""; +} + +/** + * Pretty prints a JSON path with HTML formatting + */ +function prettyPrintJsonPath(jsonPath: string): string { + return `
    ${jsonPath}`; +} + +/** + * Gets the extra information for non-Result messages + */ +function getExtra(msg: BrChMsgRecord): string { + return JSON.stringify(msg.extra || {}) + .replace(/[{}]/g, "") + .replace(/,/g, ",
    "); +} + +/** + * Sorts breaking change markdown rows by description + */ +function sortBreakingChangeMdRows(rows: BreakingChangeMdRow[]): BreakingChangeMdRow[] { + return rows.sort((row1, row2) => row1.index - row2.index); +} diff --git a/eng/tools/openapi-diff-runner/src/utils/markdown-report.ts b/eng/tools/openapi-diff-runner/src/utils/markdown-report.ts new file mode 100644 index 000000000000..2680f39a5c78 --- /dev/null +++ b/eng/tools/openapi-diff-runner/src/utils/markdown-report.ts @@ -0,0 +1,232 @@ +import { LogLevel, logMessage } from "../log.js"; +import { BrChMsgRecord, MessageLevel, ResultMessageRecord, getKey } from "../types/message.js"; +import { + createBreakingChangeMdRows, + getDeficitRow, + getMdTableHeader, + rowToString, +} from "./markdown-report-row.js"; + +/** + * Represents a markdown report for breaking change violations + */ +export interface BreakingChangeMdReport { + readonly msgs: BrChMsgRecord[]; + readonly rows: string[]; + readonly type: BrChMsgRecord["type"]; + readonly level: MessageLevel; + readonly id?: string; + readonly rawMessage: string; +} + +/** + * Type mappings for sorting + */ +const TYPE_ORDER_MAP: Record = { + Raw: 0, + Result: 1, +}; + +const LEVEL_ORDER_MAP: Record = { + Info: 0, + Warning: 1, + Error: 2, +}; + +/** + * Creates a breaking change markdown report + */ +export function createBreakingChangeMdReport(msgs: BrChMsgRecord[]): BreakingChangeMdReport { + // Validation + validateMessages(msgs); + + const rows = buildRows(msgs); + const firstMsg = msgs[0]; + + return { + msgs, + rows, + type: firstMsg.type, + level: firstMsg.level, + id: firstMsg.type === "Result" ? firstMsg.id : undefined, + rawMessage: firstMsg.type === "Raw" ? firstMsg.message : "", + }; +} + +/** + * Sorts breaking change reports according to specified criteria + */ +export function sortBreakingChangeMdReports( + reports: BreakingChangeMdReport[], +): BreakingChangeMdReport[] { + return reports.sort((rep1, rep2) => { + const typeCompare = compareByType(rep1, rep2); + if (typeCompare !== 0) return typeCompare; + + const levelCompare = compareByLevel(rep1, rep2); + if (levelCompare !== 0) return -levelCompare; + + const idCompare = compareById(rep1, rep2); + if (idCompare !== 0) return idCompare; + + return 0; + }); +} + +/** + * Gets the length of the report when rendered as string + */ +export function getReportLength(report: BreakingChangeMdReport, maxRowCount: number): number { + return reportToString(report, maxRowCount).length; +} + +/** + * Converts a report to markdown string + */ +export function reportToString(report: BreakingChangeMdReport, maxRowCount: number): string { + return ( + getPreamble(report, maxRowCount) + getMdTableHeader() + getRows(report, maxRowCount).join("") + ); +} + +/** + * Gets the number of rows in the report + */ +export function getRowCount(report: BreakingChangeMdReport): number { + return report.rows.length; +} + +/** + * Validates the input messages + */ +function validateMessages(msgs: BrChMsgRecord[]): void { + if (msgs.length === 0) { + logMessage(`BreakingChangeMdReport: ASSERTION VIOLATION! msgs are of length 0.`, LogLevel.Warn); + } + + if (msgs.some((msg) => msg.type !== msgs[0].type)) { + logMessage( + `BreakingChangeMdReport: ASSERTION VIOLATION! Not all messages have the same type. msgs[0].type = ${msgs[0].type}.`, + LogLevel.Warn, + ); + } + + if (msgs.some((msg) => msg.groupName !== msgs[0].groupName)) { + logMessage( + `BreakingChangeMdReport: ASSERTION VIOLATION! Not all messages have the same groupName. msgs[0].groupName = '${msgs[0].groupName}'.`, + LogLevel.Warn, + ); + } +} + +/** + * Builds the markdown table rows + */ +function buildRows(msgs: BrChMsgRecord[]): string[] { + return createBreakingChangeMdRows(msgs).map(rowToString); +} + +/** + * Gets the preamble text for the report + */ +function getPreamble(report: BreakingChangeMdReport, maxRowCount: number): string { + const ruleColumn = getRuleColumn(report.msgs[0]); + const mainMsg = `## ${ruleColumn}\nDisplaying ${Math.min(maxRowCount, report.msgs.length)} out of ${report.msgs.length} occurrences.\n`; + const moreRowsMsg = + report.msgs.length > maxRowCount + ? `⚠️ To view the remaining ${report.msgs.length - maxRowCount} occurrences, see the build log.\n` + : ""; + return mainMsg + moreRowsMsg; +} + +/** + * Gets the rows to display, including deficit row if needed + */ +function getRows(report: BreakingChangeMdReport, maxRowCount: number): string[] { + let rows = report.rows.slice(0, maxRowCount); + const deficit = report.msgs.length - maxRowCount; + if (deficit > 0) { + rows = rows.concat([getDeficitRow(deficit)]); + } + return rows; +} + +/** + * Gets the rule column text + */ +function getRuleColumn(msg: BrChMsgRecord): string { + if (msg.type === "Result") { + return getRuleName(msg); + } else { + return `${getMark(msg)} ${msg.message}`; + } +} + +/** + * Gets the rule name with link + */ +function getRuleName(msg: ResultMessageRecord): string { + return `${getMark(msg)} [${getKey(msg)}](${msg.docUrl})`; +} + +/** + * Gets the emoji mark for the message level + */ +function getMark(msgRecord: BrChMsgRecord): string { + switch (msgRecord.level) { + case "Error": + return ""; + case "Info": + return ":speech_balloon:"; + case "Warning": + return ":warning:"; + } +} + +/** + * Compares reports by type + */ +function compareByType(rep1: BreakingChangeMdReport, rep2: BreakingChangeMdReport): number { + return TYPE_ORDER_MAP[rep1.type] - TYPE_ORDER_MAP[rep2.type]; +} + +/** + * Compares reports by message level + */ +function compareByLevel(rep1: BreakingChangeMdReport, rep2: BreakingChangeMdReport): number { + return LEVEL_ORDER_MAP[rep1.level] - LEVEL_ORDER_MAP[rep2.level]; +} + +/** + * Compares reports by ID or message + */ +function compareById(rep1: BreakingChangeMdReport, rep2: BreakingChangeMdReport): number { + if (rep1.type === "Result" && rep2.type === "Result") { + const thisId = getIntFromId(rep1.id); + const otherId = getIntFromId(rep2.id); + if (typeof thisId === "number" && typeof otherId === "number") { + return thisId - otherId; + } + return getStrFromId(rep1.id).localeCompare(getStrFromId(rep2.id)); + } + if (rep1.type === "Raw" && rep2.type === "Raw") { + return rep1.rawMessage.localeCompare(rep2.rawMessage); + } + return 0; +} + +/** + * Converts ID string to number if possible + */ +function getIntFromId(id?: string): number | undefined { + const idStr = getStrFromId(id); + const idInt = parseInt(idStr); + return isNaN(idInt) ? undefined : idInt; +} + +/** + * Gets string representation of ID + */ +function getStrFromId(id?: string): string { + return id ?? ""; +} diff --git a/eng/tools/openapi-diff-runner/src/utils/oad-message-processor.ts b/eng/tools/openapi-diff-runner/src/utils/oad-message-processor.ts new file mode 100644 index 000000000000..0e753463307e --- /dev/null +++ b/eng/tools/openapi-diff-runner/src/utils/oad-message-processor.ts @@ -0,0 +1,192 @@ +import fs from "node:fs"; +import path from "node:path"; +import { logMessage, logMessageSafe } from "../log.js"; +import { Context, logFileName } from "../types/breaking-change.js"; +import { JsonPath, MessageLevel, ResultMessageRecord } from "../types/message.js"; +import { OadMessage } from "../types/oad-types.js"; +import { sourceBranchHref, specificBranchHref } from "./common-utils.js"; + +/** + * Context for OAD message processing operations + */ +export interface OadMessageProcessorContext { + logFilePath: string; + prUrl: string; + messageCache: OadMessage[]; +} + +/** + * Convert OAD messages to result message records + */ +export function convertOadMessagesToResultMessageRecords( + context: Context, + messages: OadMessage[], + baseBranchName: string, +): ResultMessageRecord[] { + return messages.map((oadMessage) => { + // These paths will be printed out to GitHub check pane table row by invocations to + // BreakingChangeMdRow.ts / getLocation + const paths: JsonPath[] = []; + if (oadMessage.new.location) { + paths.push({ + tag: "New", + path: sourceBranchHref( + context.sourceRepo, + context.headCommit, + oadMessage.new.location || "", + ), + jsonPath: oadMessage.new?.path, + }); + } + if (oadMessage.old.location) { + paths.push({ + tag: "Old", + path: specificBranchHref(context.targetRepo, oadMessage.old.location || "", baseBranchName), + jsonPath: oadMessage.old?.path, + }); + } + return { + type: "Result", + level: oadMessage.type as MessageLevel, + message: oadMessage.message, + code: oadMessage.code, + id: oadMessage.id, + docUrl: oadMessage.docUrl, + time: new Date(), + groupName: oadMessage.groupName, + extra: { + mode: oadMessage.mode, + }, + paths: paths, + } as ResultMessageRecord; + }); +} + +/** + * Create a new OAD message processor context + */ +export function createOadMessageProcessor( + logFileFolder: string, + prUrl: string, +): OadMessageProcessorContext { + const logFilePath = path.join(logFileFolder || ".", logFileName); + + // Remove the log file if it exists + if (fs.existsSync(logFilePath)) { + fs.unlinkSync(logFilePath); + } + + // Create the log file explicitly + fs.writeFileSync(logFilePath, ""); + + return { + logFilePath, + prUrl, + messageCache: [], + }; +} + +/** + * Create a deterministic key for an OAD message to enable deduplication + */ +export function createMessageKey(message: OadMessage): string { + // Create a deterministic key based on message properties that define uniqueness + // Adjust these properties based on what makes an OadMessage unique + return JSON.stringify({ + code: message.code, + message: message.message, + type: message.type, + mode: message.mode, + newLocation: message.new?.location, + oldLocation: message.old?.location, + newPath: message.new?.path, + oldPath: message.old?.path, + // Add other properties that determine uniqueness + }); +} + +/** + * Append a message to the log file + */ +export function appendToLogFile(logFilePath: string, msg: string): void { + fs.appendFileSync(logFilePath, msg); + fs.appendFileSync(logFilePath, "\n"); + logMessageSafe("oad-message-processor.appendMsg: " + msg); +} + +/** + * Append markdown content to the log file + */ +export function appendMarkdownToLog( + context: OadMessageProcessorContext, + errorMsg: string, + levelType = "Error", +): void { + const markdownRecord = JSON.stringify({ + type: "Markdown", + mode: "append", + level: levelType, + message: errorMsg, + time: new Date(), + }); + appendToLogFile(context.logFilePath, markdownRecord); +} + +/** + * Process and deduplicate OAD messages, then append to log + * This function is invoked by BreakingChangeDetector.doBreakingChangeDetection() + */ +export function processAndAppendOadMessages( + context: Context, + oadMessages: OadMessage[], + baseBranch: string, +): ResultMessageRecord[] { + // Use Set for O(1) lookup instead of O(n) array operations + const cacheKeys = new Set( + context.oadMessageProcessorContext.messageCache.map((msg) => createMessageKey(msg)), + ); + + // Filter out duplicates - O(n) instead of O(n²) + const dedupedOadMessages = oadMessages.filter((oadMessage) => { + const key = createMessageKey(oadMessage); + return !cacheKeys.has(key); + }); + + // Count duplicates for logging + const duplicateCount = oadMessages.length - dedupedOadMessages.length; + // We are using this log as a metric to track and measure impact of the work on improving "breaking changes" tooling. + // Log statement added around 12/5/2023. + // See: https://github.com/Azure/azure-sdk-tools/issues/7223#issuecomment-1839830834 + // TODO output PR information. + logMessage( + `oad-message-processor.processAndAppendOadMessages: PR:${context.oadMessageProcessorContext.prUrl}, baseBranch: ${baseBranch}, ` + + `oadMessages.length: ${oadMessages.length}, duplicateOadMessages.length: ${duplicateCount}, ` + + `messageCache.length: ${context.oadMessageProcessorContext.messageCache.length}.`, + ); + + context.oadMessageProcessorContext.messageCache.push(...dedupedOadMessages); + + const msgs: ResultMessageRecord[] = convertOadMessagesToResultMessageRecords( + context, + dedupedOadMessages, + baseBranch, + ); + + appendToLogFile(context.oadMessageProcessorContext.logFilePath, JSON.stringify(msgs)); + + return msgs; +} + +/** + * Clear the message cache for testing purposes + */ +export function clearMessageCache(context: OadMessageProcessorContext): void { + context.messageCache = []; +} + +/** + * Get the current message cache size + */ +export function getMessageCacheSize(context: OadMessageProcessorContext): number { + return context.messageCache.length; +} diff --git a/eng/tools/openapi-diff-runner/src/utils/oad-rule-map.ts b/eng/tools/openapi-diff-runner/src/utils/oad-rule-map.ts new file mode 100644 index 000000000000..0d3492943ade --- /dev/null +++ b/eng/tools/openapi-diff-runner/src/utils/oad-rule-map.ts @@ -0,0 +1,148 @@ +import { BreakingChangesCheckType, ReviewRequiredLabel } from "../types/breaking-change.js"; +import { MessageLevel } from "../types/message.js"; +import { OadRuleCode } from "../types/oad-types.js"; + +/** + * This oadMessagesRuleMap is applied by applyRules() function, invoked by BreakingChangeDetector, + * to messages returned from OAD (i.e. from a call to runOad() function). + * + * The oadMessagesRuleMap is expected to: + * (a) have entry for every possible combination of (scenario, code). + * (b) have "label" defined if severity is "Error". + * + * If (a) is violated, then const fallbackRule will be used instead. + * If (b) is violated, then const fallbackLabel will be used instead. + */ +// prettier-ignore +export const oadMessagesRuleMap: OadMessagesRuleMap = [ + { scenario: "CrossVersion" , code: "AddedAdditionalProperties" , severity: "Info" }, + { scenario: "CrossVersion" , code: "AddedEnumValue" , severity: "Info" }, + { scenario: "CrossVersion" , code: "AddedOperation" , severity: "Warning" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "AddedOptionalProperty" , severity: "Info" }, + { scenario: "CrossVersion" , code: "AddedPath" , severity: "Warning" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "AddedPropertyInResponse" , severity: "Info" }, + { scenario: "CrossVersion" , code: "AddedReadOnlyPropertyInResponse" , severity: "Warning" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "AddedRequiredProperty" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "AddedXmsEnum" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "AddingHeader" , severity: "Info" }, + { scenario: "CrossVersion" , code: "AddingOptionalParameter" , severity: "Info" }, + { scenario: "CrossVersion" , code: "AddingRequiredParameter" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "AddingResponseCode" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "ArrayCollectionFormatChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "ChangedParameterOrder" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "ConstantStatusHasChanged" , severity: "Warning" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "ConstraintChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "ConstraintIsStronger" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "ConstraintIsWeaker" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "DefaultValueChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "DifferentAllOf" , severity: "Warning" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "DifferentDiscriminator" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "DifferentExtends" , severity: "Warning" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "ModifiedOperationId" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "NoVersionChange" , severity: "Info" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "ParameterInHasChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "ParameterLocationHasChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "ProtocolNoLongerSupported" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "ReadonlyPropertyChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "ReferenceRedirection" , severity: "Warning" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "RemovedAdditionalProperties" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "RemovedClientParameter" , severity: "Info" , label: "BreakingChangeReviewRequired" }, // [1] + { scenario: "CrossVersion" , code: "RemovedDefinition" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "RemovedEnumValue" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "RemovedOperation" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "RemovedOptionalParameter" , severity: "Warning" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "RemovedPath" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "RemovedProperty" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "RemovedRequiredParameter" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "RemovedResponseCode" , severity: "Info" }, + { scenario: "CrossVersion" , code: "RemovedXmsEnum" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "RemovingHeader" , severity: "Warning" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "RequestBodyFormatNoLongerSupported" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "RequiredStatusChange" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "ResponseBodyFormatNowSupported" , severity: "Info" }, + { scenario: "CrossVersion" , code: "TypeChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "TypeFormatChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "VersionsReversed" , severity: "Warning" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "XmsEnumChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "CrossVersion" , code: "XmsLongRunningOperationChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "AddedAdditionalProperties" , severity: "Error" , label: "VersioningReviewRequired" }, + { scenario: "SameVersion" , code: "AddedEnumValue" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "AddedOperation" , severity: "Error" , label: "VersioningReviewRequired" }, + { scenario: "SameVersion" , code: "AddedOptionalProperty" , severity: "Error" , label: "VersioningReviewRequired" }, + { scenario: "SameVersion" , code: "AddedPath" , severity: "Error" , label: "VersioningReviewRequired" }, + { scenario: "SameVersion" , code: "AddedPropertyInResponse" , severity: "Error" , label: "VersioningReviewRequired" }, + { scenario: "SameVersion" , code: "AddedReadOnlyPropertyInResponse" , severity: "Error" , label: "VersioningReviewRequired" }, + { scenario: "SameVersion" , code: "AddedRequiredProperty" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "AddedXmsEnum" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "AddingHeader" , severity: "Info" }, + { scenario: "SameVersion" , code: "AddingOptionalParameter" , severity: "Error" , label: "VersioningReviewRequired" }, + { scenario: "SameVersion" , code: "AddingRequiredParameter" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "AddingResponseCode" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "ArrayCollectionFormatChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "ChangedParameterOrder" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "ConstantStatusHasChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "ConstraintChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "ConstraintIsStronger" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "ConstraintIsWeaker" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "DefaultValueChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "DifferentAllOf" , severity: "Warning" }, + { scenario: "SameVersion" , code: "DifferentDiscriminator" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "DifferentExtends" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "ModifiedOperationId" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "NoVersionChange" , severity: "Info" }, + { scenario: "SameVersion" , code: "ParameterInHasChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "ParameterLocationHasChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "ProtocolNoLongerSupported" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "ReadonlyPropertyChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "ReferenceRedirection" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "RemovedAdditionalProperties" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "RemovedClientParameter" , severity: "Info" , label: "BreakingChangeReviewRequired" }, // [1] + { scenario: "SameVersion" , code: "RemovedDefinition" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "RemovedEnumValue" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "RemovedOperation" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "RemovedOptionalParameter" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "RemovedPath" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "RemovedProperty" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "RemovedRequiredParameter" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "RemovedResponseCode" , severity: "Error" , label: "VersioningReviewRequired" }, + { scenario: "SameVersion" , code: "RemovedXmsEnum" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "RemovingHeader" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "RequestBodyFormatNoLongerSupported" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "RequiredStatusChange" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "ResponseBodyFormatNowSupported" , severity: "Error" , label: "VersioningReviewRequired" }, + { scenario: "SameVersion" , code: "TypeChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "TypeFormatChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "VersionsReversed" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "XmsEnumChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, + { scenario: "SameVersion" , code: "XmsLongRunningOperationChanged" , severity: "Error" , label: "BreakingChangeReviewRequired" }, +]; + +/** + * See comment on oadMessagesRuleMap. + */ +export const fallbackLabel: ReviewRequiredLabel = "BreakingChangeReviewRequired"; + +/** + * See comment on oadMessagesRuleMap. + */ +export const fallbackRule: Omit = { + severity: "Error", + label: fallbackLabel, +}; + +// [1]: Reduced RemovedClientParameter severity from "Error" to "Info" per https://github.com/Azure/azure-sdk-tools/issues/5025 + +/** + * Type of entry of oadMessagesRuleMap. + */ +export type OadMessageRule = { + scenario: BreakingChangesCheckType; + code: OadRuleCode; + severity: MessageLevel; + label?: ReviewRequiredLabel; +}; + +/** + * See comment on oadMessagesRuleMap. + */ +export type OadMessagesRuleMap = OadMessageRule[]; diff --git a/eng/tools/openapi-diff-runner/src/utils/pull-request.ts b/eng/tools/openapi-diff-runner/src/utils/pull-request.ts new file mode 100644 index 000000000000..b02821c5611b --- /dev/null +++ b/eng/tools/openapi-diff-runner/src/utils/pull-request.ts @@ -0,0 +1,156 @@ +import { existsSync, mkdirSync } from "node:fs"; +import path from "node:path"; +import { simpleGit } from "simple-git"; +import { logError, logMessage } from "../log.js"; +import { Context } from "../types/breaking-change.js"; + +/** + * Properties of Pull Request in Azure DevOps CI. + */ +export type PullRequestProperties = { + /** + * Target Branch, for example `main`. + */ + readonly targetBranch: string; + + /** + * Source Branch, for example `myname/newchanges`. + */ + readonly sourceBranch: string; + + /** + * Base Branch for breaking change detection, for example `main`. + */ + baseBranch: string; + + /** + * the PR repo current branch. + */ + currentBranch: string; + + /** + * Working folder for a cloned directory. We can't switch branches in the original Git repository + * so we use cloned repository. + */ + readonly tempRepoFolder: string; + + /** + * Checkout Git branch, for example, it can be `targetBranch` or `sourceBranch`. + */ + readonly checkout: (branch: string) => Promise; +}; + +const sourceBranch = "source-b6791c5f-e0a5-49b1-9175-d7fd3e341cb8"; + +const options = { + baseDir: process.cwd(), + binary: "git", + maxConcurrentProcesses: 1, +}; + +/** + * It creates a clone of the Git repository and returns properties of the Pull Request, such as + * `targetBranch` and `sourceBranch`. + * the `cwd` should point to the source Git repository. + */ +export const createPullRequestProperties = async ( + context: Context, + prefix: string, + skipInitializeBase: boolean = false, +): Promise => { + const baseBranch = context.baseBranch; + if (baseBranch === undefined) { + return undefined; + } + const originGitRepository = simpleGit({ ...options, baseDir: context.localSpecRepoPath }); + // Helper function to safely create a branch if it doesn't exist + const createBranchIfNotExists = async (branchName: string, startPoint?: string) => { + try { + // Get fresh branch list to avoid stale data + const currentBranches = await originGitRepository.branch(); + if (!currentBranches.all.includes(branchName)) { + const branchArgs = startPoint ? [branchName, startPoint] : [branchName]; + await originGitRepository.branch(branchArgs); + logMessage(`Created branch ${branchName}${startPoint ? ` from ${startPoint}` : ""}`); + } else { + logMessage(`Branch ${branchName} already exists, skipping creation`); + } + } catch (error: any) { + // If the error is about branch already existing, that's fine - continue + if ( + error.message?.includes("already exists") || + error.message?.includes("fatal: a branch named") + ) { + logMessage(`Branch ${branchName} already exists (caught during creation), continuing`); + } else { + logError(`Failed to create branch ${branchName}: ${error.message}`); + throw error; + } + } + }; + + // Create branches if they don't exist + await createBranchIfNotExists(sourceBranch); + + if (!skipInitializeBase) { + await createBranchIfNotExists(baseBranch, `remotes/origin/${baseBranch}`); + } + + await createBranchIfNotExists(context.prTargetBranch, `remotes/origin/${context.prTargetBranch}`); + + // we have to clone the repository because we need to switch branches. + // Switching branches in the current repository can be dangerous because Avocado + // may be running from it. + const tempRepoFolder = path.resolve( + path.join(process.cwd(), "..", `${prefix}-c93b354fd9c14905bb574a8834c4d69b`), + ); + if (!existsSync(tempRepoFolder)) { + mkdirSync(tempRepoFolder); + } + const workingGitRepository = simpleGit({ ...options, baseDir: tempRepoFolder }); + await workingGitRepository.init(); + + // Check if origin remote already exists, if not add it + try { + const remotes = await workingGitRepository.getRemotes(); + const originExists = remotes.some((remote: any) => remote.name === "origin"); + if (!originExists) { + await workingGitRepository.addRemote("origin", context.localSpecRepoPath); + } + } catch (error) { + // If getting remotes fails, try to add origin anyway and catch the error + try { + await workingGitRepository.addRemote("origin", context.localSpecRepoPath); + } catch (addRemoteError: any) { + // Ignore the error if remote already exists + if (!addRemoteError?.message?.includes("remote origin already exists")) { + throw addRemoteError; + } + } + } + await workingGitRepository.pull("origin", context.prTargetBranch); + await workingGitRepository.fetch("origin", `${sourceBranch}`); + if (!skipInitializeBase) { + await workingGitRepository.fetch("origin", `${baseBranch}`); + } + logMessage(`checking out target branch ${context.prTargetBranch} in ${tempRepoFolder}`); + await workingGitRepository.checkout(context.prTargetBranch); + logMessage(`Current working directory: ${process.cwd()}`); + + return { + baseBranch: context.prTargetBranch, + targetBranch: context.prTargetBranch, + sourceBranch, + tempRepoFolder, + checkout: async function (this: any, branch: string) { + if (this.currentBranch !== branch) { + await workingGitRepository.checkout([branch]); + logMessage( + `checkout to ${branch} in ${tempRepoFolder}\n Current working directory: ${process.cwd()}`, + ); + this.currentBranch = branch; + } + }, + currentBranch: context.prTargetBranch, + }; +}; diff --git a/eng/tools/openapi-diff-runner/src/utils/spec.ts b/eng/tools/openapi-diff-runner/src/utils/spec.ts new file mode 100644 index 000000000000..c595342c3747 --- /dev/null +++ b/eng/tools/openapi-diff-runner/src/utils/spec.ts @@ -0,0 +1,176 @@ +import { basename } from "node:path"; +import { logError } from "../log.js"; +import { ApiVersionLifecycleStage } from "../types/breaking-change.js"; +import { getVersionFromInputFile } from "./common-utils.js"; + +// Type definitions +export interface Operation { + id: string; + path: string; + httpMethod: string; +} + +export interface Swagger { + path: string; + versionKind?: string; + getOperations?(): Promise>; +} + +/** + * Get preceding swagger version for a specific lifecycle stage + */ +async function getPrecedingSwaggerByType( + targetSwaggerPath: string, + availableSwaggers: Swagger[], + versionKind: ApiVersionLifecycleStage, +): Promise { + if (availableSwaggers.length === 0) { + return undefined; + } + + const currentVersion = getVersionFromInputFile(targetSwaggerPath); + const fileName = getBaseNameForSwagger(targetSwaggerPath, currentVersion); + + // Load version info for all swaggers to enable filtering + const swaggersWithVersions = availableSwaggers.map((swagger) => ({ + swagger, + version: getVersionFromInputFile(swagger.path), + fileName: getBaseNameForSwagger(swagger.path, getVersionFromInputFile(swagger.path)), + versionKind: getVersionFromInputFile(swagger.path, true).includes("preview") + ? ApiVersionLifecycleStage.PREVIEW + : ApiVersionLifecycleStage.STABLE, + })); + + const versionsOfType = swaggersWithVersions.filter( + (item) => + item.fileName === fileName && + item.versionKind === versionKind && + item.version <= currentVersion, + ); + + if (versionsOfType.length > 0) { + const mostRecent = versionsOfType.reduce((previous, current) => + previous.version > current.version ? previous : current, + ); + return mostRecent.swagger.path; + } + + return undefined; +} + +/** + * Get operations from all previous versions that also exist in current swagger + */ +export async function getExistedVersionOperations( + targetSwaggerPath: string, + availableSwaggers: Swagger[], + targetOperations: Operation[], +): Promise> { + const result = new Map(); + + if (availableSwaggers.length === 0 || targetOperations.length === 0) { + return result; + } + + const currentVersion = getVersionFromInputFile(targetSwaggerPath); + const fileName = getBaseNameForSwagger(targetSwaggerPath, currentVersion); + + // Load version info for all swaggers to enable filtering + const swaggersWithVersions = availableSwaggers.map((swagger) => ({ + swagger, + version: getVersionFromInputFile(swagger.path), + fileName: getBaseNameForSwagger(swagger.path, getVersionFromInputFile(swagger.path)), + })); + + // Get all current and previous versions of the swaggers with different fileNames + const previousVersionSwaggers = swaggersWithVersions + .filter((item) => item.fileName !== fileName && item.version <= currentVersion) + .sort((a, b) => a.version.localeCompare(b.version)) + .map((item) => item.swagger); + + // Collect operations from all previous versions that also exist in current version + for (const previousSwagger of previousVersionSwaggers) { + try { + // Check if getOperations method exists + if (!previousSwagger.getOperations) { + continue; + } + + const previousOperationsMap = await previousSwagger.getOperations(); + const previousOperations = [...previousOperationsMap.values()]; + + // Find operations that exist in both this previous version AND current version + const matchingOperations = previousOperations.filter((operation) => { + return targetOperations.some( + (currentOp) => + currentOp.path === operation.path && + currentOp.id === operation.id && + currentOp.httpMethod === operation.httpMethod, + ); + }); + result.set(previousSwagger.path, matchingOperations); + } catch (e) { + logError( + `Failed to get operations for previous swagger: ${previousSwagger.path}. This may be due to the swagger file being inaccessible or malformed.`, + ); + throw e; + } + } + + return result; +} + +/** + * Get preceding swagger versions for a given swagger file + */ +export async function getPrecedingSwaggers( + targetSwaggerPath: string, + availableSwaggers: Swagger[], +): Promise<{ preview?: string; stable?: string }> { + return { + preview: await getPrecedingSwaggerByType( + targetSwaggerPath, + availableSwaggers, + ApiVersionLifecycleStage.PREVIEW, + ), + stable: await getPrecedingSwaggerByType( + targetSwaggerPath, + availableSwaggers, + ApiVersionLifecycleStage.STABLE, + ), + }; +} + +/** + * Get base name for swagger file + * @param {string} filePath - Path to swagger file + * @param {string} [version] - Version string to use for base name + * @returns {string} - Base name for the swagger file + */ +export function getBaseNameForSwagger(filePath: string, version: string = ""): string { + if (version) { + const segments = filePath.split("/"); + const index = segments.findIndex((v) => v.startsWith(version)); + if (index !== -1) { + return segments.slice(index + 1).join("/"); + } + } + return basename(filePath); +} + +/** + * Deduplicate swagger objects in an array by their path. + * @param swaggers The array of swagger objects to deduplicate. + * @returns A new array with duplicate swagger objects removed. + */ +export function deduplicateSwaggers(swaggers: Swagger[]): Swagger[] { + // Deduplicate by path + const seen = new Set(); + return swaggers.filter((swagger) => { + if (seen.has(swagger.path)) { + return false; + } + seen.add(swagger.path); + return true; + }); +} diff --git a/eng/tools/openapi-diff-runner/test/command-helpers.test.ts b/eng/tools/openapi-diff-runner/test/command-helpers.test.ts new file mode 100644 index 000000000000..80f80c7858b7 --- /dev/null +++ b/eng/tools/openapi-diff-runner/test/command-helpers.test.ts @@ -0,0 +1,935 @@ +import { BREAKING_CHANGES_CHECK_TYPES } from "@azure-tools/specs-shared/breaking-change"; +import { getChangedFilesStatuses } from "@azure-tools/specs-shared/changed-files"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + BreakingChangeLabelsToBeAdded, + buildPrInfo, + changeBaseBranch, + cleanDummySwagger, + createContextFromParsedArgs, + createDummySwagger, + getCreatedDummySwaggerCount, + getSwaggerDiffs, + isSameVersionBreakingType, + logFullOadMessagesList, + outputBreakingChangeLabelVariables, + type ParsedCliArguments, +} from "../src/command-helpers.js"; +import { LogLevel } from "../src/log.js"; +import { + BreakingChangeReviewRequiredLabel, + Context, + VersioningReviewRequiredLabel, +} from "../src/types/breaking-change.js"; +import { ResultMessageRecord } from "../src/types/message.js"; + +// Test constants +const TEST_CONSTANTS = { + REPO: { + NAME: "test/repo", + CUSTOM: "custom/repo", + SOURCE: "test/repo", + }, + PATHS: { + SPEC_REPO: "/path/to/repo", + WORKING_FOLDER: "/working", + LOG_FOLDER: "/logs", + LOG_FILE: "/log/path", + TEMP_REPO: "/working/dir", + TEST_PATH: "/test/path", + FILE_JS: "/path/to/file.js", + }, + BRANCHES: { + MAIN: "main", + DEVELOP: "develop", + FEATURE: "feature", + ARM_CORE: "ARMCoreRPDev", + }, + COMMITS: { + HEAD: "HEAD", + ABC123: "abc123", + }, + PR: { + NUMBER: "123", + CUSTOM_NUMBER: "456", + URL: "https://github.com/test/repo/pull/123", + CUSTOM_URL: "https://github.com/custom/repo/pull/456", + }, + SWAGGER_PATHS: { + FOO: "specification/foo/resource-manager/Microsoft.Foo/stable/2023-01-01/foo.json", + BAR: "specification/bar/data-plane/stable/2023-01-01/bar.json", + BAZ: "specification/baz/resource-manager/Microsoft.Baz/stable/2023-01-01/baz.json", + QUX_MGMT: "specification/qux/resource-manager/Microsoft.Qux/stable/2023-01-01/qux.json", + QUX_DATA: "specification/qux/data-plane/stable/2023-01-01/qux.json", + OLD_MGMT: "specification/old/resource-manager/Microsoft.Old/stable/2023-01-01/old.json", + NEW_MGMT: "specification/new/resource-manager/Microsoft.New/stable/2023-01-01/new.json", + OLD_DATA: "specification/old/data-plane/stable/2023-01-01/old.json", + }, + NON_SWAGGER_PATHS: { + YAML: ".github/workflows/test.yaml", + EXAMPLE: + "specification/bar/resource-manager/Microsoft.Bar/stable/2023-01-01/examples/example.json", + README: "README.md", + PACKAGE_JSON: "package.json", + BUILD_JS: "dist/build.js", + OLD_README: "old-readme.md", + NEW_README: "new-readme.md", + }, + MESSAGES: { + ERROR: "Test error message", + WARNING: "Test warning message", + SWITCH_BRANCH: "switch target branch to main", + CREATED_DUMMY: "created a dummy swagger:", + }, + SWAGGER_DIRS: ["specification"], + SWAGGER_CONTENT: { + BASIC: { + swagger: "2.0", + info: { title: "Test API", version: "1.0" }, + }, + WITH_PATHS: { + swagger: "2.0", + info: { title: "Test API", version: "1.0" }, + paths: { "/test": { get: {} } }, + "x-ms-paths": { "/test2": { post: {} } }, + "x-ms-parameterized-host": { hostTemplate: "test.com" }, + parameters: { testParam: {} }, + definitions: { TestModel: {} }, + }, + }, +}; + +// Factory functions for commonly used mock objects +function createMockContext(overrides = {}): Context { + return { + localSpecRepoPath: TEST_CONSTANTS.PATHS.SPEC_REPO, + workingFolder: TEST_CONSTANTS.PATHS.WORKING_FOLDER, + logFileFolder: TEST_CONSTANTS.PATHS.LOG_FOLDER, + swaggerDirs: TEST_CONSTANTS.SWAGGER_DIRS, + baseBranch: TEST_CONSTANTS.BRANCHES.MAIN, + headCommit: TEST_CONSTANTS.COMMITS.HEAD, + runType: BREAKING_CHANGES_CHECK_TYPES.SAME_VERSION, + checkName: "test", + targetRepo: TEST_CONSTANTS.REPO.NAME, + sourceRepo: TEST_CONSTANTS.REPO.SOURCE, + prNumber: TEST_CONSTANTS.PR.NUMBER, + prSourceBranch: TEST_CONSTANTS.BRANCHES.FEATURE, + prTargetBranch: TEST_CONSTANTS.BRANCHES.MAIN, + oadMessageProcessorContext: { + logFilePath: TEST_CONSTANTS.PATHS.LOG_FILE, + prUrl: TEST_CONSTANTS.PR.URL, + messageCache: [], + }, + prUrl: TEST_CONSTANTS.PR.URL, + ...overrides, + } as Context; +} + +function createMockPrInfo(overrides = {}) { + return { + baseBranch: TEST_CONSTANTS.BRANCHES.MAIN, + targetBranch: TEST_CONSTANTS.BRANCHES.MAIN, + sourceBranch: TEST_CONSTANTS.BRANCHES.FEATURE, + tempRepoFolder: TEST_CONSTANTS.PATHS.TEMP_REPO, + currentBranch: TEST_CONSTANTS.BRANCHES.MAIN, + checkout: vi.fn(), + ...overrides, + }; +} + +function createMockSwaggerResult(overrides = {}) { + return { + additions: [TEST_CONSTANTS.SWAGGER_PATHS.FOO, TEST_CONSTANTS.SWAGGER_PATHS.BAR], + modifications: [TEST_CONSTANTS.SWAGGER_PATHS.BAZ], + deletions: [TEST_CONSTANTS.SWAGGER_PATHS.QUX_DATA], + renames: [ + { + from: TEST_CONSTANTS.SWAGGER_PATHS.OLD_MGMT, + to: TEST_CONSTANTS.SWAGGER_PATHS.NEW_MGMT, + }, + ], + total: 5, + ...overrides, + }; +} + +function createMockMessages(): ResultMessageRecord[] { + return [ + { + type: "Result", + level: "Error", + message: TEST_CONSTANTS.MESSAGES.ERROR, + time: new Date("2023-01-01"), + paths: [], + }, + { + type: "Result", + level: "Warning", + message: TEST_CONSTANTS.MESSAGES.WARNING, + time: new Date("2023-01-02"), + paths: [], + }, + ]; +} + +function setupCommonMocks() { + const mockOadMessageProcessor = { + logFilePath: TEST_CONSTANTS.PATHS.LOG_FILE, + prUrl: TEST_CONSTANTS.PR.URL, + messageCache: [], + }; + return { mockOadMessageProcessor }; +} + +// Mock dependencies +vi.mock("node:fs"); +vi.mock("node:url"); +vi.mock("../src/utils/common-utils.js"); +vi.mock("../src/utils/oad-message-processor.js"); +vi.mock("../src/utils/pull-request.js"); +vi.mock("../src/log.js"); +vi.mock("@azure-tools/specs-shared/changed-files", async () => { + const actual = await vi.importActual("@azure-tools/specs-shared/changed-files"); + return { + ...actual, + getChangedFilesStatuses: vi.fn(), + }; +}); + +describe("command-helpers", () => { + const mockExistsSync = vi.mocked(existsSync); + const mockMkdirSync = vi.mocked(mkdirSync); + const mockReadFileSync = vi.mocked(readFileSync); + const mockWriteFileSync = vi.mocked(writeFileSync); + const mockRmSync = vi.mocked(rmSync); + const mockFileURLToPath = vi.mocked(fileURLToPath); + const mockGetChangedFilesStatuses = vi.mocked(getChangedFilesStatuses); + + beforeEach(() => { + vi.clearAllMocks(); + + // Setup default mocks + mockFileURLToPath.mockReturnValue("/path/to/file.js"); + + // Mock console methods to avoid test output noise + vi.spyOn(console, "log").mockImplementation(() => {}); + vi.spyOn(console, "error").mockImplementation(() => {}); + + // Clear BreakingChangeLabelsToBeAdded set + BreakingChangeLabelsToBeAdded.clear(); + + // Clean up any dummy swagger files between tests + cleanDummySwagger(); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + describe("createContextFromParsedArgs", () => { + const createTestParsedArgs = ( + overrides: Partial = {}, + ): ParsedCliArguments => ({ + localSpecRepoPath: TEST_CONSTANTS.PATHS.SPEC_REPO, + targetRepo: TEST_CONSTANTS.REPO.NAME, + sourceRepo: TEST_CONSTANTS.REPO.SOURCE, + prNumber: TEST_CONSTANTS.PR.NUMBER, + runType: "SameVersion" as const, + baseBranch: TEST_CONSTANTS.BRANCHES.MAIN, + headCommit: TEST_CONSTANTS.COMMITS.HEAD, + prSourceBranch: "", + prTargetBranch: "", + ...overrides, + }); + + it("should create context with default values", async () => { + const { createOadMessageProcessor } = await import("../src/utils/oad-message-processor.js"); + const { mockOadMessageProcessor } = setupCommonMocks(); + vi.mocked(createOadMessageProcessor).mockReturnValue(mockOadMessageProcessor); + + const parsedArgs = createTestParsedArgs(); + const context = createContextFromParsedArgs( + parsedArgs, + TEST_CONSTANTS.PATHS.WORKING_FOLDER, + TEST_CONSTANTS.PATHS.LOG_FOLDER, + ); + + expect(context.targetRepo).toBe(TEST_CONSTANTS.REPO.NAME); + expect(context.prNumber).toBe(TEST_CONSTANTS.PR.NUMBER); + expect(context.runType).toBe("SameVersion"); + expect(context.baseBranch).toBe(TEST_CONSTANTS.BRANCHES.MAIN); + expect(context.headCommit).toBe(TEST_CONSTANTS.COMMITS.HEAD); + expect(context.checkName).toBe("Swagger BreakingChange"); + expect(context.prUrl).toBe(TEST_CONSTANTS.PR.URL); + expect(context.workingFolder).toBe(TEST_CONSTANTS.PATHS.WORKING_FOLDER); + expect(context.logFileFolder).toBe(TEST_CONSTANTS.PATHS.LOG_FOLDER); + }); + + it("should use custom values when provided", async () => { + const { createOadMessageProcessor } = await import("../src/utils/oad-message-processor.js"); + vi.mocked(createOadMessageProcessor).mockReturnValue({ + logFilePath: TEST_CONSTANTS.PATHS.LOG_FILE, + prUrl: TEST_CONSTANTS.PR.CUSTOM_URL, + messageCache: [], + }); + + const parsedArgs = createTestParsedArgs({ + targetRepo: TEST_CONSTANTS.REPO.CUSTOM, + prNumber: TEST_CONSTANTS.PR.CUSTOM_NUMBER, + runType: "CrossVersion", + baseBranch: TEST_CONSTANTS.BRANCHES.DEVELOP, + headCommit: TEST_CONSTANTS.COMMITS.ABC123, + prSourceBranch: TEST_CONSTANTS.BRANCHES.FEATURE, + prTargetBranch: TEST_CONSTANTS.BRANCHES.MAIN, + }); + + const context = createContextFromParsedArgs( + parsedArgs, + TEST_CONSTANTS.PATHS.WORKING_FOLDER, + TEST_CONSTANTS.PATHS.LOG_FOLDER, + ); + + expect(context.targetRepo).toBe(TEST_CONSTANTS.REPO.CUSTOM); + expect(context.prNumber).toBe(TEST_CONSTANTS.PR.CUSTOM_NUMBER); + expect(context.runType).toBe("CrossVersion"); + expect(context.baseBranch).toBe(TEST_CONSTANTS.BRANCHES.DEVELOP); + expect(context.headCommit).toBe(TEST_CONSTANTS.COMMITS.ABC123); + expect(context.prSourceBranch).toBe(TEST_CONSTANTS.BRANCHES.FEATURE); + expect(context.prTargetBranch).toBe(TEST_CONSTANTS.BRANCHES.MAIN); + expect(context.checkName).toBe("BreakingChange(Cross-Version)"); + }); + + it("should create proper URL and context structure", async () => { + const { createOadMessageProcessor } = await import("../src/utils/oad-message-processor.js"); + const { mockOadMessageProcessor } = setupCommonMocks(); + vi.mocked(createOadMessageProcessor).mockReturnValue(mockOadMessageProcessor); + + const parsedArgs = createTestParsedArgs(); + const context = createContextFromParsedArgs( + parsedArgs, + TEST_CONSTANTS.PATHS.WORKING_FOLDER, + TEST_CONSTANTS.PATHS.LOG_FOLDER, + ); + + expect(context.swaggerDirs).toEqual(["specification", "dev"]); + expect(context.oadMessageProcessorContext).toBe(mockOadMessageProcessor); + expect(createOadMessageProcessor).toHaveBeenCalledWith( + TEST_CONSTANTS.PATHS.LOG_FOLDER, + TEST_CONSTANTS.PR.URL, + ); + }); + }); + + describe("BreakingChangeLabelsToBeAdded", () => { + it("should be a Set that can be modified", () => { + expect(BreakingChangeLabelsToBeAdded).toBeInstanceOf(Set); + expect(BreakingChangeLabelsToBeAdded.size).toBe(0); + + BreakingChangeLabelsToBeAdded.add("test-label"); + expect(BreakingChangeLabelsToBeAdded.has("test-label")).toBe(true); + expect(BreakingChangeLabelsToBeAdded.size).toBe(1); + + BreakingChangeLabelsToBeAdded.clear(); + expect(BreakingChangeLabelsToBeAdded.size).toBe(0); + }); + }); + + describe("getSwaggerDiffs", () => { + it("should return changed files successfully", async () => { + const mockResult = createMockSwaggerResult(); + mockGetChangedFilesStatuses.mockResolvedValue(mockResult); + + const result = await getSwaggerDiffs({ + baseCommitish: TEST_CONSTANTS.BRANCHES.MAIN, + cwd: TEST_CONSTANTS.PATHS.TEST_PATH, + headCommitish: TEST_CONSTANTS.COMMITS.HEAD, + }); + + // Expected result should have renames added to additions and deletions, and no renames property + const expectedResult = { + additions: [...mockResult.additions, ...mockResult.renames.map((rename) => rename.to)], + modifications: mockResult.modifications, + deletions: [...mockResult.deletions, ...mockResult.renames.map((rename) => rename.from)], + total: + mockResult.additions.length + + mockResult.modifications.length + + mockResult.deletions.length + + mockResult.renames.length * 2, + }; + + expect(result).toEqual(expectedResult); + expect(mockGetChangedFilesStatuses).toHaveBeenCalledWith({ + baseCommitish: TEST_CONSTANTS.BRANCHES.MAIN, + cwd: TEST_CONSTANTS.PATHS.TEST_PATH, + headCommitish: TEST_CONSTANTS.COMMITS.HEAD, + paths: ["specification"], + }); + }); + + it("should return empty result on error", async () => { + mockGetChangedFilesStatuses.mockRejectedValue(new Error("Git error")); + const result = await getSwaggerDiffs(); + expect(result).toEqual({ + additions: [], + modifications: [], + deletions: [], + total: 0, + }); + }); + + it("should filter out non-Swagger files", async () => { + const mockResultWithNonSwagger = { + additions: [ + TEST_CONSTANTS.SWAGGER_PATHS.FOO, // Valid Swagger + TEST_CONSTANTS.NON_SWAGGER_PATHS.YAML, // Non-Swagger (YAML) + TEST_CONSTANTS.NON_SWAGGER_PATHS.EXAMPLE, // Example file + TEST_CONSTANTS.NON_SWAGGER_PATHS.README, // Non-JSON + TEST_CONSTANTS.SWAGGER_PATHS.BAZ, // Valid Swagger + ], + modifications: [ + TEST_CONSTANTS.SWAGGER_PATHS.QUX_MGMT, // Valid Swagger + TEST_CONSTANTS.NON_SWAGGER_PATHS.PACKAGE_JSON, // Non-Swagger JSON + ], + deletions: [ + TEST_CONSTANTS.SWAGGER_PATHS.OLD_DATA, // Valid Swagger + TEST_CONSTANTS.NON_SWAGGER_PATHS.BUILD_JS, // Non-Swagger + ], + renames: [ + { + from: TEST_CONSTANTS.SWAGGER_PATHS.OLD_MGMT, // Valid Swagger + to: TEST_CONSTANTS.SWAGGER_PATHS.NEW_MGMT, // Valid Swagger + }, + { + from: TEST_CONSTANTS.NON_SWAGGER_PATHS.OLD_README, // Non-Swagger + to: TEST_CONSTANTS.NON_SWAGGER_PATHS.NEW_README, // Non-Swagger + }, + ], + total: 9, + }; + + mockGetChangedFilesStatuses.mockResolvedValue(mockResultWithNonSwagger); + + const result = await getSwaggerDiffs(); + + // Only Swagger files should be returned, with renames added to additions and deletions + expect(result).toEqual({ + additions: [ + TEST_CONSTANTS.SWAGGER_PATHS.FOO, + TEST_CONSTANTS.SWAGGER_PATHS.BAZ, + TEST_CONSTANTS.SWAGGER_PATHS.NEW_MGMT, // from valid rename + ], + modifications: [TEST_CONSTANTS.SWAGGER_PATHS.QUX_MGMT], + deletions: [ + TEST_CONSTANTS.SWAGGER_PATHS.OLD_DATA, + TEST_CONSTANTS.SWAGGER_PATHS.OLD_MGMT, // from valid rename + ], + total: 6, // 3 additions + 1 modification + 2 deletions (including rename files) + }); + }); + + it("should use default options when none provided", async () => { + const emptyResult = createMockSwaggerResult({ + additions: [], + modifications: [], + deletions: [], + renames: [], + total: 0, + }); + + mockGetChangedFilesStatuses.mockResolvedValue(emptyResult); + + await getSwaggerDiffs(); + + expect(mockGetChangedFilesStatuses).toHaveBeenCalledWith({ + baseCommitish: undefined, + cwd: undefined, + headCommitish: undefined, + paths: ["specification"], + }); + }); + + it("should handle renames by adding them to additions and deletions", async () => { + const mockResultWithRenames = { + additions: [TEST_CONSTANTS.SWAGGER_PATHS.FOO], + modifications: [TEST_CONSTANTS.SWAGGER_PATHS.BAZ], + deletions: [TEST_CONSTANTS.SWAGGER_PATHS.OLD_DATA], + renames: [ + { + from: TEST_CONSTANTS.SWAGGER_PATHS.OLD_MGMT, + to: TEST_CONSTANTS.SWAGGER_PATHS.NEW_MGMT, + }, + { + from: "specification/oldapi/data-plane/stable/2023-01-01/oldapi.json", + to: "specification/newapi/data-plane/stable/2023-01-01/newapi.json", + }, + ], + total: 6, + }; + + mockGetChangedFilesStatuses.mockResolvedValue(mockResultWithRenames); + + const result = await getSwaggerDiffs(); + + // Renames should be added to additions and deletions, not returned as renames + expect(result).toEqual({ + additions: [ + TEST_CONSTANTS.SWAGGER_PATHS.FOO, + TEST_CONSTANTS.SWAGGER_PATHS.NEW_MGMT, // from rename.to + "specification/newapi/data-plane/stable/2023-01-01/newapi.json", // from rename.to + ], + modifications: [TEST_CONSTANTS.SWAGGER_PATHS.BAZ], + deletions: [ + TEST_CONSTANTS.SWAGGER_PATHS.OLD_DATA, + TEST_CONSTANTS.SWAGGER_PATHS.OLD_MGMT, // from rename.from + "specification/oldapi/data-plane/stable/2023-01-01/oldapi.json", // from rename.from + ], + total: 7, // 3 additions + 1 modification + 3 deletions (including from renames) + }); + }); + }); + + describe("buildPrInfo", () => { + it("should build PR info successfully", async () => { + const { createPullRequestProperties } = await import("../src/utils/pull-request.js"); + const mockContext = createMockContext(); + const mockPrInfo = createMockPrInfo(); + + vi.mocked(createPullRequestProperties).mockResolvedValue(mockPrInfo); + + await buildPrInfo(mockContext); + + expect(mockContext.prInfo).toBe(mockPrInfo); + expect(createPullRequestProperties).toHaveBeenCalledWith(mockContext, "same-version"); + }); + + it("should use cross-version prefix for CrossVersion run type", async () => { + const { createPullRequestProperties } = await import("../src/utils/pull-request.js"); + const mockContext = createMockContext({ runType: "CrossVersion" }); + const mockPrInfo = createMockPrInfo(); + + vi.mocked(createPullRequestProperties).mockResolvedValue(mockPrInfo); + + await buildPrInfo(mockContext); + + expect(createPullRequestProperties).toHaveBeenCalledWith(mockContext, "cross-version"); + }); + + it("should throw error when PR info creation fails", async () => { + const { createPullRequestProperties } = await import("../src/utils/pull-request.js"); + const mockContext = createMockContext(); + + vi.mocked(createPullRequestProperties).mockResolvedValue(undefined); + + await expect(buildPrInfo(mockContext)).rejects.toThrow("create PR failed!"); + }); + + it("should throw error when PR info has no target branch", async () => { + const { createPullRequestProperties } = await import("../src/utils/pull-request.js"); + const mockContext = createMockContext(); + const mockPrInfo = createMockPrInfo({ targetBranch: "" }); // Empty target branch + + vi.mocked(createPullRequestProperties).mockResolvedValue(mockPrInfo); + + await expect(buildPrInfo(mockContext)).rejects.toThrow("create PR failed!"); + }); + }); + + describe("changeBaseBranch", () => { + it("should change base branch when different from target and not whitelisted", async () => { + const { logMessage } = await import("../src/log.js"); + const mockContext = createMockContext({ + prTargetBranch: TEST_CONSTANTS.BRANCHES.DEVELOP, + prInfo: createMockPrInfo({ + baseBranch: TEST_CONSTANTS.BRANCHES.DEVELOP, + targetBranch: TEST_CONSTANTS.BRANCHES.DEVELOP, + currentBranch: TEST_CONSTANTS.BRANCHES.DEVELOP, + }), + }); + + changeBaseBranch(mockContext); + + expect(mockContext.prInfo!.baseBranch).toBe(TEST_CONSTANTS.BRANCHES.MAIN); + expect(logMessage).toHaveBeenCalledWith(TEST_CONSTANTS.MESSAGES.SWITCH_BRANCH); + }); + + it("should not change base branch when same as target", () => { + const mockContext = createMockContext({ + prTargetBranch: TEST_CONSTANTS.BRANCHES.MAIN, // Same as baseBranch + prInfo: createMockPrInfo(), + }); + + const originalBaseBranch = mockContext.prInfo!.baseBranch; + changeBaseBranch(mockContext); + + expect(mockContext.prInfo!.baseBranch).toBe(originalBaseBranch); + }); + + it("should not change base branch for whitelisted branches", () => { + const mockContext = createMockContext({ + prTargetBranch: TEST_CONSTANTS.BRANCHES.ARM_CORE, // Whitelisted branch + prInfo: createMockPrInfo({ + baseBranch: TEST_CONSTANTS.BRANCHES.ARM_CORE, + targetBranch: TEST_CONSTANTS.BRANCHES.ARM_CORE, + currentBranch: TEST_CONSTANTS.BRANCHES.ARM_CORE, + }), + }); + + const originalBaseBranch = mockContext.prInfo!.baseBranch; + changeBaseBranch(mockContext); + + expect(mockContext.prInfo!.baseBranch).toBe(originalBaseBranch); + }); + + it("should change base branch for CrossVersion run type when different from target", async () => { + const { logMessage } = await import("../src/log.js"); + const mockContext = createMockContext({ + runType: "CrossVersion", // CrossVersion type + prTargetBranch: TEST_CONSTANTS.BRANCHES.DEVELOP, // Different from baseBranch + prInfo: createMockPrInfo({ + baseBranch: TEST_CONSTANTS.BRANCHES.DEVELOP, + targetBranch: TEST_CONSTANTS.BRANCHES.DEVELOP, + currentBranch: TEST_CONSTANTS.BRANCHES.DEVELOP, + }), + }); + + changeBaseBranch(mockContext); + + // CrossVersion also changes base branch when different from target + expect(mockContext.prInfo!.baseBranch).toBe(TEST_CONSTANTS.BRANCHES.MAIN); + expect(logMessage).toHaveBeenCalledWith(TEST_CONSTANTS.MESSAGES.SWITCH_BRANCH); + }); + }); + + describe("logFullOadMessagesList", () => { + it("should log all messages individually", async () => { + const { logMessage } = await import("../src/log.js"); + const msgs = createMockMessages(); + + logFullOadMessagesList(msgs); + + expect(logMessage).toHaveBeenCalledWith("---- Full list of messages ----", LogLevel.Group); + expect(logMessage).toHaveBeenCalledWith("["); + expect(logMessage).toHaveBeenCalledWith(JSON.stringify(msgs[0], null, 4) + ","); + expect(logMessage).toHaveBeenCalledWith(JSON.stringify(msgs[1], null, 4) + ","); + expect(logMessage).toHaveBeenCalledWith("]"); + expect(logMessage).toHaveBeenCalledWith( + "---- End of full list of messages ----", + LogLevel.EndGroup, + ); + }); + + it("should handle empty message list", async () => { + const { logMessage } = await import("../src/log.js"); + + logFullOadMessagesList([]); + + expect(logMessage).toHaveBeenCalledWith("---- Full list of messages ----", LogLevel.Group); + expect(logMessage).toHaveBeenCalledWith("["); + expect(logMessage).toHaveBeenCalledWith("]"); + expect(logMessage).toHaveBeenCalledWith( + "---- End of full list of messages ----", + LogLevel.EndGroup, + ); + }); + }); + + describe("createDummySwagger", () => { + it("should create dummy swagger file successfully", async () => { + const { logMessage } = await import("../src/log.js"); + + const fromSwagger = "/path/to/source.json"; + const toSwagger = "/path/to/target.json"; + const mockSwaggerContent = JSON.stringify({ + swagger: "2.0", + info: { title: "Test API", version: "1.0" }, + paths: { "/test": { get: {} } }, + "x-ms-paths": { "/test2": { post: {} } }, + "x-ms-parameterized-host": { hostTemplate: "test.com" }, + parameters: { testParam: {} }, + definitions: { TestModel: {} }, + }); + + mockExistsSync.mockReturnValue(false); + mockReadFileSync.mockReturnValue(Buffer.from(mockSwaggerContent)); + + createDummySwagger(fromSwagger, toSwagger); + + expect(mockMkdirSync).toHaveBeenCalledWith("/path/to", { recursive: true }); + expect(mockReadFileSync).toHaveBeenCalledWith(fromSwagger); + expect(mockWriteFileSync).toHaveBeenCalledWith( + toSwagger, + expect.stringContaining('"paths": {}'), + ); + expect(logMessage).toHaveBeenCalledWith( + `created a dummy swagger: ${toSwagger} from ${fromSwagger}`, + ); + + // Verify the dummy swagger content + const writeCall = mockWriteFileSync.mock.calls[0]; + const writtenContent = JSON.parse(writeCall[1] as string); + expect(writtenContent.paths).toEqual({}); + expect(writtenContent["x-ms-paths"]).toEqual({}); + expect(writtenContent["x-ms-parameterized-host"]).toBeUndefined(); + expect(writtenContent.parameters).toEqual({}); + expect(writtenContent.definitions).toEqual({}); + }); + + it("should create directory if it doesn't exist", () => { + const fromSwagger = "/path/to/source.json"; + const toSwagger = "/path/to/nested/target.json"; + const mockSwaggerContent = JSON.stringify({ + swagger: "2.0", + info: { title: "Test API", version: "1.0" }, + }); + + mockExistsSync.mockReturnValue(false); + mockReadFileSync.mockReturnValue(Buffer.from(mockSwaggerContent)); + + createDummySwagger(fromSwagger, toSwagger); + + expect(mockMkdirSync).toHaveBeenCalledWith("/path/to/nested", { recursive: true }); + }); + + it("should not create directory if it already exists", () => { + const fromSwagger = "/path/to/source.json"; + const toSwagger = "/path/to/target.json"; + const mockSwaggerContent = JSON.stringify({ + swagger: "2.0", + info: { title: "Test API", version: "1.0" }, + }); + + mockExistsSync.mockReturnValue(true); + mockReadFileSync.mockReturnValue(Buffer.from(mockSwaggerContent)); + + createDummySwagger(fromSwagger, toSwagger); + + expect(mockMkdirSync).not.toHaveBeenCalled(); + }); + + it("should handle swagger without optional fields", () => { + const fromSwagger = "/path/to/source.json"; + const toSwagger = "/path/to/target.json"; + const mockSwaggerContent = JSON.stringify({ + swagger: "2.0", + info: { title: "Test API", version: "1.0" }, + paths: { "/test": { get: {} } }, + }); + + mockExistsSync.mockReturnValue(true); + mockReadFileSync.mockReturnValue(Buffer.from(mockSwaggerContent)); + + createDummySwagger(fromSwagger, toSwagger); + + expect(mockWriteFileSync).toHaveBeenCalled(); + const writeCall = mockWriteFileSync.mock.calls[0]; + const writtenContent = JSON.parse(writeCall[1] as string); + expect(writtenContent.paths).toEqual({}); + expect(writtenContent["x-ms-paths"]).toBeUndefined(); + expect(writtenContent["x-ms-parameterized-host"]).toBeUndefined(); + }); + }); + + describe("cleanDummySwagger", () => { + it("should remove all created dummy swagger files", () => { + // Create some dummy files first + const file1 = "/path/to/dummy1.json"; + const file2 = "/path/to/dummy2.json"; + + mockExistsSync.mockReturnValue(true); + mockReadFileSync.mockReturnValue(Buffer.from('{"swagger": "2.0"}')); + + createDummySwagger("/source1.json", file1); + createDummySwagger("/source2.json", file2); + + // Clear previous mock calls before testing cleanup + mockRmSync.mockClear(); + + // Now clean them up + cleanDummySwagger(); + + expect(mockRmSync).toHaveBeenCalledWith(file1, { recursive: true, force: true }); + expect(mockRmSync).toHaveBeenCalledWith(file2, { recursive: true, force: true }); + expect(mockRmSync).toHaveBeenCalledTimes(2); + }); + + it("should handle empty list of created files", () => { + // Clear any previous calls + mockRmSync.mockClear(); + + cleanDummySwagger(); + expect(mockRmSync).not.toHaveBeenCalled(); + }); + }); + + describe("isSameVersionBreakingType", () => { + it("should return true for SameVersion type", () => { + expect(isSameVersionBreakingType("SameVersion")).toBe(true); + }); + + it("should return false for CrossVersion type", () => { + expect(isSameVersionBreakingType("CrossVersion")).toBe(false); + }); + }); + + describe("getCreatedDummySwaggerCount", () => { + it("should return current count of created dummy files", () => { + // Get initial count (may not be 0 due to other tests) + const initialCount = getCreatedDummySwaggerCount(); + + mockExistsSync.mockReturnValue(true); + mockReadFileSync.mockReturnValue(Buffer.from('{"swagger": "2.0"}')); + + createDummySwagger("/source1.json", "/dummy1.json"); + expect(getCreatedDummySwaggerCount()).toBe(initialCount + 1); + + createDummySwagger("/source2.json", "/dummy2.json"); + expect(getCreatedDummySwaggerCount()).toBe(initialCount + 2); + }); + + it("should return correct count after creating multiple dummy files", () => { + const initialCount = getCreatedDummySwaggerCount(); + + mockExistsSync.mockReturnValue(true); + mockReadFileSync.mockReturnValue(Buffer.from('{"swagger": "2.0"}')); + + createDummySwagger("/source1.json", "/dummy1.json"); + createDummySwagger("/source2.json", "/dummy2.json"); + expect(getCreatedDummySwaggerCount()).toBe(initialCount + 2); + + cleanDummySwagger(); + expect(getCreatedDummySwaggerCount()).toBe(0); + }); + + it("should return 0 after cleaning all dummy files", () => { + mockExistsSync.mockReturnValue(true); + mockReadFileSync.mockReturnValue(Buffer.from('{"swagger": "2.0"}')); + + // Create some files + createDummySwagger("/source1.json", "/dummy1.json"); + createDummySwagger("/source2.json", "/dummy2.json"); + expect(getCreatedDummySwaggerCount()).toBeGreaterThan(0); + + // Clean them + cleanDummySwagger(); + expect(getCreatedDummySwaggerCount()).toBe(0); + }); + }); + + describe("outputBreakingChangeLabelVariables", () => { + beforeEach(() => { + // Clear the labels set before each test + BreakingChangeLabelsToBeAdded.clear(); + }); + + it("should set both labels to false when no labels need to be added", async () => { + const { setOutput } = await import("../src/log.js"); + + outputBreakingChangeLabelVariables(); + + expect(setOutput).toHaveBeenCalledWith( + "breakingChangeReviewLabelName", + BreakingChangeReviewRequiredLabel, + ); + expect(setOutput).toHaveBeenCalledWith("breakingChangeReviewLabelValue", "false"); + expect(setOutput).toHaveBeenCalledWith( + "versioningReviewLabelName", + VersioningReviewRequiredLabel, + ); + expect(setOutput).toHaveBeenCalledWith("versioningReviewLabelValue", "false"); + }); + + it("should set BreakingChangeReviewRequired to true when present in labels set", async () => { + const { setOutput } = await import("../src/log.js"); + + BreakingChangeLabelsToBeAdded.add(BreakingChangeReviewRequiredLabel); + + outputBreakingChangeLabelVariables(); + expect(setOutput).toHaveBeenCalledWith( + "breakingChangeReviewLabelName", + BreakingChangeReviewRequiredLabel, + ); + expect(setOutput).toHaveBeenCalledWith("breakingChangeReviewLabelValue", "true"); + expect(setOutput).toHaveBeenCalledWith( + "versioningReviewLabelName", + VersioningReviewRequiredLabel, + ); + expect(setOutput).toHaveBeenCalledWith("versioningReviewLabelValue", "false"); + }); + + it("should set VersioningReviewRequired to true when present in labels set", async () => { + const { setOutput } = await import("../src/log.js"); + + BreakingChangeLabelsToBeAdded.add(VersioningReviewRequiredLabel); + + outputBreakingChangeLabelVariables(); + expect(setOutput).toHaveBeenCalledWith( + "breakingChangeReviewLabelName", + BreakingChangeReviewRequiredLabel, + ); + expect(setOutput).toHaveBeenCalledWith("breakingChangeReviewLabelValue", "false"); + expect(setOutput).toHaveBeenCalledWith( + "versioningReviewLabelName", + VersioningReviewRequiredLabel, + ); + expect(setOutput).toHaveBeenCalledWith("versioningReviewLabelValue", "true"); + }); + + it("should set both labels to true when both are present in labels set", async () => { + const { setOutput } = await import("../src/log.js"); + + BreakingChangeLabelsToBeAdded.add(BreakingChangeReviewRequiredLabel); + BreakingChangeLabelsToBeAdded.add(VersioningReviewRequiredLabel); + + outputBreakingChangeLabelVariables(); + expect(setOutput).toHaveBeenCalledWith( + "breakingChangeReviewLabelName", + BreakingChangeReviewRequiredLabel, + ); + expect(setOutput).toHaveBeenCalledWith("breakingChangeReviewLabelValue", "true"); + expect(setOutput).toHaveBeenCalledWith( + "versioningReviewLabelName", + VersioningReviewRequiredLabel, + ); + expect(setOutput).toHaveBeenCalledWith("versioningReviewLabelValue", "true"); + }); + + it("should handle labels set with non-review labels", async () => { + const { setOutput } = await import("../src/log.js"); + + BreakingChangeLabelsToBeAdded.add("SomeOtherLabel"); + + outputBreakingChangeLabelVariables(); + expect(setOutput).toHaveBeenCalledWith( + "breakingChangeReviewLabelName", + BreakingChangeReviewRequiredLabel, + ); + expect(setOutput).toHaveBeenCalledWith("breakingChangeReviewLabelValue", "false"); + expect(setOutput).toHaveBeenCalledWith( + "versioningReviewLabelName", + VersioningReviewRequiredLabel, + ); + expect(setOutput).toHaveBeenCalledWith("versioningReviewLabelValue", "false"); + }); + + it("should handle mixed labels including one review label", async () => { + const { setOutput } = await import("../src/log.js"); + + BreakingChangeLabelsToBeAdded.add("SomeOtherLabel"); + BreakingChangeLabelsToBeAdded.add(BreakingChangeReviewRequiredLabel); + + outputBreakingChangeLabelVariables(); + expect(setOutput).toHaveBeenCalledWith( + "breakingChangeReviewLabelName", + BreakingChangeReviewRequiredLabel, + ); + expect(setOutput).toHaveBeenCalledWith("breakingChangeReviewLabelValue", "true"); + expect(setOutput).toHaveBeenCalledWith( + "versioningReviewLabelName", + VersioningReviewRequiredLabel, + ); + expect(setOutput).toHaveBeenCalledWith("versioningReviewLabelValue", "false"); + }); + }); +}); diff --git a/eng/tools/openapi-diff-runner/test/detect-breaking-change.test.ts b/eng/tools/openapi-diff-runner/test/detect-breaking-change.test.ts new file mode 100644 index 000000000000..418e22eca6bb --- /dev/null +++ b/eng/tools/openapi-diff-runner/test/detect-breaking-change.test.ts @@ -0,0 +1,1010 @@ +import { SpecModel } from "@azure-tools/specs-shared/spec-model"; +import { existsSync } from "node:fs"; +import * as path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + checkBreakingChangeOnSameVersion, + doBreakingChangeDetection, + getReadmeFolder, + getSpecModel, + isInDevFolder, + type BreakingChangeDetectionContext, +} from "../src/detect-breaking-change.js"; +import { ApiVersionLifecycleStage, Context } from "../src/types/breaking-change.js"; +import { getExistedVersionOperations, getPrecedingSwaggers } from "../src/utils/spec.js"; + +vi.mock("@azure-tools/specs-shared/spec-model", () => ({ + SpecModel: vi.fn(), +})); + +vi.mock("../src/utils/spec.js", () => ({ + getExistedVersionOperations: vi.fn(), + getPrecedingSwaggers: vi.fn(), + deduplicateSwaggers: vi.fn(), +})); + +vi.mock("node:fs", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + appendFileSync: vi.fn(), + existsSync: vi.fn(), + }; +}); + +// Mock specific functions but keep getSpecModel as real implementation +vi.mock("../src/detect-breaking-change.js", async () => { + const original = await vi.importActual("../src/detect-breaking-change.js"); + return { + ...original, + createBreakingChangeDetectionContext: vi + .fn() + .mockImplementation( + (context, existingVersionSwaggers, newVersionSwaggers, newVersionChangedSwaggers) => ({ + context, + existingVersionSwaggers, + newVersionSwaggers, + newVersionChangedSwaggers, + msgs: [], + runtimeErrors: [], + tempTagName: "oad-default-tag", + oadViolationsCnt: 0, + errorCnt: 0, + }), + ), + }; +}); + +vi.mock("../src/utils/common-utils.js", () => ({ + convertRawErrorToUnifiedMsg: vi.fn().mockReturnValue('{"type":"Raw","level":"Error"}'), + specIsPreview: vi.fn().mockReturnValue(false), + blobHref: vi.fn().mockReturnValue("https://github.com/test/test.json"), + branchHref: vi.fn().mockReturnValue("https://github.com/test/test.json"), + getRelativeSwaggerPathToRepo: vi.fn().mockImplementation((path) => path), + processOadRuntimeErrorMessage: vi.fn(), +})); + +vi.mock("../src/log.js", () => ({ + logMessage: vi.fn(), + logError: vi.fn(), + LogLevel: { + Error: "error", + Warn: "warn", + Info: "info", + Debug: "debug", + Notice: "notice", + Group: "group", + EndGroup: "endgroup", + }, + LOG_PREFIX: "Runner-", +})); + +vi.mock("../src/run-oad.js", () => ({ + runOad: vi.fn().mockResolvedValue([]), +})); + +vi.mock("../src/utils/oad-message-processor.js", () => ({ + processAndAppendOadMessages: vi.fn(), +})); + +vi.mock("../src/utils/apply-rules.js", () => ({ + applyRules: vi.fn().mockReturnValue([]), +})); + +describe("detect-breaking-change", () => { + let mockContext: Context; + let mockDetectionContext: BreakingChangeDetectionContext; + let detectionModule: any; + + // Test constants + const TEST_CONSTANTS = { + PATHS: { + network: + "specification/network/resource-manager/Microsoft.Network/stable/2019-11-01/network.json", + storage: + "specification/storage/resource-manager/Microsoft.Storage/stable/2021-04-01/storage.json", + networkStable: + "specification/network/resource-manager/Microsoft.Network/stable/2021-05-01/network.json", + }, + FOLDERS: { + tempRepo: "/test/working/dir", + specRepo: "/test/repo", + mockFolder: "/mock/folder", + // Cross-platform test repo path (avoids leading slash issues on Windows) + testRepoPath: path.resolve("path", "to", "repo"), + }, + OPERATIONS: { + operation1: { id: "operation1", path: "/api/test1", httpMethod: "GET" }, + operation2: { id: "operation2", path: "/api/test2", httpMethod: "POST" }, + operation3: { id: "operation3", path: "/api/test3", httpMethod: "DELETE" }, + }, + }; + + // Test data factories + const TestFixtures = { + createMockSpecModel: (folder = TEST_CONSTANTS.FOLDERS.mockFolder, swaggers: any[] = []) => ({ + getSwaggers: vi.fn().mockResolvedValue(swaggers || []), + folder, + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, + readmes: [] as any[], + }), + + createSpyManager: () => { + const spies: Array = []; + return { + add: (spy: any) => { + spies.push(spy); + return spy; + }, + restoreAll: () => { + spies.forEach((spy) => spy.mockRestore()); + spies.length = 0; + }, + }; + }, + + createMockOperations: () => + new Map([ + ["operation1", TEST_CONSTANTS.OPERATIONS.operation1], + ["operation2", TEST_CONSTANTS.OPERATIONS.operation2], + ]), + + createMockOperationsArray: () => + new Map([ + [ + "/test/swagger1.json", + [TEST_CONSTANTS.OPERATIONS.operation1, TEST_CONSTANTS.OPERATIONS.operation2], + ], + ["/test/swagger2.json", [TEST_CONSTANTS.OPERATIONS.operation3]], + ]), + + createMockContext: (overrides = {}) => + ({ + prInfo: { + targetBranch: "main", + sourceBranch: "feature-branch", + baseBranch: "main", + currentBranch: "feature-branch", + tempRepoFolder: TEST_CONSTANTS.FOLDERS.tempRepo, + checkout: vi.fn(), + }, + localSpecRepoPath: TEST_CONSTANTS.FOLDERS.specRepo, + workingFolder: "/test/working", + logFileFolder: "/test/logs", + swaggerDirs: ["specification"], + baseBranch: "main", + headCommit: "abc123", + runType: "SAME_VERSION" as any, + checkName: "test-check", + targetRepo: "Azure/azure-rest-api-specs", + sourceRepo: "user/azure-rest-api-specs", + prNumber: "123", + prSourceBranch: "feature-branch", + prTargetBranch: "main", + oadMessageProcessorContext: { + logFilePath: "/test/logs/openapi-diff-runner.log", + prUrl: "https://github.com/Azure/azure-rest-api-specs/pull/123", + messageCache: [], + }, + prUrl: "https://github.com/Azure/azure-rest-api-specs/pull/123", + ...overrides, + }) as Context, + + createMockDetectionContext: (contextOverrides = {}, overrides = {}) => { + const context = TestFixtures.createMockContext(contextOverrides); + return { + context, + existingVersionSwaggers: ["existing1.json", "existing2.json"], + newVersionSwaggers: ["new1.json", "new2.json"], + newVersionChangedSwaggers: ["changed1.json", "changed2.json"], + msgs: [], + runtimeErrors: [], + oadTracer: { traces: [], baseBranch: "main", context }, + tempTagName: "oad-default-tag", + ...overrides, + } as BreakingChangeDetectionContext; + }, + + createTestContext: (overrides = {}) => + ({ + localSpecRepoPath: path.resolve("path", "to", "spec", "repo"), + ...overrides, + }) as Context, + + createMockSwagger: (pathOverride?: string, operationsOverride?: Map) => ({ + path: pathOverride || `${TEST_CONSTANTS.FOLDERS.tempRepo}/${TEST_CONSTANTS.PATHS.storage}`, + getOperations: vi + .fn() + .mockResolvedValue(operationsOverride || TestFixtures.createMockOperations()), + }), + }; + + // Mock setup utilities + const MockSetup = { + setupDefaultMocks: () => { + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(getExistedVersionOperations).mockResolvedValue(new Map()); + vi.mocked(getPrecedingSwaggers).mockResolvedValue({ + stable: "/test/previous-stable.json", + preview: "/test/previous-preview.json", + }); + }, + + setupSpecModelMock: (mockInstance?: any) => { + if (mockInstance) { + // Use the provided instance + vi.mocked(SpecModel).mockImplementation(() => mockInstance as unknown as SpecModel); + return mockInstance; + } else { + // Create different instances based on folder path + vi.mocked(SpecModel).mockImplementation((folder: string) => { + return TestFixtures.createMockSpecModel(folder) as unknown as SpecModel; + }); + return null; // No specific instance to return + } + }, + + resetAllMocks: () => { + vi.clearAllMocks(); + vi.resetAllMocks(); + }, + }; + + beforeEach(async () => { + MockSetup.resetAllMocks(); + detectionModule = await import("../src/detect-breaking-change.js"); + + mockContext = TestFixtures.createMockContext(); + mockDetectionContext = TestFixtures.createMockDetectionContext(); + + MockSetup.setupDefaultMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("getReadmeFolder", () => { + it("should return first readme.md found when searching upward", () => { + const testPath = TEST_CONSTANTS.PATHS.network; + + // Mock existsSync to return true only for the resource-manager level + vi.mocked(existsSync).mockImplementation((pathArg: any) => { + return pathArg + .toString() + .endsWith(path.join("specification", "network", "resource-manager", "readme.md")); + }); + + const result = getReadmeFolder(testPath); + expect(result).toBe(path.join("specification", "network", "resource-manager")); + }); + + it("should find readme.md at data-plane level", () => { + const testPath = + "specification/cognitiveservices/data-plane/TextAnalytics/preview/v3.1/textanalytics.json"; + + vi.mocked(existsSync).mockImplementation((pathArg: any) => { + return pathArg + .toString() + .endsWith(path.join("specification", "cognitiveservices", "data-plane", "readme.md")); + }); + + const result = getReadmeFolder(testPath); + expect(result).toBe(path.join("specification", "cognitiveservices", "data-plane")); + }); + + it("should fall back to boundary when no readme.md found", () => { + const testPath = TEST_CONSTANTS.PATHS.network; + + // No readme.md files exist + vi.mocked(existsSync).mockReturnValue(false); + + const result = getReadmeFolder(testPath); + expect(result).toBe(path.join("specification", "network", "resource-manager")); + }); + + it("should fall back to first 3 segments when no boundary found", () => { + const testPath = "specification/someservice/other/deep/path/file.json"; + + vi.mocked(existsSync).mockReturnValue(false); + + const result = getReadmeFolder(testPath); + expect(result).toBe(path.join("specification", "someservice", "other")); + }); + + it("should handle dev folder conversion", () => { + const testPath = + "dev/network/resource-manager/Microsoft.Network/stable/2019-11-01/network.json"; + + vi.mocked(existsSync).mockImplementation((pathArg: any) => { + return pathArg + .toString() + .endsWith(path.join("specification", "network", "resource-manager", "readme.md")); + }); + + const result = getReadmeFolder(testPath); + expect(result).toBe(path.join("specification", "network", "resource-manager")); + }); + + it("should return undefined for short paths", () => { + const testPath = "spec/test"; + const result = getReadmeFolder(testPath); + expect(result).toBeUndefined(); + }); + + it("should handle backslashes in paths", () => { + const testPath = + "specification\\network\\resource-manager\\Microsoft.Network\\stable\\2019-11-01\\network.json"; + + vi.mocked(existsSync).mockImplementation((pathArg: any) => { + return pathArg + .toString() + .endsWith(path.join("specification", "network", "resource-manager", "readme.md")); + }); + + const result = getReadmeFolder(testPath); + expect(result).toBe(path.join("specification", "network", "resource-manager")); + }); + + it("should find readme.md within boundary search range", () => { + const testPath = + "specification/network/resource-manager/Microsoft.Network/stable/2019-11-01/network.json"; + + // Simulate readme.md at Microsoft.Network level (within search range) + vi.mocked(existsSync).mockImplementation((pathArg: any) => { + const pathStr = pathArg.toString(); + return pathStr.includes(path.join("Microsoft.Network", "readme.md")); + }); + + // Should find readme.md at Microsoft.Network level since it's within the search range + const result = getReadmeFolder(testPath); + expect(result).toBe( + path.join("specification", "network", "resource-manager", "Microsoft.Network"), + ); + }); + }); + + describe("isInDevFolder", () => { + it("should return true for dev paths", () => { + expect(isInDevFolder("dev/test/file.json")).toBe(true); + expect(isInDevFolder("dev/network/resource-manager/test.json")).toBe(true); + }); + + it("should return false for non-dev paths", () => { + expect(isInDevFolder("specification/test/file.json")).toBe(false); + expect(isInDevFolder("other/dev/file.json")).toBe(false); + }); + }); + + describe("getSpecModel", () => { + beforeEach(() => { + MockSetup.resetAllMocks(); + }); + + // Helper function to create consistent folder existence mocks + const createFolderExistenceMock = (existingFolders: string[], readmeFolders: string[] = []) => { + return vi.mocked(existsSync).mockImplementation((pathArg: any) => { + const pathStr = pathArg.toString().replace(/\\/g, "/"); // Normalize to forward slashes for comparison + + // Check for readme.md files (used by getReadmeFolder) + if (pathStr.endsWith("readme.md")) { + return readmeFolders.some((folder) => pathStr.includes(`${folder}/readme.md`)); + } + + // Check for folder existence (used by getSpecModel) - normalize both paths for comparison + return existingFolders.some((folder) => pathStr.endsWith(folder.replace(/\\/g, "/"))); + }); + }; + + it("should create SpecModel when folder exists", () => { + const mockSpecModelInstance = TestFixtures.createMockSpecModel(); + MockSetup.setupSpecModelMock(mockSpecModelInstance); + + createFolderExistenceMock( + [path.join("specification", "network", "resource-manager")], + ["resource-manager"], + ); + + const result = getSpecModel( + TEST_CONSTANTS.FOLDERS.testRepoPath, + TEST_CONSTANTS.PATHS.network, + ); + + expect(result).toBeDefined(); + expect(vi.mocked(SpecModel)).toHaveBeenCalledWith( + path.join( + TEST_CONSTANTS.FOLDERS.testRepoPath, + "specification", + "network", + "resource-manager", + ), + ); + }); + + it("should search upward and find parent folder when initial folder doesn't exist", () => { + const mockSpecModelInstance = TestFixtures.createMockSpecModel(); + MockSetup.setupSpecModelMock(mockSpecModelInstance); + + // Microsoft.Network folder doesn't exist, but resource-manager does with readme.md + createFolderExistenceMock( + [path.join("specification", "network", "resource-manager")], + ["resource-manager"], + ); + + const result = getSpecModel( + TEST_CONSTANTS.FOLDERS.testRepoPath, + TEST_CONSTANTS.PATHS.network, + ); + + expect(result).toBeDefined(); + expect(vi.mocked(SpecModel)).toHaveBeenCalledWith( + path.join( + TEST_CONSTANTS.FOLDERS.testRepoPath, + "specification", + "network", + "resource-manager", + ), + ); + }); + + it("should handle data-plane boundary correctly", () => { + const testPath = path.join( + "specification", + "cognitiveservices", + "data-plane", + "TextAnalytics", + "preview", + "v3.1", + "textanalytics.json", + ); + const mockSpecModelInstance = TestFixtures.createMockSpecModel(); + MockSetup.setupSpecModelMock(mockSpecModelInstance); + + // TextAnalytics folder doesn't exist, but data-plane does with readme.md + createFolderExistenceMock( + [path.join("specification", "cognitiveservices", "data-plane")], + ["data-plane"], + ); + + const result = getSpecModel(TEST_CONSTANTS.FOLDERS.testRepoPath, testPath); + + expect(result).toBeDefined(); + expect(vi.mocked(SpecModel)).toHaveBeenCalledWith( + path.join( + TEST_CONSTANTS.FOLDERS.testRepoPath, + "specification", + "cognitiveservices", + "data-plane", + ), + ); + }); + + it("should return undefined when no valid folder with readme.md is found", () => { + const mockSpecModelInstance = TestFixtures.createMockSpecModel(); + MockSetup.setupSpecModelMock(mockSpecModelInstance); + + // No folders exist, not even boundary folders + createFolderExistenceMock([], []); + + const result = getSpecModel( + TEST_CONSTANTS.FOLDERS.testRepoPath, + TEST_CONSTANTS.PATHS.network, + ); + + expect(result).toBeUndefined(); + expect(vi.mocked(SpecModel)).not.toHaveBeenCalled(); + }); + + it("should use boundary folder when getReadmeFolder returns boundary folder", () => { + const mockSpecModelInstance = TestFixtures.createMockSpecModel(); + MockSetup.setupSpecModelMock(mockSpecModelInstance); + + vi.mocked(existsSync).mockImplementation((pathArg: any) => { + const pathStr = pathArg.toString().replace(/\\/g, "/"); // Normalize path separators + + // getReadmeFolder finds readme.md at resource-manager level (boundary fallback) + if (pathStr.includes("resource-manager/readme.md")) return true; + + // resource-manager folder exists (this is what getReadmeFolder returned) + if (pathStr.endsWith("specification/network/resource-manager")) return true; + + return false; + }); + + const result = getSpecModel( + TEST_CONSTANTS.FOLDERS.testRepoPath, + TEST_CONSTANTS.PATHS.network, + ); + + // Should create SpecModel for the boundary folder since it exists + expect(result).toBeDefined(); + expect(vi.mocked(SpecModel)).toHaveBeenCalledWith( + path.join( + TEST_CONSTANTS.FOLDERS.testRepoPath, + "specification", + "network", + "resource-manager", + ), + ); + }); + + it("should find readme.md in intermediate folder during upward search", () => { + const testPath = path.join( + "specification", + "network", + "resource-manager", + "Microsoft.Network", + "stable", + "2019-11-01", + "network.json", + ); + const mockSpecModelInstance = TestFixtures.createMockSpecModel(); + MockSetup.setupSpecModelMock(mockSpecModelInstance); + + // getReadmeFolder returns Microsoft.Network level, but that folder doesn't exist + // However, resource-manager level has readme.md during upward search + vi.mocked(existsSync).mockImplementation((pathArg: any) => { + const pathStr = pathArg.toString().replace(/\\/g, "/"); // Normalize path separators + + // getReadmeFolder finds Microsoft.Network level + if (pathStr.includes("Microsoft.Network/readme.md")) return true; + + // Microsoft.Network folder doesn't exist + if (pathStr.endsWith("specification/network/resource-manager/Microsoft.Network")) + return false; + + // resource-manager folder exists and has readme.md + if (pathStr.endsWith("specification/network/resource-manager")) return true; + if (pathStr.endsWith("specification/network/resource-manager/readme.md")) return true; + + return false; + }); + + const result = getSpecModel(TEST_CONSTANTS.FOLDERS.testRepoPath, testPath); + + expect(result).toBeDefined(); + // Should use resource-manager folder because that's where readme.md was found during upward search + expect(vi.mocked(SpecModel)).toHaveBeenCalledWith( + path.join( + TEST_CONSTANTS.FOLDERS.testRepoPath, + "specification", + "network", + "resource-manager", + ), + ); + }); + + it("should create different SpecModels for different folders", () => { + MockSetup.setupSpecModelMock(); + + // Both services have their folders and readme.md files + createFolderExistenceMock( + [ + path.join("specification", "network", "resource-manager"), + path.join("specification", "storage", "resource-manager"), + ], + ["resource-manager"], + ); + + const result1 = getSpecModel( + TEST_CONSTANTS.FOLDERS.testRepoPath, + TEST_CONSTANTS.PATHS.network, + ); + const result2 = getSpecModel( + TEST_CONSTANTS.FOLDERS.testRepoPath, + TEST_CONSTANTS.PATHS.storage, + ); + + expect(result1).not.toBe(result2); + expect(result1).toBeDefined(); + expect(result2).toBeDefined(); + }); + + it("should handle dev folder conversion in upward search", () => { + const testPath = path.join( + "dev", + "network", + "resource-manager", + "Microsoft.Network", + "stable", + "2019-11-01", + "network.json", + ); + const mockSpecModelInstance = TestFixtures.createMockSpecModel(); + MockSetup.setupSpecModelMock(mockSpecModelInstance); + + // After dev->specification conversion, resource-manager folder exists with readme.md + createFolderExistenceMock( + [path.join("specification", "network", "resource-manager")], + ["resource-manager"], + ); + + const result = getSpecModel(TEST_CONSTANTS.FOLDERS.testRepoPath, testPath); + + expect(result).toBeDefined(); + expect(vi.mocked(SpecModel)).toHaveBeenCalledWith( + path.join( + TEST_CONSTANTS.FOLDERS.testRepoPath, + "specification", + "network", + "resource-manager", + ), + ); + }); + }); + + describe("checkAPIsBeingMovedToANewSpec", () => { + beforeEach(() => { + MockSetup.resetAllMocks(); + }); + + it("should process moved APIs when found", async () => { + const spyManager = TestFixtures.createSpyManager(); + const mockOperationsArray = TestFixtures.createMockOperationsArray(); + const mockTargetOperations = TestFixtures.createMockOperations(); + + const checkAPIsBeingMovedToANewSpecSpy = spyManager.add( + vi.spyOn(detectionModule, "checkAPIsBeingMovedToANewSpec"), + ); + + checkAPIsBeingMovedToANewSpecSpy.mockImplementation( + async (_context: any, swaggerPath: any, _availableSwaggers: any) => { + await getExistedVersionOperations(swaggerPath, [], [...mockTargetOperations.values()]); + return; + }, + ); + + vi.mocked(getExistedVersionOperations).mockResolvedValue(mockOperationsArray); + const testContext = TestFixtures.createTestContext(); + + await detectionModule.checkAPIsBeingMovedToANewSpec( + testContext, + TEST_CONSTANTS.PATHS.networkStable, + [], + ); + + expect(checkAPIsBeingMovedToANewSpecSpy).toHaveBeenCalledWith( + testContext, + TEST_CONSTANTS.PATHS.networkStable, + [], + ); + expect(vi.mocked(getExistedVersionOperations)).toHaveBeenCalledWith( + TEST_CONSTANTS.PATHS.networkStable, + [], + [...mockTargetOperations.values()], + ); + + spyManager.restoreAll(); + }); + + it("should handle empty moved APIs", async () => { + const spyManager = TestFixtures.createSpyManager(); + vi.clearAllMocks(); + + const mockTargetOperations = TestFixtures.createMockOperations(); + + const checkAPIsBeingMovedToANewSpecSpy = spyManager.add( + vi.spyOn(detectionModule, "checkAPIsBeingMovedToANewSpec"), + ); + + checkAPIsBeingMovedToANewSpecSpy.mockImplementation( + async (_context: any, swaggerPath: any, _availableSwaggers: any) => { + await getExistedVersionOperations(swaggerPath, [], [...mockTargetOperations.values()]); + return; + }, + ); + + vi.mocked(getExistedVersionOperations).mockResolvedValue(new Map()); + const testContext = TestFixtures.createTestContext(); + + await detectionModule.checkAPIsBeingMovedToANewSpec( + testContext, + TEST_CONSTANTS.PATHS.storage, + [], + ); + + expect(checkAPIsBeingMovedToANewSpecSpy).toHaveBeenCalledWith( + testContext, + TEST_CONSTANTS.PATHS.storage, + [], + ); + expect(vi.mocked(getExistedVersionOperations)).toHaveBeenCalledWith( + TEST_CONSTANTS.PATHS.storage, + [], + [...mockTargetOperations.values()], + ); + + spyManager.restoreAll(); + }); + }); + + describe("checkCrossVersionBreakingChange", () => { + let mockSpecModelInstance: any; + + beforeEach(async () => { + mockSpecModelInstance = TestFixtures.createMockSpecModel("/mock/folder", [ + { path: "/test/swagger1.json" }, + { path: "/test/swagger2.json" }, + ]); + + vi.mocked(SpecModel).mockImplementation(() => mockSpecModelInstance); + vi.mocked(getPrecedingSwaggers).mockResolvedValue({ + stable: "/test/previous-stable.json", + preview: "/test/previous-preview.json", + }); + }); + + it("should process new version swaggers", async () => { + const detectionModule = await import("../src/detect-breaking-change.js"); + const getSpecModelSpy = vi.spyOn(detectionModule, "getSpecModel"); + getSpecModelSpy.mockReturnValue(mockSpecModelInstance); + + mockDetectionContext.newVersionSwaggers = [TEST_CONSTANTS.PATHS.networkStable]; + mockDetectionContext.newVersionChangedSwaggers = []; + mockDetectionContext.existingVersionSwaggers = []; + + const result = await detectionModule.checkCrossVersionBreakingChange(mockDetectionContext); + + expect(result).toBeDefined(); + expect(result.msgs).toBeDefined(); + expect(result.runtimeErrors).toBeDefined(); + expect(result.oadViolationsCnt).toBeDefined(); + expect(result.errorCnt).toBeDefined(); + }); + + it("should process swaggers with previous versions", async () => { + // Complete mock reset to avoid interference from other tests + vi.clearAllMocks(); + vi.resetAllMocks(); + + // Ensure existsSync returns true for this test so getSpecModel doesn't return undefined + vi.mocked(existsSync).mockReturnValue(true); + + const mockTargetOperations = new Map([ + ["operation1", { id: "operation1", path: "/api/test1", httpMethod: "GET" }], + ]); + + const mockTargetSwagger = { + path: "/test/working/dir/specification/storage/resource-manager/Microsoft.Storage/stable/2021-04-01/storage.json", + getOperations: vi.fn().mockResolvedValue(mockTargetOperations), + }; + + // Create a proper mock SpecModel instance that actually works + const mockSpecModelInstance = { + getSwaggers: vi.fn().mockResolvedValue([mockTargetSwagger]), + folder: "/test/working/dir/specification/storage/resource-manager", + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, + readmes: [] as any[], + }; + + // Mock SpecModel constructor directly + vi.mocked(SpecModel).mockImplementation(() => mockSpecModelInstance as unknown as SpecModel); + + // Mock getExistedVersionOperations to return a proper Map + vi.mocked(getExistedVersionOperations).mockResolvedValue(new Map()); + + // Mock getPrecedingSwaggers to return some previous versions to avoid checkAPIsBeingMovedToANewSpec call + vi.mocked(getPrecedingSwaggers).mockResolvedValue({ + stable: "/test/previous-stable.json", + preview: undefined, + }); + + // Import the module + const detectionModule = await import("../src/detect-breaking-change.js"); + + mockDetectionContext.newVersionSwaggers = [TEST_CONSTANTS.PATHS.storage]; + mockDetectionContext.newVersionChangedSwaggers = []; + mockDetectionContext.existingVersionSwaggers = []; + mockDetectionContext.context = { + ...mockContext, + localSpecRepoPath: TEST_CONSTANTS.FOLDERS.testRepoPath, + prInfo: { + ...mockContext.prInfo, + tempRepoFolder: "/test/working/dir", + }, + } as Context; + + const result = await detectionModule.checkCrossVersionBreakingChange(mockDetectionContext); + + expect(result).toBeDefined(); + // For this simplified test, just verify that the function completes successfully + expect(result.msgs).toBeDefined(); + expect(result.runtimeErrors).toBeDefined(); + + // Verify that SpecModel was called to create the specModel + expect(vi.mocked(SpecModel)).toHaveBeenCalled(); + }); + }); + + describe("createBreakingChangeDetectionContext", () => { + it("should create context with all required properties", async () => { + vi.mocked(detectionModule.createBreakingChangeDetectionContext).mockImplementation( + ( + context: any, + existingVersionSwaggers: any, + newVersionSwaggers: any, + newVersionChangedSwaggers: any, + oadTracer: any, + ) => ({ + context, + existingVersionSwaggers, + newVersionSwaggers, + newVersionChangedSwaggers, + msgs: [], + runtimeErrors: [], + tempTagName: "oad-default-tag", + oadTracer, + }), + ); + + const context = detectionModule.createBreakingChangeDetectionContext( + mockContext, + ["existing1.json"], + ["new1.json"], + ["changed1.json"], + {} as any, + ); + + expect(detectionModule.createBreakingChangeDetectionContext).toHaveBeenCalled(); + expect(context.context).toBe(mockContext); + expect(context.existingVersionSwaggers).toEqual(["existing1.json"]); + expect(context.newVersionSwaggers).toEqual(["new1.json"]); + expect(context.newVersionChangedSwaggers).toEqual(["changed1.json"]); + }); + }); + + describe("checkBreakingChangeOnSameVersion", () => { + beforeEach(() => { + mockDetectionContext.existingVersionSwaggers = [ + TEST_CONSTANTS.PATHS.networkStable, + TEST_CONSTANTS.PATHS.storage, + ]; + mockDetectionContext.msgs = []; + mockDetectionContext.runtimeErrors = []; + }); + + it("should process all existing version swaggers", async () => { + const result = await checkBreakingChangeOnSameVersion(mockDetectionContext); + + expect(result).toBeDefined(); + expect(result.msgs).toBeDefined(); + expect(result.runtimeErrors).toBeDefined(); + expect(result.oadViolationsCnt).toBeDefined(); + expect(result.errorCnt).toBeDefined(); + }); + + it("should handle empty existing version swaggers", async () => { + mockDetectionContext.existingVersionSwaggers = []; + + const result = await checkBreakingChangeOnSameVersion(mockDetectionContext); + + expect(result.msgs).toEqual([]); + expect(result.runtimeErrors).toEqual([]); + expect(result.oadViolationsCnt).toBe(0); + expect(result.errorCnt).toBe(0); + }); + + it("should accumulate violations and errors from multiple swaggers", async () => { + mockDetectionContext.existingVersionSwaggers = [ + TEST_CONSTANTS.PATHS.networkStable, + TEST_CONSTANTS.PATHS.storage, + ]; + + const result = await checkBreakingChangeOnSameVersion(mockDetectionContext); + + expect(result).toBeDefined(); + expect(typeof result.oadViolationsCnt).toBe("number"); + expect(typeof result.errorCnt).toBe("number"); + }); + }); + + describe("doBreakingChangeDetection", () => { + const mockOldSpec = "/old/spec/path.json"; + const mockNewSpec = + "specification/test/resource-manager/Microsoft.Test/stable/2021-05-01/test.json"; + let mockCheckout: any; + + beforeEach(() => { + mockDetectionContext.msgs = []; + mockDetectionContext.runtimeErrors = []; + mockCheckout = vi.fn().mockResolvedValue(undefined); + mockDetectionContext.context = { + ...mockContext, + prInfo: { ...mockContext.prInfo, checkout: mockCheckout }, + } as any; + }); + + it("should successfully detect breaking changes", async () => { + const result = await doBreakingChangeDetection( + mockDetectionContext, + mockOldSpec, + mockNewSpec, + "SAME_VERSION" as any, + ApiVersionLifecycleStage.STABLE, + ); + + expect(result).toBeDefined(); + expect(typeof result.oadViolationsCnt).toBe("number"); + expect(typeof result.errorCnt).toBe("number"); + expect(result.oadViolationsCnt).toBeGreaterThanOrEqual(0); + expect(result.errorCnt).toBeGreaterThanOrEqual(0); + }); + + it("should handle cross-version breaking change detection", async () => { + const result = await doBreakingChangeDetection( + mockDetectionContext, + mockOldSpec, + mockNewSpec, + "CROSS_VERSION" as any, + ApiVersionLifecycleStage.PREVIEW, + ); + + expect(result).toBeDefined(); + expect(typeof result.oadViolationsCnt).toBe("number"); + expect(typeof result.errorCnt).toBe("number"); + }); + + it("should handle runtime errors gracefully", async () => { + const errorDetectionContext = { + ...mockDetectionContext, + context: { + ...mockDetectionContext.context, + prInfo: { + ...mockDetectionContext.context.prInfo, + checkout: vi.fn().mockRejectedValue(new Error("Checkout failed")), + }, + }, + } as any; + + const result = await doBreakingChangeDetection( + errorDetectionContext, + mockOldSpec, + mockNewSpec, + "SAME_VERSION" as any, + ApiVersionLifecycleStage.STABLE, + ); + + expect(result).toBeDefined(); + expect(result.errorCnt).toBeGreaterThan(0); + expect(errorDetectionContext.runtimeErrors.length).toBeGreaterThan(0); + }); + + it("should update detection context with messages and errors", async () => { + const initialMsgsLength = mockDetectionContext.msgs.length; + const initialErrorsLength = mockDetectionContext.runtimeErrors.length; + + await doBreakingChangeDetection( + mockDetectionContext, + mockOldSpec, + mockNewSpec, + "SAME_VERSION" as any, + ApiVersionLifecycleStage.STABLE, + ); + + expect(mockDetectionContext.msgs.length).toBeGreaterThanOrEqual(initialMsgsLength); + expect(mockDetectionContext.runtimeErrors.length).toBeGreaterThanOrEqual(initialErrorsLength); + }); + + it("should process different API version lifecycle stages", async () => { + const stableResult = await doBreakingChangeDetection( + mockDetectionContext, + mockOldSpec, + mockNewSpec, + "SAME_VERSION" as any, + ApiVersionLifecycleStage.STABLE, + ); + + expect(stableResult).toBeDefined(); + + const previewResult = await doBreakingChangeDetection( + mockDetectionContext, + mockOldSpec, + mockNewSpec, + "SAME_VERSION" as any, + ApiVersionLifecycleStage.PREVIEW, + ); + + expect(previewResult).toBeDefined(); + }); + }); +}); diff --git a/eng/tools/openapi-diff-runner/test/generate-report.test.ts b/eng/tools/openapi-diff-runner/test/generate-report.test.ts new file mode 100644 index 000000000000..c177f2cd07f0 --- /dev/null +++ b/eng/tools/openapi-diff-runner/test/generate-report.test.ts @@ -0,0 +1,292 @@ +import { BREAKING_CHANGES_CHECK_TYPES } from "@azure-tools/specs-shared/breaking-change"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { generateBreakingChangeResultSummary } from "../src/generate-report.js"; +import { addToSummary, logMessage } from "../src/log.js"; +import { Context } from "../src/types/breaking-change.js"; +import { RawMessageRecord, ResultMessageRecord } from "../src/types/message.js"; +import { + BreakingChangeMdReport, + createBreakingChangeMdReport, + reportToString, + sortBreakingChangeMdReports, +} from "../src/utils/markdown-report.js"; + +// Mock dependencies +vi.mock("../src/log.js"); +vi.mock("../src/utils/markdown-report.js"); + +describe("generate-report", () => { + // Test constants + const TEST_CONSTANTS = { + PATHS: { + REPO: "/path/to/repo", + WORKING: "/working", + LOGS: "/logs", + LOG_FILE: "/log/path", + SUMMARY: "/path/to/summary", + }, + MESSAGES: { + SUCCESS_LOG: "Successfully wrote", + DETECTED_ERRORS_WARNINGS: "Detected: 1 Errors, 1 Warnings", + DETECTED_WARNINGS: "Detected: 2 Warnings", + NO_BREAKING_CHANGES: "No breaking changes detected", + IMPORTANT_NOTICE: "> [!IMPORTANT]\n> Browse to the job logs to see the details.", + STABLE_VERSION_COMPARISON: + "The following breaking changes have been detected in comparison to the latest stable version", + PREVIEW_VERSION_COMPARISON: + "The following breaking changes have been detected in comparison to the latest preview version", + MESSAGE_RECORDS_LOG: "messageRecords# raw/result/all: 1/0/1", + }, + TABLE_CONTENT: { + SPECS: "| Spec | Status |\n|------|--------|\n| test.json | Modified |", + SPECS_HEADER: "| Spec | Status |", + }, + TEXTS: { + ADDITIONAL_DETAILS: "\n\nAdditional details...", + SUPPRESSION_INFO: "\n\n**Suppression Info:**\nSome details about suppressions.", + }, + CHECK_NAMES: { + SWAGGER: "Swagger BreakingChange", + CROSS_VERSION: "BreakingChange(Cross-Version)", + }, + REPO: "test/repo", + PR_NUMBER: "123", + BRANCHES: { + MAIN: "main", + FEATURE: "feature", + }, + TEST_TIME: new Date("2023-01-01"), + }; + + const mockAddToSummary = vi.mocked(addToSummary); + const mockLogMessage = vi.mocked(logMessage); + const mockCreateBreakingChangeMdReport = vi.mocked(createBreakingChangeMdReport); + const mockReportToString = vi.mocked(reportToString); + const mockSortBreakingChangeMdReports = vi.mocked(sortBreakingChangeMdReports); + + beforeEach(() => { + vi.clearAllMocks(); + + // Mock environment variable for GitHub Actions + vi.stubEnv("GITHUB_STEP_SUMMARY", TEST_CONSTANTS.PATHS.SUMMARY); + + // Setup default mock implementations + mockCreateBreakingChangeMdReport.mockReturnValue({ + msgs: [], + rows: [], + type: "Result", + level: "Error", + rawMessage: "Test report", + } as BreakingChangeMdReport); + + mockReportToString.mockReturnValue("Mock report string"); + mockSortBreakingChangeMdReports.mockImplementation((reports) => reports); + }); + + afterEach(() => { + vi.resetAllMocks(); + vi.unstubAllEnvs(); + }); + + // Factory functions + const createMockContext = (overrides: Partial = {}): Context => ({ + localSpecRepoPath: TEST_CONSTANTS.PATHS.REPO, + workingFolder: TEST_CONSTANTS.PATHS.WORKING, + logFileFolder: TEST_CONSTANTS.PATHS.LOGS, + swaggerDirs: ["specification"], + baseBranch: TEST_CONSTANTS.BRANCHES.MAIN, + headCommit: "HEAD", + runType: BREAKING_CHANGES_CHECK_TYPES.SAME_VERSION, + checkName: TEST_CONSTANTS.CHECK_NAMES.SWAGGER, + targetRepo: TEST_CONSTANTS.REPO, + sourceRepo: TEST_CONSTANTS.REPO, + prNumber: TEST_CONSTANTS.PR_NUMBER, + prSourceBranch: TEST_CONSTANTS.BRANCHES.FEATURE, + prTargetBranch: TEST_CONSTANTS.BRANCHES.MAIN, + oadMessageProcessorContext: { + logFilePath: TEST_CONSTANTS.PATHS.LOG_FILE, + prUrl: `https://github.com/${TEST_CONSTANTS.REPO}/pull/${TEST_CONSTANTS.PR_NUMBER}`, + messageCache: [], + }, + prUrl: `https://github.com/${TEST_CONSTANTS.REPO}/pull/${TEST_CONSTANTS.PR_NUMBER}`, + ...overrides, + }); + + const createMockResultMessage = ( + overrides: Partial = {}, + ): ResultMessageRecord => ({ + type: "Result", + level: "Error", + message: "Test error message", + time: TEST_CONSTANTS.TEST_TIME, + paths: [], + groupName: "stable", + ...overrides, + }); + + const createMockRawMessage = (overrides: Partial = {}): RawMessageRecord => ({ + type: "Raw", + level: "Error", + message: "Test raw error", + time: TEST_CONSTANTS.TEST_TIME, + groupName: "stable", + extra: {}, + ...overrides, + }); + + // Test parameter factory + interface TestParameters { + context?: Context; + messages?: ResultMessageRecord[]; + runtimeErrors?: RawMessageRecord[]; + comparedSpecsTableContent?: string; + summaryDataSuppressionAndDetailsText?: string; + } + + const createTestParameters = (overrides: TestParameters = {}) => ({ + context: overrides.context || createMockContext(), + messages: overrides.messages || [], + runtimeErrors: overrides.runtimeErrors || [], + comparedSpecsTableContent: overrides.comparedSpecsTableContent || "", + summaryDataSuppressionAndDetailsText: overrides.summaryDataSuppressionAndDetailsText || "", + }); + + // Helper functions + const callGenerateBreakingChangeResultSummary = async (params: TestParameters = {}) => { + const testParams = createTestParameters(params); + return generateBreakingChangeResultSummary( + testParams.context, + testParams.messages, + testParams.runtimeErrors, + testParams.comparedSpecsTableContent, + testParams.summaryDataSuppressionAndDetailsText, + ); + }; + + const expectSummaryContains = (content: string) => { + expect(mockAddToSummary).toHaveBeenCalledWith(expect.stringContaining(content)); + }; + + const expectLogMessage = (content: string) => { + expect(mockLogMessage).toHaveBeenCalledWith(expect.stringContaining(content)); + }; + + describe("generateBreakingChangeResultSummary", () => { + it("should generate summary with success status", async () => { + await callGenerateBreakingChangeResultSummary({ + comparedSpecsTableContent: TEST_CONSTANTS.TABLE_CONTENT.SPECS, + summaryDataSuppressionAndDetailsText: TEST_CONSTANTS.TEXTS.ADDITIONAL_DETAILS, + }); + + expectLogMessage(TEST_CONSTANTS.MESSAGES.SUCCESS_LOG); + }); + + it("should generate summary with failure status", async () => { + const messages = [ + createMockResultMessage({ level: "Error" }), + createMockResultMessage({ level: "Warning" }), + ]; + + await callGenerateBreakingChangeResultSummary({ messages }); + + expectSummaryContains(TEST_CONSTANTS.MESSAGES.DETECTED_ERRORS_WARNINGS); + }); + + it("should handle messages with only warnings", async () => { + const messages = [ + createMockResultMessage({ level: "Warning" }), + createMockResultMessage({ level: "Warning" }), + ]; + + await callGenerateBreakingChangeResultSummary({ messages }); + + expectSummaryContains(TEST_CONSTANTS.MESSAGES.DETECTED_WARNINGS); + }); + + it("should include compared specs table content", async () => { + await callGenerateBreakingChangeResultSummary({ + comparedSpecsTableContent: TEST_CONSTANTS.TABLE_CONTENT.SPECS, + }); + + expectSummaryContains(TEST_CONSTANTS.TABLE_CONTENT.SPECS_HEADER); + }); + + it("should handle runtime errors", async () => { + const runtimeErrors = [createMockRawMessage({ level: "Error" })]; + + await callGenerateBreakingChangeResultSummary({ runtimeErrors }); + + expectLogMessage(TEST_CONSTANTS.MESSAGES.MESSAGE_RECORDS_LOG); + }); + + it("should handle different check names", async () => { + const context = createMockContext({ + checkName: TEST_CONSTANTS.CHECK_NAMES.CROSS_VERSION, + }); + const messages = [ + createMockResultMessage({ groupName: "stable" }), + createMockResultMessage({ groupName: "preview" }), + ]; + + await callGenerateBreakingChangeResultSummary({ context, messages }); + + expectSummaryContains("Detected"); + }); + + it("should handle mixed stable and preview messages", async () => { + const messages = [ + createMockResultMessage({ level: "Error", groupName: "stable" }), + createMockResultMessage({ level: "Warning", groupName: "preview" }), + createMockResultMessage({ level: "Error", groupName: "preview" }), + ]; + + mockCreateBreakingChangeMdReport.mockReturnValue({ + msgs: [], + rows: [], + type: "Result", + level: "Error", + rawMessage: "Test report with messages", + } as BreakingChangeMdReport); + + await callGenerateBreakingChangeResultSummary({ messages }); + + expect(mockCreateBreakingChangeMdReport).toHaveBeenCalled(); + expect(mockSortBreakingChangeMdReports).toHaveBeenCalled(); + }); + + it("should handle empty message lists", async () => { + await callGenerateBreakingChangeResultSummary(); + + expectSummaryContains(TEST_CONSTANTS.MESSAGES.NO_BREAKING_CHANGES); + }); + + it("should handle cross-version check with different API versions", async () => { + const context = createMockContext({ + checkName: TEST_CONSTANTS.CHECK_NAMES.CROSS_VERSION, + }); + const messages = [ + createMockResultMessage({ groupName: "stable" }), + createMockResultMessage({ groupName: "preview" }), + ]; + + await callGenerateBreakingChangeResultSummary({ context, messages }); + + expectSummaryContains(TEST_CONSTANTS.MESSAGES.STABLE_VERSION_COMPARISON); + expectSummaryContains(TEST_CONSTANTS.MESSAGES.PREVIEW_VERSION_COMPARISON); + }); + + it("should include suppression and details text in summary", async () => { + await callGenerateBreakingChangeResultSummary({ + summaryDataSuppressionAndDetailsText: TEST_CONSTANTS.TEXTS.SUPPRESSION_INFO, + }); + + expectSummaryContains("**Suppression Info:**\nSome details about suppressions."); + }); + + it("should include important notice in summary", async () => { + await callGenerateBreakingChangeResultSummary(); + + expectSummaryContains(TEST_CONSTANTS.MESSAGES.IMPORTANT_NOTICE); + }); + }); +}); diff --git a/eng/tools/openapi-diff-runner/test/types/oad-types.test.ts b/eng/tools/openapi-diff-runner/test/types/oad-types.test.ts new file mode 100644 index 000000000000..c0c24872b95a --- /dev/null +++ b/eng/tools/openapi-diff-runner/test/types/oad-types.test.ts @@ -0,0 +1,91 @@ +import { BREAKING_CHANGES_CHECK_TYPES } from "@azure-tools/specs-shared/breaking-change"; +import { describe, expect, it } from "vitest"; +import { Context } from "../../src/types/breaking-change.js"; +import { + addOadTrace, + createOadTrace, + generateOadMarkdown, + setOadBaseBranch, +} from "../../src/types/oad-types.js"; + +const mockContext: Context = { + runType: BREAKING_CHANGES_CHECK_TYPES.SAME_VERSION, + prUrl: "https://github.com/Azure/azure-rest-api-specs/pull/12345", + prTargetBranch: "main", + prSourceBranch: "feature-branch", + headCommit: "abc123", + baseBranch: "main", + localSpecRepoPath: "/path/to/repo", + workingFolder: "/path/to/working", + logFileFolder: "/path/to/logs", + swaggerDirs: ["/path/to/swagger"], + checkName: "test-check", + targetRepo: "Azure/azure-rest-api-specs", + sourceRepo: "Azure/azure-rest-api-specs", + prNumber: "12345", + oadMessageProcessorContext: { + logFilePath: "/path/to/log", + prUrl: "https://github.com/Azure/azure-rest-api-specs/pull/12345", + messageCache: [], + }, +}; + +describe("OAD Trace Functions", () => { + it("should create an empty trace data structure", () => { + const traceData = createOadTrace(mockContext); + expect(traceData.traces).toEqual([]); + expect(traceData.context).toBe(mockContext); + }); + + it("should add a trace entry", () => { + const traceData = createOadTrace(mockContext); + const updatedTrace = addOadTrace(traceData, "path/to/old.json", "path/to/new.json"); + + expect(updatedTrace.traces).toHaveLength(1); + expect(updatedTrace.traces[0]).toEqual({ + old: "path/to/old.json", + new: "path/to/new.json", + }); + }); + + it("should set base branch", () => { + const traceData = createOadTrace(mockContext); + const updatedTrace = setOadBaseBranch(traceData, "feature-branch"); + + expect(updatedTrace.baseBranch).toBe("feature-branch"); + }); + + it("should generate empty markdown when no traces", () => { + const traceData = createOadTrace(mockContext); + const markdown = generateOadMarkdown(traceData); + + expect(markdown).toBe(""); + }); + + it("should generate markdown table when traces exist", () => { + let traceData = createOadTrace(mockContext); + traceData = addOadTrace( + traceData, + "specification/storage/resource-manager/Microsoft.Storage/stable/2021-09-01/storage.json", + "specification/storage/resource-manager/Microsoft.Storage/stable/2021-09-01/storage.json", + ); + + const markdown = generateOadMarkdown(traceData); + + expect(markdown).toContain("| Compared specs"); + expect(markdown).toContain("storage.json"); + expect(markdown).toContain("2021-09-01"); + expect(markdown).toContain("abc123"); + expect(markdown).toContain("main"); + }); + + it("should accumulate multiple traces", () => { + let traceData = createOadTrace(mockContext); + traceData = addOadTrace(traceData, "path/to/old1.json", "path/to/new1.json"); + traceData = addOadTrace(traceData, "path/to/old2.json", "path/to/new2.json"); + + expect(traceData.traces).toHaveLength(2); + expect(traceData.traces[0].old).toBe("path/to/old1.json"); + expect(traceData.traces[1].old).toBe("path/to/old2.json"); + }); +}); diff --git a/eng/tools/openapi-diff-runner/test/utils/apply-rules.test.ts b/eng/tools/openapi-diff-runner/test/utils/apply-rules.test.ts new file mode 100644 index 000000000000..068112462db5 --- /dev/null +++ b/eng/tools/openapi-diff-runner/test/utils/apply-rules.test.ts @@ -0,0 +1,223 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { BreakingChangeLabelsToBeAdded } from "../../src/command-helpers.js"; +import { logMessage, logWarning } from "../../src/log.js"; +import { ApiVersionLifecycleStage } from "../../src/types/breaking-change.js"; +import { OadMessage } from "../../src/types/oad-types.js"; +import { applyRules } from "../../src/utils/apply-rules.js"; + +// Mock the command-helpers module +vi.mock("../../src/command-helpers.js", () => ({ + BreakingChangeLabelsToBeAdded: { + add: vi.fn(), + clear: vi.fn(), + values: [], + }, +})); + +// Mock the log module +vi.mock("../../src/log.js", () => ({ + logMessage: vi.fn(), + logWarning: vi.fn(), + LogLevel: { + Info: "Info", + Warning: "Warning", + Error: "Error", + }, +})); + +// Mock the oad-rule-map module +vi.mock("../../src/utils/oad-rule-map.js", () => ({ + oadMessagesRuleMap: [ + { + scenario: "SameVersion", + code: "AddedRequiredProperty", + severity: "Error", + label: "BreakingChangeReviewRequired", + }, + { + scenario: "CrossVersion", + code: "AddedRequiredProperty", + severity: "Error", + label: "BreakingChangeReviewRequired", + }, + { + scenario: "SameVersion", + code: "RemovedProperty", + severity: "Warning", + label: null, + }, + ], + fallbackRule: { + severity: "Warning", + label: null, + }, + fallbackLabel: "BreakingChangeReviewRequired", +})); + +const createTestOadMessage = ( + code: string = "AddedRequiredProperty", + id: string = "1001", +): OadMessage => ({ + type: "Info", + code: code as any, + id, + message: `Test message for ${code}`, + docUrl: `https://docs.example.com/rules/${code}`, + mode: "Addition", + new: { location: "specification/test.json#L10", path: "specification/test.json" }, + old: { location: "specification/test.json#L8", path: "specification/test.json" }, +}); + +describe("apply-rules", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("applyRules", () => { + it("should apply matching rule for same version scenario", () => { + const oadMessages: OadMessage[] = [createTestOadMessage()]; + + const result = applyRules(oadMessages, "SameVersion", ApiVersionLifecycleStage.STABLE); + + expect(result).toHaveLength(1); + expect(result[0].type).toBe("Error"); + expect(result[0].groupName).toBe(ApiVersionLifecycleStage.STABLE); + expect(BreakingChangeLabelsToBeAdded.add).toHaveBeenCalledWith( + "BreakingChangeReviewRequired", + ); + }); + + it("should apply matching rule for cross version scenario", () => { + const oadMessages: OadMessage[] = [createTestOadMessage()]; + + const result = applyRules(oadMessages, "CrossVersion", ApiVersionLifecycleStage.STABLE); + + expect(result).toHaveLength(1); + expect(result[0].type).toBe("Error"); + expect(result[0].groupName).toBe(ApiVersionLifecycleStage.STABLE); + expect(BreakingChangeLabelsToBeAdded.add).toHaveBeenCalledWith( + "BreakingChangeReviewRequired", + ); + }); + + it("should downgrade error to warning for cross version against previous preview", () => { + const oadMessages: OadMessage[] = [createTestOadMessage()]; + + const result = applyRules(oadMessages, "CrossVersion", ApiVersionLifecycleStage.PREVIEW); + + expect(result).toHaveLength(1); + expect(result[0].type).toBe("Warning"); + expect(result[0].groupName).toBe(ApiVersionLifecycleStage.PREVIEW); + expect(BreakingChangeLabelsToBeAdded.add).not.toHaveBeenCalled(); + }); + + it("should use VersioningReviewRequired label for same version preview", () => { + const oadMessages: OadMessage[] = [createTestOadMessage()]; + + const result = applyRules(oadMessages, "SameVersion", ApiVersionLifecycleStage.PREVIEW); + + expect(result).toHaveLength(1); + expect(result[0].type).toBe("Error"); + expect(result[0].groupName).toBe(ApiVersionLifecycleStage.PREVIEW); + expect(BreakingChangeLabelsToBeAdded.add).toHaveBeenCalledWith("VersioningReviewRequired"); + }); + + it("should not add label for warning severity", () => { + const oadMessages: OadMessage[] = [createTestOadMessage("RemovedProperty", "1002")]; + + const result = applyRules(oadMessages, "SameVersion", ApiVersionLifecycleStage.STABLE); + + expect(result).toHaveLength(1); + expect(result[0].type).toBe("Warning"); + expect(result[0].groupName).toBe(ApiVersionLifecycleStage.STABLE); + expect(BreakingChangeLabelsToBeAdded.add).not.toHaveBeenCalled(); + }); + + it("should use fallback rule when no matching rule found", () => { + const oadMessages: OadMessage[] = [createTestOadMessage("TypeChanged", "1003")]; + + const result = applyRules(oadMessages, "SameVersion", ApiVersionLifecycleStage.STABLE); + + expect(result).toHaveLength(1); + expect(result[0].type).toBe("Warning"); + expect(result[0].groupName).toBe(ApiVersionLifecycleStage.STABLE); + expect(logWarning).toHaveBeenCalledWith( + expect.stringContaining("No rule found for scenario"), + ); + }); + + it("should handle multiple messages", () => { + const oadMessages: OadMessage[] = [ + createTestOadMessage("AddedRequiredProperty", "1001"), + createTestOadMessage("RemovedProperty", "1002"), + ]; + + const result = applyRules(oadMessages, "SameVersion", ApiVersionLifecycleStage.STABLE); + + expect(result).toHaveLength(2); + expect(result[0].type).toBe("Error"); + expect(result[0].groupName).toBe(ApiVersionLifecycleStage.STABLE); + expect(result[1].type).toBe("Warning"); + expect(result[1].groupName).toBe(ApiVersionLifecycleStage.STABLE); + expect(BreakingChangeLabelsToBeAdded.add).toHaveBeenCalledTimes(1); + expect(BreakingChangeLabelsToBeAdded.add).toHaveBeenCalledWith( + "BreakingChangeReviewRequired", + ); + }); + + it("should preserve original message properties", () => { + const originalMessage: OadMessage = { + type: "Info", + code: "AddedRequiredProperty", + id: "1001", + message: "Added required property 'test'", + docUrl: "https://docs.example.com/rules/AddedRequiredProperty", + mode: "Addition", + new: { + location: "specification/test.json#L10", + path: "specification/test.json", + ref: "test-ref", + }, + old: { location: "specification/test.json#L8", path: "specification/test.json" }, + }; + + const result = applyRules([originalMessage], "SameVersion", ApiVersionLifecycleStage.STABLE); + + expect(result[0]).toMatchObject({ + code: "AddedRequiredProperty", + id: "1001", + message: "Added required property 'test'", + docUrl: "https://docs.example.com/rules/AddedRequiredProperty", + mode: "Addition", + new: { + location: "specification/test.json#L10", + path: "specification/test.json", + ref: "test-ref", + }, + old: { location: "specification/test.json#L8", path: "specification/test.json" }, + }); + }); + + it("should log entry and exit", () => { + const oadMessages: OadMessage[] = [createTestOadMessage()]; + + applyRules(oadMessages, "SameVersion", ApiVersionLifecycleStage.STABLE); + + expect(logMessage).toHaveBeenCalledWith("ENTER definition applyRules"); + expect(logMessage).toHaveBeenCalledWith("RETURN definition applyRules"); + }); + + it("should warn when rule has error severity but no label", () => { + // This test would require mocking the rule map differently, + // but the current implementation should handle this case + const oadMessages: OadMessage[] = [createTestOadMessage("TypeChanged", "1001")]; + + const result = applyRules(oadMessages, "SameVersion", ApiVersionLifecycleStage.STABLE); + + expect(result[0].type).toBe("Warning"); + expect(logWarning).toHaveBeenCalledWith( + expect.stringContaining("No rule found for scenario"), + ); + }); + }); +}); diff --git a/eng/tools/openapi-diff-runner/test/utils/common-utils.test.ts b/eng/tools/openapi-diff-runner/test/utils/common-utils.test.ts new file mode 100644 index 000000000000..acbdc7679789 --- /dev/null +++ b/eng/tools/openapi-diff-runner/test/utils/common-utils.test.ts @@ -0,0 +1,645 @@ +import { existsSync, readFileSync } from "node:fs"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { Context } from "../../src/types/breaking-change.js"; +import { + blobHref, + branchHref, + convertRawErrorToUnifiedMsg, + cutoffMsg, + getArgumentValue, + getGithubStyleFilePath, + getRelativeSwaggerPathToRepo, + getVersionFromInputFile, + processOadRuntimeErrorMessage, + sourceBranchHref, + specificBranchHref, + specIsPreview, + targetBranchHref, + targetHref, +} from "../../src/utils/common-utils.js"; + +// Mock node:fs module for file content parsing tests +vi.mock("node:fs", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + existsSync: vi.fn(), + readFileSync: vi.fn(), + }; +}); + +describe("common-utils", () => { + let originalEnv: NodeJS.ProcessEnv; + + // Common test constants + const TEST_CONSTANTS = { + REPO: "owner/repo", + COMMIT: "abc123", + BRANCH: "main", + FEATURE_BRANCH: "feature-branch", + FILE_PATH: "test-file.json", + SPEC_FILE: "specification/test.json", + SPEC_PATH: "/home/user/specification/test.json", + STORAGE_SPEC_PATH: + "/home/user/azure-rest-api-specs/specification/storage/resource-manager/test.json", + GITHUB_URL_BASE: "https://github.com/owner/repo/blob", + VERSION: "2021-01-01", + PREVIEW_VERSION: "2021-01-01-preview", + DATA_PLANE_PATH: + "specification/storage/data-plane/Microsoft.Storage/stable/2021-01-01/storage.json", + DATA_PLANE_PREVIEW_PATH: + "specification/storage/data-plane/Microsoft.Storage/preview/2021-01-01-preview/storage.json", + RESOURCE_MANAGER_PATH: + "specification/storage/resource-manager/Microsoft.Storage/2021-01-01/storage.json", + PREVIEW_SPEC_PATH: + "specification/maps/data-plane/Creator/preview/2022-09-01-preview/wayfind.json", + STABLE_SPEC_PATH: "specification/maps/data-plane/Creator/stable/2022-09-01/wayfind.json", + }; + + // Helper function to create file position + const createFilePosition = (line: number, column: number) => ({ line, column }); + + // Helper function to create expected GitHub URL + const createGithubUrl = ( + repo: string, + branch: string, + file: string, + position?: { line: number; column: number }, + ) => { + const baseUrl = `https://github.com/${repo}/blob/${branch}/${file}`; + return position ? `${baseUrl}#L${position.line}:${position.column}` : baseUrl; + }; + + // Helper function to setup GitHub environment + const setupGithubEnv = (repository = TEST_CONSTANTS.REPO, baseRef = TEST_CONSTANTS.BRANCH) => { + process.env.GITHUB_REPOSITORY = repository; + process.env.GITHUB_BASE_REF = baseRef; + vi.spyOn(console, "log").mockImplementation(() => {}); + }; + + // Helper function to setup GitHub Actions environment + const setupGithubActionsEnv = ( + repository = TEST_CONSTANTS.REPO, + commit = TEST_CONSTANTS.COMMIT, + ) => { + process.env.GITHUB_ACTIONS = "true"; + process.env.GITHUB_HEAD_REPOSITORY = repository; + process.env.GITHUB_SHA = commit; + }; + + // Mock context for tests that require it + const mockContext: Context = { + localSpecRepoPath: "/path/to/repo", + workingFolder: "/working", + logFileFolder: "/logs", + swaggerDirs: ["specification"], + baseBranch: "main", + headCommit: "abc123", + runType: "SameVersion" as any, + checkName: "test", + targetRepo: "owner/repo", + sourceRepo: "owner/repo", + prNumber: "123", + prSourceBranch: "feature", + prTargetBranch: "main", + oadMessageProcessorContext: { + logFilePath: "/log/path", + prUrl: "https://github.com/owner/repo/pull/123", + messageCache: [], + }, + prUrl: "https://github.com/owner/repo/pull/123", + }; + + beforeEach(() => { + originalEnv = { ...process.env }; + vi.clearAllMocks(); + }); + + afterEach(() => { + process.env = originalEnv; + }); + + describe("blobHref", () => { + it("should return GitHub blob URL", () => { + const result = blobHref(TEST_CONSTANTS.REPO, TEST_CONSTANTS.COMMIT, TEST_CONSTANTS.FILE_PATH); + const expected = createGithubUrl( + TEST_CONSTANTS.REPO, + TEST_CONSTANTS.COMMIT, + TEST_CONSTANTS.FILE_PATH, + ); + expect(result).toBe(expected); + }); + + it("should handle empty file path", () => { + const result = blobHref(TEST_CONSTANTS.REPO, TEST_CONSTANTS.COMMIT, ""); + const expected = createGithubUrl(TEST_CONSTANTS.REPO, TEST_CONSTANTS.COMMIT, ""); + expect(result).toBe(expected); + }); + }); + + describe("targetHref", () => { + beforeEach(() => { + setupGithubEnv(); + }); + + it("should return GitHub URL for valid file", () => { + const result = targetHref( + TEST_CONSTANTS.REPO, + TEST_CONSTANTS.BRANCH, + TEST_CONSTANTS.SPEC_FILE, + ); + const expected = createGithubUrl( + TEST_CONSTANTS.REPO, + TEST_CONSTANTS.BRANCH, + TEST_CONSTANTS.SPEC_FILE, + ); + expect(result).toBe(expected); + }); + + it("should return empty string for empty file", () => { + const result = targetHref(TEST_CONSTANTS.REPO, TEST_CONSTANTS.BRANCH, ""); + expect(result).toBe(""); + }); + }); + + describe("branchHref", () => { + beforeEach(() => { + setupGithubEnv(); + }); + + it("should return GitHub URL with specified branch", () => { + const result = branchHref( + TEST_CONSTANTS.REPO, + TEST_CONSTANTS.FILE_PATH, + TEST_CONSTANTS.FEATURE_BRANCH, + ); + const expected = createGithubUrl( + TEST_CONSTANTS.REPO, + TEST_CONSTANTS.FEATURE_BRANCH, + TEST_CONSTANTS.FILE_PATH, + ); + expect(result).toBe(expected); + }); + + it("should use main as default branch", () => { + const result = branchHref(TEST_CONSTANTS.REPO, TEST_CONSTANTS.FILE_PATH); + const expected = createGithubUrl( + TEST_CONSTANTS.REPO, + TEST_CONSTANTS.BRANCH, + TEST_CONSTANTS.FILE_PATH, + ); + expect(result).toBe(expected); + }); + + it("should return empty string for empty file", () => { + const result = branchHref(TEST_CONSTANTS.REPO, ""); + expect(result).toBe(""); + }); + }); + + describe("getGithubStyleFilePath", () => { + it("should format file path with line and column", () => { + const position = createFilePosition(42, 5); + const result = getGithubStyleFilePath("test.json", position); + expect(result).toBe("test.json#L42:5"); + }); + + it("should format file path with colon replacement", () => { + const result = getGithubStyleFilePath("test.json:42:5"); + expect(result).toBe("test.json#L42:5"); + }); + + it("should handle file path without FilePosition", () => { + const result = getGithubStyleFilePath("test.json"); + expect(result).toBe("test.json"); + }); + }); + + describe("getRelativeSwaggerPathToRepo", () => { + it("should extract path from specification directory", () => { + const result = getRelativeSwaggerPathToRepo(TEST_CONSTANTS.STORAGE_SPEC_PATH); + expect(result).toBe("specification/storage/resource-manager/test.json"); + }); + + it("should use BUILD_SOURCEDIRECTORY when pattern not found", () => { + process.env.BUILD_SOURCEDIRECTORY = "/home/user/azure-rest-api-specs/"; + const filePath = "/home/user/azure-rest-api-specs/other/test.json"; + const result = getRelativeSwaggerPathToRepo(filePath); + expect(result).toBe("other/test.json"); + }); + + it("should handle custom patterns", () => { + const filePath = "/home/user/azure-rest-api-specs/custom/api/test.json"; + const result = getRelativeSwaggerPathToRepo(filePath, ["custom"]); + expect(result).toBe("custom/api/test.json"); + }); + }); + + describe("getVersionFromInputFile", () => { + beforeEach(() => { + vi.mocked(existsSync).mockReset(); + vi.mocked(readFileSync).mockReset(); + }); + + it("should extract version from data-plane path", () => { + const result = getVersionFromInputFile(TEST_CONSTANTS.DATA_PLANE_PATH); + expect(result).toBe(TEST_CONSTANTS.VERSION); + }); + + it("should extract version with preview from data-plane path", () => { + const result = getVersionFromInputFile(TEST_CONSTANTS.DATA_PLANE_PREVIEW_PATH, true); + expect(result).toBe(TEST_CONSTANTS.PREVIEW_VERSION); + }); + + it("should extract version from resource-manager path", () => { + const result = getVersionFromInputFile(TEST_CONSTANTS.RESOURCE_MANAGER_PATH); + expect(result).toBe(TEST_CONSTANTS.VERSION); + }); + + it("should return empty string when no version found in path", () => { + const result = getVersionFromInputFile("test.json"); + expect(result).toBe(""); + }); + + it("should return empty string when no valid API version found", () => { + const result = getVersionFromInputFile("invalid/path.json"); + expect(result).toBe(""); + }); + + it("should extract version from file content when path regex fails and file exists", () => { + const filePath = "some/custom/path/spec.json"; + const mockFileContent = JSON.stringify({ + info: { + version: "2023-05-01", + }, + }); + + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(readFileSync).mockReturnValue(mockFileContent); + + const result = getVersionFromInputFile(filePath); + expect(result).toBe("2023-05-01"); + expect(vi.mocked(existsSync)).toHaveBeenCalledWith(filePath); + expect(vi.mocked(readFileSync)).toHaveBeenCalledWith(filePath, "utf8"); + }); + + it("should extract preview version from file content when includePreview is true", () => { + const filePath = "some/custom/path/spec.json"; + const mockFileContent = JSON.stringify({ + info: { + version: "2023-05-01-preview", + }, + }); + + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(readFileSync).mockReturnValue(mockFileContent); + + const result = getVersionFromInputFile(filePath, true); + expect(result).toBe("2023-05-01-preview"); + expect(vi.mocked(existsSync)).toHaveBeenCalledWith(filePath); + expect(vi.mocked(readFileSync)).toHaveBeenCalledWith(filePath, "utf8"); + }); + + it("should return empty string when file content has no version", () => { + const filePath = "some/custom/path/spec.json"; + const mockFileContent = JSON.stringify({ + info: { + title: "Test API", + }, + }); + + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(readFileSync).mockReturnValue(mockFileContent); + + const result = getVersionFromInputFile(filePath); + expect(result).toBe(""); + expect(vi.mocked(existsSync)).toHaveBeenCalledWith(filePath); + expect(vi.mocked(readFileSync)).toHaveBeenCalledWith(filePath, "utf8"); + }); + + it("should throw error when file content is invalid JSON", () => { + const filePath = "some/custom/path/spec.json"; + const mockFileContent = "invalid json content"; + + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(readFileSync).mockReturnValue(mockFileContent); + + expect(() => getVersionFromInputFile(filePath)).toThrow(); + expect(vi.mocked(existsSync)).toHaveBeenCalledWith(filePath); + expect(vi.mocked(readFileSync)).toHaveBeenCalledWith(filePath, "utf8"); + }); + + it("should return empty string when file does not exist and path regex fails", () => { + const filePath = "non/existent/path/spec.json"; + + vi.mocked(existsSync).mockReturnValue(false); + + const result = getVersionFromInputFile(filePath); + expect(result).toBe(""); + expect(vi.mocked(existsSync)).toHaveBeenCalledWith(filePath); + expect(vi.mocked(readFileSync)).not.toHaveBeenCalled(); + }); + + it("should throw error when file read fails", () => { + const filePath = "some/custom/path/spec.json"; + + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(readFileSync).mockImplementation(() => { + throw new Error("File read error"); + }); + + expect(() => getVersionFromInputFile(filePath)).toThrow("File read error"); + expect(vi.mocked(existsSync)).toHaveBeenCalledWith(filePath); + expect(vi.mocked(readFileSync)).toHaveBeenCalledWith(filePath, "utf8"); + }); + }); + + describe("getArgumentValue", () => { + it("should return argument value when flag exists", () => { + const args = ["--input", "test.json", "--output", "result.json"]; + const result = getArgumentValue(args, "--input", "default.json"); + expect(result).toBe("test.json"); + }); + + it("should return default value when flag not found", () => { + const args = ["--output", "result.json"]; + const result = getArgumentValue(args, "--input", "default.json"); + expect(result).toBe("default.json"); + }); + + it("should return default value when flag is last argument", () => { + const args = ["--output", "result.json", "--input"]; + const result = getArgumentValue(args, "--input", "default.json"); + expect(result).toBe("default.json"); + }); + }); + + describe("cutoffMsg", () => { + it("should return original message when within size limit", () => { + const msg = "short message"; + const result = cutoffMsg(msg, 1024); + expect(result).toBe("short message"); + }); + + it("should truncate message when exceeding size limit", () => { + const msg = "a".repeat(2000); + const result = cutoffMsg(msg, 1024); + expect(result).toBe("a".repeat(1024)); + }); + + it("should return empty string for undefined message", () => { + const result = cutoffMsg(undefined); + expect(result).toBe(""); + }); + + it("should use default size of 1024", () => { + const msg = "a".repeat(2000); + const result = cutoffMsg(msg); + expect(result).toBe("a".repeat(1024)); + }); + }); + + describe("processOadRuntimeErrorMessage", () => { + it("should process AutoRest runtime error", () => { + const message = + 'Command failed: node "/path/to/autorest/dist/app.js" --v2\\nERROR: Schema violation\\nFATAL: Error occurred'; + const result = processOadRuntimeErrorMessage(message, 500); + + expect(result).toContain("Breaking change detector (OAD) invoked AutoRest"); + expect(result).toContain("1: Command failed:"); + expect(result).toContain("ERROR: Schema violation"); + expect(result).toContain("
    "); + }); + + it("should use cutoffMsg for non-AutoRest errors", () => { + const message = "Some other error message"; + const result = processOadRuntimeErrorMessage(message, 500); + expect(result).toBe("Some other error message"); + }); + + it("should handle empty lines in AutoRest error", () => { + const message = + 'Command failed: node "/path/to/autorest/dist/app.js"\n\nERROR: Test\n\nFATAL: Error'; + const result = processOadRuntimeErrorMessage(message, 500); + + expect(result).toContain("1: Command failed:"); + expect(result).toContain("ERROR: Test"); + expect(result).toContain("FATAL: Error"); + }); + + it("should respect stack trace max length", () => { + const lines = Array.from({ length: 10 }, (_, i) => `Line ${i + 1}`); + const message = `Command failed: node "/path/to/autorest/dist/app.js"\n${lines.join("\n")}`; + const result = processOadRuntimeErrorMessage(message, 3); + + expect(result).toContain("1: Command failed:"); + expect(result).toContain("2: Line 1"); + expect(result).toContain("3: Line 2"); + // Should not contain the 4th line since max length is 3 + expect(result).not.toContain("4:"); + }); + }); + + describe("specIsPreview", () => { + it("should return true for preview spec paths", () => { + const result = specIsPreview(TEST_CONSTANTS.PREVIEW_SPEC_PATH); + expect(result).toBe(true); + }); + + it("should return false for stable spec paths", () => { + const result = specIsPreview(TEST_CONSTANTS.STABLE_SPEC_PATH); + expect(result).toBe(false); + }); + + it("should return false when both preview and stable are in path", () => { + const specPath = + "specification/maps/data-plane/Creator/stable/2022-09-01/preview-example.json"; + const result = specIsPreview(specPath); + expect(result).toBe(false); + }); + + it("should return false for paths without preview", () => { + const specPath = "specification/maps/data-plane/Creator/2022-09-01/wayfind.json"; + const result = specIsPreview(specPath); + expect(result).toBe(false); + }); + }); + + describe("sourceBranchHref", () => { + beforeEach(() => { + setupGithubActionsEnv(); + }); + + it("should return source branch href with file position", () => { + const position = createFilePosition(10, 5); + const result = sourceBranchHref( + TEST_CONSTANTS.REPO, + TEST_CONSTANTS.COMMIT, + TEST_CONSTANTS.SPEC_PATH, + position, + ); + const expected = createGithubUrl( + TEST_CONSTANTS.REPO, + TEST_CONSTANTS.COMMIT, + TEST_CONSTANTS.SPEC_FILE, + position, + ); + expect(result).toContain(expected); + }); + + it("should return source branch href without file position", () => { + const result = sourceBranchHref( + TEST_CONSTANTS.REPO, + TEST_CONSTANTS.COMMIT, + TEST_CONSTANTS.SPEC_PATH, + ); + const expected = createGithubUrl( + TEST_CONSTANTS.REPO, + TEST_CONSTANTS.COMMIT, + TEST_CONSTANTS.SPEC_FILE, + ); + expect(result).toContain(expected); + }); + }); + + describe("targetBranchHref", () => { + beforeEach(() => { + setupGithubEnv(); + }); + + it("should return target branch href with file position", () => { + const position = createFilePosition(20, 3); + const result = targetBranchHref( + TEST_CONSTANTS.REPO, + TEST_CONSTANTS.BRANCH, + TEST_CONSTANTS.SPEC_PATH, + position, + ); + const expected = createGithubUrl( + TEST_CONSTANTS.REPO, + TEST_CONSTANTS.BRANCH, + TEST_CONSTANTS.SPEC_FILE, + position, + ); + expect(result).toBe(expected); + }); + + it("should return target branch href without file position", () => { + const result = targetBranchHref( + TEST_CONSTANTS.REPO, + TEST_CONSTANTS.BRANCH, + TEST_CONSTANTS.SPEC_PATH, + ); + const expected = createGithubUrl( + TEST_CONSTANTS.REPO, + TEST_CONSTANTS.BRANCH, + TEST_CONSTANTS.SPEC_FILE, + ); + expect(result).toBe(expected); + }); + }); + + describe("specificBranchHref", () => { + beforeEach(() => { + setupGithubEnv(); + }); + + it("should return specific branch href with file position", () => { + const position = createFilePosition(15, 2); + const result = specificBranchHref( + TEST_CONSTANTS.REPO, + TEST_CONSTANTS.SPEC_PATH, + TEST_CONSTANTS.FEATURE_BRANCH, + position, + ); + const expected = createGithubUrl( + TEST_CONSTANTS.REPO, + TEST_CONSTANTS.FEATURE_BRANCH, + TEST_CONSTANTS.SPEC_FILE, + position, + ); + expect(result).toBe(expected); + }); + + it("should return specific branch href without file position", () => { + const result = specificBranchHref( + TEST_CONSTANTS.REPO, + TEST_CONSTANTS.SPEC_PATH, + TEST_CONSTANTS.FEATURE_BRANCH, + ); + const expected = createGithubUrl( + TEST_CONSTANTS.REPO, + TEST_CONSTANTS.FEATURE_BRANCH, + TEST_CONSTANTS.SPEC_FILE, + ); + expect(result).toBe(expected); + }); + }); + + describe("convertRawErrorToUnifiedMsg", () => { + it("should create unified error message with default level", () => { + const result = convertRawErrorToUnifiedMsg( + mockContext, + "TestError", + "This is a test error message", + ); + const parsed = JSON.parse(result); + + expect(parsed.type).toBe("Raw"); + expect(parsed.level).toBe("Error"); + expect(parsed.message).toBe("TestError"); + expect(parsed.extra.details).toBe("This is a test error message"); + expect(parsed.time).toBeDefined(); + expect(parsed.extra.location).toBeUndefined(); + }); + + it("should create unified error message with custom level", () => { + const result = convertRawErrorToUnifiedMsg( + mockContext, + "TestWarning", + "This is a warning", + "Warning", + ); + const parsed = JSON.parse(result); + + expect(parsed.type).toBe("Raw"); + expect(parsed.level).toBe("Warning"); + expect(parsed.message).toBe("TestWarning"); + expect(parsed.extra.details).toBe("This is a warning"); + }); + + it("should create unified error message with location", () => { + const result = convertRawErrorToUnifiedMsg( + mockContext, + "TestError", + "Error with location", + "Error", + "specification/test/test.json", + ); + const parsed = JSON.parse(result); + + expect(parsed.type).toBe("Raw"); + expect(parsed.level).toBe("Error"); + expect(parsed.message).toBe("TestError"); + expect(parsed.extra.details).toBe("Error with location"); + expect(parsed.extra.location).toBe( + "https://github.com/owner/repo/blob/main/specification/test/test.json", + ); + }); + + it("should handle empty error message", () => { + const result = convertRawErrorToUnifiedMsg(mockContext, "EmptyError", ""); + const parsed = JSON.parse(result); + + expect(parsed.extra.details).toBe(""); + }); + + it("should handle special characters in error message", () => { + const errorMsg = 'Error with "quotes" and \n newlines \t tabs'; + const result = convertRawErrorToUnifiedMsg(mockContext, "SpecialCharsError", errorMsg); + const parsed = JSON.parse(result); + + expect(parsed.extra.details).toBe(errorMsg); + }); + }); +}); diff --git a/eng/tools/openapi-diff-runner/test/utils/markdown-report-row.test.ts b/eng/tools/openapi-diff-runner/test/utils/markdown-report-row.test.ts new file mode 100644 index 000000000000..b86de5356579 --- /dev/null +++ b/eng/tools/openapi-diff-runner/test/utils/markdown-report-row.test.ts @@ -0,0 +1,185 @@ +import { describe, expect, it } from "vitest"; +import { BrChMsgRecord, ResultMessageRecord } from "../../src/types/message.js"; +import { + BreakingChangeMdRow, + createBreakingChangeMdRows, + getDeficitRow, + getMdTableHeader, + rowToString, +} from "../../src/utils/markdown-report-row.js"; + +describe("markdown-report-row", () => { + // Test constants + const DATE = new Date("2023-01-01"); + const IDS = { + test: "test-id", + test1: "test-id-1", + test2: "test-id-2", + }; + const MESSAGES = { + error: "Test error message", + rawError: "Raw error message", + aMessage: "A message", + zMessage: "Z message", + removedProperty: "Property 'test' was removed", + withSpecialChars: "Test message\nwith newlines\tand tabs\r", + testMessage: "Test message", + }; + const PATHS = { + newSimple: "https://github.com/owner/repo/blob/main/specification/test.json#L10:5", + oldSimple: "https://github.com/owner/repo/blob/old/specification/test.json#L8:3", + newStorage: + "https://github.com/owner/repo/blob/main/specification/storage/resource-manager/Microsoft.Storage/stable/2021-01-01/storage.json#L100:5", + oldStorage: + "https://github.com/owner/repo/blob/old/specification/storage/resource-manager/Microsoft.Storage/stable/2021-01-01/storage.json#L95:3", + }; + const JSON_PATHS = { + simple: "$.paths./test.get", + simpleOld: "$.paths./test.get.old", + storageProperty: "$.definitions.StorageAccount.properties.test", + storagePropertyOld: "$.definitions.StorageAccount.properties.test.old", + }; + + // Helper functions + const createPath = (tag: string, path: string, jsonPath: string) => ({ tag, path, jsonPath }); + + const createResultMessage = ( + message: string, + paths: Array<{ tag: string; path: string; jsonPath: string }> = [], + id = IDS.test, + ): ResultMessageRecord => + ({ + type: "Result", + id, + level: "Error", + message, + time: DATE, + paths, + }) as ResultMessageRecord; + + const createRawMessage = (message: string, extra: Record = {}): BrChMsgRecord => ({ + type: "Raw", + level: "Error", + message, + time: DATE, + groupName: "test-group", + extra, + }); + + const createRow = ( + index: number, + description: string, + msg: BrChMsgRecord, + ): BreakingChangeMdRow => ({ + index, + description, + msg, + }); + + const expectRowStructure = (result: BreakingChangeMdRow[], expectedLength: number) => { + expect(result).toHaveLength(expectedLength); + if (expectedLength > 0) { + expect(result[0]).toMatchObject({ + index: 1, + msg: expect.any(Object), + description: expect.any(String), + }); + } + }; + + const createSimplePaths = () => [ + createPath("New", PATHS.newSimple, JSON_PATHS.simple), + createPath("Old", PATHS.oldSimple, JSON_PATHS.simpleOld), + ]; + + const createStoragePaths = () => [ + createPath("New", PATHS.newStorage, JSON_PATHS.storageProperty), + createPath("Old", PATHS.oldStorage, JSON_PATHS.storagePropertyOld), + ]; + describe("createBreakingChangeMdRows", () => { + it("should create rows from Result messages", () => { + const paths = createSimplePaths(); + const msgs = [createResultMessage(MESSAGES.error, paths)]; + + const result = createBreakingChangeMdRows(msgs); + + expectRowStructure(result, 1); + expect(result[0].msg).toBe(msgs[0]); + expect(result[0].description).toContain(MESSAGES.error); + }); + + it("should create rows from Raw messages", () => { + const extra = { details: "Additional details", code: "ERROR_CODE" }; + const msgs = [createRawMessage(MESSAGES.rawError, extra)]; + + const result = createBreakingChangeMdRows(msgs); + + expectRowStructure(result, 1); + expect(result[0].description).toContain('"details":"Additional details"'); + expect(result[0].description).toContain('"code":"ERROR_CODE"'); + }); + + it("should sort rows by description", () => { + const msgs = [ + createResultMessage(MESSAGES.zMessage, [], IDS.test2), + createResultMessage(MESSAGES.aMessage, [], IDS.test1), + ]; + + const result = createBreakingChangeMdRows(msgs); + + expect(result[0].msg.message).toBe(MESSAGES.zMessage); + expect(result[1].msg.message).toBe(MESSAGES.aMessage); + }); + }); + + describe("getMdTableHeader", () => { + it("should return markdown table header", () => { + const result = getMdTableHeader(); + expect(result).toBe("| Index | Description |\n|-|-|\n"); + }); + }); + + describe("getDeficitRow", () => { + it("should return singular deficit row", () => { + const result = getDeficitRow(1); + expect(result).toBe("|| ⚠️ 1 occurrence omitted. See the build log.|\n"); + }); + + it("should return plural deficit row", () => { + const result = getDeficitRow(5); + expect(result).toBe("|| ⚠️ 5 occurrences omitted. See the build log.|\n"); + }); + }); + + describe("rowToString", () => { + it("should convert row to markdown string", () => { + const msg = createResultMessage(MESSAGES.testMessage); + const row = createRow(1, "Test description with
    tags", msg); + + const result = rowToString(row); + expect(result).toBe("| 1 | Test description with
    tags |\n"); + }); + }); + + describe("integration with complex messages", () => { + it("should handle Result message with multiple paths", () => { + const paths = createStoragePaths(); + const msgs = [createResultMessage(MESSAGES.removedProperty, paths)]; + + const result = createBreakingChangeMdRows(msgs); + const description = result[0].description; + + expect(description).toContain("Property 'test' was removed"); + expect(description).toContain("New: [Microsoft.Storage/stable/2021-01-01/storage.json"); + expect(description).toContain("Old: [Microsoft.Storage/stable/2021-01-01/storage.json"); + expect(description).toContain("$.definitions.StorageAccount.properties.test"); + }); + + it("should handle message with newlines and tabs", () => { + const msgs = [createResultMessage(MESSAGES.withSpecialChars)]; + + const result = createBreakingChangeMdRows(msgs); + expect(result[0].description).toBe("Test message with newlines and tabs
    "); + }); + }); +}); diff --git a/eng/tools/openapi-diff-runner/test/utils/markdown-report.test.ts b/eng/tools/openapi-diff-runner/test/utils/markdown-report.test.ts new file mode 100644 index 000000000000..acad8c8735e5 --- /dev/null +++ b/eng/tools/openapi-diff-runner/test/utils/markdown-report.test.ts @@ -0,0 +1,319 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { LogLevel, logMessage } from "../../src/log.js"; +import { BrChMsgRecord, ResultMessageRecord } from "../../src/types/message.js"; +import { + BreakingChangeMdReport, + createBreakingChangeMdReport, + getReportLength, + getRowCount, + reportToString, + sortBreakingChangeMdReports, +} from "../../src/utils/markdown-report.js"; + +// Mock the log module +vi.mock("../../src/log.js", () => ({ + logMessage: vi.fn(), + LogLevel: { + Info: "Info", + Warning: "Warning", + Error: "Error", + }, +})); + +describe("markdown-report", () => { + // Test constants + const DATE = new Date("2023-01-01"); + const IDS = { + r001: "R001", + r002: "R002", + }; + const MESSAGES = { + testError: "Test error message", + runtimeWarning: "Runtime warning message", + testMessage: "Test message", + rawMessage: "Raw message", + resultMessage: "Result message", + infoMessage: "Info message", + errorMessage: "Error message", + messageA: "Message A", + messageB: "Message B", + }; + const URLS = { + docs: "https://docs.example.com", + docsWithRules: "https://docs.example.com/rules/R001", + github: "https://github.com/owner/repo/blob/main/specification/test.json", + }; + const CODES = { + testError: "TestError", + testInfo: "TestInfo", + }; + const LEVELS = { + error: "Error" as const, + warning: "Warning" as const, + info: "Info" as const, + }; + + // Helper functions for creating messages + const createResultMessage = (overrides: Partial = {}): ResultMessageRecord => + ({ + type: "Result", + id: IDS.r001, + level: LEVELS.error, + message: MESSAGES.testError, + time: DATE, + docUrl: URLS.docsWithRules, + code: CODES.testError, + paths: [ + { + tag: "New", + path: URLS.github, + jsonPath: "$.paths./test", + }, + ], + ...overrides, + }) as ResultMessageRecord; + + const createRawMessage = (overrides: Partial = {}) => + ({ + type: "Raw" as const, + level: LEVELS.warning, + message: MESSAGES.runtimeWarning, + time: DATE, + groupName: "test-group", + extra: { details: "Additional info" }, + ...overrides, + }) as BrChMsgRecord; + + const createReport = ( + overrides: Partial = {}, + ): BreakingChangeMdReport => ({ + msgs: [createResultMessage()], + rows: ["| 1 | Test message |\\n"], + type: "Result", + level: LEVELS.error, + id: IDS.r001, + rawMessage: "", + ...overrides, + }); + + const createMultipleResultMessages = (count: number): ResultMessageRecord[] => + Array.from({ length: count }, (_, i) => + createResultMessage({ + id: `R00${i + 1}`, + message: `Test message ${i + 1}`, + }), + ); + + const expectReportStructure = ( + result: BreakingChangeMdReport, + expected: Partial, + ) => { + expect(result).toMatchObject(expected); + expect(result.rows).toHaveLength(expected.rows?.length || 1); + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("createBreakingChangeMdReport", () => { + it("should create report from Result messages", () => { + const msgs = [createResultMessage()]; + + const result = createBreakingChangeMdReport(msgs); + + expectReportStructure(result, { + msgs, + type: "Result", + level: LEVELS.error, + id: IDS.r001, + rawMessage: "", + }); + }); + + it("should create report from Raw messages", () => { + const msgs = [createRawMessage()]; + + const result = createBreakingChangeMdReport(msgs); + + expectReportStructure(result, { + msgs, + type: "Raw", + level: LEVELS.warning, + id: undefined, + rawMessage: MESSAGES.runtimeWarning, + }); + }); + + it("should validate empty message array", () => { + const msgs: BrChMsgRecord[] = []; + + expect(() => createBreakingChangeMdReport(msgs)).toThrow(); + }); + + it("should warn about mixed message types", () => { + const msgs = [ + createResultMessage({ message: MESSAGES.testMessage, paths: [] }), + createRawMessage({ level: LEVELS.error, message: MESSAGES.rawMessage }), + ]; + + createBreakingChangeMdReport(msgs); + expect(logMessage).toHaveBeenCalledWith( + expect.stringContaining("Not all messages have the same type"), + LogLevel.Warn, + ); + }); + }); + + describe("sortBreakingChangeMdReports", () => { + it("should sort by type (Raw before Result)", () => { + const resultReport = createReport({ + msgs: [createResultMessage({ message: MESSAGES.resultMessage, paths: [] })], + rows: ["| 1 | Result message |\\n"], + type: "Result", + level: LEVELS.error, + id: IDS.r001, + rawMessage: "", + }); + + const rawReport = createReport({ + msgs: [createRawMessage({ level: LEVELS.error, message: MESSAGES.rawMessage })], + rows: ["| 1 | Raw message |\\n"], + type: "Raw", + level: LEVELS.error, + rawMessage: MESSAGES.rawMessage, + id: undefined, + }); + + const result = sortBreakingChangeMdReports([resultReport, rawReport]); + expect(result[0].type).toBe("Raw"); + expect(result[1].type).toBe("Result"); + }); + + it("should sort by level (Error before Warning before Info)", () => { + const infoReport = createReport({ + msgs: [ + createResultMessage({ + id: IDS.r001, + level: LEVELS.info, + message: MESSAGES.infoMessage, + code: CODES.testInfo, + paths: [], + }), + ], + rows: ["| 1 | Info message |\\n"], + type: "Result", + level: LEVELS.info, + id: IDS.r001, + rawMessage: "", + }); + + const errorReport = createReport({ + msgs: [ + createResultMessage({ + id: IDS.r002, + level: LEVELS.error, + message: MESSAGES.errorMessage, + paths: [], + }), + ], + rows: ["| 1 | Error message |\\n"], + type: "Result", + level: LEVELS.error, + id: IDS.r002, + rawMessage: "", + }); + + const result = sortBreakingChangeMdReports([infoReport, errorReport]); + expect(result[0].level).toBe("Error"); + expect(result[1].level).toBe("Info"); + }); + + it("should sort by ID when type and level are same", () => { + const reportB = createReport({ + msgs: [ + createResultMessage({ + id: IDS.r002, + message: MESSAGES.messageB, + paths: [], + }), + ], + rows: ["| 1 | Message B |\\n"], + type: "Result", + level: LEVELS.error, + id: IDS.r002, + rawMessage: "", + }); + + const reportA = createReport({ + msgs: [ + createResultMessage({ + id: IDS.r001, + message: MESSAGES.messageA, + paths: [], + }), + ], + rows: ["| 1 | Message A |\\n"], + type: "Result", + level: LEVELS.error, + id: IDS.r001, + rawMessage: "", + }); + + const result = sortBreakingChangeMdReports([reportB, reportA]); + expect(result[0].id).toBe("R001"); + expect(result[1].id).toBe("R002"); + }); + }); + + describe("reportToString", () => { + it("should convert report to markdown string", () => { + const report = createReport({ + msgs: [createResultMessage({ message: MESSAGES.testMessage, paths: [] })], + rows: ["| 1 | Test message |\\n"], + }); + + const result = reportToString(report, 10); + + expect(result).toContain("## "); + expect(result).toContain("[R001 - TestError](https://docs.example.com/rules/R001)"); + expect(result).toContain("Displaying 1 out of 1 occurrences"); + expect(result).toContain("| Index | Description |"); + expect(result).toContain("| 1 | Test message |"); + }); + + it("should include deficit row when maxRowCount is exceeded", () => { + const msgs = createMultipleResultMessages(5); + const report = createBreakingChangeMdReport(msgs); + const result = reportToString(report, 3); + + expect(result).toContain("Displaying 3 out of 5 occurrences"); + expect(result).toContain("⚠️ To view the remaining 2 occurrences"); + expect(result).toContain("⚠️ 2 occurrences omitted"); + }); + }); + + describe("getReportLength", () => { + it("should return correct length of report string", () => { + const report = createReport({ + msgs: [createResultMessage({ message: MESSAGES.testMessage, paths: [] })], + rows: ["| 1 | Test message |\\n"], + }); + + const expectedString = reportToString(report, 10); + const result = getReportLength(report, 10); + + expect(result).toBe(expectedString.length); + }); + }); + + describe("getRowCount", () => { + it("should return correct number of rows", () => { + const msgs = createMultipleResultMessages(3); + const report = createBreakingChangeMdReport(msgs); + const result = getRowCount(report); + + expect(result).toBe(3); + }); + }); +}); diff --git a/eng/tools/openapi-diff-runner/test/utils/oad-message-processor.test.ts b/eng/tools/openapi-diff-runner/test/utils/oad-message-processor.test.ts new file mode 100644 index 000000000000..28006758bac0 --- /dev/null +++ b/eng/tools/openapi-diff-runner/test/utils/oad-message-processor.test.ts @@ -0,0 +1,544 @@ +import fs from "node:fs"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { logMessage, logMessageSafe } from "../../src/log.js"; +import { ApiVersionLifecycleStage, Context } from "../../src/types/breaking-change.js"; +import { MessageLevel } from "../../src/types/message.js"; +import { OadMessage } from "../../src/types/oad-types.js"; +import { + appendMarkdownToLog, + appendToLogFile, + clearMessageCache, + convertOadMessagesToResultMessageRecords, + createMessageKey, + createOadMessageProcessor, + getMessageCacheSize, + OadMessageProcessorContext, + processAndAppendOadMessages, +} from "../../src/utils/oad-message-processor.js"; + +// Test constants +const TEST_CONSTANTS = { + REPO: "owner/repo", + COMMIT: "abc123", + LOG_PATH: "/path/to/log.txt", + PR_URL: "https://github.com/owner/repo/pull/123", + PR_NUMBER: "123", + BRANCH: { + MAIN: "main", + FEATURE: "feature", + TARGET: "main", + CUSTOM: "feature-branch", + DEVELOP: "develop", + }, + PATHS: { + NEW: "specification/test/new.json", + OLD: "specification/test/old.json", + DIFFERENT: "specification/test/different.json", + WORKING: "/working", + LOGS: "/logs", + REPO: "/path/to/repo", + CUSTOM: "/custom/path", + }, + MESSAGES: { + TEST: "Test error message", + WARNING: "Test markdown warning", + ERROR: "Test markdown error", + }, + RULES: { + REMOVED_PROPERTY: "RemovedProperty", + ADDED_PROPERTY: "AddedPropertyInResponse", + }, + JSON_PATHS: { + TEST_MODEL: "$.definitions.TestModel", + DIFFERENT_MODEL: "$.definitions.DifferentModel", + PATH: "$.path", + PATH2: "$.path2", + }, + LOCATIONS: { + NEW: "new.json", + OLD: "old.json", + NEW2: "new2.json", + OLD2: "old2.json", + }, +} as const; + +// Helper functions +function createMockOadMessage(overrides: Partial = {}): OadMessage { + return { + type: "Error", + code: TEST_CONSTANTS.RULES.REMOVED_PROPERTY, + message: TEST_CONSTANTS.MESSAGES.TEST, + id: "test-id", + docUrl: `https://docs.example.com/rules/${TEST_CONSTANTS.RULES.REMOVED_PROPERTY}`, + mode: "test", + groupName: ApiVersionLifecycleStage.STABLE, + new: { + location: TEST_CONSTANTS.PATHS.NEW, + path: TEST_CONSTANTS.JSON_PATHS.TEST_MODEL, + }, + old: { + location: TEST_CONSTANTS.PATHS.OLD, + path: TEST_CONSTANTS.JSON_PATHS.TEST_MODEL, + }, + ...overrides, + }; +} + +function createMockContext(): Context { + return { + sourceRepo: TEST_CONSTANTS.REPO, + headCommit: TEST_CONSTANTS.COMMIT, + targetRepo: TEST_CONSTANTS.REPO, + localSpecRepoPath: TEST_CONSTANTS.PATHS.REPO, + workingFolder: TEST_CONSTANTS.PATHS.WORKING, + logFileFolder: TEST_CONSTANTS.PATHS.LOGS, + swaggerDirs: ["specification"], + baseBranch: TEST_CONSTANTS.BRANCH.MAIN, + runType: "SameVersion", + checkName: "test", + prNumber: TEST_CONSTANTS.PR_NUMBER, + prSourceBranch: TEST_CONSTANTS.BRANCH.FEATURE, + prTargetBranch: TEST_CONSTANTS.BRANCH.TARGET, + oadMessageProcessorContext: { + logFilePath: TEST_CONSTANTS.LOG_PATH, + prUrl: TEST_CONSTANTS.PR_URL, + messageCache: [], + }, + prUrl: TEST_CONSTANTS.PR_URL, + } as Context; +} + +function createProcessorContext( + logFilePath: string = TEST_CONSTANTS.LOG_PATH, + prUrl: string = TEST_CONSTANTS.PR_URL, + messageCache: OadMessage[] = [], +): OadMessageProcessorContext { + return { + logFilePath, + prUrl, + messageCache, + }; +} + +// Mock dependencies +vi.mock("node:fs"); +vi.mock("../../src/log.js"); +vi.mock("../../src/utils/common-utils.js", () => ({ + sourceBranchHref: vi.fn( + (repo: string, sha: string, file: string) => `https://github.com/${repo}/blob/${sha}/${file}`, + ), + specificBranchHref: vi.fn( + (repo: string, file: string, branchName: string) => + `https://github.com/${repo}/blob/${branchName}/${file}`, + ), +})); +vi.mock("../../src/types/breaking-change.js", async (importOriginal) => { + const actual = (await importOriginal()) as any; + return { + ...actual, + logFileName: "breaking-change.log", + }; +}); + +describe("oad-message-processor", () => { + const mockAppendFileSync = vi.mocked(fs.appendFileSync); + const mockLogMessage = vi.mocked(logMessage); + const mockLogMessageSafe = vi.mocked(logMessageSafe); + const mockContext = createMockContext(); + + // Helper functions with access to mocks + function expectAppendFileSync(callIndex: number, filePath: string, content: string) { + expect(mockAppendFileSync).toHaveBeenNthCalledWith(callIndex, filePath, content); + } + + function expectLogMessage(message: string) { + expect(mockLogMessage).toHaveBeenCalledWith(expect.stringContaining(message)); + } + function expectLogMessageSafe(message: string) { + expect(mockLogMessageSafe).toHaveBeenCalledWith(expect.stringContaining(message)); + } + + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + describe("convertOadMessagesToResultMessageRecords", () => { + it("should convert OAD messages to result message records", () => { + const oadMessages: OadMessage[] = [createMockOadMessage()]; + + const result = convertOadMessagesToResultMessageRecords( + mockContext, + oadMessages, + TEST_CONSTANTS.BRANCH.MAIN, + ); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + type: "Result", + level: "Error", + message: TEST_CONSTANTS.MESSAGES.TEST, + code: TEST_CONSTANTS.RULES.REMOVED_PROPERTY, + id: "test-id", + docUrl: `https://docs.example.com/rules/${TEST_CONSTANTS.RULES.REMOVED_PROPERTY}`, + time: expect.any(Date), + groupName: ApiVersionLifecycleStage.STABLE, + extra: { + mode: "test", + }, + paths: [ + { + tag: "New", + path: `https://github.com/${TEST_CONSTANTS.REPO}/blob/${TEST_CONSTANTS.COMMIT}/${TEST_CONSTANTS.PATHS.NEW}`, + jsonPath: TEST_CONSTANTS.JSON_PATHS.TEST_MODEL, + }, + { + tag: "Old", + path: `https://github.com/${TEST_CONSTANTS.REPO}/blob/${TEST_CONSTANTS.BRANCH.MAIN}/${TEST_CONSTANTS.PATHS.OLD}`, + jsonPath: TEST_CONSTANTS.JSON_PATHS.TEST_MODEL, + }, + ], + }); + }); + + it("should handle custom base branch name", () => { + const oadMessages: OadMessage[] = [createMockOadMessage()]; + + const result = convertOadMessagesToResultMessageRecords( + mockContext, + oadMessages, + TEST_CONSTANTS.BRANCH.CUSTOM, + ); + + expect(result[0].paths[1].path).toBe( + `https://github.com/${TEST_CONSTANTS.REPO}/blob/${TEST_CONSTANTS.BRANCH.CUSTOM}/${TEST_CONSTANTS.PATHS.OLD}`, + ); + }); + + it("should handle messages with missing new location", () => { + const oadMessages: OadMessage[] = [ + createMockOadMessage({ + new: { location: "", path: "" }, + }), + ]; + + const result = convertOadMessagesToResultMessageRecords( + mockContext, + oadMessages, + TEST_CONSTANTS.BRANCH.MAIN, + ); + + expect(result[0].paths).toHaveLength(1); + expect(result[0].paths[0].tag).toBe("Old"); + }); + + it("should handle messages with missing old location", () => { + const oadMessages: OadMessage[] = [ + createMockOadMessage({ + old: { location: "", path: "" }, + }), + ]; + + const result = convertOadMessagesToResultMessageRecords( + mockContext, + oadMessages, + TEST_CONSTANTS.BRANCH.MAIN, + ); + + expect(result[0].paths).toHaveLength(1); + expect(result[0].paths[0].tag).toBe("New"); + }); + + it("should handle messages with no locations", () => { + const oadMessages: OadMessage[] = [ + createMockOadMessage({ + new: { location: "", path: "" }, + old: { location: "", path: "" }, + }), + ]; + + const result = convertOadMessagesToResultMessageRecords( + mockContext, + oadMessages, + TEST_CONSTANTS.BRANCH.MAIN, + ); + + expect(result[0].paths).toHaveLength(0); + }); + + it("should handle different message levels", () => { + const levels: MessageLevel[] = ["Error", "Warning", "Info"]; + const oadMessages: OadMessage[] = levels.map((level) => + createMockOadMessage({ type: level }), + ); + + const result = convertOadMessagesToResultMessageRecords( + mockContext, + oadMessages, + TEST_CONSTANTS.BRANCH.MAIN, + ); + + expect(result).toHaveLength(3); + result.forEach((msg, index) => { + expect(msg.level).toBe(levels[index]); + }); + }); + }); + + describe("createOadMessageProcessor", () => { + it("should create processor context with default folder", () => { + const context = createOadMessageProcessor("", TEST_CONSTANTS.PR_URL); + + expect(context.logFilePath).toBe(path.join(".", "breaking-change.log")); + expect(context.prUrl).toBe(TEST_CONSTANTS.PR_URL); + expect(context.messageCache).toEqual([]); + }); + + it("should create processor context with custom folder", () => { + const customPrUrl = "https://github.com/owner/repo/pull/456"; + const context = createOadMessageProcessor(TEST_CONSTANTS.PATHS.CUSTOM, customPrUrl); + + expect(context.logFilePath).toBe( + path.join(TEST_CONSTANTS.PATHS.CUSTOM, "breaking-change.log"), + ); + expect(context.prUrl).toBe(customPrUrl); + expect(context.messageCache).toEqual([]); + }); + }); + + describe("createMessageKey", () => { + it("should create consistent keys for identical messages", () => { + const message1 = createMockOadMessage(); + const message2 = createMockOadMessage(); + + const key1 = createMessageKey(message1); + const key2 = createMessageKey(message2); + + expect(key1).toBe(key2); + }); + + it("should create different keys for different messages", () => { + const message1 = createMockOadMessage(); + const message2 = createMockOadMessage({ code: TEST_CONSTANTS.RULES.ADDED_PROPERTY }); + + const key1 = createMessageKey(message1); + const key2 = createMessageKey(message2); + + expect(key1).not.toBe(key2); + }); + + it("should create different keys for different locations", () => { + const message1 = createMockOadMessage(); + const message2 = createMockOadMessage({ + new: { + location: TEST_CONSTANTS.PATHS.DIFFERENT, + path: TEST_CONSTANTS.JSON_PATHS.TEST_MODEL, + }, + }); + + const key1 = createMessageKey(message1); + const key2 = createMessageKey(message2); + + expect(key1).not.toBe(key2); + }); + + it("should create different keys for different paths", () => { + const message1 = createMockOadMessage(); + const message2 = createMockOadMessage({ + new: { + location: TEST_CONSTANTS.PATHS.NEW, + path: TEST_CONSTANTS.JSON_PATHS.DIFFERENT_MODEL, + }, + }); + + const key1 = createMessageKey(message1); + const key2 = createMessageKey(message2); + + expect(key1).not.toBe(key2); + }); + }); + + describe("appendToLogFile", () => { + it("should append message to log file with newline", () => { + const message = "Test log message"; + + appendToLogFile(TEST_CONSTANTS.LOG_PATH, message); + + expect(mockAppendFileSync).toHaveBeenCalledTimes(2); + expectAppendFileSync(1, TEST_CONSTANTS.LOG_PATH, message); + expectAppendFileSync(2, TEST_CONSTANTS.LOG_PATH, "\n"); + expectLogMessageSafe("oad-message-processor.appendMsg: " + message); + }); + }); + + describe("appendMarkdownToLog", () => { + it("should append markdown with default error level", () => { + const context = createProcessorContext(); + + appendMarkdownToLog(context, TEST_CONSTANTS.MESSAGES.ERROR); + + expect(mockAppendFileSync).toHaveBeenCalledTimes(2); + const appendedContent = mockAppendFileSync.mock.calls[0][1] as string; + const parsedContent = JSON.parse(appendedContent); + + expect(parsedContent).toEqual({ + type: "Markdown", + mode: "append", + level: "Error", + message: TEST_CONSTANTS.MESSAGES.ERROR, + time: expect.any(String), + }); + }); + + it("should append markdown with custom level", () => { + const context = createProcessorContext(); + + appendMarkdownToLog(context, TEST_CONSTANTS.MESSAGES.WARNING, "Warning"); + + const appendedContent = mockAppendFileSync.mock.calls[0][1] as string; + const parsedContent = JSON.parse(appendedContent); + + expect(parsedContent.level).toBe("Warning"); + }); + }); + + describe("processAndAppendOadMessages", () => { + it("should process and append new messages", () => { + // Reset message cache for this test + mockContext.oadMessageProcessorContext.messageCache = []; + + const oadMessages = [createMockOadMessage()]; + + const result = processAndAppendOadMessages( + mockContext, + oadMessages, + TEST_CONSTANTS.BRANCH.MAIN, + ); + + expect(result).toHaveLength(1); + expect(mockContext.oadMessageProcessorContext.messageCache).toHaveLength(1); + expect(mockAppendFileSync).toHaveBeenCalledTimes(2); + expectLogMessage("oad-message-processor.processAndAppendOadMessages"); + }); + + it("should deduplicate messages", () => { + // Reset message cache for this test + mockContext.oadMessageProcessorContext.messageCache = []; + + const baseMessage = createMockOadMessage(); + const differentMessage = createMockOadMessage({ code: TEST_CONSTANTS.RULES.ADDED_PROPERTY }); + const oadMessages = [baseMessage, differentMessage]; + + const result = processAndAppendOadMessages( + mockContext, + oadMessages, + TEST_CONSTANTS.BRANCH.MAIN, + ); + + expect(result).toHaveLength(2); // Both unique messages + expect(mockContext.oadMessageProcessorContext.messageCache).toHaveLength(2); + expectLogMessage("duplicateOadMessages.length: 0"); + }); + + it("should test actual deduplication with cache", () => { + // First, add a message to the cache + const existingMessage = createMockOadMessage(); + mockContext.oadMessageProcessorContext.messageCache = [existingMessage]; + + // Create new messages including one that matches the cached message + const sameMessage = { ...existingMessage }; + const differentMessage = createMockOadMessage({ code: TEST_CONSTANTS.RULES.ADDED_PROPERTY }); + const oadMessages = [sameMessage, differentMessage]; + + const result = processAndAppendOadMessages( + mockContext, + oadMessages, + TEST_CONSTANTS.BRANCH.MAIN, + ); + + expect(result).toHaveLength(1); // Only the different message should be processed + expect(mockContext.oadMessageProcessorContext.messageCache).toHaveLength(2); // Original + new different message + expectLogMessage("duplicateOadMessages.length: 1"); + }); + + it("should not process messages already in cache", () => { + const existingMessage = createMockOadMessage(); + mockContext.oadMessageProcessorContext.messageCache = [existingMessage]; + + const oadMessages = [createMockOadMessage()]; // Same as existing + + const result = processAndAppendOadMessages( + mockContext, + oadMessages, + TEST_CONSTANTS.BRANCH.MAIN, + ); + + expect(result).toHaveLength(0); // No new messages + expect(mockContext.oadMessageProcessorContext.messageCache).toHaveLength(1); // Cache unchanged + expectLogMessage("duplicateOadMessages.length: 1"); + }); + + it("should log processing statistics", () => { + mockContext.oadMessageProcessorContext.messageCache = []; + + const message1 = createMockOadMessage(); + const message2 = createMockOadMessage({ code: TEST_CONSTANTS.RULES.ADDED_PROPERTY }); + const oadMessages = [message1, message2]; + + processAndAppendOadMessages(mockContext, oadMessages, TEST_CONSTANTS.BRANCH.DEVELOP); + + expect(mockLogMessage).toHaveBeenCalledWith( + `oad-message-processor.processAndAppendOadMessages: PR:${TEST_CONSTANTS.PR_URL}, ` + + `baseBranch: ${TEST_CONSTANTS.BRANCH.DEVELOP}, oadMessages.length: 2, duplicateOadMessages.length: 0, messageCache.length: 0.`, + ); + }); + }); + + describe("clearMessageCache", () => { + it("should clear the message cache", () => { + const context = createProcessorContext(TEST_CONSTANTS.LOG_PATH, TEST_CONSTANTS.PR_URL, [ + createMockOadMessage(), + ]); + + clearMessageCache(context); + + expect(context.messageCache).toHaveLength(0); + }); + }); + + describe("getMessageCacheSize", () => { + it("should return the size of message cache", () => { + const context = createProcessorContext(TEST_CONSTANTS.LOG_PATH, TEST_CONSTANTS.PR_URL, [ + createMockOadMessage({ + message: "Test 1", + id: "test-id-1", + }), + createMockOadMessage({ + type: "Warning", + code: TEST_CONSTANTS.RULES.ADDED_PROPERTY, + message: "Test 2", + id: "test-id-2", + groupName: ApiVersionLifecycleStage.PREVIEW, + new: { location: TEST_CONSTANTS.LOCATIONS.NEW2, path: TEST_CONSTANTS.JSON_PATHS.PATH2 }, + old: { location: TEST_CONSTANTS.LOCATIONS.OLD2, path: TEST_CONSTANTS.JSON_PATHS.PATH2 }, + }), + ]); + + const size = getMessageCacheSize(context); + + expect(size).toBe(2); + }); + + it("should return 0 for empty cache", () => { + const context = createProcessorContext(); + + const size = getMessageCacheSize(context); + + expect(size).toBe(0); + }); + }); +}); diff --git a/eng/tools/openapi-diff-runner/test/utils/pull-request.test.ts b/eng/tools/openapi-diff-runner/test/utils/pull-request.test.ts new file mode 100644 index 000000000000..26b99c5736a3 --- /dev/null +++ b/eng/tools/openapi-diff-runner/test/utils/pull-request.test.ts @@ -0,0 +1,449 @@ +import { existsSync, mkdirSync } from "node:fs"; +import path from "node:path"; +import { simpleGit } from "simple-git"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { Context } from "../../src/types/breaking-change.js"; +import { createPullRequestProperties } from "../../src/utils/pull-request.js"; + +// Test constants +const TEST_CONSTANTS = { + BRANCHES: { + MAIN: "main", + DEVELOP: "develop", + FEATURE: "feature-branch", + SOURCE: "source-b6791c5f-e0a5-49b1-9175-d7fd3e341cb8", + }, + PATHS: { + SPEC_REPO: "/path/to/spec/repo", + WORKING_FOLDER: "/path/to/working/folder", + LOG_FOLDER: "/path/to/log/folder", + LOG_FILE: "/path/to/log/file.log", + CURRENT_WORKING_DIR: "/current/working/dir", + RESOLVED_WORKING_DIR: "/resolved/working/dir", + CUSTOM_PREFIX_PATH: "../custom-prefix-c93b354fd9c14905bb574a8834c4d69b", + }, + REPO: { + NAME: "owner/repo", + COMMIT: "abc123", + PR_NUMBER: "123", + PR_URL: "https://github.com/owner/repo/pull/123", + }, + PREFIXES: { + TEST: "test-prefix", + CUSTOM: "custom-prefix", + }, + SWAGGER_DIRS: ["specification/service/path"] as string[], + ERRORS: { + GET_REMOTES: "Failed to get remotes", + REMOTE_EXISTS: "fatal: remote origin already exists", + OTHER_ERROR: "Some other error", + }, +} as const; + +// Helper functions +function createMockContext(overrides: Partial = {}): Context { + return { + localSpecRepoPath: TEST_CONSTANTS.PATHS.SPEC_REPO, + workingFolder: TEST_CONSTANTS.PATHS.WORKING_FOLDER, + logFileFolder: TEST_CONSTANTS.PATHS.LOG_FOLDER, + swaggerDirs: TEST_CONSTANTS.SWAGGER_DIRS, + baseBranch: TEST_CONSTANTS.BRANCHES.MAIN, + headCommit: TEST_CONSTANTS.REPO.COMMIT, + runType: "SameVersion", + checkName: "BreakingChange", + targetRepo: TEST_CONSTANTS.REPO.NAME, + sourceRepo: TEST_CONSTANTS.REPO.NAME, + prNumber: TEST_CONSTANTS.REPO.PR_NUMBER, + prSourceBranch: TEST_CONSTANTS.BRANCHES.FEATURE, + prTargetBranch: TEST_CONSTANTS.BRANCHES.MAIN, + oadMessageProcessorContext: { + logFilePath: TEST_CONSTANTS.PATHS.LOG_FILE, + prUrl: TEST_CONSTANTS.REPO.PR_URL, + messageCache: [], + }, + prUrl: TEST_CONSTANTS.REPO.PR_URL, + ...overrides, + }; +} + +function setupBasicGitMocks(mockGitRepo: any) { + mockGitRepo.branch.mockResolvedValue({ + all: [TEST_CONSTANTS.BRANCHES.MAIN, TEST_CONSTANTS.BRANCHES.SOURCE], + }); + mockGitRepo.getRemotes.mockResolvedValue([{ name: "origin" }]); + mockGitRepo.init.mockResolvedValue(undefined); + mockGitRepo.addRemote.mockResolvedValue(undefined); + mockGitRepo.pull.mockResolvedValue(undefined); + mockGitRepo.fetch.mockResolvedValue(undefined); + mockGitRepo.checkout.mockResolvedValue(undefined); +} + +function setupCustomBranchMocks(mockGitRepo: any, branches: string[]) { + mockGitRepo.branch.mockResolvedValue({ all: branches }); + mockGitRepo.getRemotes.mockResolvedValue([{ name: "origin" }]); + mockGitRepo.init.mockResolvedValue(undefined); + mockGitRepo.addRemote.mockResolvedValue(undefined); + mockGitRepo.pull.mockResolvedValue(undefined); + mockGitRepo.fetch.mockResolvedValue(undefined); + mockGitRepo.checkout.mockResolvedValue(undefined); +} + +function setupGitErrorMocks(mockGitRepo: any, errorType: string) { + mockGitRepo.branch.mockResolvedValue({ + all: [TEST_CONSTANTS.BRANCHES.MAIN, TEST_CONSTANTS.BRANCHES.SOURCE], + }); + mockGitRepo.getRemotes.mockRejectedValue(new Error(errorType)); + mockGitRepo.init.mockResolvedValue(undefined); + mockGitRepo.pull.mockResolvedValue(undefined); + mockGitRepo.fetch.mockResolvedValue(undefined); + mockGitRepo.checkout.mockResolvedValue(undefined); +} + +function setupPathMocks(mockPath: any, mockExistsSync: any, exists: boolean = true) { + mockExistsSync.mockReturnValue(exists); + mockPath.resolve.mockReturnValue(TEST_CONSTANTS.PATHS.RESOLVED_WORKING_DIR); + mockPath.join.mockImplementation((...paths: string[]) => paths.join("/")); +} + +function expectPullRequestResult(result: any, expectedProps: Partial) { + expect(result).toBeDefined(); + if (expectedProps.baseBranch) expect(result!.baseBranch).toBe(expectedProps.baseBranch); + if (expectedProps.targetBranch) expect(result!.targetBranch).toBe(expectedProps.targetBranch); + if (expectedProps.sourceBranch) expect(result!.sourceBranch).toBe(expectedProps.sourceBranch); + if (expectedProps.tempRepoFolder) + expect(result!.tempRepoFolder).toBe(expectedProps.tempRepoFolder); + if (expectedProps.currentBranch) expect(result!.currentBranch).toBe(expectedProps.currentBranch); + if (expectedProps.hasCheckout) expect(typeof result!.checkout).toBe("function"); +} + +function expectGitOperationCalls(mockGitRepo: any, operations: string[]) { + operations.forEach((operation) => { + expect(mockGitRepo[operation]).toHaveBeenCalled(); + }); +} + +function expectCheckoutResult(result: any, expectedBranch: string) { + expect(result).toBeDefined(); + expect(result!.currentBranch).toBe(expectedBranch); +} + +// Mock dependencies +vi.mock("node:fs"); +vi.mock("node:path"); +vi.mock("simple-git"); + +describe("pull-request", () => { + const mockExistsSync = vi.mocked(existsSync); + const mockMkdirSync = vi.mocked(mkdirSync); + const mockPath = vi.mocked(path); + const mockSimpleGit = vi.mocked(simpleGit); + + // Mock git repository instance + const mockGitRepo = { + branch: vi.fn(), + init: vi.fn(), + getRemotes: vi.fn(), + addRemote: vi.fn(), + pull: vi.fn(), + fetch: vi.fn(), + checkout: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + + // Setup default mocks + mockSimpleGit.mockReturnValue(mockGitRepo as any); + mockPath.resolve.mockImplementation((...paths: string[]) => paths.join("/")); + mockPath.join.mockImplementation((...paths: string[]) => paths.join("/")); + + // Mock console.log to avoid test output noise + vi.spyOn(console, "log").mockImplementation(() => {}); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + describe("createPullRequestProperties", () => { + it("should return undefined when baseBranch is undefined", async () => { + const context = createMockContext({ baseBranch: undefined }); + + const result = await createPullRequestProperties(context, TEST_CONSTANTS.PREFIXES.TEST); + + expect(result).toBeUndefined(); + }); + + it("should create pull request properties successfully", async () => { + const context = createMockContext(); + + setupBasicGitMocks(mockGitRepo); + setupPathMocks(mockPath, mockExistsSync, false); + + const result = await createPullRequestProperties(context, TEST_CONSTANTS.PREFIXES.TEST); + + expectPullRequestResult(result, { + baseBranch: TEST_CONSTANTS.BRANCHES.MAIN, + targetBranch: TEST_CONSTANTS.BRANCHES.MAIN, + sourceBranch: TEST_CONSTANTS.BRANCHES.SOURCE, + tempRepoFolder: TEST_CONSTANTS.PATHS.RESOLVED_WORKING_DIR, + currentBranch: TEST_CONSTANTS.BRANCHES.MAIN, + hasCheckout: true, + }); + }); + + it("should create source branch if it doesn't exist", async () => { + const context = createMockContext(); + + setupCustomBranchMocks(mockGitRepo, [TEST_CONSTANTS.BRANCHES.MAIN]); // Source branch doesn't exist + setupPathMocks(mockPath, mockExistsSync); + + await createPullRequestProperties(context, TEST_CONSTANTS.PREFIXES.TEST); + + expect(mockGitRepo.branch).toHaveBeenCalledWith([TEST_CONSTANTS.BRANCHES.SOURCE]); + }); + + it("should create base branch if it doesn't exist and skipInitializeBase is false", async () => { + const context = createMockContext({ baseBranch: TEST_CONSTANTS.BRANCHES.DEVELOP }); + + setupCustomBranchMocks(mockGitRepo, [ + TEST_CONSTANTS.BRANCHES.MAIN, + TEST_CONSTANTS.BRANCHES.SOURCE, + ]); // Base branch doesn't exist + setupPathMocks(mockPath, mockExistsSync); + + await createPullRequestProperties(context, TEST_CONSTANTS.PREFIXES.TEST, false); + + expect(mockGitRepo.branch).toHaveBeenCalledWith([ + TEST_CONSTANTS.BRANCHES.DEVELOP, + `remotes/origin/${TEST_CONSTANTS.BRANCHES.DEVELOP}`, + ]); + }); + + it("should not create base branch if skipInitializeBase is true", async () => { + const context = createMockContext({ baseBranch: TEST_CONSTANTS.BRANCHES.DEVELOP }); + + setupBasicGitMocks(mockGitRepo); + setupPathMocks(mockPath, mockExistsSync); + + await createPullRequestProperties(context, TEST_CONSTANTS.PREFIXES.TEST, true); + + expect(mockGitRepo.branch).not.toHaveBeenCalledWith([ + TEST_CONSTANTS.BRANCHES.DEVELOP, + `remotes/origin/${TEST_CONSTANTS.BRANCHES.DEVELOP}`, + ]); + expect(mockGitRepo.fetch).not.toHaveBeenCalledWith("origin", TEST_CONSTANTS.BRANCHES.DEVELOP); + }); + + it("should create target branch if it doesn't exist", async () => { + const context = createMockContext({ prTargetBranch: TEST_CONSTANTS.BRANCHES.FEATURE }); + + setupCustomBranchMocks(mockGitRepo, [ + TEST_CONSTANTS.BRANCHES.MAIN, + TEST_CONSTANTS.BRANCHES.SOURCE, + ]); // Target branch doesn't exist + setupPathMocks(mockPath, mockExistsSync); + + await createPullRequestProperties(context, TEST_CONSTANTS.PREFIXES.TEST); + + expect(mockGitRepo.branch).toHaveBeenCalledWith([ + TEST_CONSTANTS.BRANCHES.FEATURE, + `remotes/origin/${TEST_CONSTANTS.BRANCHES.FEATURE}`, + ]); + }); + + it("should create working directory if it doesn't exist", async () => { + const context = createMockContext(); + + setupBasicGitMocks(mockGitRepo); + setupPathMocks(mockPath, mockExistsSync, false); // Directory doesn't exist + + await createPullRequestProperties(context, TEST_CONSTANTS.PREFIXES.TEST); + + expect(mockMkdirSync).toHaveBeenCalledWith(expect.any(String)); + }); + + it("should add origin remote if it doesn't exist", async () => { + const context = createMockContext(); + + mockGitRepo.branch.mockResolvedValue({ + all: [TEST_CONSTANTS.BRANCHES.MAIN, TEST_CONSTANTS.BRANCHES.SOURCE], + }); + mockGitRepo.getRemotes.mockResolvedValue([]); // No remotes exist + setupPathMocks(mockPath, mockExistsSync); + + await createPullRequestProperties(context, TEST_CONSTANTS.PREFIXES.TEST); + + expect(mockGitRepo.addRemote).toHaveBeenCalledWith("origin", context.localSpecRepoPath); + }); + + it("should not add origin remote if it already exists", async () => { + const context = createMockContext(); + + setupBasicGitMocks(mockGitRepo); + setupPathMocks(mockPath, mockExistsSync); + + await createPullRequestProperties(context, TEST_CONSTANTS.PREFIXES.TEST); + + expect(mockGitRepo.addRemote).not.toHaveBeenCalled(); + }); + + it("should handle getRemotes error and try to add origin anyway", async () => { + const context = createMockContext(); + + setupGitErrorMocks(mockGitRepo, TEST_CONSTANTS.ERRORS.GET_REMOTES); + mockGitRepo.addRemote.mockResolvedValue(undefined); + setupPathMocks(mockPath, mockExistsSync); + + await createPullRequestProperties(context, TEST_CONSTANTS.PREFIXES.TEST); + + expect(mockGitRepo.addRemote).toHaveBeenCalledWith("origin", context.localSpecRepoPath); + }); + + it("should ignore 'remote origin already exists' error when adding remote", async () => { + const context = createMockContext(); + + setupGitErrorMocks(mockGitRepo, TEST_CONSTANTS.ERRORS.GET_REMOTES); + mockGitRepo.addRemote.mockRejectedValue(new Error(TEST_CONSTANTS.ERRORS.REMOTE_EXISTS)); + setupPathMocks(mockPath, mockExistsSync); + + // Should not throw an error + await expect( + createPullRequestProperties(context, TEST_CONSTANTS.PREFIXES.TEST), + ).resolves.toBeDefined(); + }); + + it("should throw error if addRemote fails with other error", async () => { + const context = createMockContext(); + + setupGitErrorMocks(mockGitRepo, TEST_CONSTANTS.ERRORS.GET_REMOTES); + mockGitRepo.addRemote.mockRejectedValue(new Error(TEST_CONSTANTS.ERRORS.OTHER_ERROR)); + setupPathMocks(mockPath, mockExistsSync); + + await expect( + createPullRequestProperties(context, TEST_CONSTANTS.PREFIXES.TEST), + ).rejects.toThrow(TEST_CONSTANTS.ERRORS.OTHER_ERROR); + }); + + it("should perform git operations in correct order", async () => { + const context = createMockContext(); + + setupBasicGitMocks(mockGitRepo); + setupPathMocks(mockPath, mockExistsSync); + + await createPullRequestProperties(context, TEST_CONSTANTS.PREFIXES.TEST); + + // Verify order of operations + expectGitOperationCalls(mockGitRepo, ["init"]); + expect(mockGitRepo.pull).toHaveBeenCalledWith("origin", TEST_CONSTANTS.BRANCHES.MAIN); + expect(mockGitRepo.fetch).toHaveBeenCalledWith("origin", TEST_CONSTANTS.BRANCHES.SOURCE); + expect(mockGitRepo.fetch).toHaveBeenCalledWith("origin", TEST_CONSTANTS.BRANCHES.MAIN); + expect(mockGitRepo.checkout).toHaveBeenCalledWith(TEST_CONSTANTS.BRANCHES.MAIN); + }); + + describe("checkout function", () => { + it("should checkout to different branch", async () => { + const context = createMockContext(); + + setupBasicGitMocks(mockGitRepo); + setupPathMocks(mockPath, mockExistsSync); + + const result = await createPullRequestProperties(context, TEST_CONSTANTS.PREFIXES.TEST); + + expectCheckoutResult(result, TEST_CONSTANTS.BRANCHES.MAIN); + + // Test checkout function + await result!.checkout(TEST_CONSTANTS.BRANCHES.FEATURE); + + expect(mockGitRepo.checkout).toHaveBeenCalledWith([TEST_CONSTANTS.BRANCHES.FEATURE]); + expect(result!.currentBranch).toBe(TEST_CONSTANTS.BRANCHES.FEATURE); + }); + + it("should not checkout if already on the target branch", async () => { + const context = createMockContext(); + + setupBasicGitMocks(mockGitRepo); + setupPathMocks(mockPath, mockExistsSync); + + const result = await createPullRequestProperties(context, TEST_CONSTANTS.PREFIXES.TEST); + + expectCheckoutResult(result, TEST_CONSTANTS.BRANCHES.MAIN); + + // Clear previous checkout calls + mockGitRepo.checkout.mockClear(); + + // Test checkout to same branch + await result!.checkout(TEST_CONSTANTS.BRANCHES.MAIN); + + expect(mockGitRepo.checkout).not.toHaveBeenCalled(); + expect(result!.currentBranch).toBe(TEST_CONSTANTS.BRANCHES.MAIN); + }); + + it("should update currentBranch after successful checkout", async () => { + const context = createMockContext(); + + setupBasicGitMocks(mockGitRepo); + setupPathMocks(mockPath, mockExistsSync); + + const result = await createPullRequestProperties(context, TEST_CONSTANTS.PREFIXES.TEST); + + expectCheckoutResult(result, TEST_CONSTANTS.BRANCHES.MAIN); + + // Test multiple checkouts + await result!.checkout(TEST_CONSTANTS.BRANCHES.FEATURE); + expect(result!.currentBranch).toBe(TEST_CONSTANTS.BRANCHES.FEATURE); + + await result!.checkout(TEST_CONSTANTS.BRANCHES.DEVELOP); + expect(result!.currentBranch).toBe(TEST_CONSTANTS.BRANCHES.DEVELOP); + + // Verify checkout was called for each different branch + expect(mockGitRepo.checkout).toHaveBeenCalledTimes(3); // 1 initial + 2 from tests + }); + }); + + it("should use correct working directory path", async () => { + const context = createMockContext(); + const prefix = TEST_CONSTANTS.PREFIXES.CUSTOM; + + setupBasicGitMocks(mockGitRepo); + setupPathMocks(mockPath, mockExistsSync); + + // Mock path operations + vi.spyOn(process, "cwd").mockReturnValue(TEST_CONSTANTS.PATHS.CURRENT_WORKING_DIR); + mockPath.join.mockReturnValue(TEST_CONSTANTS.PATHS.CUSTOM_PREFIX_PATH); + mockPath.resolve.mockReturnValue(TEST_CONSTANTS.PATHS.RESOLVED_WORKING_DIR); + + const result = await createPullRequestProperties(context, prefix); + + expect(mockPath.join).toHaveBeenCalledWith( + TEST_CONSTANTS.PATHS.CURRENT_WORKING_DIR, + "..", + "custom-prefix-c93b354fd9c14905bb574a8834c4d69b", + ); + expect(mockPath.resolve).toHaveBeenCalledWith(TEST_CONSTANTS.PATHS.CUSTOM_PREFIX_PATH); + expect(result!.tempRepoFolder).toBe(TEST_CONSTANTS.PATHS.RESOLVED_WORKING_DIR); + }); + + it("should use correct git options", async () => { + const context = createMockContext(); + + setupBasicGitMocks(mockGitRepo); + setupPathMocks(mockPath, mockExistsSync); + + await createPullRequestProperties(context, TEST_CONSTANTS.PREFIXES.TEST); + + // Verify simpleGit was called with correct options + expect(mockSimpleGit).toHaveBeenCalledWith({ + baseDir: context.localSpecRepoPath, + binary: "git", + maxConcurrentProcesses: 1, + }); + + expect(mockSimpleGit).toHaveBeenCalledWith({ + baseDir: expect.any(String), // working directory + binary: "git", + maxConcurrentProcesses: 1, + }); + }); + }); +}); diff --git a/eng/tools/openapi-diff-runner/test/utils/spec.test.ts b/eng/tools/openapi-diff-runner/test/utils/spec.test.ts new file mode 100644 index 000000000000..2200e0a29991 --- /dev/null +++ b/eng/tools/openapi-diff-runner/test/utils/spec.test.ts @@ -0,0 +1,362 @@ +import { describe, expect, it } from "vitest"; +import { ApiVersionLifecycleStage } from "../../src/types/breaking-change.js"; +import { + deduplicateSwaggers, + getBaseNameForSwagger, + getExistedVersionOperations, + getPrecedingSwaggers, + type Swagger, +} from "../../src/utils/spec.js"; + +// Type definitions for tests (extending the base interfaces if needed) +interface MockSwagger extends Swagger { + // Can add test-specific properties if needed +} + +describe("Helper functions for version analysis", () => { + // Helper function to create mock operations + const createMockOperation = (id: string, path: string, httpMethod = "GET") => ({ + id, + path, + httpMethod, + }); + + // Helper function to create mock swagger objects + const createMockSwagger = ( + path: string, + versionKind?: ApiVersionLifecycleStage, + operations?: Map, + ): MockSwagger => ({ + path, + ...(versionKind && { versionKind }), + ...(operations && { getOperations: async () => operations }), + }); + + // Helper function to create operations map + const createOperationsMap = ( + operations: Array<{ id: string; path: string; httpMethod?: string }>, + ) => { + const map = new Map(); + operations.forEach((op) => { + map.set(op.id, createMockOperation(op.id, op.path, op.httpMethod || "GET")); + }); + return map; + }; + + // Helper function to expect standard result structure + const expectResultStructure = (result: any, stable?: string, preview?: string) => { + expect(result).toHaveProperty("stable"); + expect(result).toHaveProperty("preview"); + if (stable !== undefined) expect(result.stable).toBe(stable); + if (preview !== undefined) expect(result.preview).toBe(preview); + }; + + // Common test paths + const TEST_PATHS = { + stable2020_07_02: "/test/stable/2020-07-02/a.json", + stable2020_08_04: "/test/stable/2020-08-04/a.json", + preview2020_07_02: "/test/preview/2020-07-02/a.json", + preview2020_08_04: "/test/preview/2020-08-04-preview/a.json", + differentFile2020_07_02: "/test/stable/2020-07-02/b.json", + differentFile2020_05_01: "/test/stable/2020-05-01/b.json", + differentFile2020_07_02_c: "/test/stable/2020-07-02/c.json", + nonexistent: "/nonexistent/path.json", + }; + + // Common operations + const OPERATIONS = { + test1: { id: "Operation_Test1", path: "/test/path1", httpMethod: "GET" }, + test2: { id: "Operation_Test2", path: "/test/path2", httpMethod: "POST" }, + test3: { id: "Operation_Test3", path: "/test/path3", httpMethod: "DELETE" }, + old: { id: "Operation_Old", path: "/test/old", httpMethod: "GET" }, + new: { id: "Operation_New", path: "/test/new", httpMethod: "POST" }, + shared: { id: "SharedOperation", path: "/shared/path", httpMethod: "GET" }, + }; + describe("getPrecedingSwaggers", () => { + it("should find preceding stable and preview versions", async () => { + // Mock swagger objects for different versions + const mockSwaggers: MockSwagger[] = [ + createMockSwagger(TEST_PATHS.stable2020_07_02, ApiVersionLifecycleStage.STABLE), + createMockSwagger(TEST_PATHS.stable2020_08_04, ApiVersionLifecycleStage.STABLE), + createMockSwagger(TEST_PATHS.preview2020_07_02, ApiVersionLifecycleStage.PREVIEW), + createMockSwagger(TEST_PATHS.preview2020_08_04, ApiVersionLifecycleStage.PREVIEW), + ]; + + const result = await getPrecedingSwaggers(TEST_PATHS.stable2020_08_04, mockSwaggers); + + expectResultStructure(result, TEST_PATHS.stable2020_08_04, TEST_PATHS.preview2020_08_04); + }); + + it("should return the version itself when it's the only version", async () => { + const mockSwaggers: MockSwagger[] = [ + createMockSwagger(TEST_PATHS.stable2020_07_02, ApiVersionLifecycleStage.STABLE), + ]; + + const result = await getPrecedingSwaggers(TEST_PATHS.stable2020_07_02, mockSwaggers); + + expectResultStructure(result, TEST_PATHS.stable2020_07_02); + expect(result.preview).toBeUndefined(); + }); + + it("should return undefined when target swagger is not found", async () => { + const mockSwaggers: MockSwagger[] = [ + createMockSwagger(TEST_PATHS.stable2020_07_02, ApiVersionLifecycleStage.STABLE), + ]; + + const result = await getPrecedingSwaggers(TEST_PATHS.nonexistent, mockSwaggers); + + expectResultStructure(result); + expect(result.stable).toBeUndefined(); + expect(result.preview).toBeUndefined(); + }); + + it("should handle null or undefined availableSwaggers array", async () => { + const testCases = [[], [], []]; // Testing empty arrays multiple times + + for (const swaggers of testCases) { + const result = await getPrecedingSwaggers(TEST_PATHS.stable2020_08_04, swaggers); + expectResultStructure(result); + expect(result.stable).toBeUndefined(); + expect(result.preview).toBeUndefined(); + } + }); + }); + + describe("getExistedVersionOperations", () => { + it("should find operations that exist in both previous and current versions", async () => { + const mockOperations1 = createOperationsMap([OPERATIONS.test1, OPERATIONS.test2]); + const mockOperations2 = createOperationsMap([OPERATIONS.test1, OPERATIONS.test3]); + + const mockSwaggers: MockSwagger[] = [ + createMockSwagger(TEST_PATHS.differentFile2020_07_02, undefined, mockOperations1), + createMockSwagger(TEST_PATHS.stable2020_08_04, undefined, mockOperations2), + ]; + + const targetOperations = [ + createMockOperation(OPERATIONS.test1.id, OPERATIONS.test1.path), + createMockOperation( + OPERATIONS.test3.id, + OPERATIONS.test3.path, + OPERATIONS.test3.httpMethod, + ), + ]; + + const result = await getExistedVersionOperations( + TEST_PATHS.stable2020_08_04, + mockSwaggers, + targetOperations, + ); + + expect(result).toBeInstanceOf(Map); + expect(result.has(TEST_PATHS.differentFile2020_07_02)).toBe(true); + + const operations = result.get(TEST_PATHS.differentFile2020_07_02); + expect(operations).toBeDefined(); + if (operations) { + expect(operations).toHaveLength(1); + expect(operations[0]).toHaveProperty("id", OPERATIONS.test1.id); + expect(operations[0]).toHaveProperty("path", OPERATIONS.test1.path); + expect(operations[0]).toHaveProperty("httpMethod", OPERATIONS.test1.httpMethod); + } + }); + + it("should return empty result when no matching operations exist", async () => { + const mockOperations1 = createOperationsMap([OPERATIONS.old]); + const mockOperations2 = createOperationsMap([OPERATIONS.new]); + + const mockSwaggers: MockSwagger[] = [ + createMockSwagger(TEST_PATHS.differentFile2020_07_02, undefined, mockOperations1), + createMockSwagger(TEST_PATHS.stable2020_08_04, undefined, mockOperations2), + ]; + + const targetOperations = [ + createMockOperation(OPERATIONS.new.id, OPERATIONS.new.path, OPERATIONS.new.httpMethod), + ]; + + const result = await getExistedVersionOperations( + TEST_PATHS.stable2020_08_04, + mockSwaggers, + targetOperations, + ); + + expect(result).toBeInstanceOf(Map); + expect(result.has(TEST_PATHS.differentFile2020_07_02)).toBe(true); + + const operations = result.get(TEST_PATHS.differentFile2020_07_02); + expect(operations).toBeDefined(); + if (operations) { + expect(operations).toHaveLength(0); + } + }); + + it("should return empty map when target swagger is not found", async () => { + const mockSwaggers = [createMockSwagger(TEST_PATHS.stable2020_07_02, undefined, new Map())]; + + const result = await getExistedVersionOperations(TEST_PATHS.nonexistent, mockSwaggers, []); + + expect(result).toBeInstanceOf(Map); + expect(result.size).toBe(0); + }); + + it("should handle multiple previous versions correctly", async () => { + const sharedOperationMap = createOperationsMap([OPERATIONS.shared]); + + const mockSwaggers: MockSwagger[] = [ + createMockSwagger(TEST_PATHS.differentFile2020_05_01, undefined, sharedOperationMap), + createMockSwagger(TEST_PATHS.differentFile2020_07_02_c, undefined, sharedOperationMap), + createMockSwagger(TEST_PATHS.stable2020_08_04, undefined, sharedOperationMap), + ]; + + const targetOperations = [createMockOperation(OPERATIONS.shared.id, OPERATIONS.shared.path)]; + const result = await getExistedVersionOperations( + TEST_PATHS.stable2020_08_04, + mockSwaggers, + targetOperations, + ); + + expect(result).toBeInstanceOf(Map); + expect(result.has(TEST_PATHS.differentFile2020_05_01)).toBe(true); + expect(result.has(TEST_PATHS.differentFile2020_07_02_c)).toBe(true); + + // Check operations from first previous version + const operations1 = result.get(TEST_PATHS.differentFile2020_05_01); + expect(operations1).toBeDefined(); + if (operations1) { + expect(operations1).toHaveLength(1); + expect(operations1[0]).toHaveProperty("id", OPERATIONS.shared.id); + } + + // Check operations from second previous version + const operations2 = result.get(TEST_PATHS.differentFile2020_07_02_c); + expect(operations2).toBeDefined(); + if (operations2) { + expect(operations2).toHaveLength(1); + expect(operations2[0]).toHaveProperty("id", OPERATIONS.shared.id); + } + }); + + it("should handle null or undefined availableSwaggers array", async () => { + const testCases = [[], [], []]; // Testing empty arrays multiple times + + for (const swaggers of testCases) { + const result = await getExistedVersionOperations(TEST_PATHS.stable2020_08_04, swaggers, []); + expect(result).toBeInstanceOf(Map); + expect(result.size).toBe(0); + } + }); + }); + + describe("getBaseNameForSwagger", () => { + it("should extract the filename after the version segment", () => { + const result = getBaseNameForSwagger( + "specification/network/resource-manager/Microsoft.Network/stable/2019-11-01/network.json", + "2019-11-01", + ); + expect(result).toBe("network.json"); + }); + + it("should return the basename when no version is provided", () => { + const result = getBaseNameForSwagger( + "specification/network/resource-manager/Microsoft.Network/stable/2019-11-01/network.json", + ); + expect(result).toBe("network.json"); + }); + + it("should handle paths without version segments", () => { + const result = getBaseNameForSwagger("specification/network/test.json"); + expect(result).toBe("test.json"); + }); + }); + + describe("deduplicateSwaggers", () => { + it("should return empty array for empty input", () => { + const result = deduplicateSwaggers([]); + expect(result).toEqual([]); + }); + + it("should return the same array when no duplicates exist", () => { + const mockSwaggers: MockSwagger[] = [ + createMockSwagger(TEST_PATHS.stable2020_07_02), + createMockSwagger(TEST_PATHS.stable2020_08_04), + createMockSwagger(TEST_PATHS.preview2020_07_02), + ]; + + const result = deduplicateSwaggers(mockSwaggers); + + expect(result).toHaveLength(3); + expect(result[0].path).toBe(TEST_PATHS.stable2020_07_02); + expect(result[1].path).toBe(TEST_PATHS.stable2020_08_04); + expect(result[2].path).toBe(TEST_PATHS.preview2020_07_02); + }); + + it("should remove duplicate swaggers with same path", () => { + const mockSwaggers: MockSwagger[] = [ + createMockSwagger(TEST_PATHS.stable2020_07_02), + createMockSwagger(TEST_PATHS.stable2020_08_04), + createMockSwagger(TEST_PATHS.stable2020_07_02), // duplicate + createMockSwagger(TEST_PATHS.preview2020_07_02), + createMockSwagger(TEST_PATHS.stable2020_08_04), // duplicate + ]; + + const result = deduplicateSwaggers(mockSwaggers); + + expect(result).toHaveLength(3); + expect(result.map((s) => s.path)).toEqual([ + TEST_PATHS.stable2020_07_02, + TEST_PATHS.stable2020_08_04, + TEST_PATHS.preview2020_07_02, + ]); + }); + + it("should preserve the first occurrence when duplicates exist", () => { + const swagger1 = createMockSwagger(TEST_PATHS.stable2020_07_02); + const swagger2 = createMockSwagger(TEST_PATHS.stable2020_07_02); // same path, different object + swagger1.versionKind = ApiVersionLifecycleStage.STABLE; + swagger2.versionKind = ApiVersionLifecycleStage.PREVIEW; + + const mockSwaggers: MockSwagger[] = [swagger1, swagger2]; + + const result = deduplicateSwaggers(mockSwaggers); + + expect(result).toHaveLength(1); + expect(result[0]).toBe(swagger1); // First occurrence preserved + expect(result[0].versionKind).toBe(ApiVersionLifecycleStage.STABLE); + }); + + it("should handle single swagger", () => { + const mockSwaggers: MockSwagger[] = [createMockSwagger(TEST_PATHS.stable2020_07_02)]; + + const result = deduplicateSwaggers(mockSwaggers); + + expect(result).toHaveLength(1); + expect(result[0].path).toBe(TEST_PATHS.stable2020_07_02); + }); + + it("should handle all duplicates scenario", () => { + const mockSwaggers: MockSwagger[] = [ + createMockSwagger(TEST_PATHS.stable2020_07_02), + createMockSwagger(TEST_PATHS.stable2020_07_02), + createMockSwagger(TEST_PATHS.stable2020_07_02), + ]; + + const result = deduplicateSwaggers(mockSwaggers); + + expect(result).toHaveLength(1); + expect(result[0].path).toBe(TEST_PATHS.stable2020_07_02); + }); + + it("should not mutate the original array", () => { + const mockSwaggers: MockSwagger[] = [ + createMockSwagger(TEST_PATHS.stable2020_07_02), + createMockSwagger(TEST_PATHS.stable2020_07_02), + ]; + const originalLength = mockSwaggers.length; + + const result = deduplicateSwaggers(mockSwaggers); + + expect(mockSwaggers).toHaveLength(originalLength); // Original unchanged + expect(result).not.toBe(mockSwaggers); // Different array instance + expect(result).toHaveLength(1); + }); + }); +}); diff --git a/eng/tools/openapi-diff-runner/tsconfig.json b/eng/tools/openapi-diff-runner/tsconfig.json new file mode 100644 index 000000000000..58ee0b661a2b --- /dev/null +++ b/eng/tools/openapi-diff-runner/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": ".", + "allowJs": true, + "resolveJsonModule": true, + }, + "include": ["*.ts", "src/**/*.ts", "test/**/*.ts"], +} diff --git a/eng/tools/package.json b/eng/tools/package.json index 3fe1225aee41..e7bceb3ca520 100644 --- a/eng/tools/package.json +++ b/eng/tools/package.json @@ -1,14 +1,17 @@ { "name": "azure-rest-api-specs-eng-tools", "devDependencies": { + "@azure-tools/lint-diff": "file:lint-diff", + "@azure-tools/oav-runner": "file:oav-runner", + "@azure-tools/sdk-suppressions": "file:sdk-suppressions", + "@azure-tools/openapi-diff-runner": "file:openapi-diff-runner", + "@azure-tools/spec-gen-sdk-runner": "file:spec-gen-sdk-runner", "@azure-tools/suppressions": "file:suppressions", "@azure-tools/tsp-client-tests": "file:tsp-client-tests", + "@azure-tools/typespec-migration-validation": "file:typespec-migration-validation", "@azure-tools/typespec-requirement": "file:typespec-requirement", "@azure-tools/typespec-validation": "file:typespec-validation", - "@azure-tools/sdk-suppressions": "file:sdk-suppressions", - "@azure-tools/spec-gen-sdk-runner": "file:spec-gen-sdk-runner", - "@azure-tools/lint-diff": "file:lint-diff", - "@azure-tools/typespec-migration-validation": "file:typespec-migration-validation" + "@azure-tools/summarize-impact": "file:summarize-impact" }, "scripts": { "build": "tsc --build", diff --git a/eng/tools/sdk-suppressions/package.json b/eng/tools/sdk-suppressions/package.json index 9523b89f7575..7c16b065fbbb 100644 --- a/eng/tools/sdk-suppressions/package.json +++ b/eng/tools/sdk-suppressions/package.json @@ -9,6 +9,9 @@ }, "scripts": { "build": "tsc --build", + "format": "prettier . --ignore-path ../.prettierignore --write", + "format:check": "prettier . --ignore-path ../.prettierignore --check", + "format:check:ci": "prettier . --ignore-path ../.prettierignore --check --log-level debug", "test": "vitest", "test:ci": "vitest run --coverage --reporter=verbose" }, @@ -28,6 +31,8 @@ "@types/lodash": "^4.14.161", "@types/node": "^20.0.0", "@vitest/coverage-v8": "^3.0.7", + "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "typescript": "~5.8.2", "vitest": "^3.0.7" } diff --git a/eng/tools/sdk-suppressions/src/common.ts b/eng/tools/sdk-suppressions/src/common.ts index 8fb57b0d62eb..12818c77bf1b 100644 --- a/eng/tools/sdk-suppressions/src/common.ts +++ b/eng/tools/sdk-suppressions/src/common.ts @@ -1,12 +1,12 @@ -import { parse as yamlParse } from "yaml"; import { getChangedFiles } from "@azure-tools/specs-shared/changed-files"; +import { parse as yamlParse } from "yaml"; /** * @returns {string[]} * @description get the changed files in the current PR */ export async function getSDKSuppressionsChangedFiles() { - const changedFiles = await getChangedFiles(); + const changedFiles = await getChangedFiles({ paths: ["specification"] }); const sdkSuppressionsFiles = changedFiles.filter((file) => file.endsWith("sdk-suppressions.yaml"), ); diff --git a/eng/tools/sdk-suppressions/src/sdkSuppressions.ts b/eng/tools/sdk-suppressions/src/sdkSuppressions.ts index 268805c9fd6d..bdb6b05fc803 100644 --- a/eng/tools/sdk-suppressions/src/sdkSuppressions.ts +++ b/eng/tools/sdk-suppressions/src/sdkSuppressions.ts @@ -4,8 +4,8 @@ * - https://microsoftapc-my.sharepoint.com/:w:/g/personal/raychen_microsoft_com/EbOAA9SkhQhGlgxtf7mc0kUB-25bFue0EFbXKXS3TFLTQA */ -import { Ajv } from "ajv"; import { SdkName, sdkLabels } from "@azure-tools/specs-shared/sdk-types"; +import { Ajv } from "ajv"; export const sdkSuppressionsFileName = "sdk-suppressions.yaml"; @@ -22,9 +22,15 @@ export type SdkPackageSuppressionsEntry = { "breaking-changes": string[]; }; -function exitWithError(error: string): never { - console.error("Error:", error); - process.exit(1); +function errorResult(error: string): { + result: boolean; + message: string; +} { + console.error("Error:", error); + return { + result: false, + message: error, + }; } export function validateSdkSuppressionsFile( @@ -34,11 +40,13 @@ export function validateSdkSuppressionsFile( message: string; } { if (suppressionContent === null) { - exitWithError("This suppression file is a empty file"); + return errorResult("This suppression file is a empty file"); } if (!suppressionContent) { - exitWithError("This suppression file is not a valid yaml. Refer to https://aka.ms/azsdk/sdk-suppression for more information."); + return errorResult( + "This suppression file is not a valid yaml. Refer to https://aka.ms/azsdk/sdk-suppression for more information.", + ); } const suppressionFileSchema = { @@ -80,6 +88,9 @@ export function validateSdkSuppressionsFile( message: "This suppression file is a valid yaml.", }; } else { - exitWithError("This suppression file is a valid yaml but the schema is wrong: " + suppressionAjv.errorsText(suppressionAjvCompile.errors, { separator: "\n" })); + return errorResult( + "This suppression file is a valid yaml but the schema is wrong: " + + suppressionAjv.errorsText(suppressionAjvCompile.errors, { separator: "\n" }), + ); } } diff --git a/eng/tools/sdk-suppressions/src/updateSdkSuppressionsLabel.ts b/eng/tools/sdk-suppressions/src/updateSdkSuppressionsLabel.ts index 747d80567083..9f5c14db693d 100644 --- a/eng/tools/sdk-suppressions/src/updateSdkSuppressionsLabel.ts +++ b/eng/tools/sdk-suppressions/src/updateSdkSuppressionsLabel.ts @@ -1,16 +1,16 @@ -import _ from "lodash"; +import { sdkLabels, SdkName } from "@azure-tools/specs-shared/sdk-types"; import debug from "debug"; import { writeFileSync } from "fs"; +import _ from "lodash"; import { simpleGit } from "simple-git"; -import { sdkLabels, SdkName } from "@azure-tools/specs-shared/sdk-types"; +import { getSDKSuppressionsChangedFiles, parseYamlContent } from "./common.js"; import { - SdkSuppressionsYml, - SdkSuppressionsSection, - sdkSuppressionsFileName, SdkPackageSuppressionsEntry, + sdkSuppressionsFileName, + SdkSuppressionsSection, + SdkSuppressionsYml, validateSdkSuppressionsFile, } from "./sdkSuppressions.js"; -import { getSDKSuppressionsChangedFiles, parseYamlContent } from "./common.js"; // Enable simple-git debug logging to improve console output debug.enable("simple-git"); @@ -52,8 +52,12 @@ export async function getSdkSuppressionsSdkNames( // if the head suppression file is present but anything is wrong like schema error with it return const validateSdkSuppressionsFileResult = validateSdkSuppressionsFile(headSuppressionContent).result; + // If the head suppression file is not valid or empty, we get _sdkNameList with []. if (!validateSdkSuppressionsFileResult) { - return []; + console.log( + `Returned empty SDK name list — head suppression file at ${suppressionFile}/${headCommitHash} is invalid or empty.`, + ); + continue; } // if base suppression file does not exist, set it to an empty object but has correct schema if (!baseSuppressionContent) { @@ -67,10 +71,14 @@ export async function getSdkSuppressionsSdkNames( `${JSON.stringify(headSuppressionContent)} to get different SDK.`, ); - sdkNameList = getSdkNamesWithChangedSuppressions( + let _sdkNameList = getSdkNamesWithChangedSuppressions( headSuppressionContent as SdkSuppressionsYml, baseSuppressionContent as SdkSuppressionsYml, ); + console.log( + `Retrieved SDK names after comparing suppression file ${suppressionFile}: [${_sdkNameList.join(",")}].`, + ); + sdkNameList = [..._sdkNameList, ...sdkNameList]; } } diff --git a/eng/tools/sdk-suppressions/test/updateSdkSuppressionsLabel.test.ts b/eng/tools/sdk-suppressions/test/updateSdkSuppressionsLabel.test.ts index e03c3b570012..1f9f563037ca 100644 --- a/eng/tools/sdk-suppressions/test/updateSdkSuppressionsLabel.test.ts +++ b/eng/tools/sdk-suppressions/test/updateSdkSuppressionsLabel.test.ts @@ -1,6 +1,10 @@ -import { vi, expect, test } from "vitest"; -import { filterSuppressionList, getSdkNamesWithChangedSuppressions, processLabels } from "../src/updateSdkSuppressionsLabel.js"; +import { expect, test, vi } from "vitest"; import { validateSdkSuppressionsFile } from "../src/sdkSuppressions.js"; +import { + filterSuppressionList, + getSdkNamesWithChangedSuppressions, + processLabels, +} from "../src/updateSdkSuppressionsLabel.js"; vi.mock("process", () => ({ exit: vi.fn(), @@ -8,153 +12,154 @@ vi.mock("process", () => ({ test("test filterSuppressionList for only resource-manager files", () => { const changeFiles = [ - "specification/datafactory/resource-manager/Microsoft.DataFactory/stable/2018-06-01/datafactory.json", - "specification/datafactory/resource-manager/sdk-suppressions.yaml" + "specification/datafactory/resource-manager/Microsoft.DataFactory/stable/2018-06-01/datafactory.json", + "specification/datafactory/resource-manager/sdk-suppressions.yaml", ]; const suppressionsFiles: String[] = filterSuppressionList(changeFiles); - expect(suppressionsFiles).toEqual(["specification/datafactory/resource-manager/sdk-suppressions.yaml"]); + expect(suppressionsFiles).toEqual([ + "specification/datafactory/resource-manager/sdk-suppressions.yaml", + ]); }); test("test filterSuppressionList for both tsp files and resource-manager files", () => { const changeFiles = [ - "specification/workloads/Workloads.Operations.Management/main.tsp", - "specification/workloads/Workloads.Operations.Management/sdk-suppressions.yaml", - "specification/workloads/resource-manager/Microsoft.Workloads/operations/preview/2023-10-01-preview/operations.json", - "specification/workloads/resource-manager/Microsoft.Workloads/operations/preview/2024-02-01-preview/operations.json", - "specification/workloads/resource-manager/Microsoft.Workloads/operations/preview/2023-12-01-preview/operations.json", - "specification/workloads/resource-manager/Microsoft.Workloads/operations/stable/2024-09-01/operations.json", - "specification/workloads/resource-manager/sdk-suppressions.yaml" + "specification/workloads/Workloads.Operations.Management/main.tsp", + "specification/workloads/Workloads.Operations.Management/sdk-suppressions.yaml", + "specification/workloads/resource-manager/Microsoft.Workloads/operations/preview/2023-10-01-preview/operations.json", + "specification/workloads/resource-manager/Microsoft.Workloads/operations/preview/2024-02-01-preview/operations.json", + "specification/workloads/resource-manager/Microsoft.Workloads/operations/preview/2023-12-01-preview/operations.json", + "specification/workloads/resource-manager/Microsoft.Workloads/operations/stable/2024-09-01/operations.json", + "specification/workloads/resource-manager/sdk-suppressions.yaml", ]; const suppressionsFiles: String[] = filterSuppressionList(changeFiles); - expect(suppressionsFiles).toEqual(["specification/workloads/Workloads.Operations.Management/sdk-suppressions.yaml"]); + expect(suppressionsFiles).toEqual([ + "specification/workloads/Workloads.Operations.Management/sdk-suppressions.yaml", + ]); }); test("test validateSdkSuppressionsFile for sdk-suppression file", () => { const suppressionContent = { - "suppressions": { - "azure-sdk-for-go": [ - { - "package": "sdk/resourcemanager/appcontainers/armappcontainers", - "breaking-changes": [ - "Field `EndTime`, `StartTime`, `Status`, `Template` of struct `JobExecution` has been removed" - ] - } - ], - "azure-sdk-for-python": [ - { - "package": "azure-mgmt-appcontainers", - "breaking-changes": [ - "Model BillingMeter no longer has parameter system_data" - ] - } - ] - } + suppressions: { + "azure-sdk-for-go": [ + { + package: "sdk/resourcemanager/appcontainers/armappcontainers", + "breaking-changes": [ + "Field `EndTime`, `StartTime`, `Status`, `Template` of struct `JobExecution` has been removed", + ], + }, + ], + "azure-sdk-for-python": [ + { + package: "azure-mgmt-appcontainers", + "breaking-changes": ["Model BillingMeter no longer has parameter system_data"], + }, + ], + }, }; - + const validateResult = validateSdkSuppressionsFile(suppressionContent); - expect(validateResult).toEqual({ result: true, message: 'This suppression file is a valid yaml.' }); + expect(validateResult).toEqual({ + result: true, + message: "This suppression file is a valid yaml.", + }); }); test("test validateSdkSuppressionsFile for empty file", () => { const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - const mockProcessExit = vi.spyOn(process, "exit").mockImplementation(() => { - throw new Error("process.exit called"); // Prevent actual exit - }); - expect(() => validateSdkSuppressionsFile(null)).toThrow("process.exit called"); + expect(validateSdkSuppressionsFile(null)).toEqual({ + result: false, + message: "This suppression file is a empty file", + }); expect(consoleSpy).toHaveBeenCalledWith("Error:", "This suppression file is a empty file"); - expect(mockProcessExit).toHaveBeenCalledWith(1); consoleSpy.mockRestore(); - mockProcessExit.mockRestore(); }); test("test validateSdkSuppressionsFile for undefined file", () => { const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - const mockProcessExit = vi.spyOn(process, "exit").mockImplementation(() => { - throw new Error("process.exit called"); // Prevent actual exit - }); - expect(() => validateSdkSuppressionsFile(undefined)).toThrow("process.exit called"); - expect(consoleSpy).toHaveBeenCalledWith("Error:", "This suppression file is not a valid yaml. Refer to https://aka.ms/azsdk/sdk-suppression for more information."); - expect(mockProcessExit).toHaveBeenCalledWith(1); + expect(validateSdkSuppressionsFile(undefined)).toEqual({ + message: + "This suppression file is not a valid yaml. Refer to https://aka.ms/azsdk/sdk-suppression for more information.", + result: false, + }); + expect(consoleSpy).toHaveBeenCalledWith( + "Error:", + "This suppression file is not a valid yaml. Refer to https://aka.ms/azsdk/sdk-suppression for more information.", + ); consoleSpy.mockRestore(); - mockProcessExit.mockRestore(); }); test("test validateSdkSuppressionsFile for error structor file", () => { const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - const mockProcessExit = vi.spyOn(process, "exit").mockImplementation(() => { - throw new Error("process.exit called"); // Prevent actual exit - }); const suppressionContent = { - "suppressions": { - "azure-sdk-for-go": [ - { - "package": "sdk/resourcemanager/appcontainers/armappcontainers" - } - ], - "azure-sdk-for-python": [ - { - "package": "azure-mgmt-appcontainers", - "breaking-changes": [ - "Model BillingMeter no longer has parameter system_data" - ] - } - ] - } + suppressions: { + "azure-sdk-for-go": [ + { + package: "sdk/resourcemanager/appcontainers/armappcontainers", + }, + ], + "azure-sdk-for-python": [ + { + package: "azure-mgmt-appcontainers", + "breaking-changes": ["Model BillingMeter no longer has parameter system_data"], + }, + ], + }, }; - expect(() => validateSdkSuppressionsFile(suppressionContent)).toThrow("process.exit called"); - expect(consoleSpy).toHaveBeenCalledWith("Error:", "This suppression file is a valid yaml but the schema is wrong: data/suppressions/azure-sdk-for-go/0 must have required property 'breaking-changes'"); - expect(mockProcessExit).toHaveBeenCalledWith(1); + expect(validateSdkSuppressionsFile(suppressionContent)).toEqual({ + message: + "This suppression file is a valid yaml but the schema is wrong: data/suppressions/azure-sdk-for-go/0 must have required property 'breaking-changes'", + result: false, + }); + expect(consoleSpy).toHaveBeenCalledWith( + "Error:", + "This suppression file is a valid yaml but the schema is wrong: data/suppressions/azure-sdk-for-go/0 must have required property 'breaking-changes'", + ); consoleSpy.mockRestore(); - mockProcessExit.mockRestore(); }); test("test getSdkNamesWithChangedSuppressions", () => { const headCont = { - "suppressions": { - "azure-sdk-for-python": [ - { - "package": "azure-mgmt-appcontainers", - "breaking-changes": [ - "Model BillingMeter no longer has parameter system_data AAA" - ] - } - ], - "azure-sdk-for-go": [ - { - "package": "sdk/resourcemanager/appcontainers/armappcontainers", - "breaking-changes": [ - "Field `EndTime`, `StartTime`, `Status`, `Template` of struct `JobExecution` has been removed" - ] - } - ] - } + suppressions: { + "azure-sdk-for-python": [ + { + package: "azure-mgmt-appcontainers", + "breaking-changes": ["Model BillingMeter no longer has parameter system_data AAA"], + }, + ], + "azure-sdk-for-go": [ + { + package: "sdk/resourcemanager/appcontainers/armappcontainers", + "breaking-changes": [ + "Field `EndTime`, `StartTime`, `Status`, `Template` of struct `JobExecution` has been removed", + ], + }, + ], + }, }; const baseCont = { - "suppressions": { + suppressions: { "azure-sdk-for-python": [ { - "package": "azure-mgmt-appcontainers", - "breaking-changes": [ - "Model BillingMeter no longer has parameter system_data" - ] - } + package: "azure-mgmt-appcontainers", + "breaking-changes": ["Model BillingMeter no longer has parameter system_data"], + }, ], "azure-sdk-for-go": [ { - "package": "sdk/resourcemanager/appcontainers/armappcontainers", + package: "sdk/resourcemanager/appcontainers/armappcontainers", "breaking-changes": [ - "Field `EndTime`, `StartTime`, `Status`, `Template` of struct `JobExecution` has been removed" - ] - } - ] - } + "Field `EndTime`, `StartTime`, `Status`, `Template` of struct `JobExecution` has been removed", + ], + }, + ], + }, }; const sdkNames = getSdkNamesWithChangedSuppressions(headCont, baseCont); @@ -165,20 +170,30 @@ test("test processLabels will add new label when has sdkNames", () => { const sdkNames: string[] = ["azure-sdk-for-go", "azure-sdk-for-js"]; const presentLabels: string[] = ["aa", "BreakingChange-Go-Sdk-Suppression"]; const result = processLabels(presentLabels, sdkNames); - expect(result).toEqual({ labelsToAdd: ["BreakingChange-JavaScript-Sdk-Suppression"], labelsToRemove: [] }); - + expect(result).toEqual({ + labelsToAdd: ["BreakingChange-JavaScript-Sdk-Suppression"], + labelsToRemove: [], + }); }); test("test processLabels will remove old label when has the sdkNames not exist", () => { - const sdkNames: string[] = ["azure-sdk-for-js"]; - const presentLabels: string[] = ["aa", "BreakingChange-Go-Sdk-Suppression"]; - const result = processLabels(presentLabels, sdkNames); - expect(result).toEqual({ labelsToAdd: ["BreakingChange-JavaScript-Sdk-Suppression"], labelsToRemove: ["BreakingChange-Go-Sdk-Suppression"] }); - }); + const sdkNames: string[] = ["azure-sdk-for-js"]; + const presentLabels: string[] = ["aa", "BreakingChange-Go-Sdk-Suppression"]; + const result = processLabels(presentLabels, sdkNames); + expect(result).toEqual({ + labelsToAdd: ["BreakingChange-JavaScript-Sdk-Suppression"], + labelsToRemove: ["BreakingChange-Go-Sdk-Suppression"], + }); +}); test("test processLabels will not remove old label when has the sdkNames not exist & has corresponding suppression approved", () => { const sdkNames: string[] = ["azure-sdk-for-go"]; - const presentLabels: string[] = ["aa", "BreakingChange-Go-Sdk-Suppression", "BreakingChange-JavaScript-Sdk-Suppression", "BreakingChange-JavaScript-Sdk-Suppression-Approved"]; + const presentLabels: string[] = [ + "aa", + "BreakingChange-Go-Sdk-Suppression", + "BreakingChange-JavaScript-Sdk-Suppression", + "BreakingChange-JavaScript-Sdk-Suppression-Approved", + ]; const result = processLabels(presentLabels, sdkNames); expect(result).toEqual({ labelsToAdd: [], labelsToRemove: [] }); }); diff --git a/eng/tools/sdk-suppressions/tsconfig.json b/eng/tools/sdk-suppressions/tsconfig.json index 754edaf4714c..5f48d4c6a5b5 100644 --- a/eng/tools/sdk-suppressions/tsconfig.json +++ b/eng/tools/sdk-suppressions/tsconfig.json @@ -3,11 +3,7 @@ "compilerOptions": { "outDir": "./dist", "rootDir": ".", - "allowJs": true + "allowJs": true, }, - "include": [ - "*.ts", - "src/**/*.ts", - "test/**/*.ts", - ], + "include": ["*.ts", "src/**/*.ts", "test/**/*.ts"], } diff --git a/eng/tools/spec-gen-sdk-runner/eslint.config.js b/eng/tools/spec-gen-sdk-runner/eslint.config.js index cc13ff9fc4a3..e5e61e4a84f1 100644 --- a/eng/tools/spec-gen-sdk-runner/eslint.config.js +++ b/eng/tools/spec-gen-sdk-runner/eslint.config.js @@ -88,7 +88,7 @@ const config = tseslint.config( // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-export-from.md "unicorn/prefer-export-from": ["error", { ignoreUsedVariables: true }], }, - } + }, ); export default config; diff --git a/eng/tools/spec-gen-sdk-runner/package.json b/eng/tools/spec-gen-sdk-runner/package.json index c82f7196eb56..7268ae6ff5a8 100644 --- a/eng/tools/spec-gen-sdk-runner/package.json +++ b/eng/tools/spec-gen-sdk-runner/package.json @@ -9,6 +9,9 @@ }, "scripts": { "build": "tsc --build", + "format": "prettier . --ignore-path ../.prettierignore --write", + "format:check": "prettier . --ignore-path ../.prettierignore --check", + "format:check:ci": "prettier . --ignore-path ../.prettierignore --check --log-level debug", "lint": "eslint . -c eslint.config.js --report-unused-disable-directives --max-warnings 0", "lint:fix": "eslint . -c eslint.config.js --fix", "test": "vitest run", @@ -24,6 +27,8 @@ "@vitest/coverage-v8": "^3.0.7", "eslint": "^9.21.0", "eslint-plugin-unicorn": "^59.0.0", + "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "typescript": "~5.8.2", "typescript-eslint": "^8.26.0", "vitest": "^3.0.7" diff --git a/eng/tools/spec-gen-sdk-runner/src/command-helpers.ts b/eng/tools/spec-gen-sdk-runner/src/command-helpers.ts index 65f083ceb0b3..3dbda439e9ac 100644 --- a/eng/tools/spec-gen-sdk-runner/src/command-helpers.ts +++ b/eng/tools/spec-gen-sdk-runner/src/command-helpers.ts @@ -1,14 +1,8 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { - findReadmeFiles, - getArgumentValue, - getAllTypeSpecPaths, - objectToMap, - SpecConfigs, -} from "./utils.js"; import { LogIssueType, LogLevel, logMessage, setVsoVariable, vsoLogIssue } from "./log.js"; +import { groupSpecConfigPaths } from "./spec-helpers.js"; import { APIViewRequestData, SdkName, @@ -17,7 +11,13 @@ import { SpecGenSdkRequiredSettings, VsoLogs, } from "./types.js"; -import { groupSpecConfigPaths } from "./spec-helpers.js"; +import { + findReadmeFiles, + getAllTypeSpecPaths, + getArgumentValue, + objectToMap, + SpecConfigs, +} from "./utils.js"; /** * Load execution-report.json. @@ -263,24 +263,21 @@ export function logIssuesToPipeline(logPath: string, specConfigDisplayText: stri * Process the breaking change label artifacts. * * @param executionReport - The spec-gen-sdk execution report. - * @returns [flag of lable breaking change, breaking change label]. + * @returns flag of lable breaking change. */ -export function getBreakingChangeInfo(executionReport: any): [boolean, string] { - let breakingChangeLabel = ""; +export function getBreakingChangeInfo(executionReport: any): boolean { for (const packageInfo of executionReport.packages) { - breakingChangeLabel = packageInfo.breakingChangeLabel; if (packageInfo.shouldLabelBreakingChange) { - return [true, breakingChangeLabel]; + return true; } } - return [false, breakingChangeLabel]; + return false; } /** * Generate the spec-gen-sdk artifacts. * @param commandInput - The command input. * @param result - The spec-gen-sdk execution result. - * @param breakingChangeLabel - The breaking change label. * @param hasBreakingChange - A flag indicating whether there are breaking changes. * @param hasManagementPlaneSpecs - A flag indicating whether there are management plane specs. * @param stagedArtifactsFolder - The staged artifacts folder. @@ -291,7 +288,6 @@ export function getBreakingChangeInfo(executionReport: any): [boolean, string] { export function generateArtifact( commandInput: SpecGenSdkCmdInput, result: string, - breakingChangeLabel: string, hasBreakingChange: boolean, hasManagementPlaneSpecs: boolean, stagedArtifactsFolder: string, @@ -333,8 +329,6 @@ export function generateArtifact( setVsoVariable("SpecGenSdkArtifactName", specGenSdkArtifactName); setVsoVariable("SpecGenSdkArtifactPath", specGenSdkArtifactPath); setVsoVariable("StagedArtifactsFolder", stagedArtifactsFolder); - setVsoVariable("BreakingChangeLabelAction", hasBreakingChange ? "add" : "remove"); - setVsoVariable("BreakingChangeLabel", breakingChangeLabel); setVsoVariable("HasAPIViewArtifact", apiViewRequestData.length > 0 ? "true" : "false"); } catch (error) { logMessage("Runner: errors occurred while processing breaking change", LogLevel.Group); diff --git a/eng/tools/spec-gen-sdk-runner/src/commands.ts b/eng/tools/spec-gen-sdk-runner/src/commands.ts index 66a254c5b41f..ad4d542a68c9 100644 --- a/eng/tools/spec-gen-sdk-runner/src/commands.ts +++ b/eng/tools/spec-gen-sdk-runner/src/commands.ts @@ -1,9 +1,5 @@ import fs from "node:fs"; import path from "node:path"; -import { runSpecGenSdkCommand, resetGitRepo, SpecConfigs } from "./utils.js"; -import { LogLevel, logMessage, vsoAddAttachment, vsoLogIssue } from "./log.js"; -import { APIViewRequestData, SpecGenSdkCmdInput } from "./types.js"; -import { detectChangedSpecConfigFiles } from "./spec-helpers.js"; import { generateArtifact, getBreakingChangeInfo, @@ -15,6 +11,10 @@ import { prepareSpecGenSdkCommand, setPipelineVariables, } from "./command-helpers.js"; +import { LogLevel, logMessage, vsoAddAttachment, vsoLogIssue } from "./log.js"; +import { detectChangedSpecConfigFiles } from "./spec-helpers.js"; +import { APIViewRequestData, SpecGenSdkCmdInput } from "./types.js"; +import { resetGitRepo, runSpecGenSdkCommand, SpecConfigs } from "./utils.js"; /** * Generate SDK for a single spec. @@ -84,7 +84,6 @@ export async function generateSdkForSpecPr(): Promise { let statusCode = 0; let pushedSpecConfigCount; - let breakingChangeLabel = ""; let executionReport; let changedSpecPathText = ""; let hasManagementPlaneSpecs = false; @@ -164,7 +163,7 @@ export async function generateSdkForSpecPr(): Promise { if (overallExecutionResult !== "failed") { overallExecutionResult = currentExecutionResult; } - [currentRunHasBreakingChange, breakingChangeLabel] = getBreakingChangeInfo(executionReport); + currentRunHasBreakingChange = getBreakingChangeInfo(executionReport); overallRunHasBreakingChange = overallRunHasBreakingChange || currentRunHasBreakingChange; logMessage(`Runner command execution result:${currentExecutionResult}`); } catch (error) { @@ -180,7 +179,6 @@ export async function generateSdkForSpecPr(): Promise { generateArtifact( commandInput, overallExecutionResult, - breakingChangeLabel, overallRunHasBreakingChange, hasManagementPlaneSpecs, stagedArtifactsFolder, diff --git a/eng/tools/spec-gen-sdk-runner/src/index.ts b/eng/tools/spec-gen-sdk-runner/src/index.ts index a66fe5eaa8fe..a14f53c8a409 100644 --- a/eng/tools/spec-gen-sdk-runner/src/index.ts +++ b/eng/tools/spec-gen-sdk-runner/src/index.ts @@ -1,12 +1,12 @@ -import { exit } from "node:process"; -import path from "node:path"; import { existsSync, mkdirSync } from "node:fs"; -import { getArgumentValue } from "./utils.js"; +import path from "node:path"; +import { exit } from "node:process"; import { generateSdkForBatchSpecs, generateSdkForSingleSpec, generateSdkForSpecPr, } from "./commands.js"; +import { getArgumentValue } from "./utils.js"; export async function main() { // Get the arguments passed to the script diff --git a/eng/tools/spec-gen-sdk-runner/src/spec-helpers.ts b/eng/tools/spec-gen-sdk-runner/src/spec-helpers.ts index 8e2596800855..7763aef2f9bc 100644 --- a/eng/tools/spec-gen-sdk-runner/src/spec-helpers.ts +++ b/eng/tools/spec-gen-sdk-runner/src/spec-helpers.ts @@ -1,33 +1,148 @@ import path from "node:path"; +import { logMessage } from "./log.js"; +import { SpecGenSdkCmdInput } from "./types.js"; import { + createCombinedSpecs, getChangedFiles, + getLastPathSegment, + groupPathsByService, searchRelatedParentFolders, - searchSharedLibrary, searchRelatedTypeSpecProjectBySharedLibrary, - groupPathsByService, - createCombinedSpecs, - type SpecResults, + searchSharedLibrary, type ChangedSpecs, type SpecConfigs, - getLastPathSegment, + type SpecResults, } from "./utils.js"; -import { logMessage } from "./log.js"; -import { SpecGenSdkCmdInput } from "./types.js"; export const readmeMdRegex = /^readme.md$/; export const typespecProjectRegex = /^tspconfig.yaml$/; export const typespecProjectSharedLibraryRegex = /[^/]+\.Shared/; +/** + * Processes typespec projects that follow the resource-manager or data-plane folder structure + * and matches them with corresponding readme files if they exist in the same folder. + * @param readmeMDResult - Object mapping folder paths to readme file paths + * @param typespecProjectResult - Object mapping folder paths to typespec project file paths + * @returns An array of ChangedSpecs objects containing the paths to the readme and TypeSpec config files + */ +export function processTypeSpecProjectsV2FolderStructure( + readmeMDResult: { [folderPath: string]: string[] }, + typespecProjectResult: { [folderPath: string]: string[] }, +): ChangedSpecs[] { + const changedSpecs: ChangedSpecs[] = []; + + // Iterate through each typespec project folder + for (const folderPath of Object.keys(typespecProjectResult)) { + // Split the path into segments to check for specific components + const segments = folderPath.split(/[/\\]/); + // Check if the folder path contains resource-manager or data-plane segments + if (segments.includes("resource-manager") || segments.includes("data-plane")) { + const cs: ChangedSpecs = { + specs: [], + }; + + // Set the typespec project path + cs.typespecProject = path.join(folderPath, "tspconfig.yaml"); + // Initialize the specs array with typespec project files + cs.specs = [...typespecProjectResult[folderPath]]; // Check if the same folder has a readme.md file + if (readmeMDResult[folderPath]) { + cs.readmeMd = path.join(folderPath, "readme.md"); + // Merge the specs arrays, removing duplicates + cs.specs = [...new Set([...cs.specs, ...readmeMDResult[folderPath]])]; + // Remove the processed entry from readmeMDResult + delete readmeMDResult[folderPath]; + } + + // Add the ChangedSpecs object to the result array + changedSpecs.push(cs); + // Remove the processed entry from typespecProjectResult + delete typespecProjectResult[folderPath]; + + // Delete readme entries that match specific folder structure patterns and are in the same parent folder hierarchy + // such as: + // "specification/service/data-plane" + // "specification/service/resource-manager" + // "specification/service/resource-manager/Microsoft.Service" + for (const readmePath of Object.keys(readmeMDResult)) { + // Split the paths into segments to work with path components rather than raw strings with separators + const folderSegments = folderPath.split(/[/\\]/); // Split on either / or \ + const readmeSegments = readmePath.split(/[/\\]/); + + // Find the position of "resource-manager" or "data-plane" in folder segments + const rmIndex = folderSegments.indexOf("resource-manager"); + const dpIndex = folderSegments.indexOf("data-plane"); + + // For resource-manager paths + if (rmIndex !== -1) { + // Get the service path segments (everything before resource-manager) + const serviceSegments = folderSegments.slice(0, rmIndex); + // Check if readmePath shares the same service prefix + const isRelatedService = serviceSegments.every( + (segment, i) => i < readmeSegments.length && readmeSegments[i] === segment, + ); + + if (isRelatedService) { + // Case 1: Readme ends with resource-manager + // Example: specification/service/resource-manager + if ( + readmeSegments.length === rmIndex + 1 && + readmeSegments[rmIndex] === "resource-manager" + ) { + logMessage(`\t Removing related readme: ${readmePath} for folder: ${folderPath}`); + delete readmeMDResult[readmePath]; + continue; + } + + // Case 2: Readme is one level down from resource-manager + // Example: specification/service/resource-manager/Microsoft.Service + if ( + readmeSegments.length === rmIndex + 2 && + readmeSegments[rmIndex] === "resource-manager" && + folderSegments.length > rmIndex + 1 && + folderSegments[rmIndex + 1] === readmeSegments[rmIndex + 1] + ) { + logMessage(`\t Removing related readme: ${readmePath} for folder: ${folderPath}`); + delete readmeMDResult[readmePath]; + continue; + } + } + } + // For data-plane paths + else if (dpIndex !== -1) { + // Get the service path segments (everything before data-plane) + const serviceSegments = folderSegments.slice(0, dpIndex); + // Check if readmePath shares the same service prefix and ends with data-plane + const isRelatedService = serviceSegments.every( + (segment, i) => i < readmeSegments.length && readmeSegments[i] === segment, + ); + + if ( + isRelatedService && + readmeSegments.length === dpIndex + 1 && + readmeSegments[dpIndex] === "data-plane" + ) { + logMessage(`\t Removing related readme: ${readmePath} for folder: ${folderPath}`); + delete readmeMDResult[readmePath]; + } + } + } + } + } + + return changedSpecs; +} + export function detectChangedSpecConfigFiles(commandInput: SpecGenSdkCmdInput): ChangedSpecs[] { const prChangedFiles: string[] = getChangedFiles(commandInput.localSpecRepoPath) ?? []; if (prChangedFiles.length === 0) { logMessage("No files changed in the PR"); } - logMessage(`Changed files in the PR: ${prChangedFiles.length}`); - for (const file of prChangedFiles) { + const normalizedChangedFiles = prChangedFiles.map((f) => f.replaceAll("\\", "/")); + logMessage(`Changed files in the PR: ${normalizedChangedFiles.length}`); + for (const file of normalizedChangedFiles) { logMessage(`\t${file}`); } - const fileList = prChangedFiles + const fileList = normalizedChangedFiles .filter((p) => p.startsWith("specification/")) .filter((p) => !p.includes("/scenarios/")); @@ -75,88 +190,105 @@ export function detectChangedSpecConfigFiles(commandInput: SpecGenSdkCmdInput): } } - // Group paths by service - const serviceMap = groupPathsByService(readmeMDResult, typespecProjectResult); + // Process TypeSpec projects with the V2 folder structure + const newFolderStructureSpecs = processTypeSpecProjectsV2FolderStructure( + readmeMDResult, + typespecProjectResult, + ); - const results: SpecResults = { readmeMDResult, typespecProjectResult }; + if (newFolderStructureSpecs.length > 0) { + logMessage(`Found ${newFolderStructureSpecs.length} specs with the new folder structure`); + changedSpecs.push(...newFolderStructureSpecs); + for (const spec of newFolderStructureSpecs) { + logMessage(`\t\t tspconfig: ${spec.typespecProject}, readme: ${spec.readmeMd}`); + } + } - // Process each service - for (const [, info] of serviceMap) { - // Case: Resource Manager with .Management - if (info.managementPaths.length > 0) { - if (info.resourceManagerPaths.length === 1) { - // Single resource-manager path - match with all Management paths - const newSpecs = createCombinedSpecs( - info.resourceManagerPaths[0].path, - info.managementPaths, - results, - ); - changedSpecs.push(...newSpecs); - logMessage( - `\t readme folders: ${info.resourceManagerPaths[0].path}, tspconfig folders: ${info.managementPaths}`, - ); - for (const p of info.managementPaths) { - delete typespecProjectResult[p]; - } - delete readmeMDResult[info.resourceManagerPaths[0].path]; - } else { - // Multiple resource-manager paths - match by subfolder name - for (const rmPath of info.resourceManagerPaths) { - const matchingManagements = info.managementPaths.filter((mPath) => { - const rmSubPath = rmPath.subPath; - const managementName = getLastPathSegment(mPath).replace(".Management", ""); - return rmSubPath && rmSubPath === managementName; - }); - if (matchingManagements.length > 0) { - const newSpecs = createCombinedSpecs(rmPath.path, matchingManagements, results); - changedSpecs.push(...newSpecs); - logMessage( - `\t readme folders: ${rmPath.path}, tspconfig folders: ${matchingManagements}`, - ); - for (const p of matchingManagements) { - delete typespecProjectResult[p]; + // Process TypeSpec projects with the old folder structure + if (Object.keys(readmeMDResult).length > 0 && Object.keys(typespecProjectResult).length > 0) { + // Group paths by service + const serviceMap = groupPathsByService(readmeMDResult, typespecProjectResult); + + const results: SpecResults = { readmeMDResult, typespecProjectResult }; + + // Process each service + for (const [, info] of serviceMap) { + // Case: Resource Manager with .Management + if (info.managementPaths.length > 0) { + if (info.resourceManagerPaths.length === 1) { + // Single resource-manager path - match with all Management paths + const newSpecs = createCombinedSpecs( + info.resourceManagerPaths[0].path, + info.managementPaths, + results, + ); + changedSpecs.push(...newSpecs); + logMessage( + `\t readme folders: ${info.resourceManagerPaths[0].path}, tspconfig folders: ${info.managementPaths}`, + ); + for (const p of info.managementPaths) { + delete typespecProjectResult[p]; + } + delete readmeMDResult[info.resourceManagerPaths[0].path]; + } else { + // Multiple resource-manager paths - match by subfolder name + for (const rmPath of info.resourceManagerPaths) { + const matchingManagements = info.managementPaths.filter((mPath) => { + const rmSubPath = rmPath.subPath; + const managementName = getLastPathSegment(mPath).replace(".Management", ""); + return rmSubPath && rmSubPath === managementName; + }); + if (matchingManagements.length > 0) { + const newSpecs = createCombinedSpecs(rmPath.path, matchingManagements, results); + changedSpecs.push(...newSpecs); + logMessage( + `\t readme folders: ${rmPath.path}, tspconfig folders: ${matchingManagements}`, + ); + for (const p of matchingManagements) { + delete typespecProjectResult[p]; + } + delete readmeMDResult[rmPath.path]; } - delete readmeMDResult[rmPath.path]; } } } - } - // Case: Data Plane matching - if (info.dataPlanePaths.length > 0 && info.otherTypeSpecPaths.length > 0) { - if (info.dataPlanePaths.length === 1) { - // Single data-plane path - match with all non-Management TypeSpec paths - const newSpecs = createCombinedSpecs( - info.dataPlanePaths[0].path, - info.otherTypeSpecPaths, - results, - ); - changedSpecs.push(...newSpecs); - logMessage( - `\t readme folders: ${info.dataPlanePaths[0].path}, tspconfig folders: ${info.otherTypeSpecPaths}`, - ); - for (const p of info.otherTypeSpecPaths) { - delete typespecProjectResult[p]; - } - delete readmeMDResult[info.dataPlanePaths[0].path]; - } else { - // Multiple data-plane paths - match by subfolder name - for (const dpPath of info.dataPlanePaths) { - const matchingTypeSpecs = info.otherTypeSpecPaths.filter((tsPath) => { - const dpSubFolder = dpPath.subFolder; - const tsLastSegment = getLastPathSegment(tsPath); - return dpSubFolder && dpSubFolder === tsLastSegment; - }); - if (matchingTypeSpecs.length > 0) { - const newSpecs = createCombinedSpecs(dpPath.path, matchingTypeSpecs, results); - changedSpecs.push(...newSpecs); - logMessage( - `\t readme folders: ${dpPath.path}, tspconfig folders: ${matchingTypeSpecs}`, - ); - for (const p of matchingTypeSpecs) { - delete typespecProjectResult[p]; + // Case: Data Plane matching + if (info.dataPlanePaths.length > 0 && info.otherTypeSpecPaths.length > 0) { + if (info.dataPlanePaths.length === 1) { + // Single data-plane path - match with all non-Management TypeSpec paths + const newSpecs = createCombinedSpecs( + info.dataPlanePaths[0].path, + info.otherTypeSpecPaths, + results, + ); + changedSpecs.push(...newSpecs); + logMessage( + `\t readme folders: ${info.dataPlanePaths[0].path}, tspconfig folders: ${info.otherTypeSpecPaths}`, + ); + for (const p of info.otherTypeSpecPaths) { + delete typespecProjectResult[p]; + } + delete readmeMDResult[info.dataPlanePaths[0].path]; + } else { + // Multiple data-plane paths - match by subfolder name + for (const dpPath of info.dataPlanePaths) { + const matchingTypeSpecs = info.otherTypeSpecPaths.filter((tsPath) => { + const dpSubFolder = dpPath.subFolder; + const tsLastSegment = getLastPathSegment(tsPath); + return dpSubFolder && dpSubFolder === tsLastSegment; + }); + if (matchingTypeSpecs.length > 0) { + const newSpecs = createCombinedSpecs(dpPath.path, matchingTypeSpecs, results); + changedSpecs.push(...newSpecs); + logMessage( + `\t readme folders: ${dpPath.path}, tspconfig folders: ${matchingTypeSpecs}`, + ); + for (const p of matchingTypeSpecs) { + delete typespecProjectResult[p]; + } + delete readmeMDResult[dpPath.path]; } - delete readmeMDResult[dpPath.path]; } } } diff --git a/eng/tools/spec-gen-sdk-runner/src/types.ts b/eng/tools/spec-gen-sdk-runner/src/types.ts index c91588e23ad8..e50d63947b77 100644 --- a/eng/tools/spec-gen-sdk-runner/src/types.ts +++ b/eng/tools/spec-gen-sdk-runner/src/types.ts @@ -89,7 +89,7 @@ export const SpecGenSdkRequiredSettings: Record = { }, "azure-sdk-for-js": { dataPlane: false, - managementPlane: false, + managementPlane: true, }, "azure-sdk-for-net": { dataPlane: false, diff --git a/eng/tools/spec-gen-sdk-runner/src/utils.ts b/eng/tools/spec-gen-sdk-runner/src/utils.ts index 3c5c2766b7b9..55ac719e30c9 100644 --- a/eng/tools/spec-gen-sdk-runner/src/utils.ts +++ b/eng/tools/spec-gen-sdk-runner/src/utils.ts @@ -1,8 +1,8 @@ -import { spawn, spawnSync, exec } from "node:child_process"; -import path from "node:path"; +import { exec, spawn, spawnSync } from "node:child_process"; import fs from "node:fs"; -import { LogLevel, logMessage } from "./log.js"; +import path from "node:path"; import { promisify } from "node:util"; +import { LogLevel, logMessage } from "./log.js"; type Dirent = fs.Dirent; @@ -177,8 +177,9 @@ export function getChangedFiles( specRepoPath: string, baseCommitish: string = "HEAD^", targetCommitish: string = "HEAD", - diffFilter: string = "d", ): string[] | undefined { + // set diff filter to include added, copied, modified, deleted, renamed, and type changed files + const diffFilter = "ACMDRT"; const scriptPath = path.resolve(specRepoPath, "eng/scripts/ChangedFiles-Functions.ps1"); const args = [ "-Command", @@ -223,7 +224,9 @@ export function findParentWithFile( return undefined; } currentPath = path.dirname(currentPath); - if (stopAtFolder && currentPath === stopAtFolder) { + // Check if we've reached the root of the path (stopAtFolder) or + // if we've reached '.' which prevents infinite loops with path.dirname('.') + if ((stopAtFolder && currentPath === stopAtFolder) || currentPath === ".") { return undefined; } } @@ -407,7 +410,7 @@ export function groupPathsByService( } const info = serviceMap.get(serviceName)!; - if (folderPath.endsWith(".Management")) { + if (folderPath.endsWith(".Management") || folderPath.includes("resource-manager")) { info.managementPaths.push(folderPath); } else { info.otherTypeSpecPaths.push(folderPath); diff --git a/eng/tools/spec-gen-sdk-runner/test/command-helpers.test.ts b/eng/tools/spec-gen-sdk-runner/test/command-helpers.test.ts index c433ead24894..09a42dc67ab5 100644 --- a/eng/tools/spec-gen-sdk-runner/test/command-helpers.test.ts +++ b/eng/tools/spec-gen-sdk-runner/test/command-helpers.test.ts @@ -1,22 +1,22 @@ -import { describe, test, expect, vi, beforeEach } from "vitest"; -import * as log from "../src/log.js"; -import * as utils from "../src/utils.js"; -import * as specHelpers from "../src/spec-helpers.js"; -import { fileURLToPath } from "node:url"; import fs from "node:fs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import { + generateArtifact, getBreakingChangeInfo, getRequiredSettingValue, getSpecPaths, logIssuesToPipeline, parseArguments, prepareSpecGenSdkCommand, - generateArtifact, setPipelineVariables, } from "../src/command-helpers.js"; +import * as log from "../src/log.js"; import { LogLevel } from "../src/log.js"; +import * as specHelpers from "../src/spec-helpers.js"; import { APIViewRequestData } from "../src/types.js"; +import * as utils from "../src/utils.js"; // Get the absolute path to the repo root const currentFilePath = fileURLToPath(import.meta.url); @@ -368,22 +368,22 @@ describe("commands.ts", () => { describe("getBreakingChangeInfo", () => { test("should return breaking change info if applicable", () => { const mockExecutionReport = { - packages: [{ shouldLabelBreakingChange: true, breakingChangeLabel: "breaking-change" }], + packages: [{ shouldLabelBreakingChange: true }], }; const result = getBreakingChangeInfo(mockExecutionReport); - expect(result).toEqual([true, "breaking-change"]); + expect(result).toBe(true); }); test("should return no breaking change info if not applicable", () => { const mockExecutionReport = { - packages: [{ shouldLabelBreakingChange: false, breakingChangeLabel: "" }], + packages: [{ shouldLabelBreakingChange: false }], }; const result = getBreakingChangeInfo(mockExecutionReport); - expect(result).toEqual([false, ""]); + expect(result).toBe(false); }); test("should return no breaking change info if not executionReport", () => { @@ -393,7 +393,7 @@ describe("commands.ts", () => { const result = getBreakingChangeInfo(mockExecutionReport); - expect(result).toEqual([false, ""]); + expect(result).toBe(false); }); }); @@ -426,7 +426,6 @@ describe("commands.ts", () => { specRepoHttpsUrl: "", }; const mockResult = "succeeded"; - const mockBreakingchangeLabel = "breaking-change"; const mockhasBreakingChange = false; const mockhasManagementPlaneSpecs = false; const mockStagedArtifactsFolder = "mockStagedArtifactsFolder"; @@ -434,7 +433,6 @@ describe("commands.ts", () => { const result = generateArtifact( mockCommandInput, mockResult, - mockBreakingchangeLabel, mockhasBreakingChange, mockhasManagementPlaneSpecs, mockStagedArtifactsFolder, @@ -478,8 +476,6 @@ describe("commands.ts", () => { "StagedArtifactsFolder", "mockStagedArtifactsFolder", ); - expect(log.setVsoVariable).toHaveBeenCalledWith("BreakingChangeLabelAction", "remove"); - expect(log.setVsoVariable).toHaveBeenCalledWith("BreakingChangeLabel", "breaking-change"); expect(log.setVsoVariable).toHaveBeenCalledWith("HasAPIViewArtifact", "false"); }); @@ -507,7 +503,6 @@ describe("commands.ts", () => { }; const mockResult = "failed"; - const mockBreakingchangeLabel = "breaking-change"; const mockhasBreakingChange = false; const mockhasManagementPlaneSpecs = false; const mockStagedArtifactsFolder = ""; @@ -515,7 +510,6 @@ describe("commands.ts", () => { const result = generateArtifact( mockCommandInput, mockResult, - mockBreakingchangeLabel, mockhasBreakingChange, mockhasManagementPlaneSpecs, mockStagedArtifactsFolder, @@ -553,7 +547,6 @@ describe("commands.ts", () => { specRepoHttpsUrl: "", }; const mockResult = "succeeded"; - const mockBreakingchangeLabel = "breaking-change"; const mockhasBreakingChange = false; // Using true for hasManagementPlaneSpecs, which would normally make isSpecGenSdkCheckRequired=true // for Go SDK (as tested in the getRequiredSettingValue tests) @@ -565,7 +558,6 @@ describe("commands.ts", () => { const result = generateArtifact( mockCommandInput, mockResult, - mockBreakingchangeLabel, mockhasBreakingChange, mockhasManagementPlaneSpecs, mockStagedArtifactsFolder, @@ -605,8 +597,8 @@ describe("commands.ts", () => { // Based on the constants in types.ts, Go SDK requires check for management plane expect(result).toBe(true); - const result2 = getRequiredSettingValue(true, "azure-sdk-for-js"); - // Based on the constants in types.ts, JS SDK does not require check for management plane + const result2 = getRequiredSettingValue(true, "azure-sdk-for-net"); + // Based on the constants in types.ts, .NET SDK does not require check for management plane expect(result2).toBe(false); }); diff --git a/eng/tools/spec-gen-sdk-runner/test/commands.test.ts b/eng/tools/spec-gen-sdk-runner/test/commands.test.ts index 6301859d5e70..26c02c92fdee 100644 --- a/eng/tools/spec-gen-sdk-runner/test/commands.test.ts +++ b/eng/tools/spec-gen-sdk-runner/test/commands.test.ts @@ -1,16 +1,16 @@ -import { describe, test, expect, vi, beforeEach, type Mock } from "vitest"; -import * as utils from "../src/utils.js"; +import fs from "node:fs"; +import path from "node:path"; +import { beforeEach, describe, expect, test, vi, type Mock } from "vitest"; +import * as commandHelpers from "../src/command-helpers.js"; import { generateSdkForBatchSpecs, generateSdkForSingleSpec, generateSdkForSpecPr, } from "../src/commands.js"; -import * as commandHelpers from "../src/command-helpers.js"; import * as log from "../src/log.js"; -import * as changeFiles from "../src/spec-helpers.js"; -import fs from "node:fs"; -import path from "node:path"; import { LogLevel } from "../src/log.js"; +import * as changeFiles from "../src/spec-helpers.js"; +import * as utils from "../src/utils.js"; function getNormalizedFsCalls(mockFn: Mock): unknown[][] { return mockFn.mock.calls.map((args: unknown[]) => { @@ -209,7 +209,7 @@ describe("generateSdkForSpecPr", () => { vi.spyOn(utils, "resetGitRepo").mockResolvedValue(undefined); vi.spyOn(utils, "runSpecGenSdkCommand").mockResolvedValue(undefined); vi.spyOn(commandHelpers, "getExecutionReport").mockReturnValue(mockExecutionReport); - vi.spyOn(commandHelpers, "getBreakingChangeInfo").mockReturnValue([false, ""]); + vi.spyOn(commandHelpers, "getBreakingChangeInfo").mockReturnValue(false); vi.spyOn(commandHelpers, "generateArtifact").mockReturnValue(0); vi.spyOn(commandHelpers, "logIssuesToPipeline").mockImplementation(() => { // mock implementation intentionally left blank @@ -239,7 +239,6 @@ describe("generateSdkForSpecPr", () => { expect(commandHelpers.generateArtifact).toHaveBeenCalledWith( mockCommandInput, "succeeded", // overallExecutionResult - "", // breakingChangeLabel false, // overallRunHasBreakingChange true, // hasManagementPlaneSpecs "", // stagedArtifactsFolder @@ -282,7 +281,6 @@ describe("generateSdkForSpecPr", () => { expect(generateArtifactSpy).toHaveBeenCalledWith( mockCommandInput, "succeeded", // overallExecutionResult should be set to "succeeded" - "", // breakingChangeLabel false, // overallRunHasBreakingChange false, // hasManagementPlaneSpecs "", // stagedArtifactsFolder @@ -321,7 +319,6 @@ describe("generateSdkForSpecPr", () => { expect(commandHelpers.generateArtifact).toHaveBeenCalledWith( mockCommandInput, "", // overallExecutionResult is empty because no spec was actually processed - "", // breakingChangeLabel false, // overallRunHasBreakingChange false, // hasManagementPlaneSpecs "", // stagedArtifactsFolder diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/readme.md b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/readme.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/client.tsp b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/client.tsp new file mode 100644 index 000000000000..be91eaf5c748 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/client.tsp @@ -0,0 +1,7 @@ +import "./main.tsp"; +import "@azure-tools/typespec-client-generator-core"; + +using Widget; +using Azure.ClientGenerator.Core; + +@@clientName(Widgets, "AzureWidgets", "csharp"); diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/examples/2022-12-01/Widgets_CreateOrUpdateWidgetSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/examples/2022-12-01/Widgets_CreateOrUpdateWidgetSample.json new file mode 100644 index 000000000000..dbd333fb52dc --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/examples/2022-12-01/Widgets_CreateOrUpdateWidgetSample.json @@ -0,0 +1,37 @@ +{ + "title": "Widgets_CreateOrUpdateWidget", + "operationId": "Widgets_CreateOrUpdateWidget", + "parameters": { + "widgetName": "name1", + "api-version": "2022-12-01", + "resource": { + "manufacturerId": "manufacturer id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + } + }, + "responses": { + "200": { + "body": { + "name": "name1", + "manufacturerId": "manufacturer id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + } + }, + "201": { + "body": { + "name": "name1", + "manufacturerId": "manufacturer id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/examples/2022-12-01/Widgets_DeleteWidgetSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/examples/2022-12-01/Widgets_DeleteWidgetSample.json new file mode 100644 index 000000000000..c5de7719085d --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/examples/2022-12-01/Widgets_DeleteWidgetSample.json @@ -0,0 +1,29 @@ +{ + "operationId": "Widgets_DeleteWidget", + "title": "Delete widget by widget name using long-running operation.", + "parameters": { + "api-version": "2022-12-01", + "widgetName": "searchbox" + }, + "responses": { + "202": { + "headers": { + "location": "https://contosowidgetmanager.azure.com/operations/00000000-0000-0000-0000-000000000123/result?api-version=2022-12-01", + "operation-location": "https://contosowidgetmanager.azure.com/operations/00000000-0000-0000-0000-000000000123?api-version=2022-12-01" + }, + "body": { + "id": "id1", + "status": "deleted" + } + }, + "default": { + "body": { + "error": { + "code": "Error code", + "message": "Error message", + "details": [] + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/examples/2022-12-01/Widgets_GetWidgetOperationStatusSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/examples/2022-12-01/Widgets_GetWidgetOperationStatusSample.json new file mode 100644 index 000000000000..a840b19f26e6 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/examples/2022-12-01/Widgets_GetWidgetOperationStatusSample.json @@ -0,0 +1,45 @@ +{ + "title": "Widgets_GetWidgetOperationStatus", + "operationId": "Widgets_GetWidgetOperationStatus", + "parameters": { + "widgetName": "name1", + "operationId": "opreation id1", + "api-version": "2022-12-01" + }, + "responses": { + "200": { + "body": { + "id": "opreation id1", + "status": "InProgress", + "error": { + "code": "Error code", + "message": "Error message", + "target": "op1", + "details": [ + { + "code": "code1", + "message": "message1", + "target": "op1", + "details": [], + "innererror": { + "code": "code1" + } + } + ], + "innererror": { + "code": "code1" + } + }, + "result": { + "name": "bingsearch", + "manufacturerId": "manufacturer Id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + }, + "widgetName": "rfazvwnfwwomiwrh" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/examples/2022-12-01/Widgets_GetWidgetSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/examples/2022-12-01/Widgets_GetWidgetSample.json new file mode 100644 index 000000000000..ecab18c65303 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/examples/2022-12-01/Widgets_GetWidgetSample.json @@ -0,0 +1,25 @@ +{ + "operationId": "Widgets_GetWidget", + "title": "Get widget by widget name.", + "parameters": { + "api-version": "2022-12-01", + "widgetName": "searchbox" + }, + "responses": { + "200": { + "body": { + "name": "bingsearch", + "manufacturerId": "a-22-01" + } + }, + "default": { + "body": { + "error": { + "code": "Error code", + "message": "Error message", + "details": [] + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/examples/2022-12-01/Widgets_ListWidgetsSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/examples/2022-12-01/Widgets_ListWidgetsSample.json new file mode 100644 index 000000000000..fa68ab418490 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/examples/2022-12-01/Widgets_ListWidgetsSample.json @@ -0,0 +1,27 @@ +{ + "title": "Widgets_ListWidgets", + "operationId": "Widgets_ListWidgets", + "parameters": { + "top": 8, + "skip": 15, + "maxpagesize": 27, + "api-version": "2022-12-01" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "name": "bingsearch", + "manufacturerId": "manufacturer Id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + } + ], + "nextLink": "https://microsoft.com/a" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/main.tsp b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/main.tsp new file mode 100644 index 000000000000..a30b708fdb62 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/main.tsp @@ -0,0 +1,61 @@ +import "@typespec/http"; +import "@typespec/rest"; +import "@typespec/versioning"; +import "@azure-tools/typespec-azure-core"; +import "./shared.tsp"; + +using TypeSpec.Http; +using TypeSpec.Rest; +using TypeSpec.Versioning; +using Azure.Core; + +@useAuth(AadOauth2Auth<["https://azure.com/.default"]>) +@service(#{ title: "Widget" }) +@versioned(Widget.Versions) +namespace Widget; + +@doc("Versions info.") +enum Versions { + @doc("The 2022-12-01 version.") + @useDependency(Azure.Core.Versions.v1_0_Preview_1) + v2022_12_01: "2022-12-01", +} + +@doc("A widget.") +@resource("widgets") +model WidgetSuite { + @key("widgetName") + @doc("The widget name.") + @visibility(Lifecycle.Read) + name: string; + + @doc("The ID of the widget's manufacturer.") + manufacturerId: string; + + @doc("The faked shared model.") + sharedModel?: FakedSharedModel; +} + +interface Widgets { + @doc("Fetch a Widget by name.") + getWidget is ResourceRead; + + @doc("Gets status of a Widget operation.") + getWidgetOperationStatus is GetResourceOperationStatus; + + @doc("Creates or updates a Widget asynchronously.") + @pollingOperation(Widgets.getWidgetOperationStatus) + createOrUpdateWidget is StandardResourceOperations.LongRunningResourceCreateOrUpdate; + + @doc("Delete a Widget asynchronously.") + @pollingOperation(Widgets.getWidgetOperationStatus) + deleteWidget is LongRunningResourceDelete; + + @doc("List Widget resources") + listWidgets is ResourceList< + WidgetSuite, + { + parameters: StandardListQueryParameters; + } + >; +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/readme.md b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/readme.md new file mode 100644 index 000000000000..6daa7d4c93ee --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/readme.md @@ -0,0 +1,78 @@ +# Widget + +> see https://aka.ms/autorest + +This is the AutoRest configuration file for Widget. + +## Configuration + +### Basic Information + +This is a TypeSpec project so we only want to readme to default the default tag and point to the outputted swagger file. +This is used for some tools such as doc generation and swagger apiview generation it isn't used for SDK code gen as we +use the native TypeSpec code generation configured in the tspconfig.yaml file. + +```yaml +openapi-type: data-plane +tag: package-2022-12-01 +``` + +### Tag: package-2022-12-01 + +These settings apply only when `--tag=package-2022-12-01` is specified on the command line. + +```yaml $(tag) == 'package-2022-12-01' +input-file: + - stable/2022-12-01/widgets.json +``` + +### Suppress non-TypeSpec SDK related linting rules + +These set of linting rules aren't applicable to the new TypeSpec SDK code generators so suppressing them here. Eventually we will +opt-out these rules from running in the linting tools for TypeSpec generated swagger files. + +```yaml +suppressions: + - code: AvoidAnonymousTypes + - code: PatchInOperationName + - code: OperationIdNounVerb + - code: RequiredReadOnlyProperties + - code: SchemaNamesConvention + - code: SchemaDescriptionOrTitle +``` + +### Tag: package-2022-11-01-preview + +These settings apply only when `--tag=package-2022-11-01-preview` is specified on the command line. + +```yaml $(tag) == 'package-2022-11-01-preview' +input-file: + - preview/2022-11-01-preview/widgets.json +``` + +### Suppress non-TypeSpec SDK related linting rules + +These set of linting rules aren't applicable to the new TypeSpec SDK code generators so suppressing them here. Eventually we will +opt-out these rules from running in the linting tools for TypeSpec generated swagger files. + +```yaml +suppressions: + - code: AvoidAnonymousTypes + - code: PatchInOperationName + - code: OperationIdNounVerb + - code: RequiredReadOnlyProperties + - code: SchemaNamesConvention + - code: SchemaDescriptionOrTitle +``` + +### Suppress rules that might be fixed + +These set of linting rules we expect to fixed in typespec-autorest emitter but for now suppressing. +Github issue filed at https://github.com/Azure/typespec-azure/issues/2762 + +```yaml +suppressions: + - code: LroExtension + - code: SchemaTypeAndFormat + - code: PathParameterSchema +``` diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/shared.tsp b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/shared.tsp new file mode 100644 index 000000000000..1b94bb705031 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/shared.tsp @@ -0,0 +1,8 @@ +@doc("Faked shared model") +model FakedSharedModel { + @doc("The tag.") + tag: string; + + @doc("The created date.") + createdAt: utcDateTime; +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/stable/2022-12-01/examples/Widgets_CreateOrUpdateWidgetSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/stable/2022-12-01/examples/Widgets_CreateOrUpdateWidgetSample.json new file mode 100644 index 000000000000..dbd333fb52dc --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/stable/2022-12-01/examples/Widgets_CreateOrUpdateWidgetSample.json @@ -0,0 +1,37 @@ +{ + "title": "Widgets_CreateOrUpdateWidget", + "operationId": "Widgets_CreateOrUpdateWidget", + "parameters": { + "widgetName": "name1", + "api-version": "2022-12-01", + "resource": { + "manufacturerId": "manufacturer id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + } + }, + "responses": { + "200": { + "body": { + "name": "name1", + "manufacturerId": "manufacturer id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + } + }, + "201": { + "body": { + "name": "name1", + "manufacturerId": "manufacturer id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/stable/2022-12-01/examples/Widgets_DeleteWidgetSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/stable/2022-12-01/examples/Widgets_DeleteWidgetSample.json new file mode 100644 index 000000000000..c5de7719085d --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/stable/2022-12-01/examples/Widgets_DeleteWidgetSample.json @@ -0,0 +1,29 @@ +{ + "operationId": "Widgets_DeleteWidget", + "title": "Delete widget by widget name using long-running operation.", + "parameters": { + "api-version": "2022-12-01", + "widgetName": "searchbox" + }, + "responses": { + "202": { + "headers": { + "location": "https://contosowidgetmanager.azure.com/operations/00000000-0000-0000-0000-000000000123/result?api-version=2022-12-01", + "operation-location": "https://contosowidgetmanager.azure.com/operations/00000000-0000-0000-0000-000000000123?api-version=2022-12-01" + }, + "body": { + "id": "id1", + "status": "deleted" + } + }, + "default": { + "body": { + "error": { + "code": "Error code", + "message": "Error message", + "details": [] + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/stable/2022-12-01/examples/Widgets_GetWidgetOperationStatusSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/stable/2022-12-01/examples/Widgets_GetWidgetOperationStatusSample.json new file mode 100644 index 000000000000..a840b19f26e6 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/stable/2022-12-01/examples/Widgets_GetWidgetOperationStatusSample.json @@ -0,0 +1,45 @@ +{ + "title": "Widgets_GetWidgetOperationStatus", + "operationId": "Widgets_GetWidgetOperationStatus", + "parameters": { + "widgetName": "name1", + "operationId": "opreation id1", + "api-version": "2022-12-01" + }, + "responses": { + "200": { + "body": { + "id": "opreation id1", + "status": "InProgress", + "error": { + "code": "Error code", + "message": "Error message", + "target": "op1", + "details": [ + { + "code": "code1", + "message": "message1", + "target": "op1", + "details": [], + "innererror": { + "code": "code1" + } + } + ], + "innererror": { + "code": "code1" + } + }, + "result": { + "name": "bingsearch", + "manufacturerId": "manufacturer Id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + }, + "widgetName": "rfazvwnfwwomiwrh" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/stable/2022-12-01/examples/Widgets_GetWidgetSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/stable/2022-12-01/examples/Widgets_GetWidgetSample.json new file mode 100644 index 000000000000..ecab18c65303 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/stable/2022-12-01/examples/Widgets_GetWidgetSample.json @@ -0,0 +1,25 @@ +{ + "operationId": "Widgets_GetWidget", + "title": "Get widget by widget name.", + "parameters": { + "api-version": "2022-12-01", + "widgetName": "searchbox" + }, + "responses": { + "200": { + "body": { + "name": "bingsearch", + "manufacturerId": "a-22-01" + } + }, + "default": { + "body": { + "error": { + "code": "Error code", + "message": "Error message", + "details": [] + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/stable/2022-12-01/examples/Widgets_ListWidgetsSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/stable/2022-12-01/examples/Widgets_ListWidgetsSample.json new file mode 100644 index 000000000000..fa68ab418490 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/stable/2022-12-01/examples/Widgets_ListWidgetsSample.json @@ -0,0 +1,27 @@ +{ + "title": "Widgets_ListWidgets", + "operationId": "Widgets_ListWidgets", + "parameters": { + "top": 8, + "skip": 15, + "maxpagesize": 27, + "api-version": "2022-12-01" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "name": "bingsearch", + "manufacturerId": "manufacturer Id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + } + ], + "nextLink": "https://microsoft.com/a" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/stable/2022-12-01/widget.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/stable/2022-12-01/widget.json new file mode 100644 index 000000000000..418c1091069f --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/stable/2022-12-01/widget.json @@ -0,0 +1,550 @@ +{ + "swagger": "2.0", + "info": { + "title": "Widget", + "version": "2022-12-01", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "security": [ + { + "AadOauth2Auth": [ + "https://azure.com/.default" + ] + } + ], + "securityDefinitions": { + "AadOauth2Auth": { + "type": "oauth2", + "description": "The Azure Active Directory OAuth2 Flow", + "flow": "accessCode", + "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize", + "scopes": { + "https://azure.com/.default": "" + }, + "tokenUrl": "https://login.microsoftonline.com/common/oauth2/token" + } + }, + "tags": [], + "paths": { + "/widgets": { + "get": { + "operationId": "Widgets_ListWidgets", + "description": "List Widget resources", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/PagedWidgetSuite" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Widgets_ListWidgets": { + "$ref": "./examples/Widgets_ListWidgetsSample.json" + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/widgets/{widgetName}": { + "get": { + "operationId": "Widgets_GetWidget", + "description": "Fetch a Widget by name.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The widget name.", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/WidgetSuite" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Get widget by widget name.": { + "$ref": "./examples/Widgets_GetWidgetSample.json" + } + } + }, + "patch": { + "operationId": "Widgets_CreateOrUpdateWidget", + "description": "Creates or updates a Widget asynchronously.", + "consumes": [ + "application/merge-patch+json" + ], + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The widget name.", + "required": true, + "type": "string" + }, + { + "name": "resource", + "in": "body", + "description": "The resource instance.", + "required": true, + "schema": { + "$ref": "#/definitions/WidgetSuiteCreateOrUpdate" + } + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/WidgetSuite" + }, + "headers": { + "Operation-Location": { + "type": "string", + "format": "uri", + "description": "The location for monitoring the operation state." + } + } + }, + "201": { + "description": "The request has succeeded and a new resource has been created as a result.", + "schema": { + "$ref": "#/definitions/WidgetSuite" + }, + "headers": { + "Operation-Location": { + "type": "string", + "format": "uri", + "description": "The location for monitoring the operation state." + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Widgets_CreateOrUpdateWidget": { + "$ref": "./examples/Widgets_CreateOrUpdateWidgetSample.json" + } + }, + "x-ms-long-running-operation": true + }, + "delete": { + "operationId": "Widgets_DeleteWidget", + "description": "Delete a Widget asynchronously.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The widget name.", + "required": true, + "type": "string" + } + ], + "responses": { + "202": { + "description": "The request has been accepted for processing, but processing has not yet completed.", + "schema": { + "type": "object", + "description": "Provides status details for long running operations.", + "properties": { + "id": { + "type": "string", + "description": "The unique ID of the operation." + }, + "status": { + "$ref": "#/definitions/Azure.Core.Foundations.OperationState", + "description": "The status of the operation" + }, + "error": { + "$ref": "#/definitions/Azure.Core.Foundations.Error", + "description": "Error object that describes the error when status is \"Failed\"." + } + }, + "required": [ + "id", + "status" + ] + }, + "headers": { + "Operation-Location": { + "type": "string", + "format": "uri", + "description": "The location for monitoring the operation state." + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Delete widget by widget name using long-running operation.": { + "$ref": "./examples/Widgets_DeleteWidgetSample.json" + } + }, + "x-ms-long-running-operation": true + } + }, + "/widgets/{widgetName}/operations/{operationId}": { + "get": { + "operationId": "Widgets_GetWidgetOperationStatus", + "description": "Gets status of a Widget operation.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The widget name.", + "required": true, + "type": "string" + }, + { + "name": "operationId", + "in": "path", + "description": "The unique ID of the operation.", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "type": "object", + "description": "Provides status details for long running operations.", + "properties": { + "id": { + "type": "string", + "description": "The unique ID of the operation." + }, + "status": { + "$ref": "#/definitions/Azure.Core.Foundations.OperationState", + "description": "The status of the operation" + }, + "error": { + "$ref": "#/definitions/Azure.Core.Foundations.Error", + "description": "Error object that describes the error when status is \"Failed\"." + }, + "result": { + "$ref": "#/definitions/WidgetSuite", + "description": "The result of the operation." + } + }, + "required": [ + "id", + "status" + ] + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Widgets_GetWidgetOperationStatus": { + "$ref": "./examples/Widgets_GetWidgetOperationStatusSample.json" + } + } + } + } + }, + "definitions": { + "Azure.Core.Foundations.Error": { + "type": "object", + "description": "The error object.", + "properties": { + "code": { + "type": "string", + "description": "One of a server-defined set of error codes." + }, + "message": { + "type": "string", + "description": "A human-readable representation of the error." + }, + "target": { + "type": "string", + "description": "The target of the error." + }, + "details": { + "type": "array", + "description": "An array of details about specific errors that led to this reported error.", + "items": { + "$ref": "#/definitions/Azure.Core.Foundations.Error" + }, + "x-ms-identifiers": [] + }, + "innererror": { + "$ref": "#/definitions/Azure.Core.Foundations.InnerError", + "description": "An object containing more specific information than the current object about the error." + } + }, + "required": [ + "code", + "message" + ] + }, + "Azure.Core.Foundations.ErrorResponse": { + "type": "object", + "description": "A response containing error details.", + "properties": { + "error": { + "$ref": "#/definitions/Azure.Core.Foundations.Error", + "description": "The error object." + } + }, + "required": [ + "error" + ] + }, + "Azure.Core.Foundations.InnerError": { + "type": "object", + "description": "An object containing more specific information about the error. As per Microsoft One API guidelines - https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#handling-errors.", + "properties": { + "code": { + "type": "string", + "description": "One of a server-defined set of error codes." + }, + "innererror": { + "$ref": "#/definitions/Azure.Core.Foundations.InnerError", + "description": "Inner error." + } + } + }, + "Azure.Core.Foundations.OperationState": { + "type": "string", + "description": "Enum describing allowed operation states.", + "enum": [ + "NotStarted", + "Running", + "Succeeded", + "Failed", + "Canceled" + ], + "x-ms-enum": { + "name": "OperationState", + "modelAsString": true, + "values": [ + { + "name": "NotStarted", + "value": "NotStarted", + "description": "The operation has not started." + }, + { + "name": "Running", + "value": "Running", + "description": "The operation is in progress." + }, + { + "name": "Succeeded", + "value": "Succeeded", + "description": "The operation has completed successfully." + }, + { + "name": "Failed", + "value": "Failed", + "description": "The operation has failed." + }, + { + "name": "Canceled", + "value": "Canceled", + "description": "The operation has been canceled by the user." + } + ] + } + }, + "FakedSharedModel": { + "type": "object", + "description": "Faked shared model", + "properties": { + "tag": { + "type": "string", + "description": "The tag." + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "The created date." + } + }, + "required": [ + "tag", + "createdAt" + ] + }, + "FakedSharedModelCreateOrUpdate": { + "type": "object", + "description": "Faked shared model", + "properties": { + "tag": { + "type": "string", + "description": "The tag." + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "The created date." + } + } + }, + "PagedWidgetSuite": { + "type": "object", + "description": "Paged collection of WidgetSuite items", + "properties": { + "value": { + "type": "array", + "description": "The WidgetSuite items on this page", + "items": { + "$ref": "#/definitions/WidgetSuite" + }, + "x-ms-identifiers": [] + }, + "nextLink": { + "type": "string", + "format": "uri", + "description": "The link to the next page of items" + } + }, + "required": [ + "value" + ] + }, + "WidgetSuite": { + "type": "object", + "description": "A widget.", + "properties": { + "name": { + "type": "string", + "description": "The widget name.", + "readOnly": true + }, + "manufacturerId": { + "type": "string", + "description": "The ID of the widget's manufacturer." + }, + "sharedModel": { + "$ref": "#/definitions/FakedSharedModel", + "description": "The faked shared model." + } + }, + "required": [ + "name", + "manufacturerId" + ] + }, + "WidgetSuiteCreateOrUpdate": { + "type": "object", + "description": "A widget.", + "properties": { + "manufacturerId": { + "type": "string", + "description": "The ID of the widget's manufacturer." + }, + "sharedModel": { + "$ref": "#/definitions/FakedSharedModelCreateOrUpdate", + "description": "The faked shared model." + } + } + } + }, + "parameters": { + "Azure.Core.Foundations.ApiVersionParameter": { + "name": "api-version", + "in": "query", + "description": "The API version to use for this operation.", + "required": true, + "type": "string", + "minLength": 1, + "x-ms-parameter-location": "method", + "x-ms-client-name": "apiVersion" + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/tspconfig.yaml b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/tspconfig.yaml new file mode 100644 index 000000000000..2de0c6c6c6cf --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/data-plane/widget/tspconfig.yaml @@ -0,0 +1,47 @@ +parameters: + "service-dir": + default: "sdk/widget" + "dependencies": + default: "" +emit: + - "@azure-tools/typespec-autorest" +linter: + extends: + - "@azure-tools/typespec-azure-rulesets/data-plane" +options: + "@azure-tools/typespec-autorest": + # TODO: Does anything need this set, if it's not used in output-file? + azure-resource-provider-folder: "data-plane" + emit-lro-options: "none" + emitter-output-dir: "{project-root}" + output-file: "{version-status}/{version}/widgets.json" + "@azure-tools/typespec-python": + package-dir: "azure-widget" + namespace: "azure.widget" + generate-test: true + generate-sample: true + flavor: azure + "@azure-tools/typespec-csharp": + package-dir: "Azure.Widget" + clear-output-folder: true + model-namespace: false + namespace: "{package-dir}" + flavor: azure + "@azure-tools/typespec-ts": + package-dir: "widget-rest" + package-details: + name: "@azure-rest/azure-widget" + flavor: azure + "@azure-tools/typespec-java": + package-dir: "azure-widget" + namespace: com.azure.widget + flavor: azure + "@azure-tools/typespec-go": + module: "github.com/Azure/azure-sdk-for-go/{service-dir}/{package-dir}" + service-dir: "sdk/widget" + package-dir: "azmanager" + module-version: "0.0.1" + generate-fakes: true + inject-spans: true + single-client: true + slice-elements-byval: true diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/employee.tsp b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/employee.tsp new file mode 100644 index 000000000000..d77152e040ec --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/employee.tsp @@ -0,0 +1,63 @@ +import "@typespec/rest"; +import "@typespec/http"; +import "@azure-tools/typespec-azure-core"; +import "@azure-tools/typespec-azure-resource-manager"; + +using TypeSpec.Rest; +using TypeSpec.Http; +using Azure.Core; +using Azure.ResourceManager; + +namespace WidgetManagement; + +/** Employee resource */ +model Employee is TrackedResource { + ...ResourceNameParameter; +} + +/** Employee properties */ +model EmployeeProperties { + /** Age of employee */ + age?: int32; + + /** City of employee */ + city?: string; + + /** Profile of employee */ + @encode("base64url") + profile?: bytes; + + /** The status of the last operation. */ + @visibility(Lifecycle.Read) + provisioningState?: ProvisioningState; +} + +/** The resource provisioning state. */ +@lroStatus +union ProvisioningState { + ResourceProvisioningState, + + /** The resource is being provisioned */ + Provisioning: "Provisioning", + + /** The resource is updating */ + Updating: "Updating", + + /** The resource is being deleted */ + Deleting: "Deleting", + + /** The resource create request has been accepted */ + Accepted: "Accepted", + + string, +} + +@armResourceOperations +interface Employees { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + update is ArmResourcePatchSync; + delete is ArmResourceDeleteWithoutOkAsync; + listByResourceGroup is ArmResourceListByParent; + listBySubscription is ArmListBySubscription; +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Employees_CreateOrUpdate.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Employees_CreateOrUpdate.json new file mode 100644 index 000000000000..4a13a329e3b8 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Employees_CreateOrUpdate.json @@ -0,0 +1,76 @@ +{ + "title": "Employees_CreateOrUpdate", + "operationId": "Employees_CreateOrUpdate", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi", + "employeeName": "9KF-f-8b", + "resource": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl" + } + }, + "responses": { + "200": { + "body": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/le-8MU--J3W6q8D386p3-iT3", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + }, + "201": { + "headers": { + "Azure-AsyncOperation": "https://contoso.com/operationstatus" + }, + "body": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/9KF-f-8b", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Employees_Delete.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Employees_Delete.json new file mode 100644 index 000000000000..15176d86b029 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Employees_Delete.json @@ -0,0 +1,19 @@ +{ + "title": "Employees_Delete", + "operationId": "Employees_Delete", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi", + "employeeName": "5vX--BxSu3ux48rI4O9OQ569" + }, + "responses": { + "202": { + "headers": { + "Retry-After": 30, + "location": "https://contoso.com/operationstatus" + } + }, + "204": {} + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Employees_Get.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Employees_Get.json new file mode 100644 index 000000000000..eb1917859e24 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Employees_Get.json @@ -0,0 +1,37 @@ +{ + "title": "Employees_Get", + "operationId": "Employees_Get", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi", + "employeeName": "le-8MU--J3W6q8D386p3-iT3" + }, + "responses": { + "200": { + "body": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/le-8MU--J3W6q8D386p3-iT3", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Employees_ListByResourceGroup.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Employees_ListByResourceGroup.json new file mode 100644 index 000000000000..860fab85a9b8 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Employees_ListByResourceGroup.json @@ -0,0 +1,41 @@ +{ + "title": "Employees_ListByResourceGroup", + "operationId": "Employees_ListByResourceGroup", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/test", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + ], + "nextLink": "https://microsoft.com/a" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Employees_ListBySubscription.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Employees_ListBySubscription.json new file mode 100644 index 000000000000..18432d58de37 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Employees_ListBySubscription.json @@ -0,0 +1,40 @@ +{ + "title": "Employees_ListBySubscription", + "operationId": "Employees_ListBySubscription", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/test", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + ], + "nextLink": "https://microsoft.com/a" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Employees_Update.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Employees_Update.json new file mode 100644 index 000000000000..de46fc8ef2e8 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Employees_Update.json @@ -0,0 +1,47 @@ +{ + "title": "Employees_Update", + "operationId": "Employees_Update", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi", + "employeeName": "-XhyNJ--", + "properties": { + "tags": { + "key7952": "no" + }, + "properties": { + "age": 24, + "city": "uyfg", + "profile": "oapgijcswfkruiuuzbwco" + } + } + }, + "responses": { + "200": { + "body": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/contoso/providers/Microsoft.Contoso/employees/test", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Operations_List.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Operations_List.json new file mode 100644 index 000000000000..4d74e755c020 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/Operations_List.json @@ -0,0 +1,28 @@ +{ + "title": "Operations_List", + "operationId": "Operations_List", + "parameters": { + "api-version": "2021-11-01" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "name": "ymeow", + "isDataAction": true, + "display": { + "provider": "qxyznq", + "resource": "bqfwkox", + "operation": "td", + "description": "yvgkhsuwartgxb" + }, + "origin": "user", + "actionType": "Internal" + } + ], + "nextLink": "https://sample.com/nextLink" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/main.tsp b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/main.tsp new file mode 100644 index 000000000000..6a7f5047f36b --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/main.tsp @@ -0,0 +1,35 @@ +import "@typespec/http"; +import "@typespec/rest"; +import "@typespec/versioning"; +import "@azure-tools/typespec-azure-core"; +import "@azure-tools/typespec-azure-resource-manager"; +import "./employee.tsp"; + +using TypeSpec.Http; +using TypeSpec.Rest; +using TypeSpec.Versioning; +using Azure.Core; +using Azure.ResourceManager; + +/** Microsoft.Contoso Resource Provider management API. */ +@armProviderNamespace +@service(#{ title: "WidgetManagement" }) +@versioned(WidgetManagement.Versions) +namespace WidgetManagement; + +/** The available API versions. */ +enum Versions { + /** 2021-10-01-preview version */ + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + @useDependency(Azure.Core.Versions.v1_0_Preview_2) + @armCommonTypesVersion(Azure.ResourceManager.CommonTypes.Versions.v5) + v2021_10_01_preview: "2021-10-01-preview", + + /** 2021-11-01 version */ + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + @useDependency(Azure.Core.Versions.v1_0_Preview_2) + @armCommonTypesVersion(Azure.ResourceManager.CommonTypes.Versions.v5) + v2021_11_01: "2021-11-01", +} + +interface Operations extends Azure.ResourceManager.Operations {} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/readme.md b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/readme.md new file mode 100644 index 000000000000..98556255f8f4 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/readme.md @@ -0,0 +1,42 @@ +# WidgetManagement + +> see https://aka.ms/autorest +> This is the AutoRest configuration file for WidgetManagement. + +## Getting Started + +To build the SDKs for My API, simply install AutoRest via `npm` (`npm install -g autorest`) and then run: + +> `autorest readme.md` +> To see additional help and options, run: + +> `autorest --help` +> For other options on installation see [Installing AutoRest](https://aka.ms/autorest/install) on the AutoRest github page. + +--- + +## Configuration + +### Basic Information + +These are the global settings. + +```yaml +openapi-type: arm +openapi-subtype: rpaas +tag: package-2021-11-01 +``` + +### Tag: package-2021-11-01 + +These settings apply only when `--tag=package-2021-11-01` is specified on the command line. + +```yaml $(tag) == 'package-2021-11-01' +input-file: + - stable/2021-11-01/widgetmanagement.json +suppressions: + - code: PathContainsResourceType + - code: PathResourceProviderMatchNamespace +``` + +--- diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/shared.tsp b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/shared.tsp new file mode 100644 index 000000000000..1b94bb705031 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/shared.tsp @@ -0,0 +1,8 @@ +@doc("Faked shared model") +model FakedSharedModel { + @doc("The tag.") + tag: string; + + @doc("The created date.") + createdAt: utcDateTime; +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Employees_CreateOrUpdate.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Employees_CreateOrUpdate.json new file mode 100644 index 000000000000..4a13a329e3b8 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Employees_CreateOrUpdate.json @@ -0,0 +1,76 @@ +{ + "title": "Employees_CreateOrUpdate", + "operationId": "Employees_CreateOrUpdate", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi", + "employeeName": "9KF-f-8b", + "resource": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl" + } + }, + "responses": { + "200": { + "body": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/le-8MU--J3W6q8D386p3-iT3", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + }, + "201": { + "headers": { + "Azure-AsyncOperation": "https://contoso.com/operationstatus" + }, + "body": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/9KF-f-8b", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Employees_Delete.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Employees_Delete.json new file mode 100644 index 000000000000..15176d86b029 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Employees_Delete.json @@ -0,0 +1,19 @@ +{ + "title": "Employees_Delete", + "operationId": "Employees_Delete", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi", + "employeeName": "5vX--BxSu3ux48rI4O9OQ569" + }, + "responses": { + "202": { + "headers": { + "Retry-After": 30, + "location": "https://contoso.com/operationstatus" + } + }, + "204": {} + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Employees_Get.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Employees_Get.json new file mode 100644 index 000000000000..eb1917859e24 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Employees_Get.json @@ -0,0 +1,37 @@ +{ + "title": "Employees_Get", + "operationId": "Employees_Get", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi", + "employeeName": "le-8MU--J3W6q8D386p3-iT3" + }, + "responses": { + "200": { + "body": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/le-8MU--J3W6q8D386p3-iT3", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Employees_ListByResourceGroup.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Employees_ListByResourceGroup.json new file mode 100644 index 000000000000..860fab85a9b8 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Employees_ListByResourceGroup.json @@ -0,0 +1,41 @@ +{ + "title": "Employees_ListByResourceGroup", + "operationId": "Employees_ListByResourceGroup", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/test", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + ], + "nextLink": "https://microsoft.com/a" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Employees_ListBySubscription.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Employees_ListBySubscription.json new file mode 100644 index 000000000000..18432d58de37 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Employees_ListBySubscription.json @@ -0,0 +1,40 @@ +{ + "title": "Employees_ListBySubscription", + "operationId": "Employees_ListBySubscription", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/test", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + ], + "nextLink": "https://microsoft.com/a" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Employees_Update.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Employees_Update.json new file mode 100644 index 000000000000..de46fc8ef2e8 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Employees_Update.json @@ -0,0 +1,47 @@ +{ + "title": "Employees_Update", + "operationId": "Employees_Update", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi", + "employeeName": "-XhyNJ--", + "properties": { + "tags": { + "key7952": "no" + }, + "properties": { + "age": 24, + "city": "uyfg", + "profile": "oapgijcswfkruiuuzbwco" + } + } + }, + "responses": { + "200": { + "body": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/contoso/providers/Microsoft.Contoso/employees/test", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Operations_List.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Operations_List.json new file mode 100644 index 000000000000..4d74e755c020 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/examples/Operations_List.json @@ -0,0 +1,28 @@ +{ + "title": "Operations_List", + "operationId": "Operations_List", + "parameters": { + "api-version": "2021-11-01" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "name": "ymeow", + "isDataAction": true, + "display": { + "provider": "qxyznq", + "resource": "bqfwkox", + "operation": "td", + "description": "yvgkhsuwartgxb" + }, + "origin": "user", + "actionType": "Internal" + } + ], + "nextLink": "https://sample.com/nextLink" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/widgetmanagement.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/widgetmanagement.json new file mode 100644 index 000000000000..c8a8b95d6b40 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/widgetmanagement.json @@ -0,0 +1,557 @@ +{ + "swagger": "2.0", + "info": { + "title": "WidgetManagement", + "version": "2021-11-01", + "description": "Microsoft.Contoso Resource Provider management API.", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "host": "management.azure.com", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "security": [ + { + "azure_auth": [ + "user_impersonation" + ] + } + ], + "securityDefinitions": { + "azure_auth": { + "type": "oauth2", + "description": "Azure Active Directory OAuth2 Flow.", + "flow": "implicit", + "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize", + "scopes": { + "user_impersonation": "impersonate your user account" + } + } + }, + "tags": [ + { + "name": "Operations" + }, + { + "name": "Employees" + } + ], + "paths": { + "/providers/WidgetManagement/operations": { + "get": { + "operationId": "Operations_List", + "tags": [ + "Operations" + ], + "description": "List the operations for the provider", + "parameters": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/OperationListResult" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "Operations_List": { + "$ref": "./examples/Operations_List.json" + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/providers/WidgetManagement/employees": { + "get": { + "operationId": "Employees_ListBySubscription", + "tags": [ + "Employees" + ], + "description": "List Employee resources by subscription ID", + "parameters": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "#/definitions/EmployeeListResult" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "Employees_ListBySubscription": { + "$ref": "./examples/Employees_ListBySubscription.json" + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/WidgetManagement/employees": { + "get": { + "operationId": "Employees_ListByResourceGroup", + "tags": [ + "Employees" + ], + "description": "List Employee resources by resource group", + "parameters": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "#/definitions/EmployeeListResult" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "Employees_ListByResourceGroup": { + "$ref": "./examples/Employees_ListByResourceGroup.json" + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/WidgetManagement/employees/{employeeName}": { + "get": { + "operationId": "Employees_Get", + "tags": [ + "Employees" + ], + "description": "Get a Employee", + "parameters": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "employeeName", + "in": "path", + "description": "The name of the Employee", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9-]{3,24}$" + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "#/definitions/Employee" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "Employees_Get": { + "$ref": "./examples/Employees_Get.json" + } + } + }, + "put": { + "operationId": "Employees_CreateOrUpdate", + "tags": [ + "Employees" + ], + "description": "Create a Employee", + "parameters": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "employeeName", + "in": "path", + "description": "The name of the Employee", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9-]{3,24}$" + }, + { + "name": "resource", + "in": "body", + "description": "Resource create parameters.", + "required": true, + "schema": { + "$ref": "#/definitions/Employee" + } + } + ], + "responses": { + "200": { + "description": "Resource 'Employee' update operation succeeded", + "schema": { + "$ref": "#/definitions/Employee" + } + }, + "201": { + "description": "Resource 'Employee' create operation succeeded", + "schema": { + "$ref": "#/definitions/Employee" + }, + "headers": { + "Azure-AsyncOperation": { + "type": "string", + "description": "A link to the status monitor" + }, + "Retry-After": { + "type": "integer", + "format": "int32", + "description": "The Retry-After header can indicate how long the client should wait before polling the operation status." + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "Employees_CreateOrUpdate": { + "$ref": "./examples/Employees_CreateOrUpdate.json" + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-long-running-operation": true + }, + "patch": { + "operationId": "Employees_Update", + "tags": [ + "Employees" + ], + "description": "Update a Employee", + "parameters": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "employeeName", + "in": "path", + "description": "The name of the Employee", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9-]{3,24}$" + }, + { + "name": "properties", + "in": "body", + "description": "The resource properties to be updated.", + "required": true, + "schema": { + "$ref": "#/definitions/EmployeeUpdate" + } + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "#/definitions/Employee" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "Employees_Update": { + "$ref": "./examples/Employees_Update.json" + } + } + }, + "delete": { + "operationId": "Employees_Delete", + "tags": [ + "Employees" + ], + "description": "Delete a Employee", + "parameters": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "employeeName", + "in": "path", + "description": "The name of the Employee", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9-]{3,24}$" + } + ], + "responses": { + "202": { + "description": "Resource deletion accepted.", + "headers": { + "Location": { + "type": "string", + "description": "The Location header contains the URL where the status of the long running operation can be checked." + }, + "Retry-After": { + "type": "integer", + "format": "int32", + "description": "The Retry-After header can indicate how long the client should wait before polling the operation status." + } + } + }, + "204": { + "description": "Resource does not exist." + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "Employees_Delete": { + "$ref": "./examples/Employees_Delete.json" + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "location" + }, + "x-ms-long-running-operation": true + } + } + }, + "definitions": { + "Azure.ResourceManager.CommonTypes.TrackedResourceUpdate": { + "type": "object", + "title": "Tracked Resource", + "description": "The resource model definition for an Azure Resource Manager tracked top level resource which has 'tags' and a 'location'", + "properties": { + "tags": { + "type": "object", + "description": "Resource tags.", + "additionalProperties": { + "type": "string" + } + } + }, + "allOf": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/Resource" + } + ] + }, + "Employee": { + "type": "object", + "description": "Employee resource", + "properties": { + "properties": { + "$ref": "#/definitions/EmployeeProperties", + "description": "The resource-specific properties for this resource." + } + }, + "allOf": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/TrackedResource" + } + ] + }, + "EmployeeListResult": { + "type": "object", + "description": "The response of a Employee list operation.", + "properties": { + "value": { + "type": "array", + "description": "The Employee items on this page", + "items": { + "$ref": "#/definitions/Employee" + } + }, + "nextLink": { + "type": "string", + "format": "uri", + "description": "The link to the next page of items" + } + }, + "required": [ + "value" + ] + }, + "EmployeeProperties": { + "type": "object", + "description": "Employee properties", + "properties": { + "age": { + "type": "integer", + "format": "int32", + "description": "Age of employee" + }, + "city": { + "type": "string", + "description": "City of employee" + }, + "profile": { + "type": "string", + "format": "base64url", + "description": "Profile of employee" + }, + "provisioningState": { + "$ref": "#/definitions/ProvisioningState", + "description": "The status of the last operation.", + "readOnly": true + } + } + }, + "EmployeeUpdate": { + "type": "object", + "description": "Employee resource", + "properties": { + "properties": { + "$ref": "#/definitions/EmployeeProperties", + "description": "The resource-specific properties for this resource." + } + }, + "allOf": [ + { + "$ref": "#/definitions/Azure.ResourceManager.CommonTypes.TrackedResourceUpdate" + } + ] + }, + "ProvisioningState": { + "type": "string", + "description": "The resource provisioning state.", + "enum": [ + "Succeeded", + "Failed", + "Canceled", + "Provisioning", + "Updating", + "Deleting", + "Accepted" + ], + "x-ms-enum": { + "name": "ProvisioningState", + "modelAsString": true, + "values": [ + { + "name": "Succeeded", + "value": "Succeeded", + "description": "Resource has been created." + }, + { + "name": "Failed", + "value": "Failed", + "description": "Resource creation failed." + }, + { + "name": "Canceled", + "value": "Canceled", + "description": "Resource creation was canceled." + }, + { + "name": "Provisioning", + "value": "Provisioning", + "description": "The resource is being provisioned" + }, + { + "name": "Updating", + "value": "Updating", + "description": "The resource is updating" + }, + { + "name": "Deleting", + "value": "Deleting", + "description": "The resource is being deleted" + }, + { + "name": "Accepted", + "value": "Accepted", + "description": "The resource create request has been accepted" + } + ] + }, + "readOnly": true + } + }, + "parameters": {} +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/tspconfig.yaml b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/tspconfig.yaml new file mode 100644 index 000000000000..52522ba9eca6 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/tspconfig.yaml @@ -0,0 +1,49 @@ +parameters: + "service-dir": + default: "sdk/widgetmanagement" +emit: + - "@azure-tools/typespec-autorest" +options: + "@azure-tools/typespec-autorest": + use-read-only-status-schema: true + emitter-output-dir: "{project-root}" + # TODO: Does anything need this set, if it's not used in output-file? Currently required by TSV. + azure-resource-provider-folder: "resource-manager" + output-file: "{version-status}/{version}/widgetmanagement.json" + arm-types-dir: "{project-root}/../../../../common-types/resource-management" + "@azure-tools/typespec-csharp": + flavor: azure + package-dir: "Azure.ResourceManager.Widget" + clear-output-folder: true + model-namespace: true + namespace: "{package-dir}" + "@azure-tools/typespec-python": + package-dir: "azure-mgmt-widget" + namespace: "azure.mgmt.widget" + generate-test: true + generate-sample: true + flavor: "azure" + "@azure-tools/typespec-java": + package-dir: "azure-resourcemanager-widget" + namespace: "com.azure.resourcemanager.widget" + service-name: "Widget" # human-readable service name, whitespace allowed + flavor: azure + "@azure-tools/typespec-ts": + package-dir: "arm-widget" + flavor: azure + experimental-extensible-enums: true + package-details: + name: "@azure/arm-widget" + "@azure-tools/typespec-go": + service-dir: "sdk/resourcemanager/widget" + package-dir: "armwidget" + module: "github.com/Azure/azure-sdk-for-go/{service-dir}/{package-dir}" + fix-const-stuttering: true + flavor: "azure" + generate-samples: true + generate-fakes: true + head-as-boolean: true + inject-spans: true +linter: + extends: + - "@azure-tools/typespec-azure-rulesets/resource-manager" diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/readme.md b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/Microsoft.Service1/readme.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/readme.md b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service1/resource-manager/readme.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/readme.md b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/readme.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/client.tsp b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/client.tsp new file mode 100644 index 000000000000..be91eaf5c748 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/client.tsp @@ -0,0 +1,7 @@ +import "./main.tsp"; +import "@azure-tools/typespec-client-generator-core"; + +using Widget; +using Azure.ClientGenerator.Core; + +@@clientName(Widgets, "AzureWidgets", "csharp"); diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/examples/2022-12-01/Widgets_CreateOrUpdateWidgetSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/examples/2022-12-01/Widgets_CreateOrUpdateWidgetSample.json new file mode 100644 index 000000000000..dbd333fb52dc --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/examples/2022-12-01/Widgets_CreateOrUpdateWidgetSample.json @@ -0,0 +1,37 @@ +{ + "title": "Widgets_CreateOrUpdateWidget", + "operationId": "Widgets_CreateOrUpdateWidget", + "parameters": { + "widgetName": "name1", + "api-version": "2022-12-01", + "resource": { + "manufacturerId": "manufacturer id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + } + }, + "responses": { + "200": { + "body": { + "name": "name1", + "manufacturerId": "manufacturer id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + } + }, + "201": { + "body": { + "name": "name1", + "manufacturerId": "manufacturer id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/examples/2022-12-01/Widgets_DeleteWidgetSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/examples/2022-12-01/Widgets_DeleteWidgetSample.json new file mode 100644 index 000000000000..c5de7719085d --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/examples/2022-12-01/Widgets_DeleteWidgetSample.json @@ -0,0 +1,29 @@ +{ + "operationId": "Widgets_DeleteWidget", + "title": "Delete widget by widget name using long-running operation.", + "parameters": { + "api-version": "2022-12-01", + "widgetName": "searchbox" + }, + "responses": { + "202": { + "headers": { + "location": "https://contosowidgetmanager.azure.com/operations/00000000-0000-0000-0000-000000000123/result?api-version=2022-12-01", + "operation-location": "https://contosowidgetmanager.azure.com/operations/00000000-0000-0000-0000-000000000123?api-version=2022-12-01" + }, + "body": { + "id": "id1", + "status": "deleted" + } + }, + "default": { + "body": { + "error": { + "code": "Error code", + "message": "Error message", + "details": [] + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/examples/2022-12-01/Widgets_GetWidgetOperationStatusSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/examples/2022-12-01/Widgets_GetWidgetOperationStatusSample.json new file mode 100644 index 000000000000..a840b19f26e6 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/examples/2022-12-01/Widgets_GetWidgetOperationStatusSample.json @@ -0,0 +1,45 @@ +{ + "title": "Widgets_GetWidgetOperationStatus", + "operationId": "Widgets_GetWidgetOperationStatus", + "parameters": { + "widgetName": "name1", + "operationId": "opreation id1", + "api-version": "2022-12-01" + }, + "responses": { + "200": { + "body": { + "id": "opreation id1", + "status": "InProgress", + "error": { + "code": "Error code", + "message": "Error message", + "target": "op1", + "details": [ + { + "code": "code1", + "message": "message1", + "target": "op1", + "details": [], + "innererror": { + "code": "code1" + } + } + ], + "innererror": { + "code": "code1" + } + }, + "result": { + "name": "bingsearch", + "manufacturerId": "manufacturer Id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + }, + "widgetName": "rfazvwnfwwomiwrh" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/examples/2022-12-01/Widgets_GetWidgetSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/examples/2022-12-01/Widgets_GetWidgetSample.json new file mode 100644 index 000000000000..ecab18c65303 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/examples/2022-12-01/Widgets_GetWidgetSample.json @@ -0,0 +1,25 @@ +{ + "operationId": "Widgets_GetWidget", + "title": "Get widget by widget name.", + "parameters": { + "api-version": "2022-12-01", + "widgetName": "searchbox" + }, + "responses": { + "200": { + "body": { + "name": "bingsearch", + "manufacturerId": "a-22-01" + } + }, + "default": { + "body": { + "error": { + "code": "Error code", + "message": "Error message", + "details": [] + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/examples/2022-12-01/Widgets_ListWidgetsSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/examples/2022-12-01/Widgets_ListWidgetsSample.json new file mode 100644 index 000000000000..fa68ab418490 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/examples/2022-12-01/Widgets_ListWidgetsSample.json @@ -0,0 +1,27 @@ +{ + "title": "Widgets_ListWidgets", + "operationId": "Widgets_ListWidgets", + "parameters": { + "top": 8, + "skip": 15, + "maxpagesize": 27, + "api-version": "2022-12-01" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "name": "bingsearch", + "manufacturerId": "manufacturer Id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + } + ], + "nextLink": "https://microsoft.com/a" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/main.tsp b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/main.tsp new file mode 100644 index 000000000000..a30b708fdb62 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/main.tsp @@ -0,0 +1,61 @@ +import "@typespec/http"; +import "@typespec/rest"; +import "@typespec/versioning"; +import "@azure-tools/typespec-azure-core"; +import "./shared.tsp"; + +using TypeSpec.Http; +using TypeSpec.Rest; +using TypeSpec.Versioning; +using Azure.Core; + +@useAuth(AadOauth2Auth<["https://azure.com/.default"]>) +@service(#{ title: "Widget" }) +@versioned(Widget.Versions) +namespace Widget; + +@doc("Versions info.") +enum Versions { + @doc("The 2022-12-01 version.") + @useDependency(Azure.Core.Versions.v1_0_Preview_1) + v2022_12_01: "2022-12-01", +} + +@doc("A widget.") +@resource("widgets") +model WidgetSuite { + @key("widgetName") + @doc("The widget name.") + @visibility(Lifecycle.Read) + name: string; + + @doc("The ID of the widget's manufacturer.") + manufacturerId: string; + + @doc("The faked shared model.") + sharedModel?: FakedSharedModel; +} + +interface Widgets { + @doc("Fetch a Widget by name.") + getWidget is ResourceRead; + + @doc("Gets status of a Widget operation.") + getWidgetOperationStatus is GetResourceOperationStatus; + + @doc("Creates or updates a Widget asynchronously.") + @pollingOperation(Widgets.getWidgetOperationStatus) + createOrUpdateWidget is StandardResourceOperations.LongRunningResourceCreateOrUpdate; + + @doc("Delete a Widget asynchronously.") + @pollingOperation(Widgets.getWidgetOperationStatus) + deleteWidget is LongRunningResourceDelete; + + @doc("List Widget resources") + listWidgets is ResourceList< + WidgetSuite, + { + parameters: StandardListQueryParameters; + } + >; +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/readme.md b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/readme.md new file mode 100644 index 000000000000..6daa7d4c93ee --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/readme.md @@ -0,0 +1,78 @@ +# Widget + +> see https://aka.ms/autorest + +This is the AutoRest configuration file for Widget. + +## Configuration + +### Basic Information + +This is a TypeSpec project so we only want to readme to default the default tag and point to the outputted swagger file. +This is used for some tools such as doc generation and swagger apiview generation it isn't used for SDK code gen as we +use the native TypeSpec code generation configured in the tspconfig.yaml file. + +```yaml +openapi-type: data-plane +tag: package-2022-12-01 +``` + +### Tag: package-2022-12-01 + +These settings apply only when `--tag=package-2022-12-01` is specified on the command line. + +```yaml $(tag) == 'package-2022-12-01' +input-file: + - stable/2022-12-01/widgets.json +``` + +### Suppress non-TypeSpec SDK related linting rules + +These set of linting rules aren't applicable to the new TypeSpec SDK code generators so suppressing them here. Eventually we will +opt-out these rules from running in the linting tools for TypeSpec generated swagger files. + +```yaml +suppressions: + - code: AvoidAnonymousTypes + - code: PatchInOperationName + - code: OperationIdNounVerb + - code: RequiredReadOnlyProperties + - code: SchemaNamesConvention + - code: SchemaDescriptionOrTitle +``` + +### Tag: package-2022-11-01-preview + +These settings apply only when `--tag=package-2022-11-01-preview` is specified on the command line. + +```yaml $(tag) == 'package-2022-11-01-preview' +input-file: + - preview/2022-11-01-preview/widgets.json +``` + +### Suppress non-TypeSpec SDK related linting rules + +These set of linting rules aren't applicable to the new TypeSpec SDK code generators so suppressing them here. Eventually we will +opt-out these rules from running in the linting tools for TypeSpec generated swagger files. + +```yaml +suppressions: + - code: AvoidAnonymousTypes + - code: PatchInOperationName + - code: OperationIdNounVerb + - code: RequiredReadOnlyProperties + - code: SchemaNamesConvention + - code: SchemaDescriptionOrTitle +``` + +### Suppress rules that might be fixed + +These set of linting rules we expect to fixed in typespec-autorest emitter but for now suppressing. +Github issue filed at https://github.com/Azure/typespec-azure/issues/2762 + +```yaml +suppressions: + - code: LroExtension + - code: SchemaTypeAndFormat + - code: PathParameterSchema +``` diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/shared.tsp b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/shared.tsp new file mode 100644 index 000000000000..1b94bb705031 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/shared.tsp @@ -0,0 +1,8 @@ +@doc("Faked shared model") +model FakedSharedModel { + @doc("The tag.") + tag: string; + + @doc("The created date.") + createdAt: utcDateTime; +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/stable/2022-12-01/examples/Widgets_CreateOrUpdateWidgetSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/stable/2022-12-01/examples/Widgets_CreateOrUpdateWidgetSample.json new file mode 100644 index 000000000000..dbd333fb52dc --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/stable/2022-12-01/examples/Widgets_CreateOrUpdateWidgetSample.json @@ -0,0 +1,37 @@ +{ + "title": "Widgets_CreateOrUpdateWidget", + "operationId": "Widgets_CreateOrUpdateWidget", + "parameters": { + "widgetName": "name1", + "api-version": "2022-12-01", + "resource": { + "manufacturerId": "manufacturer id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + } + }, + "responses": { + "200": { + "body": { + "name": "name1", + "manufacturerId": "manufacturer id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + } + }, + "201": { + "body": { + "name": "name1", + "manufacturerId": "manufacturer id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/stable/2022-12-01/examples/Widgets_DeleteWidgetSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/stable/2022-12-01/examples/Widgets_DeleteWidgetSample.json new file mode 100644 index 000000000000..c5de7719085d --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/stable/2022-12-01/examples/Widgets_DeleteWidgetSample.json @@ -0,0 +1,29 @@ +{ + "operationId": "Widgets_DeleteWidget", + "title": "Delete widget by widget name using long-running operation.", + "parameters": { + "api-version": "2022-12-01", + "widgetName": "searchbox" + }, + "responses": { + "202": { + "headers": { + "location": "https://contosowidgetmanager.azure.com/operations/00000000-0000-0000-0000-000000000123/result?api-version=2022-12-01", + "operation-location": "https://contosowidgetmanager.azure.com/operations/00000000-0000-0000-0000-000000000123?api-version=2022-12-01" + }, + "body": { + "id": "id1", + "status": "deleted" + } + }, + "default": { + "body": { + "error": { + "code": "Error code", + "message": "Error message", + "details": [] + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/stable/2022-12-01/examples/Widgets_GetWidgetOperationStatusSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/stable/2022-12-01/examples/Widgets_GetWidgetOperationStatusSample.json new file mode 100644 index 000000000000..a840b19f26e6 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/stable/2022-12-01/examples/Widgets_GetWidgetOperationStatusSample.json @@ -0,0 +1,45 @@ +{ + "title": "Widgets_GetWidgetOperationStatus", + "operationId": "Widgets_GetWidgetOperationStatus", + "parameters": { + "widgetName": "name1", + "operationId": "opreation id1", + "api-version": "2022-12-01" + }, + "responses": { + "200": { + "body": { + "id": "opreation id1", + "status": "InProgress", + "error": { + "code": "Error code", + "message": "Error message", + "target": "op1", + "details": [ + { + "code": "code1", + "message": "message1", + "target": "op1", + "details": [], + "innererror": { + "code": "code1" + } + } + ], + "innererror": { + "code": "code1" + } + }, + "result": { + "name": "bingsearch", + "manufacturerId": "manufacturer Id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + }, + "widgetName": "rfazvwnfwwomiwrh" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/stable/2022-12-01/examples/Widgets_GetWidgetSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/stable/2022-12-01/examples/Widgets_GetWidgetSample.json new file mode 100644 index 000000000000..ecab18c65303 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/stable/2022-12-01/examples/Widgets_GetWidgetSample.json @@ -0,0 +1,25 @@ +{ + "operationId": "Widgets_GetWidget", + "title": "Get widget by widget name.", + "parameters": { + "api-version": "2022-12-01", + "widgetName": "searchbox" + }, + "responses": { + "200": { + "body": { + "name": "bingsearch", + "manufacturerId": "a-22-01" + } + }, + "default": { + "body": { + "error": { + "code": "Error code", + "message": "Error message", + "details": [] + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/stable/2022-12-01/examples/Widgets_ListWidgetsSample.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/stable/2022-12-01/examples/Widgets_ListWidgetsSample.json new file mode 100644 index 000000000000..fa68ab418490 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/stable/2022-12-01/examples/Widgets_ListWidgetsSample.json @@ -0,0 +1,27 @@ +{ + "title": "Widgets_ListWidgets", + "operationId": "Widgets_ListWidgets", + "parameters": { + "top": 8, + "skip": 15, + "maxpagesize": 27, + "api-version": "2022-12-01" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "name": "bingsearch", + "manufacturerId": "manufacturer Id1", + "sharedModel": { + "tag": "tag1", + "createdAt": "2023-01-09T02:12:25.689Z" + } + } + ], + "nextLink": "https://microsoft.com/a" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/stable/2022-12-01/widget.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/stable/2022-12-01/widget.json new file mode 100644 index 000000000000..418c1091069f --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/stable/2022-12-01/widget.json @@ -0,0 +1,550 @@ +{ + "swagger": "2.0", + "info": { + "title": "Widget", + "version": "2022-12-01", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "security": [ + { + "AadOauth2Auth": [ + "https://azure.com/.default" + ] + } + ], + "securityDefinitions": { + "AadOauth2Auth": { + "type": "oauth2", + "description": "The Azure Active Directory OAuth2 Flow", + "flow": "accessCode", + "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize", + "scopes": { + "https://azure.com/.default": "" + }, + "tokenUrl": "https://login.microsoftonline.com/common/oauth2/token" + } + }, + "tags": [], + "paths": { + "/widgets": { + "get": { + "operationId": "Widgets_ListWidgets", + "description": "List Widget resources", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/PagedWidgetSuite" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Widgets_ListWidgets": { + "$ref": "./examples/Widgets_ListWidgetsSample.json" + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/widgets/{widgetName}": { + "get": { + "operationId": "Widgets_GetWidget", + "description": "Fetch a Widget by name.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The widget name.", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/WidgetSuite" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Get widget by widget name.": { + "$ref": "./examples/Widgets_GetWidgetSample.json" + } + } + }, + "patch": { + "operationId": "Widgets_CreateOrUpdateWidget", + "description": "Creates or updates a Widget asynchronously.", + "consumes": [ + "application/merge-patch+json" + ], + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The widget name.", + "required": true, + "type": "string" + }, + { + "name": "resource", + "in": "body", + "description": "The resource instance.", + "required": true, + "schema": { + "$ref": "#/definitions/WidgetSuiteCreateOrUpdate" + } + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/WidgetSuite" + }, + "headers": { + "Operation-Location": { + "type": "string", + "format": "uri", + "description": "The location for monitoring the operation state." + } + } + }, + "201": { + "description": "The request has succeeded and a new resource has been created as a result.", + "schema": { + "$ref": "#/definitions/WidgetSuite" + }, + "headers": { + "Operation-Location": { + "type": "string", + "format": "uri", + "description": "The location for monitoring the operation state." + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Widgets_CreateOrUpdateWidget": { + "$ref": "./examples/Widgets_CreateOrUpdateWidgetSample.json" + } + }, + "x-ms-long-running-operation": true + }, + "delete": { + "operationId": "Widgets_DeleteWidget", + "description": "Delete a Widget asynchronously.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The widget name.", + "required": true, + "type": "string" + } + ], + "responses": { + "202": { + "description": "The request has been accepted for processing, but processing has not yet completed.", + "schema": { + "type": "object", + "description": "Provides status details for long running operations.", + "properties": { + "id": { + "type": "string", + "description": "The unique ID of the operation." + }, + "status": { + "$ref": "#/definitions/Azure.Core.Foundations.OperationState", + "description": "The status of the operation" + }, + "error": { + "$ref": "#/definitions/Azure.Core.Foundations.Error", + "description": "Error object that describes the error when status is \"Failed\"." + } + }, + "required": [ + "id", + "status" + ] + }, + "headers": { + "Operation-Location": { + "type": "string", + "format": "uri", + "description": "The location for monitoring the operation state." + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Delete widget by widget name using long-running operation.": { + "$ref": "./examples/Widgets_DeleteWidgetSample.json" + } + }, + "x-ms-long-running-operation": true + } + }, + "/widgets/{widgetName}/operations/{operationId}": { + "get": { + "operationId": "Widgets_GetWidgetOperationStatus", + "description": "Gets status of a Widget operation.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The widget name.", + "required": true, + "type": "string" + }, + { + "name": "operationId", + "in": "path", + "description": "The unique ID of the operation.", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "type": "object", + "description": "Provides status details for long running operations.", + "properties": { + "id": { + "type": "string", + "description": "The unique ID of the operation." + }, + "status": { + "$ref": "#/definitions/Azure.Core.Foundations.OperationState", + "description": "The status of the operation" + }, + "error": { + "$ref": "#/definitions/Azure.Core.Foundations.Error", + "description": "Error object that describes the error when status is \"Failed\"." + }, + "result": { + "$ref": "#/definitions/WidgetSuite", + "description": "The result of the operation." + } + }, + "required": [ + "id", + "status" + ] + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Widgets_GetWidgetOperationStatus": { + "$ref": "./examples/Widgets_GetWidgetOperationStatusSample.json" + } + } + } + } + }, + "definitions": { + "Azure.Core.Foundations.Error": { + "type": "object", + "description": "The error object.", + "properties": { + "code": { + "type": "string", + "description": "One of a server-defined set of error codes." + }, + "message": { + "type": "string", + "description": "A human-readable representation of the error." + }, + "target": { + "type": "string", + "description": "The target of the error." + }, + "details": { + "type": "array", + "description": "An array of details about specific errors that led to this reported error.", + "items": { + "$ref": "#/definitions/Azure.Core.Foundations.Error" + }, + "x-ms-identifiers": [] + }, + "innererror": { + "$ref": "#/definitions/Azure.Core.Foundations.InnerError", + "description": "An object containing more specific information than the current object about the error." + } + }, + "required": [ + "code", + "message" + ] + }, + "Azure.Core.Foundations.ErrorResponse": { + "type": "object", + "description": "A response containing error details.", + "properties": { + "error": { + "$ref": "#/definitions/Azure.Core.Foundations.Error", + "description": "The error object." + } + }, + "required": [ + "error" + ] + }, + "Azure.Core.Foundations.InnerError": { + "type": "object", + "description": "An object containing more specific information about the error. As per Microsoft One API guidelines - https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#handling-errors.", + "properties": { + "code": { + "type": "string", + "description": "One of a server-defined set of error codes." + }, + "innererror": { + "$ref": "#/definitions/Azure.Core.Foundations.InnerError", + "description": "Inner error." + } + } + }, + "Azure.Core.Foundations.OperationState": { + "type": "string", + "description": "Enum describing allowed operation states.", + "enum": [ + "NotStarted", + "Running", + "Succeeded", + "Failed", + "Canceled" + ], + "x-ms-enum": { + "name": "OperationState", + "modelAsString": true, + "values": [ + { + "name": "NotStarted", + "value": "NotStarted", + "description": "The operation has not started." + }, + { + "name": "Running", + "value": "Running", + "description": "The operation is in progress." + }, + { + "name": "Succeeded", + "value": "Succeeded", + "description": "The operation has completed successfully." + }, + { + "name": "Failed", + "value": "Failed", + "description": "The operation has failed." + }, + { + "name": "Canceled", + "value": "Canceled", + "description": "The operation has been canceled by the user." + } + ] + } + }, + "FakedSharedModel": { + "type": "object", + "description": "Faked shared model", + "properties": { + "tag": { + "type": "string", + "description": "The tag." + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "The created date." + } + }, + "required": [ + "tag", + "createdAt" + ] + }, + "FakedSharedModelCreateOrUpdate": { + "type": "object", + "description": "Faked shared model", + "properties": { + "tag": { + "type": "string", + "description": "The tag." + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "The created date." + } + } + }, + "PagedWidgetSuite": { + "type": "object", + "description": "Paged collection of WidgetSuite items", + "properties": { + "value": { + "type": "array", + "description": "The WidgetSuite items on this page", + "items": { + "$ref": "#/definitions/WidgetSuite" + }, + "x-ms-identifiers": [] + }, + "nextLink": { + "type": "string", + "format": "uri", + "description": "The link to the next page of items" + } + }, + "required": [ + "value" + ] + }, + "WidgetSuite": { + "type": "object", + "description": "A widget.", + "properties": { + "name": { + "type": "string", + "description": "The widget name.", + "readOnly": true + }, + "manufacturerId": { + "type": "string", + "description": "The ID of the widget's manufacturer." + }, + "sharedModel": { + "$ref": "#/definitions/FakedSharedModel", + "description": "The faked shared model." + } + }, + "required": [ + "name", + "manufacturerId" + ] + }, + "WidgetSuiteCreateOrUpdate": { + "type": "object", + "description": "A widget.", + "properties": { + "manufacturerId": { + "type": "string", + "description": "The ID of the widget's manufacturer." + }, + "sharedModel": { + "$ref": "#/definitions/FakedSharedModelCreateOrUpdate", + "description": "The faked shared model." + } + } + } + }, + "parameters": { + "Azure.Core.Foundations.ApiVersionParameter": { + "name": "api-version", + "in": "query", + "description": "The API version to use for this operation.", + "required": true, + "type": "string", + "minLength": 1, + "x-ms-parameter-location": "method", + "x-ms-client-name": "apiVersion" + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/tspconfig.yaml b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/tspconfig.yaml new file mode 100644 index 000000000000..2de0c6c6c6cf --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/data-plane/widget2/tspconfig.yaml @@ -0,0 +1,47 @@ +parameters: + "service-dir": + default: "sdk/widget" + "dependencies": + default: "" +emit: + - "@azure-tools/typespec-autorest" +linter: + extends: + - "@azure-tools/typespec-azure-rulesets/data-plane" +options: + "@azure-tools/typespec-autorest": + # TODO: Does anything need this set, if it's not used in output-file? + azure-resource-provider-folder: "data-plane" + emit-lro-options: "none" + emitter-output-dir: "{project-root}" + output-file: "{version-status}/{version}/widgets.json" + "@azure-tools/typespec-python": + package-dir: "azure-widget" + namespace: "azure.widget" + generate-test: true + generate-sample: true + flavor: azure + "@azure-tools/typespec-csharp": + package-dir: "Azure.Widget" + clear-output-folder: true + model-namespace: false + namespace: "{package-dir}" + flavor: azure + "@azure-tools/typespec-ts": + package-dir: "widget-rest" + package-details: + name: "@azure-rest/azure-widget" + flavor: azure + "@azure-tools/typespec-java": + package-dir: "azure-widget" + namespace: com.azure.widget + flavor: azure + "@azure-tools/typespec-go": + module: "github.com/Azure/azure-sdk-for-go/{service-dir}/{package-dir}" + service-dir: "sdk/widget" + package-dir: "azmanager" + module-version: "0.0.1" + generate-fakes: true + inject-spans: true + single-client: true + slice-elements-byval: true diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/employee.tsp b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/employee.tsp new file mode 100644 index 000000000000..d77152e040ec --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/employee.tsp @@ -0,0 +1,63 @@ +import "@typespec/rest"; +import "@typespec/http"; +import "@azure-tools/typespec-azure-core"; +import "@azure-tools/typespec-azure-resource-manager"; + +using TypeSpec.Rest; +using TypeSpec.Http; +using Azure.Core; +using Azure.ResourceManager; + +namespace WidgetManagement; + +/** Employee resource */ +model Employee is TrackedResource { + ...ResourceNameParameter; +} + +/** Employee properties */ +model EmployeeProperties { + /** Age of employee */ + age?: int32; + + /** City of employee */ + city?: string; + + /** Profile of employee */ + @encode("base64url") + profile?: bytes; + + /** The status of the last operation. */ + @visibility(Lifecycle.Read) + provisioningState?: ProvisioningState; +} + +/** The resource provisioning state. */ +@lroStatus +union ProvisioningState { + ResourceProvisioningState, + + /** The resource is being provisioned */ + Provisioning: "Provisioning", + + /** The resource is updating */ + Updating: "Updating", + + /** The resource is being deleted */ + Deleting: "Deleting", + + /** The resource create request has been accepted */ + Accepted: "Accepted", + + string, +} + +@armResourceOperations +interface Employees { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + update is ArmResourcePatchSync; + delete is ArmResourceDeleteWithoutOkAsync; + listByResourceGroup is ArmResourceListByParent; + listBySubscription is ArmListBySubscription; +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Employees_CreateOrUpdate.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Employees_CreateOrUpdate.json new file mode 100644 index 000000000000..4a13a329e3b8 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Employees_CreateOrUpdate.json @@ -0,0 +1,76 @@ +{ + "title": "Employees_CreateOrUpdate", + "operationId": "Employees_CreateOrUpdate", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi", + "employeeName": "9KF-f-8b", + "resource": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl" + } + }, + "responses": { + "200": { + "body": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/le-8MU--J3W6q8D386p3-iT3", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + }, + "201": { + "headers": { + "Azure-AsyncOperation": "https://contoso.com/operationstatus" + }, + "body": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/9KF-f-8b", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Employees_Delete.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Employees_Delete.json new file mode 100644 index 000000000000..15176d86b029 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Employees_Delete.json @@ -0,0 +1,19 @@ +{ + "title": "Employees_Delete", + "operationId": "Employees_Delete", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi", + "employeeName": "5vX--BxSu3ux48rI4O9OQ569" + }, + "responses": { + "202": { + "headers": { + "Retry-After": 30, + "location": "https://contoso.com/operationstatus" + } + }, + "204": {} + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Employees_Get.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Employees_Get.json new file mode 100644 index 000000000000..eb1917859e24 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Employees_Get.json @@ -0,0 +1,37 @@ +{ + "title": "Employees_Get", + "operationId": "Employees_Get", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi", + "employeeName": "le-8MU--J3W6q8D386p3-iT3" + }, + "responses": { + "200": { + "body": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/le-8MU--J3W6q8D386p3-iT3", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Employees_ListByResourceGroup.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Employees_ListByResourceGroup.json new file mode 100644 index 000000000000..860fab85a9b8 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Employees_ListByResourceGroup.json @@ -0,0 +1,41 @@ +{ + "title": "Employees_ListByResourceGroup", + "operationId": "Employees_ListByResourceGroup", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/test", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + ], + "nextLink": "https://microsoft.com/a" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Employees_ListBySubscription.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Employees_ListBySubscription.json new file mode 100644 index 000000000000..18432d58de37 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Employees_ListBySubscription.json @@ -0,0 +1,40 @@ +{ + "title": "Employees_ListBySubscription", + "operationId": "Employees_ListBySubscription", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/test", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + ], + "nextLink": "https://microsoft.com/a" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Employees_Update.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Employees_Update.json new file mode 100644 index 000000000000..de46fc8ef2e8 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Employees_Update.json @@ -0,0 +1,47 @@ +{ + "title": "Employees_Update", + "operationId": "Employees_Update", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi", + "employeeName": "-XhyNJ--", + "properties": { + "tags": { + "key7952": "no" + }, + "properties": { + "age": 24, + "city": "uyfg", + "profile": "oapgijcswfkruiuuzbwco" + } + } + }, + "responses": { + "200": { + "body": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/contoso/providers/Microsoft.Contoso/employees/test", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Operations_List.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Operations_List.json new file mode 100644 index 000000000000..4d74e755c020 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/examples/2021-11-01/Operations_List.json @@ -0,0 +1,28 @@ +{ + "title": "Operations_List", + "operationId": "Operations_List", + "parameters": { + "api-version": "2021-11-01" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "name": "ymeow", + "isDataAction": true, + "display": { + "provider": "qxyznq", + "resource": "bqfwkox", + "operation": "td", + "description": "yvgkhsuwartgxb" + }, + "origin": "user", + "actionType": "Internal" + } + ], + "nextLink": "https://sample.com/nextLink" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/main.tsp b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/main.tsp new file mode 100644 index 000000000000..6a7f5047f36b --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/main.tsp @@ -0,0 +1,35 @@ +import "@typespec/http"; +import "@typespec/rest"; +import "@typespec/versioning"; +import "@azure-tools/typespec-azure-core"; +import "@azure-tools/typespec-azure-resource-manager"; +import "./employee.tsp"; + +using TypeSpec.Http; +using TypeSpec.Rest; +using TypeSpec.Versioning; +using Azure.Core; +using Azure.ResourceManager; + +/** Microsoft.Contoso Resource Provider management API. */ +@armProviderNamespace +@service(#{ title: "WidgetManagement" }) +@versioned(WidgetManagement.Versions) +namespace WidgetManagement; + +/** The available API versions. */ +enum Versions { + /** 2021-10-01-preview version */ + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + @useDependency(Azure.Core.Versions.v1_0_Preview_2) + @armCommonTypesVersion(Azure.ResourceManager.CommonTypes.Versions.v5) + v2021_10_01_preview: "2021-10-01-preview", + + /** 2021-11-01 version */ + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + @useDependency(Azure.Core.Versions.v1_0_Preview_2) + @armCommonTypesVersion(Azure.ResourceManager.CommonTypes.Versions.v5) + v2021_11_01: "2021-11-01", +} + +interface Operations extends Azure.ResourceManager.Operations {} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/readme.md b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/readme.md new file mode 100644 index 000000000000..98556255f8f4 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/readme.md @@ -0,0 +1,42 @@ +# WidgetManagement + +> see https://aka.ms/autorest +> This is the AutoRest configuration file for WidgetManagement. + +## Getting Started + +To build the SDKs for My API, simply install AutoRest via `npm` (`npm install -g autorest`) and then run: + +> `autorest readme.md` +> To see additional help and options, run: + +> `autorest --help` +> For other options on installation see [Installing AutoRest](https://aka.ms/autorest/install) on the AutoRest github page. + +--- + +## Configuration + +### Basic Information + +These are the global settings. + +```yaml +openapi-type: arm +openapi-subtype: rpaas +tag: package-2021-11-01 +``` + +### Tag: package-2021-11-01 + +These settings apply only when `--tag=package-2021-11-01` is specified on the command line. + +```yaml $(tag) == 'package-2021-11-01' +input-file: + - stable/2021-11-01/widgetmanagement.json +suppressions: + - code: PathContainsResourceType + - code: PathResourceProviderMatchNamespace +``` + +--- diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/shared.tsp b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/shared.tsp new file mode 100644 index 000000000000..1b94bb705031 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/shared.tsp @@ -0,0 +1,8 @@ +@doc("Faked shared model") +model FakedSharedModel { + @doc("The tag.") + tag: string; + + @doc("The created date.") + createdAt: utcDateTime; +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Employees_CreateOrUpdate.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Employees_CreateOrUpdate.json new file mode 100644 index 000000000000..4a13a329e3b8 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Employees_CreateOrUpdate.json @@ -0,0 +1,76 @@ +{ + "title": "Employees_CreateOrUpdate", + "operationId": "Employees_CreateOrUpdate", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi", + "employeeName": "9KF-f-8b", + "resource": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl" + } + }, + "responses": { + "200": { + "body": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/le-8MU--J3W6q8D386p3-iT3", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + }, + "201": { + "headers": { + "Azure-AsyncOperation": "https://contoso.com/operationstatus" + }, + "body": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/9KF-f-8b", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Employees_Delete.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Employees_Delete.json new file mode 100644 index 000000000000..15176d86b029 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Employees_Delete.json @@ -0,0 +1,19 @@ +{ + "title": "Employees_Delete", + "operationId": "Employees_Delete", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi", + "employeeName": "5vX--BxSu3ux48rI4O9OQ569" + }, + "responses": { + "202": { + "headers": { + "Retry-After": 30, + "location": "https://contoso.com/operationstatus" + } + }, + "204": {} + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Employees_Get.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Employees_Get.json new file mode 100644 index 000000000000..eb1917859e24 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Employees_Get.json @@ -0,0 +1,37 @@ +{ + "title": "Employees_Get", + "operationId": "Employees_Get", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi", + "employeeName": "le-8MU--J3W6q8D386p3-iT3" + }, + "responses": { + "200": { + "body": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/le-8MU--J3W6q8D386p3-iT3", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Employees_ListByResourceGroup.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Employees_ListByResourceGroup.json new file mode 100644 index 000000000000..860fab85a9b8 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Employees_ListByResourceGroup.json @@ -0,0 +1,41 @@ +{ + "title": "Employees_ListByResourceGroup", + "operationId": "Employees_ListByResourceGroup", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/test", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + ], + "nextLink": "https://microsoft.com/a" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Employees_ListBySubscription.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Employees_ListBySubscription.json new file mode 100644 index 000000000000..18432d58de37 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Employees_ListBySubscription.json @@ -0,0 +1,40 @@ +{ + "title": "Employees_ListBySubscription", + "operationId": "Employees_ListBySubscription", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/rgopenapi/providers/Microsoft.Contoso/employees/test", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + ], + "nextLink": "https://microsoft.com/a" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Employees_Update.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Employees_Update.json new file mode 100644 index 000000000000..de46fc8ef2e8 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Employees_Update.json @@ -0,0 +1,47 @@ +{ + "title": "Employees_Update", + "operationId": "Employees_Update", + "parameters": { + "api-version": "2021-11-01", + "subscriptionId": "11809CA1-E126-4017-945E-AA795CD5C5A9", + "resourceGroupName": "rgopenapi", + "employeeName": "-XhyNJ--", + "properties": { + "tags": { + "key7952": "no" + }, + "properties": { + "age": 24, + "city": "uyfg", + "profile": "oapgijcswfkruiuuzbwco" + } + } + }, + "responses": { + "200": { + "body": { + "properties": { + "age": 30, + "city": "gydhnntudughbmxlkyzrskcdkotrxn", + "profile": "ms", + "provisioningState": "Succeeded" + }, + "tags": { + "key2913": "urperxmkkhhkp" + }, + "location": "itajgxyqozseoygnl", + "id": "/subscriptions/11809CA1-E126-4017-945E-AA795CD5C5A9/resourceGroups/contoso/providers/Microsoft.Contoso/employees/test", + "name": "xepyxhpb", + "type": "svvamxrdnnv", + "systemData": { + "createdBy": "iewyxsnriqktsvp", + "createdByType": "User", + "createdAt": "2023-05-19T00:28:48.610Z", + "lastModifiedBy": "xrchbnnuzierzpxw", + "lastModifiedByType": "User", + "lastModifiedAt": "2023-05-19T00:28:48.610Z" + } + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Operations_List.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Operations_List.json new file mode 100644 index 000000000000..4d74e755c020 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/examples/Operations_List.json @@ -0,0 +1,28 @@ +{ + "title": "Operations_List", + "operationId": "Operations_List", + "parameters": { + "api-version": "2021-11-01" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "name": "ymeow", + "isDataAction": true, + "display": { + "provider": "qxyznq", + "resource": "bqfwkox", + "operation": "td", + "description": "yvgkhsuwartgxb" + }, + "origin": "user", + "actionType": "Internal" + } + ], + "nextLink": "https://sample.com/nextLink" + } + } + } +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/widgetmanagement.json b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/widgetmanagement.json new file mode 100644 index 000000000000..c8a8b95d6b40 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/stable/2021-11-01/widgetmanagement.json @@ -0,0 +1,557 @@ +{ + "swagger": "2.0", + "info": { + "title": "WidgetManagement", + "version": "2021-11-01", + "description": "Microsoft.Contoso Resource Provider management API.", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "host": "management.azure.com", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "security": [ + { + "azure_auth": [ + "user_impersonation" + ] + } + ], + "securityDefinitions": { + "azure_auth": { + "type": "oauth2", + "description": "Azure Active Directory OAuth2 Flow.", + "flow": "implicit", + "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize", + "scopes": { + "user_impersonation": "impersonate your user account" + } + } + }, + "tags": [ + { + "name": "Operations" + }, + { + "name": "Employees" + } + ], + "paths": { + "/providers/WidgetManagement/operations": { + "get": { + "operationId": "Operations_List", + "tags": [ + "Operations" + ], + "description": "List the operations for the provider", + "parameters": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/OperationListResult" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "Operations_List": { + "$ref": "./examples/Operations_List.json" + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/providers/WidgetManagement/employees": { + "get": { + "operationId": "Employees_ListBySubscription", + "tags": [ + "Employees" + ], + "description": "List Employee resources by subscription ID", + "parameters": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "#/definitions/EmployeeListResult" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "Employees_ListBySubscription": { + "$ref": "./examples/Employees_ListBySubscription.json" + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/WidgetManagement/employees": { + "get": { + "operationId": "Employees_ListByResourceGroup", + "tags": [ + "Employees" + ], + "description": "List Employee resources by resource group", + "parameters": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "#/definitions/EmployeeListResult" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "Employees_ListByResourceGroup": { + "$ref": "./examples/Employees_ListByResourceGroup.json" + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/WidgetManagement/employees/{employeeName}": { + "get": { + "operationId": "Employees_Get", + "tags": [ + "Employees" + ], + "description": "Get a Employee", + "parameters": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "employeeName", + "in": "path", + "description": "The name of the Employee", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9-]{3,24}$" + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "#/definitions/Employee" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "Employees_Get": { + "$ref": "./examples/Employees_Get.json" + } + } + }, + "put": { + "operationId": "Employees_CreateOrUpdate", + "tags": [ + "Employees" + ], + "description": "Create a Employee", + "parameters": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "employeeName", + "in": "path", + "description": "The name of the Employee", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9-]{3,24}$" + }, + { + "name": "resource", + "in": "body", + "description": "Resource create parameters.", + "required": true, + "schema": { + "$ref": "#/definitions/Employee" + } + } + ], + "responses": { + "200": { + "description": "Resource 'Employee' update operation succeeded", + "schema": { + "$ref": "#/definitions/Employee" + } + }, + "201": { + "description": "Resource 'Employee' create operation succeeded", + "schema": { + "$ref": "#/definitions/Employee" + }, + "headers": { + "Azure-AsyncOperation": { + "type": "string", + "description": "A link to the status monitor" + }, + "Retry-After": { + "type": "integer", + "format": "int32", + "description": "The Retry-After header can indicate how long the client should wait before polling the operation status." + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "Employees_CreateOrUpdate": { + "$ref": "./examples/Employees_CreateOrUpdate.json" + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-long-running-operation": true + }, + "patch": { + "operationId": "Employees_Update", + "tags": [ + "Employees" + ], + "description": "Update a Employee", + "parameters": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "employeeName", + "in": "path", + "description": "The name of the Employee", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9-]{3,24}$" + }, + { + "name": "properties", + "in": "body", + "description": "The resource properties to be updated.", + "required": true, + "schema": { + "$ref": "#/definitions/EmployeeUpdate" + } + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "#/definitions/Employee" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "Employees_Update": { + "$ref": "./examples/Employees_Update.json" + } + } + }, + "delete": { + "operationId": "Employees_Delete", + "tags": [ + "Employees" + ], + "description": "Delete a Employee", + "parameters": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "employeeName", + "in": "path", + "description": "The name of the Employee", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9-]{3,24}$" + } + ], + "responses": { + "202": { + "description": "Resource deletion accepted.", + "headers": { + "Location": { + "type": "string", + "description": "The Location header contains the URL where the status of the long running operation can be checked." + }, + "Retry-After": { + "type": "integer", + "format": "int32", + "description": "The Retry-After header can indicate how long the client should wait before polling the operation status." + } + } + }, + "204": { + "description": "Resource does not exist." + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-examples": { + "Employees_Delete": { + "$ref": "./examples/Employees_Delete.json" + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "location" + }, + "x-ms-long-running-operation": true + } + } + }, + "definitions": { + "Azure.ResourceManager.CommonTypes.TrackedResourceUpdate": { + "type": "object", + "title": "Tracked Resource", + "description": "The resource model definition for an Azure Resource Manager tracked top level resource which has 'tags' and a 'location'", + "properties": { + "tags": { + "type": "object", + "description": "Resource tags.", + "additionalProperties": { + "type": "string" + } + } + }, + "allOf": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/Resource" + } + ] + }, + "Employee": { + "type": "object", + "description": "Employee resource", + "properties": { + "properties": { + "$ref": "#/definitions/EmployeeProperties", + "description": "The resource-specific properties for this resource." + } + }, + "allOf": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/TrackedResource" + } + ] + }, + "EmployeeListResult": { + "type": "object", + "description": "The response of a Employee list operation.", + "properties": { + "value": { + "type": "array", + "description": "The Employee items on this page", + "items": { + "$ref": "#/definitions/Employee" + } + }, + "nextLink": { + "type": "string", + "format": "uri", + "description": "The link to the next page of items" + } + }, + "required": [ + "value" + ] + }, + "EmployeeProperties": { + "type": "object", + "description": "Employee properties", + "properties": { + "age": { + "type": "integer", + "format": "int32", + "description": "Age of employee" + }, + "city": { + "type": "string", + "description": "City of employee" + }, + "profile": { + "type": "string", + "format": "base64url", + "description": "Profile of employee" + }, + "provisioningState": { + "$ref": "#/definitions/ProvisioningState", + "description": "The status of the last operation.", + "readOnly": true + } + } + }, + "EmployeeUpdate": { + "type": "object", + "description": "Employee resource", + "properties": { + "properties": { + "$ref": "#/definitions/EmployeeProperties", + "description": "The resource-specific properties for this resource." + } + }, + "allOf": [ + { + "$ref": "#/definitions/Azure.ResourceManager.CommonTypes.TrackedResourceUpdate" + } + ] + }, + "ProvisioningState": { + "type": "string", + "description": "The resource provisioning state.", + "enum": [ + "Succeeded", + "Failed", + "Canceled", + "Provisioning", + "Updating", + "Deleting", + "Accepted" + ], + "x-ms-enum": { + "name": "ProvisioningState", + "modelAsString": true, + "values": [ + { + "name": "Succeeded", + "value": "Succeeded", + "description": "Resource has been created." + }, + { + "name": "Failed", + "value": "Failed", + "description": "Resource creation failed." + }, + { + "name": "Canceled", + "value": "Canceled", + "description": "Resource creation was canceled." + }, + { + "name": "Provisioning", + "value": "Provisioning", + "description": "The resource is being provisioned" + }, + { + "name": "Updating", + "value": "Updating", + "description": "The resource is updating" + }, + { + "name": "Deleting", + "value": "Deleting", + "description": "The resource is being deleted" + }, + { + "name": "Accepted", + "value": "Accepted", + "description": "The resource create request has been accepted" + } + ] + }, + "readOnly": true + } + }, + "parameters": {} +} diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/tspconfig.yaml b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/tspconfig.yaml new file mode 100644 index 000000000000..52522ba9eca6 --- /dev/null +++ b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/WidgetManagement2/tspconfig.yaml @@ -0,0 +1,49 @@ +parameters: + "service-dir": + default: "sdk/widgetmanagement" +emit: + - "@azure-tools/typespec-autorest" +options: + "@azure-tools/typespec-autorest": + use-read-only-status-schema: true + emitter-output-dir: "{project-root}" + # TODO: Does anything need this set, if it's not used in output-file? Currently required by TSV. + azure-resource-provider-folder: "resource-manager" + output-file: "{version-status}/{version}/widgetmanagement.json" + arm-types-dir: "{project-root}/../../../../common-types/resource-management" + "@azure-tools/typespec-csharp": + flavor: azure + package-dir: "Azure.ResourceManager.Widget" + clear-output-folder: true + model-namespace: true + namespace: "{package-dir}" + "@azure-tools/typespec-python": + package-dir: "azure-mgmt-widget" + namespace: "azure.mgmt.widget" + generate-test: true + generate-sample: true + flavor: "azure" + "@azure-tools/typespec-java": + package-dir: "azure-resourcemanager-widget" + namespace: "com.azure.resourcemanager.widget" + service-name: "Widget" # human-readable service name, whitespace allowed + flavor: azure + "@azure-tools/typespec-ts": + package-dir: "arm-widget" + flavor: azure + experimental-extensible-enums: true + package-details: + name: "@azure/arm-widget" + "@azure-tools/typespec-go": + service-dir: "sdk/resourcemanager/widget" + package-dir: "armwidget" + module: "github.com/Azure/azure-sdk-for-go/{service-dir}/{package-dir}" + fix-const-stuttering: true + flavor: "azure" + generate-samples: true + generate-fakes: true + head-as-boolean: true + inject-spans: true +linter: + extends: + - "@azure-tools/typespec-azure-rulesets/resource-manager" diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/readme.md b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/Microsoft.Service2/readme.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/readme.md b/eng/tools/spec-gen-sdk-runner/test/fixtures/specification/service2/resource-manager/readme.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/eng/tools/spec-gen-sdk-runner/test/log.test.ts b/eng/tools/spec-gen-sdk-runner/test/log.test.ts index 60c3fb569262..527e13bb95c8 100644 --- a/eng/tools/spec-gen-sdk-runner/test/log.test.ts +++ b/eng/tools/spec-gen-sdk-runner/test/log.test.ts @@ -1,11 +1,11 @@ -import { describe, test, expect, vi, beforeEach, afterEach } from "vitest"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { - logMessage, - LogLevel, LogIssueType, + LogLevel, + logMessage, + setVsoVariable, vsoAddAttachment, vsoLogIssue, - setVsoVariable, } from "../src/log.js"; const logSpy = vi.spyOn(console, "log").mockImplementation(() => { diff --git a/eng/tools/spec-gen-sdk-runner/test/spec-helpers.test.ts b/eng/tools/spec-gen-sdk-runner/test/spec-helpers.test.ts index 2eff1d38de36..696e3132ca4c 100644 --- a/eng/tools/spec-gen-sdk-runner/test/spec-helpers.test.ts +++ b/eng/tools/spec-gen-sdk-runner/test/spec-helpers.test.ts @@ -1,13 +1,17 @@ -import { describe, test, expect, vi, beforeEach } from "vitest"; -import { detectChangedSpecConfigFiles, groupSpecConfigPaths } from "../src/spec-helpers.js"; -import { SpecGenSdkCmdInput } from "../src/types.js"; -import { fileURLToPath } from "node:url"; import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { + detectChangedSpecConfigFiles, + groupSpecConfigPaths, + processTypeSpecProjectsV2FolderStructure, +} from "../src/spec-helpers.js"; +import { SpecGenSdkCmdInput } from "../src/types.js"; import { type ChangedSpecs, type SpecConfigs, - normalizePath, getChangedFiles, + normalizePath, } from "../src/utils.js"; vi.mock("../src/utils.js", async () => { @@ -184,6 +188,189 @@ describe("detectChangedSpecConfigFiles", () => { typespecProject: "specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", }); }); + + test("case with V2 folder structure - resource-manager", () => { + vi.mocked(getChangedFiles).mockReturnValue([ + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/tspconfig.yaml", + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/main.tsp", + "specification/service1/resource-manager/readme.md", + ]); + + const result = detectChangedSpecConfigFiles(mockCommandInput); + + expect(result).toHaveLength(1); + const normalizedResult = normalizeResultItem(result[0]); + + // In V2 structure, the TypeSpec project should be correctly associated with the readme + expect(normalizedResult).toEqual({ + specs: [ + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/tspconfig.yaml", + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/main.tsp", + ], + readmeMd: + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/readme.md", + typespecProject: + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/tspconfig.yaml", + }); + }); + + test("case with V2 folder structure - data-plane", () => { + vi.mocked(getChangedFiles).mockReturnValue([ + "specification/service1/data-plane/widget/tspconfig.yaml", + "specification/service1/data-plane/widget/main.tsp", + "specification/service1/data-plane/readme.md", + ]); + + const result = detectChangedSpecConfigFiles(mockCommandInput); + + expect(result).toHaveLength(1); + const normalizedResult = normalizeResultItem(result[0]); + + // In V2 structure, the TypeSpec project should be correctly associated with the readme + expect(normalizedResult).toEqual({ + specs: [ + "specification/service1/data-plane/widget/tspconfig.yaml", + "specification/service1/data-plane/widget/main.tsp", + ], + readmeMd: "specification/service1/data-plane/widget/readme.md", + typespecProject: "specification/service1/data-plane/widget/tspconfig.yaml", + }); + }); + + test("case with V2 folder structure - nested subfolders", () => { + vi.mocked(getChangedFiles).mockReturnValue([ + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/create.json", + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/servicecontrol.json", + "specification/service1/resource-manager/Microsoft.Service1/readme.md", + "specification/service1/resource-manager/readme.md", + ]); + + const result = detectChangedSpecConfigFiles(mockCommandInput); + + expect(result).toHaveLength(1); + const normalizedResult = normalizeResultItem(result[0]); + + // The deepest TypeSpec project should be used, and both readme files should be cleaned up + expect(normalizedResult).toEqual({ + specs: [ + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/examples/2021-11-01/create.json", + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/stable/2021-11-01/servicecontrol.json", + ], + readmeMd: + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/readme.md", + typespecProject: + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/tspconfig.yaml", + }); + }); + + test("case with V2 folder structure mixed with old structure", () => { + vi.mocked(getChangedFiles).mockReturnValue([ + // V2 folder structure + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/tspconfig.yaml", + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/main.tsp", + "specification/service1/resource-manager/readme.md", + // Old folder structure + "specification/contosowidgetmanager/Contoso.WidgetManager/client.tsp", + "specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", + "specification/contosowidgetmanager/data-plane/readme.md", + ]); + + const result = detectChangedSpecConfigFiles(mockCommandInput); + + expect(result).toHaveLength(2); + + // First result should be for the V2 structure + const normalizedV2Result = normalizeResultItem(result[0]); + expect(normalizedV2Result).toEqual({ + specs: [ + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/tspconfig.yaml", + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/main.tsp", + ], + readmeMd: + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/readme.md", + typespecProject: + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/tspconfig.yaml", + }); + + // Second result should be for the old structure + const normalizedOldResult = normalizeResultItem(result[1]); + expect(normalizedOldResult).toEqual({ + specs: [ + "specification/contosowidgetmanager/data-plane/readme.md", + "specification/contosowidgetmanager/Contoso.WidgetManager/client.tsp", + "specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", + ], + readmeMd: "specification/contosowidgetmanager/data-plane/readme.md", + typespecProject: "specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", + }); + }); + + test("case with multiple V2 folder structures", () => { + vi.mocked(getChangedFiles).mockReturnValue([ + // First service + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/tspconfig.yaml", + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/main.tsp", + "specification/service1/resource-manager/readme.md", + // Second service + "specification/service2/data-plane/widget2/tspconfig.yaml", + "specification/service2/data-plane/widget2/main.tsp", + "specification/service2/data-plane/readme.md", + ]); + + const result = detectChangedSpecConfigFiles(mockCommandInput); + + expect(result).toHaveLength(2); + + // Results for both services should be properly processed with V2 structure + const service1Result = result.find((r) => r.typespecProject?.includes("service1")); + const service2Result = result.find((r) => r.typespecProject?.includes("service2")); + + expect(normalizeResultItem(service1Result!)).toEqual({ + specs: [ + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/tspconfig.yaml", + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/main.tsp", + ], + readmeMd: + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/readme.md", + typespecProject: + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/tspconfig.yaml", + }); + + expect(normalizeResultItem(service2Result!)).toEqual({ + specs: [ + "specification/service2/data-plane/widget2/tspconfig.yaml", + "specification/service2/data-plane/widget2/main.tsp", + ], + readmeMd: "specification/service2/data-plane/widget2/readme.md", + typespecProject: "specification/service2/data-plane/widget2/tspconfig.yaml", + }); + }); + + test("case with V2 folder structure - cross-platform path separators", () => { + // Mock getChangedFiles to return paths with both forward and backslashes + vi.mocked(getChangedFiles).mockReturnValue([ + String.raw`specification\service1\resource-manager\Microsoft.Service1\WidgetManagement\tspconfig.yaml`, + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/main.tsp", + String.raw`specification\service1\resource-manager\readme.md`, + ]); + + const result = detectChangedSpecConfigFiles(mockCommandInput); + + expect(result).toHaveLength(1); + const normalizedResult = normalizeResultItem(result[0]); + + // The function should handle mixed path separators correctly + expect(normalizedResult).toEqual({ + specs: [ + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/tspconfig.yaml", + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/main.tsp", + ], + readmeMd: + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/readme.md", + typespecProject: + "specification/service1/resource-manager/Microsoft.Service1/WidgetManagement/tspconfig.yaml", + }); + }); }); describe("groupSpecConfigPaths", () => { @@ -395,3 +582,156 @@ describe("groupSpecConfigPaths", () => { expect(computeDpResult).toBeUndefined(); }); }); + +describe("processTypeSpecProjectsV2FolderStructure", () => { + test("should process resource-manager structure and return ChangedSpecs with correct paths", () => { + // Setup test data + const readmeMDResult = { + "specification/service/resource-manager": [ + "specification/service/resource-manager/readme.md", + ], + "specification/service/resource-manager/Microsoft.Service": [ + "specification/service/resource-manager/Microsoft.Service/readme.md", + ], + }; + + const typespecProjectResult = { + "specification/service/resource-manager/Microsoft.Service": [ + "specification/service/resource-manager/Microsoft.Service/tspconfig.yaml", + "specification/service/resource-manager/Microsoft.Service/main.tsp", + ], + }; + + // Run the function + const result = processTypeSpecProjectsV2FolderStructure(readmeMDResult, typespecProjectResult); + + // Normalize results for comparison + const normalizedResults = result.map((item) => normalizeResultItem(item)); + + // Verify results + expect(normalizedResults).toHaveLength(1); + + // Check structure of the returned ChangedSpecs object + const spec = normalizedResults[0]; + expect(spec.typespecProject).toBe( + normalizePath("specification/service/resource-manager/Microsoft.Service/tspconfig.yaml"), + ); + expect(spec.readmeMd).toBe( + normalizePath("specification/service/resource-manager/Microsoft.Service/readme.md"), + ); + expect(spec.specs).toContain( + normalizePath("specification/service/resource-manager/Microsoft.Service/tspconfig.yaml"), + ); + expect(spec.specs).toContain( + normalizePath("specification/service/resource-manager/Microsoft.Service/main.tsp"), + ); + expect(spec.specs).toContain( + normalizePath("specification/service/resource-manager/Microsoft.Service/readme.md"), + ); + + // Verify the readmeMDResult was modified as expected + expect(readmeMDResult).not.toHaveProperty("specification/service/resource-manager"); + expect(readmeMDResult).not.toHaveProperty( + "specification/service/resource-manager/Microsoft.Service", + ); + + // Verify the typespecProjectResult was modified as expected + expect(typespecProjectResult).not.toHaveProperty( + "specification/service/resource-manager/Microsoft.Service", + ); + }); + + test("should process data-plane structure and clean related readme entries", () => { + // Setup test data + const readmeMDResult = { + "specification/service/data-plane": ["specification/service/data-plane/readme.md"], + "specification/otherservice/data-plane": ["specification/otherservice/data-plane/readme.md"], + }; + + const typespecProjectResult = { + "specification/service/data-plane/client": [ + "specification/service/data-plane/client/tspconfig.yaml", + "specification/service/data-plane/client/main.tsp", + ], + }; + + // Run the function + const result = processTypeSpecProjectsV2FolderStructure(readmeMDResult, typespecProjectResult); + + // Normalize results for comparison + const normalizedResults = result.map((item) => normalizeResultItem(item)); + + // Verify results + expect(normalizedResults).toHaveLength(1); + + // Check that the data-plane structure was processed correctly + const spec = normalizedResults[0]; + expect(spec.typespecProject).toBe( + normalizePath("specification/service/data-plane/client/tspconfig.yaml"), + ); + expect(spec.readmeMd).toBeUndefined(); // No direct readme in the client folder + expect(spec.specs).toContain( + normalizePath("specification/service/data-plane/client/tspconfig.yaml"), + ); + expect(spec.specs).toContain(normalizePath("specification/service/data-plane/client/main.tsp")); + + // Verify related readme was removed + expect(readmeMDResult).not.toHaveProperty("specification/service/data-plane"); + + // Verify unrelated readme remains + expect(readmeMDResult).toHaveProperty("specification/otherservice/data-plane"); + + // Verify the typespecProjectResult was cleaned + expect(typespecProjectResult).not.toHaveProperty("specification/service/data-plane/client"); + }); + + test("should handle multiple levels in the folder structure", () => { + // Setup test data with nested structure + const readmeMDResult = { + "specification/service/resource-manager": [ + "specification/service/resource-manager/readme.md", + ], + "specification/service/resource-manager/Microsoft.Service": [ + "specification/service/resource-manager/Microsoft.Service/readme.md", + ], + "specification/service/resource-manager/Microsoft.Service/nested/subfolder": [ + "specification/service/resource-manager/Microsoft.Service/nested/subfolder/readme.md", + ], + }; + + const typespecProjectResult = { + "specification/service/resource-manager/Microsoft.Service/nested/subfolder": [ + "specification/service/resource-manager/Microsoft.Service/nested/subfolder/tspconfig.yaml", + "specification/service/resource-manager/Microsoft.Service/nested/subfolder/main.tsp", + ], + }; + + // Run the function + const result = processTypeSpecProjectsV2FolderStructure(readmeMDResult, typespecProjectResult); + + // Normalize results for comparison + const normalizedResults = result.map((item) => normalizeResultItem(item)); + + // Verify results + expect(normalizedResults).toHaveLength(1); + + // Check that the deeply nested structure was processed correctly + const spec = normalizedResults[0]; + expect(spec.readmeMd).toBe( + normalizePath( + "specification/service/resource-manager/Microsoft.Service/nested/subfolder/readme.md", + ), + ); + + // Verify parent readme entries were removed + expect(readmeMDResult).not.toHaveProperty("specification/service/resource-manager"); + expect(readmeMDResult).not.toHaveProperty( + "specification/service/resource-manager/Microsoft.Service", + ); + }); + + test("should handle empty inputs", () => { + const result = processTypeSpecProjectsV2FolderStructure({}, {}); + expect(result).toHaveLength(0); + }); +}); diff --git a/eng/tools/spec-gen-sdk-runner/test/utils/createCombinedSpecs.test.ts b/eng/tools/spec-gen-sdk-runner/test/utils/createCombinedSpecs.test.ts index 591a997b5109..c6d72817c6cc 100644 --- a/eng/tools/spec-gen-sdk-runner/test/utils/createCombinedSpecs.test.ts +++ b/eng/tools/spec-gen-sdk-runner/test/utils/createCombinedSpecs.test.ts @@ -1,6 +1,6 @@ -import { describe, test, expect } from "vitest"; -import { createCombinedSpecs, type SpecResults } from "../../src/utils.js"; import path from "node:path"; +import { describe, expect, test } from "vitest"; +import { createCombinedSpecs, type SpecResults } from "../../src/utils.js"; describe("createCombinedSpecs", () => { test("combines specs from readme and typespec paths", () => { diff --git a/eng/tools/spec-gen-sdk-runner/test/utils/extractServiceName.test.ts b/eng/tools/spec-gen-sdk-runner/test/utils/extractServiceName.test.ts index c4119e911a58..ca8ccdbf22ce 100644 --- a/eng/tools/spec-gen-sdk-runner/test/utils/extractServiceName.test.ts +++ b/eng/tools/spec-gen-sdk-runner/test/utils/extractServiceName.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect } from "vitest"; +import { describe, expect, test } from "vitest"; import { extractServiceName } from "../../src/utils"; describe("extractServiceName", () => { diff --git a/eng/tools/spec-gen-sdk-runner/test/utils/findParentWithFile.test.ts b/eng/tools/spec-gen-sdk-runner/test/utils/findParentWithFile.test.ts index b2aa20e0e3f8..4a59be236a73 100644 --- a/eng/tools/spec-gen-sdk-runner/test/utils/findParentWithFile.test.ts +++ b/eng/tools/spec-gen-sdk-runner/test/utils/findParentWithFile.test.ts @@ -1,8 +1,8 @@ -import { describe, test, expect } from "vitest"; -import { findParentWithFile } from "../../src/utils.js"; -import { fileURLToPath } from "node:url"; import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, test } from "vitest"; import { typespecProjectRegex } from "../../src/spec-helpers.js"; +import { findParentWithFile } from "../../src/utils.js"; describe("findParentWithFile", () => { // Get the absolute path to the repo root @@ -27,6 +27,11 @@ describe("findParentWithFile", () => { expect(result).toBe("specification/contosowidgetmanager/Contoso.WidgetManager"); }); + test("handles single segment path", () => { + const result = findParentWithFile(".", typespecProjectRegex, repoRoot); + expect(result).toBeUndefined(); + }); + test("stops at specified boundary", () => { const result = findParentWithFile( "specification/contosowidgetmanager/Contoso.WidgetManager", diff --git a/eng/tools/spec-gen-sdk-runner/test/utils/getLastPathSegment.test.ts b/eng/tools/spec-gen-sdk-runner/test/utils/getLastPathSegment.test.ts index 3f40b940182b..ac37454c4913 100644 --- a/eng/tools/spec-gen-sdk-runner/test/utils/getLastPathSegment.test.ts +++ b/eng/tools/spec-gen-sdk-runner/test/utils/getLastPathSegment.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect } from "vitest"; +import { describe, expect, test } from "vitest"; import { getLastPathSegment } from "../../src/utils.js"; describe("getLastPathSegment", () => { diff --git a/eng/tools/spec-gen-sdk-runner/test/utils/groupPathsByService.test.ts b/eng/tools/spec-gen-sdk-runner/test/utils/groupPathsByService.test.ts index b91c582bbb71..a5c0802b0e0c 100644 --- a/eng/tools/spec-gen-sdk-runner/test/utils/groupPathsByService.test.ts +++ b/eng/tools/spec-gen-sdk-runner/test/utils/groupPathsByService.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect } from "vitest"; +import { describe, expect, test } from "vitest"; import { groupPathsByService } from "../../src/utils.js"; describe("groupPathsByService", () => { diff --git a/eng/tools/spec-gen-sdk-runner/test/utils/searchRelatedParentFolders.test.ts b/eng/tools/spec-gen-sdk-runner/test/utils/searchRelatedParentFolders.test.ts index 7a87eb8b3196..52b10464a230 100644 --- a/eng/tools/spec-gen-sdk-runner/test/utils/searchRelatedParentFolders.test.ts +++ b/eng/tools/spec-gen-sdk-runner/test/utils/searchRelatedParentFolders.test.ts @@ -1,8 +1,8 @@ -import { describe, test, expect } from "vitest"; -import { searchRelatedParentFolders } from "../../src/utils.js"; -import { fileURLToPath } from "node:url"; import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, test } from "vitest"; import { readmeMdRegex, typespecProjectRegex } from "../../src/spec-helpers.js"; +import { searchRelatedParentFolders } from "../../src/utils.js"; describe("searchRelatedParentFolders", () => { // Get the absolute path to the repo root diff --git a/eng/tools/spec-gen-sdk-runner/test/utils/searchRelatedTypeSpecProjectBySharedLibrary.test.ts b/eng/tools/spec-gen-sdk-runner/test/utils/searchRelatedTypeSpecProjectBySharedLibrary.test.ts index 227bb3521592..0bc1e91fd752 100644 --- a/eng/tools/spec-gen-sdk-runner/test/utils/searchRelatedTypeSpecProjectBySharedLibrary.test.ts +++ b/eng/tools/spec-gen-sdk-runner/test/utils/searchRelatedTypeSpecProjectBySharedLibrary.test.ts @@ -1,7 +1,7 @@ -import { describe, test, expect } from "vitest"; -import { searchRelatedTypeSpecProjectBySharedLibrary } from "../../src/utils.js"; -import { fileURLToPath } from "node:url"; import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, test } from "vitest"; +import { searchRelatedTypeSpecProjectBySharedLibrary } from "../../src/utils.js"; describe("searchRelatedTypeSpecProjectBySharedLibrary", () => { // Get the absolute path to the repo root diff --git a/eng/tools/spec-gen-sdk-runner/test/utils/searchSharedLibrary.test.ts b/eng/tools/spec-gen-sdk-runner/test/utils/searchSharedLibrary.test.ts index 5cd5cd908fa4..eacf838b7713 100644 --- a/eng/tools/spec-gen-sdk-runner/test/utils/searchSharedLibrary.test.ts +++ b/eng/tools/spec-gen-sdk-runner/test/utils/searchSharedLibrary.test.ts @@ -1,8 +1,8 @@ -import { describe, test, expect } from "vitest"; -import { searchSharedLibrary } from "../../src/utils.js"; -import { fileURLToPath } from "node:url"; import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, test } from "vitest"; import { typespecProjectSharedLibraryRegex } from "../../src/spec-helpers.js"; +import { searchSharedLibrary } from "../../src/utils.js"; describe("searchSharedLibrary", () => { // Get the absolute path to the repo root diff --git a/eng/tools/spec-gen-sdk-runner/test/utils/utils.test.ts b/eng/tools/spec-gen-sdk-runner/test/utils/utils.test.ts index 4dec6f2a9ded..13044a155ce2 100644 --- a/eng/tools/spec-gen-sdk-runner/test/utils/utils.test.ts +++ b/eng/tools/spec-gen-sdk-runner/test/utils/utils.test.ts @@ -1,15 +1,15 @@ -import { describe, test, expect, beforeEach, vi } from "vitest"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import { findFilesRecursive, findReadmeFiles, - getRelativePathFromSpecification, getArgumentValue, + getRelativePathFromSpecification, mapToObject, - objectToMap, normalizePath, + objectToMap, } from "../../src/utils.js"; -import { fileURLToPath } from "node:url"; -import path from "node:path"; // Get the absolute path to the repo root const currentFilePath = fileURLToPath(import.meta.url); diff --git a/eng/tools/spec-gen-sdk-runner/tsconfig.json b/eng/tools/spec-gen-sdk-runner/tsconfig.json index 9425747b3ef2..ec85640f015c 100644 --- a/eng/tools/spec-gen-sdk-runner/tsconfig.json +++ b/eng/tools/spec-gen-sdk-runner/tsconfig.json @@ -7,9 +7,5 @@ "target": "ESNext", "moduleResolution": "node", }, - "include": [ - "*.ts", - "src/**/*.ts", - "test/**/*.ts", - ], + "include": ["*.ts", "src/**/*.ts", "test/**/*.ts"], } diff --git a/eng/tools/summarize-impact/README.md b/eng/tools/summarize-impact/README.md new file mode 100644 index 000000000000..674eddddae5e --- /dev/null +++ b/eng/tools/summarize-impact/README.md @@ -0,0 +1,24 @@ +# `Summarize Impact` + +This tool models the PR and produces an artifact that is consumed by the `summarize-checks` check. + +The schema of the artifact looks like: + +```typescript +export type ImpactAssessment = { + prType: string[]; + resourceManagerRequired: boolean; + suppressionReviewRequired: boolean; + versioningReviewRequired: boolean; + breakingChangeReviewRequired: boolean; + isNewApiVersion: boolean; + rpaasExceptionRequired: boolean; + rpaasRpNotInPrivateRepo: boolean; + rpaasChange: boolean; + newRP: boolean; + rpaasRPMissing: boolean; + typeSpecChanged: boolean; + isDraft: boolean; + labelContext: LabelContext; +}; +``` diff --git a/eng/tools/summarize-impact/cmd/summarize-impact.js b/eng/tools/summarize-impact/cmd/summarize-impact.js new file mode 100755 index 000000000000..abdd7016b6cf --- /dev/null +++ b/eng/tools/summarize-impact/cmd/summarize-impact.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { main } from "../dist/src/cli.js"; + +await main(); diff --git a/eng/tools/summarize-impact/package.json b/eng/tools/summarize-impact/package.json new file mode 100644 index 000000000000..6eed76eca68d --- /dev/null +++ b/eng/tools/summarize-impact/package.json @@ -0,0 +1,42 @@ +{ + "name": "@azure-tools/summarize-impact", + "private": true, + "type": "module", + "main": "dist/src/main.js", + "bin": { + "summarize-impact": "cmd/summarize-impact.js" + }, + "scripts": { + "build": "tsc --build", + "format": "prettier . --ignore-path ../.prettierignore --write", + "format:check": "prettier . --ignore-path ../.prettierignore --check", + "format:check:ci": "prettier . --ignore-path ../.prettierignore --check --log-level debug", + "test": "vitest --run", + "test:ci": "vitest run --coverage --reporter=verbose" + }, + "dependencies": { + "@azure-tools/openapi-tools-common": "^1.2.2", + "@azure-tools/specs-shared": "file:../../../.github/shared", + "@azure/openapi-markdown": "0.9.4", + "@octokit/rest": "^22.0.0", + "@ts-common/commonmark-to-markdown": "^2.0.2", + "commonmark": "^0.29.0", + "glob": "^11.0.3", + "js-yaml": "^4.1.0", + "lodash": "^4.17.20", + "simple-git": "^3.27.0" + }, + "devDependencies": { + "@types/commonmark": "^0.27.4", + "@types/glob": "^8.1.0", + "@types/lodash": "^4.14.161", + "@types/node": "^20.0.0", + "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", + "typescript": "~5.8.2", + "vitest": "^3.0.7" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/eng/tools/summarize-impact/src/ImpactAssessment.ts b/eng/tools/summarize-impact/src/ImpactAssessment.ts new file mode 100644 index 000000000000..e603c95cbfd0 --- /dev/null +++ b/eng/tools/summarize-impact/src/ImpactAssessment.ts @@ -0,0 +1,16 @@ +import { LabelContext } from "./labelling-types.js"; + +export type ImpactAssessment = { + resourceManagerRequired: boolean; + suppressionReviewRequired: boolean; + isNewApiVersion: boolean; + rpaasExceptionRequired: boolean; + rpaasRpNotInPrivateRepo: boolean; + rpaasChange: boolean; + newRP: boolean; + rpaasRPMissing: boolean; + typeSpecChanged: boolean; + isDraft: boolean; + labelContext: LabelContext; + targetBranch: string; +}; diff --git a/eng/tools/summarize-impact/src/PRContext.ts b/eng/tools/summarize-impact/src/PRContext.ts new file mode 100644 index 000000000000..3ad0b676b5fc --- /dev/null +++ b/eng/tools/summarize-impact/src/PRContext.ts @@ -0,0 +1,335 @@ +import * as fs from "fs"; +import { dirname, join } from "path"; + +import { parseMarkdown } from "@azure-tools/openapi-tools-common"; +import * as amd from "@azure/openapi-markdown"; + +import { example, readme, swagger, typespec } from "@azure-tools/specs-shared/changed-files"; +import { includesFolder } from "@azure-tools/specs-shared/path"; +import { Readme } from "@azure-tools/specs-shared/readme"; +import { SpecModel } from "@azure-tools/specs-shared/spec-model"; + +import { DiffResult, ReadmeTag, TagConfigDiff, TagDiff } from "./diff-types.js"; +import { LabelContext } from "./labelling-types.js"; + +export type FileListInfo = { + additions: string[]; + modifications: string[]; + deletions: string[]; + renames: { from: string; to: string }[]; + total: number; +}; + +export type PRContextOptions = { + fileList?: FileListInfo; + sourceBranch: string; + targetBranch: string; + sha: string; + repo: string; + owner: string; + prNumber: string; + isDraft: boolean; +}; + +export class PRContext { + // we are starting with checking out before and after to different directories + // sourceDirectory corresponds to "after". EG the PR context is the "after" state of the PR. + // The "before" state is the git root directory without the changes aka targetDirectory + sourceDirectory: string; + targetDirectory: string; + sourceSpecModel?: SpecModel; + targetSpecModel?: SpecModel; + fileList?: FileListInfo; + sourceBranch: string; + targetBranch: string; + sha: string; + repo: string; + owner: string; + prNumber: string; + isDraft: boolean; + labelContext: LabelContext; + + constructor( + sourceDirectory: string, + targetDirectory: string, + labelContext: LabelContext, + options: PRContextOptions, + ) { + this.sourceDirectory = sourceDirectory; + this.targetDirectory = targetDirectory; + this.sourceSpecModel = new SpecModel(sourceDirectory); + this.targetSpecModel = new SpecModel(targetDirectory); + this.labelContext = labelContext; + this.sourceBranch = options.sourceBranch; + this.targetBranch = options.targetBranch; + this.sha = options.sha; + this.repo = options.repo; + this.prNumber = options.prNumber; + this.fileList = options.fileList; + this.isDraft = options.isDraft; + this.owner = options.owner; + } + + getChangedFiles(): string[] { + if (!this.fileList) { + return []; + } + + const changedFiles: string[] = (this.fileList.additions || []) + .concat(this.fileList.modifications || []) + .concat(this.fileList.deletions || []) + .concat(this.fileList.renames.map((r) => r.to) || []); + return changedFiles; + } + + // todo get rid of async here not necessary + // todo store the results + getTypeSpecDiffs(): DiffResult { + if (!this.fileList) { + return { + additions: [], + deletions: [], + changes: [], + }; + } + + const additions = this.fileList.additions.filter((file) => typespec(file)); + const deletions = this.fileList.deletions.filter((file) => typespec(file)); + const changes = this.fileList.modifications.filter((file) => typespec(file)); + + return { + additions, + deletions, + changes, + }; + } + + getSwaggerDiffs(): DiffResult { + if (!this.fileList) { + return { + additions: [], + deletions: [], + changes: [], + }; + } + + const additions = this.fileList.additions.filter((file) => swagger(file)); + const deletions = this.fileList.deletions.filter((file) => swagger(file)); + const changes = this.fileList.modifications.filter((file) => swagger(file)); + + return { + additions, + deletions, + changes, + }; + } + + getExampleDiffs(): DiffResult { + if (!this.fileList) { + return { + additions: [], + deletions: [], + changes: [], + }; + } + + const additions = this.fileList.additions.filter((file) => example(file)); + const deletions = this.fileList.deletions.filter((file) => example(file)); + const changes = this.fileList.modifications.filter((file) => example(file)); + + return { + additions, + deletions, + changes, + }; + } + + async getTagsFromReadme(readmePath: string): Promise { + const tags = await new Readme(readmePath).getTags(); + return [...tags.values()].map((tag) => tag.name); + } + + async getPossibleParentConfigurations(): Promise { + console.log("ENTER definition getPossibleParentConfigurations"); + const changedFiles = await this.getChangedFiles(); + console.log(`Detect changes in the PR:\n${JSON.stringify(changedFiles, null, 2)}`); + const readmes = changedFiles.filter((f) => readme(f)); + + const visitedFolder = new Set(); + changedFiles + .filter((f) => [".md", ".json", ".yaml", ".yml"].some((p) => f.endsWith(p))) + .forEach((f) => { + let dir = dirname(f); + if (visitedFolder.has(dir)) { + return; + } + while (includesFolder(dir, "specification")) { + if (visitedFolder.has(dir)) { + break; + } + visitedFolder.add(dir); + const possibleReadme = join(dir, "readme.md"); + if (fs.existsSync(possibleReadme)) { + if (!readmes.includes(possibleReadme)) { + readmes.push(possibleReadme); + } + break; + } + dir = dirname(dir); + } + }); + console.log("RETURN definition getPossibleParentConfigurations"); + return readmes; + } + + getAllTags(readMeContent: string): string[] { + // todo: we should refactor this to use the spec model, but I haven't had the the time to explicitly + // diff what oad does does here, so I'm leaving it alone for now until I can build some strong unit tests around this. + const cmd = parseMarkdown(readMeContent); + const allTags = new amd.ReadMeManipulator( + { error: (_msg: string) => {} }, + new amd.ReadMeBuilder(), + ).getAllTags(cmd); + return [...allTags]; + } + + async getInputFiles(readMeContent: string, tag: string) { + // todo: we should refactor this to use spec model, but I haven't had time to isolate exactly what + // openapi-markdown is doing here, so I'm just going to use the same logic for now + const cmd = parseMarkdown(readMeContent); + return amd.getInputFilesForTag(cmd.markDown, tag); + } + + async getChangingTags(): Promise { + // we are retrieving all the readme changes, no matter if they're additions, deletions, etc + // Additionally, we're also retrieving all the readme files that may be affected by the changes in the PR, which means + // climbing up the directory tree until we find a readme.md file if necessary. + const allAffectedReadmes: string[] = await this.getPossibleParentConfigurations(); + console.log(`all affected readme are:`); + console.log(JSON.stringify(allAffectedReadmes, null, 2)); + const Diffs: TagDiff[] = []; + for (const readme of allAffectedReadmes) { + const oldReadme = join(this.targetDirectory, readme); + const newReadme = join(this.sourceDirectory, readme); + // As the readme may be not existing , need to check if it's existing. + // we are checking target and source individually, because the readme may not exist in the target branch + const oldTags: string[] = fs.existsSync(oldReadme) + ? [...(await new Readme(oldReadme).getTags()).keys()] + : []; + const newTags: string[] = fs.existsSync(newReadme) + ? [...(await new Readme(newReadme).getTags()).keys()] + : []; + const intersect = oldTags + .filter((t) => newTags.includes(t)) + .filter((tag) => tag !== "all-api-versions"); + const insertions = newTags.filter((t) => !oldTags.includes(t)); + const deletions = oldTags.filter((t) => !newTags.includes(t)); + const differences: TagConfigDiff[] = []; + + // todo: we need to ensure we get ALL effected swaggers by their relationships, not just the swagger files that are directly changed in the PR. + // right now I'm just going to filter to the ones that are directly changed in the PR. + // this is a temporary solution, we need to ensure we get all of the swagger files + // that are affected by the changes in the readme. SpecModel will be useful for this + // we want to get all of the swagger files that are affected by the changes in the readme. + // we can do that using the specmodel + // const allAffectedInputFiles = await this.getRealAffectedSwagger(readme) + // talk to Mike and ask him how we could get all affected swagger files from a readme path. + // I want to say that readme(readme).specModel.getAffectedSwaggerFiles will work? + const allAffectedInputFiles = await (await this.getChangedFiles()).filter((f) => swagger(f)); + console.log(`all affected swagger files in ${readme} are:`); + console.log(JSON.stringify(allAffectedInputFiles, null, 2)); + const getChangedInputFiles = async (tag: string) => { + const readmeContent = await fs.promises.readFile(newReadme, "utf-8"); + const inputFiles = await this.getInputFiles(readmeContent, tag); + if (inputFiles) { + const changedInputFiles = (inputFiles as string[]).filter((f) => + allAffectedInputFiles.some((a) => a.endsWith(f)), + ); + return changedInputFiles; + } + return []; + }; + const changes: string[] = []; + for (const tag of intersect) { + const tagDiff: TagConfigDiff = { name: tag }; + const changedInputFiles = await getChangedInputFiles(tag); + if (changedInputFiles.length) { + console.log("found changed input files under tag:" + tag); + tagDiff.changedInputFiles = changedInputFiles; + changes.push(tag); + } + } + Diffs.push({ readme, insertions, deletions, changes, differences }); + } + return Diffs; + } + + // this function is based upon LocalDirContext.getReadmeDiffs() and CommonPRContext.getReadmeDiffs() which are + // very different from each other (because why not). This implementation is based MOSTLY on CommonPRContext + async getReadmeDiffs(): Promise> { + // this gets all of the readme diffs + // const readmeAdditions = this.fileList?.additions.filter(file => readme(file)) || []; + // const readmeChanges = this.fileList?.modifications.filter(file => readme(file)) + // const readmeDeletions = this.fileList?.deletions.filter(file => readme(file)); + //const changedFiles: DiffFileResult | undefined = await this.localPRContext?.getChangingFiles(); + + const changedFiles = await this.fileList; + const tagDiffs = (await this.getChangingTags()) || []; + + const readmeTagDiffs = tagDiffs + ?.filter((tagDiff: TagDiff) => readme(tagDiff.readme)) + .map((tagDiff: TagDiff) => { + return { + readme: tagDiff.readme, + tags: { + changes: tagDiff.changes, + deletions: tagDiff.deletions, + additions: tagDiff.insertions, + }, + } as ReadmeTag; + }); + + const readmeTagDiffsInAddedReadmeFiles: ReadmeTag[] = readmeTagDiffs.filter( + (readmeTag: ReadmeTag): boolean => + Boolean(changedFiles?.additions.includes(readmeTag.readme)), + ); + const readmeTagDiffsInDeletedReadmeFiles: ReadmeTag[] = readmeTagDiffs.filter( + (readmeTag: ReadmeTag): boolean => + Boolean(changedFiles?.deletions.includes(readmeTag.readme)), + ); + const readmeTagDiffsInChangedReadmeFiles: ReadmeTag[] = readmeTagDiffs.filter( + (readmeTag: ReadmeTag): boolean => { + // The README file that contains the API version tags that have been diffed in given readmeTag (i.e. readmeTag.tags) + // has been modified. + const readmeModified = changedFiles?.modifications.includes(readmeTag.readme); + + // The README was not modified, added or deleted; just the specs belonging to the API version tags in the README were modified. + // In such case we assume the README is 'changed' in the sense the specs belonging to the API version tags in the README were modified. + // The README file itself wasn't modified. + // We assume here the README is present in the repository - otherwise it would show up as 'deleted' or would not + // appear as readmeTag.readme in any of the readmeTagDiffs. + const readmeUnchanged = + !changedFiles?.additions.includes(readmeTag.readme) && + !changedFiles?.deletions.includes(readmeTag.readme); + + return readmeModified || readmeUnchanged; + }, + ); + + const result = { + additions: readmeTagDiffsInAddedReadmeFiles, + deletions: readmeTagDiffsInDeletedReadmeFiles, + changes: readmeTagDiffsInChangedReadmeFiles, + }; + + console.log( + `RETURN definition CommonPRContext.getReadmeDiffs. ` + + `changedFiles: ${JSON.stringify(changedFiles)}, ` + + `tagDiffs: ${JSON.stringify(tagDiffs)}, ` + + `readmeTagDiffs: ${JSON.stringify(readmeTagDiffs)}, ` + + `this.readmeDiffs: ${JSON.stringify(result)}.`, + ); + + return result; + } +} diff --git a/eng/tools/summarize-impact/src/cli.ts b/eng/tools/summarize-impact/src/cli.ts new file mode 100644 index 000000000000..40599b1de5f7 --- /dev/null +++ b/eng/tools/summarize-impact/src/cli.ts @@ -0,0 +1,148 @@ +#!/usr/bin/env node + +import { getChangedFilesStatuses } from "@azure-tools/specs-shared/changed-files"; +import { setOutput } from "@azure-tools/specs-shared/error-reporting"; +import { evaluateImpact } from "./impact.js"; + +import { getRootFolder } from "@azure-tools/specs-shared/simple-git"; +import { Octokit } from "@octokit/rest"; +import fs from "fs"; +import { parseArgs, ParseArgsConfig } from "node:util"; +import { join, resolve } from "path"; +import { LabelContext } from "./labelling-types.js"; +import { PRContext } from "./PRContext.js"; + +export async function getRoot(inputPath: string): Promise { + try { + const gitRoot = await getRootFolder(inputPath); + return resolve(gitRoot.trim()); + } catch (error) { + console.error( + `Error: Unable to determine the root folder of the git repository.`, + `Please ensure you are running this command within a git repository OR providing a targeted directory that is within a git repo.`, + ); + process.exit(1); + } +} + +export async function main() { + const config: ParseArgsConfig = { + options: { + // the target branch checked out + targetDirectory: { + type: "string", + multiple: false, + }, + // the pr + sourceDirectory: { + type: "string", + multiple: false, + default: process.cwd(), + }, + fileList: { + type: "string", + short: "f", + multiple: false, + default: undefined, + }, + number: { + type: "string", + short: "n", + multiple: false, + }, + sourceBranch: { + type: "string", + multiple: false, + }, + targetBranch: { + type: "string", + multiple: false, + }, + sha: { + type: "string", + short: "s", + multiple: false, + }, + repo: { + type: "string", + short: "r", + multiple: false, + }, + owner: { + type: "string", + short: "o", + multiple: false, + }, + isDraft: { + type: "boolean", + multiple: false, + }, + }, + allowPositionals: true, + }; + + const { values: opts } = parseArgs(config); + + // todo: refactor these opts + const sourceDirectory = opts.sourceDirectory as string; + const targetDirectory = opts.targetDirectory as string; + const sourceGitRoot = await getRoot(sourceDirectory); + const targetGitRoot = await getRoot(targetDirectory); + const fileList = await getChangedFilesStatuses({ cwd: sourceGitRoot, paths: ["specification"] }); + const sha = opts.sha as string; + const sourceBranch = opts.sourceBranch as string; + const targetBranch = opts.targetBranch as string; + const repo = opts.repo as string; + const owner = opts.owner as string; + const prNumber = opts.number as string; + const isDraft = opts.isDraft as boolean; + + // create github client (use token if available, otherwise unauthenticated. we will throw if unhandled) + const github = new Octokit({ + ...(process.env.GITHUB_TOKEN && { auth: process.env.GITHUB_TOKEN }), + }); + + const labels = ( + await github.paginate(github.rest.issues.listLabelsOnIssue, { + owner, + repo, + issue_number: Number(prNumber), + per_page: 100, + }) + ).map((label: any) => label.name); + + const labelContext: LabelContext = { + present: new Set(labels), + toAdd: new Set(), + toRemove: new Set(), + }; + + const prContext = new PRContext(sourceGitRoot, targetGitRoot, labelContext, { + sha, + sourceBranch, + targetBranch, + repo, + prNumber, + owner, + fileList, + isDraft, + }); + + let impact = await evaluateImpact(prContext, labelContext); + + // sets by default are not serializable, so we need to convert them to arrays + // before we can write them to the output file. + function setReplacer(_key: string, value: any) { + if (value instanceof Set) { + return [...value]; + } + return value; + } + + console.log("Evaluated impact: ", JSON.stringify(impact, setReplacer, 2)); + + // Write to a temp file that can get picked up later. + const summaryFile = join(process.cwd(), "summary.json"); + fs.writeFileSync(summaryFile, JSON.stringify(impact, setReplacer, 2)); + setOutput("summary", summaryFile); +} diff --git a/eng/tools/summarize-impact/src/diff-types.ts b/eng/tools/summarize-impact/src/diff-types.ts new file mode 100644 index 000000000000..c34f851a873c --- /dev/null +++ b/eng/tools/summarize-impact/src/diff-types.ts @@ -0,0 +1,40 @@ +export type FileTypes = "SwaggerFile" | "TypeSpecFile" | "ExampleFile" | "ReadmeFile"; +export type ChangeTypes = "Addition" | "Deletion" | "Update"; + +export type PRChange = { + fileType: FileTypes; + changeType: ChangeTypes; + filePath: string; + additionalInfo?: any; +}; + +export type ReadmeTag = { + readme: string; + tags: DiffResult; +}; + +export type TagConfigDiff = { + name: string; + oldConfig?: any; + newConfig?: any; + difference?: any; + changedInputFiles?: string[]; +}; + +export type TagDiff = { + readme: string; + changes: string[]; + insertions: string[]; + deletions: string[]; + differences?: TagConfigDiff[]; +}; + +export type ChangeHandler = { + [key in FileTypes]?: (event: PRChange) => void | Promise; +}; + +export type DiffResult = { + additions?: T[]; + deletions?: T[]; + changes?: T[]; +}; diff --git a/eng/tools/summarize-impact/src/impact.ts b/eng/tools/summarize-impact/src/impact.ts new file mode 100644 index 000000000000..3a4389e6d6b0 --- /dev/null +++ b/eng/tools/summarize-impact/src/impact.ts @@ -0,0 +1,739 @@ +#!/usr/bin/env node + +import * as fs from "fs"; +import { glob } from "glob"; +import * as path from "path"; + +import * as commonmark from "commonmark"; +import yaml from "js-yaml"; +import * as _ from "lodash"; + +import { + ChangeHandler, + ChangeTypes, + DiffResult, + FileTypes, + PRChange, + ReadmeTag, +} from "./diff-types.js"; + +import { Label, LabelContext, PRType } from "./labelling-types.js"; + +import { ImpactAssessment } from "./ImpactAssessment.js"; +import { PRContext } from "./PRContext.js"; + +import { Readme } from "@azure-tools/specs-shared/readme"; + +// todo: we need to populate this so that we can tell if it's a new APIVersion down stream +export async function isNewApiVersion(context: PRContext): Promise { + const handlers: ChangeHandler[] = []; + let isAddingNewApiVersion = false; + const apiVersionSet = new Set(); + + const rpFolders = new Set(); + + const createSwaggerFileHandler = () => { + return (e: PRChange) => { + if (e.changeType === "Addition") { + const apiVersion = getApiVersionFromSwaggerFile(e.filePath); + if (apiVersion) { + apiVersionSet.add(apiVersion); + } + const rpFolder = getRPFolderFromSwaggerFile(e.filePath); + if (rpFolder !== undefined) { + rpFolders.add(rpFolder); + } + console.log(`apiVersion: ${apiVersion}, rpFolder: ${rpFolder}`); + } else if (e.changeType === "Update") { + const rpFolder = getRPFolderFromSwaggerFile(e.filePath); + if (rpFolder !== undefined) { + rpFolders.add(rpFolder); + } + } + }; + }; + + handlers.push({ SwaggerFile: createSwaggerFileHandler() }); + await processPrChanges(context, handlers); + + console.log(`rpFolders: ${Array.from(rpFolders).join(",")}`); + + const firstRPFolder = Array.from(rpFolders)[0]; + + console.log(`apiVersion: ${Array.from(apiVersionSet).join(",")}`); + + if (firstRPFolder === undefined) { + console.log("RP folder not found."); + return false; + } + + const targetBranchRPFolder = path.resolve(context.targetDirectory, firstRPFolder); + + console.log(`targetBranchRPFolder: ${targetBranchRPFolder}`); + + const existingApiVersions = getAllApiVersionFromRPFolder(targetBranchRPFolder); + + console.log(`existingApiVersions: ${existingApiVersions.join(",")}`); + + for (const apiVersion of apiVersionSet) { + if (!existingApiVersions.includes(apiVersion)) { + console.log(`The apiVersion ${apiVersion} is added. and not found in existing ApiVersions`); + isAddingNewApiVersion = true; + } + } + return isAddingNewApiVersion; +} + +export async function evaluateImpact( + context: PRContext, + labelContext: LabelContext, +): Promise { + const typeSpecLabelShouldBePresent = await processTypeSpec(context, labelContext); + + // examine changed files. if changedpaths includes data-plane, add "data-plane" + // same for "resource-manager". We care about whether resourcemanager will be present for a later check + const { resourceManagerLabelShouldBePresent } = await processPRType(context, labelContext); + + // Has to be run in a PR context. Uses addition and update to understand + // if the suppressions have been changed. If they have, suppressionReviewRequired must be added + // as a label + const suppressionRequired = await processSuppression(context, labelContext); + console.log(`suppressionRequired: ${suppressionRequired}`); + + // needs to examine "after" context to understand if a readme that was changed is RPaaS or not + const { rpaasLabelShouldBePresent } = await processRPaaS(context, labelContext); + + // Has to be in PR context. Uses the addition to understand if the newRPNamespace label should be present. + const { newRPNamespaceLabelShouldBePresent } = await processNewRPNamespace( + context, + labelContext, + resourceManagerLabelShouldBePresent, + ); + + // doesn't necessarily need to be in the PR context. + // Uses the previous outputs that DID need to be a PR context, but otherwise only examines targetBranch and those + // output labels. + const { ciNewRPNamespaceWithoutRpaaSLabelShouldBePresent, rpaasExceptionLabelShouldBePresent } = + await processNewRpNamespaceWithoutRpaasLabel( + context, + labelContext, + resourceManagerLabelShouldBePresent, + newRPNamespaceLabelShouldBePresent, + rpaasLabelShouldBePresent, + ); + + // examines the additions. if the changetype is addition, then it will add the ciRpaasRPNotInPrivateRepo label + const { ciRpaasRPNotInPrivateRepoLabelShouldBePresent } = + await processRpaasRpNotInPrivateRepoLabel( + context, + labelContext, + resourceManagerLabelShouldBePresent, + rpaasLabelShouldBePresent, + ); + + const newApiVersion = await isNewApiVersion(context); + + return { + suppressionReviewRequired: labelContext.toAdd.has("suppressionsReviewRequired"), + rpaasChange: rpaasLabelShouldBePresent, + newRP: newRPNamespaceLabelShouldBePresent, + rpaasRPMissing: ciNewRPNamespaceWithoutRpaaSLabelShouldBePresent, + rpaasRpNotInPrivateRepo: ciRpaasRPNotInPrivateRepoLabelShouldBePresent, + resourceManagerRequired: resourceManagerLabelShouldBePresent, + rpaasExceptionRequired: rpaasExceptionLabelShouldBePresent, + typeSpecChanged: typeSpecLabelShouldBePresent, + isNewApiVersion: newApiVersion, + isDraft: context.isDraft, + labelContext: labelContext, + targetBranch: context.targetBranch, + }; +} + +export function isManagementPR(filePaths: string[]): boolean { + return filePaths.some((it) => it.includes("resource-manager")); +} + +export function isDataPlanePR(filePaths: string[]): boolean { + return filePaths.some((it) => it.includes("data-plane")); +} + +export function getAllApiVersionFromRPFolder(rpFolder: string): string[] { + const allSwaggerFilesFromRPFolder = glob.sync(`${rpFolder}/**/*.json`); + console.log(`allSwaggerFilesFromRPFolder: ${allSwaggerFilesFromRPFolder}`); + + const apiVersions: Set = new Set(); + for (const it of allSwaggerFilesFromRPFolder) { + if (!it.includes("examples")) { + const apiVersion = getApiVersionFromSwaggerFile(it); + console.log(`Get api version from swagger file: ${it}, apiVersion: ${apiVersion}`); + if (apiVersion) { + apiVersions.add(apiVersion); + } + } + } + return [...apiVersions]; +} + +export function getApiVersionFromSwaggerFile(swaggerFile: string): string | undefined { + const swagger = fs.readFileSync(swaggerFile).toString(); + const swaggerObject = JSON.parse(swagger); + if (swaggerObject["info"] && swaggerObject["info"]["version"]) { + return swaggerObject["info"]["version"]; + } + return undefined; +} + +export function getRPFolderFromSwaggerFile(swaggerFile: string): string | undefined { + const resourceProvider = getResourceProviderFromFilePath(swaggerFile); + + if (resourceProvider === undefined) { + return undefined; + } + + const lastIdx = swaggerFile.lastIndexOf(resourceProvider!); + return swaggerFile.substring(0, lastIdx + resourceProvider!.length); +} + +export const getResourceProviderFromFilePath = (filePath: string): string | undefined => { + // Example filePath: + // specification/purview/data-plane/Azure.Analytics.Purview.Workflow/preview/2023-10-01-preview/purviewWorkflow.json + // Match: + // /Azure.Analytics.Purview.Workflow/ + // Note: + // The regex matches a directory name in the path of form: /Foo.Bar.Baz/, where: + // - The directory name must have at least one period. If it would match 0 periods, + // then in the example path it would match to "/purview/" instead. + // - The directory name can have one or more periods. + // The "Foo.Bar.Baz" example given above has two periods. + const regex = /\/([a-z0-9]+(\.[a-z0-9]+)+)\//i; + const match = filePath.match(regex); + if (match && match.length > 0) { + return match[1]; + } + // Second matching attempt to cover scenarios in which: + // - the resource provider is for data-plane + // - and it has no dots in the name. + // + // Example filePath: + // specification/communication/data-plane/Sms/preview/2024-02-05-preview/communicationServicesSms.json + // Match: + // /Sms/ + // + // For details see: https://github.com/Azure/azure-sdk-tools/issues/7552 + const regexForDirImmediatelyAfterDataPlane = /\/data-plane\/([a-z0-9]+)\//i; + const match2 = filePath.match(regexForDirImmediatelyAfterDataPlane); + if (match2 && match2.length > 0) { + return match2[1]; + } + + return undefined; +}; + +async function processTypeSpec(ctx: PRContext, labelContext: LabelContext): Promise { + console.log("ENTER definition processTypeSpec"); + const typeSpecLabel = new Label("TypeSpec", labelContext.present); + // By default this label should not be present. We may determine later in this function that it should be present after all. + typeSpecLabel.shouldBePresent = false; + const handlers: ChangeHandler[] = []; + const typeSpecFileHandler = () => { + return (_: PRChange) => { + // Note: this code will be executed if the PR has a diff on a TypeSpec file, + // as defined in public/swagger-validation-common/src/context.ts/defaultFilePatterns/typespec + typeSpecLabel.shouldBePresent = true; + }; + }; + const swaggerFileHandler = () => { + return (prChange: PRChange) => { + if (prChange.changeType !== "Deletion" && isSwaggerGeneratedByTypeSpec(prChange.filePath)) { + typeSpecLabel.shouldBePresent = true; + } + }; + }; + handlers.push({ + TypeSpecFile: typeSpecFileHandler(), + SwaggerFile: swaggerFileHandler(), + }); + await processPrChanges(ctx, handlers); + typeSpecLabel.applyStateChange(labelContext.toAdd, labelContext.toRemove); + console.log("RETURN definition processTypeSpec"); + + return typeSpecLabel.shouldBePresent; +} + +function isSwaggerGeneratedByTypeSpec(swaggerFilePath: string): boolean { + try { + return !!JSON.parse(fs.readFileSync(swaggerFilePath).toString())?.info["x-typespec-generated"]; + } catch { + return false; + } +} + +export async function processPrChanges(ctx: PRContext, Handlers: ChangeHandler[]) { + console.log("ENTER definition processPrChanges"); + const prChanges = await getPRChanges(ctx); + prChanges.forEach((prChange) => { + Handlers.forEach((handler) => { + if (prChange.fileType in handler) { + handler?.[prChange.fileType]?.(prChange); + } + }); + }); + console.log("RETURN definition processPrChanges"); +} + +export async function processPRChangesAsync(ctx: PRContext, Handlers: ChangeHandler[]) { + console.log("ENTER definition processPRChangesAsync"); + const prChanges = await getPRChanges(ctx); + + for (const prChange of prChanges) { + for (const handler of Handlers) { + if (prChange.fileType in handler) { + await handler?.[prChange.fileType]?.(prChange); + } + } + } + + console.log("RETURN definition processPRChangesAsync"); +} + +export async function getPRChanges(ctx: PRContext): Promise { + console.log("ENTER definition getPRChanges"); + const results: PRChange[] = []; + + function newChange( + fileType: FileTypes, + changeType: ChangeTypes, + filePath?: string, + additionalInfo?: any, + ) { + if (filePath) { + results.push({ + filePath, + fileType, + changeType, + additionalInfo, + }); + } + } + + function newChanges(fileType: FileTypes, changeType: ChangeTypes, files?: string[]) { + if (files) { + files.forEach((filePath) => newChange(fileType, changeType, filePath)); + } + } + + function genChanges(type: FileTypes, diffs: DiffResult) { + newChanges(type, "Addition", diffs.additions); + newChanges(type, "Deletion", diffs.deletions); + newChanges(type, "Update", diffs.changes); + } + + function genReadmeChanges(readmeDiffs?: DiffResult) { + if (readmeDiffs) { + readmeDiffs.additions?.forEach((d) => newChange("ReadmeFile", "Addition", d.readme, d.tags)); + readmeDiffs.changes?.forEach((d) => newChange("ReadmeFile", "Update", d.readme, d.tags)); + readmeDiffs.deletions?.forEach((d) => newChange("ReadmeFile", "Deletion", d.readme, d.tags)); + } + } + + genChanges("SwaggerFile", ctx.getSwaggerDiffs()); + genChanges("TypeSpecFile", ctx.getTypeSpecDiffs()); + genChanges("ExampleFile", ctx.getExampleDiffs()); + genReadmeChanges(await ctx.getReadmeDiffs()); + + console.log("RETURN definition getPRChanges"); + return results; +} + +// related pending work: +// If a PR has both data-plane and resource-manager labels, it should fail with appropriate messaging #8144 +// https://github.com/Azure/azure-sdk-tools/issues/8144 +async function processPRType( + context: PRContext, + labelContext: LabelContext, +): Promise<{ resourceManagerLabelShouldBePresent: boolean }> { + console.log("ENTER definition processPRType"); + const types: PRType[] = await getPRType(context); + + const resourceManagerLabelShouldBePresent = processPRTypeLabel( + "resource-manager", + types, + labelContext, + ); + processPRTypeLabel("data-plane", types, labelContext); + + console.log("RETURN definition processPRType"); + return { resourceManagerLabelShouldBePresent }; +} + +async function getPRType(context: PRContext): Promise { + console.log("ENTER definition getPRType"); + const prChanges: PRChange[] = await getPRChanges(context); + + logPRChanges(prChanges); + + const changedFilePaths: string[] = prChanges.map((it) => it.filePath); + + const prTypes: PRType[] = []; + if (changedFilePaths.length > 0) { + if (isDataPlanePR(changedFilePaths)) { + prTypes.push("data-plane"); + } + if (isManagementPR(changedFilePaths)) { + prTypes.push("resource-manager"); + } + } + console.log("RETURN definition getPRType"); + return prTypes; + + function logPRChanges(prChanges: PRChange[]) { + console.log(`PR changes table (count: ${prChanges.length}):`); + console.log("LEGEND: changeType | fileType | filePath"); + prChanges + .map((it) => `${it.changeType} | ${it.fileType} | ${it.filePath}`) + .forEach((it) => console.log(it)); + console.log("END of PR changes table"); + + console.log("PR changes with additionalInfo:"); + prChanges.filter((it) => it.additionalInfo != null).forEach((it) => console.log(it)); + console.log("END of PR changes with additionalInfo"); + } +} + +function processPRTypeLabel( + labelName: PRType, + types: PRType[], + labelContext: LabelContext, +): boolean { + const label = new Label(labelName, labelContext.present); + label.shouldBePresent = types.includes(labelName); + + label.applyStateChange(labelContext.toAdd, labelContext.toRemove); + return label.shouldBePresent; +} + +async function processSuppression(context: PRContext, labelContext: LabelContext) { + console.log("ENTER definition processSuppression"); + + const suppressionReviewRequiredLabel = new Label( + "SuppressionReviewRequired", + labelContext.present, + ); + // By default this label should not be present. We may determine later in this function that it should be present after all. + suppressionReviewRequiredLabel.shouldBePresent = false; + const handlers: ChangeHandler[] = []; + + const createReadmeFileHandler = () => { + return (e: PRChange) => { + if ( + (e.changeType === "Addition" && getSuppressions(e.filePath).length) || + (e.changeType === "Update" && + diffSuppression( + path.resolve(context.targetDirectory, e.filePath), + path.resolve(context.sourceDirectory, e.filePath), + ).length) + ) { + suppressionReviewRequiredLabel.shouldBePresent = true; + } + }; + }; + handlers.push({ + ReadmeFile: createReadmeFileHandler(), + }); + await processPrChanges(context, handlers); + + suppressionReviewRequiredLabel.applyStateChange(labelContext.toAdd, labelContext.toRemove); + console.log("RETURN definition processSuppression"); + + return suppressionReviewRequiredLabel.shouldBePresent; +} + +function getSuppressions(readmePath: string) { + const walkToNode = ( + walker: commonmark.NodeWalker, + cb: (node: commonmark.Node) => boolean, + ): commonmark.Node | undefined => { + let event = walker.next(); + + while (event) { + const curNode = event.node; + if (cb(curNode)) { + return curNode; + } + event = walker.next(); + } + return undefined; + }; + const getAllCodeBlockNodes = (startNode: commonmark.Node) => { + const walker = startNode.walker(); + const result = []; + while (true) { + const a = walkToNode(walker, (n) => n.type === "code_block"); + if (!a) { + break; + } + result.push(a); + } + return result; + }; + let suppressionResult: any[] = []; + try { + const readme = fs.readFileSync(readmePath).toString(); + const codeBlocks = getAllCodeBlockNodes(new commonmark.Parser().parse(readme)); + for (const block of codeBlocks) { + if (block.literal) { + try { + const blockObject = yaml.load(block.literal) as any; + const directives = blockObject?.["directive"]; + if (directives && Array.isArray(directives)) { + suppressionResult = suppressionResult.concat(directives.filter((s) => s.suppress)); + } + const suppressions = blockObject?.["suppressions"]; + if (suppressions && Array.isArray(suppressions)) { + suppressionResult = suppressionResult.concat(suppressions); + } + } catch (e) {} + } + } + } catch (e) {} + return suppressionResult; +} + +export function diffSuppression(readmeBefore: string, readmeAfter: string) { + const beforeSuppressions = getSuppressions(readmeBefore); + const afterSuppressions = getSuppressions(readmeAfter); + const newSuppressions = []; + for (const suppression of afterSuppressions) { + const properties = ["suppress", "from", "where", "code", "reason"]; + if ( + -1 === + beforeSuppressions.findIndex((s) => properties.every((p) => _.isEqual(s[p], suppression[p]))) + ) { + newSuppressions.push(suppression); + } + } + return newSuppressions; +} + +async function processRPaaS( + context: PRContext, + labelContext: LabelContext, +): Promise<{ rpaasLabelShouldBePresent: boolean }> { + console.log("ENTER definition processRPaaS"); + const rpaasLabel = new Label("RPaaS", labelContext.present); + // By default this label should not be present. We may determine later in this function that it should be present after all. + rpaasLabel.shouldBePresent = false; + + const handlers: ChangeHandler[] = []; + const createReadmeFileHandler = () => { + return async (e: PRChange) => { + if ( + e.changeType !== "Deletion" && + (await isRPSaaS(path.join(context.sourceDirectory, e.filePath))) + ) { + rpaasLabel.shouldBePresent = true; + } + }; + }; + handlers.push({ + ReadmeFile: createReadmeFileHandler(), + }); + await processPRChangesAsync(context, handlers); + + rpaasLabel.applyStateChange(labelContext.toAdd, labelContext.toRemove); + console.log("RETURN definition processRPaaS"); + return { rpaasLabelShouldBePresent: rpaasLabel.shouldBePresent }; +} + +async function isRPSaaS(readmeFilePath: string) { + const config: any = await new Readme(readmeFilePath).getGlobalConfig(); + return config["openapi-subtype"] === "rpaas" || config["openapi-subtype"] === "providerHub"; +} + +async function processNewRPNamespace( + context: PRContext, + labelContext: LabelContext, + resourceManagerLabelShouldBePresent: boolean, +): Promise<{ newRPNamespaceLabelShouldBePresent: boolean }> { + console.log("ENTER definition processNewRPNamespace"); + const newRPNamespaceLabel = new Label("new-rp-namespace", labelContext.present); + // By default this label should not be present. We may determine later in this function that it should be present after all. + newRPNamespaceLabel.shouldBePresent = false; + const handlers: ChangeHandler[] = []; + + let skip = false; + + const targetBranch = context.targetBranch; + if (targetBranch !== "main" && targetBranch !== "ARMCoreRPDev") { + console.log(`Not main or ARMCoreRPDev branch, skip new RP namespace check`); + skip = true; + } + + if (!skip && !resourceManagerLabelShouldBePresent) { + console.log(`Not resource-manager, skip new RP namespace check`); + skip = true; + } + + if (!skip) { + const createSwaggerFileHandler = () => { + return (e: PRChange) => { + if (e.changeType === "Addition") { + const rpFolder = getRPFolderFromSwaggerFile(path.dirname(e.filePath)); + console.log(`Processing newRPNameSpace rpFolder: ${rpFolder}`); + if (rpFolder !== undefined) { + const rpFolderFullPath = path.resolve(context.targetDirectory, rpFolder); + if (!fs.existsSync(rpFolderFullPath)) { + console.log(`Adding newRPNameSpace rpFolder: ${rpFolder}`); + newRPNamespaceLabel.shouldBePresent = true; + } + } + } + }; + }; + + handlers.push({ SwaggerFile: createSwaggerFileHandler() }); + await processPrChanges(context, handlers); + } + + newRPNamespaceLabel.applyStateChange(labelContext.toAdd, labelContext.toRemove); + console.log("RETURN definition processNewRPNamespace"); + return { newRPNamespaceLabelShouldBePresent: newRPNamespaceLabel.shouldBePresent }; +} + +// CODESYNC: +// - see entries for related labels in https://github.com/Azure/azure-rest-api-specs/blob/main/.github.amrom.workers.devment.yml +// - requiredLabelsRules.ts / requiredLabelsRules +async function processNewRpNamespaceWithoutRpaasLabel( + context: PRContext, + labelContext: LabelContext, + resourceManagerLabelShouldBePresent: boolean, + newRPNamespaceLabelShouldBePresent: boolean, + rpaasLabelShouldBePresent: boolean, +): Promise<{ + ciNewRPNamespaceWithoutRpaaSLabelShouldBePresent: boolean; + rpaasExceptionLabelShouldBePresent: boolean; +}> { + console.log("ENTER definition processNewRpNamespaceWithoutRpaasLabel"); + const ciNewRPNamespaceWithoutRpaaSLabel = new Label( + "CI-NewRPNamespaceWithoutRPaaS", + labelContext.present, + ); + // By default this label should not be present. We may determine later in this function that it should be present after all. + ciNewRPNamespaceWithoutRpaaSLabel.shouldBePresent = false; + + const rpaasExceptionLabel = new Label("RPaaSException", labelContext.present); + + let skip = false; + + if (!resourceManagerLabelShouldBePresent) { + console.log(`Not resource-manager, skip checking for 'CI-NewRPNamespaceWithoutRPaaS'`); + skip = true; + } + + if (!skip) { + const branch = context.targetBranch; + if (branch === "RPSaaSDev" || branch === "RPSaaSMaster") { + console.log( + `PR is targeting '${branch}' branch. Skip checking for 'CI-NewRPNamespaceWithoutRPaaS'`, + ); + skip = true; + } + } + + if (!skip && newRPNamespaceLabelShouldBePresent && !rpaasLabelShouldBePresent) { + console.log( + "Adding new RP namespace, but not RPaaS. Label 'CI-NewRPNamespaceWithoutRPaaS' should be present.", + ); + ciNewRPNamespaceWithoutRpaaSLabel.shouldBePresent = true; + } + + rpaasExceptionLabel.shouldBePresent = + rpaasExceptionLabel.present && ciNewRPNamespaceWithoutRpaaSLabel.shouldBePresent; + + ciNewRPNamespaceWithoutRpaaSLabel.applyStateChange(labelContext.toAdd, labelContext.toRemove); + rpaasExceptionLabel.applyStateChange(labelContext.toAdd, labelContext.toRemove); + console.log("RETURN definition processNewRpNamespaceWithoutRpaasLabel"); + + return { + ciNewRPNamespaceWithoutRpaaSLabelShouldBePresent: + ciNewRPNamespaceWithoutRpaaSLabel.shouldBePresent, + rpaasExceptionLabelShouldBePresent: rpaasExceptionLabel.shouldBePresent as boolean, + }; +} + +// CODESYNC: see entries for related labels in https://github.com/Azure/azure-rest-api-specs/blob/main/.github.amrom.workers.devment.yml +async function processRpaasRpNotInPrivateRepoLabel( + context: PRContext, + labelContext: LabelContext, + resourceManagerLabelShouldBePresent: boolean, + rpaasLabelShouldBePresent: boolean, +): Promise<{ ciRpaasRPNotInPrivateRepoLabelShouldBePresent: boolean }> { + console.log("ENTER definition processRpaasRpNotInPrivateRepoLabel"); + const ciRpaasRPNotInPrivateRepoLabel = new Label( + "CI-RpaaSRPNotInPrivateRepo", + labelContext.present, + ); + // By default this label should not be present. We may determine later in this function that it should be present after all. + ciRpaasRPNotInPrivateRepoLabel.shouldBePresent = false; + + let skip = false; + + if (!resourceManagerLabelShouldBePresent) { + console.log(`Not resource-manager, skip RPaaSRPNotInPrivateRepo check`); + skip = true; + } + + if (!skip) { + const targetBranch = context.targetBranch; + if (targetBranch !== "main" || !rpaasLabelShouldBePresent) { + console.log("Not main branch or not RPaaS PR, skip block PR when RPaaS RP not onboard"); + skip = true; + } + } + + // todo: retrieve the list and populate this value properly. + // if (!skip) { + // // this is a request to get the list of RPaaS folders from azure-rest-api-specs-pr -> RPSaasMaster branch -> dump specification folder + // // names + // const rpaasRPFolderList = await getRPaaSFolderList(); + // const rpFolderNames: string[] = rpaasRPFolderList.map((f) => f.name); + + // console.log(`RPaaS RP folder list: ${rpFolderNames}`); + + // const handlers: ChangeHandler[] = []; + + // const processPrChange = () => { + // return (e: PRChange) => { + // if (e.changeType === "Addition") { + // const rpFolderName = getRPRootFolderName(e.filePath); + // console.log( + // `Processing processRpaasRpNotInPrivateRepoLabel rpFolderName: ${rpFolderName}` + // ); + + // if (rpFolderName === undefined) { + // console.log(`RP folder is undefined for changed file path '${e.filePath}'.`); + // return; + // } + + // if (!rpFolderNames.includes(rpFolderName)) { + // console.log( + // `This RP is RPSaaS RP but could not find rpFolderName: ${rpFolderName} in RPFolderNames: ${rpFolderNames}. ` + // + `Label 'CI-RpaaSRPNotInPrivateRepo' should be present.` + // ); + // ciRpaasRPNotInPrivateRepoLabel.shouldBePresent = true; + // } + // } + // }; + // }; + + // handlers.push({ SwaggerFile: processPrChange() }); + // await processPrChanges(context, handlers); + // } + + ciRpaasRPNotInPrivateRepoLabel.applyStateChange(labelContext.toAdd, labelContext.toRemove); + console.log("RETURN definition processRpaasRpNotInPrivateRepoLabel"); + + return { + ciRpaasRPNotInPrivateRepoLabelShouldBePresent: ciRpaasRPNotInPrivateRepoLabel.shouldBePresent, + }; +} diff --git a/eng/tools/summarize-impact/src/labelling-types.ts b/eng/tools/summarize-impact/src/labelling-types.ts new file mode 100644 index 000000000000..fbd3bccfb616 --- /dev/null +++ b/eng/tools/summarize-impact/src/labelling-types.ts @@ -0,0 +1,123 @@ +export type PRType = "resource-manager" | "data-plane"; + +/** + * The LabelContext is used by prSummary.ts / summary() and downstream invocations. + * + * The "present" set represents the set of labels that are currently present on the PR + * processed by given invocation of summary(). It is obtained via GitHub Octokit API at the beginning + * of summary(). + * + * The "toAdd" set is the set of labels to be added to the PR at the end of invocation of summary(). + * This is to be done by calling GitHub Octokit API to add the labels. + * + * The "toRemove" set is analogous to "toAdd" set, but instead it is the set of labels to be removed. + * + * The general pattern used in the code to populate "toAdd" or "toRemove" sets to be ready for + * Octokit invocation is as follows: + * + * - the summary() function passes the context through its invocation chain. + * - given function responsible for given label, like e.g. for label "ARMReview", + * creates a new instance of Label: const armReviewLabel = new Label("ARMReview", labelContext.present) + * - the function then processes the label to determine if armReviewLabel.shouldBePresent is to be set to true or false. + * - the function at the end of its invocation calls armReviewLabel.applyStateChanges(labelContext.toAdd, labelContext.toRemove) + * to update the sets. + * - the function may optionally return { armReviewLabel.shouldBePresent } to allow the caller to pass this value + * further to downstream business logic that depends on it. + * - at the end of invocation summary() calls Octokit passing it as input labelContext.toAdd and labelContext.toRemove. + * + * todo: this type is duplicated in JSDoc over in summarize-checks.js + */ +export type LabelContext = { + present: Set; + toAdd: Set; + toRemove: Set; +}; + +export class Label { + name: string; + + /** Is the label currently present on the pull request? + * + * This is determined at the time of construction of this object. + */ + present?: boolean; + + /** Should this label be present on the pull request? + * + * Must be defined before applyStateChange is called. + * + * Not set at the construction time to facilitate determining desired presence + * of multiple labels in single code block, without intermixing it with + * label construction logic. + */ + shouldBePresent: boolean | undefined = undefined; + + constructor(name: string, presentLabels?: Set) { + this.name = name; + this.present = presentLabels?.has(this.name) ?? undefined; + } + + /** + * If the label should be added, add its name to labelsToAdd. + * If the label should be removed, add its name to labelsToRemove. + * Otherwise, do nothing. + * + * Precondition: this.shouldBePresent has been defined. + */ + applyStateChange(labelsToAdd: Set, labelsToRemove: Set): void { + if (this.shouldBePresent === undefined) { + console.warn( + "ASSERTION VIOLATION! " + + `Cannot applyStateChange for label '${this.name}' ` + + "as its desired presence hasn't been defined. Returning early.", + ); + return; + } + + if (!this.present && this.shouldBePresent) { + if (!labelsToAdd.has(this.name)) { + console.log( + `Label.applyStateChange: '${this.name}' was not present and should be present. Scheduling addition.`, + ); + labelsToAdd.add(this.name); + } else { + console.log( + `Label.applyStateChange: '${this.name}' was not present and should be present. It is already scheduled for addition.`, + ); + } + } else if (this.present && !this.shouldBePresent) { + if (!labelsToRemove.has(this.name)) { + console.log( + `Label.applyStateChange: '${this.name}' was present and should not be present. Scheduling removal.`, + ); + labelsToRemove.add(this.name); + } else { + console.log( + `Label.applyStateChange: '${this.name}' was present and should not be present. It is already scheduled for removal.`, + ); + } + } else if (this.present === this.shouldBePresent) { + console.log( + `Label.applyStateChange: '${this.name}' is ${this.present ? "present" : "not present"}. This is the desired state.`, + ); + } else { + console.warn( + "ASSERTION VIOLATION! " + + `Label.applyStateChange: '${this.name}' is ${this.present ? "present" : "not present"} while it should be ${this.shouldBePresent ? "present" : "not present"}. ` + + `At this point of execution this should not happen.`, + ); + } + } + + isEqualToOrPrefixOf(label: string): boolean { + return this.name.endsWith("*") ? label.startsWith(this.name.slice(0, -1)) : this.name === label; + } + + logString(): string { + return ( + `Label: name: ${this.name}, ` + + `present: ${this.present}, ` + + `shouldBePresent: ${this.shouldBePresent}. ` + ); + } +} diff --git a/eng/tools/summarize-impact/src/types.ts b/eng/tools/summarize-impact/src/types.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/eng/tools/summarize-impact/test/cli.test.ts b/eng/tools/summarize-impact/test/cli.test.ts new file mode 100644 index 000000000000..2c5715e0b2b1 --- /dev/null +++ b/eng/tools/summarize-impact/test/cli.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it } from "vitest"; //vi + +import path from "path"; + +import { getChangedFilesStatuses } from "@azure-tools/specs-shared/changed-files"; +import { PRContext } from "../src/PRContext.js"; +import { evaluateImpact } from "../src/impact.js"; +import { LabelContext } from "../src/labelling-types.js"; + +describe("Check Changes", () => { + it.skipIf(!process.env.GITHUB_TOKEN || !process.env.INTEGRATION_TEST)( + "Integration test 35346", + async () => { + const targetDirectory = path.join("/home/semick/repo/rest-s/35346", "before"); + const sourceDirectory = path.join("/home/semick/repo/rest-s/35346", "after"); + + // Change to source directory and save original + const originalCwd = process.cwd(); + process.chdir(sourceDirectory); + + try { + const changedFileDetails = await getChangedFilesStatuses({ + cwd: sourceDirectory, + baseCommitish: "origin/main", + }); + const labelContext: LabelContext = { + present: new Set(), + toAdd: new Set(), + toRemove: new Set(), + }; + + const prContext = new PRContext(sourceDirectory, targetDirectory, labelContext, { + sha: "ad7c74cb27d2cf3ba83996aaea36b07caa4d16c8", + sourceBranch: "dev/nandiniy/DTLTypeSpec", + targetBranch: "main", + repo: "azure-rest-api-specs", + prNumber: "35346", + owner: "Azure", + fileList: changedFileDetails, + isDraft: false, + }); + + const result = await evaluateImpact(prContext, labelContext); + + expect(result).toBeDefined(); + expect(result.typeSpecChanged).toBeTruthy(); + expect(result.labelContext.toAdd.has("resource-manager")).toBeTruthy(); + expect(result.labelContext.toAdd.has("SuppressionReviewRequired")).toBeTruthy(); + expect(changedFileDetails).toBeDefined(); + expect(changedFileDetails.total).toEqual(293); + } finally { + // Restore original directory + process.chdir(originalCwd); + } + }, + 60000000, + ); + + it.skipIf(!process.env.GITHUB_TOKEN || !process.env.INTEGRATION_TEST)( + "Integration test 35982", + async () => { + const targetDirectory = path.join("/home/semick/repo/rest-s/35982", "before"); + const sourceDirectory = path.join("/home/semick/repo/rest-s/35982", "after"); + + // Change to source directory and save original + const originalCwd = process.cwd(); + process.chdir(sourceDirectory); + + try { + const changedFileDetails = await getChangedFilesStatuses({ + cwd: sourceDirectory, + baseCommitish: "origin/main", + }); + const labelContext: LabelContext = { + present: new Set(), + toAdd: new Set(), + toRemove: new Set(), + }; + + const prContext = new PRContext(sourceDirectory, targetDirectory, labelContext, { + sha: "2bd8350d465081401a0f4f03e633eca41f0991de", + sourceBranch: "features/users/deepika/cosmos-connectors-confluent", + targetBranch: "main", + repo: "azure-rest-api-specs", + prNumber: "35982", + owner: "Azure", + fileList: changedFileDetails, + isDraft: false, + }); + + const result = await evaluateImpact(prContext, labelContext); + expect(result.isNewApiVersion).toBeTruthy(); + expect(result.labelContext.toAdd.has("TypeSpec")).toBeTruthy(); + expect(result.labelContext.toAdd.has("resource-manager")).toBeTruthy(); + expect(result.isNewApiVersion).toBeTruthy(); + expect(result.labelContext.toAdd.has("ARMReview")).toBeTruthy(); + expect(result.labelContext.toAdd.has("RPaaS")).toBeTruthy(); + expect(result.labelContext.toAdd.has("WaitForARMFeedback")).toBeTruthy(); + expect(result).toBeDefined(); + } finally { + // Restore original directory + process.chdir(originalCwd); + } + }, + 60000000, + ); +}); diff --git a/eng/tools/summarize-impact/tsconfig.json b/eng/tools/summarize-impact/tsconfig.json new file mode 100644 index 000000000000..5f48d4c6a5b5 --- /dev/null +++ b/eng/tools/summarize-impact/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": ".", + "allowJs": true, + }, + "include": ["*.ts", "src/**/*.ts", "test/**/*.ts"], +} diff --git a/eng/tools/suppressions/package.json b/eng/tools/suppressions/package.json index 52250330d8d4..e94d0d80cfae 100644 --- a/eng/tools/suppressions/package.json +++ b/eng/tools/suppressions/package.json @@ -8,6 +8,9 @@ }, "scripts": { "build": "tsc --build", + "format": "prettier . --ignore-path ../.prettierignore --write", + "format:check": "prettier . --ignore-path ../.prettierignore --check", + "format:check:ci": "prettier . --ignore-path ../.prettierignore --check --log-level debug", "test": "vitest", "test:ci": "vitest run --coverage --reporter=verbose" }, @@ -17,12 +20,13 @@ "dependencies": { "minimatch": "^10.0.1", "yaml": "^2.4.2", - "zod": "^3.23.8", - "zod-validation-error": "^3.3.0" + "zod": "^4.0.2" }, "devDependencies": { "@types/node": "^20.0.0", "@vitest/coverage-v8": "^3.0.7", + "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "typescript": "~5.8.2", "vitest": "^3.0.7" } diff --git a/eng/tools/suppressions/src/suppressions.ts b/eng/tools/suppressions/src/suppressions.ts index 65389eea203f..ee18f891a645 100644 --- a/eng/tools/suppressions/src/suppressions.ts +++ b/eng/tools/suppressions/src/suppressions.ts @@ -1,14 +1,17 @@ import { Stats } from "fs"; import { access, constants, lstat, readFile } from "fs/promises"; import { minimatch } from "minimatch"; +import { createRequire } from "module"; import { dirname, join, resolve, sep } from "path"; import { sep as posixSep } from "path/posix"; +import vm from "vm"; import { parse as yamlParse } from "yaml"; -import { z } from "zod"; -import { fromError } from "zod-validation-error"; +import * as z from "zod"; export interface Suppression { tool: string; + // String of JavaScript CJS code, executed in a prepared context, that determines if a suppression should be included + if?: string; // Output only exposes "paths". For input, if "path" is defined, it is inserted at the start of "paths". paths: string[]; rules?: string[]; @@ -20,6 +23,7 @@ const suppressionSchema = z.array( z .object({ tool: z.string(), + if: z.string().optional(), // For now, input allows "path" alongside "paths". Lather, may deprecate "path". path: z.string().optional(), paths: z.array(z.string()).optional(), @@ -39,6 +43,7 @@ const suppressionSchema = z.array( } return { tool: s.tool, + if: s.if, paths: paths, rules: s.rules, subRules: s["sub-rules"], @@ -70,7 +75,11 @@ const suppressionSchema = z.array( * ); * ``` */ -export async function getSuppressions(tool: string, path: string): Promise { +export async function getSuppressions( + tool: string, + path: string, + context: Record = {}, +): Promise { path = resolve(path); // If path doesn't exist, throw instead of returning "[]" to prevent confusion @@ -86,6 +95,7 @@ export async function getSuppressions(tool: string, path: string): Promise = {}, ): Suppression[] { path = resolve(path); suppressionsFile = resolve(suppressionsFile); @@ -137,22 +148,35 @@ export function getSuppressionsFromYaml( // Throws if parsedYaml doesn't match schema suppressions = suppressionSchema.parse(parsedYaml); } catch (err) { - throw fromError(err); + let finalErr = err; + if (err instanceof z.ZodError) { + finalErr = new Error(z.prettifyError(err), { cause: err }); + } + throw finalErr; } - return suppressions - .filter((s) => s.tool === tool) - .filter((s) => { - // Minimatch only allows forward-slashes in patterns and input - const pathPosix: string = path.split(sep).join(posixSep); - - return s.paths.some((suppressionPath) => { - const pattern: string = join(dirname(suppressionsFile), suppressionPath) - .split(sep) - .join(posixSep); - return minimatch(pathPosix, pattern); - }); - }); + // Make "require" available inside sandbox for CJS imports + const sandbox = { ...context, require: createRequire(import.meta.url) }; + + return ( + suppressions + // Tool name + .filter((s) => s.tool === tool) + // Path + .filter((s) => { + // Minimatch only allows forward-slashes in patterns and input + const pathPosix: string = path.split(sep).join(posixSep); + + return s.paths.some((suppressionPath) => { + const pattern: string = join(dirname(suppressionsFile), suppressionPath) + .split(sep) + .join(posixSep); + return minimatch(pathPosix, pattern); + }); + }) + // If + .filter((s) => s.if === undefined || vm.runInNewContext(s.if, sandbox)) + ); } /** diff --git a/eng/tools/suppressions/test/e2e/merge/foo/bar/suppressions.yaml b/eng/tools/suppressions/test/e2e/merge/foo/bar/suppressions.yaml index 113dc0b7729e..e385b93acaa2 100644 --- a/eng/tools/suppressions/test/e2e/merge/foo/bar/suppressions.yaml +++ b/eng/tools/suppressions/test/e2e/merge/foo/bar/suppressions.yaml @@ -1,9 +1,9 @@ - tool: TestTool - path: '**' + path: "**" reason: bar-globstar - tool: TestTool - path: '*' + path: "*" reason: bar-star - tool: TestTool diff --git a/eng/tools/suppressions/test/e2e/merge/foo/suppressions.yaml b/eng/tools/suppressions/test/e2e/merge/foo/suppressions.yaml index cdc396f96095..7e45c2b73aff 100644 --- a/eng/tools/suppressions/test/e2e/merge/foo/suppressions.yaml +++ b/eng/tools/suppressions/test/e2e/merge/foo/suppressions.yaml @@ -1,9 +1,9 @@ - tool: TestTool - path: '**' + path: "**" reason: foo-globstar - tool: TestTool - path: '*' + path: "*" reason: foo-star - tool: TestTool diff --git a/eng/tools/suppressions/test/suppressions.test.ts b/eng/tools/suppressions/test/suppressions.test.ts index ff5bad696846..7bdd1b10bc3a 100644 --- a/eng/tools/suppressions/test/suppressions.test.ts +++ b/eng/tools/suppressions/test/suppressions.test.ts @@ -238,16 +238,18 @@ test("yaml not array", () => { expect(() => getSuppressionsFromYaml("TestTool", "foo.json", "suppressions.yaml", "foo"), ).toThrowErrorMatchingInlineSnapshot( - `[ZodValidationError: Validation error: Expected array, received string]`, + `[Error: ✖ Invalid input: expected array, received string]`, ); }); test("yaml array not suppression", () => { - expect(() => - getSuppressionsFromYaml("TestTool", "foo.json", "suppressions.yaml", "- foo: bar"), - ).toThrowErrorMatchingInlineSnapshot( - `[ZodValidationError: Validation error: Required at "[0].tool"; Required at "[0].reason"]`, - ); + expect(() => getSuppressionsFromYaml("TestTool", "foo.json", "suppressions.yaml", "- foo: bar")) + .toThrowErrorMatchingInlineSnapshot(` + [Error: ✖ Invalid input: expected string, received undefined + → at [0].tool + ✖ Invalid input: expected string, received undefined + → at [0].reason] + `); }); test("suppression with rules", () => { @@ -268,6 +270,7 @@ test("suppression with rules", () => { expect(suppressions).toStrictEqual([ { tool: "TestTool", + if: undefined, paths: ["foo"], rules: ["my-rule"], subRules: ["my.option.a", "my.option.b"], @@ -275,3 +278,69 @@ test("suppression with rules", () => { }, ]); }); + +test.each([ + { context: { foo: false, bar: false }, expected: ["no-if", "process-version"] }, + { + context: { foo: true, bar: false }, + expected: ["no-if", "if-foo", "if-foo-or-bar", "process-version"], + }, + { + context: { foo: false, bar: true }, + expected: ["no-if", "if-bar", "if-foo-or-bar", "process-version"], + }, + { + context: { foo: true, bar: true }, + expected: ["no-if", "if-foo", "if-bar", "if-foo-or-bar", "if-foo-and-bar", "process-version"], + }, +])("if($context)", ({ context, expected }) => { + const suppressionYaml = ` +- tool: TestTool + path: "**" + reason: no-if +- tool: TestTool + path: "**" + if: foo + reason: if-foo +- tool: TestTool + path: "**" + if: bar + reason: if-bar +- tool: TestTool + path: "**" + if: foo || bar + reason: if-foo-or-bar +- tool: TestTool + path: "**" + if: foo && bar + reason: if-foo-and-bar +- tool: TestTool + path: "**" + if: require("process").version.startsWith("v") + reason: process-version +`; + + let suppressions: Suppression[] = getSuppressionsFromYaml( + "TestTool", + "test-path", + "suppressions.yaml", + suppressionYaml, + context, + ); + + expect(suppressions.map((s) => s.reason).sort()).toEqual(expected.sort()); +}); + +test.each([ + ["invalid javascript", "Unexpected identifier 'javascript'"], + ["1(1)", "1 is not a function"], +])("if: %s", (ifExpression, expectedException) => { + expect(() => + getSuppressionsFromYaml( + "TestTool", + "test-path", + "suppressions.yaml", + `- tool: TestTool\n if: "${ifExpression}"\n path: "**"\n reason: test`, + ), + ).throws(expectedException); +}); diff --git a/eng/tools/suppressions/tsconfig.json b/eng/tools/suppressions/tsconfig.json index f3b416dfce52..1c9d0b24bed9 100644 --- a/eng/tools/suppressions/tsconfig.json +++ b/eng/tools/suppressions/tsconfig.json @@ -4,9 +4,5 @@ "outDir": "./dist", "rootDir": ".", }, - "include": [ - "*.ts", - "src/**/*.ts", - "test/**/*.ts", - ], + "include": ["*.ts", "src/**/*.ts", "test/**/*.ts"], } diff --git a/eng/tools/suppressions/vitest.config.ts b/eng/tools/suppressions/vitest.config.ts new file mode 100644 index 000000000000..bc6ad4809131 --- /dev/null +++ b/eng/tools/suppressions/vitest.config.ts @@ -0,0 +1,9 @@ +import { configDefaults, defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + coverage: { + exclude: [...configDefaults.coverage.exclude!, "cmd/**", "src/index.ts"], + }, + }, +}); diff --git a/eng/tools/tsconfig.json b/eng/tools/tsconfig.json index 7e39f0f15010..ba7486de2512 100644 --- a/eng/tools/tsconfig.json +++ b/eng/tools/tsconfig.json @@ -3,23 +3,46 @@ "compilerOptions": { "target": "es2024", "module": "NodeNext", - // override "importHelpers:true" in root tsconfig.json "importHelpers": false, - // required to use project references "composite": true, }, // Compile nothing at this level "files": [], "references": [ - { "path": "./suppressions" }, - { "path": "./tsp-client-tests" }, - { "path": "./typespec-requirement" }, - { "path": "./typespec-validation" }, - { "path": "./sdk-suppressions" }, - { "path": "./spec-gen-sdk-runner"}, - { "path": "./lint-diff" }, - { "path": "./typespec-migration-validation"} - ] + { + "path": "./lint-diff", + }, + { + "path": "./oav-runner", + }, + { + "path": "./sdk-suppressions", + }, + { + "path": "./openapi-diff-runner", + }, + { + "path": "./spec-gen-sdk-runner", + }, + { + "path": "./suppressions", + }, + { + "path": "./tsp-client-tests", + }, + { + "path": "./typespec-migration-validation", + }, + { + "path": "./typespec-requirement", + }, + { + "path": "./typespec-validation", + }, + { + "path": "./summarize-impact", + }, + ], } diff --git a/eng/tools/tsp-client-tests/package.json b/eng/tools/tsp-client-tests/package.json index 685920061d6b..5951effe30d2 100644 --- a/eng/tools/tsp-client-tests/package.json +++ b/eng/tools/tsp-client-tests/package.json @@ -5,11 +5,16 @@ "devDependencies": { "@azure-tools/specs-shared": "file:../../../.github/shared", "@types/node": "^20.0.0", + "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "typescript": "~5.8.2", "vitest": "^3.0.7" }, "scripts": { "build": "tsc --build", + "format": "prettier . --ignore-path ../.prettierignore --write", + "format:check": "prettier . --ignore-path ../.prettierignore --check", + "format:check:ci": "prettier . --ignore-path ../.prettierignore --check --log-level debug", "test": "vitest", "test:ci": "vitest run --reporter=verbose" }, diff --git a/eng/tools/tsp-client-tests/tsconfig.json b/eng/tools/tsp-client-tests/tsconfig.json index bbb199bce5fb..8bf1d5b99616 100644 --- a/eng/tools/tsp-client-tests/tsconfig.json +++ b/eng/tools/tsp-client-tests/tsconfig.json @@ -5,9 +5,5 @@ "outDir": "./dist", "rootDir": ".", }, - "include": [ - "*.ts", - "src/**/*.ts", - "test/**/*.ts", - ], + "include": ["*.ts", "src/**/*.ts", "test/**/*.ts"], } diff --git a/eng/tools/typespec-migration-validation/cmd/tsmv.js b/eng/tools/typespec-migration-validation/cmd/tsmv.js index 2a7c28a5d607..e2a37e0b5491 100755 --- a/eng/tools/typespec-migration-validation/cmd/tsmv.js +++ b/eng/tools/typespec-migration-validation/cmd/tsmv.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { main } from "../dist/src/index.js" +import { main } from "../dist/src/index.js"; await main(); diff --git a/eng/tools/typespec-migration-validation/package.json b/eng/tools/typespec-migration-validation/package.json index a35b1c35114c..b41785aaa4a7 100644 --- a/eng/tools/typespec-migration-validation/package.json +++ b/eng/tools/typespec-migration-validation/package.json @@ -12,20 +12,23 @@ "yargs": "^18.0.0" }, "devDependencies": { - "@types/jest": "^29.4.0", "@types/json-diff": "^1.0.3", "@types/node": "^18.19.86", "@types/yargs": "^17.0.33", "@typescript-eslint/eslint-plugin": "^8.32.1", "@typescript-eslint/parser": "^8.32.1", "eslint": "^9.26.0", + "prettier": "~3.5.3", "typescript": "^5.8.3" }, "scripts": { "build": "tsc --build", - "watch": "tsc --build --watch", + "format": "prettier . --ignore-path ../.prettierignore --write", + "format:check": "prettier . --ignore-path ../.prettierignore --check", + "format:check:ci": "prettier . --ignore-path ../.prettierignore --check --log-level debug", "test": "vitest", - "test:ci": "vitest run --coverage --reporter=verbose" + "test:ci": "vitest run --coverage --reporter=verbose", + "watch": "tsc --build --watch" }, "engines": { "node": ">=20.0.0" diff --git a/eng/tools/typespec-migration-validation/prompts/globalSuppressionToLocal.md b/eng/tools/typespec-migration-validation/prompts/globalSuppressionToLocal.md new file mode 100644 index 000000000000..ffc54909dcc7 --- /dev/null +++ b/eng/tools/typespec-migration-validation/prompts/globalSuppressionToLocal.md @@ -0,0 +1,62 @@ +# Remove global suppressions + +As a GitHub Copilot assistant, follow this guide to remove all the global suppressions and specify them locally. + +## Understanding User Input + +When users request help with converting global suppressions to inline suppressions in TypeSpec and Swagger specifications, and adding appropriate justifications for all suppressions, they typically provide a {TypeSpec-folder} in the format `{ServiceName}.Management` (where ServiceName examples include: `Compute`, `Network`, `Storage`) or a `tspconfig.yaml` file. + +When the user provides a service folder name, look for the `tspconfig.yaml` under that folder and base subsequent work on the content of that file. + +If the user provides a `tspconfig.yaml` file, use the information in that file directly to guide the migration. In this case, {TypeSpec-folder} is the folder containing that file. + +If the user does not provide this information, be sure to ask. Use this information to customize your guidance, especially in the following cases: + +- Selecting appropriate conversion commands +- Providing service-type-related examples + +## Migration Steps + +## Step 1: Locate the tspconfig.yaml file + +1. **Check the `tspconfig.yaml` file** + + - Location: `{ServiceName}.Management/tspconfig.yaml` or the `tspconfig.yaml` file directly provided by the user + +2. **Check the disable section in the tspconfig.yaml file** + - Look for all configurations under `disable:`, for example: + ```yaml + linter: + extends: + - "@azure-tools/typespec-azure-rulesets/resource-manager" + disable: + "@azure-tools/typespec-azure-core/no-nullable": "backward-compatibility" + ``` + - These configurations represent global suppressions that you need to convert to inline suppressions. + +## Step 2: Migrate global suppressions to inline suppressions + +1. Remove the `disable:` section from the `tspconfig.yaml` file, and remember all the warning types that were suppressed by the removed global suppressions.. + +2. Recompile the tsp files to generate new Swagger files while reproducing the warnings that were previously globally suppressed. + +```powershell +cd {TypeSpec-folder} +npx tsp compile . +``` + +3. For warnings that appear during the compilation process, you need to categorize the warnings according to the warning types from the previously removed global suppressions and add inline suppressions in the corresponding TypeSpec files. The format for inline suppressions is as follows: + +```typespec +#suppress "@azure-tools/typespec-azure-core/no-openapi" "For backward compatibility with existing API" +@operationId("WebPubSubCustomDomains_Get") +get is ArmResourceRead; +``` + +Note that inline suppressions need to be placed above the relevant code and require clear justification. The justification in the example, "For backward compatibility with existing API", is a good example of clear reasoning. + +## Step 3: Validation and Testing + +1. **Validate inline suppressions**: + - Ensure all inline suppressions have been correctly added with clear and understandable justifications. + - Recompile the TypeSpec files to ensure there are no new warnings or errors. diff --git a/eng/tools/typespec-migration-validation/prompts/removeOperationId.md b/eng/tools/typespec-migration-validation/prompts/removeOperationId.md new file mode 100644 index 000000000000..94ff22e70e6a --- /dev/null +++ b/eng/tools/typespec-migration-validation/prompts/removeOperationId.md @@ -0,0 +1,205 @@ +# Remove Operation Id Decorator from TypeSpec Files + +## Input Parameters + +- `tspFolder`: Required. The folder path containing TypeSpec files to be processed. This folder should contain: + - All .tsp files that need to be migrated + - The back-compatible.tsp file (will be created if it doesn't exist) + Examples: + - Relative path: "specification/healthbot/Healthbot.Management" + +## Context + +You will help users migrate from using `@operationId` decorators to using `@clientLocation` and `@clientName` decorators in TypeSpec files. + +## Instructions + +### Step 1: Process TypeSpec Files in {tspFolder} + +1. Scan all .tsp files in the specified {tspFolder} +2. For each file: + - Find operations marked with `@operationId` decorator + - Parse decorator content which follows format: `{clientLocation}_{clientName}` + - Identify the interface containing each operation + +### Step 2: Handle Client Location in {tspFolder} + +For each operation found in {tspFolder} files, check if the interface that contains the operation has a name that exactly matches `{clientLocation}` (case-sensitive): + +- If the containing interface name is exactly the same as `{clientLocation}`: No clientLocation decorator needed +- If the containing interface name doesn't match exactly (e.g., operation in "Operations" interface but `{clientLocation}` is "Widgets"): + - Check if `{clientLocation}` exists as an interface in any .tsp file within {tspFolder} (case-sensitive match) + - If it exists: Add to {tspFolder}/back-compatible.tsp: + ```typespec + @@clientLocation({interface name}.{operation name}, {clientLocation}) + ``` + - If it doesn't exist: Add to {tspFolder}/back-compatible.tsp: + ```typespec + @@clientLocation({interface name}.{operation name}, "{clientLocation}") + ``` + +### Step 3: Handle Client Name + +Capitalize the first character of the operation name, then check if it matches `{clientName}` (case sensitive): + +- Capitalize the first character of the operation name (e.g., "list" becomes "List", "create" becomes "Create") +- Compare the capitalized operation name with `{clientName}` using exact case-sensitive matching +- If they don't match exactly: Add to {tspFolder}/back-compatible.tsp: + ```typespec + @@clientName({interface name}.{operation name}, "{clientName}") + ``` + +### Step 4: Clean Up TypeSpec Files + +1. In each .tsp file within {tspFolder}: + - Remove the `@operationId` decorator + - Remove any corresponding `#suppress` statement above it + +### Step 5: Clean Up back-compatible.tsp + +After generating all the decorators, review the back-compatible.tsp file to remove any unnecessary decorators: + +1. **Remove redundant @clientLocation decorators**: + + - If an operation is already in an interface with the same name as the target client location, the @clientLocation decorator is not needed + - Example: If `Widgets.list` has `@@clientLocation(Widgets.list, Widgets)`, this can be removed since the operation is already in the Widgets interface + +2. **Remove redundant @clientName decorators**: + + - If the operation name (with first letter capitalized) already matches the target client name, the @clientName decorator is not needed + - Example: If `Operations.create` has `@@clientName(Operations.create, "Create")`, this can be removed since "create" capitalized is "Create" + +3. **Clean up empty back-compatible.tsp**: + - If after removing redundant decorators the back-compatible.tsp file is empty or only contains imports, consider removing the file entirely + +## Examples + +### Example 1: New ClientLocation doesn't exist + +Before: + +```typespec +interface Operations { + #suppress "@azure-tools/typespec-azure-core/no-openapi" "Migration in progress" + @operationId("Widgets_AnotherList") + list(): Widget[]; + + // ...other operations... +} +``` + +After: + +- Remove decorators from original file: + +```typespec +interface Operations { + list(): Widget[]; + + // ...other operations... +} +``` + +- Add to back-compatible.tsp: + +```typespec +@@clientLocation(Operations.list, "Widgets") +@@clientName(Operations.list, "AnotherList") +``` + +### Example 2: New ClientLocation exists as interface + +Before: + +```typespec +interface Widgets { + create(): Widget; +} + +interface Operations { + #suppress "@azure-tools/typespec-azure-core/operation-id" "Migration in progress" + @operationId("Widgets_AnotherList") + list(): Widget[]; + + // ...other operations... +} +``` + +After: + +- Remove decorators from original file: + +```typespec +interface Widgets { + create(): Widget; +} + +interface Operations { + list(): Widget[]; + + // ...other operations... +} +``` + +- Add to back-compatible.tsp since the target interface "Widgets" exists: + +```typespec +@@clientLocation(Operations.list, Widgets) +@@clientName(Operations.list, "AnotherList") +``` + +### Example 3: Operation already in correct interface + +Before: + +```typespec +interface Widgets { + #suppress "@azure-tools/typespec-azure-core/operation-id" "Migration in progress" + @operationId("Widgets_AnotherList") + list(): Widget[]; +} +``` + +After: + +- Remove decorators from original file: + +```typespec +interface Widgets { + list(): Widget[]; +} +``` + +- Add to back-compatible.tsp (only clientName needed since operation is already in Widgets interface): + +```typespec +@@clientName(Widgets.list, "AnotherList") +``` + +### Example 4: Operation name matches clientName + +Before: + +```typespec +interface Operations { + #suppress "@azure-tools/typespec-azure-core/operation-id" "Migration in progress" + @operationId("Widgets_Create") + create(): Widget[]; +} +``` + +After: + +- Remove decorators from original file: + +```typespec +interface Operations { + create(): Widget[]; +} +``` + +- Add to back-compatible.tsp (only clientLocation needed since operation name "create" matches clientName part): + +```typespec +@@clientLocation(Operations.create, "Widgets") +``` diff --git a/eng/tools/typespec-migration-validation/src/configuration.ts b/eng/tools/typespec-migration-validation/src/configuration.ts index 8527c5f6e89f..43ffbb407599 100644 --- a/eng/tools/typespec-migration-validation/src/configuration.ts +++ b/eng/tools/typespec-migration-validation/src/configuration.ts @@ -7,5 +7,5 @@ interface configuration { export const configuration: configuration = { ignoreDescription: true, enumNameToCamelCase: true, - ignorePathCase: false, // Normalize the segments before provider -} \ No newline at end of file + ignorePathCase: true, // Normalize the path +}; diff --git a/eng/tools/typespec-migration-validation/src/document.ts b/eng/tools/typespec-migration-validation/src/document.ts index cf96c793735c..b355219e236d 100644 --- a/eng/tools/typespec-migration-validation/src/document.ts +++ b/eng/tools/typespec-migration-validation/src/document.ts @@ -1,9 +1,33 @@ -import { OpenAPI2Document, OpenAPI2PathItem, HttpMethod, OpenAPI2Operation, OpenAPI2Schema, OpenAPI2Parameter, Ref, Refable, OpenAPI2Response, OpenAPI2SchemaProperty, OpenAPI2SchemaRefProperty } from "@azure-tools/typespec-autorest"; -import { isApiVersionParameter, isResourceGroupNameParameter, isSubscriptionIdParameter } from "./parameter.js"; +import { + OpenAPI2Document, + OpenAPI2PathItem, + HttpMethod, + OpenAPI2Operation, + OpenAPI2Schema, + OpenAPI2Parameter, + Ref, + Refable, + OpenAPI2Response, + OpenAPI2SchemaProperty, + OpenAPI2SchemaRefProperty, +} from "@azure-tools/typespec-autorest"; +import { + getOriginalParameter, + isApiVersionParameter, + isResourceGroupNameParameter, + isSubscriptionIdParameter, +} from "./parameter.js"; import { configuration } from "./configuration.js"; let originalDocument: OpenAPI2Document | undefined = undefined; +export function getOriginalDocument(): OpenAPI2Document { + if (originalDocument === undefined) { + throw new Error("Original document is not set. Please call processDocument first."); + } + return originalDocument; +} + export function processDocument(document: OpenAPI2Document): OpenAPI2Document { originalDocument = deepCopy(document); @@ -23,16 +47,26 @@ export function processDocument(document: OpenAPI2Document): OpenAPI2Document { if (document.tags) { delete newDocument.tags; } + if (document.info && document.info["x-typespec-generated"]) { + delete newDocument.info["x-typespec-generated"]; + } for (const route in document.paths) { const path = document.paths[route] as OpenAPI2PathItem; const processedPath = processPath(path); if (configuration.ignorePathCase) { - const normalizedRoute = route.replace(/\/resourcegroups\//i, '/resourceGroups/').replace(/\/subscriptions\//i, '/subscriptions/'); - delete newDocument.paths[route]; + const normalizedRoute = route + .replace(/\/resourcegroups\//i, "/resourceGroups/") + .replace(/\/subscriptions\//i, "/subscriptions/") + .split('/') + .map(segment => { + if (segment.length === 0) return segment; + return segment.charAt(0).toLowerCase() + segment.slice(1); + }) + .join('/'); + delete newDocument.paths[route]; newDocument.paths[normalizedRoute] = processedPath; - } - else { + } else { newDocument.paths[route] = processedPath; } } @@ -54,7 +88,16 @@ export function processDocument(document: OpenAPI2Document): OpenAPI2Document { function processPath(path: OpenAPI2PathItem): OpenAPI2PathItem { function isHttpMethod(key: string): key is HttpMethod { - const httpMethods: HttpMethod[] = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']; + const httpMethods: HttpMethod[] = [ + "get", + "put", + "post", + "delete", + "options", + "head", + "patch", + "trace", + ]; return httpMethods.includes(key as HttpMethod); } @@ -72,19 +115,19 @@ function processPath(path: OpenAPI2PathItem): OpenAPI2PathItem { function processOperation(operation: OpenAPI2Operation): OpenAPI2Operation { const newOperation = deepCopy(operation); - let index = newOperation.parameters.findIndex(p => isApiVersionParameter(p)); + let index = newOperation.parameters.findIndex((p) => isApiVersionParameter(p)); if (index > -1) { newOperation.parameters.splice(index, 1); } - index = newOperation.parameters.findIndex(p => isSubscriptionIdParameter(p)); + index = newOperation.parameters.findIndex((p) => isSubscriptionIdParameter(p)); if (index > -1) { newOperation.parameters.splice(index, 1); } - index = newOperation.parameters.findIndex(p => isResourceGroupNameParameter(p)); + index = newOperation.parameters.findIndex((p) => isResourceGroupNameParameter(p)); if (index > -1) { newOperation.parameters.splice(index, 1); } - newOperation.parameters = newOperation.parameters.map(p => processParameter(p)); + newOperation.parameters = newOperation.parameters.map((p) => processParameter(p)); for (const response in operation.responses) { const responseObject = operation.responses[response] as OpenAPI2Response; @@ -96,7 +139,10 @@ function processOperation(operation: OpenAPI2Operation): OpenAPI2Operation { if (newOperation["x-ms-long-running-operation"] === false) { delete newOperation["x-ms-long-running-operation"]; } - if (newOperation["x-ms-long-running-operation-options"] && newOperation["x-ms-long-running-operation-options"]["final-state-via"] === "location") { + if ( + newOperation["x-ms-long-running-operation-options"] && + newOperation["x-ms-long-running-operation-options"]["final-state-via"] === "location" + ) { delete newOperation["x-ms-long-running-operation-options"]; } @@ -107,10 +153,18 @@ function processOperation(operation: OpenAPI2Operation): OpenAPI2Operation { delete newOperation["x-ms-pageable"]["itemName"]; } - if (newOperation.produces && newOperation.produces.length === 1 && newOperation.produces[0] === "application/json") { + if ( + newOperation.produces && + ((newOperation.produces.length === 1 && newOperation.produces[0] === "application/json") || + newOperation.produces.length === 0) + ) { delete newOperation.produces; } - if (newOperation.consumes && newOperation.consumes.length === 1 && newOperation.consumes[0] === "application/json") { + if ( + newOperation.consumes && + ((newOperation.consumes.length === 1 && newOperation.consumes[0] === "application/json") || + newOperation.consumes.length === 0) + ) { delete newOperation.consumes; } @@ -118,6 +172,10 @@ function processOperation(operation: OpenAPI2Operation): OpenAPI2Operation { delete newOperation.tags; } + if (newOperation.deprecated === false) { + delete newOperation.deprecated; + } + if (configuration.ignoreDescription) { delete newOperation.description; delete newOperation.summary; @@ -125,7 +183,6 @@ function processOperation(operation: OpenAPI2Operation): OpenAPI2Operation { return newOperation; } - function processResponse(response: OpenAPI2Response): OpenAPI2Response { const newResponse: OpenAPI2Response = deepCopy(response); newResponse.description = "ignore"; @@ -146,15 +203,9 @@ function processParameter(parameter: Refable): Refable = deepCopy(parameter); if ((parameter as Ref).$ref) { const refPath = (parameter as Ref).$ref; - if (refPath.startsWith("#/parameters/")) { - const parameterName = refPath.substring("#/parameters/".length); - const originalParameter = originalDocument?.parameters?.[parameterName]; - if (originalParameter) { - return processParameter(originalParameter); - } - } - } - else { + const originalParameter = getOriginalParameter(refPath); + if (originalParameter) return processParameter(originalParameter); + } else { const inlineParameter = parameter as OpenAPI2Parameter; if ((parameter as any).enum && (newParameter as any)["x-ms-enum"]?.["values"]) { delete (newParameter as any)["x-ms-enum"]["values"]; @@ -196,12 +247,25 @@ function processDefinition(definition: OpenAPI2Schema): OpenAPI2Schema { delete newDefinition.additionalProperties; } + if ( + (newDefinition.properties || newDefinition.additionalProperties) && + newDefinition.type === undefined + ) { + newDefinition.type = "object"; + } + if (newDefinition.allOf) { newDefinition.allOf = newDefinition.allOf.map((item) => { if ((item as Ref).$ref) { const refPath = (item as Ref).$ref; - if (refPath === "../../../../../common-types/resource-management/v3/types.json#/definitions/Resource") { - return { ...item, "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ProxyResource" }; + if ( + refPath === + "../../../../../common-types/resource-management/v3/types.json#/definitions/Resource" + ) { + return { + ...item, + $ref: "../../../../../common-types/resource-management/v3/types.json#/definitions/ProxyResource", + }; } } return item; @@ -226,10 +290,16 @@ function processDefinition(definition: OpenAPI2Schema): OpenAPI2Schema { } function processProperty(property: OpenAPI2SchemaProperty): OpenAPI2SchemaProperty { + function isOpenAPI2SchemaRefProperty( + prop: OpenAPI2SchemaProperty, + ): prop is OpenAPI2SchemaRefProperty { + return (prop as OpenAPI2SchemaRefProperty).$ref !== undefined; + } + const newProperty: OpenAPI2SchemaProperty = deepCopy(property); - const isRef = (property as OpenAPI2SchemaRefProperty).$ref !== undefined; + const isRef = isOpenAPI2SchemaRefProperty(newProperty); if (isRef) { - const refPath = (property as OpenAPI2SchemaRefProperty).$ref; + const refPath = newProperty.$ref; if (refPath.startsWith("#/definitions/")) { const definitionName = refPath.substring("#/definitions/".length); const originalDefinition = originalDocument?.definitions?.[definitionName]; @@ -239,10 +309,43 @@ function processProperty(property: OpenAPI2SchemaProperty): OpenAPI2SchemaProper (newProperty as any)[key] = (processedDefinition as any)[key as string]; } delete (newProperty as any).$ref; + } else if ( + originalDefinition?.type && + ["boolean", "integer", "number", "string"].includes(originalDefinition.type) + ) { + delete (newProperty as any).$ref; + for (const key in originalDefinition) { + (newProperty as any)[key] = (originalDefinition as any)[key]; + } } } } else { - processEnumInplace(newProperty as OpenAPI2Schema); + if (newProperty.type === "array" && newProperty.items) { + const refPath = (newProperty.items as Ref).$ref; + if (refPath !== undefined) { + if (refPath.startsWith("#/definitions/")) { + const definitionName = refPath.substring("#/definitions/".length); + const originalDefinition = originalDocument?.definitions?.[definitionName]; + if (originalDefinition && originalDefinition.enum) { + const processedDefinition = processDefinition(originalDefinition); + for (const key in processedDefinition) { + (newProperty.items as any)[key] = (processedDefinition as any)[key as string]; + } + delete (newProperty.items as any).$ref; + } + } + } else { + processEnumInplace(newProperty.items as OpenAPI2Schema); + } + } + + processEnumInplace(newProperty); + if ( + (newProperty.properties || newProperty.additionalProperties) && + newProperty.type === undefined + ) { + newProperty.type = "object"; + } } const identifiers = (newProperty as any)["x-ms-identifiers"]; @@ -253,9 +356,11 @@ function processProperty(property: OpenAPI2SchemaProperty): OpenAPI2SchemaProper delete (newProperty as OpenAPI2Schema).uniqueItems; } if (newProperty["x-ms-mutability"]) { - newProperty["x-ms-mutability"] = newProperty["x-ms-mutability"].sort((a, b) => a.localeCompare(b)); + newProperty["x-ms-mutability"] = newProperty["x-ms-mutability"].sort((a, b) => + a.localeCompare(b), + ); } - if ((newProperty as any)["uniqueItems"] === false) { + if ((newProperty as any)["uniqueItems"] === false) { delete (newProperty as any)["uniqueItems"]; } @@ -301,7 +406,8 @@ function processPageModel(definition: OpenAPI2Schema): OpenAPI2Schema { newDefinition.description = "[Placeholder] Discription for page model"; newDefinition.properties!["value"]!.description = "[Placeholder] Discription for value property"; - newDefinition.properties!["nextLink"]!.description = "[Placeholder] Discription for nextLink property"; + newDefinition.properties!["nextLink"]!.description = + "[Placeholder] Discription for nextLink property"; (newDefinition.properties!["nextLink"] as any)["format"] = "uri"; if (newDefinition.properties!["nextLink"]?.readOnly) { delete newDefinition.properties!["nextLink"]?.readOnly; @@ -315,7 +421,7 @@ function deepCopy(value: T): T { return value; } - if (typeof value !== 'object') { + if (typeof value !== "object") { return value; } @@ -324,7 +430,7 @@ function deepCopy(value: T): T { } if (Array.isArray(value)) { - return value.map(item => deepCopy(item)) as unknown as T; + return value.map((item) => deepCopy(item)) as unknown as T; } const result: Record = {}; @@ -335,4 +441,4 @@ function deepCopy(value: T): T { } return result as T; -} \ No newline at end of file +} diff --git a/eng/tools/typespec-migration-validation/src/fix/default.ts b/eng/tools/typespec-migration-validation/src/fix/default.ts deleted file mode 100644 index 27f5315e46b5..000000000000 --- a/eng/tools/typespec-migration-validation/src/fix/default.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { checkPropertyAttributeDeleted, getPropertyName } from "./helper.js"; - -export function checkDefault(jsonObj: any): string[] { - const suggestedFixes: string[] = []; - - const deletedChanges = checkPropertyAttributeDeleted('default', jsonObj); - if (deletedChanges.length > 0) { - for (const change of deletedChanges) { - const { path, value } = change; - if (getPropertyName(path)) { - const [definitionName, propertyName] = getPropertyName(path)!; - suggestedFixes.push(`Find a model called "${definitionName}". Change its property "${propertyName}" by adding \` = ${typeof value === "string" ? `"${value}"` : value }\`.`); - } - } - } - - return suggestedFixes; -} \ No newline at end of file diff --git a/eng/tools/typespec-migration-validation/src/fix/definition.ts b/eng/tools/typespec-migration-validation/src/fix/definition.ts new file mode 100644 index 000000000000..4cb5c2cf2639 --- /dev/null +++ b/eng/tools/typespec-migration-validation/src/fix/definition.ts @@ -0,0 +1,138 @@ +import { Suggestion } from "../jsonOutput.js"; +import { constructJsonPath } from "../summary.js"; +import { getPropertyName } from "./helper.js"; + +const knownPropertyDecoratorMapping: { [key: string]: string } = { + minimum: "minValue", + maximum: "maxValue", + minLength: "minLength", + maxLength: "maxLength", +}; + +const addedKey = "__added"; +const deletedKey = "__deleted"; + +export function handleAdded(diff: { + path: string; + value: string; + key: string; +}): Suggestion | undefined { + const { path, value, key } = diff; + if (key.endsWith(addedKey)) { + const originalKey = key.slice(0, addedKey.length); // Remove '__added' suffix + const property = getPropertyName(path); + + if (originalKey === "x-ms-client-name") { + if (property) { + const [definitionName, propertyName] = property; + return { + suggestion: `Find this TypeSpec statement @@clientName(${definitionName}.${propertyName}, "${value}") in file back-compatible.tsp or client.tsp. Delete this statement.`, + path: constructJsonPath(path, key), + }; + } + } + } + + return undefined; +} + +export function handleDeleted(diff: { + path: string; + value: string; + key: string; +}): Suggestion | undefined { + const { path, value, key } = diff; + if (key.endsWith(deletedKey)) { + const originalKey = key.slice(0, deletedKey.length); // Remove '__deleted' suffix + const property = getPropertyName(path); + + if (originalKey === "x-ms-client-name") { + if (property) { + const [definitionName, propertyName] = property; + const suggestion = `@@clientName(${definitionName}.${propertyName}, "${value}");`; + return { + suggestion: `Find file "back-compatible.tsp" or "client.tsp" and add the following statement exactly as it is:: + \`\`\`typespec + ${suggestion} + \`\`\``, + path: constructJsonPath(path, key), + }; + } + } else if (originalKey === "x-ms-client-flatten") { + if ((value as any) === true && property) { + const [definitionName, propertyName] = property; + const suggestion = `@@flattenProperty(${definitionName}.${propertyName});`; + return { + suggestion: `Find file "back-compatible.tsp" or "client.tsp" and add the following statement exactly as it is:: + \`\`\`typespec + ${suggestion} + \`\`\``, + path: constructJsonPath(path, key), + }; + } + } else if (Object.keys(knownPropertyDecoratorMapping).includes(originalKey)) { + const decoratorName = knownPropertyDecoratorMapping[originalKey]; + if (property) { + const [definitionName, propertyName] = property; + return { + suggestion: `Find a model called "${definitionName}". Add \`@${decoratorName}(${value})\` onto its property "${propertyName}". If the property cannot access directly, add \`@@${decoratorName}(${definitionName}.${propertyName}, ${value});\` right after the model.`, + path: constructJsonPath(path, key), + }; + } + } else if (originalKey === "x-nullable") { + if ((value as any) === true && property) { + const [definitionName, propertyName] = property; + return { + suggestion: `Find a model called "${definitionName}". Change its property "${propertyName}" by adding \` | null\` to its property type.`, + path: constructJsonPath(path, key), + }; + } + } else if (originalKey === "readOnly") { + if ((value as any) === true && property) { + const [definitionName, propertyName] = property; + return { + suggestion: `Find a model called "${definitionName}". Add \`@visibility(Lifecycle.Read)\` onto its property "${propertyName}". If the property cannot access directly, add \`@@visibility(${definitionName}.${propertyName}, Lifecycle.Read);\` RIGHT AFTER the end bracket of the model.`, + path: constructJsonPath(path, key), + }; + } + } else if (originalKey === "x-ms-secret") { + if ((value as any) === true && property) { + const [definitionName, propertyName] = property; + return { + suggestion: `Find a model called "${definitionName}". Add \`@secret\` onto its property "${propertyName}". If the property cannot access directly, add \`@@secret(${definitionName}.${propertyName});\` right after the model.`, + path: constructJsonPath(path, key), + }; + } + } else if (originalKey === "default") { + if (property) { + const [definitionName, propertyName] = property; + return { + suggestion: `Find a model called "${definitionName}". Change its property "${propertyName}" by adding \` = ${typeof value === "string" ? `"${value}"` : value}\`.`, + path: constructJsonPath(path, key), + }; + } + } + } + + return undefined; +} + +export function handleChanged(diff: { + path: string; + oldValue: string; + newValue: string; + key: string; +}): Suggestion | undefined { + const { path, oldValue, newValue, key } = diff; + const property = getPropertyName(path); + if (key === "x-ms-client-name") { + if (property) { + const [definitionName, propertyName] = property; + return { + suggestion: `Find this TypeSpec statement @@clientName(${definitionName}.${propertyName}, "${newValue}") in file back-compatible.tsp or client.tsp. Change it to @@clientName(${definitionName}.${propertyName}, "${oldValue}")`, + path: path, + }; + } + } + return undefined; +} diff --git a/eng/tools/typespec-migration-validation/src/fix/helper.ts b/eng/tools/typespec-migration-validation/src/fix/helper.ts index 8149bdf20306..de9b5258f93a 100644 --- a/eng/tools/typespec-migration-validation/src/fix/helper.ts +++ b/eng/tools/typespec-migration-validation/src/fix/helper.ts @@ -1,7 +1,88 @@ -export function checkPropertyAttributeDeleted(checkKey: string, jsonObj: any, currentPath: string = ''): Array<{path: string, value: string}> { - const results: Array<{path: string, value: string}> = []; +export function checkElementAddedOrDeleted( + jsonObj: any, + currentPath: string = "", +): Array<{ path: string; value: string; key: string }> { + const results: Array<{ path: string; value: string; key: string }> = []; - if (!jsonObj || typeof jsonObj !== 'object') { + if (!jsonObj || typeof jsonObj !== "object") { + return results; + } + + for (const key in jsonObj) { + if (!Object.prototype.hasOwnProperty.call(jsonObj, key)) { + continue; + } + + const newPath = currentPath ? `${currentPath}.${key}` : key; + + if (key.endsWith("__deleted") || key.endsWith("__added")) { + // Store both the path and the value + results.push({ + path: currentPath, // Use parent path since we're interested in the property that has this extension + value: jsonObj[key], + key: key, + }); + } + + // If value is an object or array, recursively search it + if (jsonObj[key] && typeof jsonObj[key] === "object") { + const nestedResults = checkElementAddedOrDeleted(jsonObj[key], newPath); + results.push(...nestedResults); + } + } + + return results; +} + +export function checkElementChanged( + jsonObj: any, + currentPath: string = "", +): Array<{ path: string; oldValue: string; newValue: string; key: string }> { + const results: Array<{ path: string; oldValue: string; newValue: string; key: string }> = []; + + if (!jsonObj || typeof jsonObj !== "object") { + return results; + } + + for (const key in jsonObj) { + if (!Object.prototype.hasOwnProperty.call(jsonObj, key)) { + continue; + } + + const newPath = currentPath ? `${currentPath}.${key}` : key; + + if ( + typeof jsonObj[key] === "object" && + jsonObj[key]["__old"] !== undefined && + jsonObj[key]["__new"] !== undefined + ) { + // Store the path, old value and new value + results.push({ + path: currentPath, // Use parent path since we're interested in the property that has this extension + oldValue: jsonObj[key]["__old"], + newValue: jsonObj[key]["__new"], + key: key, + }); + } + + // If value is an object or array, recursively search it + if (jsonObj[key] && typeof jsonObj[key] === "object") { + const nestedResults = checkElementChanged(jsonObj[key], newPath); + results.push(...nestedResults); + } + } + + return results; +} + +export function checkPropertyAttributeDeleted( + checkKey: string, + jsonObj: any, + currentPath: string = "", +): Array<{ path: string; value: string; key: string }> { + const results: Array<{ path: string; value: string; key: string }> = []; + + if (!jsonObj || typeof jsonObj !== "object") { return results; } @@ -16,12 +97,13 @@ export function checkPropertyAttributeDeleted(checkKey: string, jsonObj: any, cu // Store both the path and the value results.push({ path: currentPath, // Use parent path since we're interested in the property that has this extension - value: jsonObj[key] + value: jsonObj[key], + key: key, }); } // If value is an object or array, recursively search it - if (jsonObj[key] && typeof jsonObj[key] === 'object') { + if (jsonObj[key] && typeof jsonObj[key] === "object") { const nestedResults = checkPropertyAttributeDeleted(checkKey, jsonObj[key], newPath); results.push(...nestedResults); } @@ -30,9 +112,13 @@ export function checkPropertyAttributeDeleted(checkKey: string, jsonObj: any, cu return results; } -export function checkPropertyAttributeAdded(checkKey: string, jsonObj: any, currentPath: string = ''): Array<{path: string, value: string}> { - const results: Array<{path: string, value: string}> = []; - if (!jsonObj || typeof jsonObj !== 'object') { +export function checkPropertyAttributeAdded( + checkKey: string, + jsonObj: any, + currentPath: string = "", +): Array<{ path: string; value: string; key: string }> { + const results: Array<{ path: string; value: string; key: string }> = []; + if (!jsonObj || typeof jsonObj !== "object") { return results; } for (const key in jsonObj) { @@ -44,11 +130,12 @@ export function checkPropertyAttributeAdded(checkKey: string, jsonObj: any, curr // Store both the path and the value results.push({ path: currentPath, // Use parent path since we're interested in the property that has this extension - value: jsonObj[key] + value: jsonObj[key], + key: key, }); } - if (jsonObj[key] && typeof jsonObj[key] === 'object') { + if (jsonObj[key] && typeof jsonObj[key] === "object") { const nestedResults = checkPropertyAttributeAdded(checkKey, jsonObj[key], newPath); results.push(...nestedResults); } @@ -56,10 +143,14 @@ export function checkPropertyAttributeAdded(checkKey: string, jsonObj: any, curr return results; } -export function checkPropertyAttributeChanged(checkKey: string, jsonObj: any, currentPath: string = ''): Array<{path: string, oldValue: string, newValue: string}> { - const results: Array<{path: string, oldValue: string, newValue: string}> = []; +export function checkPropertyAttributeChanged( + checkKey: string, + jsonObj: any, + currentPath: string = "", +): Array<{ path: string; oldValue: string; newValue: string }> { + const results: Array<{ path: string; oldValue: string; newValue: string }> = []; - if (!jsonObj || typeof jsonObj !== 'object') { + if (!jsonObj || typeof jsonObj !== "object") { return results; } @@ -70,21 +161,22 @@ export function checkPropertyAttributeChanged(checkKey: string, jsonObj: any, cu const newPath = currentPath ? `${currentPath}.${key}` : key; - // Check if this is an x-ms-client-name with __old and __new properties - if (key === checkKey && - typeof jsonObj[key] === 'object' && - jsonObj[key]['__old'] !== undefined && - jsonObj[key]['__new'] !== undefined) { + if ( + key === checkKey && + typeof jsonObj[key] === "object" && + jsonObj[key]["__old"] !== undefined && + jsonObj[key]["__new"] !== undefined + ) { // Store the path, old value and new value results.push({ path: currentPath, // Use parent path since we're interested in the property that has this extension - oldValue: jsonObj[key]['__old'], - newValue: jsonObj[key]['__new'] + oldValue: jsonObj[key]["__old"], + newValue: jsonObj[key]["__new"], }); } // If value is an object or array, recursively search it - if (jsonObj[key] && typeof jsonObj[key] === 'object') { + if (jsonObj[key] && typeof jsonObj[key] === "object") { const nestedResults = checkPropertyAttributeChanged(checkKey, jsonObj[key], newPath); results.push(...nestedResults); } @@ -93,13 +185,15 @@ export function checkPropertyAttributeChanged(checkKey: string, jsonObj: any, cu return results; } -export function getPropertyName(jsonPath: string): [definitionName: string, propertyName: string] | undefined { - const pathParts = jsonPath.split('.'); - const definitionIndex = pathParts.findIndex(part => part === 'definitions'); +export function getPropertyName( + jsonPath: string, +): [definitionName: string, propertyName: string] | undefined { + const pathParts = jsonPath.split("."); + const definitionIndex = pathParts.findIndex((part) => part === "definitions"); if (definitionIndex !== -1 && definitionIndex + 3 < pathParts.length) { const definitionName = pathParts[definitionIndex + 1]; const propertyName = pathParts[definitionIndex + 3]; return [definitionName, propertyName]; } return undefined; -} \ No newline at end of file +} diff --git a/eng/tools/typespec-migration-validation/src/fix/minMax.ts b/eng/tools/typespec-migration-validation/src/fix/minMax.ts deleted file mode 100644 index 16b2ede0cff0..000000000000 --- a/eng/tools/typespec-migration-validation/src/fix/minMax.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { checkPropertyAttributeDeleted, getPropertyName } from "./helper.js"; - -const knownPropertyDecoratorMapping: { [key: string]: string } = { - 'minimum': 'minValue', - 'maximum': 'maxValue', - 'minLength': 'minLength', - 'maxLength': 'maxLength' -}; - -export function checkMinMax(jsonObj: any): string[] { - const suggestedFixes: string[] = []; - - for (const [key, decoratorName] of Object.entries(knownPropertyDecoratorMapping)) { - const deletedChanges = checkPropertyAttributeDeleted(key, jsonObj); - if (deletedChanges.length > 0) { - for (const change of deletedChanges) { - const { path, value } = change; - if (getPropertyName(path)) { - const [definitionName, propertyName] = getPropertyName(path)!; - suggestedFixes.push(`Find a model called "${definitionName}". Add \`@${decoratorName}(${value})\` onto its property "${propertyName}". If the property cannot access directly, add \`@@${decoratorName}(${definitionName}.${propertyName}, ${value});\` right after the model.`); - } - } - } - } - - return suggestedFixes; -} diff --git a/eng/tools/typespec-migration-validation/src/fix/nullable.ts b/eng/tools/typespec-migration-validation/src/fix/nullable.ts deleted file mode 100644 index 52fcbca526d0..000000000000 --- a/eng/tools/typespec-migration-validation/src/fix/nullable.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { checkPropertyAttributeDeleted, getPropertyName } from "./helper.js"; - -export function checkNullable(jsonObj: any): string[] { - const suggestedFixes: string[] = []; - - const deletedChanges = checkPropertyAttributeDeleted('x-nullable', jsonObj); - if (deletedChanges.length > 0) { - for (const change of deletedChanges) { - const { path, value } = change; - if ((value as any) === false) continue; - - if (getPropertyName(path)) { - const [definitionName, propertyName] = getPropertyName(path)!; - suggestedFixes.push(`Find a model called "${definitionName}". Change its property "${propertyName}" by adding \` | null\` to its property type.`); - } - } - } - - return suggestedFixes; -} \ No newline at end of file diff --git a/eng/tools/typespec-migration-validation/src/fix/secret.ts b/eng/tools/typespec-migration-validation/src/fix/secret.ts deleted file mode 100644 index f18a8accbb4c..000000000000 --- a/eng/tools/typespec-migration-validation/src/fix/secret.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { checkPropertyAttributeDeleted, getPropertyName } from "./helper.js"; - -export function checkSecret(jsonObj: any): string[] { - const suggestedFixes: string[] = []; - - const deletedChanges = checkPropertyAttributeDeleted('x-ms-secret', jsonObj); - if (deletedChanges.length > 0) { - for (const change of deletedChanges) { - const { path, value } = change; - if ((value as any) === false) continue; - - if (getPropertyName(path)) { - const [definitionName, propertyName] = getPropertyName(path)!; - suggestedFixes.push(`Find a model called "${definitionName}". Add \`@secret\` onto its property "${propertyName}". If the property cannot access directly, add \`@@secret(${definitionName}.${propertyName});\` right after the model.`); - } - } - } - - return suggestedFixes; -} diff --git a/eng/tools/typespec-migration-validation/src/fix/troubleshooting.ts b/eng/tools/typespec-migration-validation/src/fix/troubleshooting.ts index a5f7a7be1c8f..ca5af741fd33 100644 --- a/eng/tools/typespec-migration-validation/src/fix/troubleshooting.ts +++ b/eng/tools/typespec-migration-validation/src/fix/troubleshooting.ts @@ -1,83 +1,39 @@ -import { jsonOutput } from "../index.js"; -import { checkDefault } from "./default.js"; -import { checkPropertyAttributeAdded, checkPropertyAttributeChanged, checkPropertyAttributeDeleted, getPropertyName } from "./helper.js"; -import { checkMinMax } from "./minMax.js"; -import { checkNullable } from "./nullable.js"; -import { checkReadOnly } from "./readonly.js"; -import { checkSecret } from "./secret.js"; - -export function suggestFix(jsonObj: any): string[] { - const suggestedFixes: string[] = []; - - const clientNameStatement = checkPropertyAttributeDeleted('x-ms-client-name', jsonObj); - for (const statement of clientNameStatement) { - const { path, value } = statement; - if (getPropertyName(path)) { - const [definitionName, propertyName] = getPropertyName(path)!; - suggestedFixes.push(`@@clientName(${definitionName}.${propertyName}, "${value}");`); - } - } - - const flatternStatement = checkPropertyAttributeDeleted('x-ms-client-flatten', jsonObj).filter(entry => (entry.value as any) === true); - for (const statement of flatternStatement) { - const { path, value } = statement; - if ((value as any) === false) continue; - - if (getPropertyName(path)) { - const [definitionName, propertyName] = getPropertyName(path)!; - suggestedFixes.push(`@@flattenProperty(${definitionName}.${propertyName});`); +import { jsonOutput, Suggestion } from "../jsonOutput.js"; +import { handleAdded, handleChanged, handleDeleted } from "./definition.js"; +import { checkElementAddedOrDeleted, checkElementChanged } from "./helper.js"; + +export function generatePrompts(jsonObj: any): string[] { + const suggestedFixes: Suggestion[] = []; + + const elementAddedOrDeleted = checkElementAddedOrDeleted(jsonObj); + for (const change of elementAddedOrDeleted) { + const addedSuggestion = handleAdded(change); + if (addedSuggestion) { + suggestedFixes.push(addedSuggestion); } - } - - if (suggestedFixes.length > 0) { - suggestedFixes.forEach((fix) => { - jsonOutput.suggestions.push( - `Find file "back-compatible.tsp" or "client.tsp" and add the following statement exactly as it is:": -\`\`\`typespec -${fix} -\`\`\``, - ); - }); - suggestedFixes.unshift( - `Add the following clientName statements to back-compatible.tsp or client.tsp:`, - ); - } - return suggestedFixes; -} - -export function suggestPrompt(jsonObj: any): string[] { - const suggestedFixes: string[] = []; - - const clientNameChanges = checkPropertyAttributeChanged('x-ms-client-name', jsonObj); - for (const change of clientNameChanges) { - const { path, oldValue, newValue } = change; - if (getPropertyName(path)) { - const [definitionName, propertyName] = getPropertyName(path)!; - suggestedFixes.push(`Find this TypeSpec statement @@clientName(${definitionName}.${propertyName}, "${newValue}") in file back-compatible.tsp or client.tsp. Change it to @@clientName(${definitionName}.${propertyName}, "${oldValue})"`); + const deletedSuggestion = handleDeleted(change); + if (deletedSuggestion) { + suggestedFixes.push(deletedSuggestion); } } - const clientNameAdded = checkPropertyAttributeAdded('x-ms-client-name', jsonObj); - for (const change of clientNameAdded) { - const { path, value } = change; - if (getPropertyName(path)) { - const [definitionName, propertyName] = getPropertyName(path)!; - suggestedFixes.push(`Find this TypeSpec statement @@clientName(${definitionName}.${propertyName}, "${value}") in file back-compatible.tsp or client.tsp. Delete this statement.`); + const elementChanged = checkElementChanged(jsonObj); + for (const change of elementChanged) { + const changedSuggestion = handleChanged(change); + if (changedSuggestion) { + suggestedFixes.push(changedSuggestion); } } - suggestedFixes.push(...checkMinMax(jsonObj)); - suggestedFixes.push(...checkReadOnly(jsonObj)); - suggestedFixes.push(...checkDefault(jsonObj)); - suggestedFixes.push(...checkNullable(jsonObj)); - suggestedFixes.push(...checkSecret(jsonObj)); - + const suggestionsAsString = suggestedFixes.map((s) => s.suggestion); if (suggestedFixes.length > 0) { jsonOutput.suggestions.push(...suggestedFixes); - suggestedFixes.unshift(`You are an expert in TypeSpec. Follow the prompt exactly as written. Do not add any additional suggestions or modifications unless explicitly requested.`); - for (let i = 1; i < suggestedFixes.length; i++) { - suggestedFixes[i] = `${i}. ${suggestedFixes[i]}`; + suggestionsAsString.unshift( + `You are an expert in TypeSpec. Follow the prompt exactly as written. Do not add any additional suggestions or modifications unless explicitly requested.`, + ); + for (let i = 1; i < suggestionsAsString.length; i++) { + suggestionsAsString[i] = `${i}. ${suggestionsAsString[i]}`; } } - return suggestedFixes; + return suggestionsAsString; } diff --git a/eng/tools/typespec-migration-validation/src/helper.ts b/eng/tools/typespec-migration-validation/src/helper.ts index 759c37c6879c..0f100d12028c 100644 --- a/eng/tools/typespec-migration-validation/src/helper.ts +++ b/eng/tools/typespec-migration-validation/src/helper.ts @@ -1,7 +1,7 @@ -import { OpenAPI2Document } from '@azure-tools/typespec-autorest'; -import fs from 'fs'; -import path from 'path'; -import { logWarning } from './log.js'; +import { OpenAPI2Document } from "@azure-tools/typespec-autorest"; +import fs from "fs"; +import path from "path"; +import { logWarning } from "./log.js"; /** * Reads all files in a directory recursively, excluding paths containing a specified string @@ -11,7 +11,7 @@ import { logWarning } from './log.js'; */ function readFilesFromDirectory( directoryPath: string, - excludePattern: string = 'example' + excludePattern: string = "example", ): string[] { const results: string[] = []; @@ -28,7 +28,7 @@ function readFilesFromDirectory( } // Skip paths that are not json files - if (!filePath.endsWith('.json')) { + if (!filePath.endsWith(".json")) { continue; } @@ -50,12 +50,16 @@ function readFilesFromDirectory( * @returns File contents as string */ export function readFileContent(filePath: string): string { - return fs.readFileSync(filePath, 'utf8'); + return fs.readFileSync(filePath, "utf8"); } export function mergeFiles(folderPath: string): OpenAPI2Document { - const files = readFilesFromDirectory(folderPath, 'example'); - const mergedContent: OpenAPI2Document = { swagger: '2.0', info: { title: "placeholder", version: "placeholder" }, paths: {} }; + const files = readFilesFromDirectory(folderPath, "example"); + const mergedContent: OpenAPI2Document = { + swagger: "2.0", + info: { title: "placeholder", version: "placeholder" }, + paths: {}, + }; for (const file of files) { const fileContent = readFileContent(file); @@ -113,4 +117,4 @@ export function mergeFiles(folderPath: string): OpenAPI2Document { } return mergedContent; -} \ No newline at end of file +} diff --git a/eng/tools/typespec-migration-validation/src/ignore.ts b/eng/tools/typespec-migration-validation/src/ignore.ts index 85f4cb87bce7..5ff5f8cc4f4e 100644 --- a/eng/tools/typespec-migration-validation/src/ignore.ts +++ b/eng/tools/typespec-migration-validation/src/ignore.ts @@ -10,18 +10,20 @@ export function addIgnorePath(path: string): void { ignoreList.add(path); } -export function processIgnoreList(sortedOldFile: OpenAPI2Document, sortedNewFile: OpenAPI2Document): void { +export function processIgnoreList( + sortedOldFile: OpenAPI2Document, + sortedNewFile: OpenAPI2Document, +): void { // Process each path in the ignore list for (const path of ignoreList) { - if (path.endsWith('__added')) { - const realPath = path.replace(/__added$/, ''); + if (path.endsWith("__added")) { + const realPath = path.replace(/__added$/, ""); // Delete the added element from the new file deleteElementByJsonPath(sortedNewFile, realPath); - } else if (path.endsWith('__deleted')) { - const realPath = path.replace(/__deleted$/, ''); + } else if (path.endsWith("__deleted")) { + const realPath = path.replace(/__deleted$/, ""); deleteElementByJsonPath(sortedOldFile, realPath); - } - else { + } else { const oldValue = getElementByJsonPath(sortedOldFile, path); if (oldValue !== undefined) { setElementByJsonPath(sortedNewFile, path, oldValue); @@ -39,26 +41,26 @@ export function processIgnoreList(sortedOldFile: OpenAPI2Document, sortedNewFile */ function parseJsonPath(path: string): string[] { const segments: string[] = []; - let currentSegment = ''; + let currentSegment = ""; let inBracket = false; let inArrayNotation = false; let bracketDepth = 0; - + for (let i = 0; i < path.length; i++) { const char = path[i]; - + // Handle start of bracket notation for property access - if (char === '[' && !inBracket && path[i+1] === "'" || path[i+1] === '"') { + if ((char === "[" && !inBracket && path[i + 1] === "'") || path[i + 1] === '"') { // Start of bracket notation for property with special chars if (currentSegment) { segments.push(currentSegment); - currentSegment = ''; + currentSegment = ""; } inBracket = true; continue; - } + } // Handle start of array index notation - else if (char === '[' && !inBracket) { + else if (char === "[" && !inBracket) { // This is an array index notation, keep it with the current segment inArrayNotation = true; bracketDepth++; @@ -68,17 +70,17 @@ function parseJsonPath(path: string): string[] { // Handle quotes in bracket notation else if (inBracket && (char === "'" || char === '"')) { continue; - } + } // Handle end of bracket notation for property access - else if (char === ']' && inBracket) { + else if (char === "]" && inBracket) { // End of bracket notation segments.push(currentSegment); - currentSegment = ''; + currentSegment = ""; inBracket = false; continue; - } + } // Handle end of array index notation - else if (char === ']' && inArrayNotation) { + else if (char === "]" && inArrayNotation) { // End of array bracket, keep it as part of the segment bracketDepth--; if (bracketDepth === 0) { @@ -86,26 +88,26 @@ function parseJsonPath(path: string): string[] { } currentSegment += char; continue; - } + } // Handle dot separator - else if (char === '.' && !inBracket && !inArrayNotation) { + else if (char === "." && !inBracket && !inArrayNotation) { // Dot separator (only when not in bracket or array notation) if (currentSegment) { segments.push(currentSegment); - currentSegment = ''; + currentSegment = ""; } continue; } - + // Regular character, add to current segment currentSegment += char; } - + // Add the last segment if any if (currentSegment) { segments.push(currentSegment); } - + return segments; } @@ -116,43 +118,51 @@ function parseJsonPath(path: string): string[] { */ function deleteElementByJsonPath(obj: any, path: string): void { const segments = parseJsonPath(path); - + let current = obj; - + // Navigate to the parent of the element to delete for (let i = 0; i < segments.length - 1; i++) { const segment = segments[i]; - + // Handle array index notation [n] const arrayMatch = segment.match(/^(.*?)\[(\d+)\]$/); if (arrayMatch) { const arrayName = arrayMatch[1]; const arrayIndex = parseInt(arrayMatch[2], 10); - - if (!current[arrayName] || !Array.isArray(current[arrayName]) || arrayIndex >= current[arrayName].length) { + + if ( + !current[arrayName] || + !Array.isArray(current[arrayName]) || + arrayIndex >= current[arrayName].length + ) { // Path doesn't exist, nothing to delete return; } current = current[arrayName][arrayIndex]; } else { - if (!current[segment] || typeof current[segment] !== 'object') { + if (!current[segment] || typeof current[segment] !== "object") { // Path doesn't exist, nothing to delete return; } current = current[segment]; } } - + // Delete the element const lastSegment = segments[segments.length - 1]; - + // Handle array index notation for the last part const arrayMatch = lastSegment.match(/^(.*?)\[(\d+)\]$/); if (arrayMatch) { const arrayName = arrayMatch[1]; const arrayIndex = parseInt(arrayMatch[2], 10); - - if (current[arrayName] && Array.isArray(current[arrayName]) && arrayIndex < current[arrayName].length) { + + if ( + current[arrayName] && + Array.isArray(current[arrayName]) && + arrayIndex < current[arrayName].length + ) { current[arrayName].splice(arrayIndex, 1); } } else { @@ -169,15 +179,19 @@ function deleteElementByJsonPath(obj: any, path: string): void { function getElementByJsonPath(obj: any, path: string): any { const parts = parseJsonPath(path); let current = obj; - + for (const part of parts) { // Handle array index notation const arrayMatch = part.match(/^(.*)\[(\d+)\]$/); if (arrayMatch) { const arrayName = arrayMatch[1]; const arrayIndex = parseInt(arrayMatch[2], 10); - - if (!current[arrayName] || !Array.isArray(current[arrayName]) || arrayIndex >= current[arrayName].length) { + + if ( + !current[arrayName] || + !Array.isArray(current[arrayName]) || + arrayIndex >= current[arrayName].length + ) { return undefined; } current = current[arrayName][arrayIndex]; @@ -188,7 +202,7 @@ function getElementByJsonPath(obj: any, path: string): any { current = current[part]; } } - + return current; } @@ -201,27 +215,27 @@ function getElementByJsonPath(obj: any, path: string): any { function setElementByJsonPath(obj: any, path: string, value: any): void { const parts = parseJsonPath(path); let current = obj; - + // Navigate to the parent of where we want to set the value for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; - + // Handle array index notation const arrayMatch = part.match(/^(.*)\[(\d+)\]$/); if (arrayMatch) { const arrayName = arrayMatch[1]; const arrayIndex = parseInt(arrayMatch[2], 10); - + // Ensure the array exists if (!current[arrayName]) { current[arrayName] = []; } - + // Ensure the array is long enough while (current[arrayName].length <= arrayIndex) { current[arrayName].push({}); } - + current = current[arrayName][arrayIndex]; } else { // Ensure the object exists @@ -231,28 +245,28 @@ function setElementByJsonPath(obj: any, path: string, value: any): void { current = current[part]; } } - + // Set the value const lastPart = parts[parts.length - 1]; - + // Handle array index notation for the last part const arrayMatch = lastPart.match(/^(.*)\[(\d+)\]$/); if (arrayMatch) { const arrayName = arrayMatch[1]; const arrayIndex = parseInt(arrayMatch[2], 10); - + // Ensure the array exists if (!current[arrayName]) { current[arrayName] = []; } - + // Ensure the array is long enough while (current[arrayName].length <= arrayIndex) { current[arrayName].push(undefined); } - + current[arrayName][arrayIndex] = value; } else { current[lastPart] = value; } -} \ No newline at end of file +} diff --git a/eng/tools/typespec-migration-validation/src/index.ts b/eng/tools/typespec-migration-validation/src/index.ts index 0bb69f44c6ce..02f85bdcd75d 100644 --- a/eng/tools/typespec-migration-validation/src/index.ts +++ b/eng/tools/typespec-migration-validation/src/index.ts @@ -5,80 +5,88 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { configuration } from "./configuration.js"; import { processDocument } from "./document.js"; -import { suggestFix, suggestPrompt } from "./fix/troubleshooting.js"; +import { generatePrompts } from "./fix/troubleshooting.js"; import { mergeFiles, readFileContent } from "./helper.js"; -import { logHeader, logWarning } from "./log.js"; -import { findChangedPaths, findDifferences, findModifiedValues, formatChangedPathsReport, formatDifferenceReport, formatModifiedValuesReport } from "./summary.js"; import { addIgnorePath, processIgnoreList } from "./ignore.js"; - -export interface JsonOutput { - suggestions: string[]; -} - -export const jsonOutput: JsonOutput = { - suggestions: [], -}; +import { jsonOutput } from "./jsonOutput.js"; +import { logHeader, logWarning } from "./log.js"; +import { + findChangedPaths, + findDifferences, + findModifiedValues, + formatChangedPathsReport, + formatDifferenceReport, + formatModifiedValuesReport, +} from "./summary.js"; function parseArguments() { return yargs(hideBin(process.argv)) - .usage('Usage: $0 [options]') - .command('add-ignore', 'Add paths to ignore file', + .usage("Usage: $0 [options]") + .command( + "add-ignore", + "Add paths to ignore file", (yargs) => { return yargs - .option('path', { - alias: 'p', - describe: 'JSON path to ignore', - type: 'string', - demandOption: true + .option("path", { + alias: "p", + describe: "JSON path to ignore", + type: "string", + demandOption: true, }) - .option('outputFolder', { - alias: 'out', - describe: 'Output folder containing ignore.json', - type: 'string', - demandOption: true + .option("outputFolder", { + alias: "out", + describe: "Output folder containing ignore.json", + type: "string", + demandOption: true, }); - }, + }, (argv) => { handleAddIgnore(argv.path as string, argv.outputFolder as string); - } + }, + ) + .example( + "$0 --oldPath ./old-spec-folder --newPath ./new-spec-file", + "Compare two swagger specs", + ) + .example("$0 oldSpecPath newSpecPath", "Compare using positional arguments") + .example( + "$0 add-ignore --path \"paths['/api/resource'].put.parameters[0].required__added\" --outputFolder ./results", + "Add a path to ignore file", ) - .example('$0 --oldPath ./old-spec-folder --newPath ./new-spec-file', 'Compare two swagger specs') - .example('$0 oldSpecPath newSpecPath', 'Compare using positional arguments') - .example('$0 add-ignore --path "paths[\'/api/resource\'].put.parameters[0].required__added" --outputFolder ./results', 'Add a path to ignore file') - .option('oldPath', { - alias: 'o', - describe: 'Path to old/original Swagger specification folder', - type: 'string', + .option("oldPath", { + alias: "o", + describe: "Path to old/original Swagger specification folder", + type: "string", }) - .option('newPath', { - alias: 'n', - describe: 'Path to new/updated Swagger specification file', - type: 'string', + .option("newPath", { + alias: "n", + describe: "Path to new/updated Swagger specification file", + type: "string", }) - .option('outputFolder', { - alias: 'out', - describe: 'Output folder for analysis results', - type: 'string', + .option("outputFolder", { + alias: "out", + describe: "Output folder for analysis results", + type: "string", }) - .option('ignoreDescription', { - description: 'Ignore description differences', - type: 'boolean', + .option("ignoreDescription", { + description: "Ignore description differences", + type: "boolean", default: true, }) - .option('ignorePathCase', { - description: 'Set case insensitive for the segments before provider, e.g. resourceGroups', - type: 'boolean' + .option("ignorePathCase", { + description: "Set case insensitive for the segments before provider, e.g. resourceGroups", + type: "boolean", }) - .option('jsonOutput', { - description: 'Also output in JSON format', - type: 'boolean', + .option("jsonOutput", { + description: "Also output in JSON format", + type: "boolean", }) .check((argv) => { // Skip validation for the add-ignore command - if (argv._[0] === 'add-ignore') { + if (argv._[0] === "add-ignore") { return true; } - + const positional = argv._; if (!argv.oldPath && positional.length > 0) { argv.oldPath = positional[0]!.toString(); @@ -91,7 +99,7 @@ function parseArguments() { } if (!argv.oldPath || !argv.newPath) { - throw new Error('Both oldPath and newPath are required'); + throw new Error("Both oldPath and newPath are required"); } // Verify paths exist @@ -106,7 +114,7 @@ function parseArguments() { return true; }) .help() - .alias('help', 'h') + .alias("help", "h") .parseSync(); } @@ -118,17 +126,17 @@ function parseArguments() { function handleAddIgnore(path: string, outputFolder: string) { const ignoreFilePath = `${outputFolder}/ignore.json`; let ignoreList: string[] = []; - + // Create output folder if it doesn't exist if (!fs.existsSync(outputFolder)) { fs.mkdirSync(outputFolder, { recursive: true }); } - + // Read existing ignore file if present if (fs.existsSync(ignoreFilePath)) { - ignoreList = JSON.parse(fs.readFileSync(ignoreFilePath, 'utf-8')); + ignoreList = JSON.parse(fs.readFileSync(ignoreFilePath, "utf-8")); } - + // Add new path if not already present if (!ignoreList.includes(path)) { ignoreList.push(path); @@ -137,15 +145,15 @@ function handleAddIgnore(path: string, outputFolder: string) { } else { console.log(`Path "${path}" already exists in ignore list`); } - + process.exit(0); } export async function main() { const args = parseArguments(); - + // If using add-ignore command, the command handler will exit the process - + const { oldPath, newPath, outputFolder, ignoreDescription, ignorePathCase } = args; configuration.ignoreDescription = ignoreDescription; if (ignorePathCase !== undefined) { @@ -167,7 +175,7 @@ export async function main() { const ignoreFilePath = `${outputFolder}/ignore.json`; if (fs.existsSync(ignoreFilePath)) { logHeader(`Processing ignore file...`); - const ignoreFileContent = JSON.parse(fs.readFileSync(ignoreFilePath, 'utf-8')); + const ignoreFileContent = JSON.parse(fs.readFileSync(ignoreFilePath, "utf-8")); for (const path of ignoreFileContent) { addIgnorePath(path); } @@ -175,20 +183,27 @@ export async function main() { processIgnoreList(sortedOldFile, sortedNewFile); } - fs.writeFileSync(`${outputFolder}/oldNormalizedSwagger.json`, JSON.stringify(sortedOldFile, null, 2)); - fs.writeFileSync(`${outputFolder}/newNormalizedSwagger.json`, JSON.stringify(sortedNewFile, null, 2)); + fs.writeFileSync( + `${outputFolder}/oldNormalizedSwagger.json`, + JSON.stringify(sortedOldFile, null, 2), + ); + fs.writeFileSync( + `${outputFolder}/newNormalizedSwagger.json`, + JSON.stringify(sortedNewFile, null, 2), + ); } let report: string = ""; const diffForFile = diff(sortedOldFile, sortedNewFile); - //fs.writeFileSync(`${outputFolder}/diff.json`, JSON.stringify(diffForFile, null, 2)); // // TO-DELETE: Read the diff file from disk // const diffForFile = JSON.parse(fs.readFileSync(`C:/Users/pashao/GIT/azure-rest-api-specs/specification/agrifood/validation-results/diff.json`, 'utf-8')); const changedPaths = findChangedPaths(diffForFile); if (changedPaths.length > 0) { - logWarning(`Found ${changedPaths.length} changed paths in the diff. If it is just case change and you confirm it is expected, run tsmv with --ignorePathCase option to ignore case changes.`); + logWarning( + `Found ${changedPaths.length} changed paths in the diff.`, + ); const changedPathsReport = formatChangedPathsReport(changedPaths); console.log(changedPathsReport); report += changedPathsReport; @@ -203,32 +218,27 @@ export async function main() { const modifiedValuesReport = formatModifiedValuesReport(modifiedValues); console.log(modifiedValuesReport); report += modifiedValuesReport; - + if (outputFolder) { + fs.writeFileSync(`${outputFolder}/diff.json`, JSON.stringify(diffForFile, null, 2)); fs.writeFileSync(`${outputFolder}/API_CHANGES.md`, report); logHeader(`Difference report written to ${outputFolder}/API_CHANGES.md`); - const suggestedFixes = suggestFix(diffForFile); - if (suggestedFixes.length > 0) { - logWarning(`Considering these suggested fixes for the diff:`); - suggestedFixes.forEach(fix => { - console.log(fix); - }); - } - const suggestedPrompt = suggestPrompt(diffForFile); + const suggestedPrompt = generatePrompts(diffForFile); if (suggestedPrompt.length > 0) { logWarning(`Considering these suggested prompts for the diff:`); - suggestedPrompt.forEach(prompt => { + suggestedPrompt.forEach((prompt) => { console.log(prompt); }); } if (args.jsonOutput) { - console.log(`---- Start of Json Output ---- -${JSON.stringify(jsonOutput, null, 2)} ----- End of Json Output ----`); + fs.writeFileSync(`${outputFolder}/tsmv_output.json`, JSON.stringify(jsonOutput, null, 2)); + logHeader(`JSON output written to ${outputFolder}/tsmv_output.json`); + console.log( + `---- Start of Json Output ----\n${JSON.stringify(jsonOutput, null, 2)}\n---- End of Json Output ----`, + ); } - } - else { + } else { console.log(report); } } diff --git a/eng/tools/typespec-migration-validation/src/jsonOutput.ts b/eng/tools/typespec-migration-validation/src/jsonOutput.ts new file mode 100644 index 000000000000..6c2877421c77 --- /dev/null +++ b/eng/tools/typespec-migration-validation/src/jsonOutput.ts @@ -0,0 +1,40 @@ +interface ChangeBase { + category: string; + path: string; +} + +export interface ValueChange extends ChangeBase { + category: "value-changed"; + oldValue: string; + newValue: string; +} + +export interface AddDeleteChange extends ChangeBase { + category: "added-or-deleted"; + key: string; + type: "added" | "deleted"; + value: string; +} + +export interface PathChange extends ChangeBase { + category: "path-changed"; + type: "added" | "deleted"; +} + +export type Change = ValueChange | AddDeleteChange | PathChange; + +export interface Suggestion { + suggestion: string; + path?: string; +} +export interface JsonOutput { + version: "1.0.0"; + suggestions: Suggestion[]; + apiChanges: Change[]; +} + +export const jsonOutput: JsonOutput = { + version: "1.0.0", + suggestions: [], + apiChanges: [], +}; diff --git a/eng/tools/typespec-migration-validation/src/log.ts b/eng/tools/typespec-migration-validation/src/log.ts index e90510451705..5b437a8ddf2a 100644 --- a/eng/tools/typespec-migration-validation/src/log.ts +++ b/eng/tools/typespec-migration-validation/src/log.ts @@ -13,7 +13,7 @@ const colors = { white: "\x1b[37m", success: "\x1b[32m✓\x1b[0m", // Green checkmark warning: "\x1b[33m⚠\x1b[0m", // Yellow warning - error: "\x1b[31m✗\x1b[0m" // Red X + error: "\x1b[31m✗\x1b[0m", // Red X }; // Helper functions for colored logging @@ -35,4 +35,4 @@ export function logError(message: string): void { export function logHeader(message: string): void { console.log(`\n${colors.cyan}${colors.bright}${message}${colors.reset}`); -} \ No newline at end of file +} diff --git a/eng/tools/typespec-migration-validation/src/parameter.ts b/eng/tools/typespec-migration-validation/src/parameter.ts index 6b06ab6478d5..624243fd334b 100644 --- a/eng/tools/typespec-migration-validation/src/parameter.ts +++ b/eng/tools/typespec-migration-validation/src/parameter.ts @@ -1,18 +1,31 @@ -const apiVersionAlias: string[] = [ - "api-version", - "apiVersion", - "apiVersionParameter", -]; +import { OpenAPI2Parameter } from "@azure-tools/typespec-autorest"; +import { getOriginalDocument } from "./document.js"; + +const apiVersionAlias: string[] = ["api-version", "apiVersion", "apiVersionParameter"]; export function isApiVersionParameter(obj: Record) { if (obj["$ref"] !== undefined) { - const commonTypePattern = /^\.\.\/\.\.\/\.\.\/\.\.\/\.\.\/common-types\/resource-management\/v[1-6]\/types.json#\/parameters\/ApiVersionParameter$/; + const commonTypePattern = + /^\.\.\/\.\.\/\.\.\/\.\.\/\.\.\/common-types\/resource-management\/v[1-6]\/types.json#\/parameters\/ApiVersionParameter$/; if (commonTypePattern.test(obj["$ref"])) return true; - if (apiVersionAlias.map(a => `#/parameters/${a}`.toLowerCase()).filter(a => (obj["$ref"] as string).toLowerCase().includes(a)).length > 0) return true; - } - else if (obj["name"] !== undefined) { - if (apiVersionAlias.map(a => a.toLowerCase()).includes(obj["name"].toLowerCase())) return true; + if ( + apiVersionAlias + .map((a) => `#/parameters/${a}`.toLowerCase()) + .filter((a) => (obj["$ref"] as string).toLowerCase().includes(a)).length > 0 + ) + return true; + + const originalParameter = getOriginalParameter(obj["$ref"]); + if ( + originalParameter?.name === "api-version" && + originalParameter?.in === "query" && + originalParameter?.required === true + ) + return true; + } else if (obj["name"] !== undefined) { + if (apiVersionAlias.map((a) => a.toLowerCase()).includes(obj["name"].toLowerCase())) + return true; } return false; @@ -26,13 +39,27 @@ const subscriptionIdAlias: string[] = [ export function isSubscriptionIdParameter(obj: Record) { if (obj["$ref"] !== undefined) { - const commonTypePattern = /^\.\.\/\.\.\/\.\.\/\.\.\/\.\.\/common-types\/resource-management\/v[1-6]\/types\.json#\/parameters\/SubscriptionIdParameter$/; + const commonTypePattern = + /^\.\.\/\.\.\/\.\.\/\.\.\/\.\.\/common-types\/resource-management\/v[1-6]\/types\.json#\/parameters\/SubscriptionIdParameter$/; if (commonTypePattern.test(obj["$ref"])) return true; - if (subscriptionIdAlias.map(a => `#/parameters/${a}`.toLowerCase()).filter(a => (obj["$ref"] as string).toLowerCase().includes(a)).length > 0) return true; - } - else if (obj["name"] !== undefined) { - if (subscriptionIdAlias.map(a => a.toLowerCase()).includes(obj["name"].toLowerCase())) return true; + if ( + subscriptionIdAlias + .map((a) => `#/parameters/${a}`.toLowerCase()) + .filter((a) => (obj["$ref"] as string).toLowerCase().includes(a)).length > 0 + ) + return true; + + const originalParameter = getOriginalParameter(obj["$ref"]); + if ( + originalParameter?.name === "subscriptionId" && + originalParameter?.in === "path" && + originalParameter?.required === true + ) + return true; + } else if (obj["name"] !== undefined) { + if (subscriptionIdAlias.map((a) => a.toLowerCase()).includes(obj["name"].toLowerCase())) + return true; } return false; @@ -49,15 +76,36 @@ const resourceGroupNameAlias: string[] = [ export function isResourceGroupNameParameter(obj: Record) { if (obj["$ref"] !== undefined) { - const commonTypePattern = /^\.\.\/\.\.\/\.\.\/\.\.\/\.\.\/common-types\/resource-management\/v[1-6]\/types\.json#\/parameters\/ResourceGroupNameParameter$/; + const commonTypePattern = + /^\.\.\/\.\.\/\.\.\/\.\.\/\.\.\/common-types\/resource-management\/v[1-6]\/types\.json#\/parameters\/ResourceGroupNameParameter$/; if (commonTypePattern.test(obj["$ref"])) return true; - if (resourceGroupNameAlias.map(a => `#/parameters/${a}`.toLowerCase()).filter(a => (obj["$ref"] as string).toLowerCase().includes(a)).length > 0) return true; - } - else if (obj["name"] !== undefined) { - if (resourceGroupNameAlias.map(a => a.toLowerCase()).includes(obj["name"].toLowerCase())) return true; + if ( + resourceGroupNameAlias + .map((a) => `#/parameters/${a}`.toLowerCase()) + .filter((a) => (obj["$ref"] as string).toLowerCase().includes(a)).length > 0 + ) + return true; + + const originalParameter = getOriginalParameter(obj["$ref"]); + if ( + originalParameter?.name === "resourceGroupName" && + originalParameter?.in === "path" && + originalParameter?.required === true + ) + return true; + } else if (obj["name"] !== undefined) { + if (resourceGroupNameAlias.map((a) => a.toLowerCase()).includes(obj["name"].toLowerCase())) + return true; } return false; } +export function getOriginalParameter(refPath: string): OpenAPI2Parameter | undefined { + const originalDocument = getOriginalDocument(); + if (refPath.startsWith("#/parameters/")) { + const parameterName = refPath.substring("#/parameters/".length); + return originalDocument?.parameters?.[parameterName]; + } else return undefined; +} diff --git a/eng/tools/typespec-migration-validation/src/summary.ts b/eng/tools/typespec-migration-validation/src/summary.ts index fa6e9ba70089..aafd9710b4f2 100644 --- a/eng/tools/typespec-migration-validation/src/summary.ts +++ b/eng/tools/typespec-migration-validation/src/summary.ts @@ -1,3 +1,5 @@ +import { jsonOutput } from "./jsonOutput.js"; + /** * Interface representing a value and the set of JSON paths where it appears */ @@ -13,7 +15,7 @@ export interface PathChangeInfo { /** The path that was changed */ path: string; /** The type of change (added or deleted) */ - changeType: 'added' | 'deleted'; + changeType: "added" | "deleted"; } /** @@ -35,33 +37,36 @@ export interface ModifiedValueInfo { * @param currentPath The current path in the JSON object (used for recursion) * @returns A Map with keys as result keys and values as sets of JSON paths */ -export function findDifferences(jsonObj: any, currentPath: string = ""): Map { +export function findDifferences( + jsonObj: any, + currentPath: string = "", +): Map { const results = new Map(); - + if (!jsonObj || typeof jsonObj !== "object") { return results; } - + // Process the current object for (const key in jsonObj) { if (!Object.prototype.hasOwnProperty.call(jsonObj, key)) { continue; } - + // Use bracket notation for keys with dots const newPath = constructJsonPath(currentPath, key); const value = jsonObj[key]; - - if (key.endsWith("__added") || key.endsWith("__deleted")) { + + if (key.endsWith("__added") || key.endsWith("__deleted")) { if (!results.has(key)) { results.set(key, { - paths: new Map() + paths: new Map(), }); } - + results.get(key)?.paths.set(newPath, value); } - + if (value !== null && typeof value === "object") { if (Array.isArray(value)) { // Handle arrays @@ -69,15 +74,15 @@ export function findDifferences(jsonObj: any, currentPath: string = ""): Map { if (!results.has(nestedKey)) { results.set(nestedKey, { - paths: new Map() + paths: new Map(), }); } - + ValueChangeInfo.paths.forEach((pathValue, path) => { results.get(nestedKey)?.paths.set(path, pathValue); }); @@ -87,15 +92,15 @@ export function findDifferences(jsonObj: any, currentPath: string = ""): Map { if (!results.has(nestedKey)) { results.set(nestedKey, { - paths: new Map() + paths: new Map(), }); } - + ValueChangeInfo.paths.forEach((pathValue, path) => { results.get(nestedKey)?.paths.set(path, pathValue); }); @@ -103,7 +108,7 @@ export function findDifferences(jsonObj: any, currentPath: string = ""): Map - a.path.toLowerCase().localeCompare(b.path.toLowerCase()) - ); + return results.sort((a, b) => a.path.toLowerCase().localeCompare(b.path.toLowerCase())); } /** @@ -186,48 +189,57 @@ export function formatDifferenceReport(keyPathsMap: Map if (keyPathsMap.size === 0) { return ""; } - + let report = "## Swagger Changes\n\n"; - + // Group by keys - const keyGroups: Map> = new Map(); - + const keyGroups: Map> = new Map(); + keyPathsMap.forEach((valueChangeInfo, key) => { - const baseKey = key.replace(/__added$|__deleted$/, ''); + const baseKey = key.replace(/__added$|__deleted$/, ""); const changeType = key.endsWith("__added") ? "added" : "deleted"; - + if (!keyGroups.has(baseKey)) { keyGroups.set(baseKey, new Map()); } - + valueChangeInfo.paths.forEach((value, path) => { keyGroups.get(baseKey)?.set(path, [changeType, value]); }); }); - + // Generate a table for each key keyGroups.forEach((pathsMap, key) => { report += `### Changes for \`${key}\`\n\n`; report += "| Path | Change Type | Value |\n"; report += "|------|------------|-------|\n"; - + // Sort paths alphabetically - const sortedPaths = Array.from(pathsMap.keys()).sort((a, b) => - a.toLowerCase().localeCompare(b.toLowerCase()) + const sortedPaths = Array.from(pathsMap.keys()).sort((a, b) => + a.toLowerCase().localeCompare(b.toLowerCase()), ); - - sortedPaths.forEach(path => { + + sortedPaths.forEach((path) => { const [changeType, value] = pathsMap.get(path)!; - const formattedValue = typeof value === "object" ? - JSON.stringify(value).substring(0, 100) + (JSON.stringify(value).length > 100 ? "..." : "") : - String(value); - + const formattedValue = + typeof value === "object" + ? JSON.stringify(value).substring(0, 100) + + (JSON.stringify(value).length > 100 ? "..." : "") + : String(value); + report += `| \`${path}\` | ${changeType} | \`${formattedValue.replace(/\\/g, "\\\\").replace(/\|/g, "\\|")}\` |\n`; + jsonOutput.apiChanges.push({ + category: "added-or-deleted", + key: key, + path: path, + type: changeType, + value: formattedValue, + }); }); - + report += "\n"; }); - + return report; } @@ -237,13 +249,18 @@ export function formatDifferenceReport(keyPathsMap: Map * @returns A formatted string with paths and their change types */ export function formatChangedPathsReport(changedPaths: PathChangeInfo[]): string { - let report = ""; + let report = ""; if (changedPaths.length === 0) return report; report += "## Changed Paths\n\n"; changedPaths.forEach(({ path, changeType }) => { report += `Path: ${path}\nChange Type: ${changeType}\n\n`; - }); + jsonOutput.apiChanges.push({ + category: "path-changed", + path: path, + type: changeType, + }); + }); return report; } @@ -254,32 +271,32 @@ export function formatChangedPathsReport(changedPaths: PathChangeInfo[]): string * @returns Array of ModifiedValueInfo objects containing paths and old/new values */ export function findModifiedValues(jsonObj: any, currentPath: string = ""): ModifiedValueInfo[] { - const oldValues = new Map(); - const newValues = new Map(); + const oldValues = new Map(); + const newValues = new Map(); const results: ModifiedValueInfo[] = []; - + if (!jsonObj || typeof jsonObj !== "object") { return results; } - + // Process the current level for (const key in jsonObj) { if (!Object.prototype.hasOwnProperty.call(jsonObj, key)) { continue; } - + const value = jsonObj[key]; const newPath = constructJsonPath(currentPath, key); - + // Check for __old and __new keys if (key.endsWith("__old")) { - const basePath = newPath.replace(/.__old$/, ''); + const basePath = newPath.replace(/.__old$/, ""); oldValues.set(basePath, { path: newPath, value }); } else if (key.endsWith("__new")) { - const basePath = newPath.replace(/.__new$/, ''); + const basePath = newPath.replace(/.__new$/, ""); newValues.set(basePath, { path: newPath, value }); } - + // Recursively process objects and arrays if (value !== null && typeof value === "object") { if (Array.isArray(value)) { @@ -294,7 +311,7 @@ export function findModifiedValues(jsonObj: any, currentPath: string = ""): Modi } } } - + // Match old and new value pairs for (const [basePath, oldInfo] of oldValues.entries()) { const newInfo = newValues.get(basePath); @@ -302,11 +319,11 @@ export function findModifiedValues(jsonObj: any, currentPath: string = ""): Modi results.push({ path: basePath, oldValue: oldInfo.value, - newValue: newInfo.value + newValue: newInfo.value, }); } } - + return results; } @@ -319,29 +336,36 @@ export function formatModifiedValuesReport(modifiedValues: ModifiedValueInfo[]): if (modifiedValues.length === 0) { return ""; } - + let report = "## Modified Values\n\n"; report += "| Path | Old Value | New Value |\n"; report += "|------|-----------|----------|\n"; - + modifiedValues.sort((a, b) => a.path.toLowerCase().localeCompare(b.path.toLowerCase())); - + modifiedValues.forEach(({ path, oldValue, newValue }) => { const formatValue = (value: any): string => { if (value === undefined) return "`undefined`"; if (value === null) return "`null`"; - - const valueStr = typeof value === "object" ? - JSON.stringify(value).substring(0, 100) + (JSON.stringify(value).length > 100 ? "..." : "") : - String(value); - + + const valueStr = + typeof value === "object" + ? JSON.stringify(value).substring(0, 100) + + (JSON.stringify(value).length > 100 ? "..." : "") + : String(value); + return `\`${valueStr.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/`/g, "\\`")}\``; }; - + report += `| \`${path}\` | ${formatValue(oldValue)} | ${formatValue(newValue)} |\n`; + jsonOutput.apiChanges.push({ + category: "value-changed", + path: path, + oldValue: formatValue(oldValue), + newValue: formatValue(newValue), + }); }); - + report += "\n"; return report; } - diff --git a/eng/tools/typespec-migration-validation/tsconfig.json b/eng/tools/typespec-migration-validation/tsconfig.json index 984c104c8dfd..512241b97047 100644 --- a/eng/tools/typespec-migration-validation/tsconfig.json +++ b/eng/tools/typespec-migration-validation/tsconfig.json @@ -3,14 +3,8 @@ "compilerOptions": { "outDir": "./dist", "rootDir": ".", - "allowJs": true + "allowJs": true, }, - "include": [ - "*.ts", - "src/**/*.ts", - "test/**/*.ts", - ], - "references": [ - { "path": "../suppressions" } - ] + "include": ["*.ts", "src/**/*.ts", "test/**/*.ts"], + "references": [{ "path": "../suppressions" }], } diff --git a/eng/tools/typespec-requirement/package.json b/eng/tools/typespec-requirement/package.json index 32a71505a6cd..31905ba7ada5 100644 --- a/eng/tools/typespec-requirement/package.json +++ b/eng/tools/typespec-requirement/package.json @@ -5,11 +5,16 @@ "devDependencies": { "@types/node": "^20.0.0", "execa": "^9.3.0", + "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "typescript": "~5.8.2", "vitest": "^3.0.7" }, "scripts": { "build": "tsc --build", + "format": "prettier . --ignore-path ../.prettierignore --write", + "format:check": "prettier . --ignore-path ../.prettierignore --check", + "format:check:ci": "prettier . --ignore-path ../.prettierignore --check --log-level debug", "test": "vitest", "test:ci": "vitest run --reporter=verbose" }, diff --git a/eng/tools/typespec-requirement/tsconfig.json b/eng/tools/typespec-requirement/tsconfig.json index f3b416dfce52..1c9d0b24bed9 100644 --- a/eng/tools/typespec-requirement/tsconfig.json +++ b/eng/tools/typespec-requirement/tsconfig.json @@ -4,9 +4,5 @@ "outDir": "./dist", "rootDir": ".", }, - "include": [ - "*.ts", - "src/**/*.ts", - "test/**/*.ts", - ], + "include": ["*.ts", "src/**/*.ts", "test/**/*.ts"], } diff --git a/eng/tools/typespec-requirement/vite.config.ts b/eng/tools/typespec-requirement/vite.config.ts index 8b6908dc0a16..346b4e424284 100644 --- a/eng/tools/typespec-requirement/vite.config.ts +++ b/eng/tools/typespec-requirement/vite.config.ts @@ -1,8 +1,8 @@ -import { defineConfig } from 'vite' +import { defineConfig } from "vite"; export default defineConfig({ test: { // Default timeout of 5 seconds is too low - testTimeout: 20000 - } -}) + testTimeout: 20000, + }, +}); diff --git a/eng/tools/typespec-validation/cmd/tsv.js b/eng/tools/typespec-validation/cmd/tsv.js index 2a7c28a5d607..e2a37e0b5491 100755 --- a/eng/tools/typespec-validation/cmd/tsv.js +++ b/eng/tools/typespec-validation/cmd/tsv.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { main } from "../dist/src/index.js" +import { main } from "../dist/src/index.js"; await main(); diff --git a/eng/tools/typespec-validation/package.json b/eng/tools/typespec-validation/package.json index b907eea39531..0f8d06aa4862 100644 --- a/eng/tools/typespec-validation/package.json +++ b/eng/tools/typespec-validation/package.json @@ -20,11 +20,16 @@ "@types/debug": "^4.1.12", "@types/node": "^20.0.0", "@vitest/coverage-v8": "^3.0.7", + "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "typescript": "~5.8.2", "vitest": "^3.0.7" }, "scripts": { "build": "tsc --build", + "format": "prettier . --ignore-path ../.prettierignore --write", + "format:check": "prettier . --ignore-path ../.prettierignore --check", + "format:check:ci": "prettier . --ignore-path ../.prettierignore --check --log-level debug", "test": "vitest", "test:ci": "vitest run --coverage --reporter=verbose" }, diff --git a/eng/tools/typespec-validation/src/index.ts b/eng/tools/typespec-validation/src/index.ts index b05d49175a73..ce5e738a93f1 100755 --- a/eng/tools/typespec-validation/src/index.ts +++ b/eng/tools/typespec-validation/src/index.ts @@ -1,5 +1,5 @@ -import { ParseArgsConfig, parseArgs } from "node:util"; import { stat } from "node:fs/promises"; +import { ParseArgsConfig, parseArgs } from "node:util"; import { Suppression } from "suppressions"; import { CompileRule } from "./rules/compile.js"; import { EmitAutorestRule } from "./rules/emit-autorest.js"; @@ -11,6 +11,9 @@ import { NpmPrefixRule } from "./rules/npm-prefix.js"; import { SdkTspConfigValidationRule } from "./rules/sdk-tspconfig-validation.js"; import { fileExists, getSuppressions, normalizePath } from "./utils.js"; +// Context argument may add new properties or override checkingAllSpecs +export var context: Record = { checkingAllSpecs: false }; + export async function main() { const args = process.argv.slice(2); const options = { @@ -18,9 +21,18 @@ export async function main() { type: "string", short: "f", }, + context: { + type: "string", + short: "c", + }, }; const parsedArgs = parseArgs({ args, options, allowPositionals: true } as ParseArgsConfig); const folder = parsedArgs.positionals[0]; + + if (parsedArgs.positionals[1]) { + context = { ...context, ...JSON.parse(parsedArgs.positionals[1]) }; + } + const absolutePath = normalizePath(folder); if (!(await fileExists(absolutePath))) { diff --git a/eng/tools/typespec-validation/src/rules/flavor-azure.ts b/eng/tools/typespec-validation/src/rules/flavor-azure.ts index 30c77655f964..ac6d1b76254a 100644 --- a/eng/tools/typespec-validation/src/rules/flavor-azure.ts +++ b/eng/tools/typespec-validation/src/rules/flavor-azure.ts @@ -18,7 +18,7 @@ export class FlavorAzureRule implements Rule { const options = config?.options; for (const emitter in options) { - if (this.isClientEmitter(emitter)) { + if (this.requiresAzureFlavor(emitter)) { const flavor = options[emitter]?.flavor; stdOutput += `"${emitter}":\n`; @@ -43,7 +43,12 @@ export class FlavorAzureRule implements Rule { }; } - isClientEmitter(name: string): boolean { + requiresAzureFlavor(name: string): boolean { + if (name === "@typespec/http-client-csharp") { + // C# emitters do not require flavor:azure. Instead, there + // is a separate emitter for Azure - @azure-typespec/http-client-csharp + return false; + } const regex = new RegExp( "^(@azure-tools/typespec-(csharp|java|python|ts)|@typespec/http-client-.+)$", ); diff --git a/eng/tools/typespec-validation/src/rules/folder-structure.ts b/eng/tools/typespec-validation/src/rules/folder-structure.ts index b53277aaab09..5a777e6db198 100644 --- a/eng/tools/typespec-validation/src/rules/folder-structure.ts +++ b/eng/tools/typespec-validation/src/rules/folder-structure.ts @@ -21,6 +21,11 @@ export class FolderStructureRule implements Rule { const gitRoot = normalizePath(await simpleGit(folder).revparse("--show-toplevel")); const relativePath = path.relative(gitRoot, folder).split(path.sep).join("/"); + // If the folder containing TypeSpec sources is under "data-plane" or "resource-manager", the spec + // must be using "folder structure v2". Otherwise, it must be using v1. + const structureVersion = + relativePath.includes("data-plane") || relativePath.includes("resource-manager") ? 2 : 1; + stdOutput += `folder: ${folder}\n`; if (!(await fileExists(folder))) { return { @@ -39,35 +44,6 @@ export class FolderStructureRule implements Rule { } }); - // Verify top level folder is lower case and remove empty entries when splitting by slash - const folderStruct = relativePath.split("/").filter(Boolean); - if (folderStruct[1].match(/[A-Z]/g)) { - success = false; - errorOutput += `Invalid folder name. Folders under specification/ must be lower case.\n`; - } - - const packageFolder = folderStruct[folderStruct.length - 1]; - - // Verify package folder is at most 3 levels deep - if (folderStruct.length > 4) { - success = false; - errorOutput += `Please limit TypeSpec folder depth to 3 levels or less`; - } - - // Verify second level folder is capitalized after each '.' - if (/(^|\. *)([a-z])/g.test(packageFolder)) { - success = false; - errorOutput += `Invalid folder name. Folders under specification/${folderStruct[1]} must be capitalized after each '.'\n`; - } - - // Verify 'Shared' follows 'Management' - if (packageFolder.includes("Management") && packageFolder.includes("Shared")) { - if (!packageFolder.includes("Management.Shared")) { - success = false; - errorOutput += `Invalid folder name. For management libraries with a shared component, 'Shared' should follow 'Management'.`; - } - } - // Verify tspconfig, main.tsp, examples/ const mainExists = await fileExists(path.join(folder, "main.tsp")); const clientExists = await fileExists(path.join(folder, "client.tsp")); @@ -83,45 +59,114 @@ export class FolderStructureRule implements Rule { success = false; } - if (!packageFolder.includes("Shared") && !tspConfigExists) { - errorOutput += `Invalid folder structure: Spec folder must contain tspconfig.yaml.`; + const folderStruct = relativePath.split("/").filter(Boolean); + + // Verify top level folder is lower case and remove empty entries when splitting by slash + if (folderStruct[1].match(/[A-Z]/g)) { success = false; + errorOutput += `Invalid folder name. Folders under specification/ must be lower case.\n`; } - if (tspConfigExists) { - const configText = await readTspConfig(folder); - const config = yamlParse(configText); - const rpFolder = - config?.options?.["@azure-tools/typespec-autorest"]?.["azure-resource-provider-folder"]; - stdOutput += `azure-resource-provider-folder: ${JSON.stringify(rpFolder)}\n`; - - if ( - rpFolder?.trim()?.endsWith("resource-manager") && - !packageFolder.endsWith(".Management") - ) { - errorOutput += `Invalid folder structure: TypeSpec for resource-manager specs must be in a folder ending with '.Management'`; + if (structureVersion === 1) { + const packageFolder = folderStruct[folderStruct.length - 1]; + + if (!packageFolder.includes("Shared") && !tspConfigExists) { + errorOutput += `Invalid folder structure: Spec folder must contain tspconfig.yaml.`; success = false; - } else if ( - !rpFolder?.trim()?.endsWith("resource-manager") && - packageFolder.endsWith(".Management") - ) { - errorOutput += `Invalid folder structure: TypeSpec for data-plane specs or shared code must be in a folder NOT ending with '.Management'`; + } + + // Verify package folder is at most 3 levels deep + if (folderStruct.length > 4) { + success = false; + errorOutput += `Please limit TypeSpec folder depth to 3 levels or less`; + } + + // Verify second level folder is capitalized after each '.' + if (/(^|\. *)([a-z])/g.test(packageFolder)) { + success = false; + errorOutput += `Invalid folder name. Folders under specification/${folderStruct[1]} must be capitalized after each '.'\n`; + } + + // Verify 'Shared' follows 'Management' + if (packageFolder.includes("Management") && packageFolder.includes("Shared")) { + if (!packageFolder.includes("Management.Shared")) { + success = false; + errorOutput += `Invalid folder name. For management libraries with a shared component, 'Shared' should follow 'Management'.`; + } + } + + if (tspConfigExists) { + const configText = await readTspConfig(folder); + const config = yamlParse(configText); + const rpFolder = + config?.options?.["@azure-tools/typespec-autorest"]?.["azure-resource-provider-folder"]; + stdOutput += `azure-resource-provider-folder: ${JSON.stringify(rpFolder)}\n`; + + if ( + rpFolder?.trim()?.endsWith("resource-manager") && + !packageFolder.endsWith(".Management") + ) { + errorOutput += `Invalid folder structure: TypeSpec for resource-manager specs must be in a folder ending with '.Management'`; + success = false; + } else if ( + !rpFolder?.trim()?.endsWith("resource-manager") && + packageFolder.endsWith(".Management") + ) { + errorOutput += `Invalid folder structure: TypeSpec for data-plane specs or shared code must be in a folder NOT ending with '.Management'`; + success = false; + } + } + } else if (structureVersion === 2) { + if (!tspConfigExists) { + errorOutput += `Invalid folder structure: Spec folder must contain tspconfig.yaml.`; + success = false; + } + + const specType = folder.includes("data-plane") ? "data-plane" : "resource-manager"; + if (specType === "data-plane") { + if (folderStruct.length !== 4) { + errorOutput += + "Invalid folder structure: TypeSpec for data-plane specs must be in a folder exactly one level under 'data-plane', like 'specification/foo/data-plane/FooAnalytics'."; + success = false; + } + } else if (specType === "resource-manager") { + if (folderStruct.length !== 5) { + errorOutput += + "Invalid folder structure: TypeSpec for resource-manager specs must be in a folder exactly two levels under 'resource-manager', like 'specification/foo/resource-management/Microsoft.Foo/Foo'."; + success = false; + } + + const rpNamespaceRegex = /^[A-Za-z0-9\.]+$/; + const rpNamespaceFolder = folderStruct[folderStruct.length - 2]; + + if (!rpNamespaceRegex.test(rpNamespaceFolder)) { + success = false; + errorOutput += `RPNamespace folder '${rpNamespaceFolder}' does not match regex ${rpNamespaceRegex}`; + } + } + + const serviceRegex = /^[A-Za-z0-9]+$/; + const serviceFolder = folderStruct[folderStruct.length - 1]; + + if (!serviceRegex.test(serviceFolder)) { success = false; + errorOutput += `Service folder '${serviceFolder}' does not match regex ${serviceRegex}`; } } // Ensure specs only import files from same folder under "specification" stdOutput += "imports:\n"; - const teamFolder = path.join(...folderStruct.slice(0, 2)); - stdOutput += ` ${teamFolder}\n`; + const allowedImportRoot = + structureVersion === 1 ? path.join(...folderStruct.slice(0, 2)) : folder; + stdOutput += ` ${allowedImportRoot}\n`; - const teamFolderResolved = path.resolve(gitRoot, teamFolder); + const allowedImportRootResolved = path.resolve(gitRoot, allowedImportRoot); - const tsps = await globby("**/*.tsp", { cwd: teamFolderResolved }); + const tsps = await globby("**/*.tsp", { cwd: allowedImportRootResolved }); for (const tsp of tsps) { - const tspResolved = path.resolve(teamFolderResolved, tsp); + const tspResolved = path.resolve(allowedImportRootResolved, tsp); const pattern = /^\s*import\s+['"]([^'"]+)['"]\s*;\s*$/gm; const text = await readFile(tspResolved, { encoding: "utf8" }); @@ -144,12 +189,12 @@ export class FolderStructureRule implements Rule { for (const fileImport of fileImports) { const fileImportResolved = path.resolve(path.dirname(tspResolved), fileImport); - const relative = path.relative(teamFolderResolved, fileImportResolved); + const relative = path.relative(allowedImportRootResolved, fileImportResolved); if (relative.startsWith("..")) { errorOutput += `Invalid folder structure: '${tsp}' imports '${fileImport}', ` + - `which is outside '${path.relative(gitRoot, teamFolder)}'`; + `which is outside '${path.relative(gitRoot, allowedImportRoot)}'`; success = false; } } diff --git a/eng/tools/typespec-validation/src/rules/sdk-tspconfig-validation.ts b/eng/tools/typespec-validation/src/rules/sdk-tspconfig-validation.ts index 85e6c574d635..40961fcc6397 100644 --- a/eng/tools/typespec-validation/src/rules/sdk-tspconfig-validation.ts +++ b/eng/tools/typespec-validation/src/rules/sdk-tspconfig-validation.ts @@ -1,8 +1,8 @@ import { join } from "path"; +import { Suppression } from "suppressions"; import { parse as yamlParse } from "yaml"; -import { Rule } from "../rule.js"; import { RuleResult } from "../rule-result.js"; -import { Suppression } from "suppressions"; +import { Rule } from "../rule.js"; import { fileExists, getSuppressions, readTspConfig } from "../utils.js"; type ExpectedValueType = string | boolean | RegExp; @@ -85,8 +85,8 @@ class TspconfigParameterSubRuleBase extends TspconfigSubRuleBase { const parameter = config?.parameters?.[this.keyToValidate]?.default; if (parameter === undefined) return this.createFailedResult( - `Failed to find "parameters.${this.keyToValidate}.default"`, - `Please add "parameters.${this.keyToValidate}.default"`, + `Failed to find "parameters.${this.keyToValidate}.default" with expected value "${this.expectedValue}"`, + `Please add "parameters.${this.keyToValidate}.default" with expected value "${this.expectedValue}".`, ); if (!this.validateValue(parameter, this.expectedValue)) @@ -129,8 +129,8 @@ class TspconfigEmitterOptionsSubRuleBase extends TspconfigSubRuleBase { const option = this.tryFindOption(config); if (option === undefined) return this.createFailedResult( - `Failed to find "options.${this.emitterName}.${this.keyToValidate}"`, - `Please add "options.${this.emitterName}.${this.keyToValidate}"`, + `Failed to find "options.${this.emitterName}.${this.keyToValidate}" with expected value "${this.expectedValue}"`, + `Please add "options.${this.emitterName}.${this.keyToValidate}" with expected value "${this.expectedValue}"`, ); const actualValue = option as unknown as undefined | string | boolean; @@ -149,7 +149,7 @@ class TspconfigEmitterOptionsSubRuleBase extends TspconfigSubRuleBase { } function isManagementSdk(folder: string): boolean { - return folder.includes(".Management"); + return folder.includes("/resource-manager/") || folder.includes(".Management"); } function skipForDataPlane(folder: string): SkipResult { @@ -225,7 +225,7 @@ export class TspConfigJavaMgmtPackageDirFormatSubRule extends TspconfigEmitterOp super( "@azure-tools/typespec-java", "package-dir", - new RegExp(/^azure-resourcemanager-[^\/]+$/) // Matches "azure-resourcemanager-" with no restriction on characters after the hyphen + new RegExp(/^azure-resourcemanager-[^\/]+$/), // Matches "azure-resourcemanager-" with no restriction on characters after the hyphen ); } @@ -239,7 +239,7 @@ export class TspConfigJavaMgmtNamespaceFormatSubRule extends TspconfigEmitterOpt super( "@azure-tools/typespec-java", "namespace", - new RegExp(/^com\.azure\.resourcemanager\.[^\.]+$/) // Matches "com.azure.resourcemanager." with no restriction on characters after the last dot + new RegExp(/^com\.azure\.resourcemanager(\.[a-z0-9_]+)+$/), // Matches "com.azure.resourcemanager." allowing a-z, 0-9, and _ in each segment ); } @@ -260,15 +260,11 @@ export class TspConfigTsMgmtModularExperimentalExtensibleEnumsTrueSubRule extend export class TspConfigTsMgmtModularPackageDirectorySubRule extends TspconfigEmitterOptionsSubRuleBase { constructor() { - super( - "@azure-tools/typespec-ts", - "package-dir", - new RegExp(/^arm-[^\/]+$/) - ); + super("@azure-tools/typespec-ts", "package-dir", new RegExp(/^arm-[^\/]+$/)); } protected skip(config: any, folder: string) { - return skipForNonModularOrDataPlaneInTsEmitter(config, folder); + return skipForNonModularOrDataPlaneInTsEmitter(config, folder); } } @@ -441,35 +437,40 @@ export class TspConfigPythonMgmtPackageDirectorySubRule extends TspconfigEmitter } } -export class TspConfigPythonMgmtNamespaceSubRule extends TspconfigEmitterOptionsSubRuleBase { +export class TspConfigPythonMgmtPackageGenerateTestTrueSubRule extends TspconfigEmitterOptionsSubRuleBase { constructor() { - super("@azure-tools/typespec-python", "namespace", new RegExp(/^azure\.mgmt(\.[a-z]+){1,2}$/)); + super("@azure-tools/typespec-python", "generate-test", true); } protected skip(_: any, folder: string) { return skipForDataPlane(folder); } } -// ----- Python data plane sub rules ----- -export class TspConfigPythonDpPackageDirectorySubRule extends TspconfigEmitterOptionsSubRuleBase { +export class TspConfigPythonMgmtPackageGenerateSampleTrueSubRule extends TspconfigEmitterOptionsSubRuleBase { constructor() { - super("@azure-tools/typespec-python", "package-dir", new RegExp(/^azure(-[a-z]+){1,3}$/)); + super("@azure-tools/typespec-python", "generate-sample", true); } protected skip(_: any, folder: string) { - return skipForManagementPlane(folder); + return skipForDataPlane(folder); } } -// ----- Python azure sub rules ----- -export class TspConfigPythonAzGenerateTestTrueSubRule extends TspconfigEmitterOptionsSubRuleBase { +export class TspConfigPythonMgmtNamespaceSubRule extends TspconfigEmitterOptionsSubRuleBase { constructor() { - super("@azure-tools/typespec-python", "generate-test", true); + super("@azure-tools/typespec-python", "namespace", new RegExp(/^azure\.mgmt(\.[a-z]+){1,2}$/)); + } + protected skip(_: any, folder: string) { + return skipForDataPlane(folder); } } -export class TspConfigPythonAzGenerateSampleTrueSubRule extends TspconfigEmitterOptionsSubRuleBase { +// ----- Python data plane sub rules ----- +export class TspConfigPythonDpPackageDirectorySubRule extends TspconfigEmitterOptionsSubRuleBase { constructor() { - super("@azure-tools/typespec-python", "generate-sample", true); + super("@azure-tools/typespec-python", "package-dir", new RegExp(/^azure(-[a-z]+){1,3}$/)); + } + protected skip(_: any, folder: string) { + return skipForManagementPlane(folder); } } @@ -489,8 +490,8 @@ export class TspConfigCsharpAzNamespaceEqualStringSubRule extends TspconfigEmitt if (option === undefined) return this.createFailedResult( - `Failed to find "options.${this.emitterName}.${this.keyToValidate}"`, - `Please add "options.${this.emitterName}.${this.keyToValidate}"`, + `Failed to find "options.${this.emitterName}.${this.keyToValidate}" with expected value "${this.expectedValue}"`, + `Please add "options.${this.emitterName}.${this.keyToValidate}" with expected value "${this.expectedValue}".`, ); const packageDir = config?.options?.[this.emitterName]?.["package-dir"]; @@ -549,8 +550,8 @@ export const defaultRules = [ new TspConfigPythonMgmtPackageDirectorySubRule(), new TspConfigPythonMgmtNamespaceSubRule(), new TspConfigPythonDpPackageDirectorySubRule(), - new TspConfigPythonAzGenerateTestTrueSubRule(), - new TspConfigPythonAzGenerateSampleTrueSubRule(), + new TspConfigPythonMgmtPackageGenerateSampleTrueSubRule(), + new TspConfigPythonMgmtPackageGenerateTestTrueSubRule(), new TspConfigCsharpAzPackageDirectorySubRule(), new TspConfigCsharpAzNamespaceEqualStringSubRule(), new TspConfigCsharpAzClearOutputFolderTrueSubRule(), @@ -582,8 +583,8 @@ export class SdkTspConfigValidationRule implements Rule { const failedResults = []; let success = true; for (const subRule of this.subRules) { - // TODO: support wildcard - if (this.suppressedKeyPaths.has(subRule.getPathOfKeyToValidate())) continue; + // Check for both direct matches and wildcard patterns + if (this.isKeyPathSuppressed(subRule.getPathOfKeyToValidate())) continue; const result = await subRule.execute(folder!); if (!result.success) failedResults.push(result); @@ -594,7 +595,9 @@ export class SdkTspConfigValidationRule implements Rule { const emitterOptionSubRule = subRule as TspconfigEmitterOptionsSubRuleBase; const emitterName = emitterOptionSubRule.getEmitterName(); if (emitterName === "@azure-tools/typespec-csharp" && isSubRuleSuccess === false) { - console.warn(`Validation on option "${emitterOptionSubRule.getPathOfKeyToValidate()}" in "${emitterName}" are failed. However, per ${emitterName}’s decision, we will treat it as passed.`); + console.warn( + `Validation on option "${emitterOptionSubRule.getPathOfKeyToValidate()}" in "${emitterName}" are failed. However, per ${emitterName}’s decision, we will treat it as passed.`, + ); isSubRuleSuccess = true; } } @@ -604,12 +607,11 @@ export class SdkTspConfigValidationRule implements Rule { const stdOutputFailedResults = failedResults.length > 0 - ? `${failedResults.map((r) => r.errorOutput).join("\n")}\nPlease see https://aka.ms/azsdk/spec-gen-sdk-config for more info.\nFor additional information on TypeSpec validation, please refer to https://aka.ms/azsdk/specs/typespec-validation.` + ? `${failedResults.map((r) => r.errorOutput).join("\n")}\nPlease see https://aka.ms/azsdk/spec-gen-sdk-config for more info.\nFor additional information on TypeSpec validation, please refer to https://aka.ms/azsdk/specs/typespec-validation` : ""; - // NOTE: to avoid huge impact on existing PRs, we always return true with info/warning messages. return { - success: true, + success, stdOutput: `[${this.name}]: validation ${success ? "passed" : "failed"}.\n${stdOutputFailedResults}`, }; } @@ -624,4 +626,23 @@ export class SdkTspConfigValidationRule implements Rule { } } } + + private isKeyPathSuppressed(keyPath: string): boolean { + // Direct match + if (this.suppressedKeyPaths.has(keyPath)) { + return true; + } + + // Only check for wildcard at the end (format: prefix.*) + for (const suppressedPath of this.suppressedKeyPaths) { + if (suppressedPath.endsWith(".*")) { + const prefix = suppressedPath.slice(0, -2); // Remove the '.*' at the end + if (keyPath.startsWith(prefix)) { + return true; + } + } + } + + return false; + } } diff --git a/eng/tools/typespec-validation/src/utils.ts b/eng/tools/typespec-validation/src/utils.ts index 97c59c46c782..9dd8df6ea216 100644 --- a/eng/tools/typespec-validation/src/utils.ts +++ b/eng/tools/typespec-validation/src/utils.ts @@ -5,6 +5,7 @@ import { access, readFile } from "fs/promises"; import defaultPath, { join, PlatformPath } from "path"; import { simpleGit } from "simple-git"; import { getSuppressions as getSuppressionsImpl, Suppression } from "suppressions"; +import { context } from "./index.js"; // Enable simple-git debug logging to improve console output debug.enable("simple-git"); @@ -41,7 +42,7 @@ export async function readTspConfig(folder: string) { } export async function getSuppressions(path: string): Promise { - return getSuppressionsImpl("TypeSpecValidation", path); + return getSuppressionsImpl("TypeSpecValidation", path, context); } export function normalizePath(folder: string, path: PlatformPath = defaultPath) { diff --git a/eng/tools/typespec-validation/test/folder-structure.test.ts b/eng/tools/typespec-validation/test/folder-structure.test.ts index e42b8bcd157a..bcda6f765fc9 100644 --- a/eng/tools/typespec-validation/test/folder-structure.test.ts +++ b/eng/tools/typespec-validation/test/folder-structure.test.ts @@ -24,6 +24,14 @@ describe("folder-structure", function () { vi.clearAllMocks(); }); + it("should fail if folder doesn't exist", async function () { + fileExistsSpy.mockResolvedValue(false); + + const result = await new FolderStructureRule().execute(mockFolder); + assert(result.errorOutput); + assert(result.errorOutput.includes("does not exist")); + }); + it("should fail if tspconfig has incorrect extension", async function () { vi.mocked(globby.globby).mockImplementation(async () => { return ["/foo/bar/tspconfig.yml"]; @@ -87,7 +95,7 @@ describe("folder-structure", function () { const result = await new FolderStructureRule().execute("/gitroot/specification/foo/data-plane"); assert(result.errorOutput); - assert(result.errorOutput.includes("must be capitalized")); + assert(result.errorOutput.includes("does not match regex")); }); it("should fail if second level folder is resource-manager", async function () { @@ -100,7 +108,7 @@ describe("folder-structure", function () { "/gitroot/specification/foo/resource-manager", ); assert(result.errorOutput); - assert(result.errorOutput.includes("must be capitalized")); + assert(result.errorOutput.includes("does not match regex")); }); it("should fail if Shared does not follow Management ", async function () { @@ -260,4 +268,78 @@ options: assert(result.errorOutput); assert(result.errorOutput.includes(".Management")); }); + + it("v2: should fail if no tspconfig.yaml", async function () { + vi.mocked(globby.globby).mockImplementation(async () => { + return ["main.tsp"]; + }); + normalizePathSpy.mockReturnValue("/gitroot"); + + fileExistsSpy.mockImplementation(async (file: string) => { + if (file.includes("tspconfig.yaml")) { + return false; + } + return true; + }); + + const result = await new FolderStructureRule().execute( + "/gitroot/specification/foo/data-plane/Foo", + ); + + assert(result.errorOutput?.includes("must contain")); + }); + + it("v2: should fail if incorrect folder depth", async function () { + vi.mocked(globby.globby).mockImplementation(async () => { + return ["tspconfig.yaml"]; + }); + normalizePathSpy.mockReturnValue("/gitroot"); + + let result = await new FolderStructureRule().execute("/gitroot/specification/foo/data-plane"); + assert(result.errorOutput?.includes("level under")); + + result = await new FolderStructureRule().execute( + "/gitroot/specification/foo/data-plane/Foo/too-deep", + ); + assert(result.errorOutput?.includes("level under")); + + result = await new FolderStructureRule().execute("/gitroot/specification/foo/resource-manager"); + assert(result.errorOutput?.includes("levels under")); + + result = await new FolderStructureRule().execute( + "/gitroot/specification/foo/resource-manager/RP.Namespace", + ); + assert(result.errorOutput?.includes("levels under")); + + result = await new FolderStructureRule().execute( + "/gitroot/specification/foo/resource-manager/RP.Namespace/FooManagement/too-deep", + ); + assert(result.errorOutput?.includes("levels under")); + }); + + it("v2: should succeed with data-plane", async function () { + vi.mocked(globby.globby).mockImplementation(async (patterns) => { + return patterns[0].includes("tspconfig") ? ["tspconfig.yaml"] : ["main.tsp"]; + }); + normalizePathSpy.mockReturnValue("/gitroot"); + + const result = await new FolderStructureRule().execute( + "/gitroot/specification/foo/data-plane/Foo", + ); + + assert(result.success); + }); + + it("v2: should succeed with resource-manager", async function () { + vi.mocked(globby.globby).mockImplementation(async (patterns) => { + return patterns[0].includes("tspconfig") ? ["tspconfig.yaml"] : ["main.tsp"]; + }); + normalizePathSpy.mockReturnValue("/gitroot"); + + const result = await new FolderStructureRule().execute( + "/gitroot/specification/foo/resource-manager/Microsoft.Foo/FooManagement", + ); + + assert(result.success); + }); }); diff --git a/eng/tools/typespec-validation/test/sdk-tspconfig-validation.test.ts b/eng/tools/typespec-validation/test/sdk-tspconfig-validation.test.ts index 6ec0f02e3df9..cff5a059c4fd 100644 --- a/eng/tools/typespec-validation/test/sdk-tspconfig-validation.test.ts +++ b/eng/tools/typespec-validation/test/sdk-tspconfig-validation.test.ts @@ -1,41 +1,42 @@ import { afterEach, beforeEach, describe, it, MockInstance, vi } from "vitest"; +import { contosoTspConfig } from "@azure-tools/specs-shared/test/examples"; +import { strictEqual } from "node:assert"; +import { join } from "path"; +import { stringify } from "yaml"; import { SdkTspConfigValidationRule, TspConfigCommonAzServiceDirMatchPatternSubRule, - TspConfigTsMgmtModularExperimentalExtensibleEnumsTrueSubRule, - TspConfigTsMgmtModularPackageDirectorySubRule, - TspConfigTsMgmtModularPackageNameMatchPatternSubRule, - TspConfigTsDpPackageDirectorySubRule, - TspConfigTsRlcDpPackageNameMatchPatternSubRule, - TspConfigGoMgmtServiceDirMatchPatternSubRule, - TspConfigGoMgmtPackageDirectorySubRule, - TspConfigGoMgmtModuleEqualStringSubRule, - TspConfigGoMgmtFixConstStutteringTrueSubRule, - TspConfigGoMgmtGenerateSamplesTrueSubRule, - TspConfigGoMgmtHeadAsBooleanTrueSubRule, + TspConfigCsharpAzClearOutputFolderTrueSubRule, + TspConfigCsharpAzNamespaceEqualStringSubRule, + TspConfigCsharpAzPackageDirectorySubRule, + TspConfigCsharpMgmtPackageDirectorySubRule, TspConfigGoAzGenerateFakesTrueSubRule, TspConfigGoAzInjectSpansTrueSubRule, TspConfigGoDpModuleMatchPatternSubRule, TspConfigGoDpPackageDirectoryMatchPatternSubRule, TspConfigGoDpServiceDirMatchPatternSubRule, + TspConfigGoMgmtFixConstStutteringTrueSubRule, + TspConfigGoMgmtGenerateSamplesTrueSubRule, + TspConfigGoMgmtHeadAsBooleanTrueSubRule, + TspConfigGoMgmtModuleEqualStringSubRule, + TspConfigGoMgmtPackageDirectorySubRule, + TspConfigGoMgmtServiceDirMatchPatternSubRule, TspConfigJavaAzPackageDirectorySubRule, - TspConfigPythonMgmtPackageDirectorySubRule, + TspConfigJavaMgmtNamespaceFormatSubRule, + TspConfigPythonDpPackageDirectorySubRule, TspConfigPythonMgmtNamespaceSubRule, - TspConfigPythonAzGenerateTestTrueSubRule, - TspConfigPythonAzGenerateSampleTrueSubRule, - TspConfigCsharpAzPackageDirectorySubRule, - TspConfigCsharpAzNamespaceEqualStringSubRule, - TspConfigCsharpAzClearOutputFolderTrueSubRule, - TspConfigCsharpMgmtPackageDirectorySubRule, + TspConfigPythonMgmtPackageDirectorySubRule, + TspConfigPythonMgmtPackageGenerateSampleTrueSubRule, + TspConfigPythonMgmtPackageGenerateTestTrueSubRule, TspconfigSubRuleBase, - TspConfigPythonDpPackageDirectorySubRule, + TspConfigTsDpPackageDirectorySubRule, + TspConfigTsMgmtModularExperimentalExtensibleEnumsTrueSubRule, + TspConfigTsMgmtModularPackageDirectorySubRule, + TspConfigTsMgmtModularPackageNameMatchPatternSubRule, TspConfigTsMlcDpPackageNameMatchPatternSubRule, + TspConfigTsRlcDpPackageNameMatchPatternSubRule, } from "../src/rules/sdk-tspconfig-validation.js"; -import { contosoTspConfig } from "@azure-tools/specs-shared/test/examples"; -import { join } from "path"; -import { strictEqual } from "node:assert"; -import { stringify } from "yaml"; import * as utils from "../src/utils.js"; @@ -386,6 +387,79 @@ const javaManagementPackageDirTestCases = createEmitterOptionTestCases( [new TspConfigJavaAzPackageDirectorySubRule()], ); +const javaMgmtNamespaceTestCases = createEmitterOptionTestCases( + "@azure-tools/typespec-java", + managementTspconfigFolder, + "namespace", + "com.azure.resourcemanager.compute", + "com.azure.compute", // Invalid: missing "resourcemanager" + [new TspConfigJavaMgmtNamespaceFormatSubRule()], +); + +const javaMgmtNamespaceExtendedTestCases: Case[] = [ + { + description: "Validate Java management namespace with numbers", + folder: managementTspconfigFolder, + tspconfigContent: createEmitterOptionExample("@azure-tools/typespec-java", { + key: "namespace", + value: "com.azure.resourcemanager.storage2024", + }), + success: true, + subRules: [new TspConfigJavaMgmtNamespaceFormatSubRule()], + }, + { + description: "Validate Java management namespace with underscores", + folder: managementTspconfigFolder, + tspconfigContent: createEmitterOptionExample("@azure-tools/typespec-java", { + key: "namespace", + value: "com.azure.resourcemanager.storage_v2", + }), + success: true, + subRules: [new TspConfigJavaMgmtNamespaceFormatSubRule()], + }, + { + description: "Validate Java management namespace with 5 segments", + folder: managementTspconfigFolder, + tspconfigContent: createEmitterOptionExample("@azure-tools/typespec-java", { + key: "namespace", + value: "com.azure.resourcemanager.storage.blob", + }), + success: true, + subRules: [new TspConfigJavaMgmtNamespaceFormatSubRule()], + }, + { + description: "Validate Java management namespace with 6 segments", + folder: managementTspconfigFolder, + tspconfigContent: createEmitterOptionExample("@azure-tools/typespec-java", { + key: "namespace", + value: "com.azure.resourcemanager.network.security.rules", + }), + success: true, + subRules: [new TspConfigJavaMgmtNamespaceFormatSubRule()], + }, + { + description: + "Validate Java management namespace with numbers and underscores in multiple segments", + folder: managementTspconfigFolder, + tspconfigContent: createEmitterOptionExample("@azure-tools/typespec-java", { + key: "namespace", + value: "com.azure.resourcemanager.storage_v2.blob_2024", + }), + success: true, + subRules: [new TspConfigJavaMgmtNamespaceFormatSubRule()], + }, + { + description: "Validate Java management namespace with invalid special characters", + folder: managementTspconfigFolder, + tspconfigContent: createEmitterOptionExample("@azure-tools/typespec-java", { + key: "namespace", + value: "com.azure.resourcemanager.storage@blob", + }), + success: false, + subRules: [new TspConfigJavaMgmtNamespaceFormatSubRule()], + }, +]; + const pythonManagementPackageDirTestCases = createEmitterOptionTestCases( "@azure-tools/typespec-python", managementTspconfigFolder, @@ -410,7 +484,7 @@ const pythonManagementGenerateTestTestCases = createEmitterOptionTestCases( "generate-test", true, false, - [new TspConfigPythonAzGenerateTestTrueSubRule()], + [new TspConfigPythonMgmtPackageGenerateTestTrueSubRule()], ); const pythonManagementGenerateSampleTestCases = createEmitterOptionTestCases( @@ -419,7 +493,7 @@ const pythonManagementGenerateSampleTestCases = createEmitterOptionTestCases( "generate-sample", true, false, - [new TspConfigPythonAzGenerateSampleTrueSubRule()], + [new TspConfigPythonMgmtPackageGenerateSampleTrueSubRule()], ); const pythonDpPackageDirTestCases = createEmitterOptionTestCases( @@ -431,24 +505,6 @@ const pythonDpPackageDirTestCases = createEmitterOptionTestCases( [new TspConfigPythonDpPackageDirectorySubRule()], ); -const pythonAzGenerateTestTestCases = createEmitterOptionTestCases( - "@azure-tools/typespec-python", - "", - "generate-test", - true, - false, - [new TspConfigPythonAzGenerateTestTrueSubRule()], -); - -const pythonAzGenerateSampleTestCases = createEmitterOptionTestCases( - "@azure-tools/typespec-python", - "", - "generate-sample", - true, - false, - [new TspConfigPythonAzGenerateSampleTrueSubRule()], -); - const csharpAzPackageDirTestCases = createEmitterOptionTestCases( "@azure-tools/typespec-csharp", "", @@ -557,6 +613,24 @@ options: success: true, ignoredKeyPaths: ["options.@azure-tools/typespec-ts.package-dir"], }, + { + description: "Suppress option with wildcard at the end", + folder: managementTspconfigFolder, + subRules: [ + new TspConfigGoMgmtPackageDirectorySubRule(), + new TspConfigGoMgmtModuleEqualStringSubRule(), + new TspConfigGoMgmtFixConstStutteringTrueSubRule(), + ], + tspconfigContent: ` +options: + "@azure-tools/typespec-go": + package-dir: "wrong/directory" + module-name: "invalid-module" + generate-consts: false +`, + success: true, + ignoredKeyPaths: ["options.@azure-tools/typespec-go.*"], + }, ]; describe("tspconfig", function () { @@ -599,14 +673,14 @@ describe("tspconfig", function () { ...goDpServiceDirTestCases, // java ...javaManagementPackageDirTestCases, + ...javaMgmtNamespaceTestCases, + ...javaMgmtNamespaceExtendedTestCases, // python ...pythonManagementPackageDirTestCases, ...pythonManagementNamespaceTestCases, ...pythonManagementGenerateTestTestCases, ...pythonManagementGenerateSampleTestCases, ...pythonDpPackageDirTestCases, - ...pythonAzGenerateTestTestCases, - ...pythonAzGenerateSampleTestCases, // csharp ...csharpAzPackageDirTestCases, ...csharpAzNamespaceTestCases, @@ -631,9 +705,7 @@ describe("tspconfig", function () { const rule = new SdkTspConfigValidationRule(c.subRules); const result = await rule.execute(c.folder); - // NOTE: to avoid huge impact on existing PRs, we always return true with info/warning messages. - const returnSuccess = true; - strictEqual(result.success, returnSuccess); + strictEqual(result.success, c.success); if (c.success) strictEqual(result.stdOutput?.includes("[SdkTspConfigValidation]: validation passed."), true); if (!c.success) @@ -686,4 +758,71 @@ describe("tspconfig", function () { strictEqual(result.success, true); strictEqual(result.stdOutput?.includes("[SdkTspConfigValidation]: validation skipped."), true); }); + + it("Tests wildcard suppression for multiple AWS connector services", async () => { + // List of AWS connector service paths to test + const awsServiceFolders = [ + "awsconnector/AccessAnalyzerAnalyzer.Management", + "awsconnector/AcmCertificateSummary.Management", + "awsconnector/ApiGatewayRestApi.Management", + "awsconnector/ApiGatewayStage.Management", + "awsconnector/AppSyncGraphqlApi.Management", + "awsconnector/AutoScalingAutoScalingGroup.Management", + "awsconnector/Awsconnector.Management", + ]; + + // Mock suppressions.yaml containing a wildcard path for awsconnector/*/tspconfig.yaml + const suppressionsSpy = vi + .spyOn(utils, "getSuppressions") + .mockImplementation(async (_path: string) => [ + { + tool: "TypeSpecValidation", + paths: ["awsconnector/*/tspconfig.yaml"], // Single wildcard pattern to match all paths + reason: "AWS Connector services have special requirements", + rules: ["SdkTspConfigValidation"], + subRules: ["parameters.service-dir.default"], + }, + ]); + + // Test each AWS connector service path + for (const awsServiceFolder of awsServiceFolders) { + // Reset mocks for each service + suppressionsSpy.mockClear(); + + // Mock configuration content + const tspconfigContent = ` +parameters: + service-dir: "${awsServiceFolder}" +`; + + // Setup mocks + readTspConfigSpy.mockImplementation(async () => tspconfigContent); + fileExistsSpy.mockImplementation(async (file: string) => { + return file === join(awsServiceFolder, "tspconfig.yaml"); + }); + + // Create validation rule and execute + const rule = new SdkTspConfigValidationRule([ + new TspConfigCommonAzServiceDirMatchPatternSubRule(), + ]); + const result = await rule.execute(awsServiceFolder); + + // Validate that validation passes for each service + strictEqual(result.success, true, `Validation should pass for ${awsServiceFolder}`); + strictEqual( + result.stdOutput?.includes("[SdkTspConfigValidation]: validation passed."), + true, + `Output should indicate validation passed for ${awsServiceFolder}`, + ); + + // Verify suppressions were called with the correct path + strictEqual( + suppressionsSpy.mock.calls.some( + (call) => call[0] === join(awsServiceFolder, "tspconfig.yaml"), + ), + true, + `getSuppressions should be called with path ${join(awsServiceFolder, "tspconfig.yaml")}`, + ); + } + }); }); diff --git a/eng/tools/typespec-validation/tsconfig.json b/eng/tools/typespec-validation/tsconfig.json index 984c104c8dfd..512241b97047 100644 --- a/eng/tools/typespec-validation/tsconfig.json +++ b/eng/tools/typespec-validation/tsconfig.json @@ -3,14 +3,8 @@ "compilerOptions": { "outDir": "./dist", "rootDir": ".", - "allowJs": true + "allowJs": true, }, - "include": [ - "*.ts", - "src/**/*.ts", - "test/**/*.ts", - ], - "references": [ - { "path": "../suppressions" } - ] + "include": ["*.ts", "src/**/*.ts", "test/**/*.ts"], + "references": [{ "path": "../suppressions" }], } diff --git a/package-lock.json b/package-lock.json index 1dcdcf4414df..1c9106567e6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,32 +6,32 @@ "": { "name": "azure-rest-api-specs", "devDependencies": { - "@autorest/openapi-to-typespec": "0.11.1", + "@autorest/openapi-to-typespec": "0.11.4", "@azure-tools/spec-gen-sdk": "~0.8.0", "@azure-tools/specs-shared": "file:.github/shared", "@azure-tools/typespec-apiview": "0.7.2", - "@azure-tools/typespec-autorest": "0.56.0", - "@azure-tools/typespec-azure-core": "0.56.0", - "@azure-tools/typespec-azure-portal-core": "0.56.0", - "@azure-tools/typespec-azure-resource-manager": "0.56.2", - "@azure-tools/typespec-azure-rulesets": "0.56.1", - "@azure-tools/typespec-client-generator-cli": "0.21.0", - "@azure-tools/typespec-client-generator-core": "0.56.2", + "@azure-tools/typespec-autorest": "0.58.1", + "@azure-tools/typespec-azure-core": "0.58.0", + "@azure-tools/typespec-azure-portal-core": "0.58.0", + "@azure-tools/typespec-azure-resource-manager": "0.58.1", + "@azure-tools/typespec-azure-rulesets": "0.58.0", + "@azure-tools/typespec-client-generator-cli": "0.26.0", + "@azure-tools/typespec-client-generator-core": "0.58.0", "@azure-tools/typespec-liftr-base": "0.8.0", "@azure/avocado": "^0.9.1", - "@typespec/compiler": "1.0.0", - "@typespec/events": "0.70.0", - "@typespec/http": "1.0.1", - "@typespec/openapi": "1.0.0", - "@typespec/openapi3": "1.0.0", - "@typespec/prettier-plugin-typespec": "1.0.0", - "@typespec/rest": "0.70.0", - "@typespec/sse": "0.70.0", - "@typespec/streams": "0.70.0", - "@typespec/versioning": "0.70.0", - "@typespec/xml": "0.70.0", + "@typespec/compiler": "1.2.1", + "@typespec/events": "0.72.1", + "@typespec/http": "1.2.1", + "@typespec/openapi": "1.2.1", + "@typespec/openapi3": "1.2.1", + "@typespec/prettier-plugin-typespec": "1.2.1", + "@typespec/rest": "0.72.1", + "@typespec/sse": "0.72.1", + "@typespec/streams": "0.72.1", + "@typespec/versioning": "0.72.1", + "@typespec/xml": "0.72.1", "azure-rest-api-specs-eng-tools": "file:eng/tools", - "oav": "^3.5.1", + "oav": "^3.6.3", "prettier": "~3.5.3", "typescript": "~5.8.2" }, @@ -44,13 +44,14 @@ "name": "@azure-tools/specs-shared", "dev": true, "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.9.3", + "@apidevtools/json-schema-ref-parser": "^14.1.1", "debug": "^4.4.0", "js-yaml": "^4.1.0", - "marked": "^15.0.7", + "marked": "^16.1.1", "simple-git": "^3.27.0" }, "bin": { + "api-doc-preview": "cmd/api-doc-preview.js", "spec-model": "cmd/spec-model.js" }, "devDependencies": { @@ -60,235 +61,27 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^20.0.0", "@vitest/coverage-v8": "^3.0.7", + "cross-env": "^7.0.3", "eslint": "^9.22.0", "globals": "^16.0.0", "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "semver": "^7.7.1", "typescript": "~5.8.2", "vitest": "^3.0.7" } }, - ".github/shared/node_modules/@types/node": { - "version": "20.17.43", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.43.tgz", - "integrity": "sha512-DnDEcDUnVAUYSa7U03QvrXbj1MZj00xoyi/a3lRGkR/c7BFUnqv+OY9EUphMqXUKdZJEOmuzu2mm+LmCisnPow==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - ".github/shared/node_modules/@vitest/coverage-v8": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.3.tgz", - "integrity": "sha512-cj76U5gXCl3g88KSnf80kof6+6w+K4BjOflCl7t6yRJPDuCrHtVu0SgNYOUARJOL5TI8RScDbm5x4s1/P9bvpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@bcoe/v8-coverage": "^1.0.2", - "debug": "^4.4.0", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.17", - "magicast": "^0.3.5", - "std-env": "^3.9.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "3.1.3", - "vitest": "3.1.3" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - ".github/shared/node_modules/@vitest/mocker": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.3.tgz", - "integrity": "sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.1.3", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - ".github/shared/node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, - "license": "MIT" - }, - ".github/shared/node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - ".github/shared/node_modules/vitest": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz", - "integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==", + ".github/shared/node_modules/marked": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.1.1.tgz", + "integrity": "sha512-ij/2lXfCRT71L6u0M29tJPhP0bM5shLL3u5BePhFwPELj2blMJ6GDtD7PfJhRLhJ/c2UwrK17ySVcDzy2YHjHQ==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/expect": "3.1.3", - "@vitest/mocker": "3.1.3", - "@vitest/pretty-format": "^3.1.3", - "@vitest/runner": "3.1.3", - "@vitest/snapshot": "3.1.3", - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", - "chai": "^5.2.0", - "debug": "^4.4.0", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.13", - "tinypool": "^1.0.2", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.3", - "why-is-node-running": "^2.3.0" - }, "bin": { - "vitest": "vitest.mjs" + "marked": "bin/marked.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.3", - "@vitest/ui": "3.1.3", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } + "node": ">= 20" } }, "eng/tools": { @@ -297,8 +90,11 @@ "hasInstallScript": true, "devDependencies": { "@azure-tools/lint-diff": "file:lint-diff", + "@azure-tools/oav-runner": "file:oav-runner", + "@azure-tools/openapi-diff-runner": "file:openapi-diff-runner", "@azure-tools/sdk-suppressions": "file:sdk-suppressions", "@azure-tools/spec-gen-sdk-runner": "file:spec-gen-sdk-runner", + "@azure-tools/summarize-impact": "file:summarize-impact", "@azure-tools/suppressions": "file:suppressions", "@azure-tools/tsp-client-tests": "file:tsp-client-tests", "@azure-tools/typespec-migration-validation": "file:typespec-migration-validation", @@ -310,14 +106,14 @@ "name": "@azure-tools/lint-diff", "dev": true, "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.9.3", + "@apidevtools/json-schema-ref-parser": "^14.1.1", "@azure-tools/specs-shared": "file:../../../.github/shared", - "@microsoft.azure/openapi-validator": "2.2.4", - "autorest": "3.6.1", + "@microsoft.azure/openapi-validator": "^2.2.4", + "autorest": "^3.7.2", "axios": "^1.8.3", "change-case": "^5.4.4", "deep-eql": "^5.0.2", - "marked": "^15.0.7" + "marked": "^16.1.1" }, "bin": { "lint-diff": "cmd/lint-diff.js" @@ -329,6 +125,7 @@ "execa": "^9.5.2", "memfs": "^4.17.0", "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "typescript": "~5.8.2", "vitest": "^3.0.2" }, @@ -336,307 +133,176 @@ "node": ">= 20.0.0" } }, - "eng/tools/lint-diff/node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "eng/tools/node_modules/@types/node": { - "version": "18.19.97", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.97.tgz", - "integrity": "sha512-4r3Y9EuCJjWduiam85Fo4GBQtneaEuoaBSdiKo+o6qwQUh0JFVBe7cRUK6I6yVzA0S1gBJJfoQx4VtBH4e5ikg==", + "eng/tools/lint-diff/node_modules/@types/node": { + "version": "18.19.120", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.120.tgz", + "integrity": "sha512-WtCGHFXnVI8WHLxDAt5TbnCM4eSE+nI0QN2NJtwzcgMhht2eNz6V9evJrk+lwC8bCY8OWV5Ym8Jz7ZEyGnKnMA==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } }, - "eng/tools/node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "eng/tools/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "eng/tools/node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "eng/tools/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", - "debug": "^4.3.4" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "eng/tools/node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "eng/tools/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "eng/tools/node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "eng/tools/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "node": ">=20" } }, - "eng/tools/node_modules/@vitest/coverage-v8": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.3.tgz", - "integrity": "sha512-cj76U5gXCl3g88KSnf80kof6+6w+K4BjOflCl7t6yRJPDuCrHtVu0SgNYOUARJOL5TI8RScDbm5x4s1/P9bvpw==", + "eng/tools/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@bcoe/v8-coverage": "^1.0.2", - "debug": "^4.4.0", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.17", - "magicast": "^0.3.5", - "std-env": "^3.9.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "3.1.3", - "vitest": "3.1.3" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } + "license": "MIT" }, - "eng/tools/node_modules/@vitest/mocker": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.3.tgz", - "integrity": "sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==", + "eng/tools/node_modules/glob": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@vitest/spy": "3.1.3", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" }, - "funding": { - "url": "https://opencollective.com/vitest" + "bin": { + "glob": "dist/esm/bin.mjs" }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0" + "engines": { + "node": "20 || >=22" }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "eng/tools/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "eng/tools/node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "@isaacs/cliui": "^8.0.2" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "eng/tools/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", "engines": { - "node": ">=12" + "node": "20 || >=22" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "eng/tools/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "eng/tools/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT", + "license": "MIT" + }, + "eng/tools/node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "dev": true, + "license": "ISC", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "20 || >=22" } }, - "eng/tools/node_modules/autorest": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/autorest/-/autorest-3.6.1.tgz", - "integrity": "sha512-tTOnfQq+LAyqnxFrOOnyCEaErXnjRTgduUN7a8LUv2u5deqDlI0zoJllHeIEYDZS2o2Kr1s8pDj2NxaFPOWldg==", + "eng/tools/node_modules/marked": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.1.1.tgz", + "integrity": "sha512-ij/2lXfCRT71L6u0M29tJPhP0bM5shLL3u5BePhFwPELj2blMJ6GDtD7PfJhRLhJ/c2UwrK17ySVcDzy2YHjHQ==", "dev": true, - "hasInstallScript": true, "license": "MIT", "bin": { - "autorest": "entrypoints/app.js" + "marked": "bin/marked.js" }, "engines": { - "node": ">=12.0.0" - } - }, - "eng/tools/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "node": ">= 20" } }, - "eng/tools/node_modules/cliui": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", - "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "eng/tools/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "dev": true, "license": "ISC", "dependencies": { - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": ">=20" - } - }, - "eng/tools/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, - "eng/tools/node_modules/ignore": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", - "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "eng/tools/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "eng/tools/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "eng/tools/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { "node": "20 || >=22" @@ -686,196 +352,95 @@ "dev": true, "license": "MIT" }, - "eng/tools/node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "eng/tools/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": ">=18" }, "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "eng/tools/node_modules/vitest": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz", - "integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==", + "eng/tools/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.3", - "@vitest/mocker": "3.1.3", - "@vitest/pretty-format": "^3.1.3", - "@vitest/runner": "3.1.3", - "@vitest/snapshot": "3.1.3", - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", - "chai": "^5.2.0", - "debug": "^4.4.0", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.13", - "tinypool": "^1.0.2", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.3", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.3", - "@vitest/ui": "3.1.3", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } + "node": "^20.19.0 || ^22.12.0 || >=23" } }, - "eng/tools/node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "eng/tools/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, + "license": "ISC", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, - "eng/tools/node_modules/yargs": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", - "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "eng/tools/oav-runner": { + "name": "@azure-tools/oav-runner", "dev": true, - "license": "MIT", "dependencies": { - "cliui": "^9.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "string-width": "^7.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^22.0.0" + "@azure-tools/specs-shared": "file:../../../.github/shared", + "js-yaml": "^4.1.0", + "oav": "^3.5.1", + "simple-git": "^3.27.0" + }, + "bin": { + "oav-runner": "cmd/oav-runner.js" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", + "typescript": "~5.8.2", + "vitest": "^3.0.7" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" + "node": ">=20.0.0" } }, - "eng/tools/node_modules/yargs-parser": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "eng/tools/openapi-diff-runner": { + "name": "@azure-tools/openapi-diff-runner", "dev": true, - "license": "ISC", + "dependencies": { + "@azure-tools/specs-shared": "file:../../../.github/shared", + "@azure/oad": "0.10.14" + }, + "bin": { + "openapi-diff-runner": "cmd/openapi-diff-runner.js" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@vitest/coverage-v8": "^3.0.7", + "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", + "typescript": "~5.8.2", + "vitest": "^3.0.7" + }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" + "node": ">=20.0.0" } }, "eng/tools/sdk-suppressions": { @@ -898,6 +463,8 @@ "@types/lodash": "^4.14.161", "@types/node": "^20.0.0", "@vitest/coverage-v8": "^3.0.7", + "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "typescript": "~5.8.2", "vitest": "^3.0.7" }, @@ -905,23 +472,6 @@ "node": ">=20.0.0" } }, - "eng/tools/sdk-suppressions/node_modules/@types/node": { - "version": "20.17.43", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.43.tgz", - "integrity": "sha512-DnDEcDUnVAUYSa7U03QvrXbj1MZj00xoyi/a3lRGkR/c7BFUnqv+OY9EUphMqXUKdZJEOmuzu2mm+LmCisnPow==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "eng/tools/sdk-suppressions/node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, - "license": "MIT" - }, "eng/tools/spec-gen-sdk-runner": { "name": "@azure-tools/spec-gen-sdk-runner", "version": "0.0.1", @@ -935,6 +485,8 @@ "@vitest/coverage-v8": "^3.0.7", "eslint": "^9.21.0", "eslint-plugin-unicorn": "^59.0.0", + "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "typescript": "~5.8.2", "typescript-eslint": "^8.26.0", "vitest": "^3.0.7" @@ -943,31 +495,45 @@ "node": ">=20.0.0" } }, - "eng/tools/spec-gen-sdk-runner/node_modules/@types/node": { - "version": "20.17.43", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.43.tgz", - "integrity": "sha512-DnDEcDUnVAUYSa7U03QvrXbj1MZj00xoyi/a3lRGkR/c7BFUnqv+OY9EUphMqXUKdZJEOmuzu2mm+LmCisnPow==", + "eng/tools/summarize-impact": { + "name": "@azure-tools/summarize-impact", "dev": true, - "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "@azure-tools/openapi-tools-common": "^1.2.2", + "@azure-tools/specs-shared": "file:../../../.github/shared", + "@azure/openapi-markdown": "0.9.4", + "@octokit/rest": "^22.0.0", + "@ts-common/commonmark-to-markdown": "^2.0.2", + "commonmark": "^0.29.0", + "glob": "^11.0.3", + "js-yaml": "^4.1.0", + "lodash": "^4.17.20", + "simple-git": "^3.27.0" + }, + "bin": { + "summarize-impact": "cmd/summarize-impact.js" + }, + "devDependencies": { + "@types/commonmark": "^0.27.4", + "@types/glob": "^8.1.0", + "@types/lodash": "^4.14.161", + "@types/node": "^20.0.0", + "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", + "typescript": "~5.8.2", + "vitest": "^3.0.7" + }, + "engines": { + "node": ">=20.0.0" } }, - "eng/tools/spec-gen-sdk-runner/node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, - "license": "MIT" - }, "eng/tools/suppressions": { "name": "@azure-tools/suppressions", "dev": true, "dependencies": { "minimatch": "^10.0.1", "yaml": "^2.4.2", - "zod": "^3.23.8", - "zod-validation-error": "^3.3.0" + "zod": "^4.0.2" }, "bin": { "get-suppressions": "cmd/get-suppressions.js" @@ -975,6 +541,8 @@ "devDependencies": { "@types/node": "^20.0.0", "@vitest/coverage-v8": "^3.0.7", + "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "typescript": "~5.8.2", "vitest": "^3.0.7" }, @@ -982,29 +550,14 @@ "node": ">=20.0.0" } }, - "eng/tools/suppressions/node_modules/@types/node": { - "version": "20.17.43", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.43.tgz", - "integrity": "sha512-DnDEcDUnVAUYSa7U03QvrXbj1MZj00xoyi/a3lRGkR/c7BFUnqv+OY9EUphMqXUKdZJEOmuzu2mm+LmCisnPow==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "eng/tools/suppressions/node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, - "license": "MIT" - }, "eng/tools/tsp-client-tests": { "name": "@azure-tools/tsp-client-tests", "dev": true, "devDependencies": { "@azure-tools/specs-shared": "file:../../../.github/shared", "@types/node": "^20.0.0", + "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "typescript": "~5.8.2", "vitest": "^3.0.7" }, @@ -1012,23 +565,6 @@ "node": ">=20.0.0" } }, - "eng/tools/tsp-client-tests/node_modules/@types/node": { - "version": "20.17.43", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.43.tgz", - "integrity": "sha512-DnDEcDUnVAUYSa7U03QvrXbj1MZj00xoyi/a3lRGkR/c7BFUnqv+OY9EUphMqXUKdZJEOmuzu2mm+LmCisnPow==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "eng/tools/tsp-client-tests/node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, - "license": "MIT" - }, "eng/tools/typespec-migration-validation": { "name": "@azure-tools/typespec-migration-validation", "dev": true, @@ -1041,25 +577,37 @@ "tsmv": "cmd/tsmv.js" }, "devDependencies": { - "@types/jest": "^29.4.0", "@types/json-diff": "^1.0.3", "@types/node": "^18.19.86", "@types/yargs": "^17.0.33", "@typescript-eslint/eslint-plugin": "^8.32.1", "@typescript-eslint/parser": "^8.32.1", "eslint": "^9.26.0", + "prettier": "~3.5.3", "typescript": "^5.8.3" }, "engines": { "node": ">=20.0.0" } }, + "eng/tools/typespec-migration-validation/node_modules/@types/node": { + "version": "18.19.120", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.120.tgz", + "integrity": "sha512-WtCGHFXnVI8WHLxDAt5TbnCM4eSE+nI0QN2NJtwzcgMhht2eNz6V9evJrk+lwC8bCY8OWV5Ym8Jz7ZEyGnKnMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, "eng/tools/typespec-requirement": { "name": "@azure-tools/typespec-requirement", "dev": true, "devDependencies": { "@types/node": "^20.0.0", "execa": "^9.3.0", + "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "typescript": "~5.8.2", "vitest": "^3.0.7" }, @@ -1067,23 +615,6 @@ "node": ">=20.0.0" } }, - "eng/tools/typespec-requirement/node_modules/@types/node": { - "version": "20.17.43", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.43.tgz", - "integrity": "sha512-DnDEcDUnVAUYSa7U03QvrXbj1MZj00xoyi/a3lRGkR/c7BFUnqv+OY9EUphMqXUKdZJEOmuzu2mm+LmCisnPow==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "eng/tools/typespec-requirement/node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, - "license": "MIT" - }, "eng/tools/typespec-validation": { "name": "@azure-tools/typespec-validation", "dev": true, @@ -1104,6 +635,8 @@ "@types/debug": "^4.1.12", "@types/node": "^20.0.0", "@vitest/coverage-v8": "^3.0.7", + "prettier": "~3.5.3", + "prettier-plugin-organize-imports": "^4.2.0", "typescript": "~5.8.2", "vitest": "^3.0.7" }, @@ -1111,23 +644,6 @@ "node": ">=20.0.0" } }, - "eng/tools/typespec-validation/node_modules/@types/node": { - "version": "20.17.43", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.43.tgz", - "integrity": "sha512-DnDEcDUnVAUYSa7U03QvrXbj1MZj00xoyi/a3lRGkR/c7BFUnqv+OY9EUphMqXUKdZJEOmuzu2mm+LmCisnPow==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "eng/tools/typespec-validation/node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, - "license": "MIT" - }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -1143,18 +659,17 @@ } }, "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "11.9.3", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", - "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.1.1.tgz", + "integrity": "sha512-uGF1YGOzzD50L7HLNWclXmsEhQflw8/zZHIz0/AzkJrKL5r9PceUipZxR/cp/8veTk4TVfdDJLyIwXLjaP5ePg==", "dev": true, "license": "MIT", "dependencies": { - "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" }, "engines": { - "node": ">= 16" + "node": ">= 20" }, "funding": { "url": "https://github.com/sponsors/philsturgeon" @@ -1178,16 +693,15 @@ "license": "MIT" }, "node_modules/@apidevtools/swagger-parser": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.1.tgz", - "integrity": "sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.0.0.tgz", + "integrity": "sha512-WLJIWcfOXrSKlZEM+yhA2Xzatgl488qr1FoOxixYmtWapBzwSC0gVGq4WObr4hHClMIiFFdOBdixNkvWqkWIWA==", "dev": true, "license": "MIT", "dependencies": { - "@apidevtools/json-schema-ref-parser": "11.7.2", + "@apidevtools/json-schema-ref-parser": "14.0.1", "@apidevtools/openapi-schemas": "^2.1.0", "@apidevtools/swagger-methods": "^3.0.2", - "@jsdevtools/ono": "^7.1.3", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "call-me-maybe": "^1.0.2" @@ -1197,13 +711,12 @@ } }, "node_modules/@apidevtools/swagger-parser/node_modules/@apidevtools/json-schema-ref-parser": { - "version": "11.7.2", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz", - "integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz", + "integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==", "dev": true, "license": "MIT", "dependencies": { - "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" }, @@ -1268,9 +781,9 @@ } }, "node_modules/@autorest/core": { - "version": "3.10.4", - "resolved": "https://registry.npmjs.org/@autorest/core/-/core-3.10.4.tgz", - "integrity": "sha512-dcjNuVNknelZ4i2YVgYEVWEPTVJvTbcYka+orkcsb27Fvcyf6Ntno4dS0aiBhQqVMsNIInZOYGG7x/EIW2RGhg==", + "version": "3.10.8", + "resolved": "https://registry.npmjs.org/@autorest/core/-/core-3.10.8.tgz", + "integrity": "sha512-7tj+zPUYu42lrzOZUC2hNaH7Xt53IVaEbWzV23aEYzDhXF0zD9TTpVexFXKTT4idBV0njsAKEKjPMkmQuHLbgQ==", "dev": true, "license": "MIT", "bin": { @@ -1297,9 +810,9 @@ } }, "node_modules/@autorest/openapi-to-typespec": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/@autorest/openapi-to-typespec/-/openapi-to-typespec-0.11.1.tgz", - "integrity": "sha512-Pdk+brpjR+oQbSxmNnejlfw4P2KAHJT1xUWEc+/0/EjZMRbzqY29c8vzcwfsCDsT6lw0YiBvouqVWu58No/ONg==", + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/@autorest/openapi-to-typespec/-/openapi-to-typespec-0.11.4.tgz", + "integrity": "sha512-3yb3C+bPUD+aBKcSMU2FUSVwILJXsKQV18KKbq9kKszI1AL+aaWx+JUSt5lp4KmXZ5MCVAtebjlwa09HroBLkw==", "dev": true, "license": "MIT", "dependencies": { @@ -1307,7 +820,7 @@ "@autorest/extension-base": "~3.6.1", "@azure-tools/codegen": "~2.10.1", "@azure-tools/openapi": "~3.6.1", - "@typespec/prettier-plugin-typespec": "^1.0.0", + "@typespec/prettier-plugin-typespec": "^1.2.1", "change-case-all": "~2.1.0", "lodash": "~4.17.20", "pluralize": "^8.0.0", @@ -1315,9 +828,9 @@ } }, "node_modules/@autorest/schemas": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@autorest/schemas/-/schemas-1.3.5.tgz", - "integrity": "sha512-HUP89Ns/4vDGcMtmFt/fxu+QqKvit/IQ8oBTQjzC6RnJojF+880KoEgTuweTuea2stzRmNyuMiBu4F8AnxdyUA==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@autorest/schemas/-/schemas-1.3.6.tgz", + "integrity": "sha512-kuditGLKhfEjHQxb1aCfs/j2hJL2y8eYEB94smxDd7Qp9beR+oYWwAM/y6PzgkAfk6OrRb3hZ+/NaZTXdoKU5A==", "dev": true, "license": "ISC" }, @@ -1374,6 +887,10 @@ "resolved": "eng/tools/lint-diff", "link": true }, + "node_modules/@azure-tools/oav-runner": { + "resolved": "eng/tools/oav-runner", + "link": true + }, "node_modules/@azure-tools/openapi": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/@azure-tools/openapi/-/openapi-3.6.1.tgz", @@ -1388,6 +905,10 @@ "node": ">=12.0.0" } }, + "node_modules/@azure-tools/openapi-diff-runner": { + "resolved": "eng/tools/openapi-diff-runner", + "link": true + }, "node_modules/@azure-tools/openapi-tools-common": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@azure-tools/openapi-tools-common/-/openapi-tools-common-1.2.2.tgz", @@ -1452,45 +973,14 @@ } } }, - "node_modules/@azure-tools/rest-api-diff": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@azure-tools/rest-api-diff/-/rest-api-diff-0.2.2.tgz", - "integrity": "sha512-M91TY7qGJJCp9fDxKxniei9ND6+5qFT3p4hwcSJn6vmilraKVc9Wj4ocN5Edt2TrKfuM8rl3jsdAAPoe42orFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure-tools/typespec-autorest": ">=0.54.0, <1.0.0", - "@azure-tools/typespec-azure-core": ">=0.54.0, <1.0.0", - "acorn": "^8.12.0", - "acorn-walk": "^8.3.3", - "arg": "^4.1.3", - "create-require": "^1.1.1", - "deep-diff": "^1.0.2", - "diff": "^7.0.0", - "diff2html": "^3.4.0", - "dotenv": "^16.4.5", - "make-error": "^1.3.6", - "undici-types": "^5.26.5", - "v8-compile-cache-lib": "^3.0.1", - "yaml": "^2.7.0", - "yargs": "^17.7.2", - "yn": "^3.1.1" - }, - "bin": { - "rest-api-diff": "cmd/rest-api-diff.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@azure-tools/sdk-suppressions": { "resolved": "eng/tools/sdk-suppressions", "link": true }, "node_modules/@azure-tools/spec-gen-sdk": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@azure-tools/spec-gen-sdk/-/spec-gen-sdk-0.8.0.tgz", - "integrity": "sha512-SXGC2ezRqbUBgFzBLjRZtrMrIP0T9pxlmPDrHJd4fM5qHR9r2dW06p807qXh+qZvWWvnhTIVIWIKfxy1kUJkTQ==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@azure-tools/spec-gen-sdk/-/spec-gen-sdk-0.8.1.tgz", + "integrity": "sha512-Im994/0TxjYouIsu41YFafIgfqM9DsJRrScLK71yn7uXp8LN2rbmWZGcj4nW0DFu39COQoDr8RoYvEa+fThfZw==", "dev": true, "license": "MIT", "dependencies": { @@ -1534,6 +1024,10 @@ "resolved": ".github/shared", "link": true }, + "node_modules/@azure-tools/summarize-impact": { + "resolved": "eng/tools/summarize-impact", + "link": true + }, "node_modules/@azure-tools/suppressions": { "resolved": "eng/tools/suppressions", "link": true @@ -1567,55 +1061,55 @@ } }, "node_modules/@azure-tools/typespec-autorest": { - "version": "0.56.0", - "resolved": "https://registry.npmjs.org/@azure-tools/typespec-autorest/-/typespec-autorest-0.56.0.tgz", - "integrity": "sha512-v10o6AGgCplrfqy3TImQXi8uxUfzU3bRkay7u7FWFOYGn7QqRAcva3H0FBZX9OcxReq3PY0bWomOqm5b3N9MIA==", + "version": "0.58.1", + "resolved": "https://registry.npmjs.org/@azure-tools/typespec-autorest/-/typespec-autorest-0.58.1.tgz", + "integrity": "sha512-cPQphQRJqK16/eet4t+eTQ5JcXoC0LLd+IBTewuVkTnyLZQBtJOQ6wGDfpjECkvqp69bGd59toaua1atNgSuRQ==", "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@azure-tools/typespec-azure-core": "^0.56.0", - "@azure-tools/typespec-azure-resource-manager": "^0.56.0", - "@azure-tools/typespec-client-generator-core": "^0.56.0", - "@typespec/compiler": "^1.0.0", - "@typespec/http": "^1.0.0", - "@typespec/openapi": "^1.0.0", - "@typespec/rest": "^0.70.0", - "@typespec/versioning": "^0.70.0" + "@azure-tools/typespec-azure-core": "^0.58.0", + "@azure-tools/typespec-azure-resource-manager": "^0.58.1", + "@azure-tools/typespec-client-generator-core": "^0.58.0", + "@typespec/compiler": "^1.2.0", + "@typespec/http": "^1.2.0", + "@typespec/openapi": "^1.2.0", + "@typespec/rest": "^0.72.0", + "@typespec/versioning": "^0.72.0" } }, "node_modules/@azure-tools/typespec-azure-core": { - "version": "0.56.0", - "resolved": "https://registry.npmjs.org/@azure-tools/typespec-azure-core/-/typespec-azure-core-0.56.0.tgz", - "integrity": "sha512-5Pb2p9MQJqcyQyptM/oo98ws3IXkXl14hVaaORFi+4VTRsOtALvnHdrUvnUQ9iTJrHioaXPxhR2+W7VuotoXsQ==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@azure-tools/typespec-azure-core/-/typespec-azure-core-0.58.0.tgz", + "integrity": "sha512-Z4vX+ic85hCPr27t9DKCVTj2MjHDaXDvh10Z4wVokXIR2/GjAQrHQp4OFu/0R+cwqmuXb6nnuBZNikwKk7dNzw==", "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.0.0", - "@typespec/http": "^1.0.0", - "@typespec/rest": "^0.70.0" + "@typespec/compiler": "^1.2.0", + "@typespec/http": "^1.2.0", + "@typespec/rest": "^0.72.0" } }, "node_modules/@azure-tools/typespec-azure-portal-core": { - "version": "0.56.0", - "resolved": "https://registry.npmjs.org/@azure-tools/typespec-azure-portal-core/-/typespec-azure-portal-core-0.56.0.tgz", - "integrity": "sha512-bd4VE/RaL3Rg/aIJwYAsasO1iVGskYui66+foUIcvC44myU1mNJbCn8yR4IRMU9n+GiskWLRdS0Rsg3T1RnoKg==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@azure-tools/typespec-azure-portal-core/-/typespec-azure-portal-core-0.58.0.tgz", + "integrity": "sha512-t5yc4PxoyK6LqzihYeHMNpc74kg42rsNFgmBEdSUdMc9ZVDeNSf3d0nER+t0LkbJt/MIbTcPCT5/Eri11Nsnhg==", "dev": true, "license": "MIT", "peerDependencies": { - "@azure-tools/typespec-azure-resource-manager": "^0.56.0", - "@typespec/compiler": "^1.0.0" + "@azure-tools/typespec-azure-resource-manager": "^0.58.0", + "@typespec/compiler": "^1.2.0" } }, "node_modules/@azure-tools/typespec-azure-resource-manager": { - "version": "0.56.2", - "resolved": "https://registry.npmjs.org/@azure-tools/typespec-azure-resource-manager/-/typespec-azure-resource-manager-0.56.2.tgz", - "integrity": "sha512-h1xYafwdSxtdu7Aqyicg7cG65rHGRw+e/H880JYlzJ41lniwPc1ci6yJmoeab8EgiQjbjoGAvVY1gYTLbFrESw==", + "version": "0.58.1", + "resolved": "https://registry.npmjs.org/@azure-tools/typespec-azure-resource-manager/-/typespec-azure-resource-manager-0.58.1.tgz", + "integrity": "sha512-gAifEZxuU0ZB00YbxwkM2Y/bktGjGltvbHN1R76wbM68L/JZhVzmOsrZP2nA+n0Diz/SXtZ4LLwpRrL29CD5Iw==", "dev": true, "license": "MIT", "dependencies": { @@ -1626,40 +1120,39 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@azure-tools/typespec-azure-core": "^0.56.0", - "@typespec/compiler": "^1.0.0", - "@typespec/http": "^1.0.1", - "@typespec/openapi": "^1.0.0", - "@typespec/rest": "^0.70.0", - "@typespec/versioning": "^0.70.0" + "@azure-tools/typespec-azure-core": "^0.58.0", + "@typespec/compiler": "^1.2.0", + "@typespec/http": "^1.2.0", + "@typespec/openapi": "^1.2.0", + "@typespec/rest": "^0.72.0", + "@typespec/versioning": "^0.72.0" } }, "node_modules/@azure-tools/typespec-azure-rulesets": { - "version": "0.56.1", - "resolved": "https://registry.npmjs.org/@azure-tools/typespec-azure-rulesets/-/typespec-azure-rulesets-0.56.1.tgz", - "integrity": "sha512-szK986vhfAZ5W2EIBH8nReOfw1zl1cryc5cs5nCEEbUf1GTq9e302Fky9CfwBLEeqTrtsJEZYtIRtchKmytVaA==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@azure-tools/typespec-azure-rulesets/-/typespec-azure-rulesets-0.58.0.tgz", + "integrity": "sha512-AkbhFxbeD3mYHbdnQEd1iWhSuQ2KxoN5JOU6BFP7mo/RVjIgGjj2ekro1tCnq381LOXdFfEoJievSFI5fvyRxQ==", "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@azure-tools/typespec-azure-core": "^0.56.0", - "@azure-tools/typespec-azure-resource-manager": "^0.56.1", - "@azure-tools/typespec-client-generator-core": "^0.56.0", - "@typespec/compiler": "^1.0.0" + "@azure-tools/typespec-azure-core": "^0.58.0", + "@azure-tools/typespec-azure-resource-manager": "^0.58.0", + "@azure-tools/typespec-client-generator-core": "^0.58.0", + "@typespec/compiler": "^1.2.0" } }, "node_modules/@azure-tools/typespec-client-generator-cli": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@azure-tools/typespec-client-generator-cli/-/typespec-client-generator-cli-0.21.0.tgz", - "integrity": "sha512-2VgbmV4UwXzsrrIypAxSpLVTFwgNpSm33CE6tB2cG8ickf+7Mp91jGyw7eCD2pRwwr4KHPSgpJfI0nOjKbttlw==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@azure-tools/typespec-client-generator-cli/-/typespec-client-generator-cli-0.26.0.tgz", + "integrity": "sha512-VltnBY0OLk0eU97EpKd6P6HFxkIvEOJvtxZKgpx9oAo8ao7RgCQjVPfBTShpN5jfEtsK/tAVrv9aVGhFGrq0JA==", "dev": true, "license": "MIT", "dependencies": { "@autorest/core": "^3.10.2", "@autorest/openapi-to-typespec": ">=0.10.6 <1.0.0", - "@azure-tools/rest-api-diff": ">=0.1.0 <1.0.0", "@azure-tools/typespec-autorest": ">=0.53.0 <1.0.0", "@azure/core-rest-pipeline": "^1.12.0", "@types/yargs": "^17.0.32", @@ -1682,30 +1175,30 @@ } }, "node_modules/@azure-tools/typespec-client-generator-core": { - "version": "0.56.2", - "resolved": "https://registry.npmjs.org/@azure-tools/typespec-client-generator-core/-/typespec-client-generator-core-0.56.2.tgz", - "integrity": "sha512-KplQWeC/iyUTaPfBAdKynCRon8BH15yc34NBXWsjllRz+X+ZwxCQeU3vxzHQnrUPTXtNKhjXpu/les1fdd/rDQ==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@azure-tools/typespec-client-generator-core/-/typespec-client-generator-core-0.58.0.tgz", + "integrity": "sha512-PK9WjPFXR14hrGyUwhstHuNVC4fkkQeNVHvvYhz3VaP6wh+iD7P3IOeIzzRJv/qSyv82t7BrVXdQAYir434ysw==", "dev": true, "license": "MIT", "dependencies": { "change-case": "~5.4.4", "pluralize": "^8.0.0", - "yaml": "~2.7.0" + "yaml": "~2.8.0" }, "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@azure-tools/typespec-azure-core": "^0.56.0", - "@typespec/compiler": "^1.0.0", - "@typespec/events": "^0.70.0", - "@typespec/http": "^1.0.1", - "@typespec/openapi": "^1.0.0", - "@typespec/rest": "^0.70.0", - "@typespec/sse": "^0.70.0", - "@typespec/streams": "^0.70.0", - "@typespec/versioning": "^0.70.0", - "@typespec/xml": "^0.70.0" + "@azure-tools/typespec-azure-core": "^0.58.0", + "@typespec/compiler": "^1.2.0", + "@typespec/events": "^0.72.0", + "@typespec/http": "^1.2.0", + "@typespec/openapi": "^1.2.0", + "@typespec/rest": "^0.72.0", + "@typespec/sse": "^0.72.0", + "@typespec/streams": "^0.72.0", + "@typespec/versioning": "^0.72.0", + "@typespec/xml": "^0.72.0" } }, "node_modules/@azure-tools/typespec-liftr-base": { @@ -1756,9 +1249,9 @@ } }, "node_modules/@azure/avocado": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@azure/avocado/-/avocado-0.9.1.tgz", - "integrity": "sha512-cnVDCL0uPnJTGp3wrhv0k7lXCPABbfcXT36Hf3jwSSXuWNQlQEvgZ/wR2kcZFsMnmMCaHWn5o7aTU3lOPFQ7Mg==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@azure/avocado/-/avocado-0.9.2.tgz", + "integrity": "sha512-dPbvYi1KpviObTaMNrQuaDPuzdPhe0r9QP9vRQ5kfqozbOpHcDjhWh652h9BaP80Nsb6G+cxKYHyj0XEqrSjEA==", "dev": true, "license": "MIT", "dependencies": { @@ -1902,21 +1395,6 @@ "node": ">=8" } }, - "node_modules/@azure/avocado/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@azure/avocado/node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -1962,9 +1440,9 @@ } }, "node_modules/@azure/core-auth": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", - "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.0.tgz", + "integrity": "sha512-88Djs5vBvGbHQHf5ZZcaoNHo6Y8BKZkt3cw2iuJIQzLEgH4Ox6Tm4hjFhbqOxyYsgIG/eJbFEHpxRIfEEWv5Ow==", "dev": true, "license": "MIT", "dependencies": { @@ -1973,7 +1451,7 @@ "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/core-http": { @@ -2052,9 +1530,9 @@ } }, "node_modules/@azure/core-rest-pipeline": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.20.0.tgz", - "integrity": "sha512-ASoP8uqZBS3H/8N8at/XwFr6vYrRP3syTK0EUjDXQy0Y1/AUS+QeIRThKmTNJO2RggvBBxaXDPM7YoIwDGeA0g==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.0.tgz", + "integrity": "sha512-OKHmb3/Kpm06HypvB3g6Q3zJuvyXcpxDpCS1PnU8OV6AJgSFaee/covXBcPbWc6XDDxtEPlbi3EMQ6nUiPaQtw==", "dev": true, "license": "MIT", "dependencies": { @@ -2063,53 +1541,53 @@ "@azure/core-tracing": "^1.0.1", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", - "@typespec/ts-http-runtime": "^0.2.2", + "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/core-tracing": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz", - "integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.0.tgz", + "integrity": "sha512-+XvmZLLWPe67WXNZo9Oc9CrPj/Tm8QnHR92fFAFdnbzwNdCH1h+7UdpaQgRSBsMY+oW1kHXNUZQLdZ1gHX3ROw==", "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/core-util": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.12.0.tgz", - "integrity": "sha512-13IyjTQgABPARvG90+N2dXpC+hwp466XCdQXPCRlbWHgd3SJd5Q1VvaBGv6k1BIa4MQm6hAF1UBU1m8QUxV8sQ==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.0.tgz", + "integrity": "sha512-o0psW8QWQ58fq3i24Q1K2XfS/jYTxr7O1HRcyUE9bV9NttLU+kYOH82Ixj8DGlMTOWgxm1Sss2QAfKK5UkSPxw==", "dev": true, "license": "MIT", "dependencies": { "@azure/abort-controller": "^2.0.0", - "@typespec/ts-http-runtime": "^0.2.2", + "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/logger": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.2.0.tgz", - "integrity": "sha512-0hKEzLhpw+ZTAfNJyRrn6s+V0nDWzXk9OjBr2TiGIu0OfMr5s2V4FpKLTAK3Ca5r5OKLbf4hkOGDPyiRjie/jA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", "dev": true, "license": "MIT", "dependencies": { - "@typespec/ts-http-runtime": "^0.2.2", + "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/ms-rest-js": { @@ -2130,15 +1608,16 @@ } }, "node_modules/@azure/ms-rest-js/node_modules/form-data": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz", - "integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" }, @@ -2146,29 +1625,6 @@ "node": ">= 0.12" } }, - "node_modules/@azure/ms-rest-js/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@azure/ms-rest-js/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/@azure/ms-rest-js/node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -2197,17 +1653,284 @@ "dev": true, "license": "0BSD" }, - "node_modules/@azure/openapi-markdown": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/@azure/openapi-markdown/-/openapi-markdown-0.9.4.tgz", - "integrity": "sha512-QBxabmf+64mQuyWRLsBoLKdvB7PH2U9RsyQMekorl17DOVEkgQxMsQdL/WMlz/V2wMyiI433FlrbdUiiFapfKg==", + "node_modules/@azure/oad": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/@azure/oad/-/oad-0.10.14.tgz", + "integrity": "sha512-lfiIsacGPoRFdossLRPptHw2OW7XBcnLCpZt3bTb/BWTR+3K1/M4t+BUOXB6aKA+iQQjurMkw4iksUWzSUVLnw==", "dev": true, "license": "MIT", "dependencies": { - "@ts-common/commonmark-to-markdown": "^2.0.2", - "@ts-common/iterator": "^0.3.1", - "@ts-common/string-map": "^0.3.0", - "@ts-common/virtual-fs": "^0.3.0", + "@ts-common/fs": "^0.2.0", + "@ts-common/iterator": "^0.3.6", + "@ts-common/json": "^0.3.1", + "@ts-common/json-parser": "^0.9.0", + "@ts-common/source-map": "^0.5.0", + "@ts-common/string-map": "^0.3.0", + "acorn": "^5.7.4", + "autorest": "^3.6.1", + "glob": "^7.1.3", + "js-yaml": "^3.13.1", + "json-pointer": "^0.6.2", + "json-refs": "^3.0.15", + "kind-of": "^6.0.3", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "request": "^2.88.0", + "set-value": "^4.1.0", + "shell-quote": "^1.8.3", + "source-map": "^0.7.4", + "tslib": "^2.6.3", + "winston": "^3.13.0", + "yargs": "^13.2.2", + "yargs-parser": "^13.1.2" + }, + "bin": { + "oad": "dist/cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/oad/node_modules/@ts-common/iterator": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@ts-common/iterator/-/iterator-0.3.6.tgz", + "integrity": "sha512-nNdcleTj3qLlchH17HI/xqOc6sNgOqJ5DdRR0nOEVdJVZCo5bfqoQTu6+Q9ZwMhuETuR2d86MSlmaL2FVHnPjQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@azure/oad/node_modules/acorn": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/@azure/oad/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@azure/oad/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@azure/oad/node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/@azure/oad/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@azure/oad/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@azure/oad/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@azure/oad/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@azure/oad/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@azure/oad/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@azure/oad/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@azure/oad/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@azure/oad/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@azure/oad/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@azure/oad/node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@azure/oad/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@azure/oad/node_modules/yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "node_modules/@azure/oad/node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/@azure/openapi-markdown": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@azure/openapi-markdown/-/openapi-markdown-0.9.4.tgz", + "integrity": "sha512-QBxabmf+64mQuyWRLsBoLKdvB7PH2U9RsyQMekorl17DOVEkgQxMsQdL/WMlz/V2wMyiI433FlrbdUiiFapfKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ts-common/commonmark-to-markdown": "^2.0.2", + "@ts-common/iterator": "^0.3.1", + "@ts-common/string-map": "^0.3.0", + "@ts-common/virtual-fs": "^0.3.0", "commonmark": "^0.28.1", "js-yaml": "^3.13.1", "tslib": "^1.9.3" @@ -2285,15 +2008,15 @@ "license": "MIT" }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -2320,13 +2043,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -2336,9 +2059,9 @@ } }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2383,9 +2106,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", "cpu": [ "ppc64" ], @@ -2400,9 +2123,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", "cpu": [ "arm" ], @@ -2417,9 +2140,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", "cpu": [ "arm64" ], @@ -2434,9 +2157,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", "cpu": [ "x64" ], @@ -2451,9 +2174,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", "cpu": [ "arm64" ], @@ -2468,9 +2191,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", "cpu": [ "x64" ], @@ -2485,9 +2208,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", "cpu": [ "arm64" ], @@ -2502,9 +2225,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", "cpu": [ "x64" ], @@ -2519,9 +2242,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", "cpu": [ "arm" ], @@ -2536,9 +2259,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", "cpu": [ "arm64" ], @@ -2553,9 +2276,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", "cpu": [ "ia32" ], @@ -2570,9 +2293,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", "cpu": [ "loong64" ], @@ -2587,9 +2310,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", "cpu": [ "mips64el" ], @@ -2604,9 +2327,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", "cpu": [ "ppc64" ], @@ -2621,9 +2344,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", "cpu": [ "riscv64" ], @@ -2638,9 +2361,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", "cpu": [ "s390x" ], @@ -2655,9 +2378,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", "cpu": [ "x64" ], @@ -2672,9 +2395,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", "cpu": [ "arm64" ], @@ -2689,9 +2412,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", "cpu": [ "x64" ], @@ -2706,9 +2429,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", "cpu": [ "arm64" ], @@ -2723,9 +2446,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", "cpu": [ "x64" ], @@ -2739,10 +2462,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", "cpu": [ "x64" ], @@ -2757,9 +2497,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", "cpu": [ "arm64" ], @@ -2774,9 +2514,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", "cpu": [ "ia32" ], @@ -2791,9 +2531,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", "cpu": [ "x64" ], @@ -2850,9 +2590,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2865,9 +2605,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2875,9 +2615,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2925,13 +2665,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", - "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -2945,13 +2688,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.15.1", "levn": "^0.4.1" }, "engines": { @@ -3028,9 +2771,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3041,38 +2784,369 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@inquirer/figures": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", - "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", + "node_modules/@inquirer/checkbox": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.0.tgz", + "integrity": "sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA==", "dev": true, "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@inquirer/confirm": { + "version": "5.1.14", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.14.tgz", + "integrity": "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" }, "engines": { - "node": ">=12" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "node_modules/@inquirer/core": { + "version": "10.1.15", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", + "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.15.tgz", + "integrity": "sha512-wst31XT8DnGOSS4nNJDIklGKnf+8shuauVrWzgKegWUe28zfCftcWZ2vktGdzJgcylWSS2SrDnYUb6alZcwnCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.17.tgz", + "integrity": "sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", + "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.1.tgz", + "integrity": "sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.17.tgz", + "integrity": "sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.17.tgz", + "integrity": "sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.7.1.tgz", + "integrity": "sha512-XDxPrEWeWUBy8scAXzXuFY45r/q49R0g72bUzgQXZ1DY/xEFX+ESDMkTQolcb5jRBzaNJX2W8XQl6krMNDTjaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.2.0", + "@inquirer/confirm": "^5.1.14", + "@inquirer/editor": "^4.2.15", + "@inquirer/expand": "^4.0.17", + "@inquirer/input": "^4.2.1", + "@inquirer/number": "^3.0.17", + "@inquirer/password": "^4.0.17", + "@inquirer/rawlist": "^4.1.5", + "@inquirer/search": "^3.0.17", + "@inquirer/select": "^4.3.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.5.tgz", + "integrity": "sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.17.tgz", + "integrity": "sha512-CuBU4BAGFqRYors4TNCYzy9X3DpKtgIW4Boi0WNkm4Ei1hvY9acxKdBdyqzqBCEe4YxSdaQQsasJlFlUJNgojw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.3.1.tgz", + "integrity": "sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", + "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { @@ -3177,118 +3251,43 @@ "node": ">=8" } }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, "license": "MIT", "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=6.0.0" } }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@jsdevtools/ono": { @@ -3490,26 +3489,17 @@ "js-yaml": "^4.1.0" } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz", - "integrity": "sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==", + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "dev": true, "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.3", - "eventsource": "^3.0.2", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, "engines": { - "node": ">=18" + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, "node_modules/@nodelib/fs.scandir": { @@ -3550,6 +3540,172 @@ "node": ">= 8" } }, + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.3.tgz", + "integrity": "sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.1", + "@octokit/request": "^10.0.2", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", + "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz", + "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.2", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.1.1.tgz", + "integrity": "sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.1.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-16.0.0.tgz", + "integrity": "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.1.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", + "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", + "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.0.tgz", + "integrity": "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/core": "^7.0.2", + "@octokit/plugin-paginate-rest": "^13.0.1", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -3560,6 +3716,16 @@ "node": ">=8.0.0" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3586,29 +3752,6 @@ "node": ">= 6" } }, - "node_modules/@postman/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@postman/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/@postman/tough-cookie": { "version": "4.1.3-postman.1", "resolved": "https://registry.npmjs.org/@postman/tough-cookie/-/tough-cookie-4.1.3-postman.1.tgz", @@ -3649,9 +3792,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", - "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz", + "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", "cpu": [ "arm" ], @@ -3663,9 +3806,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", - "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz", + "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", "cpu": [ "arm64" ], @@ -3677,9 +3820,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", - "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz", + "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", "cpu": [ "arm64" ], @@ -3691,9 +3834,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", - "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz", + "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", "cpu": [ "x64" ], @@ -3705,9 +3848,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", - "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz", + "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", "cpu": [ "arm64" ], @@ -3719,9 +3862,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", - "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz", + "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", "cpu": [ "x64" ], @@ -3733,9 +3876,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", - "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz", + "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", "cpu": [ "arm" ], @@ -3747,9 +3890,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", - "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz", + "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", "cpu": [ "arm" ], @@ -3761,9 +3904,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", - "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz", + "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", "cpu": [ "arm64" ], @@ -3775,9 +3918,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", - "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz", + "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", "cpu": [ "arm64" ], @@ -3789,9 +3932,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", - "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz", + "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", "cpu": [ "loong64" ], @@ -3803,9 +3946,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", - "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz", + "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", "cpu": [ "ppc64" ], @@ -3817,9 +3960,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", - "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz", + "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", "cpu": [ "riscv64" ], @@ -3831,9 +3974,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", - "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz", + "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", "cpu": [ "riscv64" ], @@ -3845,9 +3988,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", - "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz", + "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", "cpu": [ "s390x" ], @@ -3859,9 +4002,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", - "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz", + "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", "cpu": [ "x64" ], @@ -3873,9 +4016,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", - "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz", + "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", "cpu": [ "x64" ], @@ -3887,9 +4030,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", - "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz", + "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", "cpu": [ "arm64" ], @@ -3901,9 +4044,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", - "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz", + "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", "cpu": [ "ia32" ], @@ -3915,9 +4058,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", - "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz", + "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", "cpu": [ "x64" ], @@ -3935,13 +4078,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, "node_modules/@sindresorhus/merge-streams": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", @@ -4549,6 +4685,13 @@ "tslib": "^1.9.3" } }, + "node_modules/@ts-common/fs/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@ts-common/fs/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -4701,9 +4844,9 @@ } }, "node_modules/@tsconfig/node20": { - "version": "20.1.5", - "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.5.tgz", - "integrity": "sha512-Vm8e3WxDTqMGPU4GATF9keQAIy1Drd7bPwlgzKJnZtoOsTm1tduUTbDjg0W5qERvGuxPI2h9RbMufH0YdfBylA==", + "version": "20.1.6", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.6.tgz", + "integrity": "sha512-sz+Hqx9zwZDpZIV871WSbUzSqNIsXzghZydypnfgzPKLltVJfkINfUeTct31n/tTSa9ZE1ZOfKdRre1uHHquYQ==", "dev": true, "license": "MIT" }, @@ -4717,10 +4860,20 @@ "@types/retry": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/commonmark": { - "version": "0.27.9", - "resolved": "https://registry.npmjs.org/@types/commonmark/-/commonmark-0.27.9.tgz", - "integrity": "sha512-d3+57WgyPCcIc6oshmcPkmP4+JqRRot9eeZLsBsutWtIxwWivpoyc2wEcolOp8MyO3ZWN846mMdoR02kdHSMCw==", + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@types/commonmark/-/commonmark-0.27.10.tgz", + "integrity": "sha512-iEZobUnvlM+UX5fXWCmC4eQXwCs01Z8Xa1W0VjiWUF/XsNy4BHtskqJ9MyLZVMHbA0ezhyonCDqz3hMvsCm6Hg==", "dev": true, "license": "MIT" }, @@ -4752,48 +4905,21 @@ } }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.5.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", - "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", "dev": true, "license": "MIT", "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" + "@types/minimatch": "^5.1.2", + "@types/node": "*" } }, "node_modules/@types/js-yaml": { @@ -4818,9 +4944,16 @@ "license": "MIT" }, "node_modules/@types/lodash": { - "version": "4.17.16", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", - "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==", + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", "dev": true, "license": "MIT" }, @@ -4832,11 +4965,14 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "10.17.60", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", - "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "version": "20.19.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz", + "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } }, "node_modules/@types/node-fetch": { "version": "2.6.12", @@ -4856,13 +4992,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", @@ -4905,19 +5034,19 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz", - "integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", + "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/type-utils": "8.32.0", - "@typescript-eslint/utils": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/type-utils": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, @@ -4929,52 +5058,33 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.38.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", - "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", - "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">= 4" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", - "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", + "node_modules/@typescript-eslint/parser": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", + "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4982,19 +5092,21 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", - "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", + "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/tsconfig-utils": "^8.38.0", + "@typescript-eslint/types": "^8.38.0", "debug": "^4.3.4" }, "engines": { @@ -5005,19 +5117,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", - "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", + "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0" + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5027,360 +5138,15 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", - "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", + "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", - "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", - "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz", - "integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/utils": "8.32.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", - "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", - "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", - "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", - "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", - "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", - "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", - "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" @@ -5389,432 +5155,186 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", - "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.1", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typespec/asset-emitter": { - "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@typespec/asset-emitter/-/asset-emitter-0.70.0.tgz", - "integrity": "sha512-GuQqYtbFrZQdsSfJcr87DDuG6g2u2iYG5RdVJpq2p+RtbTFP0qorxf5AauCphx8Y+S49nT2joshCFtO2jRqozg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@typespec/compiler": "^1.0.0" - } - }, - "node_modules/@typespec/compiler": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@typespec/compiler/-/compiler-1.0.0.tgz", - "integrity": "sha512-QFy0otaB4xkN4kQmYyT17yu3OVhN0gti9+EKnZqs5JFylw2Xecx22BPwUE1Byj42pZYg5d9WlO+WwmY5ALtRDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "~7.26.2", - "@inquirer/prompts": "^7.4.0", - "ajv": "~8.17.1", - "change-case": "~5.4.4", - "env-paths": "^3.0.0", - "globby": "~14.1.0", - "is-unicode-supported": "^2.1.0", - "mustache": "~4.2.0", - "picocolors": "~1.1.1", - "prettier": "~3.5.3", - "semver": "^7.7.1", - "tar": "^7.4.3", - "temporal-polyfill": "^0.3.0", - "vscode-languageserver": "~9.0.1", - "vscode-languageserver-textdocument": "~1.0.12", - "yaml": "~2.7.0", - "yargs": "~17.7.2" - }, - "bin": { - "tsp": "cmd/tsp.js", - "tsp-server": "cmd/tsp-server.js" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@typespec/compiler/node_modules/@inquirer/checkbox": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.5.tgz", - "integrity": "sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.6", - "ansi-escapes": "^4.3.2", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@typespec/compiler/node_modules/@inquirer/confirm": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.9.tgz", - "integrity": "sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/type": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@typespec/compiler/node_modules/@inquirer/core": { - "version": "10.1.10", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.10.tgz", - "integrity": "sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.6", - "ansi-escapes": "^4.3.2", - "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@typespec/compiler/node_modules/@inquirer/editor": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.10.tgz", - "integrity": "sha512-5GVWJ+qeI6BzR6TIInLP9SXhWCEcvgFQYmcRG6d6RIlhFjM5TyG18paTGBgRYyEouvCmzeco47x9zX9tQEofkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/type": "^3.0.6", - "external-editor": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@typespec/compiler/node_modules/@inquirer/expand": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.12.tgz", - "integrity": "sha512-jV8QoZE1fC0vPe6TnsOfig+qwu7Iza1pkXoUJ3SroRagrt2hxiL+RbM432YAihNR7m7XnU0HWl/WQ35RIGmXHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/type": "^3.0.6", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@typespec/compiler/node_modules/@inquirer/input": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.9.tgz", - "integrity": "sha512-mshNG24Ij5KqsQtOZMgj5TwEjIf+F2HOESk6bjMwGWgcH5UBe8UoljwzNFHqdMbGYbgAf6v2wU/X9CAdKJzgOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/type": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@typespec/compiler/node_modules/@inquirer/number": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.12.tgz", - "integrity": "sha512-7HRFHxbPCA4e4jMxTQglHJwP+v/kpFsCf2szzfBHy98Wlc3L08HL76UDiA87TOdX5fwj2HMOLWqRWv9Pnn+Z5Q==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", + "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/type": "^3.0.6" + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "@types/node": ">=18" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typespec/compiler/node_modules/@inquirer/password": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.12.tgz", - "integrity": "sha512-FlOB0zvuELPEbnBYiPaOdJIaDzb2PmJ7ghi/SVwIHDDSQ2K4opGBkF+5kXOg6ucrtSUQdLhVVY5tycH0j0l+0g==", + "node_modules/@typescript-eslint/types": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", + "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", "dev": true, "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/type": "^3.0.6", - "ansi-escapes": "^4.3.2" - }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typespec/compiler/node_modules/@inquirer/prompts": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.5.0.tgz", - "integrity": "sha512-tk8Bx7l5AX/CR0sVfGj3Xg6v7cYlFBkEahH+EgBB+cZib6Fc83dwerTbzj7f2+qKckjIUGsviWRI1d7lx6nqQA==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", + "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.1.5", - "@inquirer/confirm": "^5.1.9", - "@inquirer/editor": "^4.2.10", - "@inquirer/expand": "^4.0.12", - "@inquirer/input": "^4.1.9", - "@inquirer/number": "^3.0.12", - "@inquirer/password": "^4.0.12", - "@inquirer/rawlist": "^4.1.0", - "@inquirer/search": "^3.0.12", - "@inquirer/select": "^4.2.0" + "@typescript-eslint/project-service": "8.38.0", + "@typescript-eslint/tsconfig-utils": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "@types/node": ">=18" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typespec/compiler/node_modules/@inquirer/rawlist": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.0.tgz", - "integrity": "sha512-6ob45Oh9pXmfprKqUiEeMz/tjtVTFQTgDDz1xAMKMrIvyrYjAmRbQZjMJfsictlL4phgjLhdLu27IkHNnNjB7g==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/type": "^3.0.6", - "yoctocolors-cjs": "^2.1.2" + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" + "node": ">=16 || 14 >=14.17" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typespec/compiler/node_modules/@inquirer/search": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.12.tgz", - "integrity": "sha512-H/kDJA3kNlnNIjB8YsaXoQI0Qccgf0Na14K1h8ExWhNmUg2E941dyFPrZeugihEa9AZNW5NdsD/NcvUME83OPQ==", + "node_modules/@typescript-eslint/utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", + "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.6", - "yoctocolors-cjs": "^2.1.2" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0" }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "@types/node": ">=18" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typespec/compiler/node_modules/@inquirer/select": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.0.tgz", - "integrity": "sha512-KkXQ4aSySWimpV4V/TUJWdB3tdfENZUU765GjOIZ0uPwdbGIG6jrxD4dDf1w68uP+DVtfNhr1A92B+0mbTZ8FA==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", + "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.6", - "ansi-escapes": "^4.3.2", - "yoctocolors-cjs": "^2.1.2" + "@typescript-eslint/types": "8.38.0", + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typespec/compiler/node_modules/@inquirer/type": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.6.tgz", - "integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==", + "node_modules/@typespec/asset-emitter": { + "version": "0.72.1", + "resolved": "https://registry.npmjs.org/@typespec/asset-emitter/-/asset-emitter-0.72.1.tgz", + "integrity": "sha512-lk41TinsVknczgl64OrEVQ+S6K5WiLAzDgIclaOVKu0ld1vNADz9grqwOtnTiYCz0pWRyZE+xhrq/9XkszU3lg==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.0.0" }, "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@typespec/compiler": "^1.2.1" } }, - "node_modules/@typespec/compiler/node_modules/@types/node": { - "version": "22.15.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.14.tgz", - "integrity": "sha512-BL1eyu/XWsFGTtDWOYULQEs4KR0qdtYfCxYAUYRoB7JP7h9ETYLgQTww6kH8Sj2C0pFGgrpM0XKv6/kbIzYJ1g==", + "node_modules/@typespec/compiler": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@typespec/compiler/-/compiler-1.2.1.tgz", + "integrity": "sha512-lUdHCRBPtianNN6QKt0G9qyyuSu7azbqKcYNimNLYQwrEIDcgSfQAUnoja9s+gtzCQQRzfbUZ8WLBC2b9cC81Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "undici-types": "~6.21.0" + "@babel/code-frame": "~7.27.1", + "@inquirer/prompts": "^7.4.0", + "ajv": "~8.17.1", + "change-case": "~5.4.4", + "env-paths": "^3.0.0", + "globby": "~14.1.0", + "is-unicode-supported": "^2.1.0", + "mustache": "~4.2.0", + "picocolors": "~1.1.1", + "prettier": "~3.5.3", + "semver": "^7.7.1", + "tar": "^7.4.3", + "temporal-polyfill": "^0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.12", + "yaml": "~2.8.0", + "yargs": "~18.0.0" + }, + "bin": { + "tsp": "cmd/tsp.js", + "tsp-server": "cmd/tsp-server.js" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@typespec/compiler/node_modules/ajv": { @@ -5835,15 +5355,53 @@ } }, "node_modules/@typespec/compiler/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@typespec/compiler/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@typespec/compiler/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" } }, + "node_modules/@typespec/compiler/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, "node_modules/@typespec/compiler/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -5851,68 +5409,111 @@ "dev": true, "license": "MIT" }, + "node_modules/@typespec/compiler/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@typespec/compiler/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@typespec/compiler/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "node_modules/@typespec/compiler/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@typespec/compiler/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } }, - "node_modules/@typespec/compiler/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "node_modules/@typespec/compiler/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, + "license": "ISC", "engines": { - "node": ">=8" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/@typespec/events": { - "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@typespec/events/-/events-0.70.0.tgz", - "integrity": "sha512-qHW1N05n8PkNf2YQGNMdl/sAYqrJv+zQ1kny+3vg/20nzVj7sZpNFIKqUIc11z0GkT7k3Q9SPTymvq+K00sAUg==", + "version": "0.72.1", + "resolved": "https://registry.npmjs.org/@typespec/events/-/events-0.72.1.tgz", + "integrity": "sha512-vUtA/mQD9csOCRLy9/EPS3oaUEmOiBXNhDkxtb7RYaZLA5975cprP+6o4ntSk6yCYQEo0/YtDcCbS4th2VGIqQ==", "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.0.0" + "@typespec/compiler": "^1.2.1" } }, "node_modules/@typespec/http": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@typespec/http/-/http-1.0.1.tgz", - "integrity": "sha512-J5tqBWlmkvI/W+kJn4EFuN0laGxbY8qT68jzEQEiYeAXSfNyFGRSoCwn8Ex6dJphq4IozOMdVTNtOZWIJlwmfw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@typespec/http/-/http-1.2.1.tgz", + "integrity": "sha512-HEPHgVFO2oQL6uZCtpqnRYVZizfSu9BO6vAgdRl1FYJWD2G0f/A4/hK6LEgpyZP44k39M1xMSqVrll2KZ5zpnw==", "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.0.0", - "@typespec/streams": "^0.70.0" + "@typespec/compiler": "^1.2.1", + "@typespec/streams": "^0.72.1" }, "peerDependenciesMeta": { "@typespec/streams": { @@ -5921,30 +5522,30 @@ } }, "node_modules/@typespec/openapi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@typespec/openapi/-/openapi-1.0.0.tgz", - "integrity": "sha512-pONzKIdK4wHgD1vBfD9opUk66zDG55DlHbueKOldH2p1LVf5FnMiuKE4kW0pl1dokT/HBNR5OJciCzzVf44AgQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@typespec/openapi/-/openapi-1.2.1.tgz", + "integrity": "sha512-PSoM6c5M7epiFdFDPL4zIJKRPUgJepMtOtO1vVOSIFuz26DcFQpc8xzBy7LBsRneSfp8b6XbsiaNXNcBP/9A1w==", "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.0.0", - "@typespec/http": "^1.0.0" + "@typespec/compiler": "^1.2.1", + "@typespec/http": "^1.2.1" } }, "node_modules/@typespec/openapi3": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@typespec/openapi3/-/openapi3-1.0.0.tgz", - "integrity": "sha512-cDsnNtJkQCx0R/+9AqXzqAKH6CgtwmnQGQMQHbkw0/Sxs5uk6hoiexx7vz0DUR7H4492MqPT2kE4351KZbDYMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@typespec/openapi3/-/openapi3-1.2.1.tgz", + "integrity": "sha512-PG4+yDTm1YI1rrxFAS3B8WZc6S66pl2WPK+9pP/5b0He9NkFmA53BIodgXpV2QuhvChCbEjr/CDa94ufv8+cKw==", "dev": true, "license": "MIT", "dependencies": { - "@apidevtools/swagger-parser": "~10.1.1", - "@typespec/asset-emitter": "^0.70.0", + "@apidevtools/swagger-parser": "~12.0.0", + "@typespec/asset-emitter": "^0.72.1", "openapi-types": "~12.1.3", - "yaml": "~2.7.0" + "yaml": "~2.8.0" }, "bin": { "tsp-openapi3": "cmd/tsp-openapi3.js" @@ -5953,11 +5554,11 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.0.0", - "@typespec/http": "^1.0.0", - "@typespec/json-schema": "^1.0.0", - "@typespec/openapi": "^1.0.0", - "@typespec/versioning": "^0.70.0" + "@typespec/compiler": "^1.2.1", + "@typespec/http": "^1.2.1", + "@typespec/json-schema": "^1.2.1", + "@typespec/openapi": "^1.2.1", + "@typespec/versioning": "^0.72.1" }, "peerDependenciesMeta": { "@typespec/json-schema": { @@ -5972,9 +5573,9 @@ } }, "node_modules/@typespec/prettier-plugin-typespec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@typespec/prettier-plugin-typespec/-/prettier-plugin-typespec-1.0.0.tgz", - "integrity": "sha512-pnZRJ0hdjDUTfem49YncT9LwiUBf/XPYn/jSdifvjyspOUg0an6YmqqmG4dGOEvkwRJQQvDftT7HxxT+7f40YQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@typespec/prettier-plugin-typespec/-/prettier-plugin-typespec-1.2.1.tgz", + "integrity": "sha512-RCMHl8r5BRS8+xi+Pj60d9W5lLCG/iQfVuctnL69bqUVYHnrK2ffcjHTTr0TRmDqXpxKFqFTtYJdurBaW1OjCg==", "dev": true, "license": "MIT", "dependencies": { @@ -5982,52 +5583,52 @@ } }, "node_modules/@typespec/rest": { - "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@typespec/rest/-/rest-0.70.0.tgz", - "integrity": "sha512-pn3roMQV6jBNT4bVA/hnrBAAHleXSyfWQqNO+DhI3+tLU4jCrJHmUZDi82nI9xBl+jkmy2WZFZOelZA9PSABeg==", + "version": "0.72.1", + "resolved": "https://registry.npmjs.org/@typespec/rest/-/rest-0.72.1.tgz", + "integrity": "sha512-w0C91JhrVos8mAdd3OVwrcS6aSjuKlw7LtoazHenAmou/zSACKZbH4g6ko1BY8fv5lgl+q7VZ3/52uEWHOTxpw==", "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.0.0", - "@typespec/http": "^1.0.0" + "@typespec/compiler": "^1.2.1", + "@typespec/http": "^1.2.1" } }, "node_modules/@typespec/sse": { - "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@typespec/sse/-/sse-0.70.0.tgz", - "integrity": "sha512-11VsIRqPuK+bIq7gHVghM5CAqvcfe9TmL9mZkxlPKuV6RRWju831k18KqlwXTOgeEMwVGA1Xbg1TTi1F4S1B+w==", + "version": "0.72.1", + "resolved": "https://registry.npmjs.org/@typespec/sse/-/sse-0.72.1.tgz", + "integrity": "sha512-J5Qitfi7uGhgkWO9aPKqHsEojt3aZHv2QbWrFvO1AkWPXHPML+1l66dmHg3XIQTmGAiUnCAj/JzS4W0E0yp9Dg==", "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.0.0", - "@typespec/events": "^0.70.0", - "@typespec/http": "^1.0.0", - "@typespec/streams": "^0.70.0" + "@typespec/compiler": "^1.2.1", + "@typespec/events": "^0.72.1", + "@typespec/http": "^1.2.1", + "@typespec/streams": "^0.72.1" } }, "node_modules/@typespec/streams": { - "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@typespec/streams/-/streams-0.70.0.tgz", - "integrity": "sha512-WIixoZ7CCLq2INX4UkN+aXlj07Je+ntW0xbeFGmpfq6Z2xifKnL6/sPiztURMXd4Z1I+XXFCn2pw1r9q5i4Cmw==", + "version": "0.72.1", + "resolved": "https://registry.npmjs.org/@typespec/streams/-/streams-0.72.1.tgz", + "integrity": "sha512-TJdFxpW9lgazOluDdT9N8Ojnb7T/hXMZOL094D2idBf33aeqJvSHZtWgY4po8hTsQLk8Y4m4WJJ70nT9DUEOdg==", "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.0.0" + "@typespec/compiler": "^1.2.1" } }, "node_modules/@typespec/ts-http-runtime": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.2.2.tgz", - "integrity": "sha512-Gz/Sm64+Sq/vklJu1tt9t+4R2lvnud8NbTD/ZfpZtMiUX7YeVpCA8j6NSW8ptwcoLL+NmYANwqP8DV0q/bwl2w==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.0.tgz", + "integrity": "sha512-sOx1PKSuFwnIl7z4RN0Ls7N9AQawmR9r66eI5rFCzLDIs8HTIYrIpH9QjYWoX0lkgGrkLxXhi4QnK7MizPRrIg==", "dev": true, "license": "MIT", "dependencies": { @@ -6036,44 +5637,79 @@ "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@typespec/versioning": { - "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@typespec/versioning/-/versioning-0.70.0.tgz", - "integrity": "sha512-LvuhDGJU9ksdUKuBZLBle7n9/xlS4e18kg5cqPpQGUI0hx9KSrZtXMoL6VRpoBVtEWcAmn4Q5dBL3+K4ur7/vg==", + "version": "0.72.1", + "resolved": "https://registry.npmjs.org/@typespec/versioning/-/versioning-0.72.1.tgz", + "integrity": "sha512-v1tBN2TcJilYpmb67v96YIVCGy8Su/c9hmuU6WABudWnYr26O4O+6gK2zx69RFxXORw+iw/LiDCU3XtFHbb7IQ==", "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.0.0" + "@typespec/compiler": "^1.2.1" } }, "node_modules/@typespec/xml": { - "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@typespec/xml/-/xml-0.70.0.tgz", - "integrity": "sha512-8feX+sFx2OVlGOZ3Bl9G/VFwbqbz6reVt8yllfO4aY0EVSM3GxIB7TivZofBrxDRYvwEADpc8+2gI+kdJaSL1w==", + "version": "0.72.1", + "resolved": "https://registry.npmjs.org/@typespec/xml/-/xml-0.72.1.tgz", + "integrity": "sha512-CmHXpwOojFT7cRI6ooB682OBIUVP9jMMx5bSoF9nrXg047Quaj7J0qPIwrG7d3O5lkogwPxqtPJPYaVq0+9gfg==", "dev": true, "license": "MIT", "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.0.0" + "@typespec/compiler": "^1.2.1" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } } }, "node_modules/@vitest/expect": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.3.tgz", - "integrity": "sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -6081,10 +5717,37 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, "node_modules/@vitest/pretty-format": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.3.tgz", - "integrity": "sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -6095,27 +5758,28 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.3.tgz", - "integrity": "sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.3", - "pathe": "^2.0.3" + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.3.tgz", - "integrity": "sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -6124,40 +5788,33 @@ } }, "node_modules/@vitest/spy": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.3.tgz", - "integrity": "sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^3.0.2" + "tinyspy": "^4.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.3.tgz", - "integrity": "sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", - "loupe": "^3.1.3", + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true, - "license": "ISC" - }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -6171,24 +5828,10 @@ "node": ">=6.5" } }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -6208,23 +5851,10 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { @@ -6352,13 +5982,6 @@ "dev": true, "license": "MIT" }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -6405,6 +6028,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -6435,6 +6065,25 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz", + "integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/astring": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", @@ -6480,9 +6129,9 @@ "license": "MIT" }, "node_modules/autorest": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/autorest/-/autorest-3.7.1.tgz", - "integrity": "sha512-6q17NtosQZPqBkIOUnaOPedf3PDIBF7Ha1iEGRhTqZF6TG2Q/1E3ID/D+ePIIzZDKvW01p/2pENq/oiBWH9IGQ==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/autorest/-/autorest-3.7.2.tgz", + "integrity": "sha512-yEeF0tJjx2fROK9VWIVHKFiUSzD0cxwqnq7z+v7kIIRRZjyOM3rpBS9OPp6tQv5d3mmxPAUNh57G1ZumQNqQGg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -6527,14 +6176,14 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "dev": true, "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -6580,6 +6229,13 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -6587,31 +6243,10 @@ "dev": true, "license": "MIT" }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -6643,9 +6278,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", "dev": true, "funding": [ { @@ -6663,8 +6298,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -6688,16 +6323,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -6786,9 +6411,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001717", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz", - "integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==", + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", "dev": true, "funding": [ { @@ -6814,9 +6439,9 @@ "license": "Apache-2.0" }, "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", "dev": true, "license": "MIT", "dependencies": { @@ -6827,7 +6452,7 @@ "pathval": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/chalk": { @@ -6864,9 +6489,9 @@ } }, "node_modules/chardet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.0.0.tgz", - "integrity": "sha512-xVgPpulCooDjY6zH4m9YW3jbkaBe3FKIAvF5sj5t7aBNsVl2ljIE+xwJ4iNgiDZHFQvNIpjdKdVOQvvk5ZfxbQ==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true, "license": "MIT" }, @@ -6901,9 +6526,9 @@ } }, "node_modules/ci-info": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz", - "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", "dev": true, "funding": [ { @@ -7016,6 +6641,24 @@ "node": ">=8" } }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -7118,6 +6761,16 @@ "node": "*" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -7125,57 +6778,21 @@ "dev": true, "license": "MIT" }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } + "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.42.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.42.0.tgz", - "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==", + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.44.0.tgz", + "integrity": "sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.24.4" + "browserslist": "^4.25.1" }, "funding": { "type": "opencollective", @@ -7189,27 +6806,25 @@ "dev": true, "license": "MIT" }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "dev": true, "license": "MIT", "dependencies": { - "object-assign": "^4", - "vary": "^1" + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" }, "engines": { - "node": ">= 0.10" + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" } }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -7320,9 +6935,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7347,13 +6962,6 @@ "node": ">=0.10.0" } }, - "node_modules/deep-diff": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz", - "integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==", - "dev": true, - "license": "MIT" - }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -7428,16 +7036,6 @@ "node": ">=0.4.0" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/dependency-graph": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", @@ -7459,41 +7057,15 @@ "minimalistic-assert": "^1.0.0" } }, - "node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/diff2html": { - "version": "3.4.51", - "resolved": "https://registry.npmjs.org/diff2html/-/diff2html-3.4.51.tgz", - "integrity": "sha512-/rVCSDyokkzSCEGaGjkkElXtIRwyNDRzIa3S8VUhR6pjk25p6+AMnb1s2zGmhjl66D5m/HnV3IeZoxnWsvTy+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "diff": "^7.0.0", - "hogan.js": "3.0.2" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "highlight.js": "11.9.0" + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" } }, "node_modules/difflib": { @@ -7509,9 +7081,9 @@ } }, "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -7566,17 +7138,10 @@ "safer-buffer": "^2.1.0" } }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, "node_modules/electron-to-chromium": { - "version": "1.5.150", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.150.tgz", - "integrity": "sha512-rOOkP2ZUMx1yL4fCxXQKDHQ8ZXwisb2OycOQVKHgvB3ZI4CvehOd4y2tfnnLDieJ3Zs1RL1Dlp3cMkyIn7nnXA==", + "version": "1.5.190", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.190.tgz", + "integrity": "sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==", "dev": true, "license": "ISC" }, @@ -7594,16 +7159,6 @@ "dev": true, "license": "MIT" }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/entities": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", @@ -7632,9 +7187,9 @@ "license": "MIT" }, "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, "license": "MIT", "dependencies": { @@ -7642,18 +7197,18 @@ "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -7665,21 +7220,24 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", + "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", + "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -7688,7 +7246,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -7698,18 +7256,18 @@ } }, "node_modules/es-aggregate-error": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.13.tgz", - "integrity": "sha512-KkzhUUuD2CUMqEc8JEqsXEMDHzDPE8RCjZeUBitsnB1eNcAJWQPiciKsMXe3Yytj4Flw1XLl46Qcf9OxvZha7A==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.14.tgz", + "integrity": "sha512-3YxX6rVb07B5TV11AV5wsL7nQCHXNwoHPsQC8S4AmBiqYhyNCJ5BRKXkXyDJvs8QzXN20NgRtxe3dEEQD9NLHA==", "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.24.0", "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "globalthis": "^1.0.3", + "globalthis": "^1.0.4", "has-property-descriptors": "^1.0.2", "set-function-name": "^2.0.2" }, @@ -7795,9 +7353,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7808,31 +7366,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" } }, "node_modules/escalade": { @@ -7845,13 +7404,6 @@ "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -7866,24 +7418,23 @@ } }, "node_modules/eslint": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", - "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.13.0", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.26.0", - "@eslint/plugin-kit": "^0.2.8", + "@eslint/js": "9.31.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", - "@modelcontextprotocol/sdk": "^1.8.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -7891,9 +7442,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -7907,8 +7458,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "zod": "^3.24.2" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -7963,10 +7513,37 @@ "eslint": ">=9.22.0" } }, + "node_modules/eslint-plugin-unicorn/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7981,9 +7558,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -8011,15 +7588,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8098,16 +7675,6 @@ "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -8118,48 +7685,25 @@ "node": ">=6" } }, - "node_modules/eventsource": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", - "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", - "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/execa": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.2.tgz", - "integrity": "sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz", + "integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==", "dev": true, "license": "MIT", "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", - "cross-spawn": "^7.0.3", + "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", - "human-signals": "^8.0.0", + "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", - "pretty-ms": "^9.0.0", + "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", - "yoctocolors": "^2.0.0" + "yoctocolors": "^2.1.1" }, "engines": { "node": "^18.19.0 || >=20.5.0" @@ -8223,92 +7767,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/expect-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", - "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.0.0" } }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -8331,26 +7799,6 @@ "node": ">=4" } }, - "node_modules/external-editor/node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true, - "license": "MIT" - }, - "node_modules/external-editor/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -8361,6 +7809,23 @@ ], "license": "MIT" }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -8419,6 +7884,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", @@ -8446,21 +7918,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -8602,9 +8059,9 @@ } }, "node_modules/filehound/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8647,24 +8104,6 @@ "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -8795,44 +8234,22 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { "node": ">= 6" } }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -8846,24 +8263,36 @@ "node": ">=12.20.0" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "node_modules/formidable": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", + "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" } }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "node_modules/formidable/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, "engines": { - "node": ">= 0.8" + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/front-matter": { @@ -9237,9 +8666,9 @@ } }, "node_modules/globals": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", - "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true, "license": "MIT", "engines": { @@ -9288,9 +8717,9 @@ } }, "node_modules/globby/node_modules/ignore": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", - "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -9324,6 +8753,16 @@ "dev": true, "license": "MIT" }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + } + }, "node_modules/growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -9479,53 +8918,12 @@ "dev": true, "license": "MIT" }, - "node_modules/highlight.js": { - "version": "11.9.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz", - "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/hogan.js": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/hogan.js/-/hogan.js-3.0.2.tgz", - "integrity": "sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==", - "dev": true, - "dependencies": { - "mkdirp": "0.3.0", - "nopt": "1.0.10" - }, - "bin": { - "hulk": "bin/hulk" - } - }, "node_modules/html-escaper": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" }, "node_modules/http-proxy-agent": { "version": "7.0.2", @@ -9623,9 +9021,9 @@ } }, "node_modules/humanize-duration": { - "version": "3.32.1", - "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.32.1.tgz", - "integrity": "sha512-inh5wue5XdfObhu/IGEMiA1nUXigSGcaKNemcbLRKa7jXYGDZXr3LoT9pTIzq2hPEbld7w/qv9h+ikWGz8fL1g==", + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.33.0.tgz", + "integrity": "sha512-vYJX7BSzn7EQ4SaP2lPYVy+icHDppB6k7myNeI3wrSRfwMS5+BHyGgzpHR0ptqJ2AQ6UuIKrclSg5ve6Ci4IAQ==", "dev": true, "license": "Unlicense" }, @@ -9640,13 +9038,13 @@ } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "safer-buffer": ">= 2.1.2 < 3" }, "engines": { "node": ">=0.10.0" @@ -9754,16 +9152,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -10003,6 +9391,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -10043,12 +9444,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-primitive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz", + "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/is-regex": { "version": "1.2.1", @@ -10255,6 +9672,16 @@ "dev": true, "license": "ISC" }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -10332,194 +9759,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-util/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jose": { "version": "4.14.4", "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", @@ -10639,6 +9878,73 @@ "foreach": "^2.0.4" } }, + "node_modules/json-refs": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/json-refs/-/json-refs-3.0.15.tgz", + "integrity": "sha512-0vOQd9eLNBL18EGl5yYaO44GhixmImes2wiYn9Z3sag3QnehWrYWlB9AFtMxCL2Bj3fyxgDYkxGFEU/chlYssw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "~4.1.1", + "graphlib": "^2.1.8", + "js-yaml": "^3.13.1", + "lodash": "^4.17.15", + "native-promise-only": "^0.8.1", + "path-loader": "^1.0.10", + "slash": "^3.0.0", + "uri-js": "^4.2.2" + }, + "bin": { + "json-refs": "bin/json-refs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/json-refs/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/json-refs/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/json-refs/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-refs/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -10784,6 +10090,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -10914,9 +10230,9 @@ } }, "node_modules/loupe": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz", + "integrity": "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==", "dev": true, "license": "MIT" }, @@ -10978,17 +10294,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, "node_modules/marked": { - "version": "15.0.11", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.11.tgz", - "integrity": "sha512-1BEXAU2euRCG3xwgLVT1y0xbJEld1XOrmRJpUwRCcy7rxhSCwMrmEu9LXoPhHSCJG41V7YcQ2mjKRr5BA3ITIA==", + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", "bin": { @@ -11026,22 +10335,12 @@ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", "dev": true, - "license": "MIT" - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } + "license": "MIT" }, "node_modules/memfs": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.1.tgz", - "integrity": "sha512-thuTRd7F4m4dReCIy7vv4eNYnU6XI/tHMLSMMHLiortw/Y0QxqKtinG523U2aerzwYWGi606oBP4oMPy4+edag==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.2.tgz", + "integrity": "sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -11058,19 +10357,6 @@ "url": "https://github.com/sponsors/streamich" } }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -11081,6 +10367,16 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -11095,23 +10391,23 @@ "node": ">=8.6" } }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8.6" + "bin": { + "mime": "cli.js" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "engines": { + "node": ">=4.0.0" } }, "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", "engines": { @@ -11129,13 +10425,13 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": "^1.54.0" + "mime-db": "1.52.0" }, "engines": { "node": ">= 0.6" @@ -11205,14 +10501,16 @@ } }, "node_modules/mkdirp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", - "integrity": "sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==", - "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true, - "license": "MIT/X11", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, "engines": { - "node": "*" + "node": ">=10" } }, "node_modules/moment": { @@ -11271,6 +10569,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/native-promise-only": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", + "integrity": "sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==", + "dev": true, + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -11278,16 +10583,6 @@ "dev": true, "license": "MIT" }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -11331,27 +10626,24 @@ "node": ">=16" } }, - "node_modules/newman/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/newman/node_modules/chardet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.0.0.tgz", + "integrity": "sha512-xVgPpulCooDjY6zH4m9YW3jbkaBe3FKIAvF5sj5t7aBNsVl2ljIE+xwJ4iNgiDZHFQvNIpjdKdVOQvvk5ZfxbQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } + "license": "MIT" }, - "node_modules/newman/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/newman/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, "node_modules/newman/node_modules/mkdirp": { @@ -11531,22 +10823,6 @@ "dev": true, "license": "MIT" }, - "node_modules/nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "*" - } - }, "node_modules/npm-run-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", @@ -11588,9 +10864,9 @@ } }, "node_modules/oav": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/oav/-/oav-3.6.0.tgz", - "integrity": "sha512-MB8/suEE9f1jibuiy35F5v6kHTZV9aDEjUH3W48UrnekDHIynANGyCxgEl2gGepb0NyuVz9nlDLzd3Q4fAsVVg==", + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/oav/-/oav-3.6.3.tgz", + "integrity": "sha512-TPIyDVJUsuujIlTTRuTZkwlJxJEZI+dTtxnbZwIdX7ZARgpXnxKSVFFpYITbEi/8/w1NoHT+f8Usi+P8IUGrFg==", "dev": true, "license": "MIT", "dependencies": { @@ -11616,7 +10892,7 @@ "json-merge-patch": "^1.0.2", "json-pointer": "^0.6.2", "json-schema-traverse": "^0.4.1", - "jsonpath-plus": "^10.2.0", + "jsonpath-plus": "^10.3.0", "junit-report-builder": "^3.0.0", "lodash": "^4.17.21", "md5-file": "^5.0.0", @@ -11747,19 +11023,6 @@ "node": ">=8" } }, - "node_modules/oav/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/oav/node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -11813,21 +11076,6 @@ "uuid": "bin/uuid" } }, - "node_modules/oav/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/oav/node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -11872,16 +11120,6 @@ "node": ">=6" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-hash": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", @@ -11936,19 +11174,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -12094,16 +11319,6 @@ "node": ">=6" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -12134,6 +11349,17 @@ "node": ">=8" } }, + "node_modules/path-loader": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/path-loader/-/path-loader-1.0.12.tgz", + "integrity": "sha512-n7oDG8B+k/p818uweWrOixY9/Dsr89o2TkCm6tOTex3fpdo2+BFDgR+KpB37mGKBRsBAlR8CIJMFN0OEy/7hIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "native-promise-only": "^0.8.1", + "superagent": "^7.1.6" + } + }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -12186,9 +11412,9 @@ "license": "MIT" }, "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", "engines": { @@ -12210,28 +11436,18 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -12263,9 +11479,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -12283,7 +11499,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -12360,27 +11576,17 @@ "node": ">=10" } }, - "node_modules/postman-collection/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/postman-collection/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/postman-collection/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, "node_modules/postman-collection/node_modules/semver": { @@ -12430,39 +11636,6 @@ "node": ">= 6" } }, - "node_modules/postman-request/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/postman-request/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/postman-request/node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.6" - } - }, "node_modules/postman-runtime": { "version": "7.39.1", "resolved": "https://registry.npmjs.org/postman-runtime/-/postman-runtime-7.39.1.tgz", @@ -12501,27 +11674,17 @@ "dev": true, "license": "MIT" }, - "node_modules/postman-runtime/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/postman-runtime/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/postman-runtime/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, "node_modules/postman-runtime/node_modules/postman-collection": { @@ -12579,27 +11742,17 @@ "node": ">=10" } }, - "node_modules/postman-sandbox/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/postman-sandbox/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/postman-sandbox/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, "node_modules/postman-sandbox/node_modules/postman-collection": { @@ -12680,32 +11833,21 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "node_modules/prettier-plugin-organize-imports": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.2.0.tgz", + "integrity": "sha512-Zdy27UhlmyvATZi67BTnLcKTo8fm6Oik59Sz6H64PgZJVs6NJpPD1mT240mmJn62c98/QaL+r3kx9Q3gRpDajg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10" + "peerDependencies": { + "prettier": ">=2.0", + "typescript": ">=2.9", + "vue-tsc": "^2.1.0 || 3" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "peerDependenciesMeta": { + "vue-tsc": { + "optional": true + } } }, "node_modules/pretty-ms": { @@ -12768,20 +11910,6 @@ "node": "*" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -12813,19 +11941,13 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", "dev": true, "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, "engines": { "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/querystringify": { @@ -12856,39 +11978,6 @@ ], "license": "MIT" }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -12991,6 +12080,97 @@ "node": ">=6" } }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/request/node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/request/node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -13057,13 +12237,13 @@ } }, "node_modules/rollup": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", - "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", + "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -13073,56 +12253,29 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.2", - "@rollup/rollup-android-arm64": "4.40.2", - "@rollup/rollup-darwin-arm64": "4.40.2", - "@rollup/rollup-darwin-x64": "4.40.2", - "@rollup/rollup-freebsd-arm64": "4.40.2", - "@rollup/rollup-freebsd-x64": "4.40.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", - "@rollup/rollup-linux-arm-musleabihf": "4.40.2", - "@rollup/rollup-linux-arm64-gnu": "4.40.2", - "@rollup/rollup-linux-arm64-musl": "4.40.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-musl": "4.40.2", - "@rollup/rollup-linux-s390x-gnu": "4.40.2", - "@rollup/rollup-linux-x64-gnu": "4.40.2", - "@rollup/rollup-linux-x64-musl": "4.40.2", - "@rollup/rollup-win32-arm64-msvc": "4.40.2", - "@rollup/rollup-win32-ia32-msvc": "4.40.2", - "@rollup/rollup-win32-x64-msvc": "4.40.2", + "@rollup/rollup-android-arm-eabi": "4.45.1", + "@rollup/rollup-android-arm64": "4.45.1", + "@rollup/rollup-darwin-arm64": "4.45.1", + "@rollup/rollup-darwin-x64": "4.45.1", + "@rollup/rollup-freebsd-arm64": "4.45.1", + "@rollup/rollup-freebsd-x64": "4.45.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", + "@rollup/rollup-linux-arm-musleabihf": "4.45.1", + "@rollup/rollup-linux-arm64-gnu": "4.45.1", + "@rollup/rollup-linux-arm64-musl": "4.45.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-musl": "4.45.1", + "@rollup/rollup-linux-s390x-gnu": "4.45.1", + "@rollup/rollup-linux-x64-gnu": "4.45.1", + "@rollup/rollup-linux-x64-musl": "4.45.1", + "@rollup/rollup-win32-arm64-msvc": "4.45.1", + "@rollup/rollup-win32-ia32-msvc": "4.45.1", + "@rollup/rollup-win32-x64-msvc": "4.45.1", "fsevents": "~2.3.2" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -13260,29 +12413,6 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/serialised-error": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/serialised-error/-/serialised-error-1.1.3.tgz", @@ -13306,22 +12436,6 @@ "uuid": "bin/uuid" } }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -13378,12 +12492,24 @@ "node": ">= 0.4" } }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "node_modules/set-value": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-4.1.0.tgz", + "integrity": "sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==", "dev": true, - "license": "ISC" + "funding": [ + "https://github.com/sponsors/jonschlinkert", + "https://paypal.me/jonathanschlinkert", + "https://jonschlinkert.dev/sponsor" + ], + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "is-primitive": "^3.0.1" + }, + "engines": { + "node": ">=11.0" + } }, "node_modules/shebang-command": { "version": "2.0.0", @@ -13408,6 +12534,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/shellwords": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", @@ -13525,15 +12664,15 @@ } }, "node_modules/simple-git": { - "version": "3.27.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.27.0.tgz", - "integrity": "sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==", + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.28.0.tgz", + "integrity": "sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==", "dev": true, "license": "MIT", "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", - "debug": "^4.3.5" + "debug": "^4.4.0" }, "funding": { "type": "github", @@ -13632,29 +12771,6 @@ "node": "*" } }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -13662,16 +12778,6 @@ "dev": true, "license": "MIT" }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/std-env": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", @@ -13679,6 +12785,20 @@ "dev": true, "license": "MIT" }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/stream-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-length/-/stream-length-1.0.2.tgz", @@ -13955,6 +13075,66 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/superagent": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-7.1.6.tgz", + "integrity": "sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==", + "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.3", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.0.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.10.3", + "readable-stream": "^3.6.0", + "semver": "^7.3.7" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/superagent/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -14053,9 +13233,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14133,27 +13313,55 @@ "dev": true, "license": "MIT" }, - "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, "engines": { - "node": ">=12.0.0" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/tinypool": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", "engines": { @@ -14171,9 +13379,9 @@ } }, "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", "dev": true, "license": "MIT", "engines": { @@ -14216,16 +13424,6 @@ "node": ">=8.0" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", @@ -14233,6 +13431,20 @@ "dev": true, "license": "MIT" }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -14241,9 +13453,9 @@ "license": "MIT" }, "node_modules/tree-dump": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", - "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.3.tgz", + "integrity": "sha512-il+Cv80yVHFBwokQSfd4bldvr1Md951DpgAGfmhydt04L+YzHgubm2tQ7zueWDcGENKHq0ZvGFR/hjvNXilHEg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -14297,6 +13509,19 @@ "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -14330,21 +13555,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -14438,15 +13648,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.0.tgz", - "integrity": "sha512-UMq2kxdXCzinFFPsXc9o2ozIpYCCOiEC46MG3yEh5Vipq6BO27otTtEBZA1fQ66DulEUgE97ucQ/3YY66CPg0A==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.38.0.tgz", + "integrity": "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.0", - "@typescript-eslint/parser": "8.32.0", - "@typescript-eslint/utils": "8.32.0" + "@typescript-eslint/eslint-plugin": "8.38.0", + "@typescript-eslint/parser": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -14501,9 +13712,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "5.28.4", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.28.4.tgz", - "integrity": "sha512-3OeMF5Lyowe8VW0skf5qaIE7Or3yS9LS7fvMUI0gg4YxpIBVg0L8BxCmROw2CcYhSkpR68Epz7CGc8MPj94Uww==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, @@ -14530,6 +13741,13 @@ "moment": "^2.14.1" } }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "dev": true, + "license": "ISC" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -14540,16 +13758,6 @@ "node": ">= 10.0.0" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -14656,33 +13864,16 @@ "dev": true, "license": "ISC" }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, "node_modules/validator": { - "version": "13.15.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", - "integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==", + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" } }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -14698,69 +13889,25 @@ "extsprintf": "^1.2.0" } }, - "node_modules/vite-node": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.3.tgz", - "integrity": "sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.0", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-node/node_modules/@types/node": { - "version": "22.15.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.14.tgz", - "integrity": "sha512-BL1eyu/XWsFGTtDWOYULQEs4KR0qdtYfCxYAUYRoB7JP7h9ETYLgQTww6kH8Sj2C0pFGgrpM0XKv6/kbIzYJ1g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/vite-node/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/vite-node/node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "node_modules/vite": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", + "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "fdir": "^6.4.6", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -14769,14 +13916,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", - "less": "*", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -14817,6 +13964,143 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vscode-jsonrpc": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-3.6.2.tgz", @@ -15098,9 +14382,9 @@ "license": "MIT" }, "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "license": "MIT", "dependencies": { @@ -15109,10 +14393,7 @@ "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/wrap-ansi-cjs": { @@ -15252,16 +14533,16 @@ } }, "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "node_modules/yargs": { @@ -15293,16 +14574,6 @@ "node": ">=12" } }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -15375,37 +14646,14 @@ } }, "node_modules/zod": { - "version": "3.24.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", - "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.8.tgz", + "integrity": "sha512-+MSh9cZU9r3QKlHqrgHMTSr3QwMGv4PLfR0M4N/sYWV5/x67HgXEhIGObdBkpnX8G78pTgWnIrBL2lZcNJOtfg==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } - }, - "node_modules/zod-validation-error": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.1.tgz", - "integrity": "sha512-1KP64yqDPQ3rupxNv7oXhf7KdhHHgaqbKuspVoiN93TT0xrBjql+Svjkdjq/Qh/7GSMmgQs3AfvBT0heE35thw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.24.4" - } } } } diff --git a/package.json b/package.json index 1d9057d1718c..7f636aee0e6c 100644 --- a/package.json +++ b/package.json @@ -4,34 +4,34 @@ "@azure-tools/spec-gen-sdk": "~0.8.0", "@azure-tools/specs-shared": "file:.github/shared", "@azure-tools/typespec-apiview": "0.7.2", - "@azure-tools/typespec-autorest": "0.56.0", - "@azure-tools/typespec-azure-core": "0.56.0", - "@azure-tools/typespec-azure-portal-core": "0.56.0", - "@azure-tools/typespec-azure-resource-manager": "0.56.2", - "@azure-tools/typespec-azure-rulesets": "0.56.1", - "@azure-tools/typespec-client-generator-cli": "0.21.0", - "@azure-tools/typespec-client-generator-core": "0.56.2", + "@azure-tools/typespec-autorest": "0.58.1", + "@azure-tools/typespec-azure-core": "0.58.0", + "@azure-tools/typespec-azure-portal-core": "0.58.0", + "@azure-tools/typespec-azure-resource-manager": "0.58.1", + "@azure-tools/typespec-azure-rulesets": "0.58.0", + "@azure-tools/typespec-client-generator-cli": "0.26.0", + "@azure-tools/typespec-client-generator-core": "0.58.0", "@azure-tools/typespec-liftr-base": "0.8.0", - "@autorest/openapi-to-typespec": "0.11.1", + "@autorest/openapi-to-typespec": "0.11.4", "@azure/avocado": "^0.9.1", - "@typespec/compiler": "1.0.0", - "@typespec/http": "1.0.1", - "@typespec/sse": "0.70.0", - "@typespec/events": "0.70.0", - "@typespec/openapi": "1.0.0", - "@typespec/openapi3": "1.0.0", - "@typespec/prettier-plugin-typespec": "1.0.0", - "@typespec/rest": "0.70.0", - "@typespec/streams": "0.70.0", - "@typespec/versioning": "0.70.0", - "@typespec/xml": "0.70.0", + "@typespec/compiler": "1.2.1", + "@typespec/http": "1.2.1", + "@typespec/sse": "0.72.1", + "@typespec/events": "0.72.1", + "@typespec/openapi": "1.2.1", + "@typespec/openapi3": "1.2.1", + "@typespec/prettier-plugin-typespec": "1.2.1", + "@typespec/rest": "0.72.1", + "@typespec/streams": "0.72.1", + "@typespec/versioning": "0.72.1", + "@typespec/xml": "0.72.1", "azure-rest-api-specs-eng-tools": "file:eng/tools", - "oav": "^3.5.1", + "oav": "^3.6.3", "prettier": "~3.5.3", "typescript": "~5.8.2" }, "overrides": { - "@typespec/asset-emitter": "0.70.0", + "@typespec/asset-emitter": "0.72.1", "jsonpath-plus": "^10.3.0" }, "engines": {