diff --git a/.github/arm-leases/README.md b/.github/arm-leases/README.md new file mode 100644 index 000000000000..327e201ec8d3 --- /dev/null +++ b/.github/arm-leases/README.md @@ -0,0 +1,97 @@ +# ARM Leases - Design Discussion Period + +## Overview + +This directory contains lease files that establish a time-limited design discussion period for Resource Providers (RPs) in the Azure REST API specifications repository. ARM leases provide a structured timeframe for Product Managers (PMs) and RP owners to collaborate on API design, review specifications. + +**Important**: Only Product Managers (PMs) are authorized to add lease.yaml files to this directory. Lease files should be added after conducting office hours discussions with RP owners. The reviewer field should contain the GitHub alias of a PM who has reviewed and approved the lease. + +## Code Owners and Contribution Guidelines + +This directory is intended to be governed via CODEOWNERS to ensure proper governance: + +- **Product Managers (PMs)**: Can add and modify `lease.yaml` files after office hours discussions with RP owners +- **Engineers**: Can enhance and improve the validation workflow and related automation +- **Approvals**: All changes should be reviewed and approved by designated code owners (PMs and engineers listed in CODEOWNERS, when configured) + +**Adding New PMs**: To grant a new PM access to add lease files, they should be added to the CODEOWNERS file for the `.github/arm-leases/` directory path when CODEOWNERS protection is configured. + +## Lease File Structure + +Each lease must be placed in the following directory structure: + +``` +.github/arm-leases///[ (optional)]/lease.yaml +``` + +### Path Requirements: + +- ``: lowercase alphanumeric only (e.g., testservice, widgetservice) +- ``: alphanumeric with dots, case-sensitive (e.g., Microsoft.TestRP, Azure.Widget) +- ``: (optional) customer-facing service name within an RP (e.g., DiskRP, ComputeRP). Must not start with "stable" or "preview" + +### Lease File Format + +The `lease.yaml` file must follow this format: + +```yaml +lease: + resource-provider: Microsoft.TestRP # Must match the rpNamespace folder name + startdate: 2026-01-07 # ISO 8601 format (YYYY-MM-DD) + duration: P180D # ISO 8601 duration (max 180 days, e.g., P180D, P90D, P5M) + reviewer: "@evanhissey" # GitHub alias of the approving reviewer +``` + +### Copy-Paste Template + +Create a file at `.github/arm-leases///(optional)/lease.yaml` with the following content (replace the placeholder values): + +```yaml +lease: + resource-provider: + startdate: + duration: P180D + reviewer: "@your-github-alias" +``` + +## Validation Rules + +All lease files are automatically validated with the following requirements: + +### 1. File Location + +- Only `lease.yaml` files are allowed in the `.github/arm-leases/` directory +- Must follow the folder structure: `//[ (optional)]/lease.yaml` + +### 2. Resource Provider Name + +- Must match the `` folder name exactly +- Example: If folder is `Microsoft.TestRP`, then `resource-provider` must be `Microsoft.TestRP` + +### 3. Start Date + +- Must be in ISO 8601 format: `YYYY-MM-DD` +- Must be a valid calendar date + +### 4. Duration + +- Required field that cannot be empty +- Must be a valid ISO 8601 duration (e.g., `P180D`, `P90D`, `P5M`) +- **Maximum duration is 180 days** +- Supports day-based (`P90D`), month-based (`P5M`), and combined formats + +### 5. Reviewer + +- Required field that cannot be empty +- Must be a GitHub alias starting with `@` (e.g., `@githubUsername`) + +## Troubleshooting + +If your PR check **"ARM Lease Validation"** is failing, review the error messages in the check output and fix the issues in your `lease.yaml` file. Common causes include: + +- **Invalid folder structure**: Ensure the path follows `//[]/lease.yaml` with lowercase org name +- **Resource provider mismatch**: The `resource-provider` value must match the `` folder name exactly +- **Past start date**: The `startdate` must be a valid date in `YYYY-MM-DD` format +- **Invalid duration**: Use a valid ISO 8601 duration that does not exceed 180 days (e.g., `P180D`, `P90D`, `P5M`) +- **Missing or empty fields**: All fields (`resource-provider`, `startdate`, `duration`, `reviewer`) are required +- **Disallowed files**: Only `lease.yaml` and `README.md` files are permitted in `.github/arm-leases/` diff --git a/.github/arm-leases/barbie/Microsoft.Barbie/Barbie/lease.yaml b/.github/arm-leases/barbie/Microsoft.Barbie/Barbie/lease.yaml new file mode 100644 index 000000000000..bc6272e57a85 --- /dev/null +++ b/.github/arm-leases/barbie/Microsoft.Barbie/Barbie/lease.yaml @@ -0,0 +1,5 @@ +lease: + resource-provider: Microsoft.Barbie + startdate: 2026-03-19 + duration: P180D + reviewer: "@vikeshi26" diff --git a/.github/arm-leases/batman/Microsoft.Batman/Batman/lease.yaml b/.github/arm-leases/batman/Microsoft.Batman/Batman/lease.yaml new file mode 100644 index 000000000000..2a15a6c9fafa --- /dev/null +++ b/.github/arm-leases/batman/Microsoft.Batman/Batman/lease.yaml @@ -0,0 +1,5 @@ +lease: + resource-provider: Microsoft.Batman + startdate: 2026-03-19 + duration: P180D + reviewer: "@vikeshi26" \ No newline at end of file diff --git a/.github/arm-leases/widget/Microsoft.Widget/Widget/lease.yaml b/.github/arm-leases/widget/Microsoft.Widget/Widget/lease.yaml new file mode 100644 index 000000000000..4948142a6a3a --- /dev/null +++ b/.github/arm-leases/widget/Microsoft.Widget/Widget/lease.yaml @@ -0,0 +1,5 @@ +lease: + resource-provider: Microsoft.Widget + startdate: "2026-03-20" + duration: P180D + reviewer: "@pshao25" diff --git a/.github/arm-leases/xyz/Microsoft.XYZ/XYZ/lease.yaml b/.github/arm-leases/xyz/Microsoft.XYZ/XYZ/lease.yaml new file mode 100644 index 000000000000..88b663c5f72e --- /dev/null +++ b/.github/arm-leases/xyz/Microsoft.XYZ/XYZ/lease.yaml @@ -0,0 +1,5 @@ +lease: + resource-provider: Microsoft.XYZ + startdate: "2026-03-19" + duration: P180D + reviewer: "@tejaswiMinnu" diff --git a/.github/cspell.yaml b/.github/cspell.yaml index 65919c993d85..b782a7617889 100644 --- a/.github/cspell.yaml +++ b/.github/cspell.yaml @@ -3,6 +3,7 @@ import: - ../cspell.yaml ignorePaths: - CODEOWNERS* + - arm-leases/** - policies/** - workflows/*.lock.yml - workflows/**/*.md diff --git a/.github/package-lock.json b/.github/package-lock.json index 7d58a00143eb..395289f4c2a3 100644 --- a/.github/package-lock.json +++ b/.github/package-lock.json @@ -6,6 +6,7 @@ "": { "dependencies": { "@apidevtools/json-schema-ref-parser": "^15.1.3", + "@js-temporal/polyfill": "0.5.1", "debug": "^4.4.3", "js-yaml": "^4.1.0", "markdown-table": "^3.0.4", @@ -143,9 +144,9 @@ "license": "MIT" }, "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-15.3.1.tgz", - "integrity": "sha512-FIweGOR9zrNuskfDXn8dfsA4eJEe8LmmGsGSDikEZvgYm36SO36yMhasXSOX7/OTGZ3b7I9iPhOxB24D8xL5uQ==", + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-15.2.2.tgz", + "integrity": "sha512-54fvjSwWiBTdVviiUItOCeyxtPSBmCrSEjlOl8XFEDuYD3lXY1lOBWKim/WJ3i1EYzdGx6rSOjK5KRDMppLI4Q==", "license": "MIT", "dependencies": { "js-yaml": "^4.1.1" @@ -157,6 +158,43 @@ "@types/json-schema": "^7.0.15" } }, + "node_modules/@azure-tools/async-io": { + "version": "3.0.254", + "resolved": "https://registry.npmjs.org/@azure-tools/async-io/-/async-io-3.0.254.tgz", + "integrity": "sha512-X1C7XdyCuo50ch9FzKtTvmK18FgDxxf1Bbt3cSoknQqeDaRegHSSCO+zByq2YA4NvUzKXeZ1engh29IDxZXgpQ==", + "license": "MIT", + "dependencies": { + "@azure-tools/tasks": "~3.0.255", + "proper-lockfile": "~2.0.1" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@azure-tools/tasks": { + "version": "3.0.255", + "resolved": "https://registry.npmjs.org/@azure-tools/tasks/-/tasks-3.0.255.tgz", + "integrity": "sha512-GjALNLz7kWMEdRVbaN5g0cJHNAr3XVTbP0611Mv2UzMgGL6FOhNZJK+oPHJKLDR8EEDZNnkwPlyi7B+INXUSQA==", + "license": "MIT", + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@azure-tools/uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@azure-tools/uri/-/uri-3.1.1.tgz", + "integrity": "sha512-UgPgD+qVtm4ASYqoTDazjowimrmMGGEQqPnNk9K/8CZdi2oSLtGqX9S1++2+NDaHlq74VyxbcNMKoxgO+2CCUQ==", + "license": "MIT", + "dependencies": { + "@azure-tools/async-io": "~3.0.0", + "file-url": "3.0.0", + "get-uri": "~3.0.2", + "urijs": "~1.19.1" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -505,6 +543,18 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-temporal/polyfill": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@js-temporal/polyfill/-/polyfill-0.5.1.tgz", + "integrity": "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==", + "license": "ISC", + "dependencies": { + "jsbi": "^4.3.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -553,6 +603,7 @@ "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -868,6 +919,7 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -1291,6 +1343,440 @@ "dev": true, "license": "MIT" }, + "node_modules/@stoplight/json": { + "version": "3.21.7", + "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.21.7.tgz", + "integrity": "sha512-xcJXgKFqv/uCEgtGlPxy3tPA+4I+ZI4vAuMJ885+ThkTHFVkC+0Fm58lA9NlsyjnkpxFh4YiQWpH+KefHdbA0A==", + "license": "Apache-2.0", + "dependencies": { + "@stoplight/ordered-object-literal": "^1.0.3", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^13.6.0", + "jsonc-parser": "~2.2.1", + "lodash": "^4.17.21", + "safe-stable-stringify": "^1.1" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json-ref-readers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@stoplight/json-ref-readers/-/json-ref-readers-1.2.2.tgz", + "integrity": "sha512-nty0tHUq2f1IKuFYsLM4CXLZGHdMn+X/IwEUIpeSOXt0QjMUbL0Em57iJUDzz+2MkWG83smIigNZ3fauGjqgdQ==", + "license": "Apache-2.0", + "dependencies": { + "node-fetch": "^2.6.0", + "tslib": "^1.14.1" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json-ref-readers/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@stoplight/json-ref-resolver": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.6.tgz", + "integrity": "sha512-YNcWv3R3n3U6iQYBsFOiWSuRGE5su1tJSiX6pAPRVk7dP0L7lqCteXGzuVRQ0gMZqUl8v1P0+fAKxF6PLo9B5A==", + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "^3.21.0", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^12.3.0 || ^13.0.0", + "@types/urijs": "^1.19.19", + "dependency-graph": "~0.11.0", + "fast-memoize": "^2.5.2", + "immer": "^9.0.6", + "lodash": "^4.17.21", + "tslib": "^2.6.0", + "urijs": "^1.19.11" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json-ref-resolver/node_modules/@stoplight/types": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.20.0.tgz", + "integrity": "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/json-ref-resolver/node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/@stoplight/json/node_modules/@stoplight/types": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.20.0.tgz", + "integrity": "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/json/node_modules/jsonc-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.1.tgz", + "integrity": "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==", + "license": "MIT" + }, + "node_modules/@stoplight/ordered-object-literal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.5.tgz", + "integrity": "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/path": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@stoplight/path/-/path-1.3.2.tgz", + "integrity": "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/spectral-core": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-core/-/spectral-core-1.21.0.tgz", + "integrity": "sha512-oj4e/FrDLUhBRocIW+lRMKlJ/q/rDZw61HkLbTFsdMd+f/FTkli2xHNB1YC6n1mrMKjjvy7XlUuFkC7XxtgbWw==", + "license": "Apache-2.0", + "dependencies": { + "@stoplight/better-ajv-errors": "1.0.3", + "@stoplight/json": "~3.21.0", + "@stoplight/path": "1.3.2", + "@stoplight/spectral-parsers": "^1.0.0", + "@stoplight/spectral-ref-resolver": "^1.0.4", + "@stoplight/spectral-runtime": "^1.1.2", + "@stoplight/types": "~13.6.0", + "@types/es-aggregate-error": "^1.0.2", + "@types/json-schema": "^7.0.11", + "ajv": "^8.17.1", + "ajv-errors": "~3.0.0", + "ajv-formats": "~2.1.1", + "es-aggregate-error": "^1.0.7", + "jsonpath-plus": "^10.3.0", + "lodash": "~4.17.23", + "lodash.topath": "^4.5.2", + "minimatch": "3.1.2", + "nimma": "0.2.3", + "pony-cause": "^1.1.1", + "simple-eval": "1.0.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/@stoplight/better-ajv-errors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", + "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", + "license": "Apache-2.0", + "dependencies": { + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": "^12.20 || >= 14.13" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/@stoplight/types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.6.0.tgz", + "integrity": "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "peer": true, + "dependencies": { + "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": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/ajv-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", + "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.0.1" + } + }, + "node_modules/@stoplight/spectral-core/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==", + "license": "MIT" + }, + "node_modules/@stoplight/spectral-core/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@stoplight/spectral-formats": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-formats/-/spectral-formats-1.8.2.tgz", + "integrity": "sha512-c06HB+rOKfe7tuxg0IdKDEA5XnjL2vrn/m/OVIIxtINtBzphZrOgtRn7epQ5bQF5SWp84Ue7UJWaGgDwVngMFw==", + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "^3.17.0", + "@stoplight/spectral-core": "^1.19.2", + "@types/json-schema": "^7.0.7", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-functions": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-functions/-/spectral-functions-1.10.1.tgz", + "integrity": "sha512-obu8ZfoHxELOapfGsCJixKZXZcffjg+lSoNuttpmUFuDzVLT3VmH8QkPXfOGOL5Pz80BR35ClNAToDkdnYIURg==", + "license": "Apache-2.0", + "dependencies": { + "@stoplight/better-ajv-errors": "1.0.3", + "@stoplight/json": "^3.17.1", + "@stoplight/spectral-core": "^1.19.4", + "@stoplight/spectral-formats": "^1.8.1", + "@stoplight/spectral-runtime": "^1.1.2", + "ajv": "^8.17.1", + "ajv-draft-04": "~1.0.0", + "ajv-errors": "~3.0.0", + "ajv-formats": "~2.1.1", + "lodash": "~4.17.21", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/@stoplight/better-ajv-errors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", + "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", + "license": "Apache-2.0", + "dependencies": { + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": "^12.20 || >= 14.13" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "peer": true, + "dependencies": { + "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": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/ajv-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", + "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.0.1" + } + }, + "node_modules/@stoplight/spectral-functions/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==", + "license": "MIT" + }, + "node_modules/@stoplight/spectral-parsers": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-parsers/-/spectral-parsers-1.0.5.tgz", + "integrity": "sha512-ANDTp2IHWGvsQDAY85/jQi9ZrF4mRrA5bciNHX+PUxPr4DwS6iv4h+FVWJMVwcEYdpyoIdyL+SRmHdJfQEPmwQ==", + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "~3.21.0", + "@stoplight/types": "^14.1.1", + "@stoplight/yaml": "~4.3.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-ref-resolver": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-ref-resolver/-/spectral-ref-resolver-1.0.5.tgz", + "integrity": "sha512-gj3TieX5a9zMW29z3mBlAtDOCgN3GEc1VgZnCVlr5irmR4Qi5LuECuFItAq4pTn5Zu+sW5bqutsCH7D4PkpyAA==", + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json-ref-readers": "1.2.2", + "@stoplight/json-ref-resolver": "~3.1.6", + "@stoplight/spectral-runtime": "^1.1.2", + "dependency-graph": "0.11.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-ref-resolver/node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/@stoplight/spectral-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-runtime/-/spectral-runtime-1.1.4.tgz", + "integrity": "sha512-YHbhX3dqW0do6DhiPSgSGQzr6yQLlWybhKwWx0cqxjMwxej3TqLv3BXMfIUYFKKUqIwH4Q2mV8rrMM8qD2N0rQ==", + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "^3.20.1", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^13.6.0", + "abort-controller": "^3.0.0", + "lodash": "^4.17.21", + "node-fetch": "^2.7.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-runtime/node_modules/@stoplight/types": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.20.0.tgz", + "integrity": "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/types": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", + "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.3.0.tgz", + "integrity": "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==", + "license": "Apache-2.0", + "dependencies": { + "@stoplight/ordered-object-literal": "^1.0.5", + "@stoplight/types": "^14.1.1", + "@stoplight/yaml-ast-parser": "0.0.50", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=10.8" + } + }, + "node_modules/@stoplight/yaml-ast-parser": { + "version": "0.0.50", + "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.50.tgz", + "integrity": "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ==", + "license": "Apache-2.0" + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/@tsconfig/node20": { "version": "20.1.9", "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.9.tgz", @@ -1337,6 +1823,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/es-aggregate-error": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/es-aggregate-error/-/es-aggregate-error-1.0.6.tgz", + "integrity": "sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -1362,7 +1857,8 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/ms": { "version": "2.1.0", @@ -1375,7 +1871,6 @@ "version": "20.19.33", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1388,6 +1883,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/urijs": { + "version": "1.19.26", + "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.26.tgz", + "integrity": "sha512-wkXrVzX5yoqLnndOwFsieJA7oKM8cNkOKJtf/3vVGSUFkWDKZvFHpIl9Pvqb/T9UsawBBFMTTD8xu7sK5MWuvg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", @@ -1433,6 +1934,7 @@ "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", @@ -1801,12 +2303,25 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1841,26 +2356,102 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "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": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/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==", + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, "engines": { - "node": ">=12" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ast-v8-to-istanbul": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", - "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", "dev": true, "license": "MIT", "dependencies": { @@ -1869,11 +2460,43 @@ "js-tokens": "^10.0.0" } }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/before-after-hook": { @@ -1894,13 +2517,65 @@ "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/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -1915,7 +2590,12 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, "node_modules/convert-source-map": { @@ -1958,6 +2638,66 @@ "node": ">= 8" } }, + "node_modules/data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1982,6 +2722,49 @@ "dev": true, "license": "MIT" }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dependency-graph": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", @@ -2025,6 +2808,7 @@ "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -2220,6 +3004,15 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -2251,7 +3044,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-json-stable-stringify": { @@ -2268,6 +3060,28 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-memoize": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", + "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2306,6 +3120,24 @@ "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz", + "integrity": "sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/file-url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/file-url/-/file-url-3.0.0.tgz", + "integrity": "sha512-g872QGsHexznxkIAdK8UiZRe7SkE6kvylShU4Nsj8NvfvZag7S0QuQ4IgvPDkk75HxgjIVDwycFTDAgIiO4nDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2344,6 +3176,35 @@ "dev": true, "license": "ISC" }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2359,96 +3220,696 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", + "node_modules/ftp": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha512-faFVML1aBx2UoDStmLwv2Wptt4vw5x03xxX172nhA5Y5HBshW5JweqQ2W4xL4dezQTG8inJsuYcpPHHU3X5OTQ==", "dependencies": { - "is-glob": "^4.0.3" + "readable-stream": "1.1.x", + "xregexp": "2.0.0" }, "engines": { - "node": ">=10.13.0" + "node": ">=0.8.0" } }, - "node_modules/globals": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", - "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", - "dev": true, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, "engines": { - "node": ">=18" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "license": "MIT", - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, "engines": { - "node": ">= 4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, "engines": { - "node": ">=0.8.19" + "node": ">= 0.4" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "node_modules/get-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-3.0.2.tgz", + "integrity": "sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg==", "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "@tootallnate/once": "1", + "data-uri-to-buffer": "3", + "debug": "4", + "file-uri-to-path": "2", + "fs-extra": "^8.1.0", + "ftp": "^0.3.10" }, "engines": { - "node": ">=0.10.0" + "node": ">= 6" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", + "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, @@ -2510,6 +3971,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbi": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.2.tgz", + "integrity": "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==", + "license": "Apache-2.0" + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2531,6 +3998,48 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpath-plus": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.4.0.tgz", + "integrity": "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==", + "license": "MIT", + "dependencies": { + "@jsep-plugin/assignment": "^1.3.0", + "@jsep-plugin/regex": "^1.0.4", + "jsep": "^1.4.0" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2541,6 +4050,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2832,6 +4350,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash.topath": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", + "integrity": "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg==", + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2881,9 +4411,9 @@ } }, "node_modules/marked": { - "version": "17.0.4", - "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz", - "integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==", + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.3.tgz", + "integrity": "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -2892,6 +4422,15 @@ "node": ">= 20" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -2937,6 +4476,86 @@ "dev": true, "license": "MIT" }, + "node_modules/nimma": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/nimma/-/nimma-0.2.3.tgz", + "integrity": "sha512-1ZOI8J+1PKKGceo/5CT5GfQOG6H8I2BencSK06YarZ2wXwH37BSSUWldqJmMJYA5JfqDqffxDXynt6f11AyKcA==", + "license": "Apache-2.0", + "dependencies": { + "@jsep-plugin/regex": "^1.0.1", + "@jsep-plugin/ternary": "^1.0.2", + "astring": "^1.8.1", + "jsep": "^1.2.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + }, + "optionalDependencies": { + "jsonpath-plus": "^6.0.1 || ^10.1.0", + "lodash.topath": "^4.5.2" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -2976,6 +4595,23 @@ "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3048,6 +4684,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3055,6 +4692,24 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pony-cause": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-1.1.1.tgz", + "integrity": "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g==", + "license": "0BSD", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", @@ -3100,6 +4755,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -3127,14 +4783,99 @@ } } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, + "node_modules/proper-lockfile": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-2.0.1.tgz", + "integrity": "sha512-rjaeGbsmhNDcDInmwi4MuI6mRwJu6zq8GjYCLuSuE7GF+4UjgzkL69sVKKJ2T2xH61kK7rXvGYpvaTu909oXaQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "retry": "^0.10.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", "engines": { - "node": ">=6" + "node": ">=0.10.0" + } + }, + "node_modules/retry": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", + "integrity": "sha512-ZXUSQYTHdl3uS7IuCehYfMzKyIDBNoAuUblvy5oGO5UJSUTmStUUVPXbA9Qxd173Bgre53yCQczQuHgRWAdvJQ==", + "license": "MIT", + "engines": { + "node": "*" } }, "node_modules/rolldown": { @@ -3171,6 +4912,76 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" } }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz", + "integrity": "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -3184,6 +4995,52 @@ "node": ">=10" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3207,6 +5064,78 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -3214,10 +5143,22 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-eval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-eval/-/simple-eval-1.0.1.tgz", + "integrity": "sha512-LH7FpTAkeD+y5xQC4fzS+tFtaNlvt3Ib1zKzvhjv/Y+cioV4zIuw4IZr2yhRLu67CWL7FR9/6KXKnjRoZTvGGQ==", + "license": "MIT", + "dependencies": { + "jsep": "^1.3.6" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/simple-git": { - "version": "3.33.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz", - "integrity": "sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng==", + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.32.2.tgz", + "integrity": "sha512-n/jhNmvYh8dwyfR6idSfpXrFazuyd57jwNMzgjGnKZV/1lTh0HKvPq20v4AQ62rP+l19bWjjXPTCdGHMt0AdrQ==", "license": "MIT", "dependencies": { "@kwsites/file-exists": "^1.1.1", @@ -3253,6 +5194,108 @@ "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "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", @@ -3313,6 +5356,12 @@ "node": ">=14.0.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -3357,12 +5406,87 @@ "node": ">= 0.8.0" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3395,6 +5519,24 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/undici": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", @@ -3409,7 +5551,6 @@ "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" }, "node_modules/universal-user-agent": { @@ -3419,6 +5560,15 @@ "dev": true, "license": "ISC" }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -3429,12 +5579,28 @@ "punycode": "^2.1.0" } }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/vite": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.32.0", @@ -3514,6 +5680,7 @@ "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.1.0", "@vitest/mocker": "4.1.0", @@ -3597,6 +5764,22 @@ "dev": true, "license": "MIT" }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3613,6 +5796,97 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -3647,6 +5921,15 @@ "dev": true, "license": "ISC" }, + "node_modules/xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/.github/package.json b/.github/package.json index 066d5c53c12a..578f4cac6c9a 100644 --- a/.github/package.json +++ b/.github/package.json @@ -7,10 +7,11 @@ }, "dependencies": { "@apidevtools/json-schema-ref-parser": "^15.1.3", + "@js-temporal/polyfill": "0.5.1", "debug": "^4.4.3", "js-yaml": "^4.1.0", - "marked": "^17.0.0", "markdown-table": "^3.0.4", + "marked": "^17.0.0", "simple-git": "^3.27.0", "zod": "^4.3.5" }, diff --git a/.github/workflows/arm-lease-validation.yaml b/.github/workflows/arm-lease-validation.yaml new file mode 100644 index 000000000000..cbb308665dbc --- /dev/null +++ b/.github/workflows/arm-lease-validation.yaml @@ -0,0 +1,42 @@ +name: ARM Lease Validation + +on: + pull_request: + types: + # default + - opened + - synchronize + - reopened + # re-run if base branch is changed, since previous merge commit may generate incorrect diff + - edited + paths: + # Only trigger if PR includes at least one changed ARM lease file + - ".github/arm-leases/**" + # Also trigger on own sources + - ".github/workflows/arm-lease-validation.yaml" + - ".github/workflows/src/arm-lease-validation/**" + +permissions: + contents: read + +jobs: + arm-lease-validation: + name: ARM Lease Validation + runs-on: ubuntu-slim + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 2 + + - name: Install dependencies for github-script actions + uses: ./.github/actions/install-deps-github-script + + - name: ARM Lease Validation + uses: actions/github-script@v8 + with: + script: | + const { default: armLeaseValidation } = + await import('${{ github.workspace }}/.github/workflows/src/arm-lease-validation/arm-lease-validation.js'); + return await armLeaseValidation(core); diff --git a/.github/workflows/arm-modeling-review.yaml b/.github/workflows/arm-modeling-review.yaml new file mode 100644 index 000000000000..afc71456a200 --- /dev/null +++ b/.github/workflows/arm-modeling-review.yaml @@ -0,0 +1,71 @@ +name: ARM Modeling Review + +on: + pull_request: + branches: + - main + - RPSaaSMaster + types: + # default + - opened + - synchronize + - reopened + # re-run if base branch is changed, since previous merge commit may generate incorrect diff + - edited + # re-run when ARMModelingSignedOff is manually added or removed by a reviewer + - labeled + - unlabeled + paths: + - "specification/**/resource-manager/**" + +permissions: + contents: read + +jobs: + arm-modeling-review: + name: ARM Modeling Review + # For code-change events (opened/synchronize/reopened/edited), always run. + # For label events, only run when ARMModelingSignedOff is the label being added or removed, + # since that label represents a manual reviewer sign-off that affects this workflow's output. + if: | + (github.event.action != 'labeled' && github.event.action != 'unlabeled') || + github.event.label.name == 'ARMModelingSignedOff' + runs-on: ubuntu-slim + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 2 + + - name: Install dependencies for github-script actions + uses: ./.github/actions/install-deps-github-script + + - name: ARM Modeling Review + id: detect + uses: actions/github-script@v8 + with: + script: | + const { default: armModelingReview } = + await import('${{ github.workspace }}/.github/workflows/src/arm-modeling-review/arm-modeling-review.js'); + return await armModelingReview({ context, core }); + + - name: Upload artifact for ARMModelingReviewRequired label + if: always() && steps.detect.outputs.result && fromJson(steps.detect.outputs.result).labelActions['ARMModelingReviewRequired'] != 'none' + uses: ./.github/actions/add-label-artifact + with: + name: "ARMModelingReviewRequired" + value: "${{ fromJson(steps.detect.outputs.result).labelActions['ARMModelingReviewRequired'] == 'add' }}" + + - name: Upload artifact for ARMModelingSignedOff label + if: always() && steps.detect.outputs.result && fromJson(steps.detect.outputs.result).labelActions['ARMModelingSignedOff'] != 'none' + uses: ./.github/actions/add-label-artifact + with: + name: "ARMModelingSignedOff" + value: "${{ fromJson(steps.detect.outputs.result).labelActions['ARMModelingSignedOff'] == 'add' }}" + + - name: Upload artifact for ARMModelingAutoSignedOff label + if: always() && steps.detect.outputs.result && fromJson(steps.detect.outputs.result).labelActions['ARMModelingAutoSignedOff'] != 'none' + uses: ./.github/actions/add-label-artifact + with: + name: "ARMModelingAutoSignedOff" + value: "${{ fromJson(steps.detect.outputs.result).labelActions['ARMModelingAutoSignedOff'] == 'add' }}" diff --git a/.github/workflows/src/arm-lease-validation/arm-lease-validation.js b/.github/workflows/src/arm-lease-validation/arm-lease-validation.js new file mode 100644 index 000000000000..224462687c2a --- /dev/null +++ b/.github/workflows/src/arm-lease-validation/arm-lease-validation.js @@ -0,0 +1,373 @@ +import { Temporal } from "@js-temporal/polyfill"; +import { readFile, stat } from "fs/promises"; +import YAML from "js-yaml"; +import { resolve } from "path"; +import { inspect } from "util"; +import * as z from "zod"; +import { getChangedFiles } from "../../../shared/src/changed-files.js"; +import { CoreLogger } from "../core-logger.js"; + +// ============================================ +// Configuration +// ============================================ + +export const LEASE_FILE_PATTERN = /^\.github\/arm-leases\/[a-z0-9-]+\/[a-zA-Z0-9.]+\/lease\.yaml$/; +export const LEASE_FILE_WITH_GROUP_PATTERN = + /^\.github\/arm-leases\/[a-z0-9-]+\/[a-zA-Z0-9.]+\/(?!stable|preview)([^/]+)\/lease\.yaml$/; + +export const ALLOWED_FILE_PATTERNS = [ + LEASE_FILE_PATTERN, + LEASE_FILE_WITH_GROUP_PATTERN, + /^\.github\/arm-leases\/README\.md$/, +]; + +/** + * Zod schema for lease.yaml file content. + * + * Example: + * ```yaml + * lease: + * resource-provider: Microsoft.Compute + * startdate: "2025-06-01" + * duration: "P180D" + * reviewer: "@githubUser" + * ``` + */ +export const leaseSchema = z.object({ + lease: z + .object({ + "resource-provider": z + .string() + .min(1, "resource-provider is required") + .refine( + (rp) => rp.split(".").every((part) => /^[A-Z]/.test(part)), + "Resource provider parts must start with a capital letter (e.g., Microsoft.Test, Azure.Widget)", + ), + startdate: z + .string() + .regex(/^\d{4}-\d{2}-\d{2}$/, "Invalid startdate format (expected: YYYY-MM-DD)") + .refine((value) => { + try { + Temporal.PlainDate.from(value); + return true; + } catch { + return false; + } + }, "startdate must be a valid calendar date"), + duration: z.string().refine((v) => { + try { + Temporal.Duration.from(v); + return true; + } catch { + return false; + } + }, "duration must be a valid ISO 8601 duration (e.g. P180D, P6M)"), + reviewer: z + .string() + .min(1, "Reviewer is required and cannot be empty") + .refine( + (r) => r.startsWith("@") && r.trim().length > 1, + "Reviewer must be a GitHub alias starting with @ (e.g., @githubUser)", + ), + }) + .superRefine((lease, ctx) => { + try { + const start = Temporal.PlainDate.from(lease.startdate); + const duration = Temporal.Duration.from(lease.duration); + const totalDays = duration.total({ unit: "days", relativeTo: start }); + if (totalDays > 180) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["duration"], + message: "duration must not exceed 180 days relative to startdate (e.g. P180D, P6M)", + }); + } + } catch { + // If parsing fails, field-level validators will report errors. + } + }), +}); + +// ============================================ +// Utility Functions +// ============================================ + +/** + * Check if a file is allowed based on patterns. + * @param {string} file - File path to check + * @returns {boolean} True if file is allowed + */ +export function isFileAllowed(file) { + return ALLOWED_FILE_PATTERNS.some((pattern) => pattern.test(file)); +} + +/** + * Validate folder structure of lease files. + * @param {string[]} files - Array of file paths + * @returns {string[]} Array of invalid files + */ +export function validateFolderStructure(files) { + return files.filter( + (file) => !LEASE_FILE_PATTERN.test(file) && !LEASE_FILE_WITH_GROUP_PATTERN.test(file), + ); +} + +/** + * Validate lease file contents using Zod schema. + * @param {string} leaseFile - Full path to lease file + * @param {string} relativePath - Relative path for folder name extraction + * @param {string} [workspaceRoot] - Workspace root for specification folder validation + * @returns {Promise<{file: string, errors: string[]}>} Validation result with errors array + */ +export async function validateLeaseContent(leaseFile, relativePath, workspaceRoot) { + const errors = []; + // Extract orgName and rpNamespace from .github/arm-leases///lease.yaml + // or .github/arm-leases////lease.yaml + const pathParts = (relativePath || leaseFile).split("/"); + const orgName = pathParts[2]; // orgName is always at index 2 + const folderRP = pathParts[3]; // rpNamespace is always at index 3 + + /** @type {string} */ + let content; + try { + content = await readFile(leaseFile, "utf-8"); + } catch (error) { + return { file: leaseFile, errors: [`Error reading file: ${inspect(error)}`] }; + } + + // Use FAILSAFE_SCHEMA to keep all values as strings (prevents YAML Date auto-parsing) + let raw; + try { + raw = /** @type {any} */ (YAML.load(content, { schema: YAML.FAILSAFE_SCHEMA })); + } catch (error) { + return { file: leaseFile, errors: [`Invalid YAML: ${inspect(error)}`] }; + } + + // Parse with Zod schema — collects all field-level errors at once + const result = leaseSchema.safeParse(raw); + if (!result.success) { + for (const issue of result.error.issues) { + errors.push(issue.message); + } + return { file: leaseFile, errors }; + } + + const lease = result.data.lease; + + // Cross-field validation: resource-provider must match folder name + if (lease["resource-provider"] !== folderRP) { + errors.push( + `Resource provider mismatch: folder=${folderRP}, yaml=${lease["resource-provider"]}`, + ); + } + + // Cross-field validation: startdate must not be more than 10 days in the past + const today = Temporal.Now.plainDateISO(); + if ( + Temporal.PlainDate.compare( + Temporal.PlainDate.from(lease.startdate), + today.subtract({ days: 10 }), + ) < 0 + ) { + errors.push( + `Startdate is in the past: ${lease.startdate} (must be within 10 days of today: ${today.toString()})`, + ); + } + + // Validate specification folder structure if workspace root is provided + if (workspaceRoot) { + // First check if the service folder exists in specification/ + let serviceExists = false; + + try { + if (!(await stat(resolve(workspaceRoot, "specification", orgName))).isDirectory()) { + errors.push( + `Service folder is not a directory: specification/${orgName}. Use a valid service name from specification/ folder.`, + ); + } else { + serviceExists = true; + } + } catch { + // Service folder doesn't exist - skip validation (new RP with no specs yet) + } + + // Then check if resource-manager// exists (skip if new RP or service doesn't exist) + if (serviceExists) { + try { + if ( + !( + await stat( + resolve(workspaceRoot, "specification", orgName, "resource-manager", folderRP), + ) + ).isDirectory() + ) { + errors.push( + `Specification path exists but is not a directory: specification/${orgName}/resource-manager/${folderRP}`, + ); + } + // Directory exists and matches - validation passes + } catch { + // Directory doesn't exist - skip validation (new RP being registered) + } + } + } + + return { file: leaseFile, errors }; +} + +// ============================================ +// Main Validation Logic +// ============================================ + +/** + * Main validation logic for GitHub script action. + * @param {import('@actions/github-script').AsyncFunctionArguments['core']} core + * @returns {Promise<{ status: string, errors: number }>} Validation result + */ +export default async function validateArmLeases(core) { + const cwd = process.env.GITHUB_WORKSPACE || process.cwd(); + let hasErrors = false; + + core.info("Running ARM Lease File Validation"); + + // Get all changed files under .github/arm-leases/ + const allChangedFiles = await getChangedFiles({ + cwd, + paths: [".github/arm-leases"], + logger: new CoreLogger(core), + }); + + // Check for disallowed files + core.startGroup("Checking for disallowed files"); + const disallowedFiles = allChangedFiles.filter((file) => !isFileAllowed(file)); + + if (disallowedFiles.length > 0) { + core.info( + `Found ${disallowedFiles.length} disallowed file(s). Only lease.yaml and README.md files within .github/arm-leases/ are allowed:`, + ); + for (const file of disallowedFiles.slice(0, 20)) { + core.info(` ${file}`); + } + if (disallowedFiles.length > 20) { + core.info(` ... and ${disallowedFiles.length - 20} more files`); + } + hasErrors = true; + } else { + core.info("No disallowed files found"); + } + core.endGroup(); + + // Check for non-lease.yaml and non-README files + core.startGroup("Checking for non-lease files"); + const nonLeaseFiles = allChangedFiles.filter( + (file) => !file.endsWith("/lease.yaml") && !file.endsWith("/README.md"), + ); + + if (nonLeaseFiles.length > 0) { + core.info(`Found ${nonLeaseFiles.length} file(s) that are not lease.yaml:`); + for (const file of nonLeaseFiles) { + core.info(`Remove or rename - ${file}`); + } + core.info("Only lease.yaml files are allowed in .github/arm-leases/ directory"); + hasErrors = true; + } else { + core.info("All files are valid lease.yaml or README.md files"); + } + core.endGroup(); + + // Get ARM lease files (only lease.yaml files) + const armLeaseFiles = allChangedFiles.filter( + (file) => file.startsWith(".github/arm-leases/") && !file.endsWith(".md"), + ); + + if (armLeaseFiles.length === 0) { + if (!hasErrors) { + core.info("No ARM lease files to validate"); + } else { + core.setFailed("ARM Lease Validation failed - fix errors above"); + } + return { status: hasErrors ? "failed" : "no-lease-files", errors: hasErrors ? 1 : 0 }; + } + + // Validate folder structure + core.startGroup("Validating folder structure"); + const invalidStructure = validateFolderStructure(armLeaseFiles); + + if (invalidStructure.length > 0) { + core.info(`${invalidStructure.length} file(s) with invalid folder structure:`); + for (const file of invalidStructure) { + core.info(` ${file}`); + } + core.info( + "Expected format: .github/arm-leases///[ (optional)]/lease.yaml", + ); + core.info("Requirements:"); + core.info(" - : lowercase alphanumeric only (e.g., testservice, widgetservice)"); + core.info( + " - : alphanumeric with dots and case-sensitive (e.g., Test.Rp, Widget.Manager)", + ); + core.info( + ' - : (optional) customer-facing service name within an RP (e.g., DiskRP, ComputeRP). Must not start with "stable" or "preview"', + ); + core.info(" - Only lease.yaml files are allowed in arm-leases folder"); + core.info("Examples:"); + core.info(" - .github/arm-leases/testservice/Test.Rp/lease.yaml"); + core.info(" - .github/arm-leases/widgetservice/Widget.Manager/lease.yaml"); + core.info(" - .github/arm-leases/compute/Microsoft.Compute/DiskRP/lease.yaml"); + hasErrors = true; + } else { + core.info(`All ${armLeaseFiles.length} lease file(s) have valid folder structure`); + } + core.endGroup(); + + // Validate lease file contents + core.startGroup("Validating lease file contents"); + const validLeaseFiles = armLeaseFiles.filter( + (file) => LEASE_FILE_PATTERN.test(file) || LEASE_FILE_WITH_GROUP_PATTERN.test(file), + ); + + if (validLeaseFiles.length === 0) { + core.endGroup(); + if (hasErrors) { + core.setFailed("ARM Lease Validation failed - fix errors above"); + } else { + core.info("All validations passed!"); + } + return { status: hasErrors ? "failed" : "passed", errors: hasErrors ? 1 : 0 }; + } + + const contentErrors = []; + + for (const leaseFile of validLeaseFiles) { + const fullPath = resolve(cwd, leaseFile); + const result = await validateLeaseContent(fullPath, leaseFile, cwd); + if (result.errors.length > 0) { + contentErrors.push({ file: leaseFile, errors: result.errors }); + hasErrors = true; + } + } + + if (contentErrors.length > 0) { + core.info("Lease content validation errors:"); + for (const { file, errors } of contentErrors) { + core.info(`${file}`); + for (const error of errors) { + core.info(` - ${error}`); + } + } + } else { + core.info(`All ${validLeaseFiles.length} lease file(s) passed content validation`); + for (const file of validLeaseFiles) { + core.info(` - ${file}`); + } + } + core.endGroup(); + + if (hasErrors) { + core.setFailed("ARM Lease Validation failed - fix errors above"); + } else { + core.info("All validations passed!"); + } + + return { status: hasErrors ? "failed" : "passed", errors: contentErrors.length }; +} diff --git a/.github/workflows/src/arm-modeling-review/arm-lease-validation-labels.js b/.github/workflows/src/arm-modeling-review/arm-lease-validation-labels.js new file mode 100644 index 000000000000..1b3ccb1b8c45 --- /dev/null +++ b/.github/workflows/src/arm-modeling-review/arm-lease-validation-labels.js @@ -0,0 +1,10 @@ +/** + * ARM lease validation label names. + * @readonly + * @enum {string} + */ +export const ArmLeaseValidationLabel = Object.freeze({ + ArmModelingReviewRequired: "ARMModelingReviewRequired", + ArmModelingSignedOff: "ARMModelingSignedOff", + ArmModelingAutoSignedOff: "ARMModelingAutoSignedOff", +}); diff --git a/.github/workflows/src/arm-modeling-review/arm-modeling-review.js b/.github/workflows/src/arm-modeling-review/arm-modeling-review.js new file mode 100644 index 000000000000..89ba1b3124a2 --- /dev/null +++ b/.github/workflows/src/arm-modeling-review/arm-modeling-review.js @@ -0,0 +1,328 @@ +import { simpleGit } from "simple-git"; +import { getChangedFiles, resourceManager, swagger } from "../../../shared/src/changed-files.js"; +import { CoreLogger } from "../core-logger.js"; +import { LabelAction } from "../label.js"; +import { ArmLeaseValidationLabel } from "./arm-lease-validation-labels.js"; +import { checkLease } from "./detect-arm-leases.js"; +import { detectNewResourceTypes } from "./detect-new-resource-types.js"; + +const ARM_OFFICE_HOURS_URL = + "https://outlook.office365.com/book/ARMOfficeHours1@microsoft.onmicrosoft.com/?ismsaljsauthenabled=true"; + +// Match pattern: specification//resource-manager//... +// Trailing slash ensures the match is a directory component, not a file like readme.md +const RESOURCE_MANAGER_PATTERN = new RegExp( + String.raw`^specification/[^/]+/resource-manager/([^/]+)/`, +); + +// Match pattern with optional service name: specification//resource-manager///... +// ServiceName folder name should not start with "stable" or "preview" +const RESOURCE_MANAGER_WITH_GROUP_PATTERN = new RegExp( + String.raw`^specification/[^/]+/resource-manager/([^/]+)/(?!stable|preview)([^/]+)/`, +); + +/** + * The workflow contract is intentionally a fixed set of keys. + * @typedef {{ + * "ARMModelingReviewRequired": LabelAction, + * "ARMModelingSignedOff": LabelAction, + * "ARMModelingAutoSignedOff": LabelAction, + * }} ManagedLabelActions + */ + +/** + * Check if a resource provider namespace existed in a specific git commit. + * Uses git ls-tree to check the base commit, avoiding false positives from + * RPs that were just added in the current PR. + * + * @param {import('simple-git').SimpleGit} git + * @param {string} commitish - Git commit reference (e.g., merge base SHA) + * @param {string} namespace - Resource provider namespace (e.g., Microsoft.App) + * @param {import('@actions/github-script').AsyncFunctionArguments['core']} core + * @returns {Promise} True if namespace existed in the commit + */ +async function resourceProviderExistsInCommit(git, commitish, namespace, core) { + /** @type {string} */ + let output; + try { + output = await git.raw(["ls-tree", "-d", "--name-only", "-r", commitish, "specification/"]); + } catch (e) { + if (e instanceof Error && e.message.includes("does not exist")) { + core.info(`Commit "${commitish}" does not exist in git history`); + return false; + } + throw e; + } + + // Look for resource-manager/ pattern in any service directory + const pattern = new RegExp( + `^specification/[^/]+/resource-manager/${namespace.replace(".", "\\.")}$`, + "m", + ); + return pattern.test(output); +} + +/** + * @param {"none" | "review-required" | "auto-signed-off"} outcome + * @returns {ManagedLabelActions} + */ +function getLabelActions(outcome) { + if (outcome === "review-required") { + return { + [ArmLeaseValidationLabel.ArmModelingReviewRequired]: LabelAction.Add, + [ArmLeaseValidationLabel.ArmModelingSignedOff]: LabelAction.Remove, + [ArmLeaseValidationLabel.ArmModelingAutoSignedOff]: LabelAction.Remove, + }; + } + + if (outcome === "auto-signed-off") { + return { + [ArmLeaseValidationLabel.ArmModelingReviewRequired]: LabelAction.Remove, + [ArmLeaseValidationLabel.ArmModelingSignedOff]: LabelAction.Remove, + [ArmLeaseValidationLabel.ArmModelingAutoSignedOff]: LabelAction.Add, + }; + } + + // outcome === "none" + return { + [ArmLeaseValidationLabel.ArmModelingReviewRequired]: LabelAction.Remove, + [ArmLeaseValidationLabel.ArmModelingSignedOff]: LabelAction.Remove, + [ArmLeaseValidationLabel.ArmModelingAutoSignedOff]: LabelAction.Remove, + }; +} + +/** + * Extract resource provider namespaces, organization names, and optional service names from file paths. + * + * @param {string[]} files + * @returns {Map} Map of namespace to service info + */ +function extractResourceProviders(files) { + /** @type {Map} */ + const resourceProviders = new Map(); + + for (const file of files) { + const match = file.match(RESOURCE_MANAGER_PATTERN); + if (match) { + const orgName = file.split("/")[1]; + const namespace = match[1]; + + const groupMatch = file.match(RESOURCE_MANAGER_WITH_GROUP_PATTERN); + if (groupMatch) { + resourceProviders.set(namespace, { orgName, serviceName: groupMatch[2] }); + } else { + resourceProviders.set(namespace, { orgName }); + } + } + } + + return resourceProviders; +} + +/** + * Main detection logic for GitHub script action. + * + * @param {Object} params - Parameters from github-script + * @param {import('@actions/github-script').AsyncFunctionArguments['core']} params.core + * @returns {Promise<{ status: string, labelActions: ManagedLabelActions }>} + */ +export default async function armModelingReview({ core }) { + const options = { + cwd: process.env.GITHUB_WORKSPACE, + paths: ["specification"], + logger: new CoreLogger(core), + }; + + const git = simpleGit(options.cwd); + + core.info("Detecting New Resource Providers"); + + const changedFiles = await getChangedFiles(options); + const rmFiles = changedFiles.filter(resourceManager); + + const changedResourceProviders = extractResourceProviders(rmFiles); + + // Pre-check: verify if any namespace directories are brand new (don't exist in base branch). + // Extract unique namespace paths directly from the changed files using the regex pattern. + const changedNamespacePaths = new Set( + rmFiles + .map((f) => { + const match = f.match(RESOURCE_MANAGER_PATTERN); + if (match) { + // Extract path up to and including the namespace: specification//resource-manager/ + // match[0] includes trailing slash, so strip it + return f.substring(0, /** @type {number} */ (match.index) + match[0].length - 1); + } + return null; + }) + .filter((p) => p !== null), + ); + + if (changedNamespacePaths.size > 0) { + let hasAtLeastOneBrandNewRP = false; + + for (const namespacePath of changedNamespacePaths) { + /** @type {string} */ + let specFilesBaseBranch; + try { + specFilesBaseBranch = await git.raw([ + "ls-tree", + "-r", + "--name-only", + "HEAD^", + namespacePath, + ]); + } catch (e) { + if (e instanceof Error && e.message.includes("does not exist")) { + core.info(`Path "${namespacePath}" does not exist in base branch`); + hasAtLeastOneBrandNewRP = true; + continue; + } + throw e; + } + + const specRmSwaggerFilesBaseBranch = specFilesBaseBranch + .split("\n") + .filter((file) => resourceManager(file) && swagger(file)); + + if (specRmSwaggerFilesBaseBranch.length === 0) { + hasAtLeastOneBrandNewRP = true; + } + } + + if (!hasAtLeastOneBrandNewRP) { + core.info("No brand new resource providers detected, spec directories exist in base branch."); + core.info("Checking for new resource types in existing RPs..."); + return await checkNewResourceTypes(rmFiles, core); + } + } + + // Filter for new resource providers (not present in base branch) + const newResourceProviders = []; + + for (const [rp, info] of changedResourceProviders) { + const existsInBase = await resourceProviderExistsInCommit(git, "HEAD^", rp, core); + if (!existsInBase) { + newResourceProviders.push({ + namespace: rp, + orgName: info.orgName, + serviceName: info.serviceName, + }); + } + } + + if (newResourceProviders.length === 0) { + core.info("No new resource providers detected."); + core.info("Checking for new resource types in existing RPs..."); + return await checkNewResourceTypes(rmFiles, core); + } + + core.info(`Detected ${newResourceProviders.length} new resource provider(s)`); + + /** @type {{ namespace: string, orgName: string, serviceName?: string, leaseValid: boolean, leaseMessage: string }[]} */ + const leaseCheckResults = []; + + for (const rp of newResourceProviders) { + const leaseValid = await checkLease(rp.orgName, rp.namespace, rp.serviceName || ""); + + leaseCheckResults.push({ + namespace: rp.namespace, + orgName: rp.orgName, + serviceName: rp.serviceName, + leaseValid, + leaseMessage: leaseValid ? "Lease is valid" : "No lease file found or lease has expired", + }); + + core.info(` - ${rp.namespace}: ${leaseValid ? "Lease valid" : "Lease invalid"}`); + } + + const invalidLeases = leaseCheckResults.filter((rp) => !rp.leaseValid); + const allLeasesValid = invalidLeases.length === 0; + + if (!allLeasesValid) { + for (const rp of invalidLeases) { + core.error(`${rp.namespace}: ${rp.leaseMessage}`); + } + core.setFailed( + `${invalidLeases.length} new resource provider(s) detected without a valid ARM lease. ` + + `Please schedule a discussion at ARM API Modeling Office Hours before merging: ${ARM_OFFICE_HOURS_URL}`, + ); + } else { + core.info("New resource provider(s) detected with valid ARM lease — no action required."); + } + + core.info("Checking for new resource types in existing RPs..."); + const newRtResult = await checkNewResourceTypes(rmFiles, core); + + // Merge label actions: 'add' wins over 'remove' wins over 'none' + /** @type {ManagedLabelActions} */ + const combinedLabelActions = { + ...getLabelActions(allLeasesValid ? "auto-signed-off" : "review-required"), + }; + for (const [labelKey, action] of Object.entries(newRtResult.labelActions)) { + /** @type {keyof ManagedLabelActions} */ + const label = /** @type {keyof ManagedLabelActions} */ (labelKey); + const currentAction = combinedLabelActions[label]; + if ( + action === LabelAction.Add || + (action === LabelAction.Remove && currentAction === LabelAction.None) + ) { + combinedLabelActions[label] = action; + } + } + + return { + status: allLeasesValid ? "new-rp-all-leases-valid" : "new-rp-invalid-lease", + labelActions: combinedLabelActions, + }; +} + +/** + * Check for new resource types in existing RPs and validate their leases. + * + * @param {string[]} rmFiles - Resource-manager file paths changed in the PR + * @param {import('@actions/github-script').AsyncFunctionArguments['core']} core + * @returns {Promise<{ status: string, labelActions: ManagedLabelActions }>} + */ +async function checkNewResourceTypes(rmFiles, core) { + const newRtResults = await detectNewResourceTypes({ + rmFiles, + core, + }); + + if (newRtResults.length === 0) { + core.info("No new resource types detected."); + return { + status: "no-new-rp", + labelActions: getLabelActions("none"), + }; + } + + core.info(`Detected new resource types in ${newRtResults.length} rpNamespace(s)`); + + let allLeasesValid = true; + for (const ns of newRtResults) { + const leaseValid = await checkLease(ns.orgName, ns.rpNamespace, ns.serviceName); + + if (leaseValid) { + core.info(` - ${ns.rpNamespace}: valid ARM lease for new resource types`); + } else { + allLeasesValid = false; + core.error(`${ns.rpNamespace}: new resource types detected without a valid ARM lease`); + } + } + + if (!allLeasesValid) { + core.setFailed( + "New resource types detected without a valid ARM lease. " + + `Please schedule a discussion at ARM API Modeling Office Hours before merging: ${ARM_OFFICE_HOURS_URL}`, + ); + } else { + core.info("New resource types detected with valid ARM lease — auto-signed-off."); + } + + return { + status: allLeasesValid ? "new-rt-all-leases-valid" : "new-rt-invalid-lease", + labelActions: getLabelActions(allLeasesValid ? "auto-signed-off" : "review-required"), + }; +} diff --git a/.github/workflows/src/arm-modeling-review/detect-arm-leases.js b/.github/workflows/src/arm-modeling-review/detect-arm-leases.js new file mode 100644 index 000000000000..52170522cf5d --- /dev/null +++ b/.github/workflows/src/arm-modeling-review/detect-arm-leases.js @@ -0,0 +1,112 @@ +import { Temporal } from "@js-temporal/polyfill"; +import { readFile } from "fs/promises"; +import yaml from "js-yaml"; +import { resolve } from "path"; +import * as z from "zod"; +import { getRootFolder } from "../../../shared/src/simple-git.js"; + +/** + * Schema for lease.yaml file + * + * Example: + * ```yaml + * lease: + * startdate: "2024-01-01" + * duration: "P180D" + * ``` + */ +const leaseSchema = z.object({ + lease: z.object({ + startdate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "startdate must be in YYYY-MM-DD format"), + duration: z.string().refine((v) => { + try { + Temporal.Duration.from(v); + return true; + } catch { + return false; + } + }, "duration must be a valid ISO 8601 duration (e.g. P180D, P6M, P1Y2M3D)"), + }), +}); + +/** + * Build the lease path based on service information. + * + * Lease files are stored at: + * - Without service name: `.github/arm-leases///lease.yaml` + * - With service name: `.github/arm-leases////lease.yaml` + * + * @param {string} repoRoot - Repository root path + * @param {string} orgName - Organization name (e.g., "compute") + * @param {string} rpNamespace - Resource provider namespace (e.g., "Microsoft.Compute") + * @param {string} serviceName - Optional service name for RPs with sub-groupings (e.g., "ComputeRP") + * @returns {string} Full path to lease.yaml file + */ +function buildLeasePath(repoRoot, orgName, rpNamespace, serviceName = "") { + const leasePathParts = [repoRoot, ".github", "arm-leases", orgName, rpNamespace]; + if (serviceName) { + leasePathParts.push(serviceName); + } + leasePathParts.push("lease.yaml"); + return resolve(...leasePathParts); +} + +/** + * Parse and validate lease YAML content. Pure function — no I/O. + * + * @param {string} content - Raw YAML string from a lease file + * @returns {{ valid: boolean, reason: string }} Whether the lease is valid and why + */ +export function parseLease(content) { + let rawParsed; + try { + rawParsed = /** @type {any} */ (yaml.load(content, { schema: yaml.FAILSAFE_SCHEMA })); + } catch { + return { valid: false, reason: "YAML parse error" }; + } + + if (!rawParsed) { + return { valid: false, reason: "Empty YAML content" }; + } + + const result = leaseSchema.safeParse(rawParsed); + if (!result.success) { + return { valid: false, reason: result.error.issues.map((i) => i.message).join("; ") }; + } + + const lease = result.data.lease; + const startDate = Temporal.PlainDate.from(lease.startdate); + const duration = Temporal.Duration.from(lease.duration); + const endDate = startDate.add(duration); + const today = Temporal.Now.plainDateISO(); + + if (Temporal.PlainDate.compare(today, endDate) > 0) { + return { valid: false, reason: `Lease expired on ${endDate.toString()}` }; + } + + return { valid: true, reason: "Lease is valid" }; +} + +/** + * Check if ARM lease exists and is valid. + * + * Looks for a lease file at the appropriate path (see buildLeasePath for path structure). + * + * @param {string} orgName - Organization name (e.g., "compute") + * @param {string} rpNamespace - Resource provider namespace (e.g., "Microsoft.Compute") + * @param {string} serviceName - Optional service name for RPs with sub-groupings + * @returns {Promise} True if lease exists and is valid, false otherwise + */ +export async function checkLease(orgName, rpNamespace, serviceName = "") { + const repoRoot = await getRootFolder(process.cwd()); + const leasePath = buildLeasePath(repoRoot, orgName, rpNamespace, serviceName); + + let content; + try { + content = await readFile(leasePath, "utf-8"); + } catch { + return false; + } + + return parseLease(content).valid; +} diff --git a/.github/workflows/src/arm-modeling-review/detect-new-resource-types.js b/.github/workflows/src/arm-modeling-review/detect-new-resource-types.js new file mode 100644 index 000000000000..15a71f2b2221 --- /dev/null +++ b/.github/workflows/src/arm-modeling-review/detect-new-resource-types.js @@ -0,0 +1,255 @@ +/** + * Detect new ARM resource types in PRs by parsing swagger paths. + * + * Compares resource types between the base branch and HEAD for existing RPs + * to identify newly introduced resource types that require ARM lease validation. + */ + +import { simpleGit } from "simple-git"; + +// Match pattern: specification//resource-manager//... +const RESOURCE_MANAGER_PATTERN = new RegExp( + String.raw`^specification/[^/]+/resource-manager/([^/]+)/`, +); + +const RESOURCE_TYPE_REGEX = new RegExp(String.raw`/providers/([^/?#]+)(/[^?#]*)?`); + +/** + * Extract resource type from API path (e.g. Microsoft.Compute/virtualMachines/extensions) + * @param {string} apiPath + * @returns {string | null} + */ +function getResourceType(apiPath) { + const match = apiPath.match(RESOURCE_TYPE_REGEX); + if (!match) return null; + + // Split and filter out parameter segments like {vmName} to get the static resource type + const provider = /** @type {string} */ (match[1]); + const typeHierarchy = /** @type {string | undefined} */ (match[2]); + if (!typeHierarchy) return provider; + + // typeHierarchy starts with "/" (e.g. "/superDisks/{diskName}"). + // Filter out empty segments and path parameters, then re-join with "/" preserving the leading slash. + const staticSegments = typeHierarchy + .split("/") + .filter((segment) => segment && !segment.startsWith("{")); + + if (staticSegments.length === 0) return provider; + + return provider + "/" + staticSegments.join("/"); +} + +/** + * Get all ARM resource types from a swagger document by parsing paths. + * + * Scans paths for ARM resource patterns like /providers/Microsoft.X/resourceTypes. + * + * @param {{paths?: Record>}} swaggerDoc - Parsed swagger JSON document + * @returns {Map}>} + */ +function getResourceTypesFromSwagger(swaggerDoc) { + /** @type {Map} */ + const resourceTypes = new Map(); + + if (!swaggerDoc.paths) { + return resourceTypes; + } + + const paths = swaggerDoc.paths; + for (const [apiPath, pathItem] of Object.entries(paths)) { + const resourceType = getResourceType(apiPath); + if (!resourceType || !resourceType.includes("/")) continue; + + const parts = resourceType.split("/"); + // Skip operations-only paths (e.g. Microsoft.Compute/operations) + if (parts[parts.length - 1].toLowerCase() === "operations") continue; + + /** @type {Array<{method: string, apiPath: string}>} */ + const ops = Object.entries(pathItem) + .filter(([method]) => + ["get", "put", "post", "patch", "delete"].includes(method.toLowerCase()), + ) + .map(([method]) => ({ method: method.toUpperCase(), apiPath })); + + if (ops.length === 0) continue; + + if (!resourceTypes.has(resourceType)) { + resourceTypes.set(resourceType, { + resourceType, + provider: parts[0], + modelName: null, + operations: ops, + }); + } else { + const existing = /** @type {{operations: Array<{method: string, apiPath: string}>}} */ ( + resourceTypes.get(resourceType) + ); + for (const op of ops) { + if (!existing.operations.some((e) => e.method === op.method && e.apiPath === op.apiPath)) { + existing.operations.push(op); + } + } + } + } + + return resourceTypes; +} + +/** + * Get resource types from swagger files at a specific git ref. + * + * @param {import("simple-git").SimpleGit} git + * @param {string} gitRef - Git ref (e.g. "HEAD" or "HEAD^") + * @param {string} specPath - e.g. `specification/compute/resource-manager/Microsoft.Compute` + * @param {string} branchName - Branch name for logging (e.g. "base" or "head") + * @returns {Promise | null>} Map of resource type to info, or null if path doesn't exist at this ref + */ +async function getResourceTypesAtRef(git, gitRef, specPath, branchName) { + /** @type {Map} */ + const allTypes = new Map(); + + let output; + try { + output = await git.raw(["ls-tree", "-r", "--name-only", gitRef, specPath]); + } catch { + return null; // path doesn't exist at this ref + } + + const swaggerFiles = output + .split("\n") + .filter((f) => f.trim() && f.endsWith(".json") && !f.includes("/examples/")) + .filter((f, index, self) => self.indexOf(f) === index); // deduplicate + + const total = swaggerFiles.length; + if (total > 0) { + console.log( + `Analyzing ${total} swagger files in ${branchName} branch (${gitRef}) for comparison...`, + ); + } + + // Iterate over files, but only log summary if it's too much + for (let i = 0; i < swaggerFiles.length; i++) { + const file = swaggerFiles[i]; + if (total > 10 && i % 20 === 0 && i > 0) { + console.log(`Progress: ${i}/${total} files processed...`); + } + + let content; + try { + content = await git.show([`${gitRef}:${file.trim()}`]); + } catch { + // Intentionally quiet to avoid log spam during comparison + continue; + } + + try { + const parsed = /** @type {unknown} */ (JSON.parse(content)); + const swaggerDoc = /** @type {{paths?: Record>}} */ (parsed); + // Skip files that aren't ARM resource-manager specs (minimal check) + if (!swaggerDoc.paths) continue; + const types = getResourceTypesFromSwagger(swaggerDoc); + for (const [type, info] of types) { + if (!allTypes.has(type)) { + allTypes.set(type, info); + } + } + } catch { + // skip un-parseable files + } + } + + return allTypes; +} + +/** + * Detect new resource types introduced in a PR for existing RPs. + * + * For each namespace that already exists in the base branch, compares the set + * of ARM resource types between base and HEAD using ArmHelper. Any resource type + * present in HEAD but absent from base is new. + * + * @param {Object} params + * @param {string[]} params.rmFiles - Resource-manager file paths changed in the PR + * @param {import("@actions/core")} params.core - GitHub Actions core for logging + * @returns {Promise}>>} + */ +export async function detectNewResourceTypes({ rmFiles, core }) { + const git = simpleGit(process.env.GITHUB_WORKSPACE); + + // Group changed RM swagger files by service subdirectory (rpNamespace + serviceName) + /** @type {Map} */ + const namespaceMap = new Map(); + + for (const file of rmFiles) { + // Use RESOURCE_MANAGER_PATTERN to ensure the file is inside a rpNamespace directory + // (the trailing "/" in the pattern requires at least one path component after the rpNamespace) + const rmMatch = file.match(RESOURCE_MANAGER_PATTERN); + if (!rmMatch) continue; + + const parts = file.split("/"); + const orgName = parts[1]; + const rpNamespace = parts[3]; + // Check if parts[4] is an actual service name (e.g. DiskRP) vs lifecycle folder (stable/preview) + // If it's stable/preview, compare at rpNamespace root to avoid false negatives when + // introducing the first preview/stable folder under an existing rpNamespace + const serviceName = parts[4]; + const isLifecycleFolder = serviceName === "stable" || serviceName === "preview"; + const specPath = isLifecycleFolder + ? `specification/${orgName}/resource-manager/${rpNamespace}` + : `specification/${orgName}/resource-manager/${rpNamespace}/${serviceName}`; + const serviceKey = isLifecycleFolder ? rpNamespace : `${rpNamespace}/${serviceName}`; + // Store actual serviceName (empty string if it's a lifecycle folder like stable/preview) + const actualServiceName = isLifecycleFolder ? "" : serviceName; + + if (!namespaceMap.has(serviceKey)) { + namespaceMap.set(serviceKey, { + orgName, + specPath, + rpNamespace, + serviceName: actualServiceName, + }); + } + } + + const results = []; + + for (const [serviceKey, { orgName, specPath, rpNamespace, serviceName }] of namespaceMap) { + core.info(`Checking for new resource types in ${serviceKey}...`); + + const baseTypes = await getResourceTypesAtRef(git, "HEAD^", specPath, "base"); + + // Skip rpNamespace if it doesn't exist in base (brand new RP — handled by RP-level detection) + if (baseTypes === null) { + core.info(` ${rpNamespace}: no resources in base (new rpNamespace), skipping RT detection`); + continue; + } + + const headTypes = await getResourceTypesAtRef(git, "HEAD", specPath, "head"); + + const newTypes = []; + /** @type {Map} */ + const typedHeadTypes = headTypes ?? new Map(); + for (const [type, info] of typedHeadTypes) { + if (!baseTypes.has(type)) { + newTypes.push({ + resourceType: type, + provider: info.provider, + modelName: info.modelName, + operations: info.operations.map((op) => op.method), + }); + } + } + + if (newTypes.length > 0) { + core.info(` ${rpNamespace}: ${newTypes.length} new resource type(s) detected`); + for (const t of newTypes) { + core.info(` - ${t.resourceType} (${t.operations.join(", ")})`); + } + results.push({ rpNamespace, orgName, serviceName, newResourceTypes: newTypes }); + } else { + core.info(` ${rpNamespace}: no new resource types`); + } + } + + return results; +} diff --git a/.github/workflows/src/summarize-checks/labelling.js b/.github/workflows/src/summarize-checks/labelling.js index ed0646ce8fa8..566289daaf91 100644 --- a/.github/workflows/src/summarize-checks/labelling.js +++ b/.github/workflows/src/summarize-checks/labelling.js @@ -651,7 +651,13 @@ function processARMReviewWorkflowLabels( ciRpaasRPNotInPrivateRepoLabelShouldBePresent, ); - const blocked = blockedOnRpaas || blockedOnVersioningPolicy; + // Block if ARMModelingReviewRequired is present (new RP namespace or new resource type detected) + const armModelingReviewLabel = new Label("ARMModelingReviewRequired", labelContext.present); + // Block if the label is already present or if it's being added by another check (e.g. detect-new-resource-provider) + const blockedOnArmModeling = + armModelingReviewLabel.present || labelContext.toAdd.has("ARMModelingReviewRequired"); + + const blocked = blockedOnRpaas || blockedOnVersioningPolicy || blockedOnArmModeling; // 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 @@ -707,7 +713,8 @@ function processARMReviewWorkflowLabels( console.log( `RETURN definition processARMReviewWorkflowLabels. ` + `presentLabels: ${[...labelContext.present].join(",")}, ` + - `blockedOnRpaas: ${blockedOnRpaas}. ` + + `blockedOnRpaas: ${blockedOnRpaas}, ` + + `blockedOnArmModeling: ${blockedOnArmModeling}. ` + `exactlyOneArmReviewWorkflowLabelShouldBePresent: ${exactlyOneArmReviewWorkflowLabelShouldBePresent}. `, ); } @@ -885,6 +892,18 @@ const rulesPri0NotReadyForArmReview = [ anyRequiredLabels: [brChRevApproval], troubleshootingGuide: notReadyForArmReviewReason(brChRev), }, + { + precedence: 0, + anyPrerequisiteLabels: ["ARMModelingReviewRequired"], + anyRequiredLabels: [], + troubleshootingGuide: wrapInArmReviewMessage( + "This PR has ARMModelingReviewRequired label. " + + "This means it is introducing a new Resource Provider namespace or a new resource type. " + + "New RPs and new resource types require a discussion with the ARM Modeling Review team before merging.
" + + "Please schedule a meeting at " + + `${href("ARM API Modeling Office Hours", "https://outlook.office365.com/book/ARMOfficeHours1@microsoft.onmicrosoft.com/?ismsaljsauthenabled=true")}.`, + ), + }, ]; /** @type {RequiredLabelRule[]} */ diff --git a/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js b/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js new file mode 100644 index 000000000000..b38895e0292b --- /dev/null +++ b/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js @@ -0,0 +1,222 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +/** @type {import("vitest").MockedFunction} */ +const mockReadFile = vi.hoisted(() => vi.fn()); +/** @type {import("vitest").MockedFunction} */ +const mockStat = vi.hoisted(() => vi.fn()); + +vi.mock("fs/promises", () => ({ + readFile: mockReadFile, + stat: mockStat, +})); + +import { + isFileAllowed, + leaseSchema, + validateFolderStructure, + validateLeaseContent, +} from "../../src/arm-lease-validation/arm-lease-validation.js"; + +describe("validate-arm-leases", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + describe("isFileAllowed", () => { + it("allows valid lease files and README.md", () => { + expect(isFileAllowed(".github/arm-leases/testservice/Microsoft.Test/lease.yaml")).toBe(true); + expect(isFileAllowed(".github/arm-leases/compute/Microsoft.Compute/DiskRP/lease.yaml")).toBe( + true, + ); + expect(isFileAllowed(".github/arm-leases/README.md")).toBe(true); + }); + + it("rejects invalid files", () => { + expect(isFileAllowed(".github/arm-leases/anything/here")).toBe(false); + expect(isFileAllowed(".github/arm-leases/testservice/Microsoft.Test/other.yaml")).toBe(false); + expect(isFileAllowed(".github/arm-leases/badtest/No.Yaml/no.md")).toBe(false); + }); + }); + + describe("validateFolderStructure", () => { + it("accepts valid paths and rejects invalid ones", () => { + expect( + validateFolderStructure([ + ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", + ".github/arm-leases/compute/Microsoft.Compute/DiskRP/lease.yaml", + ]), + ).toHaveLength(0); + + expect( + validateFolderStructure([ + ".github/arm-leases/TestService/Microsoft.Test/lease.yaml", // uppercase org + ".github/arm-leases/compute/Microsoft.Compute/stable/lease.yaml", // stable not allowed + ]), + ).toHaveLength(2); + }); + }); + + describe("leaseSchema", () => { + const validLease = { + lease: { + "resource-provider": "Microsoft.Test", + startdate: "2026-06-01", + duration: "P90D", + reviewer: "@johndoe", + }, + }; + + it("accepts valid lease data", () => { + expect(leaseSchema.safeParse(validLease).success).toBe(true); + expect( + leaseSchema.safeParse({ ...validLease, lease: { ...validLease.lease, duration: "P5M" } }) + .success, + ).toBe(true); + expect( + leaseSchema.safeParse({ ...validLease, lease: { ...validLease.lease, duration: "P180D" } }) + .success, + ).toBe(true); + }); + + it("rejects invalid resource-provider", () => { + expect( + leaseSchema.safeParse({ + lease: { ...validLease.lease, "resource-provider": "microsoft.Test" }, + }).success, + ).toBe(false); + expect( + leaseSchema.safeParse({ + lease: { ...validLease.lease, "resource-provider": "microsoft.Test" }, + }).success, + ).toBe(false); + }); + + it("rejects invalid startdate", () => { + expect( + leaseSchema.safeParse({ lease: { ...validLease.lease, startdate: "01-15-2026" } }).success, + ).toBe(false); + expect( + leaseSchema.safeParse({ lease: { ...validLease.lease, startdate: "2026-99-99" } }).success, + ).toBe(false); + }); + + it("rejects invalid duration", () => { + expect( + leaseSchema.safeParse({ lease: { ...validLease.lease, duration: "90 days" } }).success, + ).toBe(false); + expect( + leaseSchema.safeParse({ lease: { ...validLease.lease, duration: "P1Y" } }).success, + ).toBe(false); // exceeds 180 days + }); + + it("rejects invalid reviewer", () => { + expect(leaseSchema.safeParse({ lease: { ...validLease.lease, reviewer: "" } }).success).toBe( + false, + ); + expect( + leaseSchema.safeParse({ lease: { ...validLease.lease, reviewer: "johndoe" } }).success, + ).toBe(false); // no @ + }); + + it("rejects missing lease key", () => { + expect(leaseSchema.safeParse({ notlease: {} }).success).toBe(false); + }); + }); + + describe("validateLeaseContent", () => { + const validYaml = `lease: + resource-provider: Microsoft.Test + startdate: "2027-06-01" + duration: P90D + reviewer: "@johndoe" +`; + + it("validates a complete valid lease file", async () => { + mockReadFile.mockResolvedValue(validYaml); + const result = await validateLeaseContent( + "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", + ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", + ); + expect(result.errors).toHaveLength(0); + }); + + it("detects resource provider mismatch", async () => { + mockReadFile.mockResolvedValue(validYaml.replace("Microsoft.Test", "Microsoft.Other")); + const result = await validateLeaseContent( + "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", + ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", + ); + expect(result.errors.some((e) => e.includes("mismatch"))).toBe(true); + }); + + it("returns error for non-existent file", async () => { + mockReadFile.mockRejectedValue(new Error("ENOENT")); + const result = await validateLeaseContent( + "/nonexistent/lease.yaml", + ".github/arm-leases/svc/NS/lease.yaml", + ); + expect(result.errors.some((e) => e.includes("Error reading file"))).toBe(true); + }); + + it("validates startdate within 10 days grace period", async () => { + // 5 days ago - should pass + const recentDate = new Date(); + recentDate.setDate(recentDate.getDate() - 5); + mockReadFile.mockResolvedValue( + validYaml.replace("2027-06-01", recentDate.toISOString().split("T")[0]), + ); + let result = await validateLeaseContent( + "/repo/lease.yaml", + ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", + ); + expect(result.errors).toHaveLength(0); + + // Old date - should fail + mockReadFile.mockResolvedValue(validYaml.replace("2027-06-01", "2025-01-01")); + result = await validateLeaseContent( + "/repo/lease.yaml", + ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", + ); + expect(result.errors.some((e) => e.includes("Startdate is in the past"))).toBe(true); + }); + + it("validates specification folder structure", async () => { + mockReadFile.mockResolvedValue(validYaml); + mockStat.mockResolvedValue(/** @type {any} */ ({ isDirectory: () => true })); + + const result = await validateLeaseContent( + "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", + ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", + "/repo", + ); + expect(result.errors).toHaveLength(0); + expect(mockStat).toHaveBeenCalledTimes(2); + }); + + it("allows new RP when service folder does not exist", async () => { + mockReadFile.mockResolvedValue(validYaml); + mockStat.mockRejectedValue(new Error("ENOENT")); + + const result = await validateLeaseContent( + "/repo/.github/arm-leases/newservice/Microsoft.Test/lease.yaml", + ".github/arm-leases/newservice/Microsoft.Test/lease.yaml", + "/repo", + ); + expect(result.errors).toHaveLength(0); + }); + + it("allows new RP when service exists but RP folder does not", async () => { + mockReadFile.mockResolvedValue(validYaml); + mockStat + .mockResolvedValueOnce(/** @type {any} */ ({ isDirectory: () => true })) + .mockRejectedValueOnce(new Error("ENOENT")); + + const result = await validateLeaseContent( + "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", + ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", + "/repo", + ); + expect(result.errors).toHaveLength(0); + }); + }); +}); diff --git a/.github/workflows/test/arm-modeling-review/arm-modeling-review.test.js b/.github/workflows/test/arm-modeling-review/arm-modeling-review.test.js new file mode 100644 index 000000000000..ed6bdae1aeda --- /dev/null +++ b/.github/workflows/test/arm-modeling-review/arm-modeling-review.test.js @@ -0,0 +1,276 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createMockCore } from "../mocks.js"; + +/** @type {import("vitest").MockedFunction} */ +const mockRaw = vi.hoisted(() => vi.fn().mockResolvedValue("")); + +vi.mock("simple-git", () => ({ + simpleGit: vi.fn().mockReturnValue({ raw: mockRaw }), +})); + +vi.mock("../../src/arm-modeling-review/detect-arm-leases.js", () => ({ + checkLease: vi.fn().mockResolvedValue(false), +})); + +vi.mock("../../src/arm-modeling-review/detect-new-resource-types.js", () => ({ + detectNewResourceTypes: vi.fn().mockResolvedValue([]), +})); + +import * as changedFiles from "../../../shared/src/changed-files.js"; +import { checkLease } from "../../src/arm-modeling-review/detect-arm-leases.js"; +import armModelingReview from "../../src/arm-modeling-review/arm-modeling-review.js"; +import { detectNewResourceTypes } from "../../src/arm-modeling-review/detect-new-resource-types.js"; + +const core = createMockCore(); + +describe("armModelingReview", () => { + afterEach(() => { + vi.clearAllMocks(); + delete process.env.GITHUB_WORKSPACE; + }); + + it("returns no-new-rp when all RP namespaces exist in base branch", async () => { + process.env.GITHUB_WORKSPACE = "/fake/repo"; + const rmFile = + "specification/compute/resource-manager/Microsoft.Compute/stable/2024-01-01/compute.json"; + + vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([rmFile]); + + // Pre-check: file exists in base → not brand new + vi.mocked(mockRaw).mockResolvedValue(rmFile); + + const result = await armModelingReview({ core }); + + expect(result.status).toBe("no-new-rp"); + expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); + expect(core.setFailed).not.toHaveBeenCalled(); + }); + + it("returns new-rp-all-leases-valid when new RP has valid lease", async () => { + process.env.GITHUB_WORKSPACE = "/fake/repo"; + const rmFile = + "specification/newservice/resource-manager/Microsoft.NewService/stable/2025-01-01/api.json"; + + vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([rmFile]); + + // Pre-check: no files in base → brand new; namespace doesn't exist in base directories + vi.mocked(mockRaw).mockResolvedValue(""); + vi.mocked(checkLease).mockResolvedValue(true); + + const result = await armModelingReview({ core }); + + expect(result.status).toBe("new-rp-all-leases-valid"); + expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); + expect(result.labelActions.ARMModelingAutoSignedOff).toBe("add"); + expect(core.setFailed).not.toHaveBeenCalled(); + expect(core.info).toHaveBeenCalledWith(expect.stringContaining("valid ARM lease")); + expect(checkLease).toHaveBeenCalledWith("newservice", "Microsoft.NewService", ""); + }); + + it("returns new-rp-invalid-lease and calls setFailed when lease is invalid", async () => { + process.env.GITHUB_WORKSPACE = "/fake/repo"; + const rmFile = + "specification/badservice/resource-manager/Microsoft.BadService/preview/2025-01-01/api.json"; + + vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([rmFile]); + vi.mocked(mockRaw).mockResolvedValue(""); + vi.mocked(checkLease).mockResolvedValue(false); + + const result = await armModelingReview({ core }); + + expect(result.status).toBe("new-rp-invalid-lease"); + expect(result.labelActions.ARMModelingReviewRequired).toBe("add"); + expect(result.labelActions.ARMModelingAutoSignedOff).toBe("remove"); + expect(core.setFailed).toHaveBeenCalledTimes(1); + expect(core.setFailed).toHaveBeenCalledWith( + expect.stringContaining("ARM API Modeling Office Hours"), + ); + expect(core.error).toHaveBeenCalledWith(expect.stringContaining("Microsoft.BadService")); + }); + + it("fails when at least one of multiple new RPs has invalid lease", async () => { + process.env.GITHUB_WORKSPACE = "/fake/repo"; + + vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([ + "specification/svcA/resource-manager/Microsoft.SvcA/stable/2025-01-01/a.json", + "specification/svcB/resource-manager/Microsoft.SvcB/stable/2025-01-01/b.json", + ]); + vi.mocked(mockRaw).mockResolvedValue(""); + + // SvcA valid, SvcB invalid + vi.mocked(checkLease).mockImplementation((orgName) => Promise.resolve(orgName === "svcA")); + + const result = await armModelingReview({ core }); + + expect(result.status).toBe("new-rp-invalid-lease"); + expect(result.labelActions.ARMModelingReviewRequired).toBe("add"); + expect(core.setFailed).toHaveBeenCalledTimes(1); + expect(core.setFailed).toHaveBeenCalledWith( + expect.stringContaining("without a valid ARM lease"), + ); + }); + + it("passes when all multiple new RPs have valid leases", async () => { + process.env.GITHUB_WORKSPACE = "/fake/repo"; + + vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([ + "specification/svcA/resource-manager/Microsoft.SvcA/stable/2025-01-01/a.json", + "specification/svcB/resource-manager/Microsoft.SvcB/stable/2025-01-01/b.json", + ]); + vi.mocked(mockRaw).mockResolvedValue(""); + vi.mocked(checkLease).mockResolvedValue(true); + + const result = await armModelingReview({ core }); + + expect(result.status).toBe("new-rp-all-leases-valid"); + expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); + expect(result.labelActions.ARMModelingAutoSignedOff).toBe("add"); + expect(core.setFailed).not.toHaveBeenCalled(); + }); + + it("passes serviceName to checkLease when present", async () => { + process.env.GITHUB_WORKSPACE = "/fake/repo"; + const rmFile = + "specification/svc/resource-manager/Microsoft.Svc/ComputeRP/stable/2025-01-01/api.json"; + + vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([rmFile]); + vi.mocked(mockRaw).mockResolvedValue(""); + vi.mocked(checkLease).mockResolvedValue(true); + + await armModelingReview({ core }); + + expect(checkLease).toHaveBeenCalledWith("svc", "Microsoft.Svc", "ComputeRP"); + }); + + it("returns Remove for ARMModelingReviewRequired when no new RPs", async () => { + process.env.GITHUB_WORKSPACE = "/fake/repo"; + const rmFile = + "specification/compute/resource-manager/Microsoft.Compute/stable/2024-01-01/compute.json"; + + vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([rmFile]); + vi.mocked(mockRaw).mockResolvedValue(rmFile); + + const result = await armModelingReview({ core }); + + expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); + expect(result.labelActions.ARMModelingSignedOff).toBe("remove"); + expect(result.labelActions.ARMModelingAutoSignedOff).toBe("remove"); + }); + + it("returns Add for ARMModelingReviewRequired when new RP has invalid lease", async () => { + process.env.GITHUB_WORKSPACE = "/fake/repo"; + + vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([ + "specification/svc/resource-manager/Microsoft.Svc/stable/2025-01-01/api.json", + ]); + vi.mocked(mockRaw).mockResolvedValue(""); + vi.mocked(checkLease).mockResolvedValue(false); + + const result = await armModelingReview({ core }); + + expect(result.labelActions.ARMModelingReviewRequired).toBe("add"); + expect(result.labelActions.ARMModelingSignedOff).toBe("remove"); + expect(result.labelActions.ARMModelingAutoSignedOff).toBe("remove"); + }); + + it("returns Remove for ARMModelingReviewRequired when new RP has valid lease", async () => { + process.env.GITHUB_WORKSPACE = "/fake/repo"; + + vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([ + "specification/svc/resource-manager/Microsoft.Svc/stable/2025-01-01/api.json", + ]); + vi.mocked(mockRaw).mockResolvedValue(""); + vi.mocked(checkLease).mockResolvedValue(true); + + const result = await armModelingReview({ core }); + + expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); + expect(result.labelActions.ARMModelingSignedOff).toBe("remove"); + expect(result.labelActions.ARMModelingAutoSignedOff).toBe("add"); + }); + + // ── New resource type detection (no new RP) ────────────────────────── + + it("checks for new resource types when no new RP is detected", async () => { + process.env.GITHUB_WORKSPACE = "/fake/repo"; + const rmFile = + "specification/compute/resource-manager/Microsoft.Compute/stable/2024-01-01/compute.json"; + + vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([rmFile]); + vi.mocked(mockRaw).mockResolvedValue(rmFile); + + // detectNewResourceTypes returns new RT + vi.mocked(detectNewResourceTypes).mockResolvedValue([ + { + rpNamespace: "Microsoft.Compute", + orgName: "compute", + serviceName: "", + newResourceTypes: [ + { + resourceType: "Microsoft.Compute/disks", + provider: "Microsoft.Compute", + modelName: null, + operations: ["GET"], + }, + ], + }, + ]); + vi.mocked(checkLease).mockResolvedValue(true); + + const result = await armModelingReview({ core }); + + expect(result.status).toBe("new-rt-all-leases-valid"); + expect(result.labelActions.ARMModelingAutoSignedOff).toBe("add"); + expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); + expect(core.setFailed).not.toHaveBeenCalled(); + }); + + it("adds ARMModelingReviewRequired when new RT has no valid lease", async () => { + process.env.GITHUB_WORKSPACE = "/fake/repo"; + const rmFile = + "specification/compute/resource-manager/Microsoft.Compute/stable/2024-01-01/compute.json"; + + vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([rmFile]); + vi.mocked(mockRaw).mockResolvedValue(rmFile); + + vi.mocked(detectNewResourceTypes).mockResolvedValue([ + { + rpNamespace: "Microsoft.Compute", + orgName: "compute", + serviceName: "", + newResourceTypes: [ + { + resourceType: "Microsoft.Compute/disks", + provider: "Microsoft.Compute", + modelName: null, + operations: ["GET"], + }, + ], + }, + ]); + vi.mocked(checkLease).mockResolvedValue(false); + + const result = await armModelingReview({ core }); + + expect(result.status).toBe("new-rt-invalid-lease"); + expect(result.labelActions.ARMModelingReviewRequired).toBe("add"); + expect(result.labelActions.ARMModelingAutoSignedOff).toBe("remove"); + expect(core.setFailed).toHaveBeenCalled(); + }); + + it("returns no-new-rp when no new RTs detected either", async () => { + process.env.GITHUB_WORKSPACE = "/fake/repo"; + const rmFile = + "specification/compute/resource-manager/Microsoft.Compute/stable/2024-01-01/compute.json"; + + vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([rmFile]); + vi.mocked(mockRaw).mockResolvedValue(rmFile); + vi.mocked(detectNewResourceTypes).mockResolvedValue([]); + + const result = await armModelingReview({ core }); + + expect(result.status).toBe("no-new-rp"); + expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); + expect(result.labelActions.ARMModelingAutoSignedOff).toBe("remove"); + }); +}); diff --git a/.github/workflows/test/arm-modeling-review/detect-arm-leases.test.js b/.github/workflows/test/arm-modeling-review/detect-arm-leases.test.js new file mode 100644 index 000000000000..a7727841aa0b --- /dev/null +++ b/.github/workflows/test/arm-modeling-review/detect-arm-leases.test.js @@ -0,0 +1,164 @@ +import { Temporal } from "@js-temporal/polyfill"; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; + +/** @type {import("vitest").MockedFunction} */ +const mockReadFile = vi.hoisted(() => vi.fn()); + +vi.mock("fs/promises", () => ({ + readFile: mockReadFile, +})); + +/** @type {import("vitest").MockedFunction} */ +const mockGetRootFolder = vi.hoisted(() => vi.fn().mockResolvedValue("/fake/repo")); + +vi.mock("../../../shared/src/simple-git.js", () => ({ + getRootFolder: mockGetRootFolder, +})); + +import { checkLease, parseLease } from "../../src/arm-modeling-review/detect-arm-leases.js"; + +// Use a fixed date for deterministic tests (avoids flakiness around midnight) +const FIXED_TEST_DATE = new Date("2025-06-15T12:00:00Z"); +const FIXED_PLAIN_DATE = Temporal.PlainDate.from("2025-06-15"); + +/** Get fixed today's date using Temporal */ +function today() { + return FIXED_PLAIN_DATE; +} + +/** Subtract days from today and return YYYY-MM-DD string + * @param {number} n - Number of days to subtract + */ +function daysAgo(n) { + return today().subtract({ days: n }).toString(); +} + +/** Build a valid lease YAML string + * @param {string} startdate + * @param {string} duration + */ +function leaseYaml(startdate, duration) { + return `lease:\n startdate: "${startdate}"\n duration: "${duration}"\n`; +} + +describe("detect-arm-leases", () => { + beforeAll(() => { + vi.useFakeTimers(); + vi.setSystemTime(FIXED_TEST_DATE); + // Stub Temporal.Now.plainDateISO since the polyfill may not respect vi.useFakeTimers() + vi.spyOn(Temporal.Now, "plainDateISO").mockReturnValue(FIXED_PLAIN_DATE); + }); + + afterAll(() => { + vi.useRealTimers(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe("parseLease", () => { + it("returns valid for a non-expired lease", () => { + const result = parseLease(leaseYaml(daysAgo(30), "P90D")); + expect(result.valid).toBe(true); + }); + + it("returns invalid when lease has expired", () => { + const result = parseLease(leaseYaml(daysAgo(100), "P90D")); + expect(result.valid).toBe(false); + expect(result.reason).toContain("expired"); + }); + + it("returns valid on the last day of lease", () => { + const result = parseLease(leaseYaml(daysAgo(90), "P90D")); + expect(result.valid).toBe(true); + }); + + it("returns invalid one day after lease expires", () => { + const result = parseLease(leaseYaml(daysAgo(91), "P90D")); + expect(result.valid).toBe(false); + }); + + it("supports month-based durations", () => { + const start = today().subtract({ months: 3 }).toString(); + const result = parseLease(leaseYaml(start, "P6M")); + expect(result.valid).toBe(true); + }); + + it("supports year-based durations", () => { + const start = today().subtract({ years: 1 }).add({ months: 1 }).toString(); + const result = parseLease(leaseYaml(start, "P2Y")); + expect(result.valid).toBe(true); + }); + + it("supports combined durations like P1Y6M", () => { + const result = parseLease(leaseYaml("2025-01-01", "P1Y6M")); + expect(result.valid).toBe(true); + }); + + it("handles single day duration", () => { + const result = parseLease(leaseYaml(today().toString(), "P1D")); + expect(result.valid).toBe(true); + }); + + it("returns invalid for malformed YAML", () => { + const result = parseLease("invalid: yaml: content"); + expect(result.valid).toBe(false); + }); + + it("returns invalid for empty content", () => { + const result = parseLease(""); + expect(result.valid).toBe(false); + expect(result.reason).toContain("Empty"); + }); + + it("returns valid for future start dates", () => { + const start = today().add({ days: 10 }).toString(); + const result = parseLease(leaseYaml(start, "P90D")); + expect(result.valid).toBe(true); + }); + }); + + describe("checkLease", () => { + it("returns false when lease file does not exist", async () => { + mockReadFile.mockRejectedValue(new Error("ENOENT")); + + const result = await checkLease("testservice", "Microsoft.Test"); + expect(result).toBe(false); + }); + + it("returns true when lease is valid and not expired", async () => { + mockReadFile.mockResolvedValue(leaseYaml(daysAgo(30), "P90D")); + + const result = await checkLease("testservice", "Microsoft.Test"); + expect(result).toBe(true); + }); + + it("returns false when lease has expired", async () => { + mockReadFile.mockResolvedValue(leaseYaml(daysAgo(100), "P90D")); + + const result = await checkLease("testservice", "Microsoft.Test"); + expect(result).toBe(false); + }); + + it("returns false for invalid lease file format", async () => { + mockReadFile.mockResolvedValue("invalid: yaml: content"); + + const result = await checkLease("testservice", "Microsoft.Test"); + expect(result).toBe(false); + }); + + it("handles multiple services and namespaces", async () => { + mockReadFile.mockResolvedValue(leaseYaml(daysAgo(30), "P90D")); + + expect(await checkLease("app", "Microsoft.App")).toBe(true); + expect(await checkLease("compute", "Microsoft.Compute")).toBe(true); + }); + + it("returns false for missing namespace", async () => { + mockReadFile.mockRejectedValue(new Error("ENOENT")); + + expect(await checkLease("storage", "Microsoft.Storage")).toBe(false); + }); + }); +}); diff --git a/.github/workflows/test/arm-modeling-review/detect-new-resource-types.test.js b/.github/workflows/test/arm-modeling-review/detect-new-resource-types.test.js new file mode 100644 index 000000000000..5de3b17c019e --- /dev/null +++ b/.github/workflows/test/arm-modeling-review/detect-new-resource-types.test.js @@ -0,0 +1,421 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createMockCore } from "../mocks.js"; + +/** @type {import("vitest").MockedFunction} */ +const mockRaw = vi.hoisted(() => vi.fn().mockResolvedValue("")); + +/** @type {import("vitest").MockedFunction} */ +const mockShow = vi.hoisted(() => vi.fn().mockResolvedValue("")); + +vi.mock("simple-git", () => ({ + simpleGit: vi.fn().mockReturnValue({ raw: mockRaw, show: mockShow }), +})); + +import { detectNewResourceTypes } from "../../src/arm-modeling-review/detect-new-resource-types.js"; + +const core = createMockCore(); + +const emptySwagger = JSON.stringify({ swagger: "2.0", paths: {} }); + +const vmSwagger = JSON.stringify({ + swagger: "2.0", + paths: { + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName}": + { + get: { operationId: "VirtualMachines_Get", responses: { 200: { description: "OK" } } }, + }, + }, +}); + +const vmAndDiskSwagger = JSON.stringify({ + swagger: "2.0", + paths: { + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName}": + { + get: { operationId: "VirtualMachines_Get", responses: { 200: { description: "OK" } } }, + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/disks/{diskName}": + { + get: { operationId: "Disks_Get", responses: { 200: { description: "OK" } } }, + put: { operationId: "Disks_CreateOrUpdate", responses: { 200: { description: "OK" } } }, + }, + }, +}); + +// ── helpers ───────────────────────────────────────────────────────────── + +/** + * Configure git mocks for ls-tree and show. + * + * @param {Object} opts + * @param {Map} [opts.baseFiles] - namespace path → file list at base ref + * @param {Map} [opts.headFiles] - namespace path → file list at HEAD + * @param {Map} [opts.fileContents] - "ref:file" → JSON string + */ +function setupGit({ baseFiles = new Map(), headFiles = new Map(), fileContents = new Map() } = {}) { + mockRaw.mockImplementation( + /** @type {any} */ ( + /** @param {string[]} args */ (args) => { + if (args[0] === "ls-tree" && args.includes("-r")) { + const commitish = args[3]; + const namespacePath = args[4]; + const filesMap = commitish === "HEAD" ? headFiles : baseFiles; + const files = filesMap.get(namespacePath); + if (files) { + return files.join("\n"); + } + throw new Error(`path '${namespacePath}' does not exist`); + } + return ""; + } + ), + ); + + mockShow.mockImplementation( + /** @type {any} */ ( + /** @param {string[]=} args */ (args) => { + const key = String(args?.[0]); // "ref:filepath" + const content = fileContents.get(key); + if (content !== undefined) { + return content; + } + const msg = `path does not exist: ${key}`; + throw new Error(msg); + } + ), + ); +} + +// ── tests ─────────────────────────────────────────────────────────────── + +describe("detectNewResourceTypes", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("returns empty when rmFiles has no version-pattern matches", async () => { + const result = await detectNewResourceTypes({ + rmFiles: ["specification/compute/resource-manager/readme.md"], + core, + }); + + expect(result).toEqual([]); + expect(mockRaw).not.toHaveBeenCalled(); + }); + + it("returns empty when rmFiles is empty", async () => { + const result = await detectNewResourceTypes({ + rmFiles: [], + core, + }); + + expect(result).toEqual([]); + }); + + it("skips namespace when no resources exist in base (new RP)", async () => { + const rmFile = + "specification/newservice/resource-manager/Microsoft.NewService/stable/2025-01-01/api.json"; + + setupGit({ baseFiles: new Map() }); // ls-tree throws → no base + + const result = await detectNewResourceTypes({ + rmFiles: [rmFile], + core, + }); + + expect(result).toEqual([]); + expect(core.info).toHaveBeenCalledWith(expect.stringContaining("no resources in base")); + }); + + it("returns empty when HEAD has same resource types as base", async () => { + const ns = "specification/compute/resource-manager/Microsoft.Compute"; + const file = `${ns}/stable/2024-01-01/compute.json`; + // When path contains stable/preview, compare at namespace root + const namespacePath = ns; + + setupGit({ + baseFiles: new Map([[namespacePath, [file]]]), + headFiles: new Map([[namespacePath, [file]]]), + fileContents: new Map([ + [`HEAD^:${file}`, vmSwagger], + [`HEAD:${file}`, vmSwagger], + ]), + }); + + const result = await detectNewResourceTypes({ + rmFiles: [file], + core, + }); + + expect(result).toEqual([]); + expect(core.info).toHaveBeenCalledWith(expect.stringContaining("no new resource types")); + }); + + it("detects new resource types present in HEAD but absent from base", async () => { + const ns = "specification/compute/resource-manager/Microsoft.Compute"; + const file = `${ns}/stable/2024-01-01/compute.json`; + // When path contains stable/preview, compare at namespace root + const namespacePath = ns; + + setupGit({ + baseFiles: new Map([[namespacePath, [file]]]), + headFiles: new Map([[namespacePath, [file]]]), + fileContents: new Map([ + [`HEAD^:${file}`, vmSwagger], // base: VM only + [`HEAD:${file}`, vmAndDiskSwagger], // HEAD: VM + Disk + ]), + }); + + const result = await detectNewResourceTypes({ + rmFiles: [file], + core, + }); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + rpNamespace: "Microsoft.Compute", + orgName: "compute", + serviceName: "", + }); + expect(result[0].newResourceTypes).toHaveLength(1); + expect(result[0].newResourceTypes[0]).toMatchObject({ + resourceType: "Microsoft.Compute/disks", + }); + expect(result[0].newResourceTypes[0].operations).toContain("GET"); + expect(result[0].newResourceTypes[0].operations).toContain("PUT"); + }); + + it("processes multiple namespaces independently", async () => { + const computeNs = "specification/compute/resource-manager/Microsoft.Compute"; + const networkNs = "specification/network/resource-manager/Microsoft.Network"; + const computeFile = `${computeNs}/stable/2024-01-01/compute.json`; + const networkFile = `${networkNs}/stable/2024-01-01/network.json`; + // When path contains stable/preview, compare at namespace root + const computeNamespacePath = computeNs; + const networkNamespacePath = networkNs; + + setupGit({ + baseFiles: new Map([ + [computeNamespacePath, [computeFile]], + [networkNamespacePath, [networkFile]], + ]), + headFiles: new Map([ + [computeNamespacePath, [computeFile]], + [networkNamespacePath, [networkFile]], + ]), + fileContents: new Map([ + [`HEAD^:${computeFile}`, emptySwagger], + [`HEAD:${computeFile}`, emptySwagger], + [`HEAD^:${networkFile}`, emptySwagger], + [`HEAD:${networkFile}`, emptySwagger], + ]), + }); + + const result = await detectNewResourceTypes({ + rmFiles: [computeFile, networkFile], + core, + }); + + expect(result).toEqual([]); + expect(core.info).toHaveBeenCalledWith(expect.stringContaining("Microsoft.Compute")); + expect(core.info).toHaveBeenCalledWith(expect.stringContaining("Microsoft.Network")); + }); + + it("skips example files from ls-tree output", async () => { + const ns = "specification/compute/resource-manager/Microsoft.Compute"; + const file = `${ns}/stable/2024-01-01/compute.json`; + const exampleFile = `${ns}/stable/2024-01-01/examples/create.json`; + // When path contains stable/preview, compare at namespace root + const namespacePath = ns; + + setupGit({ + baseFiles: new Map([[namespacePath, [file, exampleFile]]]), + headFiles: new Map([[namespacePath, [file, exampleFile]]]), + fileContents: new Map([ + [`HEAD^:${file}`, emptySwagger], + [`HEAD:${file}`, emptySwagger], + ]), + }); + + await detectNewResourceTypes({ + rmFiles: [file], + core, + }); + + expect(mockShow).not.toHaveBeenCalledWith( + expect.arrayContaining([expect.stringContaining("examples")]), + ); + }); + + it("skips non-JSON files from ls-tree output", async () => { + const ns = "specification/compute/resource-manager/Microsoft.Compute"; + const file = `${ns}/stable/2024-01-01/compute.json`; + const readme = `${ns}/stable/readme.md`; + // When path contains stable/preview, compare at namespace root + const namespacePath = ns; + + setupGit({ + baseFiles: new Map([[namespacePath, [file, readme]]]), + headFiles: new Map([[namespacePath, [file, readme]]]), + fileContents: new Map([ + [`HEAD^:${file}`, emptySwagger], + [`HEAD:${file}`, emptySwagger], + ]), + }); + + await detectNewResourceTypes({ + rmFiles: [file], + core, + }); + + expect(mockShow).not.toHaveBeenCalledWith( + expect.arrayContaining([expect.stringContaining("readme.md")]), + ); + }); + + it("detects new RT when introducing first preview folder under existing stable-only namespace", async () => { + // This tests the fix for false-negative when introducing the first preview folder + // under an existing namespace that only had stable. Without the fix, the code + // would try to compare .../Microsoft.Compute/preview which doesn't exist in base + // and skip detection entirely. + const ns = "specification/compute/resource-manager/Microsoft.Compute"; + const stableFile = `${ns}/stable/2024-01-01/compute.json`; + const previewFile = `${ns}/preview/2026-01-01-preview/compute.json`; + // Compare at namespace root to include both stable and preview + const namespacePath = ns; + + const stableSwagger = JSON.stringify({ + swagger: "2.0", + paths: { + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName}": + { + get: { operationId: "VirtualMachines_Get", responses: { 200: { description: "OK" } } }, + }, + }, + }); + + const previewSwagger = JSON.stringify({ + swagger: "2.0", + paths: { + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/quantumVMs/{vmName}": + { + get: { operationId: "QuantumVMs_Get", responses: { 200: { description: "OK" } } }, + put: { + operationId: "QuantumVMs_CreateOrUpdate", + responses: { 200: { description: "OK" } }, + }, + }, + }, + }); + + setupGit({ + // Base only has stable folder + baseFiles: new Map([[namespacePath, [stableFile]]]), + // HEAD has both stable and new preview folder + headFiles: new Map([[namespacePath, [stableFile, previewFile]]]), + fileContents: new Map([ + [`HEAD^:${stableFile}`, stableSwagger], + [`HEAD:${stableFile}`, stableSwagger], + [`HEAD:${previewFile}`, previewSwagger], + ]), + }); + + const result = await detectNewResourceTypes({ + rmFiles: [previewFile], + core, + }); + + // Should detect the new quantumVMs resource type from preview + expect(result).toHaveLength(1); + expect(result[0].rpNamespace).toBe("Microsoft.Compute"); + const quantumType = result[0].newResourceTypes.find( + (t) => t.resourceType === "Microsoft.Compute/quantumVMs", + ); + expect(quantumType).toBeDefined(); + expect(quantumType?.operations).toContain("GET"); + expect(quantumType?.operations).toContain("PUT"); + }); + + it("detects new resource types from path-based detection", async () => { + const ns = "specification/compute/resource-manager/Microsoft.Compute"; + const existingFile = `${ns}/DiskRP/stable/2024-01-01/disk.json`; + const newFile = `${ns}/DiskRP/preview/2026-05-01-preview/disk.json`; + + const existingSwagger = JSON.stringify({ swagger: "2.0", paths: {} }); + const newSwagger = JSON.stringify({ + swagger: "2.0", + paths: { + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/superDisks/{diskName}": + { + get: { operationId: "SuperDisks_Get", responses: { 200: { description: "OK" } } }, + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/superDisks/{diskName}/metrics/{metricName}": + { + get: { operationId: "SuperDiskMetrics_Get", responses: { 200: { description: "OK" } } }, + }, + }, + }); + + setupGit({ + baseFiles: new Map([[`${ns}/DiskRP`, [existingFile]]]), + headFiles: new Map([[`${ns}/DiskRP`, [existingFile, newFile]]]), + fileContents: new Map([ + [`HEAD^:${existingFile}`, existingSwagger], + [`HEAD:${existingFile}`, existingSwagger], + [`HEAD:${newFile}`, newSwagger], + ]), + }); + + const result = await detectNewResourceTypes({ + rmFiles: [newFile], + core, + }); + + expect(result).toHaveLength(1); + expect(result[0].rpNamespace).toBe("Microsoft.Compute"); + // superDisks should be detected + const superDisksType = result[0].newResourceTypes.find( + (t) => t.resourceType === "Microsoft.Compute/superDisks", + ); + expect(superDisksType).toBeDefined(); + expect(superDisksType?.operations).toContain("GET"); + // superDisks/metrics is also new + const metricsType = result[0].newResourceTypes.find( + (t) => t.resourceType === "Microsoft.Compute/superDisks/metrics", + ); + expect(metricsType).toBeDefined(); + }); + + it("excludes operations-only paths", async () => { + const ns = "specification/compute/resource-manager/Microsoft.Compute"; + const existingPreviewFile = `${ns}/DiskRP/stable/2024-01-01/compute.json`; + const newFile = `${ns}/DiskRP/preview/2026-01-01-preview/compute.json`; + + const existingSwagger = JSON.stringify({ swagger: "2.0", paths: {} }); + const operationsOnlySwagger = JSON.stringify({ + swagger: "2.0", + paths: { + "/providers/Microsoft.Compute/operations": { + get: { operationId: "Operations_List", responses: { 200: { description: "OK" } } }, + }, + }, + }); + + setupGit({ + baseFiles: new Map([[`${ns}/DiskRP`, [existingPreviewFile]]]), + headFiles: new Map([[`${ns}/DiskRP`, [existingPreviewFile, newFile]]]), + fileContents: new Map([ + [`HEAD^:${existingPreviewFile}`, existingSwagger], + [`HEAD:${existingPreviewFile}`, existingSwagger], + [`HEAD:${newFile}`, operationsOnlySwagger], + ]), + }); + + const result = await detectNewResourceTypes({ + rmFiles: [newFile], + core, + }); + + expect(result).toEqual([]); + }); +}); diff --git a/.github/workflows/update-labels.yaml b/.github/workflows/update-labels.yaml index 2cb27384a79b..e615e3eb00e1 100644 --- a/.github/workflows/update-labels.yaml +++ b/.github/workflows/update-labels.yaml @@ -9,6 +9,7 @@ on: workflows: [ "ARM Auto SignOff - Set Status", + "ARM Modeling Review", "SDK Breaking Change Labels", "SDK Suppressions", "TypeSpec Requirement", diff --git a/package-lock.json b/package-lock.json index 94706c6c95a1..d50ebb6b99f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11616,9 +11616,9 @@ } }, "node_modules/simple-git": { - "version": "3.33.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz", - "integrity": "sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng==", + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.30.0.tgz", + "integrity": "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==", "dev": true, "license": "MIT", "dependencies": { @@ -11921,9 +11921,9 @@ "license": "MIT" }, "node_modules/tar": { - "version": "7.5.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", - "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", + "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { diff --git a/specification/batman/cspell.yaml b/specification/batman/cspell.yaml new file mode 100644 index 000000000000..4d1901c644fe --- /dev/null +++ b/specification/batman/cspell.yaml @@ -0,0 +1,2 @@ +words: + - batman diff --git a/specification/batman/resource-manager/Microsoft.Batman/Batman/main.tsp b/specification/batman/resource-manager/Microsoft.Batman/Batman/main.tsp new file mode 100644 index 000000000000..d8b9a0ca4d1a --- /dev/null +++ b/specification/batman/resource-manager/Microsoft.Batman/Batman/main.tsp @@ -0,0 +1,61 @@ +import "@typespec/http"; +import "@typespec/rest"; +import "@typespec/versioning"; +import "@azure-tools/typespec-azure-core"; +import "@azure-tools/typespec-azure-resource-manager"; + +using TypeSpec.Http; +using TypeSpec.Rest; +using TypeSpec.Versioning; +using Azure.Core; +using Azure.ResourceManager; + +/** Microsoft.Batman Resource Provider management API. */ +@armProviderNamespace +@service(#{ title: "Batman" }) +@versioned(Microsoft.Batman.Versions) +namespace Microsoft.Batman; + +/** The available API versions. */ +enum Versions { + /** 2024-01-01-preview version */ + @armCommonTypesVersion(Azure.ResourceManager.CommonTypes.Versions.v5) + v2024_01_01_preview: "2024-01-01-preview", +} + +interface Operations extends Azure.ResourceManager.Operations {} + +/** A Batman resource */ +model BatmanResource is TrackedResource { + ...ResourceNameParameter; +} + +/** Batman resource properties */ +model BatmanProperties { + /** Display name */ + displayName?: string; + + /** The provisioning state */ + @visibility(Lifecycle.Read) + provisioningState?: ProvisioningState; +} + +/** The resource provisioning state. */ +@lroStatus +union ProvisioningState { + ResourceProvisioningState, + + /** The resource is being provisioned */ + Provisioning: "Provisioning", + + string, +} + +@armResourceOperations +interface BatmanResources { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + delete is ArmResourceDeleteWithoutOkAsync; + listByResourceGroup is ArmResourceListByParent; + listBySubscription is ArmListBySubscription; +} diff --git a/specification/batman/resource-manager/Microsoft.Batman/Batman/preview/2024-01-01-preview/batman.json b/specification/batman/resource-manager/Microsoft.Batman/Batman/preview/2024-01-01-preview/batman.json new file mode 100644 index 000000000000..f03929042570 --- /dev/null +++ b/specification/batman/resource-manager/Microsoft.Batman/Batman/preview/2024-01-01-preview/batman.json @@ -0,0 +1,411 @@ +{ + "swagger": "2.0", + "info": { + "title": "Batman", + "version": "2024-01-01-preview", + "description": "Microsoft.Batman 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": "BatmanResources" + } + ], + "paths": { + "/providers/Microsoft.Batman/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-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/providers/Microsoft.Batman/BatmanResources": { + "get": { + "operationId": "BatmanResources_ListBySubscription", + "tags": [ + "BatmanResources" + ], + "description": "List BatmanResource 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/BatmanResourceListResult" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Batman/BatmanResources": { + "get": { + "operationId": "BatmanResources_ListByResourceGroup", + "tags": [ + "BatmanResources" + ], + "description": "List BatmanResource 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/BatmanResourceListResult" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Batman/BatmanResources/{BatmanResourceName}": { + "get": { + "operationId": "BatmanResources_Get", + "tags": [ + "BatmanResources" + ], + "description": "Get a BatmanResource", + "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": "BatmanResourceName", + "in": "path", + "description": "The name of the BatmanResource", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9-]{3,24}$" + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "#/definitions/BatmanResource" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "operationId": "BatmanResources_CreateOrUpdate", + "tags": [ + "BatmanResources" + ], + "description": "Create a BatmanResource", + "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": "BatmanResourceName", + "in": "path", + "description": "The name of the BatmanResource", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9-]{3,24}$" + }, + { + "name": "resource", + "in": "body", + "description": "Resource create parameters.", + "required": true, + "schema": { + "$ref": "#/definitions/BatmanResource" + } + } + ], + "responses": { + "200": { + "description": "Resource 'BatmanResource' update operation succeeded", + "schema": { + "$ref": "#/definitions/BatmanResource" + } + }, + "201": { + "description": "Resource 'BatmanResource' create operation succeeded", + "schema": { + "$ref": "#/definitions/BatmanResource" + }, + "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-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-long-running-operation": true + }, + "delete": { + "operationId": "BatmanResources_Delete", + "tags": [ + "BatmanResources" + ], + "description": "Delete a BatmanResource", + "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": "BatmanResourceName", + "in": "path", + "description": "The name of the BatmanResource", + "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-long-running-operation-options": { + "final-state-via": "location" + }, + "x-ms-long-running-operation": true + } + } + }, + "definitions": { + "BatmanProperties": { + "type": "object", + "description": "Batman resource properties", + "properties": { + "displayName": { + "type": "string", + "description": "Display name" + }, + "provisioningState": { + "$ref": "#/definitions/ProvisioningState", + "description": "The provisioning state", + "readOnly": true + } + } + }, + "BatmanResource": { + "type": "object", + "description": "A Batman resource", + "properties": { + "properties": { + "$ref": "#/definitions/BatmanProperties", + "description": "The resource-specific properties for this resource." + } + }, + "allOf": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/TrackedResource" + } + ] + }, + "BatmanResourceListResult": { + "type": "object", + "description": "The response of a BatmanResource list operation.", + "properties": { + "value": { + "type": "array", + "description": "The BatmanResource items on this page", + "items": { + "$ref": "#/definitions/BatmanResource" + } + }, + "nextLink": { + "type": "string", + "format": "uri", + "description": "The link to the next page of items" + } + }, + "required": [ + "value" + ] + }, + "ProvisioningState": { + "type": "string", + "description": "The resource provisioning state.", + "enum": [ + "Succeeded", + "Failed", + "Canceled", + "Provisioning" + ], + "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" + } + ] + }, + "readOnly": true + } + }, + "parameters": {} +} diff --git a/specification/batman/resource-manager/Microsoft.Batman/Batman/readme.md b/specification/batman/resource-manager/Microsoft.Batman/Batman/readme.md new file mode 100644 index 000000000000..9e794e783050 --- /dev/null +++ b/specification/batman/resource-manager/Microsoft.Batman/Batman/readme.md @@ -0,0 +1,26 @@ +# Batman + +> see https://aka.ms/autorest + +This is the AutoRest configuration file for Batman. + +## Configuration + +### Basic Information + +```yaml +openapi-type: arm +openapi-subtype: rpaas +tag: package-2024-01-01-preview +``` + +### Tag: package-2024-01-01-preview + +These settings apply only when `--tag=package-2024-01-01-preview` is specified on the command line. + +```yaml $(tag) == 'package-2024-01-01-preview' +input-file: + - preview/2024-01-01-preview/batman.json +``` + +--- diff --git a/specification/batman/resource-manager/Microsoft.Batman/Batman/tspconfig.yaml b/specification/batman/resource-manager/Microsoft.Batman/Batman/tspconfig.yaml new file mode 100644 index 000000000000..e9153cf8227b --- /dev/null +++ b/specification/batman/resource-manager/Microsoft.Batman/Batman/tspconfig.yaml @@ -0,0 +1,14 @@ +parameters: + "service-dir": + default: "sdk/batman" +emit: + - "@azure-tools/typespec-autorest" +options: + "@azure-tools/typespec-autorest": + use-read-only-status-schema: true + emitter-output-dir: "{project-root}" + output-file: "{version-status}/{version}/batman.json" + arm-types-dir: "{project-root}/../../../../common-types/resource-management" +linter: + extends: + - "@azure-tools/typespec-azure-rulesets/resource-manager" diff --git a/specification/batman/resource-manager/readme.md b/specification/batman/resource-manager/readme.md new file mode 100644 index 000000000000..162aabdcec58 --- /dev/null +++ b/specification/batman/resource-manager/readme.md @@ -0,0 +1,3 @@ +# Microsoft.Batman + +> see https://aka.ms/autorest