From ba1a98805a1586b4cbba229cebd3b91cbc480353 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 7 Jan 2026 12:30:37 -0600 Subject: [PATCH 001/125] Add GitHub Action to validate ARM leases --- .github/arm-leases/README.md | 67 ++++ .github/arm-leases/badtest/bad.test/noyaml.md | 5 + .../testservice/Microsoft.TestRP/lease.yaml | 0 .github/workflows/src/validate-arm-leases.js | 267 ++++++++++++++ .../test/validate-arm-leases.test.js | 331 ++++++++++++++++++ .github/workflows/validate-arm-leases.yaml | 46 +++ 6 files changed, 716 insertions(+) create mode 100644 .github/arm-leases/README.md create mode 100644 .github/arm-leases/badtest/bad.test/noyaml.md create mode 100644 .github/arm-leases/testservice/Microsoft.TestRP/lease.yaml create mode 100644 .github/workflows/src/validate-arm-leases.js create mode 100644 .github/workflows/test/validate-arm-leases.test.js create mode 100644 .github/workflows/validate-arm-leases.yaml diff --git a/.github/arm-leases/README.md b/.github/arm-leases/README.md new file mode 100644 index 000000000000..7ed3d029088f --- /dev/null +++ b/.github/arm-leases/README.md @@ -0,0 +1,67 @@ +# 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 Program Managers (PMs) and RP owners to collaborate on API design, review specifications. + +**Important**: Only Program 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 in the lease file must also be a PM who has reviewed and approved the lease. + +## Code Owners and Contribution Guidelines + +This directory is protected by CODEOWNERS to ensure proper governance: + +- **Program 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 require approval from code owners (PMs and engineers listed in CODEOWNERS) + +**Adding New PMs**: To grant a new PM access to add lease files, they must be added to the CODEOWNERS file for the `.github/arm-leases/` directory path. + +## Lease File Structure + +Each lease must be placed in the following directory structure: +``` +.github/arm-leases///lease.yaml +``` + +### Path Requirements: +- ``: lowercase alphanumeric only (e.g., testservice, widgetservice) +- ``: alphanumeric with dots, case-sensitive (e.g., Microsoft.TestRP, Azure.Widget) + +### Lease File Format + +The `lease.yaml` file must follow this format: + +```yaml +lease: + resource-provider: Microsoft.TestRP # Must match the namespace folder name + startdate: 2026-01-07 # ISO 8601 format (YYYY-MM-DD) + duration: 180 days # Maximum allowed is 180 days + reviewer: Evan Hissey # Name of the approving reviewer +``` + +## 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 exact folder structure: `//lease.yaml` + +### 2. Resource Provider Name +- Must match the namespace 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` +- Cannot be in the past (must be today or a future date) + +### 4. Duration +- Required field that cannot be empty +- Must be in the format: `X days` (e.g., `90 days`, `180 days`) +- **Cannot exceed 180 days** (maximum lease period) +- Must be greater than 0 days + +### 5. Reviewer +- Required field that cannot be empty +- Must contain the name of the PM who approved the lease +- Only PMs can be listed as reviewers \ No newline at end of file diff --git a/.github/arm-leases/badtest/bad.test/noyaml.md b/.github/arm-leases/badtest/bad.test/noyaml.md new file mode 100644 index 000000000000..bc3dcfa4839a --- /dev/null +++ b/.github/arm-leases/badtest/bad.test/noyaml.md @@ -0,0 +1,5 @@ +lease: + resource-provider: Microsoft.TestRP + startdate: 2026-07-01 # ISO 8601 format (YYYY-MM-DD) + duration: 180 days # Maximum allowed is 180 days + reviewer: Tejaswi Salaigari \ No newline at end of file diff --git a/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml b/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/.github/workflows/src/validate-arm-leases.js b/.github/workflows/src/validate-arm-leases.js new file mode 100644 index 000000000000..73628052535f --- /dev/null +++ b/.github/workflows/src/validate-arm-leases.js @@ -0,0 +1,267 @@ +#!/usr/bin/env node + +import { execSync } from 'child_process'; +import { readFileSync, existsSync } from 'fs'; +import { join } from 'path'; +import YAML from 'yaml'; +import { getChangedFiles } from '../../shared/src/changed-files.js'; + +// ============================================ +// Configuration +// ============================================ + +const ALLOWED_FILE_PATTERNS = [ + /^\.github\/arm-leases\//, + /^\.github\/workflows\/validate-arm-leases\.yaml$/, + /^\.github\/workflows\/src\/validate-arm-leases\.js$/ +]; + +const LEASE_FILE_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/lease\.yaml$/; +const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; + +// ============================================ +// 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 + */ +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 + */ +function validateFolderStructure(files) { + return files.filter(file => !LEASE_FILE_PATTERN.test(file)); +} + +/** + * Parse lease YAML file + * @param {string} filePath - Path to lease.yaml file + * @returns {{resourceProvider: string, startdate: string, duration: string, reviewer: string}|{error: string}} Parsed lease data or error object + */ +function parseLeaseFile(filePath) { + try { + const content = readFileSync(filePath, 'utf-8'); + const data = YAML.parse(content); + + if (!data || !data.lease) { + return { error: 'Invalid YAML structure: missing "lease" key' }; + } + + return { + resourceProvider: data.lease['resource-provider'] || '', + startdate: data.lease.startdate || '', + duration: data.lease.duration || '', + reviewer: data.lease.reviewer || '' + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + return { error: `Error reading file: ${errorMessage}` }; + } +} + +/** + * Validate lease file contents + * @param {string} leaseFile - Path to lease file (can be full or relative) + * @param {string} today - Today's date in YYYY-MM-DD format + * @param {string} relativePath - Relative path for folder name extraction + * @returns {{file: string, errors: string[]}} Validation result with errors array + */ +function validateLeaseContent(leaseFile, today, relativePath) { + const errors = []; + const pathForExtraction = relativePath || leaseFile; + // Extract namespace from .github/arm-leases///lease.yaml + const folderRP = pathForExtraction.split('/')[2]; + + if (!existsSync(leaseFile)) { + return { file: leaseFile, errors: ['File does not exist'] }; + } + + const leaseData = parseLeaseFile(leaseFile); + + if (!leaseData) { + return { file: leaseFile, errors: ['Failed to parse lease file'] }; + } + + if ('error' in leaseData) { + return { file: leaseFile, errors: [leaseData.error] }; + } + + // Validation 1: Resource provider name matches folder name + if (leaseData.resourceProvider !== folderRP) { + errors.push(`Resource provider mismatch: folder=${folderRP}, yaml=${leaseData.resourceProvider}`); + } + + // Validation 1b: Resource provider must start with a capital letter (before and after dot) + if (leaseData.resourceProvider) { + const parts = leaseData.resourceProvider.split('.'); + const invalidParts = parts.filter(part => part && !/^[A-Z]/.test(part)); + if (invalidParts.length > 0) { + errors.push(`Resource provider parts must start with a capital letter: ${leaseData.resourceProvider} (e.g., Microsoft.Test, Azure.Widget)`); + } + } + + // Validation 2: Startdate format (ISO 8601: YYYY-MM-DD) + if (!DATE_PATTERN.test(leaseData.startdate)) { + errors.push(`Invalid startdate format: ${leaseData.startdate} (expected: YYYY-MM-DD)`); + } else { + // Validation 3: Startdate is today or future (not past) + if (leaseData.startdate < today) { + errors.push(`Startdate is in the past: ${leaseData.startdate} (today: ${today})`); + } + } + + // Validation 4: Duration validation (must not exceed 180 days) + if (!leaseData.duration) { + errors.push('Duration is missing (expected format: "180 days")'); + } else { + const durationMatch = leaseData.duration.match(/^(\d+)\s*days?$/i); + if (!durationMatch) { + errors.push(`Invalid duration format: ${leaseData.duration} (expected format: "180 days")`); + } else { + const durationDays = parseInt(durationMatch[1], 10); + if (durationDays > 180) { + errors.push(`Duration exceeds maximum allowed: ${durationDays} days (maximum: 180 days)`); + } + if (durationDays <= 0) { + errors.push(`Duration must be greater than 0: ${durationDays} days`); + } + } + } + + // Validation 5: Reviewer cannot be null or empty + if (!leaseData.reviewer || leaseData.reviewer.trim() === '') { + errors.push('Reviewer is required and cannot be empty'); + } + + return { file: leaseFile, errors }; +} + +// ============================================ +// Main Validation Logic +// ============================================ + +async function main() { + const baseBranch = process.argv[2] || 'main'; + const today = new Date().toISOString().split('T')[0]; + const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim(); + let exitCode = 0; + + console.log('Running ARM Lease File Validation\n'); + + // Step 1: Get all changed files + const allChangedFiles = await getChangedFiles({ + baseCommitish: `origin/${baseBranch}`, + headCommitish: 'HEAD', + paths: ['.github/arm-leases/', '.github/'] + }); + + // Step 2: Check for disallowed files + const disallowedFiles = allChangedFiles.filter(file => !isFileAllowed(file)); + + if (disallowedFiles.length > 0) { + console.log(`Found ${disallowedFiles.length} file(s) outside the .github/arm-leases/ directory. Only changes within .github/arm-leases/ directory are allowed. Please move the following files under .github/arm-leases/ directory:\n`); + console.log('Disallowed files:'); + disallowedFiles.slice(0, 20).forEach(file => console.log(` ${file}`)); + if (disallowedFiles.length > 20) { + console.log(` ... and ${disallowedFiles.length - 20} more files`); + } + console.log(''); + exitCode = 1; + } + + // Step 3: Get ARM lease files + const armLeaseFiles = allChangedFiles.filter(file => + file.startsWith('.github/arm-leases/') && !file.endsWith('.md') + ); + + if (armLeaseFiles.length === 0) { + if (exitCode === 0) { + console.log('No ARM lease files to validate'); + } + process.exit(exitCode); + } + + console.log(`Found ${armLeaseFiles.length} ARM lease file(s) to validate\n`); + + // Step 4: Check for non-lease.yaml files + const nonLeaseFiles = armLeaseFiles.filter(file => !file.endsWith('/lease.yaml')); + + if (nonLeaseFiles.length > 0) { + console.log(`Found ${nonLeaseFiles.length} file(s) that are not lease.yaml:`); + nonLeaseFiles.forEach(file => console.log(` ${file}`)); + console.log('\nOnly lease.yaml files are allowed in .github/arm-leases/ directory\n'); + exitCode = 1; + } + + // Step 5: Validate folder structure + const invalidStructure = validateFolderStructure(armLeaseFiles); + + if (invalidStructure.length > 0) { + console.log(`${invalidStructure.length} file(s) with invalid folder structure:`); + invalidStructure.forEach(file => console.log(` ${file}`)); + console.log('\nExpected format: .github/arm-leases///lease.yaml'); + console.log('Requirements:'); + console.log(' - : lowercase alphanumeric only (e.g., testservice, widgetservice)'); + console.log(' - : alphanumeric with dots and case-sensitive (e.g., test.Rp, Widget.Manager)'); + console.log(' - Only lease.yaml files are allowed in arm-leases folder'); + console.log('Examples:'); + console.log(' - .github/arm-leases/testservice/test.Rp/lease.yaml'); + console.log(' - .github/arm-leases/widgetservice/Widget.Manager/lease.yaml\n'); + exitCode = 1; + } + + // Step 6: Validate lease file contents + const validLeaseFiles = armLeaseFiles.filter(file => LEASE_FILE_PATTERN.test(file)); + + if (validLeaseFiles.length === 0) { + printSummary(exitCode); + process.exit(exitCode); + } + + const contentErrors = []; + + for (const leaseFile of validLeaseFiles) { + const fullPath = join(repoRoot, leaseFile); + const result = validateLeaseContent(fullPath, today, leaseFile); + if (result.errors.length > 0) { + // Store with relative path for display + contentErrors.push({ file: leaseFile, errors: result.errors }); + exitCode = 1; + } + } + + // Step 7: Print lease file content errors + if (contentErrors.length > 0) { + console.log('Lease content validation errors:\n'); + contentErrors.forEach(({ file, errors }) => { + console.log(`${file}`); + errors.forEach(error => console.log(` - ${error}`)); + console.log(''); + }); + } + + printSummary(exitCode); + process.exit(exitCode); +} + +/** + * Print final validation summary + * @param {number} exitCode - Exit code (0 = success, 1 = failure) + */ +function printSummary(exitCode) { + if (exitCode === 0) { + console.log('All validations passed!'); + } else { + console.log('ARM Lease Validation failed - fix errors above'); + } +} + +main(); \ No newline at end of file diff --git a/.github/workflows/test/validate-arm-leases.test.js b/.github/workflows/test/validate-arm-leases.test.js new file mode 100644 index 000000000000..6b88ef73ffd6 --- /dev/null +++ b/.github/workflows/test/validate-arm-leases.test.js @@ -0,0 +1,331 @@ +import { describe, it, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert'; +import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs'; +import { join } from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Test directory setup +const TEST_DIR = join(__dirname, 'test-temp'); +const ARM_LEASES_DIR = join(TEST_DIR, '.github', 'arm-leases'); + +// Helper function to create test lease file +function createLeaseFile(serviceName, namespace, content) { + const leaseDir = join(ARM_LEASES_DIR, serviceName, namespace); + mkdirSync(leaseDir, { recursive: true }); + const leaseFilePath = join(leaseDir, 'lease.yaml'); + writeFileSync(leaseFilePath, content); + return leaseFilePath; +} + +// Helper function to get relative path +function getRelativePath(fullPath) { + return fullPath.replace(TEST_DIR + '/', ''); +} + +describe('ARM Leases Validation Tests', () => { + beforeEach(() => { + // Clean up and create fresh test directory + if (existsSync(TEST_DIR)) { + rmSync(TEST_DIR, { recursive: true, force: true }); + } + mkdirSync(ARM_LEASES_DIR, { recursive: true }); + }); + + afterEach(() => { + // Clean up test directory + if (existsSync(TEST_DIR)) { + rmSync(TEST_DIR, { recursive: true, force: true }); + } + }); + + describe('File Pattern Validation', () => { + it('should accept valid lease file pattern', () => { + const validPaths = [ + '.github/arm-leases/testservice/Microsoft.Test/lease.yaml', + '.github/arm-leases/widgetservice/Azure.Widget/lease.yaml', + '.github/arm-leases/service123/Contoso.Manager/lease.yaml' + ]; + + const LEASE_FILE_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/lease\.yaml$/; + + validPaths.forEach(path => { + assert.ok(LEASE_FILE_PATTERN.test(path), `${path} should be valid`); + }); + }); + + it('should reject invalid lease file patterns', () => { + const invalidPaths = [ + '.github/arm-leases/TestService/Microsoft.Test/lease.yaml', // uppercase service name + '.github/arm-leases/test-service/Microsoft.Test/lease.yaml', // hyphen in service name + '.github/arm-leases/testservice/Microsoft.Test/other.yaml', // not lease.yaml + 'arm-leases/testservice/Microsoft.Test/lease.yaml', // missing .github + ]; + + const LEASE_FILE_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/lease\.yaml$/; + + invalidPaths.forEach(path => { + assert.ok(!LEASE_FILE_PATTERN.test(path), `${path} should be invalid`); + }); + }); + }); + + describe('Resource Provider Validation', () => { + it('should validate resource provider starts with capital letter', () => { + const validProviders = [ + 'Microsoft.Test', + 'Azure.Widget', + 'Contoso.WidgetManager', + 'MyCompany.Service.Core' + ]; + + validProviders.forEach(provider => { + const parts = provider.split('.'); + const invalidParts = parts.filter(part => part && !/^[A-Z]/.test(part)); + assert.strictEqual(invalidParts.length, 0, `${provider} should be valid`); + }); + }); + + it('should reject resource provider with lowercase parts', () => { + const invalidProviders = [ + 'microsoft.Test', + 'Microsoft.test', + 'azure.Widget', + 'Contoso.widgetManager' + ]; + + invalidProviders.forEach(provider => { + const parts = provider.split('.'); + const invalidParts = parts.filter(part => part && !/^[A-Z]/.test(part)); + assert.ok(invalidParts.length > 0, `${provider} should be invalid`); + }); + }); + }); + + describe('Date Validation', () => { + it('should accept valid ISO 8601 date format', () => { + const validDates = [ + '2026-01-07', + '2026-12-31', + '2027-06-15' + ]; + + const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; + + validDates.forEach(date => { + assert.ok(DATE_PATTERN.test(date), `${date} should be valid`); + }); + }); + + it('should reject invalid date formats', () => { + const invalidDates = [ + '01-07-2026', // wrong order + '2026/01/07', // wrong separator + '2026-1-7', // missing leading zeros + '26-01-07', // two-digit year + 'January 7, 2026' // text format + ]; + + const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; + + invalidDates.forEach(date => { + assert.ok(!DATE_PATTERN.test(date), `${date} should be invalid`); + }); + }); + + it('should reject past dates', () => { + const today = '2026-01-07'; + const pastDate = '2026-01-06'; + + assert.ok(pastDate < today, 'Past date should be less than today'); + }); + + it('should accept today and future dates', () => { + const today = '2026-01-07'; + const todayDate = '2026-01-07'; + const futureDate = '2026-01-08'; + + assert.ok(todayDate >= today, 'Today should be valid'); + assert.ok(futureDate >= today, 'Future date should be valid'); + }); + }); + + describe('Duration Validation', () => { + it('should accept valid duration formats', () => { + const validDurations = [ + '90 days', + '180 days', + '1 day', + '30days', + '60 Days' + ]; + + validDurations.forEach(duration => { + const match = duration.match(/^(\d+)\s*days?$/i); + assert.ok(match !== null, `${duration} should match pattern`); + + const days = parseInt(match[1], 10); + assert.ok(days > 0, `${duration} should be greater than 0`); + assert.ok(days <= 180, `${duration} should not exceed 180 days`); + }); + }); + + it('should reject invalid duration formats', () => { + const invalidDurations = [ + '90', // missing 'days' + 'ninety days', // not numeric + '90 months', // wrong unit + '-10 days', // negative + '0 days' // zero + ]; + + invalidDurations.forEach(duration => { + const match = duration.match(/^(\d+)\s*days?$/i); + if (match) { + const days = parseInt(match[1], 10); + assert.ok(days <= 0, `${duration} should be invalid (0 or negative)`); + } else { + assert.ok(!match, `${duration} should not match pattern`); + } + }); + }); + + it('should reject duration exceeding 180 days', () => { + const excessiveDurations = ['181 days', '200 days', '365 days']; + + excessiveDurations.forEach(duration => { + const match = duration.match(/^(\d+)\s*days?$/i); + assert.ok(match !== null, `${duration} should match pattern`); + + const days = parseInt(match[1], 10); + assert.ok(days > 180, `${duration} should exceed 180 days`); + }); + }); + }); + + describe('Reviewer Validation', () => { + it('should accept non-empty reviewer names', () => { + const validReviewers = [ + 'John Doe', + 'Jane Smith', + 'PM Team' + ]; + + validReviewers.forEach(reviewer => { + assert.ok(reviewer && reviewer.trim() !== '', `${reviewer} should be valid`); + }); + }); + + it('should reject empty or null reviewers', () => { + const invalidReviewers = [ + '', + ' ', + null, + undefined + ]; + + invalidReviewers.forEach(reviewer => { + const isValid = reviewer && reviewer.trim() !== ''; + assert.ok(!isValid, `${reviewer} should be invalid`); + }); + }); + }); + + describe('Integration Tests', () => { + it('should validate complete valid lease file', () => { + const validLease = `lease: + resource-provider: Microsoft.Test + startdate: 2026-01-15 + duration: 90 days + reviewer: John Doe`; + + const leaseFile = createLeaseFile('testservice', 'Microsoft.Test', validLease); + assert.ok(existsSync(leaseFile), 'Lease file should be created'); + }); + + it('should detect resource provider mismatch', () => { + const invalidLease = `lease: + resource-provider: Microsoft.Other + startdate: 2026-01-15 + duration: 90 days + reviewer: John Doe`; + + // Creating in Microsoft.Test folder but declaring Microsoft.Other + const leaseFile = createLeaseFile('testservice', 'Microsoft.Test', invalidLease); + const folderRP = 'Microsoft.Test'; + const fileRP = 'Microsoft.Other'; + + assert.notStrictEqual(folderRP, fileRP, 'Resource providers should not match'); + }); + + it('should detect invalid startdate format', () => { + const invalidLease = `lease: + resource-provider: Microsoft.Test + startdate: 01-15-2026 + duration: 90 days + reviewer: John Doe`; + + const leaseFile = createLeaseFile('testservice', 'Microsoft.Test', invalidLease); + + const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; + const invalidDate = '01-15-2026'; + assert.ok(!DATE_PATTERN.test(invalidDate), 'Date format should be invalid'); + }); + + it('should detect missing reviewer', () => { + const invalidLease = `lease: + resource-provider: Microsoft.Test + startdate: 2026-01-15 + duration: 90 days + reviewer: `; + + const leaseFile = createLeaseFile('testservice', 'Microsoft.Test', invalidLease); + const reviewer = ''; + + assert.ok(!reviewer || reviewer.trim() === '', 'Reviewer should be empty'); + }); + }); + + describe('Allowed File Patterns', () => { + it('should allow files in arm-leases directory', () => { + const ALLOWED_FILE_PATTERNS = [ + /^\.github\/arm-leases\//, + /^\.github\/workflows\/validate-arm-leases\.yaml$/, + /^\.github\/workflows\/src\/validate-arm-leases\.js$/ + ]; + + const allowedFiles = [ + '.github/arm-leases/testservice/Microsoft.Test/lease.yaml', + '.github/workflows/validate-arm-leases.yaml', + '.github/workflows/src/validate-arm-leases.js' + ]; + + allowedFiles.forEach(file => { + const isAllowed = ALLOWED_FILE_PATTERNS.some(pattern => pattern.test(file)); + assert.ok(isAllowed, `${file} should be allowed`); + }); + }); + + it('should reject files outside allowed patterns', () => { + const ALLOWED_FILE_PATTERNS = [ + /^\.github\/arm-leases\//, + /^\.github\/workflows\/validate-arm-leases\.yaml$/, + /^\.github\/workflows\/src\/validate-arm-leases\.js$/ + ]; + + const disallowedFiles = [ + 'specification/testservice/readme.md', + '.github/other-file.yaml', + 'arm-leases/testservice/Microsoft.Test/lease.yaml' + ]; + + disallowedFiles.forEach(file => { + const isAllowed = ALLOWED_FILE_PATTERNS.some(pattern => pattern.test(file)); + assert.ok(!isAllowed, `${file} should be disallowed`); + }); + }); + }); +}); diff --git a/.github/workflows/validate-arm-leases.yaml b/.github/workflows/validate-arm-leases.yaml new file mode 100644 index 000000000000..83362431568c --- /dev/null +++ b/.github/workflows/validate-arm-leases.yaml @@ -0,0 +1,46 @@ +name: Validate ARM Leases + +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/**" + +permissions: + contents: read + +jobs: + validate-arm-leases: + name: Validate ARM Leases + runs-on: ubuntu-24.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + + - name: Install dependencies + run: npm ci + working-directory: .github + + - name: Validate ARM leases + uses: actions/github-script@v8 + with: + result-encoding: string + script: | + const { default: validateArmLeases } = + await import('${{ github.workspace }}/.github/workflows/src/validate-arm-leases.js'); + return await validateArmLeases({ github, context, core }); \ No newline at end of file From 2a36e3cad671787d3b4f83f505c2c32e5e096d6a Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 7 Jan 2026 12:44:50 -0600 Subject: [PATCH 002/125] added test yaml --- .github/arm-leases/testservice/Microsoft.TestRP/lease.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml b/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml index e69de29bb2d1..bc3dcfa4839a 100644 --- a/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml +++ b/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml @@ -0,0 +1,5 @@ +lease: + resource-provider: Microsoft.TestRP + startdate: 2026-07-01 # ISO 8601 format (YYYY-MM-DD) + duration: 180 days # Maximum allowed is 180 days + reviewer: Tejaswi Salaigari \ No newline at end of file From c2233327e5804778afc18aade8b94f784b339172 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 7 Jan 2026 12:51:41 -0600 Subject: [PATCH 003/125] yaml package was missing --- .github/workflows/validate-arm-leases.yaml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/validate-arm-leases.yaml b/.github/workflows/validate-arm-leases.yaml index 83362431568c..db3de38d75fc 100644 --- a/.github/workflows/validate-arm-leases.yaml +++ b/.github/workflows/validate-arm-leases.yaml @@ -37,10 +37,5 @@ jobs: working-directory: .github - name: Validate ARM leases - uses: actions/github-script@v8 - with: - result-encoding: string - script: | - const { default: validateArmLeases } = - await import('${{ github.workspace }}/.github/workflows/src/validate-arm-leases.js'); - return await validateArmLeases({ github, context, core }); \ No newline at end of file + run: node workflows/src/validate-arm-leases.js ${{ github.event.pull_request.base.ref }} + working-directory: .github \ No newline at end of file From 30fbed28a31d5a56385c95b812aec656a4729363 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 7 Jan 2026 12:55:10 -0600 Subject: [PATCH 004/125] yaml package was missing --- .github/workflows/src/validate-arm-leases.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/src/validate-arm-leases.js b/.github/workflows/src/validate-arm-leases.js index 73628052535f..fb25e82eee05 100644 --- a/.github/workflows/src/validate-arm-leases.js +++ b/.github/workflows/src/validate-arm-leases.js @@ -3,7 +3,7 @@ import { execSync } from 'child_process'; import { readFileSync, existsSync } from 'fs'; import { join } from 'path'; -import YAML from 'yaml'; +import YAML from 'js-yaml'; import { getChangedFiles } from '../../shared/src/changed-files.js'; // ============================================ From e3dabae68e5e161cfbd202a61dfe8ba6159d4541 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 7 Jan 2026 13:00:09 -0600 Subject: [PATCH 005/125] workflow unable to fetch base branch --- .github/workflows/validate-arm-leases.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/validate-arm-leases.yaml b/.github/workflows/validate-arm-leases.yaml index db3de38d75fc..ac846a49d356 100644 --- a/.github/workflows/validate-arm-leases.yaml +++ b/.github/workflows/validate-arm-leases.yaml @@ -26,6 +26,10 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - name: Fetch base branch + run: git fetch origin ${{ github.event.pull_request.base.ref }} - name: Setup Node.js uses: actions/setup-node@v4 From f0c389d1ede3bdb11b778e531356097084481700 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 7 Jan 2026 13:07:13 -0600 Subject: [PATCH 006/125] added merge base to check proper comparison --- .github/workflows/src/validate-arm-leases.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/src/validate-arm-leases.js b/.github/workflows/src/validate-arm-leases.js index fb25e82eee05..0d921a91718e 100644 --- a/.github/workflows/src/validate-arm-leases.js +++ b/.github/workflows/src/validate-arm-leases.js @@ -156,11 +156,20 @@ async function main() { console.log('Running ARM Lease File Validation\n'); + // Get the merge base for proper PR comparison + let mergeBase; + try { + mergeBase = execSync(`git merge-base origin/${baseBranch} HEAD`, { encoding: 'utf-8' }).trim(); + } catch (error) { + console.log('Warning: Could not find merge-base, using origin/main directly'); + mergeBase = `origin/${baseBranch}`; + } + // Step 1: Get all changed files const allChangedFiles = await getChangedFiles({ - baseCommitish: `origin/${baseBranch}`, + baseCommitish: mergeBase, headCommitish: 'HEAD', - paths: ['.github/arm-leases/', '.github/'] + paths: ['.github/arm-leases/'] }); // Step 2: Check for disallowed files From 89d7708f277ebb41d69d2edad616b59fd97f36b4 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 7 Jan 2026 13:15:50 -0600 Subject: [PATCH 007/125] debug why arm-leases is not detected --- .github/workflows/src/validate-arm-leases.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/src/validate-arm-leases.js b/.github/workflows/src/validate-arm-leases.js index 0d921a91718e..bc2fc2eca2ae 100644 --- a/.github/workflows/src/validate-arm-leases.js +++ b/.github/workflows/src/validate-arm-leases.js @@ -165,13 +165,30 @@ async function main() { mergeBase = `origin/${baseBranch}`; } - // Step 1: Get all changed files + // Step 1: Get all changed files (first without filter to debug) + const allFiles = await getChangedFiles({ + baseCommitish: mergeBase, + headCommitish: 'HEAD' + }); + + console.log(`DEBUG: All changed files (${allFiles.length}):`); + allFiles.forEach(f => console.log(` ${f}`)); + console.log(''); + + // Now get only arm-leases files const allChangedFiles = await getChangedFiles({ baseCommitish: mergeBase, headCommitish: 'HEAD', paths: ['.github/arm-leases/'] }); + console.log(`Comparing ${mergeBase} to HEAD`); + console.log(`ARM lease files found: ${allChangedFiles.length}`); + if (allChangedFiles.length > 0) { + console.log('Changed files:', allChangedFiles); + } + console.log(''); + // Step 2: Check for disallowed files const disallowedFiles = allChangedFiles.filter(file => !isFileAllowed(file)); From 51ca6a1e7bc7ba8f663291bfedd8b6b2ae5003ed Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 7 Jan 2026 13:20:29 -0600 Subject: [PATCH 008/125] debug why arm-leases is not detected --- .github/workflows/src/validate-arm-leases.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/src/validate-arm-leases.js b/.github/workflows/src/validate-arm-leases.js index bc2fc2eca2ae..33ecc5943780 100644 --- a/.github/workflows/src/validate-arm-leases.js +++ b/.github/workflows/src/validate-arm-leases.js @@ -175,12 +175,8 @@ async function main() { allFiles.forEach(f => console.log(` ${f}`)); console.log(''); - // Now get only arm-leases files - const allChangedFiles = await getChangedFiles({ - baseCommitish: mergeBase, - headCommitish: 'HEAD', - paths: ['.github/arm-leases/'] - }); + // Filter for arm-leases files from all changed files + const allChangedFiles = allFiles.filter(file => file.startsWith('.github/arm-leases/')); console.log(`Comparing ${mergeBase} to HEAD`); console.log(`ARM lease files found: ${allChangedFiles.length}`); From e0846beef0728a3b7cd96a0ea65d89a2ddbedc7e Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 7 Jan 2026 13:30:34 -0600 Subject: [PATCH 009/125] debug --- .github/workflows/src/validate-arm-leases.js | 26 +++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/.github/workflows/src/validate-arm-leases.js b/.github/workflows/src/validate-arm-leases.js index 33ecc5943780..124802e18400 100644 --- a/.github/workflows/src/validate-arm-leases.js +++ b/.github/workflows/src/validate-arm-leases.js @@ -49,7 +49,7 @@ function validateFolderStructure(files) { function parseLeaseFile(filePath) { try { const content = readFileSync(filePath, 'utf-8'); - const data = YAML.parse(content); + const data = YAML.load(content); if (!data || !data.lease) { return { error: 'Invalid YAML structure: missing "lease" key' }; @@ -199,7 +199,19 @@ async function main() { exitCode = 1; } - // Step 3: Get ARM lease files + // Step 3: Check for non-lease.yaml and non-README files + const nonLeaseFiles = allChangedFiles.filter(file => + !file.endsWith('/lease.yaml') && !file.endsWith('/README.md') + ); + + if (nonLeaseFiles.length > 0) { + console.log(`āŒ Found ${nonLeaseFiles.length} file(s) that are not lease.yaml:`); + nonLeaseFiles.forEach(file => console.log(` ${file}`)); + console.log('\nāŒ Only lease.yaml and README.md files are allowed in .github/arm-leases/ directory\n'); + exitCode = 1; + } + + // Step 4: Get ARM lease files (only lease.yaml files) const armLeaseFiles = allChangedFiles.filter(file => file.startsWith('.github/arm-leases/') && !file.endsWith('.md') ); @@ -213,16 +225,6 @@ async function main() { console.log(`Found ${armLeaseFiles.length} ARM lease file(s) to validate\n`); - // Step 4: Check for non-lease.yaml files - const nonLeaseFiles = armLeaseFiles.filter(file => !file.endsWith('/lease.yaml')); - - if (nonLeaseFiles.length > 0) { - console.log(`Found ${nonLeaseFiles.length} file(s) that are not lease.yaml:`); - nonLeaseFiles.forEach(file => console.log(` ${file}`)); - console.log('\nOnly lease.yaml files are allowed in .github/arm-leases/ directory\n'); - exitCode = 1; - } - // Step 5: Validate folder structure const invalidStructure = validateFolderStructure(armLeaseFiles); From c570ea5ec418199c071d5630347af518a0eb8178 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 7 Jan 2026 13:49:27 -0600 Subject: [PATCH 010/125] remove debug logs --- .github/arm-leases/testservice/Microsoft.TestRP/lease.yaml | 2 +- .github/workflows/src/validate-arm-leases.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml b/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml index bc3dcfa4839a..70e7bb67a1d7 100644 --- a/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml +++ b/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml @@ -1,5 +1,5 @@ lease: resource-provider: Microsoft.TestRP - startdate: 2026-07-01 # ISO 8601 format (YYYY-MM-DD) + startdate: 2026-01-07 # ISO 8601 format (YYYY-MM-DD) duration: 180 days # Maximum allowed is 180 days reviewer: Tejaswi Salaigari \ No newline at end of file diff --git a/.github/workflows/src/validate-arm-leases.js b/.github/workflows/src/validate-arm-leases.js index 124802e18400..18df4eff7152 100644 --- a/.github/workflows/src/validate-arm-leases.js +++ b/.github/workflows/src/validate-arm-leases.js @@ -78,7 +78,7 @@ function validateLeaseContent(leaseFile, today, relativePath) { const errors = []; const pathForExtraction = relativePath || leaseFile; // Extract namespace from .github/arm-leases///lease.yaml - const folderRP = pathForExtraction.split('/')[2]; + const folderRP = pathForExtraction.split('/')[3]; if (!existsSync(leaseFile)) { return { file: leaseFile, errors: ['File does not exist'] }; @@ -205,9 +205,9 @@ async function main() { ); if (nonLeaseFiles.length > 0) { - console.log(`āŒ Found ${nonLeaseFiles.length} file(s) that are not lease.yaml:`); + console.log(`Found ${nonLeaseFiles.length} file(s) that are not lease.yaml:`); nonLeaseFiles.forEach(file => console.log(` ${file}`)); - console.log('\nāŒ Only lease.yaml and README.md files are allowed in .github/arm-leases/ directory\n'); + console.log('\nOnly lease.yaml files are allowed in .github/arm-leases/ directory\n'); exitCode = 1; } From 1fa798722120cb1fee96fae70d6b8cd11cc2fb5e Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 7 Jan 2026 13:54:53 -0600 Subject: [PATCH 011/125] remove debug logs --- .github/workflows/src/validate-arm-leases.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.github/workflows/src/validate-arm-leases.js b/.github/workflows/src/validate-arm-leases.js index 18df4eff7152..f8a020139d46 100644 --- a/.github/workflows/src/validate-arm-leases.js +++ b/.github/workflows/src/validate-arm-leases.js @@ -171,20 +171,9 @@ async function main() { headCommitish: 'HEAD' }); - console.log(`DEBUG: All changed files (${allFiles.length}):`); - allFiles.forEach(f => console.log(` ${f}`)); - console.log(''); - // Filter for arm-leases files from all changed files const allChangedFiles = allFiles.filter(file => file.startsWith('.github/arm-leases/')); - console.log(`Comparing ${mergeBase} to HEAD`); - console.log(`ARM lease files found: ${allChangedFiles.length}`); - if (allChangedFiles.length > 0) { - console.log('Changed files:', allChangedFiles); - } - console.log(''); - // Step 2: Check for disallowed files const disallowedFiles = allChangedFiles.filter(file => !isFileAllowed(file)); From 00f63e000eadd63c40cae1d28b95d790edf26173 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 7 Jan 2026 14:04:07 -0600 Subject: [PATCH 012/125] refined the code --- .github/workflows/src/validate-arm-leases.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/src/validate-arm-leases.js b/.github/workflows/src/validate-arm-leases.js index f8a020139d46..22f6111efaec 100644 --- a/.github/workflows/src/validate-arm-leases.js +++ b/.github/workflows/src/validate-arm-leases.js @@ -55,9 +55,15 @@ function parseLeaseFile(filePath) { return { error: 'Invalid YAML structure: missing "lease" key' }; } + // Convert Date objects to YYYY-MM-DD strings + let startdate = data.lease.startdate || ''; + if (startdate instanceof Date) { + startdate = startdate.toISOString().split('T')[0]; + } + return { resourceProvider: data.lease['resource-provider'] || '', - startdate: data.lease.startdate || '', + startdate: startdate, duration: data.lease.duration || '', reviewer: data.lease.reviewer || '' }; From 61119eb07524a37885e42abfb81cfde85d05ed46 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 7 Jan 2026 14:13:48 -0600 Subject: [PATCH 013/125] refined the code --- .github/workflows/src/validate-arm-leases.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/src/validate-arm-leases.js b/.github/workflows/src/validate-arm-leases.js index 22f6111efaec..f5421271c8e0 100644 --- a/.github/workflows/src/validate-arm-leases.js +++ b/.github/workflows/src/validate-arm-leases.js @@ -201,8 +201,8 @@ async function main() { if (nonLeaseFiles.length > 0) { console.log(`Found ${nonLeaseFiles.length} file(s) that are not lease.yaml:`); - nonLeaseFiles.forEach(file => console.log(` ${file}`)); - console.log('\nOnly lease.yaml files are allowed in .github/arm-leases/ directory\n'); + nonLeaseFiles.forEach(file => console.log(`Remove or rename - ${file}`)); + console.log('Only lease.yaml files are allowed in .github/arm-leases/ directory\n'); exitCode = 1; } @@ -213,26 +213,24 @@ async function main() { if (armLeaseFiles.length === 0) { if (exitCode === 0) { - console.log('No ARM lease files to validate'); + console.log('--------- No ARM lease files to validate ------------'); } process.exit(exitCode); } - console.log(`Found ${armLeaseFiles.length} ARM lease file(s) to validate\n`); - // Step 5: Validate folder structure const invalidStructure = validateFolderStructure(armLeaseFiles); if (invalidStructure.length > 0) { console.log(`${invalidStructure.length} file(s) with invalid folder structure:`); invalidStructure.forEach(file => console.log(` ${file}`)); - console.log('\nExpected format: .github/arm-leases///lease.yaml'); + console.log('Expected format: .github/arm-leases///lease.yaml'); console.log('Requirements:'); console.log(' - : lowercase alphanumeric only (e.g., testservice, widgetservice)'); - console.log(' - : alphanumeric with dots and case-sensitive (e.g., test.Rp, Widget.Manager)'); + console.log(' - : alphanumeric with dots and case-sensitive (e.g., Test.Rp, Widget.Manager)'); console.log(' - Only lease.yaml files are allowed in arm-leases folder'); console.log('Examples:'); - console.log(' - .github/arm-leases/testservice/test.Rp/lease.yaml'); + console.log(' - .github/arm-leases/testservice/Test.Rp/lease.yaml'); console.log(' - .github/arm-leases/widgetservice/Widget.Manager/lease.yaml\n'); exitCode = 1; } From e5d76c36fd5ebc03fcc6c6a585b5a7233ec2445b Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Tue, 13 Jan 2026 15:30:25 -0600 Subject: [PATCH 014/125] Detect new Resource Provider Namespaces, check and validate arm-leases for the new RP's --- .../detect-new-resource-provider.yaml | 85 ++++ .github/workflows/src/detect-arm-leases.js | 73 ++++ .../src/detect-new-resource-provider.js | 170 ++++++++ .../workflows/test/detect-arm-leases.test.js | 177 ++++++++ .../test/detect-new-resource-provider.test.js | 406 ++++++++++++++++++ 5 files changed, 911 insertions(+) create mode 100644 .github/workflows/detect-new-resource-provider.yaml create mode 100644 .github/workflows/src/detect-arm-leases.js create mode 100644 .github/workflows/src/detect-new-resource-provider.js create mode 100644 .github/workflows/test/detect-arm-leases.test.js create mode 100644 .github/workflows/test/detect-new-resource-provider.test.js diff --git a/.github/workflows/detect-new-resource-provider.yaml b/.github/workflows/detect-new-resource-provider.yaml new file mode 100644 index 000000000000..20671ad878d9 --- /dev/null +++ b/.github/workflows/detect-new-resource-provider.yaml @@ -0,0 +1,85 @@ +name: Detect New Resource Provider + +on: + pull_request: + types: + - opened + - synchronize + - reopened + paths: + - "specification/**/resource-manager/**" + +permissions: + contents: read + pull-requests: write + +env: + ENABLE_NEW_RP_DETECTION: false + +jobs: + detect-new-rp: + name: Detect New Resource Provider + runs-on: ubuntu-24.04 + if: env.ENABLE_NEW_RP_DETECTION == 'true' + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - name: Fetch base branch + run: git fetch origin ${{ github.event.pull_request.base.ref }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + + - name: Install dependencies + run: npm ci + working-directory: .github + + - name: Detect new resource providers + id: detect + run: node workflows/src/detect-new-resource-provider.js ${{ github.event.pull_request.base.ref }} + working-directory: .github + continue-on-error: true + + - name: Comment on PR + if: steps.detect.outcome == 'failure' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const outputFile = '.github/new-rp-output.json'; + + if (fs.existsSync(outputFile)) { + const data = JSON.parse(fs.readFileSync(outputFile, 'utf8')); + const rpList = data.newResourceProviders.map(rp => `- \`${rp}\``).join('\n'); + + const body = `## šŸ†• New Resource Provider Detected + +The following new resource provider namespace(s) were detected in this PR: + +${rpList} + +### āš ļø Action Required + +New resource provider namespaces require attending **ARM API Modeling Office Hours** before merging. + +šŸ“… **Please schedule here:** https://outlook.office365.com/book/ARMOfficeHours1@microsoft.onmicrosoft.com/?ismsaljsauthenabled=true + +During the office hours, the ARM team will review: +- Resource provider namespace naming conventions +- API design patterns and best practices +- Registration requirements for Azure environments`; + + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + } diff --git a/.github/workflows/src/detect-arm-leases.js b/.github/workflows/src/detect-arm-leases.js new file mode 100644 index 000000000000..016be12d036a --- /dev/null +++ b/.github/workflows/src/detect-arm-leases.js @@ -0,0 +1,73 @@ +import { existsSync, readFileSync } from 'fs'; +import { join } from 'path'; +import { execSync } from 'child_process'; +import YAML from 'js-yaml'; + +/** + * Check if ARM lease exists and is valid + * @param {string} serviceName - Service name + * @param {string} resourceProvider - Resource provider namespace + * @returns {boolean} True if lease exists and is valid, false otherwise + */ +export function checkLease(serviceName, resourceProvider) { + try { + const repoRoot = process.env.TEST_REPO_ROOT || execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim(); + const leasePath = join(repoRoot, '.github', 'arm-leases', serviceName, resourceProvider, 'lease.yaml'); + + if (!existsSync(leasePath)) { + return false; + } + + const content = readFileSync(leasePath, 'utf-8'); + const parsed = YAML.load(content); + const lease = parsed.lease; + + let startdate = lease.startdate; + if (startdate instanceof Date) { + startdate = startdate.toISOString().split('T')[0]; + } + + const durationDays = parseInt(lease.duration.match(/^(\d+)\s*days?$/i)[1], 10); + const endDate = new Date(startdate); + endDate.setDate(endDate.getDate() + durationDays); + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + return today <= endDate; + } catch (error) { + return false; + } +} + +function main() { + const serviceName = process.argv[2]; + const resourceProvider = process.argv[3]; + + if (!serviceName || !resourceProvider) { + console.error('Usage: node detect-arm-leases.js '); + process.exit(1); + } + + const result = checkLease(serviceName, resourceProvider); + + if (!result) { + const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim(); + const leasePath = join(repoRoot, '.github', 'arm-leases', serviceName, resourceProvider, 'lease.yaml'); + + if (!existsSync(leasePath)) { + console.log('No lease file found'); + } else { + console.error('Lease has expired'); + } + process.exit(1); + } + + console.log('Lease is valid'); + process.exit(0); +} + +// Only run main if this file is executed directly (not imported) +if (process.argv[1] && process.argv[1].endsWith('detect-arm-leases.js')) { + main(); +} diff --git a/.github/workflows/src/detect-new-resource-provider.js b/.github/workflows/src/detect-new-resource-provider.js new file mode 100644 index 000000000000..c841d97bd2e6 --- /dev/null +++ b/.github/workflows/src/detect-new-resource-provider.js @@ -0,0 +1,170 @@ +#!/usr/bin/env node + +import { execSync } from 'child_process'; +import { readdirSync, existsSync, statSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { getChangedFiles } from '../../shared/src/changed-files.js'; +import { checkLease } from './detect-arm-leases.js'; + +// ============================================ +// Configuration +// ============================================ + +const SPECIFICATION_PATH = 'specification'; +// Match pattern: specification//resource-manager//... +const RESOURCE_MANAGER_PATTERN = /^specification\/[^\/]+\/resource-manager\/([^\/]+)/; + +// ============================================ +// Utility Functions +// ============================================ + +/** + * Check if a resource provider namespace exists in the specification directory + * @param {string} specPath - Path to specification directory + * @param {string} namespace - Resource provider namespace (e.g., Microsoft.App) + * @returns {boolean} True if namespace exists + */ +function resourceProviderExists(specPath, namespace) { + if (!existsSync(specPath)) { + return false; + } + + try { + const serviceDirs = readdirSync(specPath); + + for (const serviceDir of serviceDirs) { + const servicePath = join(specPath, serviceDir); + if (!statSync(servicePath).isDirectory()) continue; + + const rmPath = join(servicePath, 'resource-manager'); + if (!existsSync(rmPath)) continue; + + const namespacePath = join(rmPath, namespace); + if (existsSync(namespacePath) && statSync(namespacePath).isDirectory()) { + return true; + } + } + } catch (error) { + console.error('Error checking resource provider existence:', error.message); + } + + return false; +} + +/** + * Extract resource provider namespaces and service names from file paths + * @param {string[]} files - Array of file paths + * @returns {Map} Map of namespace to service name + */ +function extractResourceProviders(files) { + const resourceProviders = new Map(); + + for (const file of files) { + const match = file.match(RESOURCE_MANAGER_PATTERN); + if (match) { + const parts = file.split('/'); + const serviceName = parts[1]; + const namespace = match[1]; + resourceProviders.set(namespace, serviceName); + } + } + + return resourceProviders; +} + +// ============================================ +// Main Detection Logic +// ============================================ + +async function main() { + const baseBranch = process.argv[2] || 'main'; + const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim(); + let exitCode = 0; + + console.log('Detecting New Resource Providers\n'); + + // Get the merge base for proper PR comparison + let mergeBase; + try { + mergeBase = execSync(`git merge-base origin/${baseBranch} HEAD`, { encoding: 'utf-8' }).trim(); + } catch (error) { + console.log('Warning: Could not find merge-base, using origin/main directly'); + mergeBase = `origin/${baseBranch}`; + } + + // Step 1: Get all changed files in specification/*/resource-manager/ + const allFiles = await getChangedFiles({ + baseCommitish: mergeBase, + headCommitish: 'HEAD' + }); + + const rmFiles = allFiles.filter(file => + file.includes('/resource-manager/') && file.startsWith('specification/') + ); + + console.log(`Found ${rmFiles.length} changed file(s) in resource-manager directories\n`); + + if (rmFiles.length === 0) { + console.log('No resource-manager files changed'); + process.exit(0); + } + + // Step 2: Extract resource providers from changed files + const changedResourceProviders = extractResourceProviders(rmFiles); + + console.log(`Resource provider namespaces in changed files: ${changedResourceProviders.size}`); + changedResourceProviders.forEach((serviceName, rp) => console.log(` - ${rp} (service: ${serviceName})`)); + + // Step 3: Check which resource providers are new (don't exist in main branch) + const specPath = join(repoRoot, SPECIFICATION_PATH); + const newResourceProviders = []; + + for (const [rp, serviceName] of changedResourceProviders) { + if (!resourceProviderExists(specPath, rp)) { + newResourceProviders.push({ namespace: rp, serviceName }); + } + } + + // Step 4: Check ARM leases for new resource providers + const leaseCheckResults = []; + + if (newResourceProviders.length > 0) { + console.log(`\nšŸ†• Detected ${newResourceProviders.length} NEW resource provider namespace(s):\n`); + + for (const rp of newResourceProviders) { + console.log(` ${rp.namespace} (service: ${rp.serviceName})`); + + const leaseValid = checkLease(rp.serviceName, rp.namespace); + const leaseMessage = leaseValid ? 'Lease is valid' : 'No lease file found or lease has expired'; + + leaseCheckResults.push({ + namespace: rp.namespace, + serviceName: rp.serviceName, + leaseValid: leaseValid, + leaseMessage: leaseMessage + }); + + console.log(` Lease check: ${leaseMessage}`); + } + + console.log('\nNew resource provider namespaces require attending "ARM API Modeling Office Hours".'); + console.log('A comment will be posted on the PR with lease validation results.\n'); + + // Step 5: Write output for GitHub Actions to use in PR comment + const outputData = { + newResourceProviders: leaseCheckResults + }; + writeFileSync( + join(repoRoot, '.github', 'new-rp-output.json'), + JSON.stringify(outputData, null, 2) + ); + + exitCode = 1; + } else { + console.log('No new resource provider namespaces detected. All changes are to existing namespaces.\n'); + } + + process.exit(exitCode); +} + +main(); diff --git a/.github/workflows/test/detect-arm-leases.test.js b/.github/workflows/test/detect-arm-leases.test.js new file mode 100644 index 000000000000..08a413d4ed53 --- /dev/null +++ b/.github/workflows/test/detect-arm-leases.test.js @@ -0,0 +1,177 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs'; +import { join } from 'path'; +import { checkLease } from '../src/detect-arm-leases.js'; + +const TEST_ROOT = join(process.cwd(), '.test-arm-leases'); +const ARM_LEASES_DIR = join(TEST_ROOT, '.github', 'arm-leases'); + +describe('detect-arm-leases', () => { + beforeEach(() => { + if (existsSync(TEST_ROOT)) { + rmSync(TEST_ROOT, { recursive: true, force: true }); + } + mkdirSync(ARM_LEASES_DIR, { recursive: true }); + process.env.TEST_REPO_ROOT = TEST_ROOT; + }); + + afterEach(() => { + if (existsSync(TEST_ROOT)) { + rmSync(TEST_ROOT, { recursive: true, force: true }); + } + delete process.env.TEST_REPO_ROOT; + }); + + function createLeaseFile(serviceName, resourceProvider, startdate, duration) { + const leaseDir = join(ARM_LEASES_DIR, serviceName, resourceProvider); + mkdirSync(leaseDir, { recursive: true }); + + const leaseContent = `lease: + resource-provider: ${resourceProvider} + startdate: ${startdate} + duration: ${duration} + reviewer: Test Reviewer +`; + + writeFileSync(join(leaseDir, 'lease.yaml'), leaseContent); + } + + describe('checkLease', () => { + it('returns false when lease file does not exist', () => { + const result = checkLease('testservice', 'Microsoft.Test'); + expect(result).toBe(false); + }); + + it('returns true when lease is valid and not expired', () => { + const today = new Date(); + const startDate = new Date(today); + startDate.setDate(today.getDate() - 30); + + createLeaseFile( + 'testservice', + 'Microsoft.Test', + startDate.toISOString().split('T')[0], + '90 days' + ); + + const result = checkLease('testservice', 'Microsoft.Test'); + expect(result).toBe(true); + }); + + it('returns false when lease has expired', () => { + const today = new Date(); + const startDate = new Date(today); + startDate.setDate(today.getDate() - 100); + + createLeaseFile( + 'testservice', + 'Microsoft.Test', + startDate.toISOString().split('T')[0], + '90 days' + ); + + const result = checkLease('testservice', 'Microsoft.Test'); + expect(result).toBe(false); + }); + + it('returns true on the last day of lease', () => { + const today = new Date(); + const startDate = new Date(today); + startDate.setDate(today.getDate() - 89); + + createLeaseFile( + 'testservice', + 'Microsoft.Test', + startDate.toISOString().split('T')[0], + '90 days' + ); + + const result = checkLease('testservice', 'Microsoft.Test'); + expect(result).toBe(true); + }); + + it('returns false one day after lease expires', () => { + const today = new Date(); + const startDate = new Date(today); + startDate.setDate(today.getDate() - 91); + + createLeaseFile( + 'testservice', + 'Microsoft.Test', + startDate.toISOString().split('T')[0], + '90 days' + ); + + const result = checkLease('testservice', 'Microsoft.Test'); + expect(result).toBe(false); + }); + + it('handles different duration formats', () => { + const today = new Date(); + const startDate = new Date(today); + startDate.setDate(today.getDate() - 10); + + createLeaseFile( + 'testservice', + 'Microsoft.Test', + startDate.toISOString().split('T')[0], + '180 Days' + ); + + const result = checkLease('testservice', 'Microsoft.Test'); + expect(result).toBe(true); + }); + + it('handles single day duration', () => { + const today = new Date(); + + createLeaseFile( + 'testservice', + 'Microsoft.Test', + today.toISOString().split('T')[0], + '1 day' + ); + + const result = checkLease('testservice', 'Microsoft.Test'); + expect(result).toBe(true); + }); + + it('returns false for invalid lease file format', () => { + const leaseDir = join(ARM_LEASES_DIR, 'testservice', 'Microsoft.Test'); + mkdirSync(leaseDir, { recursive: true }); + writeFileSync(join(leaseDir, 'lease.yaml'), 'invalid: yaml: content'); + + const result = checkLease('testservice', 'Microsoft.Test'); + expect(result).toBe(false); + }); + + it('handles multiple services and namespaces', () => { + const today = new Date(); + const startDate = new Date(today); + startDate.setDate(today.getDate() - 30); + + createLeaseFile('app', 'Microsoft.App', startDate.toISOString().split('T')[0], '90 days'); + createLeaseFile('compute', 'Microsoft.Compute', startDate.toISOString().split('T')[0], '90 days'); + + expect(checkLease('app', 'Microsoft.App')).toBe(true); + expect(checkLease('compute', 'Microsoft.Compute')).toBe(true); + expect(checkLease('storage', 'Microsoft.Storage')).toBe(false); + }); + + it('handles future start dates', () => { + const today = new Date(); + const futureDate = new Date(today); + futureDate.setDate(today.getDate() + 10); + + createLeaseFile( + 'testservice', + 'Microsoft.Test', + futureDate.toISOString().split('T')[0], + '90 days' + ); + + const result = checkLease('testservice', 'Microsoft.Test'); + expect(result).toBe(true); + }); + }); +}); diff --git a/.github/workflows/test/detect-new-resource-provider.test.js b/.github/workflows/test/detect-new-resource-provider.test.js new file mode 100644 index 000000000000..dd437c8c37c9 --- /dev/null +++ b/.github/workflows/test/detect-new-resource-provider.test.js @@ -0,0 +1,406 @@ +import { describe, it, expect } from 'vitest'; + +// Mock functions for testing +function mockResourceProviderExists(specPath, namespace) { + const existingNamespaces = ['Microsoft.Existing', 'Microsoft.Another']; + return existingNamespaces.includes(namespace); +} + +function mockExtractResourceProviders(files) { + const resourceProviders = new Map(); + const pattern = /^specification\/([^\/]+)\/resource-manager\/([^\/]+)/; + + for (const file of files) { + const match = file.match(pattern); + if (match) { + const serviceName = match[1]; + const namespace = match[2]; + resourceProviders.set(namespace, serviceName); + } + } + + return resourceProviders; +} + +describe('detect-new-resource-provider', () => { + describe('extractResourceProviders', () => { + it('extracts namespace and service from valid paths', () => { + const files = [ + 'specification/app/resource-manager/Microsoft.App/stable/2023-01-01/app.json', + 'specification/compute/resource-manager/Microsoft.Compute/stable/2023-01-01/compute.json', + ]; + + const result = mockExtractResourceProviders(files); + + expect(result.size).toBe(2); + expect(result.get('Microsoft.App')).toBe('app'); + expect(result.get('Microsoft.Compute')).toBe('compute'); + }); + + it('handles multiple files from same namespace', () => { + const files = [ + 'specification/app/resource-manager/Microsoft.App/stable/2023-01-01/app.json', + 'specification/app/resource-manager/Microsoft.App/stable/2023-01-01/models.json', + 'specification/app/resource-manager/Microsoft.App/preview/2023-01-01-preview/app.json', + ]; + + const result = mockExtractResourceProviders(files); + + expect(result.size).toBe(1); + expect(result.get('Microsoft.App')).toBe('app'); + }); + + it('ignores non-resource-manager files', () => { + const files = [ + 'specification/app/data-plane/Microsoft.App/stable/2023-01-01/app.json', + 'specification/app/resource-manager/Microsoft.App/stable/2023-01-01/app.json', + 'specification/app/docs/readme.md', + ]; + + const result = mockExtractResourceProviders(files); + + expect(result.size).toBe(1); + expect(result.get('Microsoft.App')).toBe('app'); + }); + + it('handles empty file list', () => { + const files = []; + const result = mockExtractResourceProviders(files); + expect(result.size).toBe(0); + }); + + it('handles nested namespace paths', () => { + const files = [ + 'specification/test-service/resource-manager/Microsoft.TestService/stable/2023-01-01/operations.json', + ]; + + const result = mockExtractResourceProviders(files); + + expect(result.size).toBe(1); + expect(result.get('Microsoft.TestService')).toBe('test-service'); + }); + }); + + describe('resourceProviderExists', () => { + it('returns true for existing namespace', () => { + const result = mockResourceProviderExists('/path/to/spec', 'Microsoft.Existing'); + expect(result).toBe(true); + }); + + it('returns false for new namespace', () => { + const result = mockResourceProviderExists('/path/to/spec', 'Microsoft.NewService'); + expect(result).toBe(false); + }); + + it('returns false for non-matching namespace', () => { + const result = mockResourceProviderExists('/path/to/spec', 'Microsoft.Unknown'); + expect(result).toBe(false); + }); + }); + + describe('new resource provider detection logic', () => { + it('identifies new resource providers correctly', () => { + const changedFiles = [ + 'specification/newservice/resource-manager/Microsoft.NewService/stable/2023-01-01/service.json', + 'specification/existing/resource-manager/Microsoft.Existing/stable/2023-01-01/service.json', + ]; + + const changedResourceProviders = mockExtractResourceProviders(changedFiles); + const newResourceProviders = []; + + for (const [rp, serviceName] of changedResourceProviders) { + if (!mockResourceProviderExists('/spec', rp)) { + newResourceProviders.push({ namespace: rp, serviceName }); + } + } + + expect(newResourceProviders.length).toBe(1); + expect(newResourceProviders[0].namespace).toBe('Microsoft.NewService'); + expect(newResourceProviders[0].serviceName).toBe('newservice'); + }); + + it('handles no new resource providers', () => { + const changedFiles = [ + 'specification/existing/resource-manager/Microsoft.Existing/stable/2023-01-01/service.json', + 'specification/another/resource-manager/Microsoft.Another/stable/2023-01-01/service.json', + ]; + + const changedResourceProviders = mockExtractResourceProviders(changedFiles); + const newResourceProviders = []; + + for (const [rp, serviceName] of changedResourceProviders) { + if (!mockResourceProviderExists('/spec', rp)) { + newResourceProviders.push({ namespace: rp, serviceName }); + } + } + + expect(newResourceProviders.length).toBe(0); + }); + + it('handles multiple new resource providers', () => { + const changedFiles = [ + 'specification/service1/resource-manager/Microsoft.Service1/stable/2023-01-01/service.json', + 'specification/service2/resource-manager/Microsoft.Service2/stable/2023-01-01/service.json', + ]; + + const changedResourceProviders = mockExtractResourceProviders(changedFiles); + const newResourceProviders = []; + + for (const [rp, serviceName] of changedResourceProviders) { + if (!mockResourceProviderExists('/spec', rp)) { + newResourceProviders.push({ namespace: rp, serviceName }); + } + } + + expect(newResourceProviders.length).toBe(2); + expect(newResourceProviders[0].namespace).toBe('Microsoft.Service1'); + expect(newResourceProviders[1].namespace).toBe('Microsoft.Service2'); + }); + }); + + describe('lease validation integration', () => { + it('creates correct output structure with lease validation', () => { + const newResourceProviders = [ + { namespace: 'Microsoft.NewService', serviceName: 'newservice' } + ]; + + const leaseCheckResults = []; + + for (const rp of newResourceProviders) { + const leaseValid = false; // Mock lease check + const leaseMessage = 'No lease file found or lease has expired'; + + leaseCheckResults.push({ + namespace: rp.namespace, + serviceName: rp.serviceName, + leaseValid: leaseValid, + leaseMessage: leaseMessage + }); + } + + const outputData = { + newResourceProviders: leaseCheckResults + }; + + expect(outputData.newResourceProviders).toHaveLength(1); + expect(outputData.newResourceProviders[0]).toEqual({ + namespace: 'Microsoft.NewService', + serviceName: 'newservice', + leaseValid: false, + leaseMessage: 'No lease file found or lease has expired' + }); + }); + + it('handles valid lease in output', () => { + const newResourceProviders = [ + { namespace: 'Microsoft.ValidLease', serviceName: 'validservice' } + ]; + + const leaseCheckResults = []; + + for (const rp of newResourceProviders) { + const leaseValid = true; // Mock valid lease + const leaseMessage = 'Lease is valid'; + + leaseCheckResults.push({ + namespace: rp.namespace, + serviceName: rp.serviceName, + leaseValid: leaseValid, + leaseMessage: leaseMessage + }); + } + + const outputData = { + newResourceProviders: leaseCheckResults + }; + + expect(outputData.newResourceProviders[0].leaseValid).toBe(true); + expect(outputData.newResourceProviders[0].leaseMessage).toBe('Lease is valid'); + }); + }); + + describe('JSON output file generation', () => { + it('creates output JSON file with correct structure', () => { + const leaseCheckResults = [ + { + namespace: 'Microsoft.NewService', + serviceName: 'newservice', + leaseValid: false, + leaseMessage: 'No lease file found or lease has expired' + }, + { + namespace: 'Microsoft.AnotherNew', + serviceName: 'anothernew', + leaseValid: true, + leaseMessage: 'Lease is valid' + } + ]; + + const outputData = { + newResourceProviders: leaseCheckResults + }; + + expect(outputData).toHaveProperty('newResourceProviders'); + expect(Array.isArray(outputData.newResourceProviders)).toBe(true); + expect(outputData.newResourceProviders).toHaveLength(2); + }); + + it('includes all required fields for each resource provider', () => { + const leaseCheckResults = [ + { + namespace: 'Microsoft.Test', + serviceName: 'test', + leaseValid: false, + leaseMessage: 'No lease file found or lease has expired' + } + ]; + + const outputData = { + newResourceProviders: leaseCheckResults + }; + + const rp = outputData.newResourceProviders[0]; + expect(rp).toHaveProperty('namespace'); + expect(rp).toHaveProperty('serviceName'); + expect(rp).toHaveProperty('leaseValid'); + expect(rp).toHaveProperty('leaseMessage'); + expect(typeof rp.namespace).toBe('string'); + expect(typeof rp.serviceName).toBe('string'); + expect(typeof rp.leaseValid).toBe('boolean'); + expect(typeof rp.leaseMessage).toBe('string'); + }); + + it('formats output data for PR comment consumption', () => { + const leaseCheckResults = [ + { + namespace: 'Microsoft.NewRP', + serviceName: 'newrp', + leaseValid: false, + leaseMessage: 'No lease file found or lease has expired' + } + ]; + + const outputData = { + newResourceProviders: leaseCheckResults + }; + + const jsonString = JSON.stringify(outputData, null, 2); + const parsed = JSON.parse(jsonString); + + expect(parsed.newResourceProviders).toEqual(leaseCheckResults); + expect(jsonString).toContain('newResourceProviders'); + expect(jsonString).toContain('Microsoft.NewRP'); + }); + + it('handles multiple new RPs with mixed lease statuses', () => { + const leaseCheckResults = [ + { + namespace: 'Microsoft.Valid', + serviceName: 'valid', + leaseValid: true, + leaseMessage: 'Lease is valid' + }, + { + namespace: 'Microsoft.Expired', + serviceName: 'expired', + leaseValid: false, + leaseMessage: 'Lease has expired' + }, + { + namespace: 'Microsoft.Missing', + serviceName: 'missing', + leaseValid: false, + leaseMessage: 'No lease file found or lease has expired' + } + ]; + + const outputData = { + newResourceProviders: leaseCheckResults + }; + + expect(outputData.newResourceProviders).toHaveLength(3); + expect(outputData.newResourceProviders.filter(rp => rp.leaseValid)).toHaveLength(1); + expect(outputData.newResourceProviders.filter(rp => !rp.leaseValid)).toHaveLength(2); + }); + }); + + describe('PR comment data preparation', () => { + it('provides data for office hours attendance message', () => { + const leaseCheckResults = [ + { + namespace: 'Microsoft.NewService', + serviceName: 'newservice', + leaseValid: false, + leaseMessage: 'No lease file found or lease has expired' + } + ]; + + const outputData = { + newResourceProviders: leaseCheckResults + }; + + // Verify data structure for PR comment + expect(outputData.newResourceProviders.length).toBeGreaterThan(0); + + const hasNewRP = outputData.newResourceProviders.length > 0; + expect(hasNewRP).toBe(true); + + // Verify each RP has info needed for comment + outputData.newResourceProviders.forEach(rp => { + expect(rp.namespace).toBeTruthy(); + expect(rp.serviceName).toBeTruthy(); + expect(typeof rp.leaseValid).toBe('boolean'); + expect(rp.leaseMessage).toBeTruthy(); + }); + }); + + it('identifies RPs without valid lease for office hours requirement', () => { + const leaseCheckResults = [ + { + namespace: 'Microsoft.WithLease', + serviceName: 'withlease', + leaseValid: true, + leaseMessage: 'Lease is valid' + }, + { + namespace: 'Microsoft.WithoutLease', + serviceName: 'withoutlease', + leaseValid: false, + leaseMessage: 'No lease file found or lease has expired' + } + ]; + + const outputData = { + newResourceProviders: leaseCheckResults + }; + + const rpsNeedingOfficeHours = outputData.newResourceProviders.filter(rp => !rp.leaseValid); + const rpsWithValidLease = outputData.newResourceProviders.filter(rp => rp.leaseValid); + + expect(rpsNeedingOfficeHours).toHaveLength(1); + expect(rpsNeedingOfficeHours[0].namespace).toBe('Microsoft.WithoutLease'); + expect(rpsWithValidLease).toHaveLength(1); + expect(rpsWithValidLease[0].namespace).toBe('Microsoft.WithLease'); + }); + + it('formats lease path information for PR comment', () => { + const leaseCheckResults = [ + { + namespace: 'Microsoft.Test', + serviceName: 'testservice', + leaseValid: false, + leaseMessage: 'No lease file found or lease has expired' + } + ]; + + const outputData = { + newResourceProviders: leaseCheckResults + }; + + const rp = outputData.newResourceProviders[0]; + const expectedLeasePath = `.github/arm-leases/${rp.serviceName}/${rp.namespace}/lease.yaml`; + + expect(expectedLeasePath).toBe('.github/arm-leases/testservice/Microsoft.Test/lease.yaml'); + }); + }); +}); From 32ac2def8ff12e87925b1ce496d1ad7a6f518db9 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 14 Jan 2026 15:06:15 -0600 Subject: [PATCH 015/125] service group level leases --- .github/workflows/src/detect-arm-leases.js | 29 ++- .../src/detect-new-resource-provider.js | 41 ++-- .../test/detect-new-resource-provider.test.js | 217 ++++++++++++++++-- 3 files changed, 253 insertions(+), 34 deletions(-) diff --git a/.github/workflows/src/detect-arm-leases.js b/.github/workflows/src/detect-arm-leases.js index 016be12d036a..78c8ca77d296 100644 --- a/.github/workflows/src/detect-arm-leases.js +++ b/.github/workflows/src/detect-arm-leases.js @@ -3,16 +3,34 @@ import { join } from 'path'; import { execSync } from 'child_process'; import YAML from 'js-yaml'; +/** + * Build the lease path based on service information + * @param {string} repoRoot - Repository root path + * @param {string} serviceName - Service name + * @param {string} resourceProvider - Resource provider namespace + * @param {string} serviceGroup - Optional service group + * @returns {string} Full path to lease.yaml file + */ +function buildLeasePath(repoRoot, serviceName, resourceProvider, serviceGroup = '') { + const leasePathParts = [repoRoot, '.github', 'arm-leases', serviceName, resourceProvider]; + if (serviceGroup) { + leasePathParts.push(serviceGroup); + } + leasePathParts.push('lease.yaml'); + return join(...leasePathParts); +} + /** * Check if ARM lease exists and is valid * @param {string} serviceName - Service name * @param {string} resourceProvider - Resource provider namespace + * @param {string} serviceGroup - Optional service group * @returns {boolean} True if lease exists and is valid, false otherwise */ -export function checkLease(serviceName, resourceProvider) { +export function checkLease(serviceName, resourceProvider, serviceGroup = '') { try { const repoRoot = process.env.TEST_REPO_ROOT || execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim(); - const leasePath = join(repoRoot, '.github', 'arm-leases', serviceName, resourceProvider, 'lease.yaml'); + const leasePath = buildLeasePath(repoRoot, serviceName, resourceProvider, serviceGroup); if (!existsSync(leasePath)) { return false; @@ -43,17 +61,18 @@ export function checkLease(serviceName, resourceProvider) { function main() { const serviceName = process.argv[2]; const resourceProvider = process.argv[3]; + const serviceGroup = process.argv[4] || ''; if (!serviceName || !resourceProvider) { - console.error('Usage: node detect-arm-leases.js '); + console.error('Usage: node detect-arm-leases.js [service-group]'); process.exit(1); } - const result = checkLease(serviceName, resourceProvider); + const result = checkLease(serviceName, resourceProvider, serviceGroup); if (!result) { const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim(); - const leasePath = join(repoRoot, '.github', 'arm-leases', serviceName, resourceProvider, 'lease.yaml'); + const leasePath = buildLeasePath(repoRoot, serviceName, resourceProvider, serviceGroup); if (!existsSync(leasePath)) { console.log('No lease file found'); diff --git a/.github/workflows/src/detect-new-resource-provider.js b/.github/workflows/src/detect-new-resource-provider.js index c841d97bd2e6..8c1854e79c42 100644 --- a/.github/workflows/src/detect-new-resource-provider.js +++ b/.github/workflows/src/detect-new-resource-provider.js @@ -13,6 +13,9 @@ import { checkLease } from './detect-arm-leases.js'; const SPECIFICATION_PATH = 'specification'; // Match pattern: specification//resource-manager//... const RESOURCE_MANAGER_PATTERN = /^specification\/[^\/]+\/resource-manager\/([^\/]+)/; +// Match pattern with optional service group: specification//resource-manager///... +// ServiceGroup folder name should not start with "stable" or "preview" +const RESOURCE_MANAGER_WITH_GROUP_PATTERN = /^specification\/[^\/]+\/resource-manager\/([^\/]+)\/(?!stable|preview)([^\/]+)\//; // ============================================ // Utility Functions @@ -52,9 +55,9 @@ function resourceProviderExists(specPath, namespace) { } /** - * Extract resource provider namespaces and service names from file paths + * Extract resource provider namespaces, service names, and optional service groups from file paths * @param {string[]} files - Array of file paths - * @returns {Map} Map of namespace to service name + * @returns {Map} Map of namespace to service info */ function extractResourceProviders(files) { const resourceProviders = new Map(); @@ -65,7 +68,15 @@ function extractResourceProviders(files) { const parts = file.split('/'); const serviceName = parts[1]; const namespace = match[1]; - resourceProviders.set(namespace, serviceName); + + // Check if there's a service group + const groupMatch = file.match(RESOURCE_MANAGER_WITH_GROUP_PATTERN); + if (groupMatch) { + const serviceGroup = groupMatch[2]; + resourceProviders.set(namespace, { serviceName, serviceGroup }); + } else { + resourceProviders.set(namespace, { serviceName }); + } } } @@ -109,37 +120,35 @@ async function main() { process.exit(0); } - // Step 2: Extract resource providers from changed files + // Step 2: Extract resource providers and filter for new ones (don't exist in main branch) const changedResourceProviders = extractResourceProviders(rmFiles); - - console.log(`Resource provider namespaces in changed files: ${changedResourceProviders.size}`); - changedResourceProviders.forEach((serviceName, rp) => console.log(` - ${rp} (service: ${serviceName})`)); - - // Step 3: Check which resource providers are new (don't exist in main branch) const specPath = join(repoRoot, SPECIFICATION_PATH); const newResourceProviders = []; - for (const [rp, serviceName] of changedResourceProviders) { + for (const [rp, info] of changedResourceProviders) { if (!resourceProviderExists(specPath, rp)) { - newResourceProviders.push({ namespace: rp, serviceName }); + newResourceProviders.push({ + namespace: rp, + serviceName: info.serviceName, + serviceGroup: info.serviceGroup + }); } } - // Step 4: Check ARM leases for new resource providers + // Step 3: Check ARM leases for new resource providers const leaseCheckResults = []; if (newResourceProviders.length > 0) { console.log(`\nšŸ†• Detected ${newResourceProviders.length} NEW resource provider namespace(s):\n`); for (const rp of newResourceProviders) { - console.log(` ${rp.namespace} (service: ${rp.serviceName})`); - - const leaseValid = checkLease(rp.serviceName, rp.namespace); + const leaseValid = checkLease(rp.serviceName, rp.namespace, rp.serviceGroup || ''); const leaseMessage = leaseValid ? 'Lease is valid' : 'No lease file found or lease has expired'; leaseCheckResults.push({ namespace: rp.namespace, serviceName: rp.serviceName, + serviceGroup: rp.serviceGroup, leaseValid: leaseValid, leaseMessage: leaseMessage }); @@ -150,7 +159,7 @@ async function main() { console.log('\nNew resource provider namespaces require attending "ARM API Modeling Office Hours".'); console.log('A comment will be posted on the PR with lease validation results.\n'); - // Step 5: Write output for GitHub Actions to use in PR comment + // Step 4: Write output for GitHub Actions to use in PR comment const outputData = { newResourceProviders: leaseCheckResults }; diff --git a/.github/workflows/test/detect-new-resource-provider.test.js b/.github/workflows/test/detect-new-resource-provider.test.js index dd437c8c37c9..c5216613767e 100644 --- a/.github/workflows/test/detect-new-resource-provider.test.js +++ b/.github/workflows/test/detect-new-resource-provider.test.js @@ -9,13 +9,22 @@ function mockResourceProviderExists(specPath, namespace) { function mockExtractResourceProviders(files) { const resourceProviders = new Map(); const pattern = /^specification\/([^\/]+)\/resource-manager\/([^\/]+)/; + const groupPattern = /^specification\/[^\/]+\/resource-manager\/([^\/]+)\/([^\/]+)\/(stable|preview|preview-internal)\//; for (const file of files) { const match = file.match(pattern); if (match) { const serviceName = match[1]; const namespace = match[2]; - resourceProviders.set(namespace, serviceName); + + // Check if there's a service group (folder between namespace and version folder) + const groupMatch = file.match(groupPattern); + if (groupMatch && groupMatch[2] !== 'stable' && groupMatch[2] !== 'preview' && groupMatch[2] !== 'preview-internal') { + const serviceGroup = groupMatch[2]; + resourceProviders.set(namespace, { serviceName, serviceGroup }); + } else { + resourceProviders.set(namespace, { serviceName }); + } } } @@ -33,8 +42,8 @@ describe('detect-new-resource-provider', () => { const result = mockExtractResourceProviders(files); expect(result.size).toBe(2); - expect(result.get('Microsoft.App')).toBe('app'); - expect(result.get('Microsoft.Compute')).toBe('compute'); + expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); + expect(result.get('Microsoft.Compute')).toEqual({ serviceName: 'compute' }); }); it('handles multiple files from same namespace', () => { @@ -47,7 +56,7 @@ describe('detect-new-resource-provider', () => { const result = mockExtractResourceProviders(files); expect(result.size).toBe(1); - expect(result.get('Microsoft.App')).toBe('app'); + expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); }); it('ignores non-resource-manager files', () => { @@ -60,7 +69,7 @@ describe('detect-new-resource-provider', () => { const result = mockExtractResourceProviders(files); expect(result.size).toBe(1); - expect(result.get('Microsoft.App')).toBe('app'); + expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); }); it('handles empty file list', () => { @@ -77,7 +86,78 @@ describe('detect-new-resource-provider', () => { const result = mockExtractResourceProviders(files); expect(result.size).toBe(1); - expect(result.get('Microsoft.TestService')).toBe('test-service'); + expect(result.get('Microsoft.TestService')).toEqual({ serviceName: 'test-service' }); + }); + + it('extracts service group when present', () => { + const files = [ + 'specification/compute/resource-manager/Microsoft.Compute/DiskRP/stable/2023-01-01/disk.json', + ]; + + const result = mockExtractResourceProviders(files); + + expect(result.size).toBe(1); + expect(result.get('Microsoft.Compute')).toEqual({ serviceName: 'compute', serviceGroup: 'DiskRP' }); + }); + + it('handles mix of files with and without service groups', () => { + const files = [ + 'specification/compute/resource-manager/Microsoft.Compute/DiskRP/stable/2023-01-01/disk.json', + 'specification/app/resource-manager/Microsoft.App/stable/2023-01-01/app.json', + ]; + + const result = mockExtractResourceProviders(files); + + expect(result.size).toBe(2); + expect(result.get('Microsoft.Compute')).toEqual({ serviceName: 'compute', serviceGroup: 'DiskRP' }); + expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); + }); + + it('does not treat preview- as service group', () => { + const files = [ + 'specification/app/resource-manager/Microsoft.App/preview-2025-10-11/app.json', + ]; + + const result = mockExtractResourceProviders(files); + + expect(result.size).toBe(1); + expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); + }); + + it('does not treat preview-internal as service group', () => { + const files = [ + 'specification/app/resource-manager/Microsoft.App/preview-internal/2023-01-01/app.json', + ]; + + const result = mockExtractResourceProviders(files); + + expect(result.size).toBe(1); + expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); + }); + + it('does not treat stable- as service group', () => { + const files = [ + 'specification/app/resource-manager/Microsoft.App/stable-2024-01-01/app.json', + ]; + + const result = mockExtractResourceProviders(files); + + expect(result.size).toBe(1); + expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); + }); + + it('extracts service group when folder does not start with stable or preview', () => { + const files = [ + 'specification/compute/resource-manager/Microsoft.Compute/DiskRP/stable/2023-01-01/disk.json', + 'specification/compute/resource-manager/Microsoft.Compute/ComputeRP/preview/2023-01-01/compute.json', + 'specification/storage/resource-manager/Microsoft.Storage/StorageRP/stable/2023-01-01/storage.json', + ]; + + const result = mockExtractResourceProviders(files); + + expect(result.size).toBe(2); + expect(result.get('Microsoft.Compute')).toEqual({ serviceName: 'compute', serviceGroup: 'ComputeRP' }); + expect(result.get('Microsoft.Storage')).toEqual({ serviceName: 'storage', serviceGroup: 'StorageRP' }); }); }); @@ -108,15 +188,20 @@ describe('detect-new-resource-provider', () => { const changedResourceProviders = mockExtractResourceProviders(changedFiles); const newResourceProviders = []; - for (const [rp, serviceName] of changedResourceProviders) { + for (const [rp, info] of changedResourceProviders) { if (!mockResourceProviderExists('/spec', rp)) { - newResourceProviders.push({ namespace: rp, serviceName }); + newResourceProviders.push({ + namespace: rp, + serviceName: info.serviceName, + serviceGroup: info.serviceGroup + }); } } expect(newResourceProviders.length).toBe(1); expect(newResourceProviders[0].namespace).toBe('Microsoft.NewService'); expect(newResourceProviders[0].serviceName).toBe('newservice'); + expect(newResourceProviders[0].serviceGroup).toBeUndefined(); }); it('handles no new resource providers', () => { @@ -128,9 +213,13 @@ describe('detect-new-resource-provider', () => { const changedResourceProviders = mockExtractResourceProviders(changedFiles); const newResourceProviders = []; - for (const [rp, serviceName] of changedResourceProviders) { + for (const [rp, info] of changedResourceProviders) { if (!mockResourceProviderExists('/spec', rp)) { - newResourceProviders.push({ namespace: rp, serviceName }); + newResourceProviders.push({ + namespace: rp, + serviceName: info.serviceName, + serviceGroup: info.serviceGroup + }); } } @@ -146,9 +235,13 @@ describe('detect-new-resource-provider', () => { const changedResourceProviders = mockExtractResourceProviders(changedFiles); const newResourceProviders = []; - for (const [rp, serviceName] of changedResourceProviders) { + for (const [rp, info] of changedResourceProviders) { if (!mockResourceProviderExists('/spec', rp)) { - newResourceProviders.push({ namespace: rp, serviceName }); + newResourceProviders.push({ + namespace: rp, + serviceName: info.serviceName, + serviceGroup: info.serviceGroup + }); } } @@ -156,6 +249,30 @@ describe('detect-new-resource-provider', () => { expect(newResourceProviders[0].namespace).toBe('Microsoft.Service1'); expect(newResourceProviders[1].namespace).toBe('Microsoft.Service2'); }); + + it('identifies new resource providers with service groups', () => { + const changedFiles = [ + 'specification/compute/resource-manager/Microsoft.Compute/DiskRP/stable/2023-01-01/disk.json', + ]; + + const changedResourceProviders = mockExtractResourceProviders(changedFiles); + const newResourceProviders = []; + + for (const [rp, info] of changedResourceProviders) { + if (!mockResourceProviderExists('/spec', rp)) { + newResourceProviders.push({ + namespace: rp, + serviceName: info.serviceName, + serviceGroup: info.serviceGroup + }); + } + } + + expect(newResourceProviders.length).toBe(1); + expect(newResourceProviders[0].namespace).toBe('Microsoft.Compute'); + expect(newResourceProviders[0].serviceName).toBe('compute'); + expect(newResourceProviders[0].serviceGroup).toBe('DiskRP'); + }); }); describe('lease validation integration', () => { @@ -173,6 +290,7 @@ describe('detect-new-resource-provider', () => { leaseCheckResults.push({ namespace: rp.namespace, serviceName: rp.serviceName, + serviceGroup: rp.serviceGroup, leaseValid: leaseValid, leaseMessage: leaseMessage }); @@ -186,14 +304,49 @@ describe('detect-new-resource-provider', () => { expect(outputData.newResourceProviders[0]).toEqual({ namespace: 'Microsoft.NewService', serviceName: 'newservice', + serviceGroup: undefined, leaseValid: false, leaseMessage: 'No lease file found or lease has expired' }); }); + it('creates correct output structure with service group', () => { + const newResourceProviders = [ + { namespace: 'Microsoft.Compute', serviceName: 'compute', serviceGroup: 'DiskRP' } + ]; + + const leaseCheckResults = []; + + for (const rp of newResourceProviders) { + const leaseValid = true; // Mock lease check + const leaseMessage = 'Lease is valid'; + + leaseCheckResults.push({ + namespace: rp.namespace, + serviceName: rp.serviceName, + serviceGroup: rp.serviceGroup, + leaseValid: leaseValid, + leaseMessage: leaseMessage + }); + } + + const outputData = { + newResourceProviders: leaseCheckResults + }; + + expect(outputData.newResourceProviders).toHaveLength(1); + expect(outputData.newResourceProviders[0]).toEqual({ + namespace: 'Microsoft.Compute', + serviceName: 'compute', + serviceGroup: 'DiskRP', + leaseValid: true, + leaseMessage: 'Lease is valid' + }); + }); + it('handles valid lease in output', () => { const newResourceProviders = [ - { namespace: 'Microsoft.ValidLease', serviceName: 'validservice' } + { namespace: 'Microsoft.ValidLease', serviceName: 'validservice', serviceGroup: undefined } ]; const leaseCheckResults = []; @@ -225,12 +378,14 @@ describe('detect-new-resource-provider', () => { { namespace: 'Microsoft.NewService', serviceName: 'newservice', + serviceGroup: undefined, leaseValid: false, leaseMessage: 'No lease file found or lease has expired' }, { namespace: 'Microsoft.AnotherNew', serviceName: 'anothernew', + serviceGroup: undefined, leaseValid: true, leaseMessage: 'Lease is valid' } @@ -250,6 +405,7 @@ describe('detect-new-resource-provider', () => { { namespace: 'Microsoft.Test', serviceName: 'test', + serviceGroup: undefined, leaseValid: false, leaseMessage: 'No lease file found or lease has expired' } @@ -262,6 +418,7 @@ describe('detect-new-resource-provider', () => { const rp = outputData.newResourceProviders[0]; expect(rp).toHaveProperty('namespace'); expect(rp).toHaveProperty('serviceName'); + expect(rp).toHaveProperty('serviceGroup'); expect(rp).toHaveProperty('leaseValid'); expect(rp).toHaveProperty('leaseMessage'); expect(typeof rp.namespace).toBe('string'); @@ -275,6 +432,7 @@ describe('detect-new-resource-provider', () => { { namespace: 'Microsoft.NewRP', serviceName: 'newrp', + serviceGroup: undefined, leaseValid: false, leaseMessage: 'No lease file found or lease has expired' } @@ -297,18 +455,21 @@ describe('detect-new-resource-provider', () => { { namespace: 'Microsoft.Valid', serviceName: 'valid', + serviceGroup: undefined, leaseValid: true, leaseMessage: 'Lease is valid' }, { namespace: 'Microsoft.Expired', serviceName: 'expired', + serviceGroup: 'SomeRP', leaseValid: false, leaseMessage: 'Lease has expired' }, { namespace: 'Microsoft.Missing', serviceName: 'missing', + serviceGroup: undefined, leaseValid: false, leaseMessage: 'No lease file found or lease has expired' } @@ -330,6 +491,7 @@ describe('detect-new-resource-provider', () => { { namespace: 'Microsoft.NewService', serviceName: 'newservice', + serviceGroup: undefined, leaseValid: false, leaseMessage: 'No lease file found or lease has expired' } @@ -359,12 +521,14 @@ describe('detect-new-resource-provider', () => { { namespace: 'Microsoft.WithLease', serviceName: 'withlease', + serviceGroup: undefined, leaseValid: true, leaseMessage: 'Lease is valid' }, { namespace: 'Microsoft.WithoutLease', serviceName: 'withoutlease', + serviceGroup: 'TestRP', leaseValid: false, leaseMessage: 'No lease file found or lease has expired' } @@ -388,6 +552,7 @@ describe('detect-new-resource-provider', () => { { namespace: 'Microsoft.Test', serviceName: 'testservice', + serviceGroup: undefined, leaseValid: false, leaseMessage: 'No lease file found or lease has expired' } @@ -402,5 +567,31 @@ describe('detect-new-resource-provider', () => { expect(expectedLeasePath).toBe('.github/arm-leases/testservice/Microsoft.Test/lease.yaml'); }); + + it('formats lease path with service group', () => { + const leaseCheckResults = [ + { + namespace: 'Microsoft.Compute', + serviceName: 'compute', + serviceGroup: 'DiskRP', + leaseValid: false, + leaseMessage: 'No lease file found or lease has expired' + } + ]; + + const outputData = { + newResourceProviders: leaseCheckResults + }; + + const rp = outputData.newResourceProviders[0]; + const leasePathParts = ['.github/arm-leases', rp.serviceName, rp.namespace]; + if (rp.serviceGroup) { + leasePathParts.push(rp.serviceGroup); + } + leasePathParts.push('lease.yaml'); + const expectedLeasePath = leasePathParts.join('/'); + + expect(expectedLeasePath).toBe('.github/arm-leases/compute/Microsoft.Compute/DiskRP/lease.yaml'); + }); }); }); From 4eb05374d95b029f9c5e6abac27959b619ff0805 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 14 Jan 2026 18:03:53 -0600 Subject: [PATCH 016/125] added servicegroup --- .github/arm-leases/README.md | 3 +- .../testServicegroup/lease.yaml | 5 ++++ .github/workflows/src/validate-arm-leases.js | 16 ++++++---- .../test/validate-arm-leases.test.js | 30 +++++++++++++++++++ 4 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 .github/arm-leases/testservice1/Microsoft.TestRP1/testServicegroup/lease.yaml diff --git a/.github/arm-leases/README.md b/.github/arm-leases/README.md index 7ed3d029088f..990bc5eb8acb 100644 --- a/.github/arm-leases/README.md +++ b/.github/arm-leases/README.md @@ -20,12 +20,13 @@ This directory is protected by CODEOWNERS to ensure proper governance: Each lease must be placed in the following directory structure: ``` -.github/arm-leases///lease.yaml +.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) logical grouping within an RP (e.g., DiskRP, ComputeRP). Must not start with "stable" or "preview" ### Lease File Format diff --git a/.github/arm-leases/testservice1/Microsoft.TestRP1/testServicegroup/lease.yaml b/.github/arm-leases/testservice1/Microsoft.TestRP1/testServicegroup/lease.yaml new file mode 100644 index 000000000000..e30e996dfdec --- /dev/null +++ b/.github/arm-leases/testservice1/Microsoft.TestRP1/testServicegroup/lease.yaml @@ -0,0 +1,5 @@ +lease: + resource-provider: Microsoft.TestRP1 + startdate: 2026-01-15 # ISO 8601 format (YYYY-MM-DD) + duration: 180 days # Maximum allowed is 180 days + reviewer: Tejaswi Salaigari \ No newline at end of file diff --git a/.github/workflows/src/validate-arm-leases.js b/.github/workflows/src/validate-arm-leases.js index f5421271c8e0..2646bdd1fd96 100644 --- a/.github/workflows/src/validate-arm-leases.js +++ b/.github/workflows/src/validate-arm-leases.js @@ -17,6 +17,7 @@ const ALLOWED_FILE_PATTERNS = [ ]; const LEASE_FILE_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/lease\.yaml$/; +const LEASE_FILE_WITH_GROUP_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/(?!stable|preview)([^\/]+)\/lease\.yaml$/; const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; // ============================================ @@ -38,7 +39,7 @@ function isFileAllowed(file) { * @returns {string[]} Array of invalid files */ function validateFolderStructure(files) { - return files.filter(file => !LEASE_FILE_PATTERN.test(file)); + return files.filter(file => !LEASE_FILE_PATTERN.test(file) && !LEASE_FILE_WITH_GROUP_PATTERN.test(file)); } /** @@ -84,7 +85,8 @@ function validateLeaseContent(leaseFile, today, relativePath) { const errors = []; const pathForExtraction = relativePath || leaseFile; // Extract namespace from .github/arm-leases///lease.yaml - const folderRP = pathForExtraction.split('/')[3]; + // or .github/arm-leases////lease.yaml + const folderRP = pathForExtraction.split('/')[3]; // namespace is always at index 3 if (!existsSync(leaseFile)) { return { file: leaseFile, errors: ['File does not exist'] }; @@ -224,19 +226,23 @@ async function main() { if (invalidStructure.length > 0) { console.log(`${invalidStructure.length} file(s) with invalid folder structure:`); invalidStructure.forEach(file => console.log(` ${file}`)); - console.log('Expected format: .github/arm-leases///lease.yaml'); + console.log('Expected format: .github/arm-leases///[ (optional)]/lease.yaml'); console.log('Requirements:'); console.log(' - : lowercase alphanumeric only (e.g., testservice, widgetservice)'); console.log(' - : alphanumeric with dots and case-sensitive (e.g., Test.Rp, Widget.Manager)'); + console.log(' - : (optional) logical grouping within an RP (e.g., DiskRP, ComputeRP). Must not start with "stable" or "preview"'); console.log(' - Only lease.yaml files are allowed in arm-leases folder'); console.log('Examples:'); console.log(' - .github/arm-leases/testservice/Test.Rp/lease.yaml'); - console.log(' - .github/arm-leases/widgetservice/Widget.Manager/lease.yaml\n'); + console.log(' - .github/arm-leases/widgetservice/Widget.Manager/lease.yaml'); + console.log(' - .github/arm-leases/compute/Microsoft.Compute/DiskRP/lease.yaml\n'); exitCode = 1; } // Step 6: Validate lease file contents - const validLeaseFiles = armLeaseFiles.filter(file => LEASE_FILE_PATTERN.test(file)); + const validLeaseFiles = armLeaseFiles.filter(file => + LEASE_FILE_PATTERN.test(file) || LEASE_FILE_WITH_GROUP_PATTERN.test(file) + ); if (validLeaseFiles.length === 0) { printSummary(exitCode); diff --git a/.github/workflows/test/validate-arm-leases.test.js b/.github/workflows/test/validate-arm-leases.test.js index 6b88ef73ffd6..c94a464660e1 100644 --- a/.github/workflows/test/validate-arm-leases.test.js +++ b/.github/workflows/test/validate-arm-leases.test.js @@ -57,6 +57,36 @@ describe('ARM Leases Validation Tests', () => { }); }); + it('should accept valid lease file pattern with service group', () => { + const validPaths = [ + '.github/arm-leases/compute/Microsoft.Compute/DiskRP/lease.yaml', + '.github/arm-leases/compute/Microsoft.Compute/ComputeRP/lease.yaml', + '.github/arm-leases/storage/Azure.Storage/BlobRP/lease.yaml' + ]; + + const LEASE_FILE_WITH_GROUP_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/(?!stable|preview)([^\/]+)\/lease\.yaml$/; + + validPaths.forEach(path => { + assert.ok(LEASE_FILE_WITH_GROUP_PATTERN.test(path), `${path} should be valid`); + }); + }); + + it('should reject lease files with stable or preview as service group', () => { + const invalidPaths = [ + '.github/arm-leases/compute/Microsoft.Compute/stable/lease.yaml', + '.github/arm-leases/compute/Microsoft.Compute/preview/lease.yaml', + '.github/arm-leases/compute/Microsoft.Compute/stable-2024-01-01/lease.yaml', + '.github/arm-leases/compute/Microsoft.Compute/preview-2025-10-11/lease.yaml', + '.github/arm-leases/compute/Microsoft.Compute/preview-internal/lease.yaml' + ]; + + const LEASE_FILE_WITH_GROUP_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/(?!stable|preview)([^\/]+)\/lease\.yaml$/; + + invalidPaths.forEach(path => { + assert.ok(!LEASE_FILE_WITH_GROUP_PATTERN.test(path), `${path} should be invalid`); + }); + }); + it('should reject invalid lease file patterns', () => { const invalidPaths = [ '.github/arm-leases/TestService/Microsoft.Test/lease.yaml', // uppercase service name From c9fee427b76a88cbf778041111e143c44816a3ec Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Thu, 22 Jan 2026 15:56:28 -0600 Subject: [PATCH 017/125] Update terminology from 'Program Managers' to 'Product Managers' --- .github/arm-leases/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/arm-leases/README.md b/.github/arm-leases/README.md index 990bc5eb8acb..8b9d93832963 100644 --- a/.github/arm-leases/README.md +++ b/.github/arm-leases/README.md @@ -2,15 +2,15 @@ ## 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 Program Managers (PMs) and RP owners to collaborate on API design, review specifications. +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 Program 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 in the lease file must also be a PM who has reviewed and approved the lease. +**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 in the lease file must also be a PM who has reviewed and approved the lease. ## Code Owners and Contribution Guidelines This directory is protected by CODEOWNERS to ensure proper governance: -- **Program Managers (PMs)**: Can add and modify `lease.yaml` files after office hours discussions with RP owners +- **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 require approval from code owners (PMs and engineers listed in CODEOWNERS) @@ -65,4 +65,4 @@ All lease files are automatically validated with the following requirements: ### 5. Reviewer - Required field that cannot be empty - Must contain the name of the PM who approved the lease -- Only PMs can be listed as reviewers \ No newline at end of file +- Only PMs can be listed as reviewers From 3020396c7f946df03de1f5645acf95d687ab08c8 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Fri, 23 Jan 2026 15:07:35 -0600 Subject: [PATCH 018/125] corrected parse failure --- .../detect-new-resource-provider.yaml | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/detect-new-resource-provider.yaml b/.github/workflows/detect-new-resource-provider.yaml index 20671ad878d9..df3f7569374c 100644 --- a/.github/workflows/detect-new-resource-provider.yaml +++ b/.github/workflows/detect-new-resource-provider.yaml @@ -57,24 +57,24 @@ jobs: if (fs.existsSync(outputFile)) { const data = JSON.parse(fs.readFileSync(outputFile, 'utf8')); - const rpList = data.newResourceProviders.map(rp => `- \`${rp}\``).join('\n'); + const rpList = data.newResourceProviders.map(rp => \`- \\\`\${rp}\\\`\`).join('\\n'); - const body = `## šŸ†• New Resource Provider Detected + const body = \`## šŸ†• New Resource Provider Detected -The following new resource provider namespace(s) were detected in this PR: + The following new resource provider namespace(s) were detected in this PR: -${rpList} + \${rpList} -### āš ļø Action Required + ### āš ļø Action Required -New resource provider namespaces require attending **ARM API Modeling Office Hours** before merging. + New resource provider namespaces require attending **ARM API Modeling Office Hours** before merging. -šŸ“… **Please schedule here:** https://outlook.office365.com/book/ARMOfficeHours1@microsoft.onmicrosoft.com/?ismsaljsauthenabled=true + šŸ“… **Please schedule here:** https://outlook.office365.com/book/ARMOfficeHours1@microsoft.onmicrosoft.com/?ismsaljsauthenabled=true -During the office hours, the ARM team will review: -- Resource provider namespace naming conventions -- API design patterns and best practices -- Registration requirements for Azure environments`; + During the office hours, the ARM team will review: + - Resource provider namespace naming conventions + - API design patterns and best practices + - Registration requirements for Azure environments\`; github.rest.issues.createComment({ owner: context.repo.owner, From 82c8f5432944eab98b12ee43ed95ffac97d42356 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Mon, 26 Jan 2026 22:58:54 -0600 Subject: [PATCH 019/125] Added labels and check new directory to identify new RP's --- .../detect-new-resource-provider.yaml | 16 +- .../src/detect-new-resource-provider.js | 114 +- .../test/detect-new-resource-provider.test.js | 983 ++++++++++++------ 3 files changed, 743 insertions(+), 370 deletions(-) diff --git a/.github/workflows/detect-new-resource-provider.yaml b/.github/workflows/detect-new-resource-provider.yaml index df3f7569374c..15bcbde64b2e 100644 --- a/.github/workflows/detect-new-resource-provider.yaml +++ b/.github/workflows/detect-new-resource-provider.yaml @@ -46,6 +46,11 @@ jobs: run: node workflows/src/detect-new-resource-provider.js ${{ github.event.pull_request.base.ref }} working-directory: .github continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO_OWNER: ${{ github.repository_owner }} + REPO_NAME: ${{ github.event.repository.name }} - name: Comment on PR if: steps.detect.outcome == 'failure' @@ -67,16 +72,11 @@ jobs: ### āš ļø Action Required - New resource provider namespaces require attending **ARM API Modeling Office Hours** before merging. + New resource provider namespaces require having a discussion with the ARM team at **ARM API Modeling Office Hours** before merging. - šŸ“… **Please schedule here:** https://outlook.office365.com/book/ARMOfficeHours1@microsoft.onmicrosoft.com/?ismsaljsauthenabled=true + šŸ“… **Please schedule here:** https://outlook.office365.com/book/ARMOfficeHours1@microsoft.onmicrosoft.com/?ismsaljsauthenabled=true\`; - During the office hours, the ARM team will review: - - Resource provider namespace naming conventions - - API design patterns and best practices - - Registration requirements for Azure environments\`; - - github.rest.issues.createComment({ + await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, diff --git a/.github/workflows/src/detect-new-resource-provider.js b/.github/workflows/src/detect-new-resource-provider.js index 8c1854e79c42..c6960bccb656 100644 --- a/.github/workflows/src/detect-new-resource-provider.js +++ b/.github/workflows/src/detect-new-resource-provider.js @@ -2,8 +2,10 @@ import { execSync } from 'child_process'; import { readdirSync, existsSync, statSync, writeFileSync } from 'fs'; -import { join } from 'path'; -import { getChangedFiles } from '../../shared/src/changed-files.js'; +import { join, dirname } from 'path'; +import { Octokit } from '@octokit/rest'; +import { simpleGit } from 'simple-git'; +import { getChangedFiles, swagger, example } from '../../shared/src/changed-files.js'; import { checkLease } from './detect-arm-leases.js'; // ============================================ @@ -17,6 +19,9 @@ const RESOURCE_MANAGER_PATTERN = /^specification\/[^\/]+\/resource-manager\/([^\ // ServiceGroup folder name should not start with "stable" or "preview" const RESOURCE_MANAGER_WITH_GROUP_PATTERN = /^specification\/[^\/]+\/resource-manager\/([^\/]+)\/(?!stable|preview)([^\/]+)\//; +// Labels to add when new resource providers are detected +const NEW_RP_LABELS = ['ARMModelingReviewRequired', 'NotReadyForARMReview']; + // ============================================ // Utility Functions // ============================================ @@ -54,6 +59,42 @@ function resourceProviderExists(specPath, namespace) { return false; } +/** + * Add labels to the PR for new resource providers + * @returns {Promise} + */ +async function addNewResourceProviderLabels() { + const githubToken = process.env.GITHUB_TOKEN; + const prNumber = process.env.PR_NUMBER; + const repoOwner = process.env.REPO_OWNER || 'Azure'; + const repoName = process.env.REPO_NAME || 'azure-rest-api-specs'; + + if (!githubToken) { + console.log('GITHUB_TOKEN not available, skipping label addition'); + return; + } + + if (!prNumber) { + console.log('PR_NUMBER not available, skipping label addition'); + return; + } + + try { + const octokit = new Octokit({ auth: githubToken }); + + await octokit.rest.issues.addLabels({ + owner: repoOwner, + repo: repoName, + issue_number: parseInt(prNumber), + labels: NEW_RP_LABELS + }); + + console.log(`Successfully added labels: ${NEW_RP_LABELS.join(', ')}`); + } catch (error) { + console.log(`[FAILED] Failed to add labels: ${error.message}`); + } +} + /** * Extract resource provider namespaces, service names, and optional service groups from file paths * @param {string[]} files - Array of file paths @@ -99,11 +140,10 @@ async function main() { try { mergeBase = execSync(`git merge-base origin/${baseBranch} HEAD`, { encoding: 'utf-8' }).trim(); } catch (error) { - console.log('Warning: Could not find merge-base, using origin/main directly'); mergeBase = `origin/${baseBranch}`; } - // Step 1: Get all changed files in specification/*/resource-manager/ + // Get all changed files in specification/*/resource-manager/ const allFiles = await getChangedFiles({ baseCommitish: mergeBase, headCommitish: 'HEAD' @@ -113,15 +153,52 @@ async function main() { file.includes('/resource-manager/') && file.startsWith('specification/') ); - console.log(`Found ${rmFiles.length} changed file(s) in resource-manager directories\n`); - if (rmFiles.length === 0) { console.log('No resource-manager files changed'); process.exit(0); } - // Step 2: Extract resource providers and filter for new ones (don't exist in main branch) - const changedResourceProviders = extractResourceProviders(rmFiles); + // Pre-check: Verify if spec directories are brand new (don't exist in base branch) + const git = simpleGit(repoRoot); + const changedSpecDirs = new Set([ + ...rmFiles.filter(swagger).map((f) => dirname(dirname(dirname(f)))), + ...rmFiles.filter(example).map((f) => dirname(dirname(dirname(dirname(f))))), + ]); + + if (changedSpecDirs.size > 0) { + let hasAtLeastOneBrandNewRP = false; + + for (const changedSpecDir of changedSpecDirs) { + try { + const specFilesBaseBranch = await git.raw([ + 'ls-tree', + '-r', + '--name-only', + mergeBase, + changedSpecDir, + ]); + + const specRmSwaggerFilesBaseBranch = specFilesBaseBranch + .split('\n') + .filter((file) => file.includes('/resource-manager/') && swagger(file)); + + if (specRmSwaggerFilesBaseBranch.length === 0) { + hasAtLeastOneBrandNewRP = true; + } + } catch (error) { + // Directory doesn't exist in base - brand new RP + hasAtLeastOneBrandNewRP = true; + } + } + + if (!hasAtLeastOneBrandNewRP) { + console.log('No brand new resource providers detected, spec directories exist in base branch.'); + console.log('Skipping workflow.\n'); + process.exit(0); + } + } + + // Extract resource providers and filter for new ones const specPath = join(repoRoot, SPECIFICATION_PATH); const newResourceProviders = []; @@ -135,42 +212,35 @@ async function main() { } } - // Step 3: Check ARM leases for new resource providers + // Check ARM leases for new resource providers const leaseCheckResults = []; if (newResourceProviders.length > 0) { - console.log(`\nšŸ†• Detected ${newResourceProviders.length} NEW resource provider namespace(s):\n`); + console.log(`\nšŸ†• Detected ${newResourceProviders.length} new resource provider(s)\n`); for (const rp of newResourceProviders) { const leaseValid = checkLease(rp.serviceName, rp.namespace, rp.serviceGroup || ''); - const leaseMessage = leaseValid ? 'Lease is valid' : 'No lease file found or lease has expired'; leaseCheckResults.push({ namespace: rp.namespace, serviceName: rp.serviceName, serviceGroup: rp.serviceGroup, leaseValid: leaseValid, - leaseMessage: leaseMessage + leaseMessage: leaseValid ? 'Lease is valid' : 'No lease file found or lease has expired' }); - console.log(` Lease check: ${leaseMessage}`); + console.log(` - ${rp.namespace}: ${leaseValid ? '[OK]' : '[FAILED]'} Lease ${leaseValid ? 'valid' : 'invalid'}`); } - console.log('\nNew resource provider namespaces require attending "ARM API Modeling Office Hours".'); - console.log('A comment will be posted on the PR with lease validation results.\n'); - - // Step 4: Write output for GitHub Actions to use in PR comment - const outputData = { - newResourceProviders: leaseCheckResults - }; writeFileSync( join(repoRoot, '.github', 'new-rp-output.json'), - JSON.stringify(outputData, null, 2) + JSON.stringify({ newResourceProviders: leaseCheckResults }, null, 2) ); + await addNewResourceProviderLabels(); exitCode = 1; } else { - console.log('No new resource provider namespaces detected. All changes are to existing namespaces.\n'); + console.log('No new resource providers detected.\n'); } process.exit(exitCode); diff --git a/.github/workflows/test/detect-new-resource-provider.test.js b/.github/workflows/test/detect-new-resource-provider.test.js index c5216613767e..7b9f8c1d5b57 100644 --- a/.github/workflows/test/detect-new-resource-provider.test.js +++ b/.github/workflows/test/detect-new-resource-provider.test.js @@ -1,9 +1,29 @@ import { describe, it, expect } from 'vitest'; -// Mock functions for testing +// ============================================ +// Test Fixtures and Mocks +// ============================================ + +const EXISTING_NAMESPACES = ['Microsoft.Existing', 'Microsoft.Another']; + +const SAMPLE_PATHS = { + basic: 'specification/app/resource-manager/Microsoft.App/stable/2023-01-01/app.json', + withGroup: 'specification/compute/resource-manager/Microsoft.Compute/DiskRP/stable/2023-01-01/disk.json', + example: 'specification/app/resource-manager/Microsoft.App/stable/2023-01-01/examples/example.json', + dataPlane: 'specification/app/data-plane/Microsoft.App/stable/2023-01-01/app.json' +}; + +const LABELS = { + armModeling: 'ARMModelingReviewRequired', + notReady: 'NotReadyForARMReview' +}; + +// ============================================ +// Mock Functions +// ============================================ + function mockResourceProviderExists(specPath, namespace) { - const existingNamespaces = ['Microsoft.Existing', 'Microsoft.Another']; - return existingNamespaces.includes(namespace); + return EXISTING_NAMESPACES.includes(namespace); } function mockExtractResourceProviders(files) { @@ -17,11 +37,9 @@ function mockExtractResourceProviders(files) { const serviceName = match[1]; const namespace = match[2]; - // Check if there's a service group (folder between namespace and version folder) const groupMatch = file.match(groupPattern); if (groupMatch && groupMatch[2] !== 'stable' && groupMatch[2] !== 'preview' && groupMatch[2] !== 'preview-internal') { - const serviceGroup = groupMatch[2]; - resourceProviders.set(namespace, { serviceName, serviceGroup }); + resourceProviders.set(namespace, { serviceName, serviceGroup: groupMatch[2] }); } else { resourceProviders.set(namespace, { serviceName }); } @@ -31,9 +49,18 @@ function mockExtractResourceProviders(files) { return resourceProviders; } +// ============================================ +// Test Suites +// ============================================ + describe('detect-new-resource-provider', () => { + + // ========================================== + // Resource Provider Extraction Tests + // ========================================== + describe('extractResourceProviders', () => { - it('extracts namespace and service from valid paths', () => { + it('should extract namespace and service from valid paths', () => { const files = [ 'specification/app/resource-manager/Microsoft.App/stable/2023-01-01/app.json', 'specification/compute/resource-manager/Microsoft.Compute/stable/2023-01-01/compute.json', @@ -46,7 +73,7 @@ describe('detect-new-resource-provider', () => { expect(result.get('Microsoft.Compute')).toEqual({ serviceName: 'compute' }); }); - it('handles multiple files from same namespace', () => { + it('should handle multiple files from same namespace', () => { const files = [ 'specification/app/resource-manager/Microsoft.App/stable/2023-01-01/app.json', 'specification/app/resource-manager/Microsoft.App/stable/2023-01-01/models.json', @@ -59,10 +86,10 @@ describe('detect-new-resource-provider', () => { expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); }); - it('ignores non-resource-manager files', () => { + it('should ignore non-resource-manager files', () => { const files = [ - 'specification/app/data-plane/Microsoft.App/stable/2023-01-01/app.json', - 'specification/app/resource-manager/Microsoft.App/stable/2023-01-01/app.json', + SAMPLE_PATHS.dataPlane, + SAMPLE_PATHS.basic, 'specification/app/docs/readme.md', ]; @@ -72,40 +99,24 @@ describe('detect-new-resource-provider', () => { expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); }); - it('handles empty file list', () => { - const files = []; - const result = mockExtractResourceProviders(files); + it('should handle empty file list', () => { + const result = mockExtractResourceProviders([]); expect(result.size).toBe(0); }); - it('handles nested namespace paths', () => { - const files = [ - 'specification/test-service/resource-manager/Microsoft.TestService/stable/2023-01-01/operations.json', - ]; - - const result = mockExtractResourceProviders(files); - - expect(result.size).toBe(1); - expect(result.get('Microsoft.TestService')).toEqual({ serviceName: 'test-service' }); - }); - - it('extracts service group when present', () => { - const files = [ - 'specification/compute/resource-manager/Microsoft.Compute/DiskRP/stable/2023-01-01/disk.json', - ]; - + it('should extract service group when present', () => { + const files = [SAMPLE_PATHS.withGroup]; const result = mockExtractResourceProviders(files); expect(result.size).toBe(1); - expect(result.get('Microsoft.Compute')).toEqual({ serviceName: 'compute', serviceGroup: 'DiskRP' }); + expect(result.get('Microsoft.Compute')).toEqual({ + serviceName: 'compute', + serviceGroup: 'DiskRP' + }); }); - it('handles mix of files with and without service groups', () => { - const files = [ - 'specification/compute/resource-manager/Microsoft.Compute/DiskRP/stable/2023-01-01/disk.json', - 'specification/app/resource-manager/Microsoft.App/stable/2023-01-01/app.json', - ]; - + it('should handle mix of files with and without service groups', () => { + const files = [SAMPLE_PATHS.withGroup, SAMPLE_PATHS.basic]; const result = mockExtractResourceProviders(files); expect(result.size).toBe(2); @@ -113,401 +124,366 @@ describe('detect-new-resource-provider', () => { expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); }); - it('does not treat preview- as service group', () => { - const files = [ + it('should not treat version folders as service groups', () => { + const testCases = [ 'specification/app/resource-manager/Microsoft.App/preview-2025-10-11/app.json', + 'specification/app/resource-manager/Microsoft.App/preview-internal/2023-01-01/app.json', + 'specification/app/resource-manager/Microsoft.App/stable-2024-01-01/app.json', ]; - const result = mockExtractResourceProviders(files); - - expect(result.size).toBe(1); - expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); + testCases.forEach(filePath => { + const result = mockExtractResourceProviders([filePath]); + expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); + }); }); - it('does not treat preview-internal as service group', () => { + it('should extract service group when folder does not start with stable or preview', () => { const files = [ - 'specification/app/resource-manager/Microsoft.App/preview-internal/2023-01-01/app.json', + 'specification/compute/resource-manager/Microsoft.Compute/CloudServiceRP/stable/2023-01-01/cloudservice.json', ]; const result = mockExtractResourceProviders(files); - expect(result.size).toBe(1); - expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); + expect(result.get('Microsoft.Compute')).toEqual({ + serviceName: 'compute', + serviceGroup: 'CloudServiceRP' + }); }); - it('does not treat stable- as service group', () => { + it('should handle nested paths correctly', () => { const files = [ - 'specification/app/resource-manager/Microsoft.App/stable-2024-01-01/app.json', + 'specification/compute/resource-manager/Microsoft.Compute/DiskRP/stable/2023-01-01/disk.json', + 'specification/compute/resource-manager/Microsoft.Compute/DiskRP/stable/2023-01-01/models.json', ]; const result = mockExtractResourceProviders(files); expect(result.size).toBe(1); - expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); + expect(result.get('Microsoft.Compute')).toEqual({ + serviceName: 'compute', + serviceGroup: 'DiskRP' + }); }); - it('extracts service group when folder does not start with stable or preview', () => { + it('should extract multiple namespaces with different service groups', () => { const files = [ 'specification/compute/resource-manager/Microsoft.Compute/DiskRP/stable/2023-01-01/disk.json', - 'specification/compute/resource-manager/Microsoft.Compute/ComputeRP/preview/2023-01-01/compute.json', - 'specification/storage/resource-manager/Microsoft.Storage/StorageRP/stable/2023-01-01/storage.json', + 'specification/compute/resource-manager/Microsoft.Compute/VirtualMachineRP/stable/2023-01-01/vm.json', ]; const result = mockExtractResourceProviders(files); - expect(result.size).toBe(2); - expect(result.get('Microsoft.Compute')).toEqual({ serviceName: 'compute', serviceGroup: 'ComputeRP' }); - expect(result.get('Microsoft.Storage')).toEqual({ serviceName: 'storage', serviceGroup: 'StorageRP' }); + expect(result.size).toBe(1); + expect(result.get('Microsoft.Compute')).toEqual({ serviceName: 'compute', serviceGroup: 'VirtualMachineRP' }); }); }); + // ========================================== + // Resource Provider Existence Tests + // ========================================== + describe('resourceProviderExists', () => { - it('returns true for existing namespace', () => { - const result = mockResourceProviderExists('/path/to/spec', 'Microsoft.Existing'); - expect(result).toBe(true); + it('should return true for existing namespaces', () => { + expect(mockResourceProviderExists('/path/to/spec', 'Microsoft.Existing')).toBe(true); + expect(mockResourceProviderExists('/path/to/spec', 'Microsoft.Another')).toBe(true); }); - it('returns false for new namespace', () => { - const result = mockResourceProviderExists('/path/to/spec', 'Microsoft.NewService'); - expect(result).toBe(false); + it('should return false for new namespaces', () => { + expect(mockResourceProviderExists('/path/to/spec', 'Microsoft.NewService')).toBe(false); }); - it('returns false for non-matching namespace', () => { - const result = mockResourceProviderExists('/path/to/spec', 'Microsoft.Unknown'); - expect(result).toBe(false); + it('should return false for non-matching namespaces', () => { + expect(mockResourceProviderExists('/path/to/spec', 'Microsoft.Unknown')).toBe(false); }); }); - describe('new resource provider detection logic', () => { - it('identifies new resource providers correctly', () => { - const changedFiles = [ - 'specification/newservice/resource-manager/Microsoft.NewService/stable/2023-01-01/service.json', - 'specification/existing/resource-manager/Microsoft.Existing/stable/2023-01-01/service.json', - ]; + // ========================================== + // New Resource Provider Detection Tests + // ========================================== + + describe('new resource provider detection', () => { + it('should identify new resource providers correctly', () => { + const changedRPs = new Map([ + ['Microsoft.NewService', { serviceName: 'newservice' }], + ['Microsoft.Existing', { serviceName: 'existing' }] + ]); - const changedResourceProviders = mockExtractResourceProviders(changedFiles); const newResourceProviders = []; - - for (const [rp, info] of changedResourceProviders) { - if (!mockResourceProviderExists('/spec', rp)) { - newResourceProviders.push({ - namespace: rp, - serviceName: info.serviceName, - serviceGroup: info.serviceGroup - }); + for (const [rp, info] of changedRPs) { + if (!mockResourceProviderExists('/path/to/spec', rp)) { + newResourceProviders.push({ namespace: rp, ...info }); } } - expect(newResourceProviders.length).toBe(1); + expect(newResourceProviders).toHaveLength(1); expect(newResourceProviders[0].namespace).toBe('Microsoft.NewService'); expect(newResourceProviders[0].serviceName).toBe('newservice'); - expect(newResourceProviders[0].serviceGroup).toBeUndefined(); }); - it('handles no new resource providers', () => { - const changedFiles = [ - 'specification/existing/resource-manager/Microsoft.Existing/stable/2023-01-01/service.json', - 'specification/another/resource-manager/Microsoft.Another/stable/2023-01-01/service.json', - ]; + it('should handle no new resource providers', () => { + const changedRPs = new Map([ + ['Microsoft.Existing', { serviceName: 'existing' }], + ['Microsoft.Another', { serviceName: 'another' }] + ]); - const changedResourceProviders = mockExtractResourceProviders(changedFiles); const newResourceProviders = []; - - for (const [rp, info] of changedResourceProviders) { - if (!mockResourceProviderExists('/spec', rp)) { - newResourceProviders.push({ - namespace: rp, - serviceName: info.serviceName, - serviceGroup: info.serviceGroup - }); + for (const [rp, info] of changedRPs) { + if (!mockResourceProviderExists('/path/to/spec', rp)) { + newResourceProviders.push({ namespace: rp, ...info }); } } - expect(newResourceProviders.length).toBe(0); + expect(newResourceProviders).toHaveLength(0); }); - it('handles multiple new resource providers', () => { - const changedFiles = [ - 'specification/service1/resource-manager/Microsoft.Service1/stable/2023-01-01/service.json', - 'specification/service2/resource-manager/Microsoft.Service2/stable/2023-01-01/service.json', - ]; + it('should handle multiple new resource providers', () => { + const changedRPs = new Map([ + ['Microsoft.Service1', { serviceName: 'service1' }], + ['Microsoft.Service2', { serviceName: 'service2' }] + ]); - const changedResourceProviders = mockExtractResourceProviders(changedFiles); const newResourceProviders = []; - - for (const [rp, info] of changedResourceProviders) { - if (!mockResourceProviderExists('/spec', rp)) { - newResourceProviders.push({ - namespace: rp, - serviceName: info.serviceName, - serviceGroup: info.serviceGroup - }); + for (const [rp, info] of changedRPs) { + if (!mockResourceProviderExists('/path/to/spec', rp)) { + newResourceProviders.push({ namespace: rp, ...info }); } } - expect(newResourceProviders.length).toBe(2); + expect(newResourceProviders).toHaveLength(2); expect(newResourceProviders[0].namespace).toBe('Microsoft.Service1'); expect(newResourceProviders[1].namespace).toBe('Microsoft.Service2'); }); - it('identifies new resource providers with service groups', () => { - const changedFiles = [ - 'specification/compute/resource-manager/Microsoft.Compute/DiskRP/stable/2023-01-01/disk.json', - ]; + it('should identify new resource providers with service groups', () => { + const changedRPs = new Map([ + ['Microsoft.Compute', { serviceName: 'compute', serviceGroup: 'DiskRP' }] + ]); - const changedResourceProviders = mockExtractResourceProviders(changedFiles); const newResourceProviders = []; - - for (const [rp, info] of changedResourceProviders) { - if (!mockResourceProviderExists('/spec', rp)) { - newResourceProviders.push({ - namespace: rp, - serviceName: info.serviceName, - serviceGroup: info.serviceGroup - }); + for (const [rp, info] of changedRPs) { + if (!mockResourceProviderExists('/path/to/spec', rp)) { + newResourceProviders.push({ namespace: rp, ...info }); } } - expect(newResourceProviders.length).toBe(1); - expect(newResourceProviders[0].namespace).toBe('Microsoft.Compute'); - expect(newResourceProviders[0].serviceName).toBe('compute'); + expect(newResourceProviders).toHaveLength(1); expect(newResourceProviders[0].serviceGroup).toBe('DiskRP'); }); }); - describe('lease validation integration', () => { - it('creates correct output structure with lease validation', () => { - const newResourceProviders = [ - { namespace: 'Microsoft.NewService', serviceName: 'newservice' } - ]; - - const leaseCheckResults = []; - - for (const rp of newResourceProviders) { - const leaseValid = false; // Mock lease check - const leaseMessage = 'No lease file found or lease has expired'; - - leaseCheckResults.push({ - namespace: rp.namespace, - serviceName: rp.serviceName, - serviceGroup: rp.serviceGroup, - leaseValid: leaseValid, - leaseMessage: leaseMessage - }); - } + // ========================================== + // Lease Validation Tests + // ========================================== + + describe('lease validation', () => { + it('should create correct output structure', () => { + const leaseCheckResults = [{ + namespace: 'Microsoft.Test', + serviceName: 'test', + leaseValid: true, + leaseMessage: 'Lease is valid' + }]; - const outputData = { - newResourceProviders: leaseCheckResults - }; + const outputData = { newResourceProviders: leaseCheckResults }; expect(outputData.newResourceProviders).toHaveLength(1); - expect(outputData.newResourceProviders[0]).toEqual({ - namespace: 'Microsoft.NewService', - serviceName: 'newservice', - serviceGroup: undefined, - leaseValid: false, - leaseMessage: 'No lease file found or lease has expired' - }); + expect(outputData.newResourceProviders[0]).toHaveProperty('namespace'); + expect(outputData.newResourceProviders[0]).toHaveProperty('leaseValid'); }); - it('creates correct output structure with service group', () => { - const newResourceProviders = [ - { namespace: 'Microsoft.Compute', serviceName: 'compute', serviceGroup: 'DiskRP' } - ]; - - const leaseCheckResults = []; - - for (const rp of newResourceProviders) { - const leaseValid = true; // Mock lease check - const leaseMessage = 'Lease is valid'; - - leaseCheckResults.push({ - namespace: rp.namespace, - serviceName: rp.serviceName, - serviceGroup: rp.serviceGroup, - leaseValid: leaseValid, - leaseMessage: leaseMessage - }); - } - - const outputData = { - newResourceProviders: leaseCheckResults - }; - - expect(outputData.newResourceProviders).toHaveLength(1); - expect(outputData.newResourceProviders[0]).toEqual({ + it('should include service group in output when present', () => { + const leaseCheckResults = [{ namespace: 'Microsoft.Compute', serviceName: 'compute', serviceGroup: 'DiskRP', - leaseValid: true, - leaseMessage: 'Lease is valid' - }); + leaseValid: false, + leaseMessage: 'No lease file found' + }]; + + const outputData = { newResourceProviders: leaseCheckResults }; + + expect(outputData.newResourceProviders[0].serviceGroup).toBe('DiskRP'); }); - it('handles valid lease in output', () => { - const newResourceProviders = [ - { namespace: 'Microsoft.ValidLease', serviceName: 'validservice', serviceGroup: undefined } - ]; + it('should handle valid lease in output', () => { + const leaseCheckResults = [{ + namespace: 'Microsoft.Test', + serviceName: 'test', + leaseValid: true, + leaseMessage: 'Lease is valid' + }]; - const leaseCheckResults = []; - - for (const rp of newResourceProviders) { - const leaseValid = true; // Mock valid lease - const leaseMessage = 'Lease is valid'; - - leaseCheckResults.push({ - namespace: rp.namespace, - serviceName: rp.serviceName, - leaseValid: leaseValid, - leaseMessage: leaseMessage - }); - } + expect(leaseCheckResults[0].leaseValid).toBe(true); + expect(leaseCheckResults[0].leaseMessage).toBe('Lease is valid'); + }); - const outputData = { - newResourceProviders: leaseCheckResults - }; + it('should handle invalid lease in output', () => { + const leaseCheckResults = [{ + namespace: 'Microsoft.Test', + serviceName: 'test', + leaseValid: false, + leaseMessage: 'No lease file found' + }]; - expect(outputData.newResourceProviders[0].leaseValid).toBe(true); - expect(outputData.newResourceProviders[0].leaseMessage).toBe('Lease is valid'); + expect(leaseCheckResults[0].leaseValid).toBe(false); + expect(leaseCheckResults[0].leaseMessage).toBe('No lease file found'); }); }); - describe('JSON output file generation', () => { - it('creates output JSON file with correct structure', () => { - const leaseCheckResults = [ - { - namespace: 'Microsoft.NewService', - serviceName: 'newservice', - serviceGroup: undefined, - leaseValid: false, - leaseMessage: 'No lease file found or lease has expired' - }, - { - namespace: 'Microsoft.AnotherNew', - serviceName: 'anothernew', - serviceGroup: undefined, - leaseValid: true, - leaseMessage: 'Lease is valid' - } - ]; + // ========================================== + // JSON Output Generation Tests + // ========================================== + + describe('JSON output generation', () => { + it('should create output with correct structure', () => { + const leaseCheckResults = [{ + namespace: 'Microsoft.Test', + serviceName: 'test', + leaseValid: false, + leaseMessage: 'No lease file found' + }]; - const outputData = { - newResourceProviders: leaseCheckResults - }; + const outputData = { newResourceProviders: leaseCheckResults }; expect(outputData).toHaveProperty('newResourceProviders'); expect(Array.isArray(outputData.newResourceProviders)).toBe(true); - expect(outputData.newResourceProviders).toHaveLength(2); }); - it('includes all required fields for each resource provider', () => { - const leaseCheckResults = [ - { - namespace: 'Microsoft.Test', - serviceName: 'test', - serviceGroup: undefined, - leaseValid: false, - leaseMessage: 'No lease file found or lease has expired' - } - ]; + it('should include all required fields', () => { + const leaseCheckResults = [{ + namespace: 'Microsoft.Test', + serviceName: 'test', + serviceGroup: 'TestRP', + leaseValid: false, + leaseMessage: 'No lease file found' + }]; - const outputData = { - newResourceProviders: leaseCheckResults - }; + const rp = leaseCheckResults[0]; - const rp = outputData.newResourceProviders[0]; expect(rp).toHaveProperty('namespace'); expect(rp).toHaveProperty('serviceName'); expect(rp).toHaveProperty('serviceGroup'); expect(rp).toHaveProperty('leaseValid'); expect(rp).toHaveProperty('leaseMessage'); - expect(typeof rp.namespace).toBe('string'); - expect(typeof rp.serviceName).toBe('string'); - expect(typeof rp.leaseValid).toBe('boolean'); - expect(typeof rp.leaseMessage).toBe('string'); }); - it('formats output data for PR comment consumption', () => { + it('should format output for PR comment consumption', () => { const leaseCheckResults = [ { - namespace: 'Microsoft.NewRP', - serviceName: 'newrp', - serviceGroup: undefined, + namespace: 'Microsoft.Test1', + serviceName: 'test1', + leaseValid: true, + leaseMessage: 'Lease is valid' + }, + { + namespace: 'Microsoft.Test2', + serviceName: 'test2', leaseValid: false, - leaseMessage: 'No lease file found or lease has expired' + leaseMessage: 'No lease file found' } ]; - const outputData = { - newResourceProviders: leaseCheckResults - }; + const outputData = { newResourceProviders: leaseCheckResults }; - const jsonString = JSON.stringify(outputData, null, 2); - const parsed = JSON.parse(jsonString); - - expect(parsed.newResourceProviders).toEqual(leaseCheckResults); - expect(jsonString).toContain('newResourceProviders'); - expect(jsonString).toContain('Microsoft.NewRP'); + expect(outputData.newResourceProviders).toHaveLength(2); + outputData.newResourceProviders.forEach(rp => { + expect(rp.namespace).toBeTruthy(); + expect(typeof rp.leaseValid).toBe('boolean'); + }); }); - it('handles multiple new RPs with mixed lease statuses', () => { + it('should handle multiple new RPs with mixed lease statuses', () => { const leaseCheckResults = [ { - namespace: 'Microsoft.Valid', - serviceName: 'valid', - serviceGroup: undefined, + namespace: 'Microsoft.WithLease', + serviceName: 'withlease', leaseValid: true, leaseMessage: 'Lease is valid' }, { - namespace: 'Microsoft.Expired', - serviceName: 'expired', - serviceGroup: 'SomeRP', - leaseValid: false, - leaseMessage: 'Lease has expired' - }, - { - namespace: 'Microsoft.Missing', - serviceName: 'missing', - serviceGroup: undefined, + namespace: 'Microsoft.WithoutLease', + serviceName: 'withoutlease', leaseValid: false, - leaseMessage: 'No lease file found or lease has expired' + leaseMessage: 'No lease file found' } ]; - const outputData = { - newResourceProviders: leaseCheckResults - }; + const outputData = { newResourceProviders: leaseCheckResults }; + + const validLeases = outputData.newResourceProviders.filter(rp => rp.leaseValid); + const invalidLeases = outputData.newResourceProviders.filter(rp => !rp.leaseValid); - expect(outputData.newResourceProviders).toHaveLength(3); - expect(outputData.newResourceProviders.filter(rp => rp.leaseValid)).toHaveLength(1); - expect(outputData.newResourceProviders.filter(rp => !rp.leaseValid)).toHaveLength(2); + expect(validLeases).toHaveLength(1); + expect(invalidLeases).toHaveLength(1); }); - }); - describe('PR comment data preparation', () => { - it('provides data for office hours attendance message', () => { + it('should maintain order of RPs in output', () => { const leaseCheckResults = [ - { - namespace: 'Microsoft.NewService', - serviceName: 'newservice', - serviceGroup: undefined, - leaseValid: false, - leaseMessage: 'No lease file found or lease has expired' - } + { namespace: 'Microsoft.First', serviceName: 'first', leaseValid: true, leaseMessage: 'Valid' }, + { namespace: 'Microsoft.Second', serviceName: 'second', leaseValid: false, leaseMessage: 'Invalid' }, + { namespace: 'Microsoft.Third', serviceName: 'third', leaseValid: true, leaseMessage: 'Valid' } ]; - const outputData = { - newResourceProviders: leaseCheckResults - }; + const outputData = { newResourceProviders: leaseCheckResults }; + + expect(outputData.newResourceProviders[0].namespace).toBe('Microsoft.First'); + expect(outputData.newResourceProviders[1].namespace).toBe('Microsoft.Second'); + expect(outputData.newResourceProviders[2].namespace).toBe('Microsoft.Third'); + }); + + it('should serialize to valid JSON', () => { + const leaseCheckResults = [{ + namespace: 'Microsoft.Test', + serviceName: 'test', + leaseValid: false, + leaseMessage: 'No lease file found' + }]; + + const outputData = { newResourceProviders: leaseCheckResults }; + const jsonString = JSON.stringify(outputData); + const parsed = JSON.parse(jsonString); + + expect(parsed).toEqual(outputData); + }); + + it('should handle empty RPs list', () => { + const outputData = { newResourceProviders: [] }; + + expect(outputData.newResourceProviders).toHaveLength(0); + expect(Array.isArray(outputData.newResourceProviders)).toBe(true); + }); + + it('should include optional service group field', () => { + const leaseCheckResults = [{ + namespace: 'Microsoft.Compute', + serviceName: 'compute', + serviceGroup: 'DiskRP', + leaseValid: true, + leaseMessage: 'Valid' + }]; + + const outputData = { newResourceProviders: leaseCheckResults }; + + expect(outputData.newResourceProviders[0].serviceGroup).toBe('DiskRP'); + }); + }); + + // ========================================== + // PR Comment Data Preparation Tests + // ========================================== + + describe('PR comment data preparation', () => { + it('should provide data for office hours message', () => { + const leaseCheckResults = [{ + namespace: 'Microsoft.Test', + serviceName: 'test', + leaseValid: false, + leaseMessage: 'No lease file found' + }]; + + const outputData = { newResourceProviders: leaseCheckResults }; - // Verify data structure for PR comment expect(outputData.newResourceProviders.length).toBeGreaterThan(0); - - const hasNewRP = outputData.newResourceProviders.length > 0; - expect(hasNewRP).toBe(true); - - // Verify each RP has info needed for comment outputData.newResourceProviders.forEach(rp => { expect(rp.namespace).toBeTruthy(); expect(rp.serviceName).toBeTruthy(); @@ -516,12 +492,11 @@ describe('detect-new-resource-provider', () => { }); }); - it('identifies RPs without valid lease for office hours requirement', () => { + it('should identify RPs needing office hours', () => { const leaseCheckResults = [ { namespace: 'Microsoft.WithLease', serviceName: 'withlease', - serviceGroup: undefined, leaseValid: true, leaseMessage: 'Lease is valid' }, @@ -530,14 +505,11 @@ describe('detect-new-resource-provider', () => { serviceName: 'withoutlease', serviceGroup: 'TestRP', leaseValid: false, - leaseMessage: 'No lease file found or lease has expired' + leaseMessage: 'No lease file found' } ]; - const outputData = { - newResourceProviders: leaseCheckResults - }; - + const outputData = { newResourceProviders: leaseCheckResults }; const rpsNeedingOfficeHours = outputData.newResourceProviders.filter(rp => !rp.leaseValid); const rpsWithValidLease = outputData.newResourceProviders.filter(rp => rp.leaseValid); @@ -547,51 +519,382 @@ describe('detect-new-resource-provider', () => { expect(rpsWithValidLease[0].namespace).toBe('Microsoft.WithLease'); }); - it('formats lease path information for PR comment', () => { + it('should format lease path for PR comment', () => { + const rp = { + namespace: 'Microsoft.Test', + serviceName: 'testservice', + leaseValid: false + }; + + const leasePath = `.github/arm-leases/${rp.serviceName}/${rp.namespace}/lease.yaml`; + + expect(leasePath).toBe('.github/arm-leases/testservice/Microsoft.Test/lease.yaml'); + }); + + it('should format lease path with service group', () => { + const rp = { + namespace: 'Microsoft.Compute', + serviceName: 'compute', + serviceGroup: 'DiskRP', + leaseValid: false + }; + + const leasePathParts = ['.github/arm-leases', rp.serviceName, rp.namespace]; + if (rp.serviceGroup) { + leasePathParts.push(rp.serviceGroup); + } + leasePathParts.push('lease.yaml'); + const leasePath = leasePathParts.join('/'); + + expect(leasePath).toBe('.github/arm-leases/compute/Microsoft.Compute/DiskRP/lease.yaml'); + }); + + it('should distinguish between valid and invalid leases for messaging', () => { const leaseCheckResults = [ - { - namespace: 'Microsoft.Test', - serviceName: 'testservice', - serviceGroup: undefined, - leaseValid: false, - leaseMessage: 'No lease file found or lease has expired' - } + { namespace: 'Microsoft.Valid', serviceName: 'valid', leaseValid: true, leaseMessage: 'Valid' }, + { namespace: 'Microsoft.Invalid', serviceName: 'invalid', leaseValid: false, leaseMessage: 'Invalid' } ]; - const outputData = { - newResourceProviders: leaseCheckResults + const invalidLeases = leaseCheckResults.filter(rp => !rp.leaseValid); + const validLeases = leaseCheckResults.filter(rp => rp.leaseValid); + + expect(invalidLeases.length).toBe(1); + expect(validLeases.length).toBe(1); + }); + + it('should provide complete RP information for PR comment', () => { + const rp = { + namespace: 'Microsoft.Test', + serviceName: 'test', + serviceGroup: 'SubGroup', + leaseValid: false, + leaseMessage: 'No lease file found' }; - const rp = outputData.newResourceProviders[0]; - const expectedLeasePath = `.github/arm-leases/${rp.serviceName}/${rp.namespace}/lease.yaml`; + expect(rp).toHaveProperty('namespace'); + expect(rp).toHaveProperty('serviceName'); + expect(rp).toHaveProperty('serviceGroup'); + expect(rp).toHaveProperty('leaseValid'); + expect(rp).toHaveProperty('leaseMessage'); + }); + + it('should handle RPs without service group in PR comment data', () => { + const rp = { + namespace: 'Microsoft.Simple', + serviceName: 'simple', + leaseValid: false, + leaseMessage: 'No lease' + }; - expect(expectedLeasePath).toBe('.github/arm-leases/testservice/Microsoft.Test/lease.yaml'); + expect(rp.serviceGroup).toBeUndefined(); + expect(rp.namespace).toBe('Microsoft.Simple'); + expect(rp.serviceName).toBe('simple'); }); - it('formats lease path with service group', () => { + it('should prepare data for multiple RPs in single comment', () => { const leaseCheckResults = [ - { - namespace: 'Microsoft.Compute', - serviceName: 'compute', - serviceGroup: 'DiskRP', - leaseValid: false, - leaseMessage: 'No lease file found or lease has expired' - } + { namespace: 'Microsoft.One', serviceName: 'one', leaseValid: false, leaseMessage: 'No lease' }, + { namespace: 'Microsoft.Two', serviceName: 'two', leaseValid: false, leaseMessage: 'No lease' }, + { namespace: 'Microsoft.Three', serviceName: 'three', leaseValid: true, leaseMessage: 'Valid' } + ]; + + const needsOfficeHours = leaseCheckResults.filter(rp => !rp.leaseValid); + const hasValidLease = leaseCheckResults.filter(rp => rp.leaseValid); + + expect(needsOfficeHours).toHaveLength(2); + expect(hasValidLease).toHaveLength(1); + }); + }); + + // ========================================== + // Brand New RP Validation Tests + // ========================================== + + describe('brand new resource provider validation', () => { + it('should identify directory as brand new when no files exist in base', () => { + const gitLsTreeResult = ''; + const hasSwaggerFiles = gitLsTreeResult.split('\n') + .filter(file => file.includes('/resource-manager/') && file.endsWith('.json')) + .length > 0; + + expect(hasSwaggerFiles).toBe(false); + }); + + it('should identify directory as brand new when only non-swagger files exist', () => { + const gitLsTreeResult = [ + 'specification/newservice/resource-manager/Microsoft.NewService/readme.md', + 'specification/newservice/resource-manager/Microsoft.NewService/examples/example.json' + ].join('\n'); + + const hasSwaggerFiles = gitLsTreeResult.split('\n') + .filter(file => file.includes('/resource-manager/') && file.endsWith('.json') && !file.includes('/examples/')) + .length > 0; + + expect(hasSwaggerFiles).toBe(false); + }); + + it('should identify directory as existing when swagger files exist in base', () => { + const gitLsTreeResult = [ + 'specification/compute/resource-manager/Microsoft.Compute/stable/2023-01-01/compute.json', + 'specification/compute/resource-manager/Microsoft.Compute/stable/2023-01-01/models.json' + ].join('\n'); + + const hasSwaggerFiles = gitLsTreeResult.split('\n') + .filter(file => file.includes('/resource-manager/') && file.endsWith('.json') && !file.includes('/examples/')) + .length > 0; + + expect(hasSwaggerFiles).toBe(true); + }); + + it('should continue workflow when at least one brand new RP exists', () => { + const specDirChecks = [ + { dir: 'specification/newservice/resource-manager/Microsoft.NewService', hasFilesInBase: false }, + { dir: 'specification/compute/resource-manager/Microsoft.Compute', hasFilesInBase: true }, + ]; + + const hasAtLeastOneBrandNewRP = specDirChecks.some(check => !check.hasFilesInBase); + + expect(hasAtLeastOneBrandNewRP).toBe(true); + }); + + it('should skip workflow when no brand new RPs exist', () => { + const specDirChecks = [ + { dir: 'specification/compute/resource-manager/Microsoft.Compute', hasFilesInBase: true }, + { dir: 'specification/storage/resource-manager/Microsoft.Storage', hasFilesInBase: true }, + ]; + + const hasAtLeastOneBrandNewRP = specDirChecks.some(check => !check.hasFilesInBase); + + expect(hasAtLeastOneBrandNewRP).toBe(false); + }); + + it('should extract spec directory from swagger file path', () => { + const swaggerFile = 'specification/newservice/resource-manager/Microsoft.NewService/stable/2024-01-01/service.json'; + const parts = swaggerFile.split('/'); + const specDir = parts.slice(0, parts.indexOf('stable')).join('/'); + + expect(specDir).toBe('specification/newservice/resource-manager/Microsoft.NewService'); + }); + + it('should extract spec directory from preview file path', () => { + const previewFile = 'specification/newservice/resource-manager/Microsoft.NewService/preview/2024-01-01-preview/service.json'; + const parts = previewFile.split('/'); + const versionIndex = parts.indexOf('preview'); + const specDir = parts.slice(0, versionIndex).join('/'); + + expect(specDir).toBe('specification/newservice/resource-manager/Microsoft.NewService'); + }); + + it('should treat git ls-tree errors as brand new RP', () => { + let gitError = false; + let isBrandNew = false; + + try { + throw new Error('path not found in ref'); + } catch (error) { + gitError = true; + isBrandNew = true; + } + + expect(gitError).toBe(true); + expect(isBrandNew).toBe(true); + }); + + it('should handle service group in spec directory extraction', () => { + const swaggerFile = 'specification/compute/resource-manager/Microsoft.Compute/DiskRP/stable/2024-01-01/disk.json'; + const parts = swaggerFile.split('/'); + const stableIndex = parts.indexOf('stable'); + const specDir = parts.slice(0, stableIndex).join('/'); + + expect(specDir).toBe('specification/compute/resource-manager/Microsoft.Compute/DiskRP'); + }); + }); + + // ========================================== + // Label Addition Tests + // ========================================== + + describe('label addition', () => { + const expectedLabels = [LABELS.armModeling, LABELS.notReady]; + + it('should define correct label constants', () => { + expect(expectedLabels).toHaveLength(2); + expect(expectedLabels).toContain(LABELS.armModeling); + expect(expectedLabels).toContain(LABELS.notReady); + }); + + it('should add both labels when new RPs detected', () => { + const labelsToAdd = expectedLabels; + + expect(labelsToAdd).toEqual([LABELS.armModeling, LABELS.notReady]); + }); + + it('should skip when GITHUB_TOKEN unavailable', () => { + const githubToken = undefined; + const prNumber = '123'; + + expect(!!(githubToken && prNumber)).toBe(false); + }); + + it('should skip when PR_NUMBER unavailable', () => { + const githubToken = 'token'; + const prNumber = undefined; + + expect(!!(githubToken && prNumber)).toBe(false); + }); + + it('should proceed when both GITHUB_TOKEN and PR_NUMBER available', () => { + const githubToken = 'token'; + const prNumber = '123'; + + expect(!!(githubToken && prNumber)).toBe(true); + }); + + it('should use default repo owner and name', () => { + const repoOwner = process.env.REPO_OWNER || 'Azure'; + const repoName = process.env.REPO_NAME || 'azure-rest-api-specs'; + + expect(repoOwner).toBe('Azure'); + expect(repoName).toBe('azure-rest-api-specs'); + }); + + it('should parse PR number as integer', () => { + const parsedPrNumber = parseInt('456'); + + expect(typeof parsedPrNumber).toBe('number'); + expect(parsedPrNumber).toBe(456); + }); + + it('should add labels once per PR regardless of RP count', () => { + const newRPs = [ + { namespace: 'Microsoft.Service1', serviceName: 'service1' }, + { namespace: 'Microsoft.Service2', serviceName: 'service2' }, + { namespace: 'Microsoft.Service3', serviceName: 'service3' }, ]; + + expect(newRPs).toHaveLength(3); + expect(expectedLabels).toEqual([LABELS.armModeling, LABELS.notReady]); + }); - const outputData = { - newResourceProviders: leaseCheckResults + it('should not add labels when no new RPs detected', () => { + const newRPs = []; + + expect(newRPs.length > 0).toBe(false); + }); + + it('should construct correct API request', () => { + const apiRequest = { + owner: 'Azure', + repo: 'azure-rest-api-specs', + issue_number: 123, + labels: expectedLabels }; + + expect(apiRequest.owner).toBe('Azure'); + expect(apiRequest.repo).toBe('azure-rest-api-specs'); + expect(apiRequest.issue_number).toBe(123); + expect(apiRequest.labels).toEqual([LABELS.armModeling, LABELS.notReady]); + }); - const rp = outputData.newResourceProviders[0]; - const leasePathParts = ['.github/arm-leases', rp.serviceName, rp.namespace]; - if (rp.serviceGroup) { - leasePathParts.push(rp.serviceGroup); + it('should handle errors gracefully', () => { + let errorHandled = false; + + try { + throw new Error('API error'); + } catch (error) { + errorHandled = true; } - leasePathParts.push('lease.yaml'); - const expectedLeasePath = leasePathParts.join('/'); + + expect(errorHandled).toBe(true); + }); + + it('should format error log message with [FAILED] prefix', () => { + const errorMessage = 'Request failed with status code 401'; + const logMessage = `[FAILED] Failed to add labels: ${errorMessage}`; + + expect(logMessage).toContain('[FAILED]'); + expect(logMessage).toContain('Failed to add labels:'); + expect(logMessage).toContain(errorMessage); + }); + }); + + // ========================================== + // Log Message Format Tests + // ========================================== + + describe('log message formatting', () => { + it('should format valid lease log with [OK] indicator', () => { + const rp = { + namespace: 'Microsoft.Test', + leaseValid: true + }; + + const logMessage = ` - ${rp.namespace}: ${rp.leaseValid ? '[OK]' : '[FAILED]'} Lease ${rp.leaseValid ? 'valid' : 'invalid'}`; + + expect(logMessage).toContain('[OK]'); + expect(logMessage).toContain('Lease valid'); + expect(logMessage).not.toContain('[FAILED]'); + expect(logMessage).not.toContain('āœ“'); + expect(logMessage).not.toContain('āœ—'); + }); - expect(expectedLeasePath).toBe('.github/arm-leases/compute/Microsoft.Compute/DiskRP/lease.yaml'); + it('should format invalid lease log with [FAILED] indicator', () => { + const rp = { + namespace: 'Microsoft.Test', + leaseValid: false + }; + + const logMessage = ` - ${rp.namespace}: ${rp.leaseValid ? '[OK]' : '[FAILED]'} Lease ${rp.leaseValid ? 'valid' : 'invalid'}`; + + expect(logMessage).toContain('[FAILED]'); + expect(logMessage).toContain('Lease invalid'); + expect(logMessage).not.toContain('[OK]'); + expect(logMessage).not.toContain('āœ“'); + expect(logMessage).not.toContain('āœ—'); + }); + + it('should format log messages consistently without unicode symbols', () => { + const testCases = [ + { leaseValid: true, expected: '[OK]', notExpected: '[FAILED]' }, + { leaseValid: false, expected: '[FAILED]', notExpected: '[OK]' } + ]; + + testCases.forEach(testCase => { + const logMessage = ` - Microsoft.Test: ${testCase.leaseValid ? '[OK]' : '[FAILED]'} Lease ${testCase.leaseValid ? 'valid' : 'invalid'}`; + + expect(logMessage).toContain(testCase.expected); + expect(logMessage).not.toContain(testCase.notExpected); + expect(logMessage).not.toMatch(/[āœ“āœ—]/); + }); + }); + + it('should use text-based indicators for multiple RPs', () => { + const resourceProviders = [ + { namespace: 'Microsoft.Valid', leaseValid: true }, + { namespace: 'Microsoft.Invalid', leaseValid: false }, + { namespace: 'Microsoft.AnotherValid', leaseValid: true } + ]; + + const logMessages = resourceProviders.map(rp => + ` - ${rp.namespace}: ${rp.leaseValid ? '[OK]' : '[FAILED]'} Lease ${rp.leaseValid ? 'valid' : 'invalid'}` + ); + + expect(logMessages[0]).toContain('[OK]'); + expect(logMessages[1]).toContain('[FAILED]'); + expect(logMessages[2]).toContain('[OK]'); + + logMessages.forEach(msg => { + expect(msg).not.toMatch(/[āœ“āœ—]/); + }); + }); + + it('should format error message with [FAILED] prefix for label addition failures', () => { + const error = new Error('Network timeout'); + const errorLog = `[FAILED] Failed to add labels: ${error.message}`; + + expect(errorLog).toContain('[FAILED]'); + expect(errorLog).toContain('Failed to add labels:'); + expect(errorLog).not.toContain('āœ—'); }); }); }); From f2f002e68782f8ec8778ac422d3869f9d6d3f514 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Tue, 27 Jan 2026 17:18:22 -0600 Subject: [PATCH 020/125] Addressed comments : moved comment on PR to .js, used GH script, changed fetch-depth --- .../detect-new-resource-provider.yaml | 66 +- .../src/detect-new-resource-provider.js | 88 +- .../test/detect-new-resource-provider.test.js | 1053 ++++------------- 3 files changed, 313 insertions(+), 894 deletions(-) diff --git a/.github/workflows/detect-new-resource-provider.yaml b/.github/workflows/detect-new-resource-provider.yaml index 15bcbde64b2e..7137ed738ae0 100644 --- a/.github/workflows/detect-new-resource-provider.yaml +++ b/.github/workflows/detect-new-resource-provider.yaml @@ -26,60 +26,24 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 with: - fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 2 + sparse-checkout: | + .github - - name: Fetch base branch - run: git fetch origin ${{ github.event.pull_request.base.ref }} - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '24' - - - name: Install dependencies - run: npm ci - working-directory: .github + - name: Install dependencies for github-script actions + uses: ./.github/actions/install-deps-github-script - name: Detect new resource providers id: detect - run: node workflows/src/detect-new-resource-provider.js ${{ github.event.pull_request.base.ref }} - working-directory: .github - continue-on-error: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - REPO_OWNER: ${{ github.repository_owner }} - REPO_NAME: ${{ github.event.repository.name }} - - - name: Comment on PR - if: steps.detect.outcome == 'failure' - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: + result-encoding: string script: | - const fs = require('fs'); - const outputFile = '.github/new-rp-output.json'; - - if (fs.existsSync(outputFile)) { - const data = JSON.parse(fs.readFileSync(outputFile, 'utf8')); - const rpList = data.newResourceProviders.map(rp => \`- \\\`\${rp}\\\`\`).join('\\n'); - - const body = \`## šŸ†• New Resource Provider Detected - - The following new resource provider namespace(s) were detected in this PR: - - \${rpList} - - ### āš ļø Action Required - - New resource provider namespaces require having a discussion with the ARM team at **ARM API Modeling Office Hours** before merging. - - šŸ“… **Please schedule here:** https://outlook.office365.com/book/ARMOfficeHours1@microsoft.onmicrosoft.com/?ismsaljsauthenabled=true\`; - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: body - }); - } + const { default: detectNewResourceProvider } = + await import('${{ github.workspace }}/.github/workflows/src/detect-new-resource-provider.js'); + return await detectNewResourceProvider({ + github, + context, + core, + baseBranch: '${{ github.event.pull_request.base.ref }}' + }); diff --git a/.github/workflows/src/detect-new-resource-provider.js b/.github/workflows/src/detect-new-resource-provider.js index c6960bccb656..76c774572f9b 100644 --- a/.github/workflows/src/detect-new-resource-provider.js +++ b/.github/workflows/src/detect-new-resource-provider.js @@ -3,7 +3,6 @@ import { execSync } from 'child_process'; import { readdirSync, existsSync, statSync, writeFileSync } from 'fs'; import { join, dirname } from 'path'; -import { Octokit } from '@octokit/rest'; import { simpleGit } from 'simple-git'; import { getChangedFiles, swagger, example } from '../../shared/src/changed-files.js'; import { checkLease } from './detect-arm-leases.js'; @@ -61,31 +60,16 @@ function resourceProviderExists(specPath, namespace) { /** * Add labels to the PR for new resource providers + * @param {Object} github - GitHub API client from github-script + * @param {Object} context - GitHub context from github-script * @returns {Promise} */ -async function addNewResourceProviderLabels() { - const githubToken = process.env.GITHUB_TOKEN; - const prNumber = process.env.PR_NUMBER; - const repoOwner = process.env.REPO_OWNER || 'Azure'; - const repoName = process.env.REPO_NAME || 'azure-rest-api-specs'; - - if (!githubToken) { - console.log('GITHUB_TOKEN not available, skipping label addition'); - return; - } - - if (!prNumber) { - console.log('PR_NUMBER not available, skipping label addition'); - return; - } - +async function addNewResourceProviderLabels(github, context) { try { - const octokit = new Octokit({ auth: githubToken }); - - await octokit.rest.issues.addLabels({ - owner: repoOwner, - repo: repoName, - issue_number: parseInt(prNumber), + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, labels: NEW_RP_LABELS }); @@ -128,10 +112,17 @@ function extractResourceProviders(files) { // Main Detection Logic // ============================================ -async function main() { - const baseBranch = process.argv[2] || 'main'; +/** + * Main detection logic for GitHub script action + * @param {Object} params - Parameters from github-script + * @param {Object} params.github - GitHub API client + * @param {Object} params.context - GitHub context + * @param {Object} params.core - GitHub Actions core + * @param {string} params.baseBranch - Base branch name + * @returns {Promise} Result status + */ +export default async function detectNewResourceProvider({ github, context, core, baseBranch }) { const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim(); - let exitCode = 0; console.log('Detecting New Resource Providers\n'); @@ -155,9 +146,12 @@ async function main() { if (rmFiles.length === 0) { console.log('No resource-manager files changed'); - process.exit(0); + return 'no-changes'; } + // Extract resource providers from changed files + const changedResourceProviders = extractResourceProviders(rmFiles); + // Pre-check: Verify if spec directories are brand new (don't exist in base branch) const git = simpleGit(repoRoot); const changedSpecDirs = new Set([ @@ -194,7 +188,7 @@ async function main() { if (!hasAtLeastOneBrandNewRP) { console.log('No brand new resource providers detected, spec directories exist in base branch.'); console.log('Skipping workflow.\n'); - process.exit(0); + return 'no-new-rp'; } } @@ -237,13 +231,41 @@ async function main() { JSON.stringify({ newResourceProviders: leaseCheckResults }, null, 2) ); - await addNewResourceProviderLabels(); - exitCode = 1; + await addNewResourceProviderLabels(github, context); + + // Create PR comment with detected resource providers + const rpList = leaseCheckResults + .map(rp => `- \`${rp.namespace}\``) + .join('\n'); + + const commentBody = [ + '## New Resource Provider Detected', + 'The following new resource provider namespace(s) were detected in this PR:\n', + rpList, + '\n### Action Required', + 'New resource provider namespaces require having a discussion with the ARM team at **ARM API Modeling Office Hours** before merging.\n', + '**Please schedule here:**', + 'https://outlook.office365.com/book/ARMOfficeHours1@microsoft.onmicrosoft.com/?ismsaljsauthenabled=true' + ].join('\n'); + + try { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: commentBody + }); + console.log('PR comment created successfully'); + } catch (error) { + console.log(`[FAILED] Failed to create PR comment: ${error.message}`); + } + + // Set the action as failed to indicate new RPs were detected + core.setFailed('New resource providers detected'); + return 'new-rp-detected'; } else { console.log('No new resource providers detected.\n'); + return 'no-new-rp'; } - - process.exit(exitCode); } -main(); diff --git a/.github/workflows/test/detect-new-resource-provider.test.js b/.github/workflows/test/detect-new-resource-provider.test.js index 7b9f8c1d5b57..291095afff27 100644 --- a/.github/workflows/test/detect-new-resource-provider.test.js +++ b/.github/workflows/test/detect-new-resource-provider.test.js @@ -1,900 +1,333 @@ -import { describe, it, expect } from 'vitest'; - -// ============================================ -// Test Fixtures and Mocks -// ============================================ - -const EXISTING_NAMESPACES = ['Microsoft.Existing', 'Microsoft.Another']; - -const SAMPLE_PATHS = { - basic: 'specification/app/resource-manager/Microsoft.App/stable/2023-01-01/app.json', - withGroup: 'specification/compute/resource-manager/Microsoft.Compute/DiskRP/stable/2023-01-01/disk.json', - example: 'specification/app/resource-manager/Microsoft.App/stable/2023-01-01/examples/example.json', - dataPlane: 'specification/app/data-plane/Microsoft.App/stable/2023-01-01/app.json' -}; - -const LABELS = { - armModeling: 'ARMModelingReviewRequired', - notReady: 'NotReadyForARMReview' -}; - -// ============================================ -// Mock Functions -// ============================================ - -function mockResourceProviderExists(specPath, namespace) { - return EXISTING_NAMESPACES.includes(namespace); -} - -function mockExtractResourceProviders(files) { - const resourceProviders = new Map(); - const pattern = /^specification\/([^\/]+)\/resource-manager\/([^\/]+)/; - const groupPattern = /^specification\/[^\/]+\/resource-manager\/([^\/]+)\/([^\/]+)\/(stable|preview|preview-internal)\//; - - for (const file of files) { - const match = file.match(pattern); - if (match) { - const serviceName = match[1]; - const namespace = match[2]; - - const groupMatch = file.match(groupPattern); - if (groupMatch && groupMatch[2] !== 'stable' && groupMatch[2] !== 'preview' && groupMatch[2] !== 'preview-internal') { - resourceProviders.set(namespace, { serviceName, serviceGroup: groupMatch[2] }); - } else { - resourceProviders.set(namespace, { serviceName }); - } - } - } - - return resourceProviders; -} - -// ============================================ -// Test Suites -// ============================================ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import detectNewResourceProvider from '../src/detect-new-resource-provider.js'; describe('detect-new-resource-provider', () => { - - // ========================================== - // Resource Provider Extraction Tests - // ========================================== - - describe('extractResourceProviders', () => { - it('should extract namespace and service from valid paths', () => { - const files = [ - 'specification/app/resource-manager/Microsoft.App/stable/2023-01-01/app.json', - 'specification/compute/resource-manager/Microsoft.Compute/stable/2023-01-01/compute.json', - ]; - - const result = mockExtractResourceProviders(files); - - expect(result.size).toBe(2); - expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); - expect(result.get('Microsoft.Compute')).toEqual({ serviceName: 'compute' }); - }); - - it('should handle multiple files from same namespace', () => { - const files = [ - 'specification/app/resource-manager/Microsoft.App/stable/2023-01-01/app.json', - 'specification/app/resource-manager/Microsoft.App/stable/2023-01-01/models.json', - 'specification/app/resource-manager/Microsoft.App/preview/2023-01-01-preview/app.json', - ]; - - const result = mockExtractResourceProviders(files); - - expect(result.size).toBe(1); - expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); - }); - - it('should ignore non-resource-manager files', () => { - const files = [ - SAMPLE_PATHS.dataPlane, - SAMPLE_PATHS.basic, - 'specification/app/docs/readme.md', - ]; + let mockGithub; + let mockContext; + let mockCore; + + beforeEach(() => { + mockGithub = { + rest: { + issues: { + addLabels: vi.fn().mockResolvedValue({}), + createComment: vi.fn().mockResolvedValue({}) + } + } + }; - const result = mockExtractResourceProviders(files); + mockContext = { + repo: { owner: 'Azure', repo: 'azure-rest-api-specs' }, + issue: { number: 123 } + }; - expect(result.size).toBe(1); - expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); - }); + mockCore = { + setFailed: vi.fn() + }; + }); - it('should handle empty file list', () => { - const result = mockExtractResourceProviders([]); - expect(result.size).toBe(0); + describe('function export', () => { + it('should be exported as default export', () => { + expect(typeof detectNewResourceProvider).toBe('function'); }); + }); - it('should extract service group when present', () => { - const files = [SAMPLE_PATHS.withGroup]; - const result = mockExtractResourceProviders(files); - - expect(result.size).toBe(1); - expect(result.get('Microsoft.Compute')).toEqual({ - serviceName: 'compute', - serviceGroup: 'DiskRP' + describe('no changes scenario', () => { + it('should return "no-changes" when no resource-manager files are changed', async () => { + const result = await detectNewResourceProvider({ + github: mockGithub, + context: mockContext, + core: mockCore, + baseBranch: 'main' }); - }); - it('should handle mix of files with and without service groups', () => { - const files = [SAMPLE_PATHS.withGroup, SAMPLE_PATHS.basic]; - const result = mockExtractResourceProviders(files); - - expect(result.size).toBe(2); - expect(result.get('Microsoft.Compute')).toEqual({ serviceName: 'compute', serviceGroup: 'DiskRP' }); - expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); + expect(result).toBe('no-changes'); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + expect(mockGithub.rest.issues.addLabels).not.toHaveBeenCalled(); + expect(mockGithub.rest.issues.createComment).not.toHaveBeenCalled(); }); + }); - it('should not treat version folders as service groups', () => { - const testCases = [ - 'specification/app/resource-manager/Microsoft.App/preview-2025-10-11/app.json', - 'specification/app/resource-manager/Microsoft.App/preview-internal/2023-01-01/app.json', - 'specification/app/resource-manager/Microsoft.App/stable-2024-01-01/app.json', - ]; - - testCases.forEach(filePath => { - const result = mockExtractResourceProviders([filePath]); - expect(result.get('Microsoft.App')).toEqual({ serviceName: 'app' }); + describe('parameter validation', () => { + it('should accept all required parameters', async () => { + const result = await detectNewResourceProvider({ + github: mockGithub, + context: mockContext, + core: mockCore, + baseBranch: 'main' }); - }); - - it('should extract service group when folder does not start with stable or preview', () => { - const files = [ - 'specification/compute/resource-manager/Microsoft.Compute/CloudServiceRP/stable/2023-01-01/cloudservice.json', - ]; - const result = mockExtractResourceProviders(files); - - expect(result.get('Microsoft.Compute')).toEqual({ - serviceName: 'compute', - serviceGroup: 'CloudServiceRP' - }); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); }); - it('should handle nested paths correctly', () => { - const files = [ - 'specification/compute/resource-manager/Microsoft.Compute/DiskRP/stable/2023-01-01/disk.json', - 'specification/compute/resource-manager/Microsoft.Compute/DiskRP/stable/2023-01-01/models.json', - ]; - - const result = mockExtractResourceProviders(files); - - expect(result.size).toBe(1); - expect(result.get('Microsoft.Compute')).toEqual({ - serviceName: 'compute', - serviceGroup: 'DiskRP' + it('should work with different baseBranch values', async () => { + const result = await detectNewResourceProvider({ + github: mockGithub, + context: mockContext, + core: mockCore, + baseBranch: 'main' }); - }); - it('should extract multiple namespaces with different service groups', () => { - const files = [ - 'specification/compute/resource-manager/Microsoft.Compute/DiskRP/stable/2023-01-01/disk.json', - 'specification/compute/resource-manager/Microsoft.Compute/VirtualMachineRP/stable/2023-01-01/vm.json', - ]; - - const result = mockExtractResourceProviders(files); - - expect(result.size).toBe(1); - expect(result.get('Microsoft.Compute')).toEqual({ serviceName: 'compute', serviceGroup: 'VirtualMachineRP' }); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); }); }); - // ========================================== - // Resource Provider Existence Tests - // ========================================== - - describe('resourceProviderExists', () => { - it('should return true for existing namespaces', () => { - expect(mockResourceProviderExists('/path/to/spec', 'Microsoft.Existing')).toBe(true); - expect(mockResourceProviderExists('/path/to/spec', 'Microsoft.Another')).toBe(true); - }); - - it('should return false for new namespaces', () => { - expect(mockResourceProviderExists('/path/to/spec', 'Microsoft.NewService')).toBe(false); - }); - - it('should return false for non-matching namespaces', () => { - expect(mockResourceProviderExists('/path/to/spec', 'Microsoft.Unknown')).toBe(false); - }); - }); - - // ========================================== - // New Resource Provider Detection Tests - // ========================================== - - describe('new resource provider detection', () => { - it('should identify new resource providers correctly', () => { - const changedRPs = new Map([ - ['Microsoft.NewService', { serviceName: 'newservice' }], - ['Microsoft.Existing', { serviceName: 'existing' }] - ]); - - const newResourceProviders = []; - for (const [rp, info] of changedRPs) { - if (!mockResourceProviderExists('/path/to/spec', rp)) { - newResourceProviders.push({ namespace: rp, ...info }); - } - } - - expect(newResourceProviders).toHaveLength(1); - expect(newResourceProviders[0].namespace).toBe('Microsoft.NewService'); - expect(newResourceProviders[0].serviceName).toBe('newservice'); - }); - - it('should handle no new resource providers', () => { - const changedRPs = new Map([ - ['Microsoft.Existing', { serviceName: 'existing' }], - ['Microsoft.Another', { serviceName: 'another' }] - ]); - - const newResourceProviders = []; - for (const [rp, info] of changedRPs) { - if (!mockResourceProviderExists('/path/to/spec', rp)) { - newResourceProviders.push({ namespace: rp, ...info }); - } - } + describe('github API interactions', () => { + it('should not call GitHub API when no changes detected', async () => { + await detectNewResourceProvider({ + github: mockGithub, + context: mockContext, + core: mockCore, + baseBranch: 'main' + }); - expect(newResourceProviders).toHaveLength(0); + expect(mockGithub.rest.issues.addLabels).not.toHaveBeenCalled(); + expect(mockGithub.rest.issues.createComment).not.toHaveBeenCalled(); }); - it('should handle multiple new resource providers', () => { - const changedRPs = new Map([ - ['Microsoft.Service1', { serviceName: 'service1' }], - ['Microsoft.Service2', { serviceName: 'service2' }] - ]); - - const newResourceProviders = []; - for (const [rp, info] of changedRPs) { - if (!mockResourceProviderExists('/path/to/spec', rp)) { - newResourceProviders.push({ namespace: rp, ...info }); + it('should handle GitHub API errors gracefully', async () => { + const errorGithub = { + rest: { + issues: { + addLabels: vi.fn().mockRejectedValue(new Error('API Error')), + createComment: vi.fn().mockRejectedValue(new Error('API Error')) + } } - } - - expect(newResourceProviders).toHaveLength(2); - expect(newResourceProviders[0].namespace).toBe('Microsoft.Service1'); - expect(newResourceProviders[1].namespace).toBe('Microsoft.Service2'); - }); - - it('should identify new resource providers with service groups', () => { - const changedRPs = new Map([ - ['Microsoft.Compute', { serviceName: 'compute', serviceGroup: 'DiskRP' }] - ]); + }; - const newResourceProviders = []; - for (const [rp, info] of changedRPs) { - if (!mockResourceProviderExists('/path/to/spec', rp)) { - newResourceProviders.push({ namespace: rp, ...info }); - } - } + const result = await detectNewResourceProvider({ + github: errorGithub, + context: mockContext, + core: mockCore, + baseBranch: 'main' + }); - expect(newResourceProviders).toHaveLength(1); - expect(newResourceProviders[0].serviceGroup).toBe('DiskRP'); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); }); }); - // ========================================== - // Lease Validation Tests - // ========================================== - - describe('lease validation', () => { - it('should create correct output structure', () => { - const leaseCheckResults = [{ - namespace: 'Microsoft.Test', - serviceName: 'test', - leaseValid: true, - leaseMessage: 'Lease is valid' - }]; - - const outputData = { newResourceProviders: leaseCheckResults }; - - expect(outputData.newResourceProviders).toHaveLength(1); - expect(outputData.newResourceProviders[0]).toHaveProperty('namespace'); - expect(outputData.newResourceProviders[0]).toHaveProperty('leaseValid'); - }); - - it('should include service group in output when present', () => { - const leaseCheckResults = [{ - namespace: 'Microsoft.Compute', - serviceName: 'compute', - serviceGroup: 'DiskRP', - leaseValid: false, - leaseMessage: 'No lease file found' - }]; - - const outputData = { newResourceProviders: leaseCheckResults }; - - expect(outputData.newResourceProviders[0].serviceGroup).toBe('DiskRP'); - }); - - it('should handle valid lease in output', () => { - const leaseCheckResults = [{ - namespace: 'Microsoft.Test', - serviceName: 'test', - leaseValid: true, - leaseMessage: 'Lease is valid' - }]; - - expect(leaseCheckResults[0].leaseValid).toBe(true); - expect(leaseCheckResults[0].leaseMessage).toBe('Lease is valid'); - }); - - it('should handle invalid lease in output', () => { - const leaseCheckResults = [{ - namespace: 'Microsoft.Test', - serviceName: 'test', - leaseValid: false, - leaseMessage: 'No lease file found' - }]; + describe('return values', () => { + it('should return one of three valid statuses', async () => { + const result = await detectNewResourceProvider({ + github: mockGithub, + context: mockContext, + core: mockCore, + baseBranch: 'main' + }); - expect(leaseCheckResults[0].leaseValid).toBe(false); - expect(leaseCheckResults[0].leaseMessage).toBe('No lease file found'); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); }); }); - // ========================================== - // JSON Output Generation Tests - // ========================================== - - describe('JSON output generation', () => { - it('should create output with correct structure', () => { - const leaseCheckResults = [{ - namespace: 'Microsoft.Test', - serviceName: 'test', - leaseValid: false, - leaseMessage: 'No lease file found' - }]; - - const outputData = { newResourceProviders: leaseCheckResults }; - - expect(outputData).toHaveProperty('newResourceProviders'); - expect(Array.isArray(outputData.newResourceProviders)).toBe(true); - }); - - it('should include all required fields', () => { - const leaseCheckResults = [{ - namespace: 'Microsoft.Test', - serviceName: 'test', - serviceGroup: 'TestRP', - leaseValid: false, - leaseMessage: 'No lease file found' - }]; - - const rp = leaseCheckResults[0]; - - expect(rp).toHaveProperty('namespace'); - expect(rp).toHaveProperty('serviceName'); - expect(rp).toHaveProperty('serviceGroup'); - expect(rp).toHaveProperty('leaseValid'); - expect(rp).toHaveProperty('leaseMessage'); - }); - - it('should format output for PR comment consumption', () => { - const leaseCheckResults = [ - { - namespace: 'Microsoft.Test1', - serviceName: 'test1', - leaseValid: true, - leaseMessage: 'Lease is valid' - }, - { - namespace: 'Microsoft.Test2', - serviceName: 'test2', - leaseValid: false, - leaseMessage: 'No lease file found' - } - ]; - - const outputData = { newResourceProviders: leaseCheckResults }; + describe('context validation', () => { + it('should handle minimal context object', async () => { + const minimalContext = { + repo: { owner: 'test-owner', repo: 'test-repo' }, + issue: { number: 1 } + }; - expect(outputData.newResourceProviders).toHaveLength(2); - outputData.newResourceProviders.forEach(rp => { - expect(rp.namespace).toBeTruthy(); - expect(typeof rp.leaseValid).toBe('boolean'); + const result = await detectNewResourceProvider({ + github: mockGithub, + context: minimalContext, + core: mockCore, + baseBranch: 'main' }); - }); - - it('should handle multiple new RPs with mixed lease statuses', () => { - const leaseCheckResults = [ - { - namespace: 'Microsoft.WithLease', - serviceName: 'withlease', - leaseValid: true, - leaseMessage: 'Lease is valid' - }, - { - namespace: 'Microsoft.WithoutLease', - serviceName: 'withoutlease', - leaseValid: false, - leaseMessage: 'No lease file found' - } - ]; - - const outputData = { newResourceProviders: leaseCheckResults }; - - const validLeases = outputData.newResourceProviders.filter(rp => rp.leaseValid); - const invalidLeases = outputData.newResourceProviders.filter(rp => !rp.leaseValid); - - expect(validLeases).toHaveLength(1); - expect(invalidLeases).toHaveLength(1); - }); - - it('should maintain order of RPs in output', () => { - const leaseCheckResults = [ - { namespace: 'Microsoft.First', serviceName: 'first', leaseValid: true, leaseMessage: 'Valid' }, - { namespace: 'Microsoft.Second', serviceName: 'second', leaseValid: false, leaseMessage: 'Invalid' }, - { namespace: 'Microsoft.Third', serviceName: 'third', leaseValid: true, leaseMessage: 'Valid' } - ]; - - const outputData = { newResourceProviders: leaseCheckResults }; - expect(outputData.newResourceProviders[0].namespace).toBe('Microsoft.First'); - expect(outputData.newResourceProviders[1].namespace).toBe('Microsoft.Second'); - expect(outputData.newResourceProviders[2].namespace).toBe('Microsoft.Third'); - }); - - it('should serialize to valid JSON', () => { - const leaseCheckResults = [{ - namespace: 'Microsoft.Test', - serviceName: 'test', - leaseValid: false, - leaseMessage: 'No lease file found' - }]; - - const outputData = { newResourceProviders: leaseCheckResults }; - const jsonString = JSON.stringify(outputData); - const parsed = JSON.parse(jsonString); - - expect(parsed).toEqual(outputData); - }); - - it('should handle empty RPs list', () => { - const outputData = { newResourceProviders: [] }; - - expect(outputData.newResourceProviders).toHaveLength(0); - expect(Array.isArray(outputData.newResourceProviders)).toBe(true); - }); - - it('should include optional service group field', () => { - const leaseCheckResults = [{ - namespace: 'Microsoft.Compute', - serviceName: 'compute', - serviceGroup: 'DiskRP', - leaseValid: true, - leaseMessage: 'Valid' - }]; - - const outputData = { newResourceProviders: leaseCheckResults }; - - expect(outputData.newResourceProviders[0].serviceGroup).toBe('DiskRP'); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); }); }); - // ========================================== - // PR Comment Data Preparation Tests - // ========================================== - - describe('PR comment data preparation', () => { - it('should provide data for office hours message', () => { - const leaseCheckResults = [{ - namespace: 'Microsoft.Test', - serviceName: 'test', - leaseValid: false, - leaseMessage: 'No lease file found' - }]; - - const outputData = { newResourceProviders: leaseCheckResults }; - - expect(outputData.newResourceProviders.length).toBeGreaterThan(0); - outputData.newResourceProviders.forEach(rp => { - expect(rp.namespace).toBeTruthy(); - expect(rp.serviceName).toBeTruthy(); - expect(typeof rp.leaseValid).toBe('boolean'); - expect(rp.leaseMessage).toBeTruthy(); - }); - }); - - it('should identify RPs needing office hours', () => { - const leaseCheckResults = [ - { - namespace: 'Microsoft.WithLease', - serviceName: 'withlease', - leaseValid: true, - leaseMessage: 'Lease is valid' - }, - { - namespace: 'Microsoft.WithoutLease', - serviceName: 'withoutlease', - serviceGroup: 'TestRP', - leaseValid: false, - leaseMessage: 'No lease file found' - } - ]; - - const outputData = { newResourceProviders: leaseCheckResults }; - const rpsNeedingOfficeHours = outputData.newResourceProviders.filter(rp => !rp.leaseValid); - const rpsWithValidLease = outputData.newResourceProviders.filter(rp => rp.leaseValid); - - expect(rpsNeedingOfficeHours).toHaveLength(1); - expect(rpsNeedingOfficeHours[0].namespace).toBe('Microsoft.WithoutLease'); - expect(rpsWithValidLease).toHaveLength(1); - expect(rpsWithValidLease[0].namespace).toBe('Microsoft.WithLease'); - }); - - it('should format lease path for PR comment', () => { - const rp = { - namespace: 'Microsoft.Test', - serviceName: 'testservice', - leaseValid: false + describe('edge cases', () => { + it('should handle issue number as zero', async () => { + const zeroIssueContext = { + repo: { owner: 'Azure', repo: 'azure-rest-api-specs' }, + issue: { number: 0 } }; - const leasePath = `.github/arm-leases/${rp.serviceName}/${rp.namespace}/lease.yaml`; + const result = await detectNewResourceProvider({ + github: mockGithub, + context: zeroIssueContext, + core: mockCore, + baseBranch: 'main' + }); - expect(leasePath).toBe('.github/arm-leases/testservice/Microsoft.Test/lease.yaml'); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); }); - it('should format lease path with service group', () => { - const rp = { - namespace: 'Microsoft.Compute', - serviceName: 'compute', - serviceGroup: 'DiskRP', - leaseValid: false + it('should handle very large issue numbers', async () => { + const largeIssueContext = { + repo: { owner: 'Azure', repo: 'azure-rest-api-specs' }, + issue: { number: 999999999 } }; - const leasePathParts = ['.github/arm-leases', rp.serviceName, rp.namespace]; - if (rp.serviceGroup) { - leasePathParts.push(rp.serviceGroup); - } - leasePathParts.push('lease.yaml'); - const leasePath = leasePathParts.join('/'); - - expect(leasePath).toBe('.github/arm-leases/compute/Microsoft.Compute/DiskRP/lease.yaml'); - }); - - it('should distinguish between valid and invalid leases for messaging', () => { - const leaseCheckResults = [ - { namespace: 'Microsoft.Valid', serviceName: 'valid', leaseValid: true, leaseMessage: 'Valid' }, - { namespace: 'Microsoft.Invalid', serviceName: 'invalid', leaseValid: false, leaseMessage: 'Invalid' } - ]; - - const invalidLeases = leaseCheckResults.filter(rp => !rp.leaseValid); - const validLeases = leaseCheckResults.filter(rp => rp.leaseValid); - - expect(invalidLeases.length).toBe(1); - expect(validLeases.length).toBe(1); - }); - - it('should provide complete RP information for PR comment', () => { - const rp = { - namespace: 'Microsoft.Test', - serviceName: 'test', - serviceGroup: 'SubGroup', - leaseValid: false, - leaseMessage: 'No lease file found' - }; + const result = await detectNewResourceProvider({ + github: mockGithub, + context: largeIssueContext, + core: mockCore, + baseBranch: 'main' + }); - expect(rp).toHaveProperty('namespace'); - expect(rp).toHaveProperty('serviceName'); - expect(rp).toHaveProperty('serviceGroup'); - expect(rp).toHaveProperty('leaseValid'); - expect(rp).toHaveProperty('leaseMessage'); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); }); - it('should handle RPs without service group in PR comment data', () => { - const rp = { - namespace: 'Microsoft.Simple', - serviceName: 'simple', - leaseValid: false, - leaseMessage: 'No lease' + it('should handle empty string repo owner', async () => { + const emptyOwnerContext = { + repo: { owner: '', repo: 'azure-rest-api-specs' }, + issue: { number: 123 } }; - expect(rp.serviceGroup).toBeUndefined(); - expect(rp.namespace).toBe('Microsoft.Simple'); - expect(rp.serviceName).toBe('simple'); - }); - - it('should prepare data for multiple RPs in single comment', () => { - const leaseCheckResults = [ - { namespace: 'Microsoft.One', serviceName: 'one', leaseValid: false, leaseMessage: 'No lease' }, - { namespace: 'Microsoft.Two', serviceName: 'two', leaseValid: false, leaseMessage: 'No lease' }, - { namespace: 'Microsoft.Three', serviceName: 'three', leaseValid: true, leaseMessage: 'Valid' } - ]; - - const needsOfficeHours = leaseCheckResults.filter(rp => !rp.leaseValid); - const hasValidLease = leaseCheckResults.filter(rp => rp.leaseValid); + const result = await detectNewResourceProvider({ + github: mockGithub, + context: emptyOwnerContext, + core: mockCore, + baseBranch: 'main' + }); - expect(needsOfficeHours).toHaveLength(2); - expect(hasValidLease).toHaveLength(1); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); }); - }); - // ========================================== - // Brand New RP Validation Tests - // ========================================== - - describe('brand new resource provider validation', () => { - it('should identify directory as brand new when no files exist in base', () => { - const gitLsTreeResult = ''; - const hasSwaggerFiles = gitLsTreeResult.split('\n') - .filter(file => file.includes('/resource-manager/') && file.endsWith('.json')) - .length > 0; - - expect(hasSwaggerFiles).toBe(false); - }); + it('should handle empty string repo name', async () => { + const emptyRepoContext = { + repo: { owner: 'Azure', repo: '' }, + issue: { number: 123 } + }; - it('should identify directory as brand new when only non-swagger files exist', () => { - const gitLsTreeResult = [ - 'specification/newservice/resource-manager/Microsoft.NewService/readme.md', - 'specification/newservice/resource-manager/Microsoft.NewService/examples/example.json' - ].join('\n'); - - const hasSwaggerFiles = gitLsTreeResult.split('\n') - .filter(file => file.includes('/resource-manager/') && file.endsWith('.json') && !file.includes('/examples/')) - .length > 0; - - expect(hasSwaggerFiles).toBe(false); - }); + const result = await detectNewResourceProvider({ + github: mockGithub, + context: emptyRepoContext, + core: mockCore, + baseBranch: 'main' + }); - it('should identify directory as existing when swagger files exist in base', () => { - const gitLsTreeResult = [ - 'specification/compute/resource-manager/Microsoft.Compute/stable/2023-01-01/compute.json', - 'specification/compute/resource-manager/Microsoft.Compute/stable/2023-01-01/models.json' - ].join('\n'); - - const hasSwaggerFiles = gitLsTreeResult.split('\n') - .filter(file => file.includes('/resource-manager/') && file.endsWith('.json') && !file.includes('/examples/')) - .length > 0; - - expect(hasSwaggerFiles).toBe(true); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); }); - it('should continue workflow when at least one brand new RP exists', () => { - const specDirChecks = [ - { dir: 'specification/newservice/resource-manager/Microsoft.NewService', hasFilesInBase: false }, - { dir: 'specification/compute/resource-manager/Microsoft.Compute', hasFilesInBase: true }, - ]; - - const hasAtLeastOneBrandNewRP = specDirChecks.some(check => !check.hasFilesInBase); - - expect(hasAtLeastOneBrandNewRP).toBe(true); - }); + it('should handle addLabels throwing network timeout', async () => { + const timeoutGithub = { + rest: { + issues: { + addLabels: vi.fn().mockRejectedValue(new Error('Network timeout')), + createComment: vi.fn().mockResolvedValue({}) + } + } + }; - it('should skip workflow when no brand new RPs exist', () => { - const specDirChecks = [ - { dir: 'specification/compute/resource-manager/Microsoft.Compute', hasFilesInBase: true }, - { dir: 'specification/storage/resource-manager/Microsoft.Storage', hasFilesInBase: true }, - ]; - - const hasAtLeastOneBrandNewRP = specDirChecks.some(check => !check.hasFilesInBase); - - expect(hasAtLeastOneBrandNewRP).toBe(false); - }); + const result = await detectNewResourceProvider({ + github: timeoutGithub, + context: mockContext, + core: mockCore, + baseBranch: 'main' + }); - it('should extract spec directory from swagger file path', () => { - const swaggerFile = 'specification/newservice/resource-manager/Microsoft.NewService/stable/2024-01-01/service.json'; - const parts = swaggerFile.split('/'); - const specDir = parts.slice(0, parts.indexOf('stable')).join('/'); - - expect(specDir).toBe('specification/newservice/resource-manager/Microsoft.NewService'); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); }); - it('should extract spec directory from preview file path', () => { - const previewFile = 'specification/newservice/resource-manager/Microsoft.NewService/preview/2024-01-01-preview/service.json'; - const parts = previewFile.split('/'); - const versionIndex = parts.indexOf('preview'); - const specDir = parts.slice(0, versionIndex).join('/'); - - expect(specDir).toBe('specification/newservice/resource-manager/Microsoft.NewService'); - }); + it('should handle createComment throwing permission error', async () => { + const permissionGithub = { + rest: { + issues: { + addLabels: vi.fn().mockResolvedValue({}), + createComment: vi.fn().mockRejectedValue(new Error('Permission denied')) + } + } + }; - it('should treat git ls-tree errors as brand new RP', () => { - let gitError = false; - let isBrandNew = false; - - try { - throw new Error('path not found in ref'); - } catch (error) { - gitError = true; - isBrandNew = true; - } - - expect(gitError).toBe(true); - expect(isBrandNew).toBe(true); - }); + const result = await detectNewResourceProvider({ + github: permissionGithub, + context: mockContext, + core: mockCore, + baseBranch: 'main' + }); - it('should handle service group in spec directory extraction', () => { - const swaggerFile = 'specification/compute/resource-manager/Microsoft.Compute/DiskRP/stable/2024-01-01/disk.json'; - const parts = swaggerFile.split('/'); - const stableIndex = parts.indexOf('stable'); - const specDir = parts.slice(0, stableIndex).join('/'); - - expect(specDir).toBe('specification/compute/resource-manager/Microsoft.Compute/DiskRP'); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); }); - }); - // ========================================== - // Label Addition Tests - // ========================================== - - describe('label addition', () => { - const expectedLabels = [LABELS.armModeling, LABELS.notReady]; - - it('should define correct label constants', () => { - expect(expectedLabels).toHaveLength(2); - expect(expectedLabels).toContain(LABELS.armModeling); - expect(expectedLabels).toContain(LABELS.notReady); - }); + it('should handle both GitHub API methods failing', async () => { + const failingGithub = { + rest: { + issues: { + addLabels: vi.fn().mockRejectedValue(new Error('Service unavailable')), + createComment: vi.fn().mockRejectedValue(new Error('Service unavailable')) + } + } + }; - it('should add both labels when new RPs detected', () => { - const labelsToAdd = expectedLabels; - - expect(labelsToAdd).toEqual([LABELS.armModeling, LABELS.notReady]); - }); + const result = await detectNewResourceProvider({ + github: failingGithub, + context: mockContext, + core: mockCore, + baseBranch: 'main' + }); - it('should skip when GITHUB_TOKEN unavailable', () => { - const githubToken = undefined; - const prNumber = '123'; - - expect(!!(githubToken && prNumber)).toBe(false); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); }); - it('should skip when PR_NUMBER unavailable', () => { - const githubToken = 'token'; - const prNumber = undefined; - - expect(!!(githubToken && prNumber)).toBe(false); - }); + it('should handle core.setFailed as undefined', async () => { + const noCoreSetFailed = {}; - it('should proceed when both GITHUB_TOKEN and PR_NUMBER available', () => { - const githubToken = 'token'; - const prNumber = '123'; - - expect(!!(githubToken && prNumber)).toBe(true); - }); + const result = await detectNewResourceProvider({ + github: mockGithub, + context: mockContext, + core: noCoreSetFailed, + baseBranch: 'main' + }); - it('should use default repo owner and name', () => { - const repoOwner = process.env.REPO_OWNER || 'Azure'; - const repoName = process.env.REPO_NAME || 'azure-rest-api-specs'; - - expect(repoOwner).toBe('Azure'); - expect(repoName).toBe('azure-rest-api-specs'); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); }); - it('should parse PR number as integer', () => { - const parsedPrNumber = parseInt('456'); - - expect(typeof parsedPrNumber).toBe('number'); - expect(parsedPrNumber).toBe(456); - }); + it('should handle multiple concurrent calls', async () => { + const calls = Array(3).fill(null).map(() => + detectNewResourceProvider({ + github: mockGithub, + context: mockContext, + core: mockCore, + baseBranch: 'main' + }) + ); - it('should add labels once per PR regardless of RP count', () => { - const newRPs = [ - { namespace: 'Microsoft.Service1', serviceName: 'service1' }, - { namespace: 'Microsoft.Service2', serviceName: 'service2' }, - { namespace: 'Microsoft.Service3', serviceName: 'service3' }, - ]; - - expect(newRPs).toHaveLength(3); - expect(expectedLabels).toEqual([LABELS.armModeling, LABELS.notReady]); - }); + const results = await Promise.all(calls); - it('should not add labels when no new RPs detected', () => { - const newRPs = []; - - expect(newRPs.length > 0).toBe(false); + results.forEach(result => { + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); + }); }); - it('should construct correct API request', () => { - const apiRequest = { - owner: 'Azure', - repo: 'azure-rest-api-specs', - issue_number: 123, - labels: expectedLabels + it('should handle context with additional unexpected properties', async () => { + const extendedContext = { + repo: { owner: 'Azure', repo: 'azure-rest-api-specs', extra: 'data' }, + issue: { number: 123, title: 'Test PR', labels: [] }, + payload: { action: 'opened' }, + unexpected: 'property' }; - - expect(apiRequest.owner).toBe('Azure'); - expect(apiRequest.repo).toBe('azure-rest-api-specs'); - expect(apiRequest.issue_number).toBe(123); - expect(apiRequest.labels).toEqual([LABELS.armModeling, LABELS.notReady]); - }); - it('should handle errors gracefully', () => { - let errorHandled = false; - - try { - throw new Error('API error'); - } catch (error) { - errorHandled = true; - } - - expect(errorHandled).toBe(true); - }); - - it('should format error log message with [FAILED] prefix', () => { - const errorMessage = 'Request failed with status code 401'; - const logMessage = `[FAILED] Failed to add labels: ${errorMessage}`; - - expect(logMessage).toContain('[FAILED]'); - expect(logMessage).toContain('Failed to add labels:'); - expect(logMessage).toContain(errorMessage); - }); - }); + const result = await detectNewResourceProvider({ + github: mockGithub, + context: extendedContext, + core: mockCore, + baseBranch: 'main' + }); - // ========================================== - // Log Message Format Tests - // ========================================== - - describe('log message formatting', () => { - it('should format valid lease log with [OK] indicator', () => { - const rp = { - namespace: 'Microsoft.Test', - leaseValid: true - }; - - const logMessage = ` - ${rp.namespace}: ${rp.leaseValid ? '[OK]' : '[FAILED]'} Lease ${rp.leaseValid ? 'valid' : 'invalid'}`; - - expect(logMessage).toContain('[OK]'); - expect(logMessage).toContain('Lease valid'); - expect(logMessage).not.toContain('[FAILED]'); - expect(logMessage).not.toContain('āœ“'); - expect(logMessage).not.toContain('āœ—'); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); }); - it('should format invalid lease log with [FAILED] indicator', () => { - const rp = { - namespace: 'Microsoft.Test', - leaseValid: false + it('should not throw when GitHub methods return null', async () => { + const nullGithub = { + rest: { + issues: { + addLabels: vi.fn().mockResolvedValue(null), + createComment: vi.fn().mockResolvedValue(null) + } + } }; - - const logMessage = ` - ${rp.namespace}: ${rp.leaseValid ? '[OK]' : '[FAILED]'} Lease ${rp.leaseValid ? 'valid' : 'invalid'}`; - - expect(logMessage).toContain('[FAILED]'); - expect(logMessage).toContain('Lease invalid'); - expect(logMessage).not.toContain('[OK]'); - expect(logMessage).not.toContain('āœ“'); - expect(logMessage).not.toContain('āœ—'); - }); - it('should format log messages consistently without unicode symbols', () => { - const testCases = [ - { leaseValid: true, expected: '[OK]', notExpected: '[FAILED]' }, - { leaseValid: false, expected: '[FAILED]', notExpected: '[OK]' } - ]; - - testCases.forEach(testCase => { - const logMessage = ` - Microsoft.Test: ${testCase.leaseValid ? '[OK]' : '[FAILED]'} Lease ${testCase.leaseValid ? 'valid' : 'invalid'}`; - - expect(logMessage).toContain(testCase.expected); - expect(logMessage).not.toContain(testCase.notExpected); - expect(logMessage).not.toMatch(/[āœ“āœ—]/); + const result = await detectNewResourceProvider({ + github: nullGithub, + context: mockContext, + core: mockCore, + baseBranch: 'main' }); - }); - - it('should use text-based indicators for multiple RPs', () => { - const resourceProviders = [ - { namespace: 'Microsoft.Valid', leaseValid: true }, - { namespace: 'Microsoft.Invalid', leaseValid: false }, - { namespace: 'Microsoft.AnotherValid', leaseValid: true } - ]; - - const logMessages = resourceProviders.map(rp => - ` - ${rp.namespace}: ${rp.leaseValid ? '[OK]' : '[FAILED]'} Lease ${rp.leaseValid ? 'valid' : 'invalid'}` - ); - - expect(logMessages[0]).toContain('[OK]'); - expect(logMessages[1]).toContain('[FAILED]'); - expect(logMessages[2]).toContain('[OK]'); - - logMessages.forEach(msg => { - expect(msg).not.toMatch(/[āœ“āœ—]/); - }); - }); - it('should format error message with [FAILED] prefix for label addition failures', () => { - const error = new Error('Network timeout'); - const errorLog = `[FAILED] Failed to add labels: ${error.message}`; - - expect(errorLog).toContain('[FAILED]'); - expect(errorLog).toContain('Failed to add labels:'); - expect(errorLog).not.toContain('āœ—'); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); }); }); }); From c954ba1865ed1bf98be5733ad189433d9ce2b19d Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Thu, 5 Feb 2026 17:20:48 -0600 Subject: [PATCH 021/125] Itr 1: Addressed comments --- .../detect-new-resource-provider.yaml | 33 ++++++- .github/workflows/src/detect-arm-leases.js | 43 ++++---- .../src/detect-new-resource-provider.js | 98 ++++++++++++------- .../test/detect-new-resource-provider.test.js | 48 ++++----- 4 files changed, 141 insertions(+), 81 deletions(-) diff --git a/.github/workflows/detect-new-resource-provider.yaml b/.github/workflows/detect-new-resource-provider.yaml index 7137ed738ae0..3631f587e1fa 100644 --- a/.github/workflows/detect-new-resource-provider.yaml +++ b/.github/workflows/detect-new-resource-provider.yaml @@ -37,7 +37,6 @@ jobs: id: detect uses: actions/github-script@v8 with: - result-encoding: string script: | const { default: detectNewResourceProvider } = await import('${{ github.workspace }}/.github/workflows/src/detect-new-resource-provider.js'); @@ -47,3 +46,35 @@ jobs: core, baseBranch: '${{ github.event.pull_request.base.ref }}' }); + + - if: fromJson(steps.detect.outputs.result).labelActions['ARMModelingReviewRequired'] != 'none' + name: Upload artifact for ARMModelingReviewRequired label + uses: ./.github/actions/add-label-artifact + with: + name: "ARMModelingReviewRequired" + value: "${{ fromJson(steps.detect.outputs.result).labelActions['ARMModelingReviewRequired'] == 'add' }}" + + - if: fromJson(steps.detect.outputs.result).labelActions['NotReadyForARMReview'] != 'none' + name: Upload artifact for NotReadyForARMReview label + uses: ./.github/actions/add-label-artifact + with: + name: "NotReadyForARMReview" + value: "${{ fromJson(steps.detect.outputs.result).labelActions['NotReadyForARMReview'] == 'add' }}" + + - if: | + always() && + fromJson(steps.detect.outputs.result).headSha + name: Upload artifact with head SHA + uses: ./.github/actions/add-empty-artifact + with: + name: "head-sha" + value: "${{ fromJson(steps.detect.outputs.result).headSha }}" + + - if: | + always() && + fromJson(steps.detect.outputs.result).issueNumber > 0 + name: Upload artifact with issue number + uses: ./.github/actions/add-empty-artifact + with: + name: "issue-number" + value: "${{ fromJson(steps.detect.outputs.result).issueNumber }}" diff --git a/.github/workflows/src/detect-arm-leases.js b/.github/workflows/src/detect-arm-leases.js index 78c8ca77d296..c4e13ac15425 100644 --- a/.github/workflows/src/detect-arm-leases.js +++ b/.github/workflows/src/detect-arm-leases.js @@ -25,15 +25,15 @@ function buildLeasePath(repoRoot, serviceName, resourceProvider, serviceGroup = * @param {string} serviceName - Service name * @param {string} resourceProvider - Resource provider namespace * @param {string} serviceGroup - Optional service group - * @returns {boolean} True if lease exists and is valid, false otherwise + * @returns {{ exists: boolean, valid: boolean, leasePath: string }} Lease status */ -export function checkLease(serviceName, resourceProvider, serviceGroup = '') { - try { - const repoRoot = process.env.TEST_REPO_ROOT || execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim(); - const leasePath = buildLeasePath(repoRoot, serviceName, resourceProvider, serviceGroup); +function getLeaseStatus(serviceName, resourceProvider, serviceGroup = '') { + const repoRoot = process.env.TEST_REPO_ROOT || execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim(); + const leasePath = buildLeasePath(repoRoot, serviceName, resourceProvider, serviceGroup); + try { if (!existsSync(leasePath)) { - return false; + return { exists: false, valid: false, leasePath }; } const content = readFileSync(leasePath, 'utf-8'); @@ -52,33 +52,38 @@ export function checkLease(serviceName, resourceProvider, serviceGroup = '') { const today = new Date(); today.setHours(0, 0, 0, 0); - return today <= endDate; + return { exists: true, valid: today <= endDate, leasePath }; } catch (error) { - return false; + return { exists: existsSync(leasePath), valid: false, leasePath }; } } +/** + * Check if ARM lease exists and is valid + * @param {string} serviceName - Service name + * @param {string} resourceProvider - Resource provider namespace + * @param {string} serviceGroup - Optional service group + * @returns {boolean} True if lease exists and is valid, false otherwise + */ +export function checkLease(serviceName, resourceProvider, serviceGroup = '') { + return getLeaseStatus(serviceName, resourceProvider, serviceGroup).valid; +} + function main() { const serviceName = process.argv[2]; const resourceProvider = process.argv[3]; const serviceGroup = process.argv[4] || ''; if (!serviceName || !resourceProvider) { - console.error('Usage: node detect-arm-leases.js [service-group]'); + console.error(`Missing arguments: serviceName="${serviceName}", resourceProviderName="${resourceProvider}"`); process.exit(1); } - const result = checkLease(serviceName, resourceProvider, serviceGroup); + const status = getLeaseStatus(serviceName, resourceProvider, serviceGroup); - if (!result) { - const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim(); - const leasePath = buildLeasePath(repoRoot, serviceName, resourceProvider, serviceGroup); - - if (!existsSync(leasePath)) { - console.log('No lease file found'); - } else { - console.error('Lease has expired'); - } + if (!status.valid) { + const message = status.exists ? 'Lease has expired' : 'No lease file found'; + console.log(message); process.exit(1); } diff --git a/.github/workflows/src/detect-new-resource-provider.js b/.github/workflows/src/detect-new-resource-provider.js index 76c774572f9b..972f8f3ade11 100644 --- a/.github/workflows/src/detect-new-resource-provider.js +++ b/.github/workflows/src/detect-new-resource-provider.js @@ -4,7 +4,7 @@ import { execSync } from 'child_process'; import { readdirSync, existsSync, statSync, writeFileSync } from 'fs'; import { join, dirname } from 'path'; import { simpleGit } from 'simple-git'; -import { getChangedFiles, swagger, example } from '../../shared/src/changed-files.js'; +import { getChangedFiles, swagger, example, resourceManager } from '../../shared/src/changed-files.js'; import { checkLease } from './detect-arm-leases.js'; // ============================================ @@ -18,8 +18,27 @@ const RESOURCE_MANAGER_PATTERN = /^specification\/[^\/]+\/resource-manager\/([^\ // ServiceGroup folder name should not start with "stable" or "preview" const RESOURCE_MANAGER_WITH_GROUP_PATTERN = /^specification\/[^\/]+\/resource-manager\/([^\/]+)\/(?!stable|preview)([^\/]+)\//; -// Labels to add when new resource providers are detected -const NEW_RP_LABELS = ['ARMModelingReviewRequired', 'NotReadyForARMReview']; +/** + * New resource provider label names. + * @readonly + * @enum {string} + */ +const NewRpLabel = Object.freeze({ + ArmModelingReviewRequired: 'ARMModelingReviewRequired', + NotReadyForArmReview: 'NotReadyForARMReview' +}); + +// Label action values used by update-labels workflow +const LABEL_ACTIONS = Object.freeze({ + add: 'add', + remove: 'remove', + none: 'none' +}); + +const DEFAULT_LABEL_ACTIONS = Object.freeze({ + [NewRpLabel.ArmModelingReviewRequired]: LABEL_ACTIONS.none, + [NewRpLabel.NotReadyForArmReview]: LABEL_ACTIONS.none +}); // ============================================ // Utility Functions @@ -58,25 +77,25 @@ function resourceProviderExists(specPath, namespace) { return false; } -/** - * Add labels to the PR for new resource providers - * @param {Object} github - GitHub API client from github-script - * @param {Object} context - GitHub context from github-script - * @returns {Promise} - */ -async function addNewResourceProviderLabels(github, context) { - try { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - labels: NEW_RP_LABELS - }); - - console.log(`Successfully added labels: ${NEW_RP_LABELS.join(', ')}`); - } catch (error) { - console.log(`[FAILED] Failed to add labels: ${error.message}`); +function getLabelActions(hasNewResourceProviders) { + if (!hasNewResourceProviders) { + return { + [NewRpLabel.ArmModelingReviewRequired]: LABEL_ACTIONS.remove, + [NewRpLabel.NotReadyForArmReview]: LABEL_ACTIONS.remove + }; } + + return { + [NewRpLabel.ArmModelingReviewRequired]: LABEL_ACTIONS.add, + [NewRpLabel.NotReadyForArmReview]: LABEL_ACTIONS.add + }; +} + +function getContextOutput(context) { + const headSha = context?.payload?.pull_request?.head?.sha || ''; + const issueNumber = Number(context?.issue?.number || context?.payload?.pull_request?.number || 0); + + return { headSha, issueNumber }; } /** @@ -140,13 +159,15 @@ export default async function detectNewResourceProvider({ github, context, core, headCommitish: 'HEAD' }); - const rmFiles = allFiles.filter(file => - file.includes('/resource-manager/') && file.startsWith('specification/') - ); + const rmFiles = allFiles.filter((file) => resourceManager(file) && file.startsWith('specification/')); if (rmFiles.length === 0) { console.log('No resource-manager files changed'); - return 'no-changes'; + return { + status: 'no-changes', + labelActions: DEFAULT_LABEL_ACTIONS, + ...getContextOutput(context) + }; } // Extract resource providers from changed files @@ -174,7 +195,7 @@ export default async function detectNewResourceProvider({ github, context, core, const specRmSwaggerFilesBaseBranch = specFilesBaseBranch .split('\n') - .filter((file) => file.includes('/resource-manager/') && swagger(file)); + .filter((file) => resourceManager(file) && swagger(file)); if (specRmSwaggerFilesBaseBranch.length === 0) { hasAtLeastOneBrandNewRP = true; @@ -188,7 +209,11 @@ export default async function detectNewResourceProvider({ github, context, core, if (!hasAtLeastOneBrandNewRP) { console.log('No brand new resource providers detected, spec directories exist in base branch.'); console.log('Skipping workflow.\n'); - return 'no-new-rp'; + return { + status: 'no-new-rp', + labelActions: getLabelActions(false), + ...getContextOutput(context) + }; } } @@ -210,7 +235,7 @@ export default async function detectNewResourceProvider({ github, context, core, const leaseCheckResults = []; if (newResourceProviders.length > 0) { - console.log(`\nšŸ†• Detected ${newResourceProviders.length} new resource provider(s)\n`); + console.log(`\nDetected ${newResourceProviders.length} new resource provider(s)\n`); for (const rp of newResourceProviders) { const leaseValid = checkLease(rp.serviceName, rp.namespace, rp.serviceGroup || ''); @@ -223,7 +248,8 @@ export default async function detectNewResourceProvider({ github, context, core, leaseMessage: leaseValid ? 'Lease is valid' : 'No lease file found or lease has expired' }); - console.log(` - ${rp.namespace}: ${leaseValid ? '[OK]' : '[FAILED]'} Lease ${leaseValid ? 'valid' : 'invalid'}`); + const leaseStatus = leaseValid ? 'Lease valid' : 'Lease invalid'; + console.log(` - ${rp.namespace}: ${leaseStatus}`); } writeFileSync( @@ -231,8 +257,6 @@ export default async function detectNewResourceProvider({ github, context, core, JSON.stringify({ newResourceProviders: leaseCheckResults }, null, 2) ); - await addNewResourceProviderLabels(github, context); - // Create PR comment with detected resource providers const rpList = leaseCheckResults .map(rp => `- \`${rp.namespace}\``) @@ -262,10 +286,18 @@ export default async function detectNewResourceProvider({ github, context, core, // Set the action as failed to indicate new RPs were detected core.setFailed('New resource providers detected'); - return 'new-rp-detected'; + return { + status: 'new-rp-detected', + labelActions: getLabelActions(true), + ...getContextOutput(context) + }; } else { console.log('No new resource providers detected.\n'); - return 'no-new-rp'; + return { + status: 'no-new-rp', + labelActions: getLabelActions(false), + ...getContextOutput(context) + }; } } diff --git a/.github/workflows/test/detect-new-resource-provider.test.js b/.github/workflows/test/detect-new-resource-provider.test.js index 291095afff27..872f4877e2a1 100644 --- a/.github/workflows/test/detect-new-resource-provider.test.js +++ b/.github/workflows/test/detect-new-resource-provider.test.js @@ -10,7 +10,6 @@ describe('detect-new-resource-provider', () => { mockGithub = { rest: { issues: { - addLabels: vi.fn().mockResolvedValue({}), createComment: vi.fn().mockResolvedValue({}) } } @@ -41,9 +40,8 @@ describe('detect-new-resource-provider', () => { baseBranch: 'main' }); - expect(result).toBe('no-changes'); + expect(result.status).toBe('no-changes'); expect(mockCore.setFailed).not.toHaveBeenCalled(); - expect(mockGithub.rest.issues.addLabels).not.toHaveBeenCalled(); expect(mockGithub.rest.issues.createComment).not.toHaveBeenCalled(); }); }); @@ -57,7 +55,7 @@ describe('detect-new-resource-provider', () => { baseBranch: 'main' }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); it('should work with different baseBranch values', async () => { @@ -68,7 +66,7 @@ describe('detect-new-resource-provider', () => { baseBranch: 'main' }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); }); @@ -81,7 +79,6 @@ describe('detect-new-resource-provider', () => { baseBranch: 'main' }); - expect(mockGithub.rest.issues.addLabels).not.toHaveBeenCalled(); expect(mockGithub.rest.issues.createComment).not.toHaveBeenCalled(); }); @@ -89,7 +86,6 @@ describe('detect-new-resource-provider', () => { const errorGithub = { rest: { issues: { - addLabels: vi.fn().mockRejectedValue(new Error('API Error')), createComment: vi.fn().mockRejectedValue(new Error('API Error')) } } @@ -102,7 +98,7 @@ describe('detect-new-resource-provider', () => { baseBranch: 'main' }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); }); @@ -115,7 +111,7 @@ describe('detect-new-resource-provider', () => { baseBranch: 'main' }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); }); @@ -133,7 +129,7 @@ describe('detect-new-resource-provider', () => { baseBranch: 'main' }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); }); @@ -151,7 +147,7 @@ describe('detect-new-resource-provider', () => { baseBranch: 'main' }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); it('should handle very large issue numbers', async () => { @@ -167,7 +163,7 @@ describe('detect-new-resource-provider', () => { baseBranch: 'main' }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); it('should handle empty string repo owner', async () => { @@ -183,7 +179,7 @@ describe('detect-new-resource-provider', () => { baseBranch: 'main' }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); it('should handle empty string repo name', async () => { @@ -199,15 +195,14 @@ describe('detect-new-resource-provider', () => { baseBranch: 'main' }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); - it('should handle addLabels throwing network timeout', async () => { + it('should handle createComment throwing network timeout', async () => { const timeoutGithub = { rest: { issues: { - addLabels: vi.fn().mockRejectedValue(new Error('Network timeout')), - createComment: vi.fn().mockResolvedValue({}) + createComment: vi.fn().mockRejectedValue(new Error('Network timeout')) } } }; @@ -219,14 +214,13 @@ describe('detect-new-resource-provider', () => { baseBranch: 'main' }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); it('should handle createComment throwing permission error', async () => { const permissionGithub = { rest: { issues: { - addLabels: vi.fn().mockResolvedValue({}), createComment: vi.fn().mockRejectedValue(new Error('Permission denied')) } } @@ -239,14 +233,13 @@ describe('detect-new-resource-provider', () => { baseBranch: 'main' }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); - it('should handle both GitHub API methods failing', async () => { + it('should handle GitHub API methods failing', async () => { const failingGithub = { rest: { issues: { - addLabels: vi.fn().mockRejectedValue(new Error('Service unavailable')), createComment: vi.fn().mockRejectedValue(new Error('Service unavailable')) } } @@ -259,7 +252,7 @@ describe('detect-new-resource-provider', () => { baseBranch: 'main' }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); it('should handle core.setFailed as undefined', async () => { @@ -272,7 +265,7 @@ describe('detect-new-resource-provider', () => { baseBranch: 'main' }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); it('should handle multiple concurrent calls', async () => { @@ -288,7 +281,7 @@ describe('detect-new-resource-provider', () => { const results = await Promise.all(calls); results.forEach(result => { - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); }); @@ -307,14 +300,13 @@ describe('detect-new-resource-provider', () => { baseBranch: 'main' }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); it('should not throw when GitHub methods return null', async () => { const nullGithub = { rest: { issues: { - addLabels: vi.fn().mockResolvedValue(null), createComment: vi.fn().mockResolvedValue(null) } } @@ -327,7 +319,7 @@ describe('detect-new-resource-provider', () => { baseBranch: 'main' }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); }); }); From 5a4d5abc5125d0cbe28c16314416b99f50439880 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 11 Feb 2026 19:26:33 -0600 Subject: [PATCH 022/125] Itr 1 : addressing the comments --- .github/workflows/src/detect-arm-leases.js | 120 +++++------ .../src/detect-new-resource-provider.js | 189 +++++++++--------- .../workflows/test/detect-arm-leases.test.js | 44 ++-- .../test/detect-new-resource-provider.test.js | 116 +++++------ 4 files changed, 236 insertions(+), 233 deletions(-) diff --git a/.github/workflows/src/detect-arm-leases.js b/.github/workflows/src/detect-arm-leases.js index c4e13ac15425..175ff263c2ee 100644 --- a/.github/workflows/src/detect-arm-leases.js +++ b/.github/workflows/src/detect-arm-leases.js @@ -1,14 +1,37 @@ -import { existsSync, readFileSync } from 'fs'; -import { join } from 'path'; -import { execSync } from 'child_process'; -import YAML from 'js-yaml'; +import { readFile } from 'fs/promises'; +import { resolve } from 'path'; +import yaml from 'js-yaml'; +import * as z from 'zod'; +import { getRootFolder } from '../../shared/src/simple-git.js'; /** - * Build the lease path based on service information + * Schema for lease.yaml file + * + * Example: + * ```yaml + * lease: + * startdate: "2024-01-01" + * duration: "30 days" + * ``` + */ +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().regex(/^\d+\s*days?$/i, 'duration must be in format "N days"'), + }), +}); + +/** + * Build the lease path based on service information. + * + * Lease files are stored at: + * - Without service group: `.github/arm-leases///lease.yaml` + * - With service group: `.github/arm-leases////lease.yaml` + * * @param {string} repoRoot - Repository root path - * @param {string} serviceName - Service name - * @param {string} resourceProvider - Resource provider namespace - * @param {string} serviceGroup - Optional service group + * @param {string} serviceName - Service name (e.g., "compute") + * @param {string} resourceProvider - Resource provider namespace (e.g., "Microsoft.Compute") + * @param {string} serviceGroup - Optional service group for RPs that have sub-groupings (e.g., "ComputeRP") * @returns {string} Full path to lease.yaml file */ function buildLeasePath(repoRoot, serviceName, resourceProvider, serviceGroup = '') { @@ -17,7 +40,7 @@ function buildLeasePath(repoRoot, serviceName, resourceProvider, serviceGroup = leasePathParts.push(serviceGroup); } leasePathParts.push('lease.yaml'); - return join(...leasePathParts); + return resolve(...leasePathParts); } /** @@ -25,73 +48,54 @@ function buildLeasePath(repoRoot, serviceName, resourceProvider, serviceGroup = * @param {string} serviceName - Service name * @param {string} resourceProvider - Resource provider namespace * @param {string} serviceGroup - Optional service group - * @returns {{ exists: boolean, valid: boolean, leasePath: string }} Lease status + * @returns {Promise<{ exists: boolean, valid: boolean, leasePath: string }>} Lease status */ -function getLeaseStatus(serviceName, resourceProvider, serviceGroup = '') { - const repoRoot = process.env.TEST_REPO_ROOT || execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim(); +export async function getLeaseStatus(serviceName, resourceProvider, serviceGroup = '') { + const repoRoot = process.env.TEST_REPO_ROOT || await getRootFolder(process.cwd()); const leasePath = buildLeasePath(repoRoot, serviceName, resourceProvider, serviceGroup); + let content; try { - if (!existsSync(leasePath)) { - return { exists: false, valid: false, leasePath }; - } + content = await readFile(leasePath, 'utf-8'); + } catch { + return { exists: false, valid: false, leasePath }; + } - const content = readFileSync(leasePath, 'utf-8'); - const parsed = YAML.load(content); - const lease = parsed.lease; + try { + const rawParsed = /** @type {any} */ (yaml.load(content, { schema: yaml.FAILSAFE_SCHEMA })); - let startdate = lease.startdate; - if (startdate instanceof Date) { - startdate = startdate.toISOString().split('T')[0]; + if (!rawParsed) { + return { exists: true, valid: false, leasePath }; } + const parsed = leaseSchema.parse(rawParsed); + const lease = parsed.lease; + const durationDays = parseInt(lease.duration.match(/^(\d+)\s*days?$/i)[1], 10); - const endDate = new Date(startdate); + const endDate = new Date(lease.startdate); endDate.setDate(endDate.getDate() + durationDays); const today = new Date(); today.setHours(0, 0, 0, 0); return { exists: true, valid: today <= endDate, leasePath }; - } catch (error) { - return { exists: existsSync(leasePath), valid: false, leasePath }; + } catch { + // File exists but content is invalid + return { exists: true, valid: false, leasePath }; } } /** - * Check if ARM lease exists and is valid - * @param {string} serviceName - Service name - * @param {string} resourceProvider - Resource provider namespace - * @param {string} serviceGroup - Optional service group - * @returns {boolean} True if lease exists and is valid, false otherwise + * Check if ARM lease exists and is valid. + * + * Looks for a lease file at the appropriate path (see buildLeasePath for path structure). + * + * @param {string} serviceName - Service name (e.g., "compute") + * @param {string} resourceProvider - Resource provider namespace (e.g., "Microsoft.Compute") + * @param {string} serviceGroup - Optional service group for RPs with sub-groupings + * @returns {Promise} True if lease exists and is valid, false otherwise */ -export function checkLease(serviceName, resourceProvider, serviceGroup = '') { - return getLeaseStatus(serviceName, resourceProvider, serviceGroup).valid; -} - -function main() { - const serviceName = process.argv[2]; - const resourceProvider = process.argv[3]; - const serviceGroup = process.argv[4] || ''; - - if (!serviceName || !resourceProvider) { - console.error(`Missing arguments: serviceName="${serviceName}", resourceProviderName="${resourceProvider}"`); - process.exit(1); - } - - const status = getLeaseStatus(serviceName, resourceProvider, serviceGroup); - - if (!status.valid) { - const message = status.exists ? 'Lease has expired' : 'No lease file found'; - console.log(message); - process.exit(1); - } - - console.log('Lease is valid'); - process.exit(0); -} - -// Only run main if this file is executed directly (not imported) -if (process.argv[1] && process.argv[1].endsWith('detect-arm-leases.js')) { - main(); +export async function checkLease(serviceName, resourceProvider, serviceGroup = '') { + const status = await getLeaseStatus(serviceName, resourceProvider, serviceGroup); + return status.valid; } diff --git a/.github/workflows/src/detect-new-resource-provider.js b/.github/workflows/src/detect-new-resource-provider.js index 972f8f3ade11..0cbd2cf10440 100644 --- a/.github/workflows/src/detect-new-resource-provider.js +++ b/.github/workflows/src/detect-new-resource-provider.js @@ -1,19 +1,19 @@ -#!/usr/bin/env node - -import { execSync } from 'child_process'; -import { readdirSync, existsSync, statSync, writeFileSync } from 'fs'; -import { join, dirname } from 'path'; +import { writeFileSync } from 'fs'; +import { join } from 'path'; import { simpleGit } from 'simple-git'; -import { getChangedFiles, swagger, example, resourceManager } from '../../shared/src/changed-files.js'; +import { getChangedFiles, swagger, resourceManager } from '../../shared/src/changed-files.js'; +import { getRootFolder } from '../../shared/src/simple-git.js'; import { checkLease } from './detect-arm-leases.js'; +import { CoreLogger } from './core-logger.js'; +import { LabelAction } from './label.js'; // ============================================ // Configuration // ============================================ -const SPECIFICATION_PATH = 'specification'; // Match pattern: specification//resource-manager//... -const RESOURCE_MANAGER_PATTERN = /^specification\/[^\/]+\/resource-manager\/([^\/]+)/; +// Trailing slash ensures the match is a directory component, not a file like readme.md +const RESOURCE_MANAGER_PATTERN = /^specification\/[^\/]+\/resource-manager\/([^\/]+)\//; // Match pattern with optional service group: specification//resource-manager///... // ServiceGroup folder name should not start with "stable" or "preview" const RESOURCE_MANAGER_WITH_GROUP_PATTERN = /^specification\/[^\/]+\/resource-manager\/([^\/]+)\/(?!stable|preview)([^\/]+)\//; @@ -24,20 +24,11 @@ const RESOURCE_MANAGER_WITH_GROUP_PATTERN = /^specification\/[^\/]+\/resource-ma * @enum {string} */ const NewRpLabel = Object.freeze({ - ArmModelingReviewRequired: 'ARMModelingReviewRequired', - NotReadyForArmReview: 'NotReadyForARMReview' -}); - -// Label action values used by update-labels workflow -const LABEL_ACTIONS = Object.freeze({ - add: 'add', - remove: 'remove', - none: 'none' + ArmModelingReviewRequired: 'ARMModelingReviewRequired' }); const DEFAULT_LABEL_ACTIONS = Object.freeze({ - [NewRpLabel.ArmModelingReviewRequired]: LABEL_ACTIONS.none, - [NewRpLabel.NotReadyForArmReview]: LABEL_ACTIONS.none + [NewRpLabel.ArmModelingReviewRequired]: LabelAction.None }); // ============================================ @@ -45,59 +36,48 @@ const DEFAULT_LABEL_ACTIONS = Object.freeze({ // ============================================ /** - * Check if a resource provider namespace exists in the specification directory - * @param {string} specPath - Path to specification directory + * 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 {Object} git - simple-git instance + * @param {string} commitish - Git commit reference (e.g., merge base SHA) * @param {string} namespace - Resource provider namespace (e.g., Microsoft.App) - * @returns {boolean} True if namespace exists + * @returns {Promise} True if namespace existed in the commit */ -function resourceProviderExists(specPath, namespace) { - if (!existsSync(specPath)) { - return false; - } - +async function resourceProviderExistsInCommit(git, commitish, namespace) { try { - const serviceDirs = readdirSync(specPath); - - for (const serviceDir of serviceDirs) { - const servicePath = join(specPath, serviceDir); - if (!statSync(servicePath).isDirectory()) continue; - - const rmPath = join(servicePath, 'resource-manager'); - if (!existsSync(rmPath)) continue; - - const namespacePath = join(rmPath, namespace); - if (existsSync(namespacePath) && statSync(namespacePath).isDirectory()) { - return true; - } - } - } catch (error) { - console.error('Error checking resource provider existence:', error.message); + // List all directories under specification/*/resource-manager/ in the base commit + const output = await git.raw([ + 'ls-tree', + '-d', + '--name-only', + '-r', + commitish, + 'specification/' + ]); + + // Look for resource-manager/ pattern in any service directory + const pattern = new RegExp(`^specification/[^/]+/resource-manager/${namespace.replace('.', '\\.')}$`, 'm'); + return pattern.test(output); + } catch { + // Error checking - assume RP doesn't exist if we can't verify + return false; } - - return false; } function getLabelActions(hasNewResourceProviders) { if (!hasNewResourceProviders) { return { - [NewRpLabel.ArmModelingReviewRequired]: LABEL_ACTIONS.remove, - [NewRpLabel.NotReadyForArmReview]: LABEL_ACTIONS.remove + [NewRpLabel.ArmModelingReviewRequired]: LabelAction.Remove }; } return { - [NewRpLabel.ArmModelingReviewRequired]: LABEL_ACTIONS.add, - [NewRpLabel.NotReadyForArmReview]: LABEL_ACTIONS.add + [NewRpLabel.ArmModelingReviewRequired]: LabelAction.Add }; } -function getContextOutput(context) { - const headSha = context?.payload?.pull_request?.head?.sha || ''; - const issueNumber = Number(context?.issue?.number || context?.payload?.pull_request?.number || 0); - - return { headSha, issueNumber }; -} - /** * Extract resource provider namespaces, service names, and optional service groups from file paths * @param {string[]} files - Array of file paths @@ -137,60 +117,78 @@ function extractResourceProviders(files) { * @param {Object} params.github - GitHub API client * @param {Object} params.context - GitHub context * @param {Object} params.core - GitHub Actions core - * @param {string} params.baseBranch - Base branch name * @returns {Promise} Result status */ -export default async function detectNewResourceProvider({ github, context, core, baseBranch }) { - const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim(); +export default async function detectNewResourceProvider({ github, context, core }) { + const repoRoot = await getRootFolder(process.cwd()); + const git = simpleGit(repoRoot); - console.log('Detecting New Resource Providers\n'); + core.info('Detecting New Resource Providers'); + + // Get base branch from context + const baseBranch = context.payload.pull_request?.base?.ref; + if (!baseBranch) { + throw new Error('Could not determine base branch from pull request context'); + } // Get the merge base for proper PR comparison let mergeBase; try { - mergeBase = execSync(`git merge-base origin/${baseBranch} HEAD`, { encoding: 'utf-8' }).trim(); - } catch (error) { + mergeBase = await git.raw(['merge-base', `origin/${baseBranch}`, 'HEAD']).then(s => s.trim()); + } catch { mergeBase = `origin/${baseBranch}`; } - // Get all changed files in specification/*/resource-manager/ - const allFiles = await getChangedFiles({ - baseCommitish: mergeBase, - headCommitish: 'HEAD' - }); + // Get all changed files in specification/ + const options = { + cwd: repoRoot, + paths: ['specification'], + logger: new CoreLogger(core), + }; + + const changedFiles = await getChangedFiles(options); - const rmFiles = allFiles.filter((file) => resourceManager(file) && file.startsWith('specification/')); + // Filter to resource-manager files + const rmFiles = changedFiles.filter(resourceManager); if (rmFiles.length === 0) { - console.log('No resource-manager files changed'); + core.info("No changes to files containing path '/resource-manager/'"); return { status: 'no-changes', - labelActions: DEFAULT_LABEL_ACTIONS, - ...getContextOutput(context) + labelActions: DEFAULT_LABEL_ACTIONS }; } // Extract resource providers from changed files const changedResourceProviders = extractResourceProviders(rmFiles); - // Pre-check: Verify if spec directories are brand new (don't exist in base branch) - const git = simpleGit(repoRoot); - const changedSpecDirs = new Set([ - ...rmFiles.filter(swagger).map((f) => dirname(dirname(dirname(f)))), - ...rmFiles.filter(example).map((f) => dirname(dirname(dirname(dirname(f))))), - ]); + // 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, match.index + match[0].length - 1); + } + return null; + }) + .filter(Boolean) + ); - if (changedSpecDirs.size > 0) { + if (changedNamespacePaths.size > 0) { let hasAtLeastOneBrandNewRP = false; - for (const changedSpecDir of changedSpecDirs) { + for (const namespacePath of changedNamespacePaths) { try { const specFilesBaseBranch = await git.raw([ 'ls-tree', '-r', '--name-only', mergeBase, - changedSpecDir, + namespacePath, ]); const specRmSwaggerFilesBaseBranch = specFilesBaseBranch @@ -200,29 +198,28 @@ export default async function detectNewResourceProvider({ github, context, core, if (specRmSwaggerFilesBaseBranch.length === 0) { hasAtLeastOneBrandNewRP = true; } - } catch (error) { + } catch { // Directory doesn't exist in base - brand new RP hasAtLeastOneBrandNewRP = true; } } if (!hasAtLeastOneBrandNewRP) { - console.log('No brand new resource providers detected, spec directories exist in base branch.'); - console.log('Skipping workflow.\n'); + core.info('No brand new resource providers detected, spec directories exist in base branch.'); + core.info('Skipping workflow.'); return { status: 'no-new-rp', - labelActions: getLabelActions(false), - ...getContextOutput(context) + labelActions: getLabelActions(false) }; } } // Extract resource providers and filter for new ones - const specPath = join(repoRoot, SPECIFICATION_PATH); const newResourceProviders = []; for (const [rp, info] of changedResourceProviders) { - if (!resourceProviderExists(specPath, rp)) { + const existsInBase = await resourceProviderExistsInCommit(git, mergeBase, rp); + if (!existsInBase) { newResourceProviders.push({ namespace: rp, serviceName: info.serviceName, @@ -235,10 +232,10 @@ export default async function detectNewResourceProvider({ github, context, core, const leaseCheckResults = []; if (newResourceProviders.length > 0) { - console.log(`\nDetected ${newResourceProviders.length} new resource provider(s)\n`); + core.info(`Detected ${newResourceProviders.length} new resource provider(s)`); for (const rp of newResourceProviders) { - const leaseValid = checkLease(rp.serviceName, rp.namespace, rp.serviceGroup || ''); + const leaseValid = await checkLease(rp.serviceName, rp.namespace, rp.serviceGroup || ''); leaseCheckResults.push({ namespace: rp.namespace, @@ -249,7 +246,7 @@ export default async function detectNewResourceProvider({ github, context, core, }); const leaseStatus = leaseValid ? 'Lease valid' : 'Lease invalid'; - console.log(` - ${rp.namespace}: ${leaseStatus}`); + core.info(` - ${rp.namespace}: ${leaseStatus}`); } writeFileSync( @@ -279,24 +276,22 @@ export default async function detectNewResourceProvider({ github, context, core, issue_number: context.issue.number, body: commentBody }); - console.log('PR comment created successfully'); + core.info('PR comment created successfully'); } catch (error) { - console.log(`[FAILED] Failed to create PR comment: ${error.message}`); + core.error(`Failed to create PR comment: ${error.message}`); } // Set the action as failed to indicate new RPs were detected core.setFailed('New resource providers detected'); return { status: 'new-rp-detected', - labelActions: getLabelActions(true), - ...getContextOutput(context) + labelActions: getLabelActions(true) }; } else { - console.log('No new resource providers detected.\n'); + core.info('No new resource providers detected.'); return { status: 'no-new-rp', - labelActions: getLabelActions(false), - ...getContextOutput(context) + labelActions: getLabelActions(false) }; } } diff --git a/.github/workflows/test/detect-arm-leases.test.js b/.github/workflows/test/detect-arm-leases.test.js index 08a413d4ed53..ed9527d514a7 100644 --- a/.github/workflows/test/detect-arm-leases.test.js +++ b/.github/workflows/test/detect-arm-leases.test.js @@ -37,12 +37,12 @@ describe('detect-arm-leases', () => { } describe('checkLease', () => { - it('returns false when lease file does not exist', () => { - const result = checkLease('testservice', 'Microsoft.Test'); + it('returns false when lease file does not exist', async () => { + const result = await checkLease('testservice', 'Microsoft.Test'); expect(result).toBe(false); }); - it('returns true when lease is valid and not expired', () => { + it('returns true when lease is valid and not expired', async () => { const today = new Date(); const startDate = new Date(today); startDate.setDate(today.getDate() - 30); @@ -54,11 +54,11 @@ describe('detect-arm-leases', () => { '90 days' ); - const result = checkLease('testservice', 'Microsoft.Test'); + const result = await checkLease('testservice', 'Microsoft.Test'); expect(result).toBe(true); }); - it('returns false when lease has expired', () => { + it('returns false when lease has expired', async () => { const today = new Date(); const startDate = new Date(today); startDate.setDate(today.getDate() - 100); @@ -70,11 +70,11 @@ describe('detect-arm-leases', () => { '90 days' ); - const result = checkLease('testservice', 'Microsoft.Test'); + const result = await checkLease('testservice', 'Microsoft.Test'); expect(result).toBe(false); }); - it('returns true on the last day of lease', () => { + it('returns true on the last day of lease', async () => { const today = new Date(); const startDate = new Date(today); startDate.setDate(today.getDate() - 89); @@ -86,11 +86,11 @@ describe('detect-arm-leases', () => { '90 days' ); - const result = checkLease('testservice', 'Microsoft.Test'); + const result = await checkLease('testservice', 'Microsoft.Test'); expect(result).toBe(true); }); - it('returns false one day after lease expires', () => { + it('returns false one day after lease expires', async () => { const today = new Date(); const startDate = new Date(today); startDate.setDate(today.getDate() - 91); @@ -102,11 +102,11 @@ describe('detect-arm-leases', () => { '90 days' ); - const result = checkLease('testservice', 'Microsoft.Test'); + const result = await checkLease('testservice', 'Microsoft.Test'); expect(result).toBe(false); }); - it('handles different duration formats', () => { + it('handles different duration formats', async () => { const today = new Date(); const startDate = new Date(today); startDate.setDate(today.getDate() - 10); @@ -118,11 +118,11 @@ describe('detect-arm-leases', () => { '180 Days' ); - const result = checkLease('testservice', 'Microsoft.Test'); + const result = await checkLease('testservice', 'Microsoft.Test'); expect(result).toBe(true); }); - it('handles single day duration', () => { + it('handles single day duration', async () => { const today = new Date(); createLeaseFile( @@ -132,20 +132,20 @@ describe('detect-arm-leases', () => { '1 day' ); - const result = checkLease('testservice', 'Microsoft.Test'); + const result = await checkLease('testservice', 'Microsoft.Test'); expect(result).toBe(true); }); - it('returns false for invalid lease file format', () => { + it('returns false for invalid lease file format', async () => { const leaseDir = join(ARM_LEASES_DIR, 'testservice', 'Microsoft.Test'); mkdirSync(leaseDir, { recursive: true }); writeFileSync(join(leaseDir, 'lease.yaml'), 'invalid: yaml: content'); - const result = checkLease('testservice', 'Microsoft.Test'); + const result = await checkLease('testservice', 'Microsoft.Test'); expect(result).toBe(false); }); - it('handles multiple services and namespaces', () => { + it('handles multiple services and namespaces', async () => { const today = new Date(); const startDate = new Date(today); startDate.setDate(today.getDate() - 30); @@ -153,12 +153,12 @@ describe('detect-arm-leases', () => { createLeaseFile('app', 'Microsoft.App', startDate.toISOString().split('T')[0], '90 days'); createLeaseFile('compute', 'Microsoft.Compute', startDate.toISOString().split('T')[0], '90 days'); - expect(checkLease('app', 'Microsoft.App')).toBe(true); - expect(checkLease('compute', 'Microsoft.Compute')).toBe(true); - expect(checkLease('storage', 'Microsoft.Storage')).toBe(false); + expect(await checkLease('app', 'Microsoft.App')).toBe(true); + expect(await checkLease('compute', 'Microsoft.Compute')).toBe(true); + expect(await checkLease('storage', 'Microsoft.Storage')).toBe(false); }); - it('handles future start dates', () => { + it('handles future start dates', async () => { const today = new Date(); const futureDate = new Date(today); futureDate.setDate(today.getDate() + 10); @@ -170,7 +170,7 @@ describe('detect-arm-leases', () => { '90 days' ); - const result = checkLease('testservice', 'Microsoft.Test'); + const result = await checkLease('testservice', 'Microsoft.Test'); expect(result).toBe(true); }); }); diff --git a/.github/workflows/test/detect-new-resource-provider.test.js b/.github/workflows/test/detect-new-resource-provider.test.js index 872f4877e2a1..6debc56e3ed6 100644 --- a/.github/workflows/test/detect-new-resource-provider.test.js +++ b/.github/workflows/test/detect-new-resource-provider.test.js @@ -17,11 +17,19 @@ describe('detect-new-resource-provider', () => { mockContext = { repo: { owner: 'Azure', repo: 'azure-rest-api-specs' }, - issue: { number: 123 } + issue: { number: 123 }, + payload: { + pull_request: { + base: { ref: 'main' }, + head: { sha: 'abc123' } + } + } }; mockCore = { - setFailed: vi.fn() + setFailed: vi.fn(), + info: vi.fn(), + error: vi.fn() }; }); @@ -31,18 +39,16 @@ describe('detect-new-resource-provider', () => { }); }); - describe('no changes scenario', () => { - it('should return "no-changes" when no resource-manager files are changed', async () => { + describe('basic behavior', () => { + it('should return a valid status', async () => { const result = await detectNewResourceProvider({ github: mockGithub, context: mockContext, - core: mockCore, - baseBranch: 'main' + core: mockCore }); - expect(result.status).toBe('no-changes'); - expect(mockCore.setFailed).not.toHaveBeenCalled(); - expect(mockGithub.rest.issues.createComment).not.toHaveBeenCalled(); + // Result depends on actual workspace state + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); }); @@ -51,19 +57,22 @@ describe('detect-new-resource-provider', () => { const result = await detectNewResourceProvider({ github: mockGithub, context: mockContext, - core: mockCore, - baseBranch: 'main' + core: mockCore }); expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); - it('should work with different baseBranch values', async () => { + it('should work with different baseBranch values from context', async () => { + const rpsaasContext = { + ...mockContext, + payload: { pull_request: { base: { ref: 'RPSaaSMaster' }, head: { sha: 'abc123' } } } + }; + const result = await detectNewResourceProvider({ github: mockGithub, - context: mockContext, - core: mockCore, - baseBranch: 'main' + context: rpsaasContext, + core: mockCore }); expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); @@ -71,15 +80,15 @@ describe('detect-new-resource-provider', () => { }); describe('github API interactions', () => { - it('should not call GitHub API when no changes detected', async () => { - await detectNewResourceProvider({ + it('should complete without throwing', async () => { + // Test runs against real workspace - result depends on actual state + const result = await detectNewResourceProvider({ github: mockGithub, context: mockContext, - core: mockCore, - baseBranch: 'main' + core: mockCore }); - expect(mockGithub.rest.issues.createComment).not.toHaveBeenCalled(); + expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); it('should handle GitHub API errors gracefully', async () => { @@ -94,8 +103,7 @@ describe('detect-new-resource-provider', () => { const result = await detectNewResourceProvider({ github: errorGithub, context: mockContext, - core: mockCore, - baseBranch: 'main' + core: mockCore }); expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); @@ -107,8 +115,7 @@ describe('detect-new-resource-provider', () => { const result = await detectNewResourceProvider({ github: mockGithub, context: mockContext, - core: mockCore, - baseBranch: 'main' + core: mockCore }); expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); @@ -119,14 +126,14 @@ describe('detect-new-resource-provider', () => { it('should handle minimal context object', async () => { const minimalContext = { repo: { owner: 'test-owner', repo: 'test-repo' }, - issue: { number: 1 } + issue: { number: 1 }, + payload: { pull_request: { base: { ref: 'main' }, head: { sha: 'abc123' } } } }; const result = await detectNewResourceProvider({ github: mockGithub, context: minimalContext, - core: mockCore, - baseBranch: 'main' + core: mockCore }); expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); @@ -137,14 +144,14 @@ describe('detect-new-resource-provider', () => { it('should handle issue number as zero', async () => { const zeroIssueContext = { repo: { owner: 'Azure', repo: 'azure-rest-api-specs' }, - issue: { number: 0 } + issue: { number: 0 }, + payload: { pull_request: { base: { ref: 'main' }, head: { sha: 'abc123' } } } }; const result = await detectNewResourceProvider({ github: mockGithub, context: zeroIssueContext, - core: mockCore, - baseBranch: 'main' + core: mockCore }); expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); @@ -153,14 +160,14 @@ describe('detect-new-resource-provider', () => { it('should handle very large issue numbers', async () => { const largeIssueContext = { repo: { owner: 'Azure', repo: 'azure-rest-api-specs' }, - issue: { number: 999999999 } + issue: { number: 999999999 }, + payload: { pull_request: { base: { ref: 'main' }, head: { sha: 'abc123' } } } }; const result = await detectNewResourceProvider({ github: mockGithub, context: largeIssueContext, - core: mockCore, - baseBranch: 'main' + core: mockCore }); expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); @@ -169,14 +176,14 @@ describe('detect-new-resource-provider', () => { it('should handle empty string repo owner', async () => { const emptyOwnerContext = { repo: { owner: '', repo: 'azure-rest-api-specs' }, - issue: { number: 123 } + issue: { number: 123 }, + payload: { pull_request: { base: { ref: 'main' }, head: { sha: 'abc123' } } } }; const result = await detectNewResourceProvider({ github: mockGithub, context: emptyOwnerContext, - core: mockCore, - baseBranch: 'main' + core: mockCore }); expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); @@ -185,14 +192,14 @@ describe('detect-new-resource-provider', () => { it('should handle empty string repo name', async () => { const emptyRepoContext = { repo: { owner: 'Azure', repo: '' }, - issue: { number: 123 } + issue: { number: 123 }, + payload: { pull_request: { base: { ref: 'main' }, head: { sha: 'abc123' } } } }; const result = await detectNewResourceProvider({ github: mockGithub, context: emptyRepoContext, - core: mockCore, - baseBranch: 'main' + core: mockCore }); expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); @@ -210,8 +217,7 @@ describe('detect-new-resource-provider', () => { const result = await detectNewResourceProvider({ github: timeoutGithub, context: mockContext, - core: mockCore, - baseBranch: 'main' + core: mockCore }); expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); @@ -229,8 +235,7 @@ describe('detect-new-resource-provider', () => { const result = await detectNewResourceProvider({ github: permissionGithub, context: mockContext, - core: mockCore, - baseBranch: 'main' + core: mockCore }); expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); @@ -248,21 +253,23 @@ describe('detect-new-resource-provider', () => { const result = await detectNewResourceProvider({ github: failingGithub, context: mockContext, - core: mockCore, - baseBranch: 'main' + core: mockCore }); expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); }); - it('should handle core.setFailed as undefined', async () => { - const noCoreSetFailed = {}; + it('should handle core.setFailed as noop', async () => { + const noopCore = { + info: vi.fn(), + error: vi.fn(), + setFailed: vi.fn() // noop setFailed + }; const result = await detectNewResourceProvider({ github: mockGithub, context: mockContext, - core: noCoreSetFailed, - baseBranch: 'main' + core: noopCore }); expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); @@ -273,8 +280,7 @@ describe('detect-new-resource-provider', () => { detectNewResourceProvider({ github: mockGithub, context: mockContext, - core: mockCore, - baseBranch: 'main' + core: mockCore }) ); @@ -289,15 +295,14 @@ describe('detect-new-resource-provider', () => { const extendedContext = { repo: { owner: 'Azure', repo: 'azure-rest-api-specs', extra: 'data' }, issue: { number: 123, title: 'Test PR', labels: [] }, - payload: { action: 'opened' }, + payload: { action: 'opened', pull_request: { base: { ref: 'main' }, head: { sha: 'abc123' } } }, unexpected: 'property' }; const result = await detectNewResourceProvider({ github: mockGithub, context: extendedContext, - core: mockCore, - baseBranch: 'main' + core: mockCore }); expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); @@ -315,8 +320,7 @@ describe('detect-new-resource-provider', () => { const result = await detectNewResourceProvider({ github: nullGithub, context: mockContext, - core: mockCore, - baseBranch: 'main' + core: mockCore }); expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); From c86658f431887e7051c55d466110aa9c50d4f145 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 11 Feb 2026 19:48:29 -0600 Subject: [PATCH 023/125] Itr 1: missed to checkin yaml --- .../detect-new-resource-provider.yaml | 44 ++++--------------- 1 file changed, 9 insertions(+), 35 deletions(-) diff --git a/.github/workflows/detect-new-resource-provider.yaml b/.github/workflows/detect-new-resource-provider.yaml index 3631f587e1fa..8cba4e6bffe9 100644 --- a/.github/workflows/detect-new-resource-provider.yaml +++ b/.github/workflows/detect-new-resource-provider.yaml @@ -2,10 +2,16 @@ name: Detect New Resource Provider 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 paths: - "specification/**/resource-manager/**" @@ -13,14 +19,12 @@ permissions: contents: read pull-requests: write -env: - ENABLE_NEW_RP_DETECTION: false - jobs: detect-new-rp: name: Detect New Resource Provider runs-on: ubuntu-24.04 - if: env.ENABLE_NEW_RP_DETECTION == 'true' + # TODO: Set to true to enable new RP detection + if: false steps: - name: Checkout repository @@ -40,12 +44,7 @@ jobs: script: | const { default: detectNewResourceProvider } = await import('${{ github.workspace }}/.github/workflows/src/detect-new-resource-provider.js'); - return await detectNewResourceProvider({ - github, - context, - core, - baseBranch: '${{ github.event.pull_request.base.ref }}' - }); + return await detectNewResourceProvider({ github, context, core }); - if: fromJson(steps.detect.outputs.result).labelActions['ARMModelingReviewRequired'] != 'none' name: Upload artifact for ARMModelingReviewRequired label @@ -53,28 +52,3 @@ jobs: with: name: "ARMModelingReviewRequired" value: "${{ fromJson(steps.detect.outputs.result).labelActions['ARMModelingReviewRequired'] == 'add' }}" - - - if: fromJson(steps.detect.outputs.result).labelActions['NotReadyForARMReview'] != 'none' - name: Upload artifact for NotReadyForARMReview label - uses: ./.github/actions/add-label-artifact - with: - name: "NotReadyForARMReview" - value: "${{ fromJson(steps.detect.outputs.result).labelActions['NotReadyForARMReview'] == 'add' }}" - - - if: | - always() && - fromJson(steps.detect.outputs.result).headSha - name: Upload artifact with head SHA - uses: ./.github/actions/add-empty-artifact - with: - name: "head-sha" - value: "${{ fromJson(steps.detect.outputs.result).headSha }}" - - - if: | - always() && - fromJson(steps.detect.outputs.result).issueNumber > 0 - name: Upload artifact with issue number - uses: ./.github/actions/add-empty-artifact - with: - name: "issue-number" - value: "${{ fromJson(steps.detect.outputs.result).issueNumber }}" From 59cb31541c62cd194b3d3a6640676d58b82f567f Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 11 Feb 2026 19:59:33 -0600 Subject: [PATCH 024/125] Itr 1 : checkin labelling js file --- .../src/summarize-checks/labelling.js | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/src/summarize-checks/labelling.js b/.github/workflows/src/summarize-checks/labelling.js index ed0646ce8fa8..4263c0379a21 100644 --- a/.github/workflows/src/summarize-checks/labelling.js +++ b/.github/workflows/src/summarize-checks/labelling.js @@ -651,7 +651,10 @@ function processARMReviewWorkflowLabels( ciRpaasRPNotInPrivateRepoLabelShouldBePresent, ); - const blocked = blockedOnRpaas || blockedOnVersioningPolicy; + // Block if ARMModelingReviewRequired is present (new RP namespace detected) + const blockedOnArmModeling = new Label("ARMModelingReviewRequired", labelContext.present); + + 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 +710,8 @@ function processARMReviewWorkflowLabels( console.log( `RETURN definition processARMReviewWorkflowLabels. ` + `presentLabels: ${[...labelContext.present].join(",")}, ` + - `blockedOnRpaas: ${blockedOnRpaas}. ` + + `blockedOnRpaas: ${blockedOnRpaas}, ` + + `blockedOnArmModeling: ${blockedOnArmModeling}. ` + `exactlyOneArmReviewWorkflowLabelShouldBePresent: ${exactlyOneArmReviewWorkflowLabelShouldBePresent}. `, ); } @@ -885,6 +889,18 @@ const rulesPri0NotReadyForArmReview = [ anyRequiredLabels: [brChRevApproval], troubleshootingGuide: notReadyForArmReviewReason(brChRev), }, + { + precedence: 0, + allPrerequisiteLabels: ["NotReadyForARMReview", "ARMModelingReviewRequired"], + anyRequiredLabels: [], + troubleshootingGuide: wrapInArmReviewMessage( + "This PR has ARMModelingReviewRequired label. " + + "This means it is introducing a new Resource Provider namespace. " + + "New RPs require a discussion with the ARM Modelling 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[]} */ From 8d966ac88d5340d8bce8cc049c4c3947df79f26b Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Thu, 12 Feb 2026 15:12:47 -0600 Subject: [PATCH 025/125] Itr 1 : addressed copilots comments and modified name of workflow --- .github/workflows/arm-lease-validation.yaml | 53 ++ .../detect-new-resource-provider.yaml | 3 +- .../src/detect-new-resource-provider.js | 57 +- .../src/summarize-checks/labelling.js | 3 +- .../test/detect-new-resource-provider.test.js | 526 +++++++++--------- 5 files changed, 355 insertions(+), 287 deletions(-) create mode 100644 .github/workflows/arm-lease-validation.yaml diff --git a/.github/workflows/arm-lease-validation.yaml b/.github/workflows/arm-lease-validation.yaml new file mode 100644 index 000000000000..c0d32bdced42 --- /dev/null +++ b/.github/workflows/arm-lease-validation.yaml @@ -0,0 +1,53 @@ +name: ARM Lease Validation + +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 + paths: + - "specification/**/resource-manager/**" + +permissions: + contents: read + +jobs: + detect-new-rp: + name: Detect New Resource Provider + runs-on: ubuntu-24.04 + # TODO: Set to true to enable new RP detection + if: false + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 2 + sparse-checkout: | + .github + + - name: Install dependencies for github-script actions + uses: ./.github/actions/install-deps-github-script + + - name: Detect new resource providers + id: detect + uses: actions/github-script@v8 + with: + script: | + const { default: detectNewResourceProvider } = + await import('${{ github.workspace }}/.github/workflows/src/detect-new-resource-provider.js'); + return await detectNewResourceProvider({ context, core }); + + - if: fromJson(steps.detect.outputs.result).labelActions['ARMModelingReviewRequired'] != 'none' + name: Upload artifact for ARMModelingReviewRequired label + uses: ./.github/actions/add-label-artifact + with: + name: "ARMModelingReviewRequired" + value: "${{ fromJson(steps.detect.outputs.result).labelActions['ARMModelingReviewRequired'] == 'add' }}" diff --git a/.github/workflows/detect-new-resource-provider.yaml b/.github/workflows/detect-new-resource-provider.yaml index 8cba4e6bffe9..78b43b425b9f 100644 --- a/.github/workflows/detect-new-resource-provider.yaml +++ b/.github/workflows/detect-new-resource-provider.yaml @@ -17,7 +17,6 @@ on: permissions: contents: read - pull-requests: write jobs: detect-new-rp: @@ -44,7 +43,7 @@ jobs: script: | const { default: detectNewResourceProvider } = await import('${{ github.workspace }}/.github/workflows/src/detect-new-resource-provider.js'); - return await detectNewResourceProvider({ github, context, core }); + return await detectNewResourceProvider({ context, core }); - if: fromJson(steps.detect.outputs.result).labelActions['ARMModelingReviewRequired'] != 'none' name: Upload artifact for ARMModelingReviewRequired label diff --git a/.github/workflows/src/detect-new-resource-provider.js b/.github/workflows/src/detect-new-resource-provider.js index 0cbd2cf10440..5124694716d5 100644 --- a/.github/workflows/src/detect-new-resource-provider.js +++ b/.github/workflows/src/detect-new-resource-provider.js @@ -114,12 +114,11 @@ function extractResourceProviders(files) { /** * Main detection logic for GitHub script action * @param {Object} params - Parameters from github-script - * @param {Object} params.github - GitHub API client * @param {Object} params.context - GitHub context * @param {Object} params.core - GitHub Actions core - * @returns {Promise} Result status + * @returns {Promise<{ status: string, labelActions: Record }>} Result including status and label actions */ -export default async function detectNewResourceProvider({ github, context, core }) { +export default async function detectNewResourceProvider({ context, core }) { const repoRoot = await getRootFolder(process.cwd()); const git = simpleGit(repoRoot); @@ -131,12 +130,14 @@ export default async function detectNewResourceProvider({ github, context, core throw new Error('Could not determine base branch from pull request context'); } - // Get the merge base for proper PR comparison + // Get the merge base for proper PR comparison. + // With fetch-depth: 2, HEAD is the PR merge commit and HEAD^ is the base commit. + // git merge-base may fail if origin/ isn't fetched, so fall back to HEAD^. let mergeBase; try { mergeBase = await git.raw(['merge-base', `origin/${baseBranch}`, 'HEAD']).then(s => s.trim()); } catch { - mergeBase = `origin/${baseBranch}`; + mergeBase = 'HEAD^'; } // Get all changed files in specification/ @@ -254,38 +255,26 @@ export default async function detectNewResourceProvider({ github, context, core JSON.stringify({ newResourceProviders: leaseCheckResults }, null, 2) ); - // Create PR comment with detected resource providers - const rpList = leaseCheckResults - .map(rp => `- \`${rp.namespace}\``) - .join('\n'); + // Partition results by lease validity + const invalidLeases = leaseCheckResults.filter(rp => !rp.leaseValid); + const allLeasesValid = invalidLeases.length === 0; - const commentBody = [ - '## New Resource Provider Detected', - 'The following new resource provider namespace(s) were detected in this PR:\n', - rpList, - '\n### Action Required', - 'New resource provider namespaces require having a discussion with the ARM team at **ARM API Modeling Office Hours** before merging.\n', - '**Please schedule here:**', - 'https://outlook.office365.com/book/ARMOfficeHours1@microsoft.onmicrosoft.com/?ismsaljsauthenabled=true' - ].join('\n'); - - try { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: commentBody - }); - core.info('PR comment created successfully'); - } catch (error) { - core.error(`Failed to create PR comment: ${error.message}`); + 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: ' + + 'https://outlook.office365.com/book/ARMOfficeHours1@microsoft.onmicrosoft.com/?ismsaljsauthenabled=true' + ); + } else { + core.info('New resource provider(s) detected with valid ARM lease — no action required.'); } - - // Set the action as failed to indicate new RPs were detected - core.setFailed('New resource providers detected'); + return { - status: 'new-rp-detected', - labelActions: getLabelActions(true) + status: allLeasesValid ? 'new-rp-all-leases-valid' : 'new-rp-invalid-lease', + labelActions: getLabelActions(!allLeasesValid) }; } else { core.info('No new resource providers detected.'); diff --git a/.github/workflows/src/summarize-checks/labelling.js b/.github/workflows/src/summarize-checks/labelling.js index 4263c0379a21..18106bab4a07 100644 --- a/.github/workflows/src/summarize-checks/labelling.js +++ b/.github/workflows/src/summarize-checks/labelling.js @@ -652,7 +652,8 @@ function processARMReviewWorkflowLabels( ); // Block if ARMModelingReviewRequired is present (new RP namespace detected) - const blockedOnArmModeling = new Label("ARMModelingReviewRequired", labelContext.present); + const armModelingReviewLabel = new Label("ARMModelingReviewRequired", labelContext.present); + const blockedOnArmModeling = armModelingReviewLabel.present; const blocked = blockedOnRpaas || blockedOnVersioningPolicy || blockedOnArmModeling; diff --git a/.github/workflows/test/detect-new-resource-provider.test.js b/.github/workflows/test/detect-new-resource-provider.test.js index 6debc56e3ed6..52686811e13c 100644 --- a/.github/workflows/test/detect-new-resource-provider.test.js +++ b/.github/workflows/test/detect-new-resource-provider.test.js @@ -1,329 +1,355 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import detectNewResourceProvider from '../src/detect-new-resource-provider.js'; -describe('detect-new-resource-provider', () => { - let mockGithub; - let mockContext; - let mockCore; +// ============================================ +// Mock all external dependencies +// ============================================ + +// simple-git: mock git.raw() for merge-base and ls-tree calls +const mockGitRaw = vi.fn(); +vi.mock('simple-git', () => ({ + simpleGit: () => ({ raw: mockGitRaw }) +})); + +// fs: mock writeFileSync to avoid writing to disk +vi.mock('fs', () => ({ + writeFileSync: vi.fn() +})); + +// shared/simple-git: mock getRootFolder so it doesn't depend on cwd +vi.mock('../../shared/src/simple-git.js', () => ({ + getRootFolder: vi.fn().mockResolvedValue('/fake/repo') +})); + +// shared/changed-files: mock getChangedFiles, keep resourceManager and swagger real +const mockGetChangedFiles = vi.fn(); +vi.mock('../../shared/src/changed-files.js', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getChangedFiles: (...args) => mockGetChangedFiles(...args) + }; +}); - beforeEach(() => { - mockGithub = { - rest: { - issues: { - createComment: vi.fn().mockResolvedValue({}) - } +// detect-arm-leases: mock checkLease +const mockCheckLease = vi.fn(); +vi.mock('../src/detect-arm-leases.js', () => ({ + checkLease: (...args) => mockCheckLease(...args) +})); + +import detectNewResourceProvider from '../src/detect-new-resource-provider.js'; + +// ============================================ +// Helpers +// ============================================ + +function makeMockCore() { + return { + info: vi.fn(), + error: vi.fn(), + setFailed: vi.fn() + }; +} + +function makeMockContext(baseBranch = 'main') { + return { + repo: { owner: 'Azure', repo: 'azure-rest-api-specs' }, + issue: { number: 42 }, + payload: { + pull_request: { + base: { ref: baseBranch }, + head: { sha: 'abc123' } } - }; - - mockContext = { - repo: { owner: 'Azure', repo: 'azure-rest-api-specs' }, - issue: { number: 123 }, - payload: { - pull_request: { - base: { ref: 'main' }, - head: { sha: 'abc123' } - } + } + }; +} + +/** + * Configure mockGitRaw to handle merge-base and ls-tree calls. + * @param {Object} opts + * @param {string} opts.mergeBase - SHA returned by merge-base + * @param {string[]} [opts.lsTreeDirOutput] - output for ls-tree -d (namespace existence check) + * @param {string[]} [opts.lsTreeFileOutput] - output for ls-tree -r (brand-new RP pre-check) + */ +function setupGitMock({ mergeBase = 'base123', lsTreeDirOutput = [], lsTreeFileOutput = [] } = {}) { + mockGitRaw.mockImplementation(async (args) => { + if (args[0] === 'merge-base') { + return `${mergeBase}\n`; + } + if (args[0] === 'ls-tree') { + // ls-tree -d (directory listing for resourceProviderExistsInCommit) + if (args.includes('-d')) { + return lsTreeDirOutput.join('\n'); } - }; - - mockCore = { - setFailed: vi.fn(), - info: vi.fn(), - error: vi.fn() - }; + // ls-tree -r (file listing for brand-new RP pre-check) + if (args.includes('-r')) { + return lsTreeFileOutput.join('\n'); + } + } + return ''; }); +} - describe('function export', () => { - it('should be exported as default export', () => { - expect(typeof detectNewResourceProvider).toBe('function'); - }); - }); +// ============================================ +// Tests +// ============================================ - describe('basic behavior', () => { - it('should return a valid status', async () => { - const result = await detectNewResourceProvider({ - github: mockGithub, - context: mockContext, - core: mockCore - }); +describe('detect-new-resource-provider', () => { + let mockCore; + let mockContext; - // Result depends on actual workspace state - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); - }); + beforeEach(() => { + vi.clearAllMocks(); + mockCore = makeMockCore(); + mockContext = makeMockContext(); }); - describe('parameter validation', () => { - it('should accept all required parameters', async () => { - const result = await detectNewResourceProvider({ - github: mockGithub, - context: mockContext, - core: mockCore - }); + describe('no resource-manager changes', () => { + it('should return no-changes when no resource-manager files are modified', async () => { + mockGetChangedFiles.mockResolvedValue([ + 'specification/compute/data-plane/foo.json' + ]); + setupGitMock(); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); + const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); + + expect(result.status).toBe('no-changes'); + expect(result.labelActions.ARMModelingReviewRequired).toBe('none'); + expect(mockCore.setFailed).not.toHaveBeenCalled(); }); - it('should work with different baseBranch values from context', async () => { - const rpsaasContext = { - ...mockContext, - payload: { pull_request: { base: { ref: 'RPSaaSMaster' }, head: { sha: 'abc123' } } } - }; + it('should return no-changes when changed files list is empty', async () => { + mockGetChangedFiles.mockResolvedValue([]); + setupGitMock(); - const result = await detectNewResourceProvider({ - github: mockGithub, - context: rpsaasContext, - core: mockCore - }); + const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); + expect(result.status).toBe('no-changes'); }); }); - describe('github API interactions', () => { - it('should complete without throwing', async () => { - // Test runs against real workspace - result depends on actual state - const result = await detectNewResourceProvider({ - github: mockGithub, - context: mockContext, - core: mockCore + describe('existing resource provider (not new)', () => { + it('should return no-new-rp when all RP namespaces exist in base branch', async () => { + const rmFile = 'specification/compute/resource-manager/Microsoft.Compute/stable/2024-01-01/compute.json'; + mockGetChangedFiles.mockResolvedValue([rmFile]); + setupGitMock({ + // Pre-check: file exists in base → not brand new + lsTreeFileOutput: [rmFile], }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); - }); + const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - it('should handle GitHub API errors gracefully', async () => { - const errorGithub = { - rest: { - issues: { - createComment: vi.fn().mockRejectedValue(new Error('API Error')) - } - } - }; - - const result = await detectNewResourceProvider({ - github: errorGithub, - context: mockContext, - core: mockCore - }); - - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); + expect(result.status).toBe('no-new-rp'); + expect(result.labelActions.ARMModelingReviewRequired).toBe('remove'); + expect(mockCore.setFailed).not.toHaveBeenCalled(); }); }); - describe('return values', () => { - it('should return one of three valid statuses', async () => { - const result = await detectNewResourceProvider({ - github: mockGithub, - context: mockContext, - core: mockCore + describe('new resource provider with valid lease', () => { + it('should return new-rp-all-leases-valid and not fail', async () => { + const rmFile = 'specification/newservice/resource-manager/Microsoft.NewService/stable/2025-01-01/api.json'; + mockGetChangedFiles.mockResolvedValue([rmFile]); + setupGitMock({ + // Pre-check: no files in base for this namespace → brand new + lsTreeFileOutput: [], + // Namespace doesn't exist in base commit directories + lsTreeDirOutput: [], }); + mockCheckLease.mockResolvedValue(true); + + const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); + expect(result.status).toBe('new-rp-all-leases-valid'); + expect(result.labelActions.ARMModelingReviewRequired).toBe('remove'); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + expect(mockCore.info).toHaveBeenCalledWith( + expect.stringContaining('valid ARM lease') + ); + expect(mockCheckLease).toHaveBeenCalledWith('newservice', 'Microsoft.NewService', ''); }); }); - describe('context validation', () => { - it('should handle minimal context object', async () => { - const minimalContext = { - repo: { owner: 'test-owner', repo: 'test-repo' }, - issue: { number: 1 }, - payload: { pull_request: { base: { ref: 'main' }, head: { sha: 'abc123' } } } - }; - - const result = await detectNewResourceProvider({ - github: mockGithub, - context: minimalContext, - core: mockCore + describe('new resource provider with invalid lease', () => { + it('should return new-rp-invalid-lease and call setFailed', async () => { + const rmFile = 'specification/badservice/resource-manager/Microsoft.BadService/preview/2025-01-01/api.json'; + mockGetChangedFiles.mockResolvedValue([rmFile]); + setupGitMock({ + lsTreeFileOutput: [], + lsTreeDirOutput: [], }); + mockCheckLease.mockResolvedValue(false); + + const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); + expect(result.status).toBe('new-rp-invalid-lease'); + expect(result.labelActions.ARMModelingReviewRequired).toBe('add'); + expect(mockCore.setFailed).toHaveBeenCalledTimes(1); + expect(mockCore.setFailed).toHaveBeenCalledWith( + expect.stringContaining('ARM API Modeling Office Hours') + ); + expect(mockCore.error).toHaveBeenCalledWith( + expect.stringContaining('Microsoft.BadService') + ); }); }); - describe('edge cases', () => { - it('should handle issue number as zero', async () => { - const zeroIssueContext = { - repo: { owner: 'Azure', repo: 'azure-rest-api-specs' }, - issue: { number: 0 }, - payload: { pull_request: { base: { ref: 'main' }, head: { sha: 'abc123' } } } - }; - - const result = await detectNewResourceProvider({ - github: mockGithub, - context: zeroIssueContext, - core: mockCore + describe('multiple new resource providers with mixed leases', () => { + it('should fail when at least one lease is invalid', async () => { + mockGetChangedFiles.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', + ]); + setupGitMock({ + lsTreeFileOutput: [], + lsTreeDirOutput: [], }); + // SvcA valid, SvcB invalid + mockCheckLease.mockImplementation(async (svc) => + svc === 'svcA' + ); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); - }); - - it('should handle very large issue numbers', async () => { - const largeIssueContext = { - repo: { owner: 'Azure', repo: 'azure-rest-api-specs' }, - issue: { number: 999999999 }, - payload: { pull_request: { base: { ref: 'main' }, head: { sha: 'abc123' } } } - }; + const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - const result = await detectNewResourceProvider({ - github: mockGithub, - context: largeIssueContext, - core: mockCore - }); - - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); + expect(result.status).toBe('new-rp-invalid-lease'); + expect(result.labelActions.ARMModelingReviewRequired).toBe('add'); + expect(mockCore.setFailed).toHaveBeenCalledTimes(1); + expect(mockCore.setFailed).toHaveBeenCalledWith( + expect.stringContaining('without a valid ARM lease') + ); }); - it('should handle empty string repo owner', async () => { - const emptyOwnerContext = { - repo: { owner: '', repo: 'azure-rest-api-specs' }, - issue: { number: 123 }, - payload: { pull_request: { base: { ref: 'main' }, head: { sha: 'abc123' } } } - }; - - const result = await detectNewResourceProvider({ - github: mockGithub, - context: emptyOwnerContext, - core: mockCore + it('should pass when all leases are valid', async () => { + mockGetChangedFiles.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', + ]); + setupGitMock({ + lsTreeFileOutput: [], + lsTreeDirOutput: [], }); + mockCheckLease.mockResolvedValue(true); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); - }); + const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - it('should handle empty string repo name', async () => { - const emptyRepoContext = { - repo: { owner: 'Azure', repo: '' }, - issue: { number: 123 }, - payload: { pull_request: { base: { ref: 'main' }, head: { sha: 'abc123' } } } - }; + expect(result.status).toBe('new-rp-all-leases-valid'); + expect(result.labelActions.ARMModelingReviewRequired).toBe('remove'); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + }); + }); - const result = await detectNewResourceProvider({ - github: mockGithub, - context: emptyRepoContext, - core: mockCore + describe('service group extraction', () => { + it('should pass serviceGroup to checkLease when present', async () => { + const rmFile = 'specification/svc/resource-manager/Microsoft.Svc/ComputeRP/stable/2025-01-01/api.json'; + mockGetChangedFiles.mockResolvedValue([rmFile]); + setupGitMock({ + lsTreeFileOutput: [], + lsTreeDirOutput: [], }); + mockCheckLease.mockResolvedValue(true); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); + await detectNewResourceProvider({ context: mockContext, core: mockCore }); + + expect(mockCheckLease).toHaveBeenCalledWith('svc', 'Microsoft.Svc', 'ComputeRP'); }); + }); - it('should handle createComment throwing network timeout', async () => { - const timeoutGithub = { - rest: { - issues: { - createComment: vi.fn().mockRejectedValue(new Error('Network timeout')) + describe('merge-base fallback', () => { + it('should fall back to HEAD^ when merge-base fails', async () => { + mockGetChangedFiles.mockResolvedValue([ + 'specification/svc/resource-manager/Microsoft.Svc/stable/2025-01-01/api.json', + ]); + let callCount = 0; + mockGitRaw.mockImplementation(async (args) => { + if (args[0] === 'merge-base') { + throw new Error('Not a valid object name'); + } + if (args[0] === 'ls-tree') { + callCount++; + // For the pre-check ls-tree -r call, verify it uses HEAD^ as commitish + if (args.includes('-r') && callCount === 1) { + expect(args).toContain('HEAD^'); } + return ''; } - }; - - const result = await detectNewResourceProvider({ - github: timeoutGithub, - context: mockContext, - core: mockCore + return ''; }); + mockCheckLease.mockResolvedValue(true); + + const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); + // Should still succeed despite merge-base failure + expect(['new-rp-all-leases-valid', 'new-rp-invalid-lease', 'no-new-rp']).toContain(result.status); }); + }); - it('should handle createComment throwing permission error', async () => { - const permissionGithub = { - rest: { - issues: { - createComment: vi.fn().mockRejectedValue(new Error('Permission denied')) - } - } + describe('context validation', () => { + it('should throw when pull_request context is missing', async () => { + const badContext = { + repo: { owner: 'Azure', repo: 'azure-rest-api-specs' }, + issue: { number: 1 }, + payload: {} }; - const result = await detectNewResourceProvider({ - github: permissionGithub, - context: mockContext, - core: mockCore - }); - - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); + await expect( + detectNewResourceProvider({ context: badContext, core: mockCore }) + ).rejects.toThrow('Could not determine base branch'); }); - it('should handle GitHub API methods failing', async () => { - const failingGithub = { - rest: { - issues: { - createComment: vi.fn().mockRejectedValue(new Error('Service unavailable')) - } - } - }; + it('should use baseBranch from context payload', async () => { + const rpsaasContext = makeMockContext('RPSaaSMaster'); + mockGetChangedFiles.mockResolvedValue([]); + setupGitMock(); - const result = await detectNewResourceProvider({ - github: failingGithub, - context: mockContext, - core: mockCore - }); + await detectNewResourceProvider({ context: rpsaasContext, core: mockCore }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); + // merge-base should have been called with origin/RPSaaSMaster + expect(mockGitRaw).toHaveBeenCalledWith( + expect.arrayContaining(['merge-base', 'origin/RPSaaSMaster', 'HEAD']) + ); }); + }); - it('should handle core.setFailed as noop', async () => { - const noopCore = { - info: vi.fn(), - error: vi.fn(), - setFailed: vi.fn() // noop setFailed - }; + describe('label actions', () => { + it('should return Remove for ARMModelingReviewRequired when no new RPs', async () => { + const rmFile = 'specification/compute/resource-manager/Microsoft.Compute/stable/2024-01-01/compute.json'; + mockGetChangedFiles.mockResolvedValue([rmFile]); + setupGitMock({ lsTreeFileOutput: [rmFile] }); - const result = await detectNewResourceProvider({ - github: mockGithub, - context: mockContext, - core: noopCore - }); + const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); + expect(result.labelActions.ARMModelingReviewRequired).toBe('remove'); }); - it('should handle multiple concurrent calls', async () => { - const calls = Array(3).fill(null).map(() => - detectNewResourceProvider({ - github: mockGithub, - context: mockContext, - core: mockCore - }) - ); + it('should return Add for ARMModelingReviewRequired when new RP has invalid lease', async () => { + mockGetChangedFiles.mockResolvedValue([ + 'specification/svc/resource-manager/Microsoft.Svc/stable/2025-01-01/api.json', + ]); + setupGitMock({ lsTreeFileOutput: [], lsTreeDirOutput: [] }); + mockCheckLease.mockResolvedValue(false); - const results = await Promise.all(calls); + const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - results.forEach(result => { - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); - }); + expect(result.labelActions.ARMModelingReviewRequired).toBe('add'); }); - it('should handle context with additional unexpected properties', async () => { - const extendedContext = { - repo: { owner: 'Azure', repo: 'azure-rest-api-specs', extra: 'data' }, - issue: { number: 123, title: 'Test PR', labels: [] }, - payload: { action: 'opened', pull_request: { base: { ref: 'main' }, head: { sha: 'abc123' } } }, - unexpected: 'property' - }; + it('should return Remove for ARMModelingReviewRequired when new RP has valid lease', async () => { + mockGetChangedFiles.mockResolvedValue([ + 'specification/svc/resource-manager/Microsoft.Svc/stable/2025-01-01/api.json', + ]); + setupGitMock({ lsTreeFileOutput: [], lsTreeDirOutput: [] }); + mockCheckLease.mockResolvedValue(true); - const result = await detectNewResourceProvider({ - github: mockGithub, - context: extendedContext, - core: mockCore - }); + const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); + expect(result.labelActions.ARMModelingReviewRequired).toBe('remove'); }); - it('should not throw when GitHub methods return null', async () => { - const nullGithub = { - rest: { - issues: { - createComment: vi.fn().mockResolvedValue(null) - } - } - }; + it('should return none for no-changes status', async () => { + mockGetChangedFiles.mockResolvedValue([]); + setupGitMock(); - const result = await detectNewResourceProvider({ - github: nullGithub, - context: mockContext, - core: mockCore - }); + const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - expect(['no-changes', 'no-new-rp', 'new-rp-detected']).toContain(result.status); + expect(result.labelActions.ARMModelingReviewRequired).toBe('none'); }); }); }); From 0c0dc164e0a94295fd96cf8d02d280a771cb1638 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Thu, 12 Feb 2026 15:22:56 -0600 Subject: [PATCH 026/125] Itr 1: removed yamle file and fixed tests with spell check --- .../detect-new-resource-provider.yaml | 53 ------------------- 1 file changed, 53 deletions(-) delete mode 100644 .github/workflows/detect-new-resource-provider.yaml diff --git a/.github/workflows/detect-new-resource-provider.yaml b/.github/workflows/detect-new-resource-provider.yaml deleted file mode 100644 index 78b43b425b9f..000000000000 --- a/.github/workflows/detect-new-resource-provider.yaml +++ /dev/null @@ -1,53 +0,0 @@ -name: Detect New Resource Provider - -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 - paths: - - "specification/**/resource-manager/**" - -permissions: - contents: read - -jobs: - detect-new-rp: - name: Detect New Resource Provider - runs-on: ubuntu-24.04 - # TODO: Set to true to enable new RP detection - if: false - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - fetch-depth: 2 - sparse-checkout: | - .github - - - name: Install dependencies for github-script actions - uses: ./.github/actions/install-deps-github-script - - - name: Detect new resource providers - id: detect - uses: actions/github-script@v8 - with: - script: | - const { default: detectNewResourceProvider } = - await import('${{ github.workspace }}/.github/workflows/src/detect-new-resource-provider.js'); - return await detectNewResourceProvider({ context, core }); - - - if: fromJson(steps.detect.outputs.result).labelActions['ARMModelingReviewRequired'] != 'none' - name: Upload artifact for ARMModelingReviewRequired label - uses: ./.github/actions/add-label-artifact - with: - name: "ARMModelingReviewRequired" - value: "${{ fromJson(steps.detect.outputs.result).labelActions['ARMModelingReviewRequired'] == 'add' }}" From 337923eca3a2150b007d81f7c36a5f39310f6c03 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Fri, 13 Feb 2026 10:57:30 -0600 Subject: [PATCH 027/125] spell check --- .github/workflows/test/detect-new-resource-provider.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test/detect-new-resource-provider.test.js b/.github/workflows/test/detect-new-resource-provider.test.js index 52686811e13c..ab2ea90424f1 100644 --- a/.github/workflows/test/detect-new-resource-provider.test.js +++ b/.github/workflows/test/detect-new-resource-provider.test.js @@ -295,11 +295,11 @@ describe('detect-new-resource-provider', () => { }); it('should use baseBranch from context payload', async () => { - const rpsaasContext = makeMockContext('RPSaaSMaster'); + const altBranchContext = makeMockContext('RPSaaSMaster'); mockGetChangedFiles.mockResolvedValue([]); setupGitMock(); - await detectNewResourceProvider({ context: rpsaasContext, core: mockCore }); + await detectNewResourceProvider({ context: altBranchContext, core: mockCore }); // merge-base should have been called with origin/RPSaaSMaster expect(mockGitRaw).toHaveBeenCalledWith( From d5d3173268233f30b3d1449f0c869f12d5584ea0 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Fri, 13 Feb 2026 15:56:18 -0600 Subject: [PATCH 028/125] Itr 1: addressed all comments from Mike and Copilot --- .github/arm-leases/README.md | 28 +- .../testservice/Microsoft.TestRP/lease.yaml | 6 +- .github/workflows/src/validate-arm-leases.js | 313 +++++------ .../test/validate-arm-leases.test.js | 516 ++++++++---------- .github/workflows/validate-arm-leases.yaml | 29 +- 5 files changed, 399 insertions(+), 493 deletions(-) diff --git a/.github/arm-leases/README.md b/.github/arm-leases/README.md index 8b9d93832963..a847d337f5a9 100644 --- a/.github/arm-leases/README.md +++ b/.github/arm-leases/README.md @@ -8,13 +8,13 @@ This directory contains lease files that establish a time-limited design discuss ## Code Owners and Contribution Guidelines -This directory is protected by CODEOWNERS to ensure proper governance: +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 require approval from code owners (PMs and engineers listed in CODEOWNERS) +- **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 must be added to the CODEOWNERS file for the `.github/arm-leases/` directory path. +**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 @@ -36,17 +36,30 @@ The `lease.yaml` file must follow this format: lease: resource-provider: Microsoft.TestRP # Must match the namespace folder name startdate: 2026-01-07 # ISO 8601 format (YYYY-MM-DD) - duration: 180 days # Maximum allowed is 180 days + duration-days: P180D # ISO 8601 duration (maximum P180D) reviewer: Evan Hissey # Name of the approving reviewer ``` +### Copy-Paste Template + +Create a file at `.github/arm-leases///lease.yaml` with the following content (replace the placeholder values): + +```yaml +lease: + resource-provider: + startdate: + duration-days: P180D + reviewer: + +``` + ## 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 exact folder structure: `//lease.yaml` +- Must follow the folder structure: `//[ (optional)]/lease.yaml` ### 2. Resource Provider Name - Must match the namespace folder name exactly @@ -58,11 +71,12 @@ All lease files are automatically validated with the following requirements: ### 4. Duration - Required field that cannot be empty -- Must be in the format: `X days` (e.g., `90 days`, `180 days`) -- **Cannot exceed 180 days** (maximum lease period) +- Must be in ISO 8601 duration format: `P{n}D` (e.g., `P90D`, `P180D`) +- **Cannot exceed P180D** (maximum lease period of 180 days) - Must be greater than 0 days ### 5. Reviewer - Required field that cannot be empty - Must contain the name of the PM who approved the lease - Only PMs can be listed as reviewers + diff --git a/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml b/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml index 70e7bb67a1d7..70906149904f 100644 --- a/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml +++ b/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml @@ -1,5 +1,5 @@ lease: resource-provider: Microsoft.TestRP - startdate: 2026-01-07 # ISO 8601 format (YYYY-MM-DD) - duration: 180 days # Maximum allowed is 180 days - reviewer: Tejaswi Salaigari \ No newline at end of file + startdate: 2026-02-15 # ISO 8601 format (YYYY-MM-DD) + duration-days: P180D # ISO 8601 duration (maximum P180D) + reviewer: Tejaswi Salaigari diff --git a/.github/workflows/src/validate-arm-leases.js b/.github/workflows/src/validate-arm-leases.js index 2646bdd1fd96..35b700b29a97 100644 --- a/.github/workflows/src/validate-arm-leases.js +++ b/.github/workflows/src/validate-arm-leases.js @@ -1,10 +1,9 @@ -#!/usr/bin/env node - -import { execSync } from 'child_process'; -import { readFileSync, existsSync } from 'fs'; +import { readFile, access } from 'fs/promises'; import { join } from 'path'; import YAML from 'js-yaml'; +import * as z from 'zod'; import { getChangedFiles } from '../../shared/src/changed-files.js'; +import { CoreLogger } from './core-logger.js'; // ============================================ // Configuration @@ -12,13 +11,45 @@ import { getChangedFiles } from '../../shared/src/changed-files.js'; const ALLOWED_FILE_PATTERNS = [ /^\.github\/arm-leases\//, - /^\.github\/workflows\/validate-arm-leases\.yaml$/, - /^\.github\/workflows\/src\/validate-arm-leases\.js$/ ]; const LEASE_FILE_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/lease\.yaml$/; const LEASE_FILE_WITH_GROUP_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/(?!stable|preview)([^\/]+)\/lease\.yaml$/; -const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; + +/** + * Zod schema for lease.yaml file content. + * + * Example: + * ```yaml + * lease: + * resource-provider: Microsoft.Compute + * startdate: "2025-06-01" + * duration-days: "P180D" + * reviewer: alias + * ``` + */ +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)'), + 'duration-days': z.string().regex(/^P(\d+)D$/i, 'Invalid duration-days format (expected ISO 8601: "P180D")').refine( + (d) => { + const m = d.match(/^P(\d+)D$/i); + if (!m) return false; + const days = parseInt(m[1], 10); + return days > 0 && days <= 180; + }, + 'Duration must be between 1 and 180 days', + ), + reviewer: z.string().min(1, 'Reviewer is required and cannot be empty').refine( + (r) => r.trim().length > 0, + 'Reviewer is required and cannot be empty', + ), + }), +}); // ============================================ // Utility Functions @@ -27,173 +58,125 @@ const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; /** * Check if a file is allowed based on patterns * @param {string} file - File path to check - * @returns {boolean} True if file is allowed + * @returns {Promise} True if file is allowed */ -function isFileAllowed(file) { +async 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 + * @returns {Promise} Array of invalid files */ -function validateFolderStructure(files) { +async function validateFolderStructure(files) { return files.filter(file => !LEASE_FILE_PATTERN.test(file) && !LEASE_FILE_WITH_GROUP_PATTERN.test(file)); } /** - * Parse lease YAML file - * @param {string} filePath - Path to lease.yaml file - * @returns {{resourceProvider: string, startdate: string, duration: string, reviewer: string}|{error: string}} Parsed lease data or error object - */ -function parseLeaseFile(filePath) { - try { - const content = readFileSync(filePath, 'utf-8'); - const data = YAML.load(content); - - if (!data || !data.lease) { - return { error: 'Invalid YAML structure: missing "lease" key' }; - } - - // Convert Date objects to YYYY-MM-DD strings - let startdate = data.lease.startdate || ''; - if (startdate instanceof Date) { - startdate = startdate.toISOString().split('T')[0]; - } - - return { - resourceProvider: data.lease['resource-provider'] || '', - startdate: startdate, - duration: data.lease.duration || '', - reviewer: data.lease.reviewer || '' - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - return { error: `Error reading file: ${errorMessage}` }; - } -} - -/** - * Validate lease file contents - * @param {string} leaseFile - Path to lease file (can be full or relative) + * Validate lease file contents using Zod schema + * @param {string} leaseFile - Full path to lease file * @param {string} today - Today's date in YYYY-MM-DD format * @param {string} relativePath - Relative path for folder name extraction - * @returns {{file: string, errors: string[]}} Validation result with errors array + * @returns {Promise<{file: string, errors: string[]}>} Validation result with errors array */ -function validateLeaseContent(leaseFile, today, relativePath) { +async function validateLeaseContent(leaseFile, today, relativePath) { const errors = []; const pathForExtraction = relativePath || leaseFile; // Extract namespace from .github/arm-leases///lease.yaml // or .github/arm-leases////lease.yaml const folderRP = pathForExtraction.split('/')[3]; // namespace is always at index 3 - - if (!existsSync(leaseFile)) { + + try { + await access(leaseFile); + } catch { return { file: leaseFile, errors: ['File does not exist'] }; } - const leaseData = parseLeaseFile(leaseFile); - - if (!leaseData) { - return { file: leaseFile, errors: ['Failed to parse lease file'] }; - } - - if ('error' in leaseData) { - return { file: leaseFile, errors: [leaseData.error] }; + let content; + try { + content = await readFile(leaseFile, 'utf-8'); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + return { file: leaseFile, errors: [`Error reading file: ${msg}`] }; } - // Validation 1: Resource provider name matches folder name - if (leaseData.resourceProvider !== folderRP) { - errors.push(`Resource provider mismatch: folder=${folderRP}, yaml=${leaseData.resourceProvider}`); + // 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) { + const msg = error instanceof Error ? error.message : String(error); + return { file: leaseFile, errors: [`Invalid YAML: ${msg}`] }; } - // Validation 1b: Resource provider must start with a capital letter (before and after dot) - if (leaseData.resourceProvider) { - const parts = leaseData.resourceProvider.split('.'); - const invalidParts = parts.filter(part => part && !/^[A-Z]/.test(part)); - if (invalidParts.length > 0) { - errors.push(`Resource provider parts must start with a capital letter: ${leaseData.resourceProvider} (e.g., Microsoft.Test, Azure.Widget)`); + // 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 }; } - // Validation 2: Startdate format (ISO 8601: YYYY-MM-DD) - if (!DATE_PATTERN.test(leaseData.startdate)) { - errors.push(`Invalid startdate format: ${leaseData.startdate} (expected: YYYY-MM-DD)`); - } else { - // Validation 3: Startdate is today or future (not past) - if (leaseData.startdate < today) { - errors.push(`Startdate is in the past: ${leaseData.startdate} (today: ${today})`); - } - } + const lease = result.data.lease; - // Validation 4: Duration validation (must not exceed 180 days) - if (!leaseData.duration) { - errors.push('Duration is missing (expected format: "180 days")'); - } else { - const durationMatch = leaseData.duration.match(/^(\d+)\s*days?$/i); - if (!durationMatch) { - errors.push(`Invalid duration format: ${leaseData.duration} (expected format: "180 days")`); - } else { - const durationDays = parseInt(durationMatch[1], 10); - if (durationDays > 180) { - errors.push(`Duration exceeds maximum allowed: ${durationDays} days (maximum: 180 days)`); - } - if (durationDays <= 0) { - errors.push(`Duration must be greater than 0: ${durationDays} days`); - } - } + // 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']}`); } - // Validation 5: Reviewer cannot be null or empty - if (!leaseData.reviewer || leaseData.reviewer.trim() === '') { - errors.push('Reviewer is required and cannot be empty'); + // Cross-field validation: startdate must not be in the past + if (lease.startdate < today) { + errors.push(`Startdate is in the past: ${lease.startdate} (today: ${today})`); } return { file: leaseFile, errors }; } +export { isFileAllowed, validateFolderStructure, validateLeaseContent, leaseSchema, + ALLOWED_FILE_PATTERNS, LEASE_FILE_PATTERN, LEASE_FILE_WITH_GROUP_PATTERN }; + // ============================================ // Main Validation Logic // ============================================ -async function main() { - const baseBranch = process.argv[2] || 'main'; +/** + * 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 today = new Date().toISOString().split('T')[0]; - const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim(); - let exitCode = 0; - - console.log('Running ARM Lease File Validation\n'); + const cwd = process.env.GITHUB_WORKSPACE; + let hasErrors = false; - // Get the merge base for proper PR comparison - let mergeBase; - try { - mergeBase = execSync(`git merge-base origin/${baseBranch} HEAD`, { encoding: 'utf-8' }).trim(); - } catch (error) { - console.log('Warning: Could not find merge-base, using origin/main directly'); - mergeBase = `origin/${baseBranch}`; - } + core.info('Running ARM Lease File Validation\n'); - // Step 1: Get all changed files (first without filter to debug) - const allFiles = await getChangedFiles({ - baseCommitish: mergeBase, - headCommitish: 'HEAD' + // Step 1: Get all changed files under .github/arm-leases/ + const allChangedFiles = await getChangedFiles({ + cwd, + paths: ['.github/arm-leases'], + logger: new CoreLogger(core), }); - - // Filter for arm-leases files from all changed files - const allChangedFiles = allFiles.filter(file => file.startsWith('.github/arm-leases/')); // Step 2: Check for disallowed files - const disallowedFiles = allChangedFiles.filter(file => !isFileAllowed(file)); + const disallowedFiles = []; + for (const file of allChangedFiles) { + if (!(await isFileAllowed(file))) { + disallowedFiles.push(file); + } + } if (disallowedFiles.length > 0) { - console.log(`Found ${disallowedFiles.length} file(s) outside the .github/arm-leases/ directory. Only changes within .github/arm-leases/ directory are allowed. Please move the following files under .github/arm-leases/ directory:\n`); - console.log('Disallowed files:'); - disallowedFiles.slice(0, 20).forEach(file => console.log(` ${file}`)); + core.info(`Found ${disallowedFiles.length} disallowed file(s). Only lease.yaml and README.md files within .github/arm-leases/ are allowed:\n`); + core.info('Disallowed files:'); + disallowedFiles.slice(0, 20).forEach(file => core.info(` ${file}`)); if (disallowedFiles.length > 20) { - console.log(` ... and ${disallowedFiles.length - 20} more files`); + core.info(` ... and ${disallowedFiles.length - 20} more files`); } - console.log(''); - exitCode = 1; + core.info(''); + hasErrors = true; } // Step 3: Check for non-lease.yaml and non-README files @@ -202,10 +185,10 @@ async function main() { ); if (nonLeaseFiles.length > 0) { - console.log(`Found ${nonLeaseFiles.length} file(s) that are not lease.yaml:`); - nonLeaseFiles.forEach(file => console.log(`Remove or rename - ${file}`)); - console.log('Only lease.yaml files are allowed in .github/arm-leases/ directory\n'); - exitCode = 1; + core.info(`Found ${nonLeaseFiles.length} file(s) that are not lease.yaml:`); + nonLeaseFiles.forEach(file => core.info(`Remove or rename - ${file}`)); + core.info('Only lease.yaml files are allowed in .github/arm-leases/ directory\n'); + hasErrors = true; } // Step 4: Get ARM lease files (only lease.yaml files) @@ -214,29 +197,31 @@ async function main() { ); if (armLeaseFiles.length === 0) { - if (exitCode === 0) { - console.log('--------- No ARM lease files to validate ------------'); + if (!hasErrors) { + core.info('--------- No ARM lease files to validate ------------'); + } else { + core.setFailed('ARM Lease Validation failed - fix errors above'); } - process.exit(exitCode); + return { status: hasErrors ? 'failed' : 'no-lease-files', errors: hasErrors ? 1 : 0 }; } // Step 5: Validate folder structure - const invalidStructure = validateFolderStructure(armLeaseFiles); + const invalidStructure = await validateFolderStructure(armLeaseFiles); if (invalidStructure.length > 0) { - console.log(`${invalidStructure.length} file(s) with invalid folder structure:`); - invalidStructure.forEach(file => console.log(` ${file}`)); - console.log('Expected format: .github/arm-leases///[ (optional)]/lease.yaml'); - console.log('Requirements:'); - console.log(' - : lowercase alphanumeric only (e.g., testservice, widgetservice)'); - console.log(' - : alphanumeric with dots and case-sensitive (e.g., Test.Rp, Widget.Manager)'); - console.log(' - : (optional) logical grouping within an RP (e.g., DiskRP, ComputeRP). Must not start with "stable" or "preview"'); - console.log(' - Only lease.yaml files are allowed in arm-leases folder'); - console.log('Examples:'); - console.log(' - .github/arm-leases/testservice/Test.Rp/lease.yaml'); - console.log(' - .github/arm-leases/widgetservice/Widget.Manager/lease.yaml'); - console.log(' - .github/arm-leases/compute/Microsoft.Compute/DiskRP/lease.yaml\n'); - exitCode = 1; + core.info(`${invalidStructure.length} file(s) with invalid folder structure:`); + invalidStructure.forEach(file => 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) logical grouping 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\n'); + hasErrors = true; } // Step 6: Validate lease file contents @@ -245,46 +230,40 @@ async function main() { ); if (validLeaseFiles.length === 0) { - printSummary(exitCode); - process.exit(exitCode); + 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 = join(repoRoot, leaseFile); - const result = validateLeaseContent(fullPath, today, leaseFile); + const fullPath = join(cwd, leaseFile); + const result = await validateLeaseContent(fullPath, today, leaseFile); if (result.errors.length > 0) { - // Store with relative path for display contentErrors.push({ file: leaseFile, errors: result.errors }); - exitCode = 1; + hasErrors = true; } } // Step 7: Print lease file content errors if (contentErrors.length > 0) { - console.log('Lease content validation errors:\n'); + core.info('Lease content validation errors:\n'); contentErrors.forEach(({ file, errors }) => { - console.log(`${file}`); - errors.forEach(error => console.log(` - ${error}`)); - console.log(''); + core.info(`${file}`); + errors.forEach(error => core.info(` - ${error}`)); + core.info(''); }); } - printSummary(exitCode); - process.exit(exitCode); -} - -/** - * Print final validation summary - * @param {number} exitCode - Exit code (0 = success, 1 = failure) - */ -function printSummary(exitCode) { - if (exitCode === 0) { - console.log('All validations passed!'); + if (hasErrors) { + core.setFailed('ARM Lease Validation failed - fix errors above'); } else { - console.log('ARM Lease Validation failed - fix errors above'); + core.info('All validations passed!'); } -} -main(); \ No newline at end of file + return { status: hasErrors ? 'failed' : 'passed', errors: contentErrors.length }; +} \ No newline at end of file diff --git a/.github/workflows/test/validate-arm-leases.test.js b/.github/workflows/test/validate-arm-leases.test.js index c94a464660e1..9f891691ff5e 100644 --- a/.github/workflows/test/validate-arm-leases.test.js +++ b/.github/workflows/test/validate-arm-leases.test.js @@ -1,361 +1,275 @@ -import { describe, it, beforeEach, afterEach } from 'node:test'; -import assert from 'node:assert'; -import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs'; -import { join } from 'path'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// Test directory setup -const TEST_DIR = join(__dirname, 'test-temp'); -const ARM_LEASES_DIR = join(TEST_DIR, '.github', 'arm-leases'); - -// Helper function to create test lease file -function createLeaseFile(serviceName, namespace, content) { - const leaseDir = join(ARM_LEASES_DIR, serviceName, namespace); - mkdirSync(leaseDir, { recursive: true }); - const leaseFilePath = join(leaseDir, 'lease.yaml'); - writeFileSync(leaseFilePath, content); - return leaseFilePath; -} - -// Helper function to get relative path -function getRelativePath(fullPath) { - return fullPath.replace(TEST_DIR + '/', ''); -} - -describe('ARM Leases Validation Tests', () => { - beforeEach(() => { - // Clean up and create fresh test directory - if (existsSync(TEST_DIR)) { - rmSync(TEST_DIR, { recursive: true, force: true }); - } - mkdirSync(ARM_LEASES_DIR, { recursive: true }); - }); - +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + isFileAllowed, + validateFolderStructure, + validateLeaseContent, + leaseSchema, + LEASE_FILE_PATTERN, + LEASE_FILE_WITH_GROUP_PATTERN, +} from "../src/validate-arm-leases.js"; + +/** @type {import("vitest").MockedFunction} */ +let mockAccess; + +/** @type {import("vitest").MockedFunction} */ +let mockReadFile; + +vi.mock("fs/promises", () => ({ + access: (...args) => mockAccess(...args), + readFile: (...args) => mockReadFile(...args), +})); + +describe("validate-arm-leases", () => { afterEach(() => { - // Clean up test directory - if (existsSync(TEST_DIR)) { - rmSync(TEST_DIR, { recursive: true, force: true }); - } + vi.clearAllMocks(); }); - describe('File Pattern Validation', () => { - it('should accept valid lease file pattern', () => { - const validPaths = [ - '.github/arm-leases/testservice/Microsoft.Test/lease.yaml', - '.github/arm-leases/widgetservice/Azure.Widget/lease.yaml', - '.github/arm-leases/service123/Contoso.Manager/lease.yaml' - ]; + describe("isFileAllowed", () => { + it("allows files in arm-leases directory", async () => { + await expect(isFileAllowed(".github/arm-leases/testservice/Microsoft.Test/lease.yaml")).resolves.toBe(true); + await expect(isFileAllowed(".github/arm-leases/anything/here")).resolves.toBe(true); + }); - const LEASE_FILE_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/lease\.yaml$/; - - validPaths.forEach(path => { - assert.ok(LEASE_FILE_PATTERN.test(path), `${path} should be valid`); - }); + it("rejects files outside arm-leases directory", async () => { + await expect(isFileAllowed("specification/testservice/readme.md")).resolves.toBe(false); + await expect(isFileAllowed(".github/other-file.yaml")).resolves.toBe(false); + await expect(isFileAllowed(".github/workflows/validate-arm-leases.yaml")).resolves.toBe(false); + await expect(isFileAllowed("arm-leases/testservice/Microsoft.Test/lease.yaml")).resolves.toBe(false); }); + }); - it('should accept valid lease file pattern with service group', () => { - const validPaths = [ - '.github/arm-leases/compute/Microsoft.Compute/DiskRP/lease.yaml', - '.github/arm-leases/compute/Microsoft.Compute/ComputeRP/lease.yaml', - '.github/arm-leases/storage/Azure.Storage/BlobRP/lease.yaml' + describe("validateFolderStructure", () => { + it("accepts valid lease file paths", async () => { + const validFiles = [ + ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", + ".github/arm-leases/widgetservice/Azure.Widget/lease.yaml", + ".github/arm-leases/compute/Microsoft.Compute/DiskRP/lease.yaml", ]; - - const LEASE_FILE_WITH_GROUP_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/(?!stable|preview)([^\/]+)\/lease\.yaml$/; - - validPaths.forEach(path => { - assert.ok(LEASE_FILE_WITH_GROUP_PATTERN.test(path), `${path} should be valid`); - }); + await expect(validateFolderStructure(validFiles)).resolves.toHaveLength(0); }); - it('should reject lease files with stable or preview as service group', () => { - const invalidPaths = [ - '.github/arm-leases/compute/Microsoft.Compute/stable/lease.yaml', - '.github/arm-leases/compute/Microsoft.Compute/preview/lease.yaml', - '.github/arm-leases/compute/Microsoft.Compute/stable-2024-01-01/lease.yaml', - '.github/arm-leases/compute/Microsoft.Compute/preview-2025-10-11/lease.yaml', - '.github/arm-leases/compute/Microsoft.Compute/preview-internal/lease.yaml' + it("rejects invalid folder structures", async () => { + const invalidFiles = [ + ".github/arm-leases/TestService/Microsoft.Test/lease.yaml", + ".github/arm-leases/test-service/Microsoft.Test/lease.yaml", + ".github/arm-leases/testservice/Microsoft.Test/other.yaml", ]; - - const LEASE_FILE_WITH_GROUP_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/(?!stable|preview)([^\/]+)\/lease\.yaml$/; - - invalidPaths.forEach(path => { - assert.ok(!LEASE_FILE_WITH_GROUP_PATTERN.test(path), `${path} should be invalid`); - }); + await expect(validateFolderStructure(invalidFiles)).resolves.toHaveLength(invalidFiles.length); }); - it('should reject invalid lease file patterns', () => { - const invalidPaths = [ - '.github/arm-leases/TestService/Microsoft.Test/lease.yaml', // uppercase service name - '.github/arm-leases/test-service/Microsoft.Test/lease.yaml', // hyphen in service name - '.github/arm-leases/testservice/Microsoft.Test/other.yaml', // not lease.yaml - 'arm-leases/testservice/Microsoft.Test/lease.yaml', // missing .github + it("rejects stable or preview as service group", async () => { + const invalidFiles = [ + ".github/arm-leases/compute/Microsoft.Compute/stable/lease.yaml", + ".github/arm-leases/compute/Microsoft.Compute/preview/lease.yaml", + ".github/arm-leases/compute/Microsoft.Compute/stable-2024-01-01/lease.yaml", + ".github/arm-leases/compute/Microsoft.Compute/preview-internal/lease.yaml", ]; - - const LEASE_FILE_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/lease\.yaml$/; - - invalidPaths.forEach(path => { - assert.ok(!LEASE_FILE_PATTERN.test(path), `${path} should be invalid`); - }); + await expect(validateFolderStructure(invalidFiles)).resolves.toHaveLength(invalidFiles.length); }); }); - describe('Resource Provider Validation', () => { - it('should validate resource provider starts with capital letter', () => { - const validProviders = [ - 'Microsoft.Test', - 'Azure.Widget', - 'Contoso.WidgetManager', - 'MyCompany.Service.Core' - ]; - - validProviders.forEach(provider => { - const parts = provider.split('.'); - const invalidParts = parts.filter(part => part && !/^[A-Z]/.test(part)); - assert.strictEqual(invalidParts.length, 0, `${provider} should be valid`); - }); + describe("LEASE_FILE_PATTERN", () => { + it("matches valid patterns", () => { + expect(LEASE_FILE_PATTERN.test(".github/arm-leases/testservice/Microsoft.Test/lease.yaml")).toBe(true); + expect(LEASE_FILE_PATTERN.test(".github/arm-leases/service123/Contoso.Manager/lease.yaml")).toBe(true); }); - it('should reject resource provider with lowercase parts', () => { - const invalidProviders = [ - 'microsoft.Test', - 'Microsoft.test', - 'azure.Widget', - 'Contoso.widgetManager' - ]; - - invalidProviders.forEach(provider => { - const parts = provider.split('.'); - const invalidParts = parts.filter(part => part && !/^[A-Z]/.test(part)); - assert.ok(invalidParts.length > 0, `${provider} should be invalid`); - }); + it("rejects invalid patterns", () => { + expect(LEASE_FILE_PATTERN.test(".github/arm-leases/TestService/Microsoft.Test/lease.yaml")).toBe(false); + expect(LEASE_FILE_PATTERN.test("arm-leases/testservice/Microsoft.Test/lease.yaml")).toBe(false); }); }); - describe('Date Validation', () => { - it('should accept valid ISO 8601 date format', () => { - const validDates = [ - '2026-01-07', - '2026-12-31', - '2027-06-15' - ]; - - const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; - - validDates.forEach(date => { - assert.ok(DATE_PATTERN.test(date), `${date} should be valid`); - }); + describe("LEASE_FILE_WITH_GROUP_PATTERN", () => { + it("matches valid service group patterns", () => { + expect(LEASE_FILE_WITH_GROUP_PATTERN.test(".github/arm-leases/compute/Microsoft.Compute/DiskRP/lease.yaml")).toBe(true); + expect(LEASE_FILE_WITH_GROUP_PATTERN.test(".github/arm-leases/storage/Azure.Storage/BlobRP/lease.yaml")).toBe(true); }); - it('should reject invalid date formats', () => { - const invalidDates = [ - '01-07-2026', // wrong order - '2026/01/07', // wrong separator - '2026-1-7', // missing leading zeros - '26-01-07', // two-digit year - 'January 7, 2026' // text format - ]; - - const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; - - invalidDates.forEach(date => { - assert.ok(!DATE_PATTERN.test(date), `${date} should be invalid`); - }); + it("rejects stable/preview as service group", () => { + expect(LEASE_FILE_WITH_GROUP_PATTERN.test(".github/arm-leases/compute/Microsoft.Compute/stable/lease.yaml")).toBe(false); + expect(LEASE_FILE_WITH_GROUP_PATTERN.test(".github/arm-leases/compute/Microsoft.Compute/preview/lease.yaml")).toBe(false); }); + }); - it('should reject past dates', () => { - const today = '2026-01-07'; - const pastDate = '2026-01-06'; - - assert.ok(pastDate < today, 'Past date should be less than today'); + describe("leaseSchema", () => { + it("accepts valid lease data", () => { + const valid = { + lease: { + "resource-provider": "Microsoft.Test", + startdate: "2026-06-01", + "duration-days": "P90D", + reviewer: "John Doe", + }, + }; + expect(leaseSchema.safeParse(valid).success).toBe(true); }); - it('should accept today and future dates', () => { - const today = '2026-01-07'; - const todayDate = '2026-01-07'; - const futureDate = '2026-01-08'; - - assert.ok(todayDate >= today, 'Today should be valid'); - assert.ok(futureDate >= today, 'Future date should be valid'); + it("rejects resource provider with lowercase parts", () => { + const invalid = { + lease: { + "resource-provider": "microsoft.Test", + startdate: "2026-06-01", + "duration-days": "P90D", + reviewer: "John Doe", + }, + }; + expect(leaseSchema.safeParse(invalid).success).toBe(false); }); - }); - describe('Duration Validation', () => { - it('should accept valid duration formats', () => { - const validDurations = [ - '90 days', - '180 days', - '1 day', - '30days', - '60 Days' - ]; + it("rejects invalid startdate format", () => { + const invalid = { + lease: { + "resource-provider": "Microsoft.Test", + startdate: "01-15-2026", + "duration-days": "P90D", + reviewer: "John Doe", + }, + }; + expect(leaseSchema.safeParse(invalid).success).toBe(false); + }); - validDurations.forEach(duration => { - const match = duration.match(/^(\d+)\s*days?$/i); - assert.ok(match !== null, `${duration} should match pattern`); - - const days = parseInt(match[1], 10); - assert.ok(days > 0, `${duration} should be greater than 0`); - assert.ok(days <= 180, `${duration} should not exceed 180 days`); - }); + it("rejects invalid duration-days format", () => { + const invalid = { + lease: { + "resource-provider": "Microsoft.Test", + startdate: "2026-06-01", + "duration-days": "90 days", + reviewer: "John Doe", + }, + }; + expect(leaseSchema.safeParse(invalid).success).toBe(false); }); - it('should reject invalid duration formats', () => { - const invalidDurations = [ - '90', // missing 'days' - 'ninety days', // not numeric - '90 months', // wrong unit - '-10 days', // negative - '0 days' // zero - ]; + it("rejects duration exceeding 180 days", () => { + const invalid = { + lease: { + "resource-provider": "Microsoft.Test", + startdate: "2026-06-01", + "duration-days": "P200D", + reviewer: "John Doe", + }, + }; + expect(leaseSchema.safeParse(invalid).success).toBe(false); + }); - invalidDurations.forEach(duration => { - const match = duration.match(/^(\d+)\s*days?$/i); - if (match) { - const days = parseInt(match[1], 10); - assert.ok(days <= 0, `${duration} should be invalid (0 or negative)`); - } else { - assert.ok(!match, `${duration} should not match pattern`); - } - }); + it("rejects zero duration", () => { + const invalid = { + lease: { + "resource-provider": "Microsoft.Test", + startdate: "2026-06-01", + "duration-days": "P0D", + reviewer: "John Doe", + }, + }; + expect(leaseSchema.safeParse(invalid).success).toBe(false); }); - it('should reject duration exceeding 180 days', () => { - const excessiveDurations = ['181 days', '200 days', '365 days']; + it("rejects empty reviewer", () => { + const invalid = { + lease: { + "resource-provider": "Microsoft.Test", + startdate: "2026-06-01", + "duration-days": "P90D", + reviewer: "", + }, + }; + expect(leaseSchema.safeParse(invalid).success).toBe(false); + }); - excessiveDurations.forEach(duration => { - const match = duration.match(/^(\d+)\s*days?$/i); - assert.ok(match !== null, `${duration} should match pattern`); - - const days = parseInt(match[1], 10); - assert.ok(days > 180, `${duration} should exceed 180 days`); - }); + it("rejects missing lease key", () => { + expect(leaseSchema.safeParse({ notlease: {} }).success).toBe(false); }); }); - describe('Reviewer Validation', () => { - it('should accept non-empty reviewer names', () => { - const validReviewers = [ - 'John Doe', - 'Jane Smith', - 'PM Team' - ]; - - validReviewers.forEach(reviewer => { - assert.ok(reviewer && reviewer.trim() !== '', `${reviewer} should be valid`); - }); + describe("validateLeaseContent", () => { + const validYaml = `lease: + resource-provider: Microsoft.Test + startdate: "2027-06-01" + duration-days: P90D + reviewer: John Doe +`; + + it("validates a complete valid lease file", async () => { + mockAccess = vi.fn().mockResolvedValue(undefined); + mockReadFile = vi.fn().mockResolvedValue(validYaml); + + const result = await validateLeaseContent( + "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", + "2026-01-07", + ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", + ); + + expect(result.errors).toHaveLength(0); + expect(mockAccess).toHaveBeenCalledOnce(); + expect(mockReadFile).toHaveBeenCalledOnce(); }); - it('should reject empty or null reviewers', () => { - const invalidReviewers = [ - '', - ' ', - null, - undefined - ]; + it("detects resource provider mismatch", async () => { + const mismatchYaml = validYaml.replace("Microsoft.Test", "Microsoft.Other"); + mockAccess = vi.fn().mockResolvedValue(undefined); + mockReadFile = vi.fn().mockResolvedValue(mismatchYaml); + + const result = await validateLeaseContent( + "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", + "2026-01-07", + ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", + ); - invalidReviewers.forEach(reviewer => { - const isValid = reviewer && reviewer.trim() !== ''; - assert.ok(!isValid, `${reviewer} should be invalid`); - }); + expect(result.errors.some((e) => e.includes("mismatch"))).toBe(true); }); - }); - describe('Integration Tests', () => { - it('should validate complete valid lease file', () => { - const validLease = `lease: - resource-provider: Microsoft.Test - startdate: 2026-01-15 - duration: 90 days - reviewer: John Doe`; + it("detects past startdate", async () => { + const pastYaml = validYaml.replace("2027-06-01", "2025-01-01"); + mockAccess = vi.fn().mockResolvedValue(undefined); + mockReadFile = vi.fn().mockResolvedValue(pastYaml); - const leaseFile = createLeaseFile('testservice', 'Microsoft.Test', validLease); - assert.ok(existsSync(leaseFile), 'Lease file should be created'); - }); + const result = await validateLeaseContent( + "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", + "2026-01-07", + ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", + ); - it('should detect resource provider mismatch', () => { - const invalidLease = `lease: - resource-provider: Microsoft.Other - startdate: 2026-01-15 - duration: 90 days - reviewer: John Doe`; - - // Creating in Microsoft.Test folder but declaring Microsoft.Other - const leaseFile = createLeaseFile('testservice', 'Microsoft.Test', invalidLease); - const folderRP = 'Microsoft.Test'; - const fileRP = 'Microsoft.Other'; - - assert.notStrictEqual(folderRP, fileRP, 'Resource providers should not match'); + expect(result.errors.some((e) => e.includes("past"))).toBe(true); }); - it('should detect invalid startdate format', () => { - const invalidLease = `lease: - resource-provider: Microsoft.Test - startdate: 01-15-2026 - duration: 90 days - reviewer: John Doe`; - - const leaseFile = createLeaseFile('testservice', 'Microsoft.Test', invalidLease); - - const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; - const invalidDate = '01-15-2026'; - assert.ok(!DATE_PATTERN.test(invalidDate), 'Date format should be invalid'); - }); + it("returns error for non-existent file", async () => { + mockAccess = vi.fn().mockRejectedValue(new Error("ENOENT")); - it('should detect missing reviewer', () => { - const invalidLease = `lease: - resource-provider: Microsoft.Test - startdate: 2026-01-15 - duration: 90 days - reviewer: `; - - const leaseFile = createLeaseFile('testservice', 'Microsoft.Test', invalidLease); - const reviewer = ''; - - assert.ok(!reviewer || reviewer.trim() === '', 'Reviewer should be empty'); + const result = await validateLeaseContent( + "/nonexistent/lease.yaml", + "2026-01-07", + ".github/arm-leases/svc/NS/lease.yaml", + ); + + expect(result.errors.some((e) => e.includes("does not exist"))).toBe(true); }); - }); - describe('Allowed File Patterns', () => { - it('should allow files in arm-leases directory', () => { - const ALLOWED_FILE_PATTERNS = [ - /^\.github\/arm-leases\//, - /^\.github\/workflows\/validate-arm-leases\.yaml$/, - /^\.github\/workflows\/src\/validate-arm-leases\.js$/ - ]; + it("detects invalid YAML content", async () => { + mockAccess = vi.fn().mockResolvedValue(undefined); + mockReadFile = vi.fn().mockResolvedValue("invalid yaml content without structure"); - const allowedFiles = [ - '.github/arm-leases/testservice/Microsoft.Test/lease.yaml', - '.github/workflows/validate-arm-leases.yaml', - '.github/workflows/src/validate-arm-leases.js' - ]; + const result = await validateLeaseContent( + "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", + "2026-01-07", + ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", + ); - allowedFiles.forEach(file => { - const isAllowed = ALLOWED_FILE_PATTERNS.some(pattern => pattern.test(file)); - assert.ok(isAllowed, `${file} should be allowed`); - }); + expect(result.errors.length).toBeGreaterThan(0); }); - it('should reject files outside allowed patterns', () => { - const ALLOWED_FILE_PATTERNS = [ - /^\.github\/arm-leases\//, - /^\.github\/workflows\/validate-arm-leases\.yaml$/, - /^\.github\/workflows\/src\/validate-arm-leases\.js$/ - ]; + it("accepts today as startdate", async () => { + const todayYaml = validYaml.replace("2027-06-01", "2026-01-07"); + mockAccess = vi.fn().mockResolvedValue(undefined); + mockReadFile = vi.fn().mockResolvedValue(todayYaml); - const disallowedFiles = [ - 'specification/testservice/readme.md', - '.github/other-file.yaml', - 'arm-leases/testservice/Microsoft.Test/lease.yaml' - ]; + const result = await validateLeaseContent( + "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", + "2026-01-07", + ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", + ); - disallowedFiles.forEach(file => { - const isAllowed = ALLOWED_FILE_PATTERNS.some(pattern => pattern.test(file)); - assert.ok(!isAllowed, `${file} should be disallowed`); - }); + expect(result.errors).toHaveLength(0); }); }); }); diff --git a/.github/workflows/validate-arm-leases.yaml b/.github/workflows/validate-arm-leases.yaml index ac846a49d356..881946bb8a48 100644 --- a/.github/workflows/validate-arm-leases.yaml +++ b/.github/workflows/validate-arm-leases.yaml @@ -12,6 +12,9 @@ on: paths: # Only trigger if PR includes at least one changed ARM lease file - ".github/arm-leases/**" + # Also trigger on own sources + - ".github/workflows/validate-arm-leases.yaml" + - ".github/workflows/src/validate-arm-leases.js" permissions: contents: read @@ -25,21 +28,17 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 with: - fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 2 + sparse-checkout: | + .github - - name: Fetch base branch - run: git fetch origin ${{ github.event.pull_request.base.ref }} - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '24' - - - name: Install dependencies - run: npm ci - working-directory: .github + - name: Install dependencies for github-script actions + uses: ./.github/actions/install-deps-github-script - name: Validate ARM leases - run: node workflows/src/validate-arm-leases.js ${{ github.event.pull_request.base.ref }} - working-directory: .github \ No newline at end of file + uses: actions/github-script@v8 + with: + script: | + const { default: validateArmLeases } = + await import('${{ github.workspace }}/.github/workflows/src/validate-arm-leases.js'); + return await validateArmLeases(core); \ No newline at end of file From abb646ab37c350099e1833149f95dbd81f7d0e7f Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Fri, 13 Feb 2026 16:44:04 -0600 Subject: [PATCH 029/125] Updated duration-days and ISO format --- .github/workflows/src/detect-arm-leases.js | 6 ++--- .../workflows/test/detect-arm-leases.test.js | 22 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/src/detect-arm-leases.js b/.github/workflows/src/detect-arm-leases.js index 175ff263c2ee..4f8afcc6902b 100644 --- a/.github/workflows/src/detect-arm-leases.js +++ b/.github/workflows/src/detect-arm-leases.js @@ -11,13 +11,13 @@ import { getRootFolder } from '../../shared/src/simple-git.js'; * ```yaml * lease: * startdate: "2024-01-01" - * duration: "30 days" + * duration-days: "P30D" * ``` */ 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().regex(/^\d+\s*days?$/i, 'duration must be in format "N days"'), + 'duration-days': z.string().regex(/^P(\d+)D$/i, 'duration-days must be in ISO 8601 format (e.g. P180D)'), }), }); @@ -71,7 +71,7 @@ export async function getLeaseStatus(serviceName, resourceProvider, serviceGroup const parsed = leaseSchema.parse(rawParsed); const lease = parsed.lease; - const durationDays = parseInt(lease.duration.match(/^(\d+)\s*days?$/i)[1], 10); + const durationDays = parseInt(lease['duration-days'].match(/^P(\d+)D$/i)[1], 10); const endDate = new Date(lease.startdate); endDate.setDate(endDate.getDate() + durationDays); diff --git a/.github/workflows/test/detect-arm-leases.test.js b/.github/workflows/test/detect-arm-leases.test.js index ed9527d514a7..5691fdafbbd7 100644 --- a/.github/workflows/test/detect-arm-leases.test.js +++ b/.github/workflows/test/detect-arm-leases.test.js @@ -29,7 +29,7 @@ describe('detect-arm-leases', () => { const leaseContent = `lease: resource-provider: ${resourceProvider} startdate: ${startdate} - duration: ${duration} + duration-days: ${duration} reviewer: Test Reviewer `; @@ -51,7 +51,7 @@ describe('detect-arm-leases', () => { 'testservice', 'Microsoft.Test', startDate.toISOString().split('T')[0], - '90 days' + 'P90D' ); const result = await checkLease('testservice', 'Microsoft.Test'); @@ -67,7 +67,7 @@ describe('detect-arm-leases', () => { 'testservice', 'Microsoft.Test', startDate.toISOString().split('T')[0], - '90 days' + 'P90D' ); const result = await checkLease('testservice', 'Microsoft.Test'); @@ -83,7 +83,7 @@ describe('detect-arm-leases', () => { 'testservice', 'Microsoft.Test', startDate.toISOString().split('T')[0], - '90 days' + 'P90D' ); const result = await checkLease('testservice', 'Microsoft.Test'); @@ -99,14 +99,14 @@ describe('detect-arm-leases', () => { 'testservice', 'Microsoft.Test', startDate.toISOString().split('T')[0], - '90 days' + 'P90D' ); const result = await checkLease('testservice', 'Microsoft.Test'); expect(result).toBe(false); }); - it('handles different duration formats', async () => { + it('handles case-insensitive duration format', async () => { const today = new Date(); const startDate = new Date(today); startDate.setDate(today.getDate() - 10); @@ -115,7 +115,7 @@ describe('detect-arm-leases', () => { 'testservice', 'Microsoft.Test', startDate.toISOString().split('T')[0], - '180 Days' + 'P180d' ); const result = await checkLease('testservice', 'Microsoft.Test'); @@ -129,7 +129,7 @@ describe('detect-arm-leases', () => { 'testservice', 'Microsoft.Test', today.toISOString().split('T')[0], - '1 day' + 'P1D' ); const result = await checkLease('testservice', 'Microsoft.Test'); @@ -150,8 +150,8 @@ describe('detect-arm-leases', () => { const startDate = new Date(today); startDate.setDate(today.getDate() - 30); - createLeaseFile('app', 'Microsoft.App', startDate.toISOString().split('T')[0], '90 days'); - createLeaseFile('compute', 'Microsoft.Compute', startDate.toISOString().split('T')[0], '90 days'); + createLeaseFile('app', 'Microsoft.App', startDate.toISOString().split('T')[0], 'P90D'); + createLeaseFile('compute', 'Microsoft.Compute', startDate.toISOString().split('T')[0], 'P90D'); expect(await checkLease('app', 'Microsoft.App')).toBe(true); expect(await checkLease('compute', 'Microsoft.Compute')).toBe(true); @@ -167,7 +167,7 @@ describe('detect-arm-leases', () => { 'testservice', 'Microsoft.Test', futureDate.toISOString().split('T')[0], - '90 days' + 'P90D' ); const result = await checkLease('testservice', 'Microsoft.Test'); From bc22fc7362bfff8f588e727841e18d7f83b9b5c3 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Fri, 13 Feb 2026 17:02:00 -0600 Subject: [PATCH 030/125] Address - check failures --- .../testServicegroup/lease.yaml | 2 +- .github/workflows/src/validate-arm-leases.js | 2 +- .../test/validate-arm-leases.test.js | 42 +++++++++---------- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/.github/arm-leases/testservice1/Microsoft.TestRP1/testServicegroup/lease.yaml b/.github/arm-leases/testservice1/Microsoft.TestRP1/testServicegroup/lease.yaml index e30e996dfdec..bfe37d0f8587 100644 --- a/.github/arm-leases/testservice1/Microsoft.TestRP1/testServicegroup/lease.yaml +++ b/.github/arm-leases/testservice1/Microsoft.TestRP1/testServicegroup/lease.yaml @@ -1,5 +1,5 @@ lease: resource-provider: Microsoft.TestRP1 startdate: 2026-01-15 # ISO 8601 format (YYYY-MM-DD) - duration: 180 days # Maximum allowed is 180 days + duration-days: P180D # ISO 8601 duration (maximum P180D) reviewer: Tejaswi Salaigari \ No newline at end of file diff --git a/.github/workflows/src/validate-arm-leases.js b/.github/workflows/src/validate-arm-leases.js index 35b700b29a97..bc48a7bba74b 100644 --- a/.github/workflows/src/validate-arm-leases.js +++ b/.github/workflows/src/validate-arm-leases.js @@ -14,7 +14,7 @@ const ALLOWED_FILE_PATTERNS = [ ]; const LEASE_FILE_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/lease\.yaml$/; -const LEASE_FILE_WITH_GROUP_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/(?!stable|preview)([^\/]+)\/lease\.yaml$/; +const LEASE_FILE_WITH_GROUP_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/(?!stable|preview)([^/]+)\/lease\.yaml$/; /** * Zod schema for lease.yaml file content. diff --git a/.github/workflows/test/validate-arm-leases.test.js b/.github/workflows/test/validate-arm-leases.test.js index 9f891691ff5e..c7092d1ff53e 100644 --- a/.github/workflows/test/validate-arm-leases.test.js +++ b/.github/workflows/test/validate-arm-leases.test.js @@ -1,4 +1,13 @@ import { afterEach, describe, expect, it, vi } from "vitest"; + +// Mock fs/promises before imports +vi.mock("fs/promises", () => ({ access: vi.fn(), readFile: vi.fn() })); + +import * as fs from "fs/promises"; + +const mockAccess = /** @type {import("vitest").Mock} */ (fs.access); +const mockReadFile = /** @type {import("vitest").Mock} */ (fs.readFile); + import { isFileAllowed, validateFolderStructure, @@ -8,17 +17,6 @@ import { LEASE_FILE_WITH_GROUP_PATTERN, } from "../src/validate-arm-leases.js"; -/** @type {import("vitest").MockedFunction} */ -let mockAccess; - -/** @type {import("vitest").MockedFunction} */ -let mockReadFile; - -vi.mock("fs/promises", () => ({ - access: (...args) => mockAccess(...args), - readFile: (...args) => mockReadFile(...args), -})); - describe("validate-arm-leases", () => { afterEach(() => { vi.clearAllMocks(); @@ -191,8 +189,8 @@ describe("validate-arm-leases", () => { `; it("validates a complete valid lease file", async () => { - mockAccess = vi.fn().mockResolvedValue(undefined); - mockReadFile = vi.fn().mockResolvedValue(validYaml); + mockAccess.mockResolvedValue(undefined); + mockReadFile.mockResolvedValue(validYaml); const result = await validateLeaseContent( "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", @@ -207,8 +205,8 @@ describe("validate-arm-leases", () => { it("detects resource provider mismatch", async () => { const mismatchYaml = validYaml.replace("Microsoft.Test", "Microsoft.Other"); - mockAccess = vi.fn().mockResolvedValue(undefined); - mockReadFile = vi.fn().mockResolvedValue(mismatchYaml); + mockAccess.mockResolvedValue(undefined); + mockReadFile.mockResolvedValue(mismatchYaml); const result = await validateLeaseContent( "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", @@ -221,8 +219,8 @@ describe("validate-arm-leases", () => { it("detects past startdate", async () => { const pastYaml = validYaml.replace("2027-06-01", "2025-01-01"); - mockAccess = vi.fn().mockResolvedValue(undefined); - mockReadFile = vi.fn().mockResolvedValue(pastYaml); + mockAccess.mockResolvedValue(undefined); + mockReadFile.mockResolvedValue(pastYaml); const result = await validateLeaseContent( "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", @@ -234,7 +232,7 @@ describe("validate-arm-leases", () => { }); it("returns error for non-existent file", async () => { - mockAccess = vi.fn().mockRejectedValue(new Error("ENOENT")); + mockAccess.mockRejectedValue(new Error("ENOENT")); const result = await validateLeaseContent( "/nonexistent/lease.yaml", @@ -246,8 +244,8 @@ describe("validate-arm-leases", () => { }); it("detects invalid YAML content", async () => { - mockAccess = vi.fn().mockResolvedValue(undefined); - mockReadFile = vi.fn().mockResolvedValue("invalid yaml content without structure"); + mockAccess.mockResolvedValue(undefined); + mockReadFile.mockResolvedValue("invalid yaml content without structure"); const result = await validateLeaseContent( "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", @@ -260,8 +258,8 @@ describe("validate-arm-leases", () => { it("accepts today as startdate", async () => { const todayYaml = validYaml.replace("2027-06-01", "2026-01-07"); - mockAccess = vi.fn().mockResolvedValue(undefined); - mockReadFile = vi.fn().mockResolvedValue(todayYaml); + mockAccess.mockResolvedValue(undefined); + mockReadFile.mockResolvedValue(todayYaml); const result = await validateLeaseContent( "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", From b6cad03cf16ce460398d09bd5045077b2913c56a Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Fri, 13 Feb 2026 17:26:55 -0600 Subject: [PATCH 031/125] Address - check failures --- .github/workflows/src/validate-arm-leases.js | 22 +++++++------- .../test/validate-arm-leases.test.js | 29 ++++++++++--------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/.github/workflows/src/validate-arm-leases.js b/.github/workflows/src/validate-arm-leases.js index bc48a7bba74b..7acb9b7d2e66 100644 --- a/.github/workflows/src/validate-arm-leases.js +++ b/.github/workflows/src/validate-arm-leases.js @@ -9,13 +9,15 @@ import { CoreLogger } from './core-logger.js'; // Configuration // ============================================ -const ALLOWED_FILE_PATTERNS = [ - /^\.github\/arm-leases\//, -]; - const LEASE_FILE_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/lease\.yaml$/; const LEASE_FILE_WITH_GROUP_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/(?!stable|preview)([^/]+)\/lease\.yaml$/; +const ALLOWED_FILE_PATTERNS = [ + LEASE_FILE_PATTERN, + LEASE_FILE_WITH_GROUP_PATTERN, + /^\.github\/arm-leases\/README\.md$/, +]; + /** * Zod schema for lease.yaml file content. * @@ -58,18 +60,18 @@ const leaseSchema = z.object({ /** * Check if a file is allowed based on patterns * @param {string} file - File path to check - * @returns {Promise} True if file is allowed + * @returns {boolean} True if file is allowed */ -async function isFileAllowed(file) { +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 {Promise} Array of invalid files + * @returns {string[]} Array of invalid files */ -async function validateFolderStructure(files) { +function validateFolderStructure(files) { return files.filter(file => !LEASE_FILE_PATTERN.test(file) && !LEASE_FILE_WITH_GROUP_PATTERN.test(file)); } @@ -163,7 +165,7 @@ export default async function validateArmLeases(core) { // Step 2: Check for disallowed files const disallowedFiles = []; for (const file of allChangedFiles) { - if (!(await isFileAllowed(file))) { + if (!isFileAllowed(file)) { disallowedFiles.push(file); } } @@ -206,7 +208,7 @@ export default async function validateArmLeases(core) { } // Step 5: Validate folder structure - const invalidStructure = await validateFolderStructure(armLeaseFiles); + const invalidStructure = validateFolderStructure(armLeaseFiles); if (invalidStructure.length > 0) { core.info(`${invalidStructure.length} file(s) with invalid folder structure:`); diff --git a/.github/workflows/test/validate-arm-leases.test.js b/.github/workflows/test/validate-arm-leases.test.js index c7092d1ff53e..a88f9c869971 100644 --- a/.github/workflows/test/validate-arm-leases.test.js +++ b/.github/workflows/test/validate-arm-leases.test.js @@ -23,46 +23,47 @@ describe("validate-arm-leases", () => { }); describe("isFileAllowed", () => { - it("allows files in arm-leases directory", async () => { - await expect(isFileAllowed(".github/arm-leases/testservice/Microsoft.Test/lease.yaml")).resolves.toBe(true); - await expect(isFileAllowed(".github/arm-leases/anything/here")).resolves.toBe(true); + 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 files outside arm-leases directory", async () => { - await expect(isFileAllowed("specification/testservice/readme.md")).resolves.toBe(false); - await expect(isFileAllowed(".github/other-file.yaml")).resolves.toBe(false); - await expect(isFileAllowed(".github/workflows/validate-arm-leases.yaml")).resolves.toBe(false); - await expect(isFileAllowed("arm-leases/testservice/Microsoft.Test/lease.yaml")).resolves.toBe(false); + it("rejects files that do not match allowed patterns", () => { + 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/bad.test/noyaml.md")).toBe(false); + expect(isFileAllowed(".github/arm-leases/testservice/readme.md")).toBe(false); }); }); describe("validateFolderStructure", () => { - it("accepts valid lease file paths", async () => { + it("accepts valid lease file paths", () => { const validFiles = [ ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", ".github/arm-leases/widgetservice/Azure.Widget/lease.yaml", ".github/arm-leases/compute/Microsoft.Compute/DiskRP/lease.yaml", ]; - await expect(validateFolderStructure(validFiles)).resolves.toHaveLength(0); + expect(validateFolderStructure(validFiles)).toHaveLength(0); }); - it("rejects invalid folder structures", async () => { + it("rejects invalid folder structures", () => { const invalidFiles = [ ".github/arm-leases/TestService/Microsoft.Test/lease.yaml", ".github/arm-leases/test-service/Microsoft.Test/lease.yaml", ".github/arm-leases/testservice/Microsoft.Test/other.yaml", ]; - await expect(validateFolderStructure(invalidFiles)).resolves.toHaveLength(invalidFiles.length); + expect(validateFolderStructure(invalidFiles)).toHaveLength(invalidFiles.length); }); - it("rejects stable or preview as service group", async () => { + it("rejects stable or preview as service group", () => { const invalidFiles = [ ".github/arm-leases/compute/Microsoft.Compute/stable/lease.yaml", ".github/arm-leases/compute/Microsoft.Compute/preview/lease.yaml", ".github/arm-leases/compute/Microsoft.Compute/stable-2024-01-01/lease.yaml", ".github/arm-leases/compute/Microsoft.Compute/preview-internal/lease.yaml", ]; - await expect(validateFolderStructure(invalidFiles)).resolves.toHaveLength(invalidFiles.length); + expect(validateFolderStructure(invalidFiles)).toHaveLength(invalidFiles.length); }); }); From b537b9c8620096bbbbc98e791d5e1339586b2c28 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Tue, 17 Feb 2026 14:13:33 -0600 Subject: [PATCH 032/125] modified workflow name and removed test lease files, updated readme with more steps --- .github/arm-leases/README.md | 13 ++++++++++++- .github/arm-leases/badtest/bad.test/noyaml.md | 5 ----- .../testservice/Microsoft.TestRP/lease.yaml | 2 +- .../Microsoft.TestRP1/testServicegroup/lease.yaml | 5 ----- ...rm-leases.yaml => arm-lease-validation-pms.yaml} | 2 +- 5 files changed, 14 insertions(+), 13 deletions(-) delete mode 100644 .github/arm-leases/badtest/bad.test/noyaml.md delete mode 100644 .github/arm-leases/testservice1/Microsoft.TestRP1/testServicegroup/lease.yaml rename .github/workflows/{validate-arm-leases.yaml => arm-lease-validation-pms.yaml} (96%) diff --git a/.github/arm-leases/README.md b/.github/arm-leases/README.md index a847d337f5a9..6200e10c51cf 100644 --- a/.github/arm-leases/README.md +++ b/.github/arm-leases/README.md @@ -42,7 +42,7 @@ lease: ### Copy-Paste Template -Create a file at `.github/arm-leases///lease.yaml` with the following content (replace the placeholder values): +Create a file at `.github/arm-leases///(optional)/lease.yaml` with the following content (replace the placeholder values): ```yaml lease: @@ -80,3 +80,14 @@ All lease files are automatically validated with the following requirements: - Must contain the name of the PM who approved the lease - Only PMs can be listed as reviewers +## Troubleshooting + +If your PR check **"ARM Lease Validation - PMs as reviewer"** 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 service name +- **Resource provider mismatch**: The `resource-provider` value must match the namespace folder name exactly +- **Past start date**: The `startdate` must be today or a future date in `YYYY-MM-DD` format +- **Invalid duration**: Use ISO 8601 format `P{n}D` (e.g., `P180D`), maximum `P180D` +- **Missing or empty fields**: All fields (`resource-provider`, `startdate`, `duration-days`, `reviewer`) are required +- **Disallowed files**: Only `lease.yaml` and `README.md` files are permitted in `.github/arm-leases/` + diff --git a/.github/arm-leases/badtest/bad.test/noyaml.md b/.github/arm-leases/badtest/bad.test/noyaml.md deleted file mode 100644 index bc3dcfa4839a..000000000000 --- a/.github/arm-leases/badtest/bad.test/noyaml.md +++ /dev/null @@ -1,5 +0,0 @@ -lease: - resource-provider: Microsoft.TestRP - startdate: 2026-07-01 # ISO 8601 format (YYYY-MM-DD) - duration: 180 days # Maximum allowed is 180 days - reviewer: Tejaswi Salaigari \ No newline at end of file diff --git a/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml b/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml index 70906149904f..9647ac1de26c 100644 --- a/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml +++ b/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml @@ -1,5 +1,5 @@ lease: resource-provider: Microsoft.TestRP - startdate: 2026-02-15 # ISO 8601 format (YYYY-MM-DD) + startdate: 2026-07-15 # ISO 8601 format (YYYY-MM-DD) duration-days: P180D # ISO 8601 duration (maximum P180D) reviewer: Tejaswi Salaigari diff --git a/.github/arm-leases/testservice1/Microsoft.TestRP1/testServicegroup/lease.yaml b/.github/arm-leases/testservice1/Microsoft.TestRP1/testServicegroup/lease.yaml deleted file mode 100644 index bfe37d0f8587..000000000000 --- a/.github/arm-leases/testservice1/Microsoft.TestRP1/testServicegroup/lease.yaml +++ /dev/null @@ -1,5 +0,0 @@ -lease: - resource-provider: Microsoft.TestRP1 - startdate: 2026-01-15 # ISO 8601 format (YYYY-MM-DD) - duration-days: P180D # ISO 8601 duration (maximum P180D) - reviewer: Tejaswi Salaigari \ No newline at end of file diff --git a/.github/workflows/validate-arm-leases.yaml b/.github/workflows/arm-lease-validation-pms.yaml similarity index 96% rename from .github/workflows/validate-arm-leases.yaml rename to .github/workflows/arm-lease-validation-pms.yaml index 881946bb8a48..8e5412d6df9d 100644 --- a/.github/workflows/validate-arm-leases.yaml +++ b/.github/workflows/arm-lease-validation-pms.yaml @@ -1,4 +1,4 @@ -name: Validate ARM Leases +name: ARM Lease Validation - PMs as reviewer on: pull_request: From 0397e0f358d692edb05c62d4c8997b7d265794dc Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Feb 2026 23:19:24 -0800 Subject: [PATCH 033/125] Update .github/workflows/arm-lease-validation-pms.yaml --- .github/workflows/arm-lease-validation-pms.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/arm-lease-validation-pms.yaml b/.github/workflows/arm-lease-validation-pms.yaml index 8e5412d6df9d..a415caa31312 100644 --- a/.github/workflows/arm-lease-validation-pms.yaml +++ b/.github/workflows/arm-lease-validation-pms.yaml @@ -41,4 +41,4 @@ jobs: script: | const { default: validateArmLeases } = await import('${{ github.workspace }}/.github/workflows/src/validate-arm-leases.js'); - return await validateArmLeases(core); \ No newline at end of file + return await validateArmLeases(core); From 1f4927c6b90b4418c8680a4f0ab08772d1944b8f Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Tue, 3 Mar 2026 12:09:09 -0600 Subject: [PATCH 034/125] Resolved comments --- .github/arm-leases/README.md | 2 +- ...ion-pms.yaml => arm-lease-validation.yaml} | 10 ++--- .../arm-lease-validation.js} | 43 +++++++++---------- .../arm-lease-validation.test.js} | 2 +- 4 files changed, 27 insertions(+), 30 deletions(-) rename .github/workflows/{arm-lease-validation-pms.yaml => arm-lease-validation.yaml} (79%) rename .github/workflows/src/{validate-arm-leases.js => arm-lease-validation/arm-lease-validation.js} (90%) rename .github/workflows/test/{validate-arm-leases.test.js => arm-lease-validation/arm-lease-validation.test.js} (99%) diff --git a/.github/arm-leases/README.md b/.github/arm-leases/README.md index 6200e10c51cf..7bfc847ddaf3 100644 --- a/.github/arm-leases/README.md +++ b/.github/arm-leases/README.md @@ -82,7 +82,7 @@ All lease files are automatically validated with the following requirements: ## Troubleshooting -If your PR check **"ARM Lease Validation - PMs as reviewer"** is failing, review the error messages in the check output and fix the issues in your `lease.yaml` file. Common causes include: +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 service name - **Resource provider mismatch**: The `resource-provider` value must match the namespace folder name exactly diff --git a/.github/workflows/arm-lease-validation-pms.yaml b/.github/workflows/arm-lease-validation.yaml similarity index 79% rename from .github/workflows/arm-lease-validation-pms.yaml rename to .github/workflows/arm-lease-validation.yaml index 8e5412d6df9d..e8bebf65c5e4 100644 --- a/.github/workflows/arm-lease-validation-pms.yaml +++ b/.github/workflows/arm-lease-validation.yaml @@ -1,4 +1,4 @@ -name: ARM Lease Validation - PMs as reviewer +name: ARM Lease Validation on: pull_request: @@ -13,8 +13,8 @@ on: # Only trigger if PR includes at least one changed ARM lease file - ".github/arm-leases/**" # Also trigger on own sources - - ".github/workflows/validate-arm-leases.yaml" - - ".github/workflows/src/validate-arm-leases.js" + - ".github/workflows/arm-lease-validation-pms.yaml" + - ".github/workflows/src/arm-lease-validation/**" permissions: contents: read @@ -40,5 +40,5 @@ jobs: with: script: | const { default: validateArmLeases } = - await import('${{ github.workspace }}/.github/workflows/src/validate-arm-leases.js'); - return await validateArmLeases(core); \ No newline at end of file + await import('${{ github.workspace }}/.github/workflows/src/arm-lease-validation/arm-lease-validation.js'); + return await validateArmLeases(core); diff --git a/.github/workflows/src/validate-arm-leases.js b/.github/workflows/src/arm-lease-validation/arm-lease-validation.js similarity index 90% rename from .github/workflows/src/validate-arm-leases.js rename to .github/workflows/src/arm-lease-validation/arm-lease-validation.js index 7acb9b7d2e66..63b0437b07f7 100644 --- a/.github/workflows/src/validate-arm-leases.js +++ b/.github/workflows/src/arm-lease-validation/arm-lease-validation.js @@ -1,18 +1,18 @@ import { readFile, access } from 'fs/promises'; -import { join } from 'path'; +import { resolve } from 'path'; import YAML from 'js-yaml'; import * as z from 'zod'; -import { getChangedFiles } from '../../shared/src/changed-files.js'; -import { CoreLogger } from './core-logger.js'; +import { getChangedFiles } from '../../../shared/src/changed-files.js'; +import { CoreLogger } from '../core-logger.js'; // ============================================ // Configuration // ============================================ -const LEASE_FILE_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/lease\.yaml$/; -const LEASE_FILE_WITH_GROUP_PATTERN = /^\.github\/arm-leases\/[a-z0-9]+\/[a-zA-Z0-9.]+\/(?!stable|preview)([^/]+)\/lease\.yaml$/; +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$/; -const ALLOWED_FILE_PATTERNS = [ +export const ALLOWED_FILE_PATTERNS = [ LEASE_FILE_PATTERN, LEASE_FILE_WITH_GROUP_PATTERN, /^\.github\/arm-leases\/README\.md$/, @@ -30,7 +30,7 @@ const ALLOWED_FILE_PATTERNS = [ * reviewer: alias * ``` */ -const leaseSchema = z.object({ +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)), @@ -62,7 +62,7 @@ const leaseSchema = z.object({ * @param {string} file - File path to check * @returns {boolean} True if file is allowed */ -function isFileAllowed(file) { +export function isFileAllowed(file) { return ALLOWED_FILE_PATTERNS.some(pattern => pattern.test(file)); } @@ -71,7 +71,7 @@ function isFileAllowed(file) { * @param {string[]} files - Array of file paths * @returns {string[]} Array of invalid files */ -function validateFolderStructure(files) { +export function validateFolderStructure(files) { return files.filter(file => !LEASE_FILE_PATTERN.test(file) && !LEASE_FILE_WITH_GROUP_PATTERN.test(file)); } @@ -82,7 +82,7 @@ function validateFolderStructure(files) { * @param {string} relativePath - Relative path for folder name extraction * @returns {Promise<{file: string, errors: string[]}>} Validation result with errors array */ -async function validateLeaseContent(leaseFile, today, relativePath) { +export async function validateLeaseContent(leaseFile, today, relativePath) { const errors = []; const pathForExtraction = relativePath || leaseFile; // Extract namespace from .github/arm-leases///lease.yaml @@ -136,9 +136,6 @@ async function validateLeaseContent(leaseFile, today, relativePath) { return { file: leaseFile, errors }; } -export { isFileAllowed, validateFolderStructure, validateLeaseContent, leaseSchema, - ALLOWED_FILE_PATTERNS, LEASE_FILE_PATTERN, LEASE_FILE_WITH_GROUP_PATTERN }; - // ============================================ // Main Validation Logic // ============================================ @@ -169,7 +166,7 @@ export default async function validateArmLeases(core) { disallowedFiles.push(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:\n`); core.info('Disallowed files:'); @@ -182,10 +179,10 @@ export default async function validateArmLeases(core) { } // Step 3: Check for non-lease.yaml and non-README files - const nonLeaseFiles = allChangedFiles.filter(file => + 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:`); nonLeaseFiles.forEach(file => core.info(`Remove or rename - ${file}`)); @@ -194,7 +191,7 @@ export default async function validateArmLeases(core) { } // Step 4: Get ARM lease files (only lease.yaml files) - const armLeaseFiles = allChangedFiles.filter(file => + const armLeaseFiles = allChangedFiles.filter(file => file.startsWith('.github/arm-leases/') && !file.endsWith('.md') ); @@ -209,7 +206,7 @@ export default async function validateArmLeases(core) { // Step 5: Validate folder structure const invalidStructure = validateFolderStructure(armLeaseFiles); - + if (invalidStructure.length > 0) { core.info(`${invalidStructure.length} file(s) with invalid folder structure:`); invalidStructure.forEach(file => core.info(` ${file}`)); @@ -227,10 +224,10 @@ export default async function validateArmLeases(core) { } // Step 6: Validate lease file contents - const validLeaseFiles = armLeaseFiles.filter(file => + const validLeaseFiles = armLeaseFiles.filter(file => LEASE_FILE_PATTERN.test(file) || LEASE_FILE_WITH_GROUP_PATTERN.test(file) ); - + if (validLeaseFiles.length === 0) { if (hasErrors) { core.setFailed('ARM Lease Validation failed - fix errors above'); @@ -241,9 +238,9 @@ export default async function validateArmLeases(core) { } const contentErrors = []; - + for (const leaseFile of validLeaseFiles) { - const fullPath = join(cwd, leaseFile); + const fullPath = resolve(cwd, leaseFile); const result = await validateLeaseContent(fullPath, today, leaseFile); if (result.errors.length > 0) { contentErrors.push({ file: leaseFile, errors: result.errors }); @@ -268,4 +265,4 @@ export default async function validateArmLeases(core) { } return { status: hasErrors ? 'failed' : 'passed', errors: contentErrors.length }; -} \ No newline at end of file +} diff --git a/.github/workflows/test/validate-arm-leases.test.js b/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js similarity index 99% rename from .github/workflows/test/validate-arm-leases.test.js rename to .github/workflows/test/arm-lease-validation/arm-lease-validation.test.js index a88f9c869971..b5d09d309b5e 100644 --- a/.github/workflows/test/validate-arm-leases.test.js +++ b/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js @@ -15,7 +15,7 @@ import { leaseSchema, LEASE_FILE_PATTERN, LEASE_FILE_WITH_GROUP_PATTERN, -} from "../src/validate-arm-leases.js"; +} from "../../src/arm-lease-validation/arm-lease-validation.js"; describe("validate-arm-leases", () => { afterEach(() => { From 3bf678a11231612a4d07675e9e3c781e61c0d3c3 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Tue, 3 Mar 2026 13:22:08 -0600 Subject: [PATCH 035/125] Address comments after merge --- .github/workflows/arm-lease-validation.yaml | 4 ++-- .../arm-lease-validation/arm-lease-validation.js | 15 ++++----------- .../arm-lease-validation.test.js | 13 +++---------- 3 files changed, 9 insertions(+), 23 deletions(-) diff --git a/.github/workflows/arm-lease-validation.yaml b/.github/workflows/arm-lease-validation.yaml index f4eed87b6511..35510fc8bc97 100644 --- a/.github/workflows/arm-lease-validation.yaml +++ b/.github/workflows/arm-lease-validation.yaml @@ -20,7 +20,7 @@ permissions: contents: read jobs: - validate-arm-leases: + arm-lease-validation: name: ARM Lease Validation runs-on: ubuntu-slim @@ -35,7 +35,7 @@ jobs: - name: Install dependencies for github-script actions uses: ./.github/actions/install-deps-github-script - - name: Validate ARM leases + - name: ARM Lease Validation uses: actions/github-script@v8 with: script: | diff --git a/.github/workflows/src/arm-lease-validation/arm-lease-validation.js b/.github/workflows/src/arm-lease-validation/arm-lease-validation.js index 63b0437b07f7..496b6668ee0e 100644 --- a/.github/workflows/src/arm-lease-validation/arm-lease-validation.js +++ b/.github/workflows/src/arm-lease-validation/arm-lease-validation.js @@ -1,5 +1,6 @@ -import { readFile, access } from 'fs/promises'; +import { readFile } from 'fs/promises'; import { resolve } from 'path'; +import { inspect } from 'util'; import YAML from 'js-yaml'; import * as z from 'zod'; import { getChangedFiles } from '../../../shared/src/changed-files.js'; @@ -89,18 +90,11 @@ export async function validateLeaseContent(leaseFile, today, relativePath) { // or .github/arm-leases////lease.yaml const folderRP = pathForExtraction.split('/')[3]; // namespace is always at index 3 - try { - await access(leaseFile); - } catch { - return { file: leaseFile, errors: ['File does not exist'] }; - } - let content; try { content = await readFile(leaseFile, 'utf-8'); } catch (error) { - const msg = error instanceof Error ? error.message : String(error); - return { file: leaseFile, errors: [`Error reading file: ${msg}`] }; + return { file: leaseFile, errors: [`Error reading file: ${inspect(error)}`] }; } // Use FAILSAFE_SCHEMA to keep all values as strings (prevents YAML Date auto-parsing) @@ -108,8 +102,7 @@ export async function validateLeaseContent(leaseFile, today, relativePath) { try { raw = /** @type {any} */ (YAML.load(content, { schema: YAML.FAILSAFE_SCHEMA })); } catch (error) { - const msg = error instanceof Error ? error.message : String(error); - return { file: leaseFile, errors: [`Invalid YAML: ${msg}`] }; + return { file: leaseFile, errors: [`Invalid YAML: ${inspect(error)}`] }; } // Parse with Zod schema — collects all field-level errors at once 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 index b5d09d309b5e..7a531d8c9555 100644 --- a/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js +++ b/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js @@ -1,11 +1,10 @@ import { afterEach, describe, expect, it, vi } from "vitest"; // Mock fs/promises before imports -vi.mock("fs/promises", () => ({ access: vi.fn(), readFile: vi.fn() })); +vi.mock("fs/promises", () => ({ readFile: vi.fn() })); import * as fs from "fs/promises"; -const mockAccess = /** @type {import("vitest").Mock} */ (fs.access); const mockReadFile = /** @type {import("vitest").Mock} */ (fs.readFile); import { @@ -190,7 +189,6 @@ describe("validate-arm-leases", () => { `; it("validates a complete valid lease file", async () => { - mockAccess.mockResolvedValue(undefined); mockReadFile.mockResolvedValue(validYaml); const result = await validateLeaseContent( @@ -200,13 +198,11 @@ describe("validate-arm-leases", () => { ); expect(result.errors).toHaveLength(0); - expect(mockAccess).toHaveBeenCalledOnce(); expect(mockReadFile).toHaveBeenCalledOnce(); }); it("detects resource provider mismatch", async () => { const mismatchYaml = validYaml.replace("Microsoft.Test", "Microsoft.Other"); - mockAccess.mockResolvedValue(undefined); mockReadFile.mockResolvedValue(mismatchYaml); const result = await validateLeaseContent( @@ -220,7 +216,6 @@ describe("validate-arm-leases", () => { it("detects past startdate", async () => { const pastYaml = validYaml.replace("2027-06-01", "2025-01-01"); - mockAccess.mockResolvedValue(undefined); mockReadFile.mockResolvedValue(pastYaml); const result = await validateLeaseContent( @@ -233,7 +228,7 @@ describe("validate-arm-leases", () => { }); it("returns error for non-existent file", async () => { - mockAccess.mockRejectedValue(new Error("ENOENT")); + mockReadFile.mockRejectedValue(new Error("ENOENT: no such file or directory")); const result = await validateLeaseContent( "/nonexistent/lease.yaml", @@ -241,11 +236,10 @@ describe("validate-arm-leases", () => { ".github/arm-leases/svc/NS/lease.yaml", ); - expect(result.errors.some((e) => e.includes("does not exist"))).toBe(true); + expect(result.errors.some((e) => e.includes("Error reading file"))).toBe(true); }); it("detects invalid YAML content", async () => { - mockAccess.mockResolvedValue(undefined); mockReadFile.mockResolvedValue("invalid yaml content without structure"); const result = await validateLeaseContent( @@ -259,7 +253,6 @@ describe("validate-arm-leases", () => { it("accepts today as startdate", async () => { const todayYaml = validYaml.replace("2027-06-01", "2026-01-07"); - mockAccess.mockResolvedValue(undefined); mockReadFile.mockResolvedValue(todayYaml); const result = await validateLeaseContent( From dc26c58f4ff3bb24ebaef2050b7cb48b21ab98c8 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 4 Mar 2026 01:29:38 -0600 Subject: [PATCH 036/125] adressed all latest comments and moved to arm-lease-validation folder --- .github/workflows/arm-lease-validation.yaml | 30 +- .github/workflows/package.json | 5 + .../arm-lease-validation-labels.js | 10 + .../arm-lease-validation/detect-arm-leases.js | 115 ++++++ .../detect-new-resource-provider.js | 323 ++++++++++++++++ .../detect-new-resource-types.js | 206 ++++++++++ .../src/detect-new-resource-provider.js | 287 -------------- .../detect-arm-leases.test.js | 169 +++++++++ .../detect-new-resource-provider.test.js | 253 +++++++++++++ .../detect-new-resource-types.test.js | 301 +++++++++++++++ .../test/detect-new-resource-provider.test.js | 355 ------------------ 11 files changed, 1404 insertions(+), 650 deletions(-) create mode 100644 .github/workflows/package.json create mode 100644 .github/workflows/src/arm-lease-validation/arm-lease-validation-labels.js create mode 100644 .github/workflows/src/arm-lease-validation/detect-arm-leases.js create mode 100644 .github/workflows/src/arm-lease-validation/detect-new-resource-provider.js create mode 100644 .github/workflows/src/arm-lease-validation/detect-new-resource-types.js delete mode 100644 .github/workflows/src/detect-new-resource-provider.js create mode 100644 .github/workflows/test/arm-lease-validation/detect-arm-leases.test.js create mode 100644 .github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js create mode 100644 .github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js delete mode 100644 .github/workflows/test/detect-new-resource-provider.test.js diff --git a/.github/workflows/arm-lease-validation.yaml b/.github/workflows/arm-lease-validation.yaml index c0d32bdced42..2a79f5e6b55c 100644 --- a/.github/workflows/arm-lease-validation.yaml +++ b/.github/workflows/arm-lease-validation.yaml @@ -1,4 +1,4 @@ -name: ARM Lease Validation +name: ARM Modeling Review on: pull_request: @@ -19,9 +19,9 @@ permissions: contents: read jobs: - detect-new-rp: - name: Detect New Resource Provider - runs-on: ubuntu-24.04 + arm-modeling-review: + name: ARM Modeling Review + runs-on: ubuntu-slim # TODO: Set to true to enable new RP detection if: false @@ -36,14 +36,14 @@ jobs: - name: Install dependencies for github-script actions uses: ./.github/actions/install-deps-github-script - - name: Detect new resource providers + - name: ARM Modeling Review id: detect uses: actions/github-script@v8 with: script: | - const { default: detectNewResourceProvider } = - await import('${{ github.workspace }}/.github/workflows/src/detect-new-resource-provider.js'); - return await detectNewResourceProvider({ context, core }); + const { default: armModelingReview } = + await import('${{ github.workspace }}/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js'); + return await armModelingReview({ context, core }); - if: fromJson(steps.detect.outputs.result).labelActions['ARMModelingReviewRequired'] != 'none' name: Upload artifact for ARMModelingReviewRequired label @@ -51,3 +51,17 @@ jobs: with: name: "ARMModelingReviewRequired" value: "${{ fromJson(steps.detect.outputs.result).labelActions['ARMModelingReviewRequired'] == 'add' }}" + + - if: fromJson(steps.detect.outputs.result).labelActions['ARMModelingSignedOff'] != 'none' + name: Upload artifact for ARMModelingSignedOff label + uses: ./.github/actions/add-label-artifact + with: + name: "ARMModelingSignedOff" + value: "${{ fromJson(steps.detect.outputs.result).labelActions['ARMModelingSignedOff'] == 'add' }}" + + - if: fromJson(steps.detect.outputs.result).labelActions['ARMModelingAutoSignedOff'] != 'none' + name: Upload artifact for ARMModelingAutoSignedOff label + uses: ./.github/actions/add-label-artifact + with: + name: "ARMModelingAutoSignedOff" + value: "${{ fromJson(steps.detect.outputs.result).labelActions['ARMModelingAutoSignedOff'] == 'add' }}" diff --git a/.github/workflows/package.json b/.github/workflows/package.json new file mode 100644 index 000000000000..340ac5c3c8c1 --- /dev/null +++ b/.github/workflows/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@js-temporal/polyfill": "^0.5.1" + } +} diff --git a/.github/workflows/src/arm-lease-validation/arm-lease-validation-labels.js b/.github/workflows/src/arm-lease-validation/arm-lease-validation-labels.js new file mode 100644 index 000000000000..1b3ccb1b8c45 --- /dev/null +++ b/.github/workflows/src/arm-lease-validation/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-lease-validation/detect-arm-leases.js b/.github/workflows/src/arm-lease-validation/detect-arm-leases.js new file mode 100644 index 000000000000..96e647d5a0bc --- /dev/null +++ b/.github/workflows/src/arm-lease-validation/detect-arm-leases.js @@ -0,0 +1,115 @@ +import { readFile } from "fs/promises"; +import { resolve } from "path"; +import yaml from "js-yaml"; +import * as z from "zod"; +import { Temporal } from "@js-temporal/polyfill"; +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}` }; + } + + 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-lease-validation/detect-new-resource-provider.js b/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js new file mode 100644 index 000000000000..71f8b7ed7fa5 --- /dev/null +++ b/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js @@ -0,0 +1,323 @@ +import { simpleGit } from "simple-git"; +import { + getChangedFiles, + resourceManager, + swagger, +} from "../../../shared/src/changed-files.js"; +import { checkLease } from "./detect-arm-leases.js"; +import { detectNewResourceTypes } from "./detect-new-resource-types.js"; +import { CoreLogger } from "../core-logger.js"; +import { LabelAction } from "../label.js"; +import { ArmLeaseValidationLabel } from "./arm-lease-validation-labels.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 = /^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 = + /^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['context']} params.context + * @param {import('@actions/github-script').AsyncFunctionArguments['core']} params.core + * @returns {Promise<{ status: string, labelActions: ManagedLabelActions }>} + */ +export default async function detectNewResourceProvider({ context, core }) { + const repoRoot = process.env.GITHUB_WORKSPACE ?? process.cwd(); + const git = simpleGit(repoRoot); + + core.info("Detecting New Resource Providers"); + + const options = { + cwd: process.env.GITHUB_WORKSPACE, + paths: ["specification"], + logger: new CoreLogger(core), + }; + + 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, match.index + match[0].length - 1); + } + return null; + }) + .filter(Boolean), + ); + + 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(repoRoot, 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(repoRoot, 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."); + } + + return { + status: allLeasesValid ? "new-rp-all-leases-valid" : "new-rp-invalid-lease", + labelActions: getLabelActions(allLeasesValid ? "auto-signed-off" : "review-required"), + }; +} + +/** + * Check for new resource types in existing RPs and validate their leases. + * + * @param {string} repoRoot - Repository root directory + * @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(repoRoot, rmFiles, core) { + const newRtResults = await detectNewResourceTypes({ + repoRoot, + mergeBase: "HEAD^", + 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} namespace(s)`); + + let allLeasesValid = true; + for (const ns of newRtResults) { + const leaseValid = await checkLease(ns.orgName, ns.namespace, ""); + + if (leaseValid) { + core.info(` - ${ns.namespace}: valid ARM lease for new resource types`); + } else { + allLeasesValid = false; + core.error( + `${ns.namespace}: 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-lease-validation/detect-new-resource-types.js b/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js new file mode 100644 index 000000000000..a21b9f1ea111 --- /dev/null +++ b/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js @@ -0,0 +1,206 @@ +/** + * Detect new ARM resource types in PRs using Azure OpenAPI Validator's ArmHelper. + * + * Compares resource types between the base branch and HEAD for existing RPs + * to identify newly introduced resource types that require ARM lease validation. + */ + +import { resolve } from "path"; +import { simpleGit } from "simple-git"; +import { ArmHelper } from "@microsoft.azure/openapi-validator-rulesets/dist/native/utilities/arm-helper.js"; +import { SwaggerInventory } from "@microsoft.azure/openapi-validator-core"; + +// Match: specification//resource-manager//(stable|preview)// +const VERSION_PATTERN = + /^specification\/([^/]+)\/resource-manager\/([^/]+)\/(stable|preview)\/([^/]+)\//; + +const RESOURCE_TYPE_REGEX = /\/providers\/([^/]+\/[^/\{]+)/; + +/** Extract resource type from API path (e.g. Microsoft.Compute/virtualMachines) */ +function getResourceType(apiPath) { + return apiPath.match(RESOURCE_TYPE_REGEX)?.[1] || null; +} + +/** + * Get all ARM resource types from a swagger document using openapi-validator's ArmHelper. + * + * @param {Object} swaggerDoc - Parsed swagger JSON document + * @param {string} specPath - Absolute path to the swagger file + * @returns {Map}>} + */ +function getResourceTypesFromSwagger(swaggerDoc, specPath) { + const armHelper = new ArmHelper( + swaggerDoc, + resolve(specPath), + new SwaggerInventory(), + ); + const resourceTypes = new Map(); + + for (const resource of armHelper.getAllResources()) { + const firstOp = resource.operations[0]; + const resourceType = getResourceType(firstOp.apiPath); + + if (resourceType && !resourceTypes.has(resourceType)) { + resourceTypes.set(resourceType, { + resourceType, + provider: resourceType.split("/")[0], + modelName: resource.modelName, + operations: resource.operations.map((op) => ({ + method: op.httpMethod, + apiPath: op.apiPath, + })), + }); + } + } + + return resourceTypes; +} + +/** + * Get resource types from swagger files at a specific git ref. + * + * @param {import("simple-git").SimpleGit} git + * @param {string} commitish - Git ref + * @param {string} namespacePath - e.g. `specification/compute/resource-manager/Microsoft.Compute` + * @param {string} repoRoot - Repository root directory + * @returns {Promise>} Map of resource type to info + */ +async function getResourceTypesAtRef(git, commitish, namespacePath, repoRoot) { + const allTypes = new Map(); + + let output; + try { + output = await git.raw([ + "ls-tree", + "-r", + "--name-only", + commitish, + namespacePath, + ]); + } catch { + return allTypes; // path doesn't exist at this ref + } + + const swaggerFiles = output + .split("\n") + .filter((f) => f.endsWith(".json") && !f.includes("/examples/")); + + for (const file of swaggerFiles) { + /** @type {string} */ + let content; + try { + content = await git.show([`${commitish}:${file}`]); + } catch { + continue; + } + + try { + const swaggerDoc = JSON.parse(content); + const types = getResourceTypesFromSwagger( + swaggerDoc, + resolve(repoRoot, file), + ); + 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.repoRoot - Repository root directory + * @param {string} params.mergeBase - Git ref for the base commit + * @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({ + repoRoot, + mergeBase, + rmFiles, + core, +}) { + const git = simpleGit(repoRoot); + + // Group changed RM swagger files by namespace + /** @type {Map} */ + const namespaceMap = new Map(); + + for (const file of rmFiles) { + const match = file.match(VERSION_PATTERN); + if (!match) continue; + + const orgName = match[1]; + const namespace = match[2]; + const namespacePath = `specification/${orgName}/resource-manager/${namespace}`; + + if (!namespaceMap.has(namespace)) { + namespaceMap.set(namespace, { orgName, namespacePath }); + } + } + + const results = []; + + for (const [namespace, { orgName, namespacePath }] of namespaceMap) { + core.info(`Checking for new resource types in ${namespace}...`); + + const baseTypes = await getResourceTypesAtRef( + git, + mergeBase, + namespacePath, + repoRoot, + ); + + // If the namespace didn't exist in base, skip — that's a new RP, handled elsewhere + if (baseTypes.size === 0) { + core.info(` ${namespace}: no resources in base (new RP, skipping)`); + continue; + } + + const headTypes = await getResourceTypesAtRef( + git, + "HEAD", + namespacePath, + repoRoot, + ); + + const newTypes = []; + for (const [type, info] of headTypes) { + 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( + ` ${namespace}: ${newTypes.length} new resource type(s) detected`, + ); + for (const t of newTypes) { + core.info(` - ${t.resourceType} (${t.operations.join(", ")})`); + } + results.push({ namespace, orgName, newResourceTypes: newTypes }); + } else { + core.info(` ${namespace}: no new resource types`); + } + } + + return results; +} diff --git a/.github/workflows/src/detect-new-resource-provider.js b/.github/workflows/src/detect-new-resource-provider.js deleted file mode 100644 index 5124694716d5..000000000000 --- a/.github/workflows/src/detect-new-resource-provider.js +++ /dev/null @@ -1,287 +0,0 @@ -import { writeFileSync } from 'fs'; -import { join } from 'path'; -import { simpleGit } from 'simple-git'; -import { getChangedFiles, swagger, resourceManager } from '../../shared/src/changed-files.js'; -import { getRootFolder } from '../../shared/src/simple-git.js'; -import { checkLease } from './detect-arm-leases.js'; -import { CoreLogger } from './core-logger.js'; -import { LabelAction } from './label.js'; - -// ============================================ -// Configuration -// ============================================ - -// Match pattern: specification//resource-manager//... -// Trailing slash ensures the match is a directory component, not a file like readme.md -const RESOURCE_MANAGER_PATTERN = /^specification\/[^\/]+\/resource-manager\/([^\/]+)\//; -// Match pattern with optional service group: specification//resource-manager///... -// ServiceGroup folder name should not start with "stable" or "preview" -const RESOURCE_MANAGER_WITH_GROUP_PATTERN = /^specification\/[^\/]+\/resource-manager\/([^\/]+)\/(?!stable|preview)([^\/]+)\//; - -/** - * New resource provider label names. - * @readonly - * @enum {string} - */ -const NewRpLabel = Object.freeze({ - ArmModelingReviewRequired: 'ARMModelingReviewRequired' -}); - -const DEFAULT_LABEL_ACTIONS = Object.freeze({ - [NewRpLabel.ArmModelingReviewRequired]: LabelAction.None -}); - -// ============================================ -// Utility Functions -// ============================================ - -/** - * 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 {Object} git - simple-git instance - * @param {string} commitish - Git commit reference (e.g., merge base SHA) - * @param {string} namespace - Resource provider namespace (e.g., Microsoft.App) - * @returns {Promise} True if namespace existed in the commit - */ -async function resourceProviderExistsInCommit(git, commitish, namespace) { - try { - // List all directories under specification/*/resource-manager/ in the base commit - const output = await git.raw([ - 'ls-tree', - '-d', - '--name-only', - '-r', - commitish, - 'specification/' - ]); - - // Look for resource-manager/ pattern in any service directory - const pattern = new RegExp(`^specification/[^/]+/resource-manager/${namespace.replace('.', '\\.')}$`, 'm'); - return pattern.test(output); - } catch { - // Error checking - assume RP doesn't exist if we can't verify - return false; - } -} - -function getLabelActions(hasNewResourceProviders) { - if (!hasNewResourceProviders) { - return { - [NewRpLabel.ArmModelingReviewRequired]: LabelAction.Remove - }; - } - - return { - [NewRpLabel.ArmModelingReviewRequired]: LabelAction.Add - }; -} - -/** - * Extract resource provider namespaces, service names, and optional service groups from file paths - * @param {string[]} files - Array of file paths - * @returns {Map} Map of namespace to service info - */ -function extractResourceProviders(files) { - const resourceProviders = new Map(); - - for (const file of files) { - const match = file.match(RESOURCE_MANAGER_PATTERN); - if (match) { - const parts = file.split('/'); - const serviceName = parts[1]; - const namespace = match[1]; - - // Check if there's a service group - const groupMatch = file.match(RESOURCE_MANAGER_WITH_GROUP_PATTERN); - if (groupMatch) { - const serviceGroup = groupMatch[2]; - resourceProviders.set(namespace, { serviceName, serviceGroup }); - } else { - resourceProviders.set(namespace, { serviceName }); - } - } - } - - return resourceProviders; -} - -// ============================================ -// Main Detection Logic -// ============================================ - -/** - * Main detection logic for GitHub script action - * @param {Object} params - Parameters from github-script - * @param {Object} params.context - GitHub context - * @param {Object} params.core - GitHub Actions core - * @returns {Promise<{ status: string, labelActions: Record }>} Result including status and label actions - */ -export default async function detectNewResourceProvider({ context, core }) { - const repoRoot = await getRootFolder(process.cwd()); - const git = simpleGit(repoRoot); - - core.info('Detecting New Resource Providers'); - - // Get base branch from context - const baseBranch = context.payload.pull_request?.base?.ref; - if (!baseBranch) { - throw new Error('Could not determine base branch from pull request context'); - } - - // Get the merge base for proper PR comparison. - // With fetch-depth: 2, HEAD is the PR merge commit and HEAD^ is the base commit. - // git merge-base may fail if origin/ isn't fetched, so fall back to HEAD^. - let mergeBase; - try { - mergeBase = await git.raw(['merge-base', `origin/${baseBranch}`, 'HEAD']).then(s => s.trim()); - } catch { - mergeBase = 'HEAD^'; - } - - // Get all changed files in specification/ - const options = { - cwd: repoRoot, - paths: ['specification'], - logger: new CoreLogger(core), - }; - - const changedFiles = await getChangedFiles(options); - - // Filter to resource-manager files - const rmFiles = changedFiles.filter(resourceManager); - - if (rmFiles.length === 0) { - core.info("No changes to files containing path '/resource-manager/'"); - return { - status: 'no-changes', - labelActions: DEFAULT_LABEL_ACTIONS - }; - } - - // Extract resource providers from changed files - 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, match.index + match[0].length - 1); - } - return null; - }) - .filter(Boolean) - ); - - if (changedNamespacePaths.size > 0) { - let hasAtLeastOneBrandNewRP = false; - - for (const namespacePath of changedNamespacePaths) { - try { - const specFilesBaseBranch = await git.raw([ - 'ls-tree', - '-r', - '--name-only', - mergeBase, - namespacePath, - ]); - - const specRmSwaggerFilesBaseBranch = specFilesBaseBranch - .split('\n') - .filter((file) => resourceManager(file) && swagger(file)); - - if (specRmSwaggerFilesBaseBranch.length === 0) { - hasAtLeastOneBrandNewRP = true; - } - } catch { - // Directory doesn't exist in base - brand new RP - hasAtLeastOneBrandNewRP = true; - } - } - - if (!hasAtLeastOneBrandNewRP) { - core.info('No brand new resource providers detected, spec directories exist in base branch.'); - core.info('Skipping workflow.'); - return { - status: 'no-new-rp', - labelActions: getLabelActions(false) - }; - } - } - - // Extract resource providers and filter for new ones - const newResourceProviders = []; - - for (const [rp, info] of changedResourceProviders) { - const existsInBase = await resourceProviderExistsInCommit(git, mergeBase, rp); - if (!existsInBase) { - newResourceProviders.push({ - namespace: rp, - serviceName: info.serviceName, - serviceGroup: info.serviceGroup - }); - } - } - - // Check ARM leases for new resource providers - const leaseCheckResults = []; - - if (newResourceProviders.length > 0) { - core.info(`Detected ${newResourceProviders.length} new resource provider(s)`); - - for (const rp of newResourceProviders) { - const leaseValid = await checkLease(rp.serviceName, rp.namespace, rp.serviceGroup || ''); - - leaseCheckResults.push({ - namespace: rp.namespace, - serviceName: rp.serviceName, - serviceGroup: rp.serviceGroup, - leaseValid: leaseValid, - leaseMessage: leaseValid ? 'Lease is valid' : 'No lease file found or lease has expired' - }); - - const leaseStatus = leaseValid ? 'Lease valid' : 'Lease invalid'; - core.info(` - ${rp.namespace}: ${leaseStatus}`); - } - - writeFileSync( - join(repoRoot, '.github', 'new-rp-output.json'), - JSON.stringify({ newResourceProviders: leaseCheckResults }, null, 2) - ); - - // Partition results by lease validity - 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: ' + - 'https://outlook.office365.com/book/ARMOfficeHours1@microsoft.onmicrosoft.com/?ismsaljsauthenabled=true' - ); - } else { - core.info('New resource provider(s) detected with valid ARM lease — no action required.'); - } - - return { - status: allLeasesValid ? 'new-rp-all-leases-valid' : 'new-rp-invalid-lease', - labelActions: getLabelActions(!allLeasesValid) - }; - } else { - core.info('No new resource providers detected.'); - return { - status: 'no-new-rp', - labelActions: getLabelActions(false) - }; - } -} - diff --git a/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js b/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js new file mode 100644 index 000000000000..3950b601debc --- /dev/null +++ b/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js @@ -0,0 +1,169 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { Temporal } from "@js-temporal/polyfill"; + +/** @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-lease-validation/detect-arm-leases.js"; + +/** Get today's date string using Temporal (same as source code) */ +function today() { + return Temporal.Now.plainDateISO(); +} + +/** Subtract days from today and return YYYY-MM-DD string */ +function daysAgo(n) { + return today().subtract({ days: n }).toString(); +} + +/** Build a valid lease YAML string */ +function leaseYaml(startdate, duration) { + return [ + "lease:", + ` startdate: ${startdate}`, + ` duration: ${duration}`, + ].join("\n"); +} + +describe("detect-arm-leases", () => { + 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).toMatch(/expired/i); + }); + + 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("returns valid for single day duration starting today", () => { + const result = parseLease(leaseYaml(today().toString(), "P1D")); + expect(result.valid).toBe(true); + }); + + 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); + }); + + it("returns invalid for empty YAML content", () => { + const result = parseLease(""); + expect(result.valid).toBe(false); + }); + + it("returns invalid for malformed YAML", () => { + const result = parseLease("invalid: yaml: content"); + expect(result.valid).toBe(false); + }); + + it("returns invalid when startdate is missing", () => { + const yaml = ["lease:", " duration: P90D"].join("\n"); + const result = parseLease(yaml); + expect(result.valid).toBe(false); + }); + + it("returns invalid when duration is missing", () => { + const yaml = ["lease:", " startdate: " + daysAgo(10)].join("\n"); + const result = parseLease(yaml); + expect(result.valid).toBe(false); + }); + + it("returns invalid for bad startdate format", () => { + const result = parseLease(leaseYaml("01-01-2025", "P90D")); + expect(result.valid).toBe(false); + }); + + it("returns invalid for bad duration format", () => { + const result = parseLease(leaseYaml(daysAgo(10), "90 days")); + expect(result.valid).toBe(false); + }); + }); + + 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); + }); + }); +}); \ No newline at end of file diff --git a/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js b/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js new file mode 100644 index 000000000000..6a6c35ca60bf --- /dev/null +++ b/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js @@ -0,0 +1,253 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createMockCore, createMockContext } 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-lease-validation/detect-arm-leases.js", () => ({ + checkLease: vi.fn().mockResolvedValue(false), +})); + +vi.mock("../../src/arm-lease-validation/detect-new-resource-types.js", () => ({ + detectNewResourceTypes: vi.fn().mockResolvedValue([]), +})); + +import * as changedFiles from "../../../shared/src/changed-files.js"; +import { checkLease } from "../../src/arm-lease-validation/detect-arm-leases.js"; +import { detectNewResourceTypes } from "../../src/arm-lease-validation/detect-new-resource-types.js"; +import detectNewResourceProvider from "../../src/arm-lease-validation/detect-new-resource-provider.js"; + +const core = createMockCore(); +const context = createMockContext(); + +describe("detectNewResourceProvider", () => { + 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 detectNewResourceProvider({ context, 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 detectNewResourceProvider({ context, 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 detectNewResourceProvider({ context, 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(async (orgName) => orgName === "svcA"); + + const result = await detectNewResourceProvider({ context, 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 detectNewResourceProvider({ context, 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 detectNewResourceProvider({ context, 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 detectNewResourceProvider({ context, 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 detectNewResourceProvider({ context, 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 detectNewResourceProvider({ context, 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([ + { namespace: "Microsoft.Compute", orgName: "compute", newResourceTypes: [{ resourceType: "Microsoft.Compute/disks" }] }, + ]); + vi.mocked(checkLease).mockResolvedValue(true); + + const result = await detectNewResourceProvider({ context, 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([ + { namespace: "Microsoft.Compute", orgName: "compute", newResourceTypes: [{ resourceType: "Microsoft.Compute/disks" }] }, + ]); + vi.mocked(checkLease).mockResolvedValue(false); + + const result = await detectNewResourceProvider({ context, 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 detectNewResourceProvider({ context, core }); + + expect(result.status).toBe("no-new-rp"); + expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); + expect(result.labelActions.ARMModelingAutoSignedOff).toBe("remove"); + }); +}); \ No newline at end of file diff --git a/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js b/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js new file mode 100644 index 000000000000..9965dee346df --- /dev/null +++ b/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js @@ -0,0 +1,301 @@ +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 }), +})); + +const mockGetAllResources = vi.hoisted(() => vi.fn().mockReturnValue([])); + +vi.mock( + "@microsoft.azure/openapi-validator-rulesets/dist/native/utilities/arm-helper.js", + () => ({ + ArmHelper: vi.fn().mockImplementation(function () { + return { getAllResources: mockGetAllResources }; + }), + }), +); + +vi.mock("@microsoft.azure/openapi-validator-core", () => ({ + SwaggerInventory: vi.fn().mockImplementation(function () {}), +})); + +import { detectNewResourceTypes } from "../../src/arm-lease-validation/detect-new-resource-types.js"; + +const core = createMockCore(); + +const swaggerContent = JSON.stringify({ swagger: "2.0", paths: {} }); + +// ── test data ─────────────────────────────────────────────────────────── + +const vmResource = { + modelName: "VirtualMachine", + operations: [ + { + httpMethod: "GET", + apiPath: "/subscriptions/{id}/providers/Microsoft.Compute/virtualMachines/{name}", + }, + ], +}; + +const diskResource = { + modelName: "Disk", + operations: [ + { + httpMethod: "GET", + apiPath: "/subscriptions/{id}/providers/Microsoft.Compute/disks/{name}", + }, + { + httpMethod: "PUT", + apiPath: "/subscriptions/{id}/providers/Microsoft.Compute/disks/{name}", + }, + ], +}; + +// ── 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(async (args) => { + if (args[0] === "ls-tree" && args.includes("-r")) { + const commitish = args[3]; + const namespacePath = args[4]; + const filesMap = commitish === "HEAD" ? headFiles : baseFiles; + if (filesMap.has(namespacePath)) { + return filesMap.get(namespacePath).join("\n"); + } + throw new Error(`path '${namespacePath}' does not exist`); + } + return ""; + }); + + mockShow.mockImplementation(async (args) => { + const key = args[0]; // "ref:filepath" + if (fileContents.has(key)) { + return fileContents.get(key); + } + throw new Error(`path does not exist: ${key}`); + }); +} + +// ── tests ─────────────────────────────────────────────────────────────── + +describe("detectNewResourceTypes", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("returns empty when rmFiles has no version-pattern matches", async () => { + const result = await detectNewResourceTypes({ + repoRoot: "/fake/repo", + mergeBase: "base123", + 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({ + repoRoot: "/fake/repo", + mergeBase: "base123", + 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({ + repoRoot: "/fake/repo", + mergeBase: "base123", + 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`; + + setupGit({ + baseFiles: new Map([[ns, [file]]]), + headFiles: new Map([[ns, [file]]]), + fileContents: new Map([ + [`base123:${file}`, swaggerContent], + [`HEAD:${file}`, swaggerContent], + ]), + }); + + mockGetAllResources.mockReturnValue([vmResource]); + + const result = await detectNewResourceTypes({ + repoRoot: "/fake/repo", + mergeBase: "base123", + 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`; + + setupGit({ + baseFiles: new Map([[ns, [file]]]), + headFiles: new Map([[ns, [file]]]), + fileContents: new Map([ + [`base123:${file}`, swaggerContent], + [`HEAD:${file}`, swaggerContent], + ]), + }); + + mockGetAllResources + .mockReturnValueOnce([vmResource]) // base: VM only + .mockReturnValueOnce([vmResource, diskResource]); // HEAD: VM + Disk + + const result = await detectNewResourceTypes({ + repoRoot: "/fake/repo", + mergeBase: "base123", + rmFiles: [file], + core, + }); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + namespace: "Microsoft.Compute", + orgName: "compute", + }); + 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`; + + setupGit({ + baseFiles: new Map([ + [computeNs, [computeFile]], + [networkNs, [networkFile]], + ]), + headFiles: new Map([ + [computeNs, [computeFile]], + [networkNs, [networkFile]], + ]), + fileContents: new Map([ + [`base123:${computeFile}`, swaggerContent], + [`HEAD:${computeFile}`, swaggerContent], + [`base123:${networkFile}`, swaggerContent], + [`HEAD:${networkFile}`, swaggerContent], + ]), + }); + + mockGetAllResources.mockReturnValue([]); + + const result = await detectNewResourceTypes({ + repoRoot: "/fake/repo", + mergeBase: "base123", + 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`; + + setupGit({ + baseFiles: new Map([[ns, [file, exampleFile]]]), + headFiles: new Map([[ns, [file, exampleFile]]]), + fileContents: new Map([ + [`base123:${file}`, swaggerContent], + [`HEAD:${file}`, swaggerContent], + ]), + }); + + mockGetAllResources.mockReturnValue([]); + + await detectNewResourceTypes({ + repoRoot: "/fake/repo", + mergeBase: "base123", + 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}/readme.md`; + + setupGit({ + baseFiles: new Map([[ns, [file, readme]]]), + headFiles: new Map([[ns, [file, readme]]]), + fileContents: new Map([ + [`base123:${file}`, swaggerContent], + [`HEAD:${file}`, swaggerContent], + ]), + }); + + mockGetAllResources.mockReturnValue([]); + + await detectNewResourceTypes({ + repoRoot: "/fake/repo", + mergeBase: "base123", + rmFiles: [file], + core, + }); + + expect(mockShow).not.toHaveBeenCalledWith( + expect.arrayContaining([expect.stringContaining("readme.md")]), + ); + }); + +}); diff --git a/.github/workflows/test/detect-new-resource-provider.test.js b/.github/workflows/test/detect-new-resource-provider.test.js deleted file mode 100644 index ab2ea90424f1..000000000000 --- a/.github/workflows/test/detect-new-resource-provider.test.js +++ /dev/null @@ -1,355 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -// ============================================ -// Mock all external dependencies -// ============================================ - -// simple-git: mock git.raw() for merge-base and ls-tree calls -const mockGitRaw = vi.fn(); -vi.mock('simple-git', () => ({ - simpleGit: () => ({ raw: mockGitRaw }) -})); - -// fs: mock writeFileSync to avoid writing to disk -vi.mock('fs', () => ({ - writeFileSync: vi.fn() -})); - -// shared/simple-git: mock getRootFolder so it doesn't depend on cwd -vi.mock('../../shared/src/simple-git.js', () => ({ - getRootFolder: vi.fn().mockResolvedValue('/fake/repo') -})); - -// shared/changed-files: mock getChangedFiles, keep resourceManager and swagger real -const mockGetChangedFiles = vi.fn(); -vi.mock('../../shared/src/changed-files.js', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - getChangedFiles: (...args) => mockGetChangedFiles(...args) - }; -}); - -// detect-arm-leases: mock checkLease -const mockCheckLease = vi.fn(); -vi.mock('../src/detect-arm-leases.js', () => ({ - checkLease: (...args) => mockCheckLease(...args) -})); - -import detectNewResourceProvider from '../src/detect-new-resource-provider.js'; - -// ============================================ -// Helpers -// ============================================ - -function makeMockCore() { - return { - info: vi.fn(), - error: vi.fn(), - setFailed: vi.fn() - }; -} - -function makeMockContext(baseBranch = 'main') { - return { - repo: { owner: 'Azure', repo: 'azure-rest-api-specs' }, - issue: { number: 42 }, - payload: { - pull_request: { - base: { ref: baseBranch }, - head: { sha: 'abc123' } - } - } - }; -} - -/** - * Configure mockGitRaw to handle merge-base and ls-tree calls. - * @param {Object} opts - * @param {string} opts.mergeBase - SHA returned by merge-base - * @param {string[]} [opts.lsTreeDirOutput] - output for ls-tree -d (namespace existence check) - * @param {string[]} [opts.lsTreeFileOutput] - output for ls-tree -r (brand-new RP pre-check) - */ -function setupGitMock({ mergeBase = 'base123', lsTreeDirOutput = [], lsTreeFileOutput = [] } = {}) { - mockGitRaw.mockImplementation(async (args) => { - if (args[0] === 'merge-base') { - return `${mergeBase}\n`; - } - if (args[0] === 'ls-tree') { - // ls-tree -d (directory listing for resourceProviderExistsInCommit) - if (args.includes('-d')) { - return lsTreeDirOutput.join('\n'); - } - // ls-tree -r (file listing for brand-new RP pre-check) - if (args.includes('-r')) { - return lsTreeFileOutput.join('\n'); - } - } - return ''; - }); -} - -// ============================================ -// Tests -// ============================================ - -describe('detect-new-resource-provider', () => { - let mockCore; - let mockContext; - - beforeEach(() => { - vi.clearAllMocks(); - mockCore = makeMockCore(); - mockContext = makeMockContext(); - }); - - describe('no resource-manager changes', () => { - it('should return no-changes when no resource-manager files are modified', async () => { - mockGetChangedFiles.mockResolvedValue([ - 'specification/compute/data-plane/foo.json' - ]); - setupGitMock(); - - const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - - expect(result.status).toBe('no-changes'); - expect(result.labelActions.ARMModelingReviewRequired).toBe('none'); - expect(mockCore.setFailed).not.toHaveBeenCalled(); - }); - - it('should return no-changes when changed files list is empty', async () => { - mockGetChangedFiles.mockResolvedValue([]); - setupGitMock(); - - const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - - expect(result.status).toBe('no-changes'); - }); - }); - - describe('existing resource provider (not new)', () => { - it('should return no-new-rp when all RP namespaces exist in base branch', async () => { - const rmFile = 'specification/compute/resource-manager/Microsoft.Compute/stable/2024-01-01/compute.json'; - mockGetChangedFiles.mockResolvedValue([rmFile]); - setupGitMock({ - // Pre-check: file exists in base → not brand new - lsTreeFileOutput: [rmFile], - }); - - const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - - expect(result.status).toBe('no-new-rp'); - expect(result.labelActions.ARMModelingReviewRequired).toBe('remove'); - expect(mockCore.setFailed).not.toHaveBeenCalled(); - }); - }); - - describe('new resource provider with valid lease', () => { - it('should return new-rp-all-leases-valid and not fail', async () => { - const rmFile = 'specification/newservice/resource-manager/Microsoft.NewService/stable/2025-01-01/api.json'; - mockGetChangedFiles.mockResolvedValue([rmFile]); - setupGitMock({ - // Pre-check: no files in base for this namespace → brand new - lsTreeFileOutput: [], - // Namespace doesn't exist in base commit directories - lsTreeDirOutput: [], - }); - mockCheckLease.mockResolvedValue(true); - - const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - - expect(result.status).toBe('new-rp-all-leases-valid'); - expect(result.labelActions.ARMModelingReviewRequired).toBe('remove'); - expect(mockCore.setFailed).not.toHaveBeenCalled(); - expect(mockCore.info).toHaveBeenCalledWith( - expect.stringContaining('valid ARM lease') - ); - expect(mockCheckLease).toHaveBeenCalledWith('newservice', 'Microsoft.NewService', ''); - }); - }); - - describe('new resource provider with invalid lease', () => { - it('should return new-rp-invalid-lease and call setFailed', async () => { - const rmFile = 'specification/badservice/resource-manager/Microsoft.BadService/preview/2025-01-01/api.json'; - mockGetChangedFiles.mockResolvedValue([rmFile]); - setupGitMock({ - lsTreeFileOutput: [], - lsTreeDirOutput: [], - }); - mockCheckLease.mockResolvedValue(false); - - const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - - expect(result.status).toBe('new-rp-invalid-lease'); - expect(result.labelActions.ARMModelingReviewRequired).toBe('add'); - expect(mockCore.setFailed).toHaveBeenCalledTimes(1); - expect(mockCore.setFailed).toHaveBeenCalledWith( - expect.stringContaining('ARM API Modeling Office Hours') - ); - expect(mockCore.error).toHaveBeenCalledWith( - expect.stringContaining('Microsoft.BadService') - ); - }); - }); - - describe('multiple new resource providers with mixed leases', () => { - it('should fail when at least one lease is invalid', async () => { - mockGetChangedFiles.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', - ]); - setupGitMock({ - lsTreeFileOutput: [], - lsTreeDirOutput: [], - }); - // SvcA valid, SvcB invalid - mockCheckLease.mockImplementation(async (svc) => - svc === 'svcA' - ); - - const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - - expect(result.status).toBe('new-rp-invalid-lease'); - expect(result.labelActions.ARMModelingReviewRequired).toBe('add'); - expect(mockCore.setFailed).toHaveBeenCalledTimes(1); - expect(mockCore.setFailed).toHaveBeenCalledWith( - expect.stringContaining('without a valid ARM lease') - ); - }); - - it('should pass when all leases are valid', async () => { - mockGetChangedFiles.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', - ]); - setupGitMock({ - lsTreeFileOutput: [], - lsTreeDirOutput: [], - }); - mockCheckLease.mockResolvedValue(true); - - const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - - expect(result.status).toBe('new-rp-all-leases-valid'); - expect(result.labelActions.ARMModelingReviewRequired).toBe('remove'); - expect(mockCore.setFailed).not.toHaveBeenCalled(); - }); - }); - - describe('service group extraction', () => { - it('should pass serviceGroup to checkLease when present', async () => { - const rmFile = 'specification/svc/resource-manager/Microsoft.Svc/ComputeRP/stable/2025-01-01/api.json'; - mockGetChangedFiles.mockResolvedValue([rmFile]); - setupGitMock({ - lsTreeFileOutput: [], - lsTreeDirOutput: [], - }); - mockCheckLease.mockResolvedValue(true); - - await detectNewResourceProvider({ context: mockContext, core: mockCore }); - - expect(mockCheckLease).toHaveBeenCalledWith('svc', 'Microsoft.Svc', 'ComputeRP'); - }); - }); - - describe('merge-base fallback', () => { - it('should fall back to HEAD^ when merge-base fails', async () => { - mockGetChangedFiles.mockResolvedValue([ - 'specification/svc/resource-manager/Microsoft.Svc/stable/2025-01-01/api.json', - ]); - let callCount = 0; - mockGitRaw.mockImplementation(async (args) => { - if (args[0] === 'merge-base') { - throw new Error('Not a valid object name'); - } - if (args[0] === 'ls-tree') { - callCount++; - // For the pre-check ls-tree -r call, verify it uses HEAD^ as commitish - if (args.includes('-r') && callCount === 1) { - expect(args).toContain('HEAD^'); - } - return ''; - } - return ''; - }); - mockCheckLease.mockResolvedValue(true); - - const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - - // Should still succeed despite merge-base failure - expect(['new-rp-all-leases-valid', 'new-rp-invalid-lease', 'no-new-rp']).toContain(result.status); - }); - }); - - describe('context validation', () => { - it('should throw when pull_request context is missing', async () => { - const badContext = { - repo: { owner: 'Azure', repo: 'azure-rest-api-specs' }, - issue: { number: 1 }, - payload: {} - }; - - await expect( - detectNewResourceProvider({ context: badContext, core: mockCore }) - ).rejects.toThrow('Could not determine base branch'); - }); - - it('should use baseBranch from context payload', async () => { - const altBranchContext = makeMockContext('RPSaaSMaster'); - mockGetChangedFiles.mockResolvedValue([]); - setupGitMock(); - - await detectNewResourceProvider({ context: altBranchContext, core: mockCore }); - - // merge-base should have been called with origin/RPSaaSMaster - expect(mockGitRaw).toHaveBeenCalledWith( - expect.arrayContaining(['merge-base', 'origin/RPSaaSMaster', 'HEAD']) - ); - }); - }); - - describe('label actions', () => { - it('should return Remove for ARMModelingReviewRequired when no new RPs', async () => { - const rmFile = 'specification/compute/resource-manager/Microsoft.Compute/stable/2024-01-01/compute.json'; - mockGetChangedFiles.mockResolvedValue([rmFile]); - setupGitMock({ lsTreeFileOutput: [rmFile] }); - - const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - - expect(result.labelActions.ARMModelingReviewRequired).toBe('remove'); - }); - - it('should return Add for ARMModelingReviewRequired when new RP has invalid lease', async () => { - mockGetChangedFiles.mockResolvedValue([ - 'specification/svc/resource-manager/Microsoft.Svc/stable/2025-01-01/api.json', - ]); - setupGitMock({ lsTreeFileOutput: [], lsTreeDirOutput: [] }); - mockCheckLease.mockResolvedValue(false); - - const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - - expect(result.labelActions.ARMModelingReviewRequired).toBe('add'); - }); - - it('should return Remove for ARMModelingReviewRequired when new RP has valid lease', async () => { - mockGetChangedFiles.mockResolvedValue([ - 'specification/svc/resource-manager/Microsoft.Svc/stable/2025-01-01/api.json', - ]); - setupGitMock({ lsTreeFileOutput: [], lsTreeDirOutput: [] }); - mockCheckLease.mockResolvedValue(true); - - const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - - expect(result.labelActions.ARMModelingReviewRequired).toBe('remove'); - }); - - it('should return none for no-changes status', async () => { - mockGetChangedFiles.mockResolvedValue([]); - setupGitMock(); - - const result = await detectNewResourceProvider({ context: mockContext, core: mockCore }); - - expect(result.labelActions.ARMModelingReviewRequired).toBe('none'); - }); - }); -}); From 7be221c05ad088601735d212558142a71091bb3f Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 4 Mar 2026 01:36:58 -0600 Subject: [PATCH 037/125] deleetd files --- .github/workflows/src/detect-arm-leases.js | 101 ---------- .../workflows/test/detect-arm-leases.test.js | 177 ------------------ 2 files changed, 278 deletions(-) delete mode 100644 .github/workflows/src/detect-arm-leases.js delete mode 100644 .github/workflows/test/detect-arm-leases.test.js diff --git a/.github/workflows/src/detect-arm-leases.js b/.github/workflows/src/detect-arm-leases.js deleted file mode 100644 index 4f8afcc6902b..000000000000 --- a/.github/workflows/src/detect-arm-leases.js +++ /dev/null @@ -1,101 +0,0 @@ -import { readFile } from 'fs/promises'; -import { resolve } from 'path'; -import yaml from 'js-yaml'; -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-days: "P30D" - * ``` - */ -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-days': z.string().regex(/^P(\d+)D$/i, 'duration-days must be in ISO 8601 format (e.g. P180D)'), - }), -}); - -/** - * Build the lease path based on service information. - * - * Lease files are stored at: - * - Without service group: `.github/arm-leases///lease.yaml` - * - With service group: `.github/arm-leases////lease.yaml` - * - * @param {string} repoRoot - Repository root path - * @param {string} serviceName - Service name (e.g., "compute") - * @param {string} resourceProvider - Resource provider namespace (e.g., "Microsoft.Compute") - * @param {string} serviceGroup - Optional service group for RPs that have sub-groupings (e.g., "ComputeRP") - * @returns {string} Full path to lease.yaml file - */ -function buildLeasePath(repoRoot, serviceName, resourceProvider, serviceGroup = '') { - const leasePathParts = [repoRoot, '.github', 'arm-leases', serviceName, resourceProvider]; - if (serviceGroup) { - leasePathParts.push(serviceGroup); - } - leasePathParts.push('lease.yaml'); - return resolve(...leasePathParts); -} - -/** - * Check if ARM lease exists and is valid - * @param {string} serviceName - Service name - * @param {string} resourceProvider - Resource provider namespace - * @param {string} serviceGroup - Optional service group - * @returns {Promise<{ exists: boolean, valid: boolean, leasePath: string }>} Lease status - */ -export async function getLeaseStatus(serviceName, resourceProvider, serviceGroup = '') { - const repoRoot = process.env.TEST_REPO_ROOT || await getRootFolder(process.cwd()); - const leasePath = buildLeasePath(repoRoot, serviceName, resourceProvider, serviceGroup); - - let content; - try { - content = await readFile(leasePath, 'utf-8'); - } catch { - return { exists: false, valid: false, leasePath }; - } - - try { - const rawParsed = /** @type {any} */ (yaml.load(content, { schema: yaml.FAILSAFE_SCHEMA })); - - if (!rawParsed) { - return { exists: true, valid: false, leasePath }; - } - - const parsed = leaseSchema.parse(rawParsed); - const lease = parsed.lease; - - const durationDays = parseInt(lease['duration-days'].match(/^P(\d+)D$/i)[1], 10); - const endDate = new Date(lease.startdate); - endDate.setDate(endDate.getDate() + durationDays); - - const today = new Date(); - today.setHours(0, 0, 0, 0); - - return { exists: true, valid: today <= endDate, leasePath }; - } catch { - // File exists but content is invalid - return { exists: true, valid: false, leasePath }; - } -} - -/** - * Check if ARM lease exists and is valid. - * - * Looks for a lease file at the appropriate path (see buildLeasePath for path structure). - * - * @param {string} serviceName - Service name (e.g., "compute") - * @param {string} resourceProvider - Resource provider namespace (e.g., "Microsoft.Compute") - * @param {string} serviceGroup - Optional service group for RPs with sub-groupings - * @returns {Promise} True if lease exists and is valid, false otherwise - */ -export async function checkLease(serviceName, resourceProvider, serviceGroup = '') { - const status = await getLeaseStatus(serviceName, resourceProvider, serviceGroup); - return status.valid; -} diff --git a/.github/workflows/test/detect-arm-leases.test.js b/.github/workflows/test/detect-arm-leases.test.js deleted file mode 100644 index 5691fdafbbd7..000000000000 --- a/.github/workflows/test/detect-arm-leases.test.js +++ /dev/null @@ -1,177 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs'; -import { join } from 'path'; -import { checkLease } from '../src/detect-arm-leases.js'; - -const TEST_ROOT = join(process.cwd(), '.test-arm-leases'); -const ARM_LEASES_DIR = join(TEST_ROOT, '.github', 'arm-leases'); - -describe('detect-arm-leases', () => { - beforeEach(() => { - if (existsSync(TEST_ROOT)) { - rmSync(TEST_ROOT, { recursive: true, force: true }); - } - mkdirSync(ARM_LEASES_DIR, { recursive: true }); - process.env.TEST_REPO_ROOT = TEST_ROOT; - }); - - afterEach(() => { - if (existsSync(TEST_ROOT)) { - rmSync(TEST_ROOT, { recursive: true, force: true }); - } - delete process.env.TEST_REPO_ROOT; - }); - - function createLeaseFile(serviceName, resourceProvider, startdate, duration) { - const leaseDir = join(ARM_LEASES_DIR, serviceName, resourceProvider); - mkdirSync(leaseDir, { recursive: true }); - - const leaseContent = `lease: - resource-provider: ${resourceProvider} - startdate: ${startdate} - duration-days: ${duration} - reviewer: Test Reviewer -`; - - writeFileSync(join(leaseDir, 'lease.yaml'), leaseContent); - } - - describe('checkLease', () => { - it('returns false when lease file does not exist', async () => { - const result = await checkLease('testservice', 'Microsoft.Test'); - expect(result).toBe(false); - }); - - it('returns true when lease is valid and not expired', async () => { - const today = new Date(); - const startDate = new Date(today); - startDate.setDate(today.getDate() - 30); - - createLeaseFile( - 'testservice', - 'Microsoft.Test', - startDate.toISOString().split('T')[0], - 'P90D' - ); - - const result = await checkLease('testservice', 'Microsoft.Test'); - expect(result).toBe(true); - }); - - it('returns false when lease has expired', async () => { - const today = new Date(); - const startDate = new Date(today); - startDate.setDate(today.getDate() - 100); - - createLeaseFile( - 'testservice', - 'Microsoft.Test', - startDate.toISOString().split('T')[0], - 'P90D' - ); - - const result = await checkLease('testservice', 'Microsoft.Test'); - expect(result).toBe(false); - }); - - it('returns true on the last day of lease', async () => { - const today = new Date(); - const startDate = new Date(today); - startDate.setDate(today.getDate() - 89); - - createLeaseFile( - 'testservice', - 'Microsoft.Test', - startDate.toISOString().split('T')[0], - 'P90D' - ); - - const result = await checkLease('testservice', 'Microsoft.Test'); - expect(result).toBe(true); - }); - - it('returns false one day after lease expires', async () => { - const today = new Date(); - const startDate = new Date(today); - startDate.setDate(today.getDate() - 91); - - createLeaseFile( - 'testservice', - 'Microsoft.Test', - startDate.toISOString().split('T')[0], - 'P90D' - ); - - const result = await checkLease('testservice', 'Microsoft.Test'); - expect(result).toBe(false); - }); - - it('handles case-insensitive duration format', async () => { - const today = new Date(); - const startDate = new Date(today); - startDate.setDate(today.getDate() - 10); - - createLeaseFile( - 'testservice', - 'Microsoft.Test', - startDate.toISOString().split('T')[0], - 'P180d' - ); - - const result = await checkLease('testservice', 'Microsoft.Test'); - expect(result).toBe(true); - }); - - it('handles single day duration', async () => { - const today = new Date(); - - createLeaseFile( - 'testservice', - 'Microsoft.Test', - today.toISOString().split('T')[0], - 'P1D' - ); - - const result = await checkLease('testservice', 'Microsoft.Test'); - expect(result).toBe(true); - }); - - it('returns false for invalid lease file format', async () => { - const leaseDir = join(ARM_LEASES_DIR, 'testservice', 'Microsoft.Test'); - mkdirSync(leaseDir, { recursive: true }); - writeFileSync(join(leaseDir, 'lease.yaml'), 'invalid: yaml: content'); - - const result = await checkLease('testservice', 'Microsoft.Test'); - expect(result).toBe(false); - }); - - it('handles multiple services and namespaces', async () => { - const today = new Date(); - const startDate = new Date(today); - startDate.setDate(today.getDate() - 30); - - createLeaseFile('app', 'Microsoft.App', startDate.toISOString().split('T')[0], 'P90D'); - createLeaseFile('compute', 'Microsoft.Compute', startDate.toISOString().split('T')[0], 'P90D'); - - expect(await checkLease('app', 'Microsoft.App')).toBe(true); - expect(await checkLease('compute', 'Microsoft.Compute')).toBe(true); - expect(await checkLease('storage', 'Microsoft.Storage')).toBe(false); - }); - - it('handles future start dates', async () => { - const today = new Date(); - const futureDate = new Date(today); - futureDate.setDate(today.getDate() + 10); - - createLeaseFile( - 'testservice', - 'Microsoft.Test', - futureDate.toISOString().split('T')[0], - 'P90D' - ); - - const result = await checkLease('testservice', 'Microsoft.Test'); - expect(result).toBe(true); - }); - }); -}); From e112d2e2f0fee8a0a7420b0e5feaab6933bbf874 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 4 Mar 2026 01:43:05 -0600 Subject: [PATCH 038/125] tests were failing --- .../detect-arm-leases.test.js | 47 +++++-------------- 1 file changed, 11 insertions(+), 36 deletions(-) diff --git a/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js b/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js index 3950b601debc..f213240a7a10 100644 --- a/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js +++ b/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js @@ -29,11 +29,7 @@ function daysAgo(n) { /** Build a valid lease YAML string */ function leaseYaml(startdate, duration) { - return [ - "lease:", - ` startdate: ${startdate}`, - ` duration: ${duration}`, - ].join("\n"); + return `lease:\n startdate: "${startdate}"\n duration: "${duration}"\n`; } describe("detect-arm-leases", () => { @@ -50,7 +46,7 @@ describe("detect-arm-leases", () => { it("returns invalid when lease has expired", () => { const result = parseLease(leaseYaml(daysAgo(100), "P90D")); expect(result.valid).toBe(false); - expect(result.reason).toMatch(/expired/i); + expect(result.reason).toContain("expired"); }); it("returns valid on the last day of lease", () => { @@ -80,47 +76,26 @@ describe("detect-arm-leases", () => { expect(result.valid).toBe(true); }); - it("returns valid for single day duration starting today", () => { + it("handles single day duration", () => { const result = parseLease(leaseYaml(today().toString(), "P1D")); expect(result.valid).toBe(true); }); - 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); - }); - - it("returns invalid for empty YAML content", () => { - const result = parseLease(""); - expect(result.valid).toBe(false); - }); - it("returns invalid for malformed YAML", () => { const result = parseLease("invalid: yaml: content"); expect(result.valid).toBe(false); }); - it("returns invalid when startdate is missing", () => { - const yaml = ["lease:", " duration: P90D"].join("\n"); - const result = parseLease(yaml); - expect(result.valid).toBe(false); - }); - - it("returns invalid when duration is missing", () => { - const yaml = ["lease:", " startdate: " + daysAgo(10)].join("\n"); - const result = parseLease(yaml); - expect(result.valid).toBe(false); - }); - - it("returns invalid for bad startdate format", () => { - const result = parseLease(leaseYaml("01-01-2025", "P90D")); + it("returns invalid for empty content", () => { + const result = parseLease(""); expect(result.valid).toBe(false); + expect(result.reason).toContain("Empty"); }); - it("returns invalid for bad duration format", () => { - const result = parseLease(leaseYaml(daysAgo(10), "90 days")); - expect(result.valid).toBe(false); + 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); }); }); @@ -166,4 +141,4 @@ describe("detect-arm-leases", () => { expect(await checkLease("storage", "Microsoft.Storage")).toBe(false); }); }); -}); \ No newline at end of file +}); From 68353dd575dbea24d1240915827285f47bf5bbe8 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 4 Mar 2026 02:05:13 -0600 Subject: [PATCH 039/125] Modified duration, naming structure --- .github/arm-leases/README.md | 35 ++- .../arm-lease-validation.js | 213 ++++++++++-------- .../arm-lease-validation.test.js | 40 ++-- 3 files changed, 160 insertions(+), 128 deletions(-) diff --git a/.github/arm-leases/README.md b/.github/arm-leases/README.md index 7bfc847ddaf3..23c18122f96b 100644 --- a/.github/arm-leases/README.md +++ b/.github/arm-leases/README.md @@ -20,13 +20,13 @@ This directory is intended to be governed via CODEOWNERS to ensure proper govern Each lease must be placed in the following directory structure: ``` -.github/arm-leases///[ (optional)]/lease.yaml +.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) logical grouping within an RP (e.g., DiskRP, ComputeRP). Must not start with "stable" or "preview" +- ``: 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 @@ -34,21 +34,21 @@ The `lease.yaml` file must follow this format: ```yaml lease: - resource-provider: Microsoft.TestRP # Must match the namespace folder name + resource-provider: Microsoft.TestRP # Must match the rpNamespace folder name startdate: 2026-01-07 # ISO 8601 format (YYYY-MM-DD) - duration-days: P180D # ISO 8601 duration (maximum P180D) + duration: P180D # ISO 8601 duration (e.g., P180D, P6M, P1Y2M3D) reviewer: Evan Hissey # Name 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): +Create a file at `.github/arm-leases///(optional)/lease.yaml` with the following content (replace the placeholder values): ```yaml lease: - resource-provider: + resource-provider: startdate: - duration-days: P180D + duration: P180D reviewer: ``` @@ -59,10 +59,10 @@ 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` +- Must follow the folder structure: `//[ (optional)]/lease.yaml` ### 2. Resource Provider Name -- Must match the namespace folder name exactly +- Must match the `` folder name exactly - Example: If folder is `Microsoft.TestRP`, then `resource-provider` must be `Microsoft.TestRP` ### 3. Start Date @@ -71,9 +71,8 @@ All lease files are automatically validated with the following requirements: ### 4. Duration - Required field that cannot be empty -- Must be in ISO 8601 duration format: `P{n}D` (e.g., `P90D`, `P180D`) -- **Cannot exceed P180D** (maximum lease period of 180 days) -- Must be greater than 0 days +- Must be a valid ISO 8601 duration (e.g., `P180D`, `P6M`, `P1Y2M3D`) +- Supports day-based (`P90D`), month-based (`P6M`), and combined formats (`P1Y2M3D`) ### 5. Reviewer - Required field that cannot be empty @@ -84,10 +83,10 @@ All lease files are automatically validated with the following requirements: 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 service name -- **Resource provider mismatch**: The `resource-provider` value must match the namespace folder name exactly +- **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 today or a future date in `YYYY-MM-DD` format -- **Invalid duration**: Use ISO 8601 format `P{n}D` (e.g., `P180D`), maximum `P180D` -- **Missing or empty fields**: All fields (`resource-provider`, `startdate`, `duration-days`, `reviewer`) are required +- **Invalid duration**: Use a valid ISO 8601 duration (e.g., `P180D`, `P6M`, `P1Y2M3D`) +- **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/workflows/src/arm-lease-validation/arm-lease-validation.js b/.github/workflows/src/arm-lease-validation/arm-lease-validation.js index 496b6668ee0e..8d19c07861f3 100644 --- a/.github/workflows/src/arm-lease-validation/arm-lease-validation.js +++ b/.github/workflows/src/arm-lease-validation/arm-lease-validation.js @@ -1,17 +1,19 @@ -import { readFile } from 'fs/promises'; -import { resolve } from 'path'; -import { inspect } from 'util'; -import YAML from 'js-yaml'; -import * as z from 'zod'; -import { getChangedFiles } from '../../../shared/src/changed-files.js'; -import { CoreLogger } from '../core-logger.js'; +import { readFile } from "fs/promises"; +import { resolve } from "path"; +import { inspect } from "util"; +import YAML from "js-yaml"; +import * as z from "zod"; +import { Temporal } from "@js-temporal/polyfill"; +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 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, @@ -27,30 +29,40 @@ export const ALLOWED_FILE_PATTERNS = [ * lease: * resource-provider: Microsoft.Compute * startdate: "2025-06-01" - * duration-days: "P180D" + * duration: "P180D" * reviewer: alias * ``` */ 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)'), - 'duration-days': z.string().regex(/^P(\d+)D$/i, 'Invalid duration-days format (expected ISO 8601: "P180D")').refine( - (d) => { - const m = d.match(/^P(\d+)D$/i); - if (!m) return false; - const days = parseInt(m[1], 10); - return days > 0 && days <= 180; + "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)"), + duration: z.string().refine( + (v) => { + try { + Temporal.Duration.from(v); + return true; + } catch { + return false; + } }, - 'Duration must be between 1 and 180 days', - ), - reviewer: z.string().min(1, 'Reviewer is required and cannot be empty').refine( - (r) => r.trim().length > 0, - 'Reviewer is required and cannot be empty', + "duration must be a valid ISO 8601 duration (e.g. P180D, P6M, P1Y2M3D)", ), + reviewer: z + .string() + .min(1, "Reviewer is required and cannot be empty") + .refine( + (r) => r.trim().length > 0, + "Reviewer is required and cannot be empty", + ), }), }); @@ -59,25 +71,27 @@ export const leaseSchema = z.object({ // ============================================ /** - * Check if a file is allowed based on patterns + * 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)); + return ALLOWED_FILE_PATTERNS.some((pattern) => pattern.test(file)); } /** - * Validate folder structure of lease files + * 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)); + return files.filter( + (file) => !LEASE_FILE_PATTERN.test(file) && !LEASE_FILE_WITH_GROUP_PATTERN.test(file), + ); } /** - * Validate lease file contents using Zod schema + * Validate lease file contents using Zod schema. * @param {string} leaseFile - Full path to lease file * @param {string} today - Today's date in YYYY-MM-DD format * @param {string} relativePath - Relative path for folder name extraction @@ -86,13 +100,14 @@ export function validateFolderStructure(files) { export async function validateLeaseContent(leaseFile, today, relativePath) { const errors = []; const pathForExtraction = relativePath || leaseFile; - // Extract namespace from .github/arm-leases///lease.yaml - // or .github/arm-leases////lease.yaml - const folderRP = pathForExtraction.split('/')[3]; // namespace is always at index 3 + // Extract rpNamespace from .github/arm-leases///lease.yaml + // or .github/arm-leases////lease.yaml + const folderRP = pathForExtraction.split("/")[3]; // rpNamespace is always at index 3 + /** @type {string} */ let content; try { - content = await readFile(leaseFile, 'utf-8'); + content = await readFile(leaseFile, "utf-8"); } catch (error) { return { file: leaseFile, errors: [`Error reading file: ${inspect(error)}`] }; } @@ -117,8 +132,10 @@ export async function validateLeaseContent(leaseFile, today, relativePath) { 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']}`); + if (lease["resource-provider"] !== folderRP) { + errors.push( + `Resource provider mismatch: folder=${folderRP}, yaml=${lease["resource-provider"]}`, + ); } // Cross-field validation: startdate must not be in the past @@ -134,100 +151,115 @@ export async function validateLeaseContent(leaseFile, today, relativePath) { // ============================================ /** - * Main validation logic for GitHub script action + * 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 today = new Date().toISOString().split('T')[0]; + const today = Temporal.Now.plainDateISO().toString(); const cwd = process.env.GITHUB_WORKSPACE; let hasErrors = false; - core.info('Running ARM Lease File Validation\n'); + core.info("Running ARM Lease File Validation"); - // Step 1: Get all changed files under .github/arm-leases/ + // Get all changed files under .github/arm-leases/ const allChangedFiles = await getChangedFiles({ cwd, - paths: ['.github/arm-leases'], + paths: [".github/arm-leases"], logger: new CoreLogger(core), }); - // Step 2: Check for disallowed files - const disallowedFiles = []; - for (const file of allChangedFiles) { - if (!isFileAllowed(file)) { - disallowedFiles.push(file); - } - } + // 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:\n`); - core.info('Disallowed files:'); - disallowedFiles.slice(0, 20).forEach(file => core.info(` ${file}`)); + 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`); } - core.info(''); hasErrors = true; } + core.endGroup(); - // Step 3: Check for non-lease.yaml and non-README files - const nonLeaseFiles = allChangedFiles.filter(file => - !file.endsWith('/lease.yaml') && !file.endsWith('/README.md') + // 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:`); - nonLeaseFiles.forEach(file => core.info(`Remove or rename - ${file}`)); - core.info('Only lease.yaml files are allowed in .github/arm-leases/ directory\n'); + 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; } + core.endGroup(); - // Step 4: Get ARM lease files (only lease.yaml files) - const armLeaseFiles = allChangedFiles.filter(file => - file.startsWith('.github/arm-leases/') && !file.endsWith('.md') + // 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 ------------'); + core.info("No ARM lease files to validate"); } else { - core.setFailed('ARM Lease Validation failed - fix errors above'); + core.setFailed("ARM Lease Validation failed - fix errors above"); } - return { status: hasErrors ? 'failed' : 'no-lease-files', errors: hasErrors ? 1 : 0 }; + return { status: hasErrors ? "failed" : "no-lease-files", errors: hasErrors ? 1 : 0 }; } - // Step 5: Validate folder structure + // 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:`); - invalidStructure.forEach(file => 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) logical grouping 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\n'); + 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; } + core.endGroup(); - // Step 6: Validate lease file contents - const validLeaseFiles = armLeaseFiles.filter(file => - LEASE_FILE_PATTERN.test(file) || LEASE_FILE_WITH_GROUP_PATTERN.test(file) + // 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'); + core.setFailed("ARM Lease Validation failed - fix errors above"); } else { - core.info('All validations passed!'); + core.info("All validations passed!"); } - return { status: hasErrors ? 'failed' : 'passed', errors: hasErrors ? 1 : 0 }; + return { status: hasErrors ? "failed" : "passed", errors: hasErrors ? 1 : 0 }; } const contentErrors = []; @@ -241,21 +273,22 @@ export default async function validateArmLeases(core) { } } - // Step 7: Print lease file content errors if (contentErrors.length > 0) { - core.info('Lease content validation errors:\n'); - contentErrors.forEach(({ file, errors }) => { + core.info("Lease content validation errors:"); + for (const { file, errors } of contentErrors) { core.info(`${file}`); - errors.forEach(error => core.info(` - ${error}`)); - core.info(''); - }); + for (const error of errors) { + core.info(` - ${error}`); + } + } } + core.endGroup(); if (hasErrors) { - core.setFailed('ARM Lease Validation failed - fix errors above'); + core.setFailed("ARM Lease Validation failed - fix errors above"); } else { - core.info('All validations passed!'); + core.info("All validations passed!"); } - return { status: hasErrors ? 'failed' : 'passed', errors: contentErrors.length }; + return { status: hasErrors ? "failed" : "passed", errors: contentErrors.length }; } 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 index 7a531d8c9555..9679451bdeae 100644 --- a/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js +++ b/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js @@ -1,11 +1,11 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -// Mock fs/promises before imports -vi.mock("fs/promises", () => ({ readFile: vi.fn() })); +/** @type {import("vitest").MockedFunction} */ +const mockReadFile = vi.hoisted(() => vi.fn()); -import * as fs from "fs/promises"; - -const mockReadFile = /** @type {import("vitest").Mock} */ (fs.readFile); +vi.mock("fs/promises", () => ({ + readFile: mockReadFile, +})); import { isFileAllowed, @@ -96,7 +96,7 @@ describe("validate-arm-leases", () => { lease: { "resource-provider": "Microsoft.Test", startdate: "2026-06-01", - "duration-days": "P90D", + duration: "P90D", reviewer: "John Doe", }, }; @@ -108,7 +108,7 @@ describe("validate-arm-leases", () => { lease: { "resource-provider": "microsoft.Test", startdate: "2026-06-01", - "duration-days": "P90D", + duration: "P90D", reviewer: "John Doe", }, }; @@ -120,47 +120,47 @@ describe("validate-arm-leases", () => { lease: { "resource-provider": "Microsoft.Test", startdate: "01-15-2026", - "duration-days": "P90D", + duration: "P90D", reviewer: "John Doe", }, }; expect(leaseSchema.safeParse(invalid).success).toBe(false); }); - it("rejects invalid duration-days format", () => { + it("rejects invalid duration format", () => { const invalid = { lease: { "resource-provider": "Microsoft.Test", startdate: "2026-06-01", - "duration-days": "90 days", + duration: "90 days", reviewer: "John Doe", }, }; expect(leaseSchema.safeParse(invalid).success).toBe(false); }); - it("rejects duration exceeding 180 days", () => { - const invalid = { + it("accepts month-based durations like P6M", () => { + const valid = { lease: { "resource-provider": "Microsoft.Test", startdate: "2026-06-01", - "duration-days": "P200D", + duration: "P6M", reviewer: "John Doe", }, }; - expect(leaseSchema.safeParse(invalid).success).toBe(false); + expect(leaseSchema.safeParse(valid).success).toBe(true); }); - it("rejects zero duration", () => { - const invalid = { + it("accepts combined durations like P1Y2M3D", () => { + const valid = { lease: { "resource-provider": "Microsoft.Test", startdate: "2026-06-01", - "duration-days": "P0D", + duration: "P1Y2M3D", reviewer: "John Doe", }, }; - expect(leaseSchema.safeParse(invalid).success).toBe(false); + expect(leaseSchema.safeParse(valid).success).toBe(true); }); it("rejects empty reviewer", () => { @@ -168,7 +168,7 @@ describe("validate-arm-leases", () => { lease: { "resource-provider": "Microsoft.Test", startdate: "2026-06-01", - "duration-days": "P90D", + duration: "P90D", reviewer: "", }, }; @@ -184,7 +184,7 @@ describe("validate-arm-leases", () => { const validYaml = `lease: resource-provider: Microsoft.Test startdate: "2027-06-01" - duration-days: P90D + duration: P90D reviewer: John Doe `; From 597f0ea46b879f973b01f5d2bd9b0888f0c411cd Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 4 Mar 2026 02:11:07 -0600 Subject: [PATCH 040/125] update package --- .github/package-lock.json | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/.github/package-lock.json b/.github/package-lock.json index 3bb7c9acc9fb..51067fb5f71e 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", @@ -923,6 +924,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", @@ -954,6 +967,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", @@ -1269,6 +1283,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", @@ -1820,7 +1835,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", @@ -1835,6 +1851,7 @@ "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1891,6 +1908,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -2263,6 +2281,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2506,6 +2525,7 @@ "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -2991,6 +3011,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", @@ -3268,6 +3294,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3320,6 +3347,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -3586,6 +3614,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3661,6 +3690,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -3736,6 +3766,7 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", From 30aca2d8ea3e721808f3a9063b35932bd09690cc Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 4 Mar 2026 02:11:25 -0600 Subject: [PATCH 041/125] update package --- .github/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/package.json b/.github/package.json index 8587d5e03117..3a4503a88805 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" }, From 49b4961c47cd8b47cd8e9a8ae6005411fda5ccde Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 4 Mar 2026 02:14:17 -0600 Subject: [PATCH 042/125] update lease file --- .github/arm-leases/testservice/Microsoft.TestRP/lease.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml b/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml index 9647ac1de26c..be8580da244e 100644 --- a/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml +++ b/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml @@ -1,5 +1,5 @@ lease: resource-provider: Microsoft.TestRP startdate: 2026-07-15 # ISO 8601 format (YYYY-MM-DD) - duration-days: P180D # ISO 8601 duration (maximum P180D) + duration: P180D # ISO 8601 duration (e.g., P180D, P6M, P1Y2M3D) reviewer: Tejaswi Salaigari From 7c650525b59cb2643511f8f0ca1fd1f6d20303d7 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 4 Mar 2026 02:26:46 -0600 Subject: [PATCH 043/125] renamed file name --- .../{arm-lease-validation.yaml => arm-modeling-review.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{arm-lease-validation.yaml => arm-modeling-review.yaml} (100%) diff --git a/.github/workflows/arm-lease-validation.yaml b/.github/workflows/arm-modeling-review.yaml similarity index 100% rename from .github/workflows/arm-lease-validation.yaml rename to .github/workflows/arm-modeling-review.yaml From 1722b2a5599574d610b834844bd06be656c41265 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Thu, 5 Mar 2026 17:33:03 -0600 Subject: [PATCH 044/125] Modifed few changes after testing in feature branch --- .github/workflows/arm-modeling-review.yaml | 26 ++- .../detect-new-resource-provider.js | 37 +++- .../detect-new-resource-types.js | 195 +++++++++++++----- .../src/summarize-checks/labelling.js | 9 +- .../detect-new-resource-types.test.js | 169 +++++++++++++-- 5 files changed, 358 insertions(+), 78 deletions(-) diff --git a/.github/workflows/arm-modeling-review.yaml b/.github/workflows/arm-modeling-review.yaml index 2a79f5e6b55c..cb22c52c8a93 100644 --- a/.github/workflows/arm-modeling-review.yaml +++ b/.github/workflows/arm-modeling-review.yaml @@ -29,9 +29,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 with: - fetch-depth: 2 + fetch-depth: 0 # Fetch all history for merge-base calculation and diffing sparse-checkout: | .github + specification + eng - name: Install dependencies for github-script actions uses: ./.github/actions/install-deps-github-script @@ -43,25 +45,33 @@ jobs: script: | const { default: armModelingReview } = await import('${{ github.workspace }}/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js'); - return await armModelingReview({ context, core }); + const result = await armModelingReview({ context, core }); + return result; - - if: fromJson(steps.detect.outputs.result).labelActions['ARMModelingReviewRequired'] != 'none' - name: Upload artifact for ARMModelingReviewRequired label + - 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' }}" - - if: fromJson(steps.detect.outputs.result).labelActions['ARMModelingSignedOff'] != 'none' - name: Upload artifact for ARMModelingSignedOff label + - 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' }}" - - if: fromJson(steps.detect.outputs.result).labelActions['ARMModelingAutoSignedOff'] != 'none' - name: Upload artifact for ARMModelingAutoSignedOff label + - 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' }}" + + - name: Upload artifact for issue_number + uses: ./.github/actions/add-empty-artifact + with: + name: issue-number + value: "${{ github.event.pull_request.number }}" + \ No newline at end of file diff --git a/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js b/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js index 71f8b7ed7fa5..0c487c00a740 100644 --- a/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js +++ b/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js @@ -141,8 +141,21 @@ export default async function detectNewResourceProvider({ context, core }) { core.info("Detecting New Resource Providers"); + // Determine merge base between HEAD and the base branch (usually main) + // This ensures we detect all changes introduced in the PR, not just the last commit. + let mergeBase = "HEAD^"; + try { + const baseRef = context.payload.pull_request?.base?.sha || "main"; + mergeBase = await git.raw(["merge-base", baseRef, "HEAD"]); + mergeBase = mergeBase.trim(); + core.info(`Using merge-base: ${mergeBase} (against ${baseRef})`); + } catch (e) { + core.info(`Could not determine merge-base, falling back to HEAD^. Error: ${e.message}`); + } + const options = { cwd: process.env.GITHUB_WORKSPACE, + baseCommitish: "HEAD^", paths: ["specification"], logger: new CoreLogger(core), }; @@ -203,7 +216,7 @@ export default async function detectNewResourceProvider({ context, core }) { 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(repoRoot, rmFiles, core); + return await checkNewResourceTypes(repoRoot, mergeBase, rmFiles, core); } } @@ -224,7 +237,7 @@ export default async function detectNewResourceProvider({ context, core }) { if (newResourceProviders.length === 0) { core.info("No new resource providers detected."); core.info("Checking for new resource types in existing RPs..."); - return await checkNewResourceTypes(repoRoot, rmFiles, core); + return await checkNewResourceTypes(repoRoot, mergeBase, rmFiles, core); } core.info(`Detected ${newResourceProviders.length} new resource provider(s)`); @@ -261,9 +274,22 @@ export default async function detectNewResourceProvider({ context, core }) { 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(repoRoot, mergeBase, 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 [label, action] of Object.entries(newRtResult.labelActions)) { + 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: getLabelActions(allLeasesValid ? "auto-signed-off" : "review-required"), + labelActions: combinedLabelActions, }; } @@ -271,14 +297,15 @@ export default async function detectNewResourceProvider({ context, core }) { * Check for new resource types in existing RPs and validate their leases. * * @param {string} repoRoot - Repository root directory + * @param {string} mergeBase - Git merge base SHA * @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(repoRoot, rmFiles, core) { +async function checkNewResourceTypes(repoRoot, mergeBase, rmFiles, core) { const newRtResults = await detectNewResourceTypes({ repoRoot, - mergeBase: "HEAD^", + mergeBase, rmFiles, core, }); diff --git a/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js b/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js index a21b9f1ea111..1f69042bb05f 100644 --- a/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js +++ b/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js @@ -9,47 +9,126 @@ import { resolve } from "path"; import { simpleGit } from "simple-git"; import { ArmHelper } from "@microsoft.azure/openapi-validator-rulesets/dist/native/utilities/arm-helper.js"; import { SwaggerInventory } from "@microsoft.azure/openapi-validator-core"; +import debug from "debug"; -// Match: specification//resource-manager//(stable|preview)// -const VERSION_PATTERN = - /^specification\/([^/]+)\/resource-manager\/([^/]+)\/(stable|preview)\/([^/]+)\//; +// Disable simple-git debug logging to remove verbose [GitExecutor] logs from the output +debug.disable(); -const RESOURCE_TYPE_REGEX = /\/providers\/([^/]+\/[^/\{]+)/; +// Match pattern: specification//resource-manager//... +const RESOURCE_MANAGER_PATTERN = /^specification\/[^\/]+\/resource-manager\/([^\/]+)\//; -/** Extract resource type from API path (e.g. Microsoft.Compute/virtualMachines) */ +const RESOURCE_TYPE_REGEX = /\/providers\/([^/?#]+)(\/[^?#]*)?/; + +/** + * Extract resource type from API path (e.g. Microsoft.Compute/virtualMachines/extensions) + * @param {string} apiPath + * @returns {string | null} + */ function getResourceType(apiPath) { - return apiPath.match(RESOURCE_TYPE_REGEX)?.[1] || null; + 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 using openapi-validator's ArmHelper. * + * Primary: ArmHelper detects resources based on definitions with x-ms-azure-resource or + * ARM base types. Fallback: path-based detection for specs using inline schemas. + * * @param {Object} swaggerDoc - Parsed swagger JSON document * @param {string} specPath - Absolute path to the swagger file - * @returns {Map}>} + * @returns {Map}>} */ function getResourceTypesFromSwagger(swaggerDoc, specPath) { - const armHelper = new ArmHelper( - swaggerDoc, - resolve(specPath), - new SwaggerInventory(), - ); const resourceTypes = new Map(); - for (const resource of armHelper.getAllResources()) { - const firstOp = resource.operations[0]; - const resourceType = getResourceType(firstOp.apiPath); - - if (resourceType && !resourceTypes.has(resourceType)) { - resourceTypes.set(resourceType, { - resourceType, - provider: resourceType.split("/")[0], - modelName: resource.modelName, - operations: resource.operations.map((op) => ({ - method: op.httpMethod, - apiPath: op.apiPath, - })), - }); + // ArmHelper requires a populated SwaggerInventory to resolve cross-file $refs. + // When used with a fresh empty inventory (as here), it throws "Node does not exist" + // during getAllResources(). Wrap in try/catch so we always fall through to the + // path-based fallback when ArmHelper cannot run. + try { + const armHelper = new ArmHelper( + swaggerDoc, + resolve(specPath), + new SwaggerInventory(), + ); + + for (const resource of armHelper.getAllResources()) { + const firstOp = resource.operations[0]; + const resourceType = getResourceType(firstOp.apiPath); + + if (resourceType && !resourceTypes.has(resourceType)) { + resourceTypes.set(resourceType, { + resourceType, + provider: resourceType.split("/")[0], + modelName: resource.modelName, + operations: resource.operations.map((op) => ({ + method: op.httpMethod, + apiPath: op.apiPath, + })), + }); + } + } + } catch { + // ArmHelper failed (e.g. empty SwaggerInventory throws "Node does not exist"). + // Fall through to the path-based fallback below. + } + + // Fallback: when ArmHelper finds no resources (e.g. spec uses inline response schemas + // instead of $ref to named definitions, or ArmHelper threw), scan paths directly for + // ARM resource patterns. + if (resourceTypes.size === 0 && swaggerDoc.paths) { + /** @type {Record>} */ + const paths = /** @type {any} */ (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(/** @type {Record} */ (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); + } + } + } } } @@ -63,7 +142,7 @@ function getResourceTypesFromSwagger(swaggerDoc, specPath) { * @param {string} commitish - Git ref * @param {string} namespacePath - e.g. `specification/compute/resource-manager/Microsoft.Compute` * @param {string} repoRoot - Repository root directory - * @returns {Promise>} Map of resource type to info + * @returns {Promise | null>} Map of resource type to info, or null if path doesn't exist at this ref */ async function getResourceTypesAtRef(git, commitish, namespacePath, repoRoot) { const allTypes = new Map(); @@ -78,24 +157,38 @@ async function getResourceTypesAtRef(git, commitish, namespacePath, repoRoot) { namespacePath, ]); } catch { - return allTypes; // path doesn't exist at this ref + return null; // path doesn't exist at this ref } const swaggerFiles = output .split("\n") - .filter((f) => f.endsWith(".json") && !f.includes("/examples/")); + .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 base branch 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...`); + } - for (const file of swaggerFiles) { - /** @type {string} */ let content; try { - content = await git.show([`${commitish}:${file}`]); - } catch { + content = await git.show([`${commitish}:${file.trim()}`]); + } catch (e) { + // Intentionally quiet to avoid log spam during comparison continue; } try { const swaggerDoc = JSON.parse(content); + // Skip files that aren't ARM resource-manager specs (minimal check) + if (!swaggerDoc.paths) continue; const types = getResourceTypesFromSwagger( swaggerDoc, resolve(repoRoot, file), @@ -135,27 +228,33 @@ export async function detectNewResourceTypes({ }) { const git = simpleGit(repoRoot); - // Group changed RM swagger files by namespace - /** @type {Map} */ + // Group changed RM swagger files by service subdirectory (namespace + service group) + /** @type {Map} */ const namespaceMap = new Map(); for (const file of rmFiles) { - const match = file.match(VERSION_PATTERN); - if (!match) continue; + // Use RESOURCE_MANAGER_PATTERN to ensure the file is inside a namespace directory + // (the trailing "/" in the pattern requires at least one path component after the namespace) + const rmMatch = file.match(RESOURCE_MANAGER_PATTERN); + if (!rmMatch) continue; - const orgName = match[1]; - const namespace = match[2]; - const namespacePath = `specification/${orgName}/resource-manager/${namespace}`; + const parts = file.split("/"); + const orgName = parts[1]; + const namespace = parts[3]; + // Scope to the service subdirectory (e.g. DiskRP) to avoid scanning the entire namespace + const serviceGroup = parts[4]; + const namespacePath = `specification/${orgName}/resource-manager/${namespace}/${serviceGroup}`; + const serviceKey = `${namespace}/${serviceGroup}`; - if (!namespaceMap.has(namespace)) { - namespaceMap.set(namespace, { orgName, namespacePath }); + if (!namespaceMap.has(serviceKey)) { + namespaceMap.set(serviceKey, { orgName, namespacePath, namespace }); } } const results = []; - for (const [namespace, { orgName, namespacePath }] of namespaceMap) { - core.info(`Checking for new resource types in ${namespace}...`); + for (const [serviceKey, { orgName, namespacePath, namespace }] of namespaceMap) { + core.info(`Checking for new resource types in ${serviceKey}...`); const baseTypes = await getResourceTypesAtRef( git, @@ -164,9 +263,9 @@ export async function detectNewResourceTypes({ repoRoot, ); - // If the namespace didn't exist in base, skip — that's a new RP, handled elsewhere - if (baseTypes.size === 0) { - core.info(` ${namespace}: no resources in base (new RP, skipping)`); + // Skip namespace if it doesn't exist in base (brand new RP — handled by RP-level detection) + if (baseTypes === null) { + core.info(` ${namespace}: no resources in base (new namespace), skipping RT detection`); continue; } @@ -178,7 +277,7 @@ export async function detectNewResourceTypes({ ); const newTypes = []; - for (const [type, info] of headTypes) { + for (const [type, info] of (headTypes ?? new Map())) { if (!baseTypes.has(type)) { newTypes.push({ resourceType: type, diff --git a/.github/workflows/src/summarize-checks/labelling.js b/.github/workflows/src/summarize-checks/labelling.js index 18106bab4a07..81c3155dfb0f 100644 --- a/.github/workflows/src/summarize-checks/labelling.js +++ b/.github/workflows/src/summarize-checks/labelling.js @@ -651,9 +651,10 @@ function processARMReviewWorkflowLabels( ciRpaasRPNotInPrivateRepoLabelShouldBePresent, ); - // Block if ARMModelingReviewRequired is present (new RP namespace detected) + // Block if ARMModelingReviewRequired is present (new RP namespace or new resource type detected) const armModelingReviewLabel = new Label("ARMModelingReviewRequired", labelContext.present); - const blockedOnArmModeling = armModelingReviewLabel.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; @@ -896,8 +897,8 @@ const rulesPri0NotReadyForArmReview = [ anyRequiredLabels: [], troubleshootingGuide: wrapInArmReviewMessage( "This PR has ARMModelingReviewRequired label. " + - "This means it is introducing a new Resource Provider namespace. " + - "New RPs require a discussion with the ARM Modelling Review team before merging.
" + + "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 Modelling 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")}.`, ), diff --git a/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js b/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js index 9965dee346df..ed017b2e0076 100644 --- a/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js +++ b/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js @@ -145,10 +145,11 @@ describe("detectNewResourceTypes", () => { 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`; + const servicePath = `${ns}/stable`; setupGit({ - baseFiles: new Map([[ns, [file]]]), - headFiles: new Map([[ns, [file]]]), + baseFiles: new Map([[servicePath, [file]]]), + headFiles: new Map([[servicePath, [file]]]), fileContents: new Map([ [`base123:${file}`, swaggerContent], [`HEAD:${file}`, swaggerContent], @@ -171,10 +172,11 @@ describe("detectNewResourceTypes", () => { 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`; + const servicePath = `${ns}/stable`; setupGit({ - baseFiles: new Map([[ns, [file]]]), - headFiles: new Map([[ns, [file]]]), + baseFiles: new Map([[servicePath, [file]]]), + headFiles: new Map([[servicePath, [file]]]), fileContents: new Map([ [`base123:${file}`, swaggerContent], [`HEAD:${file}`, swaggerContent], @@ -210,15 +212,17 @@ describe("detectNewResourceTypes", () => { 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`; + const computeServicePath = `${computeNs}/stable`; + const networkServicePath = `${networkNs}/stable`; setupGit({ baseFiles: new Map([ - [computeNs, [computeFile]], - [networkNs, [networkFile]], + [computeServicePath, [computeFile]], + [networkServicePath, [networkFile]], ]), headFiles: new Map([ - [computeNs, [computeFile]], - [networkNs, [networkFile]], + [computeServicePath, [computeFile]], + [networkServicePath, [networkFile]], ]), fileContents: new Map([ [`base123:${computeFile}`, swaggerContent], @@ -246,10 +250,11 @@ describe("detectNewResourceTypes", () => { 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`; + const servicePath = `${ns}/stable`; setupGit({ - baseFiles: new Map([[ns, [file, exampleFile]]]), - headFiles: new Map([[ns, [file, exampleFile]]]), + baseFiles: new Map([[servicePath, [file, exampleFile]]]), + headFiles: new Map([[servicePath, [file, exampleFile]]]), fileContents: new Map([ [`base123:${file}`, swaggerContent], [`HEAD:${file}`, swaggerContent], @@ -273,11 +278,12 @@ describe("detectNewResourceTypes", () => { 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}/readme.md`; + const readme = `${ns}/stable/readme.md`; + const servicePath = `${ns}/stable`; setupGit({ - baseFiles: new Map([[ns, [file, readme]]]), - headFiles: new Map([[ns, [file, readme]]]), + baseFiles: new Map([[servicePath, [file, readme]]]), + headFiles: new Map([[servicePath, [file, readme]]]), fileContents: new Map([ [`base123:${file}`, swaggerContent], [`HEAD:${file}`, swaggerContent], @@ -298,4 +304,141 @@ describe("detectNewResourceTypes", () => { ); }); + it("detects new resource types via path-based fallback when ArmHelper finds nothing (inline schemas)", 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([ + [`base123:${existingFile}`, existingSwagger], + [`HEAD:${existingFile}`, existingSwagger], + [`HEAD:${newFile}`, newSwagger], + ]), + }); + + // ArmHelper returns nothing (inline schemas, no definitions) + mockGetAllResources.mockReturnValue([]); + + const result = await detectNewResourceTypes({ + repoRoot: "/fake/repo", + mergeBase: "base123", + rmFiles: [newFile], + core, + }); + + expect(result).toHaveLength(1); + expect(result[0].namespace).toBe("Microsoft.Compute"); + // superDisks should be detected via path fallback + 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 but child of superDisks + const metricsType = result[0].newResourceTypes.find((t) => + t.resourceType === "Microsoft.Compute/superDisks/metrics", + ); + expect(metricsType).toBeDefined(); + }); + + it("detects new resource types via path-based fallback when ArmHelper throws (e.g. empty SwaggerInventory)", 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" } } }, + }, + }, + }); + + setupGit({ + baseFiles: new Map([[`${ns}/DiskRP`, [existingFile]]]), + headFiles: new Map([[`${ns}/DiskRP`, [existingFile, newFile]]]), + fileContents: new Map([ + [`base123:${existingFile}`, existingSwagger], + [`HEAD:${existingFile}`, existingSwagger], + [`HEAD:${newFile}`, newSwagger], + ]), + }); + + // ArmHelper throws (simulating the real "Node does not exist" error from an + // empty SwaggerInventory — the actual failure mode in production) + mockGetAllResources.mockImplementation(() => { + throw new Error("Node does not exist: file:///path/to/spec.json"); + }); + + const result = await detectNewResourceTypes({ + repoRoot: "/fake/repo", + mergeBase: "base123", + rmFiles: [newFile], + core, + }); + + expect(result).toHaveLength(1); + expect(result[0].namespace).toBe("Microsoft.Compute"); + const superDisksType = result[0].newResourceTypes.find((t) => + t.resourceType === "Microsoft.Compute/superDisks", + ); + expect(superDisksType).toBeDefined(); + expect(superDisksType.operations).toContain("GET"); + }); + + it("path-based fallback 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([ + [`base123:${existingPreviewFile}`, existingSwagger], + [`HEAD:${existingPreviewFile}`, existingSwagger], + [`HEAD:${newFile}`, operationsOnlySwagger], + ]), + }); + + mockGetAllResources.mockReturnValue([]); + + const result = await detectNewResourceTypes({ + repoRoot: "/fake/repo", + mergeBase: "base123", + rmFiles: [newFile], + core, + }); + + expect(result).toEqual([]); + }); + }); From fde0911e932017a74685f692b206174e3cca8b81 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Mon, 9 Mar 2026 16:21:16 -0700 Subject: [PATCH 045/125] Apply suggestion from @mikeharder --- .github/workflows/arm-modeling-review.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/arm-modeling-review.yaml b/.github/workflows/arm-modeling-review.yaml index cb22c52c8a93..80eee64dddb6 100644 --- a/.github/workflows/arm-modeling-review.yaml +++ b/.github/workflows/arm-modeling-review.yaml @@ -73,5 +73,4 @@ jobs: uses: ./.github/actions/add-empty-artifact with: name: issue-number - value: "${{ github.event.pull_request.number }}" - \ No newline at end of file + value: "${{ github.event.pull_request.number }}" \ No newline at end of file From cc9dd22569dadf00628d1b14c485b7275b65b5be Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Mon, 9 Mar 2026 16:21:40 -0700 Subject: [PATCH 046/125] Apply suggestion from @mikeharder --- .github/workflows/arm-modeling-review.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/arm-modeling-review.yaml b/.github/workflows/arm-modeling-review.yaml index 80eee64dddb6..21aca07fd690 100644 --- a/.github/workflows/arm-modeling-review.yaml +++ b/.github/workflows/arm-modeling-review.yaml @@ -73,4 +73,4 @@ jobs: uses: ./.github/actions/add-empty-artifact with: name: issue-number - value: "${{ github.event.pull_request.number }}" \ No newline at end of file + value: "${{ github.event.pull_request.number }}" From 3542e7a4cc73fc2a352150d4429cde93670a489a Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 10 Mar 2026 03:21:27 +0000 Subject: [PATCH 047/125] move temporal to correct package.json --- .github/package.json | 1 + .github/workflows/package.json | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 .github/workflows/package.json diff --git a/.github/package.json b/.github/package.json index 8587d5e03117..d1b6574af4d7 100644 --- a/.github/package.json +++ b/.github/package.json @@ -17,6 +17,7 @@ "devDependencies": { "@actions/github-script": "github:actions/github-script", "@eslint/js": "^10.0.0", + "@js-temporal/polyfill": "0.5.1", "@octokit/endpoint": "^11.0.0", "@octokit/rest": "^22.0.0", "@octokit/types": "^16.0.0", diff --git a/.github/workflows/package.json b/.github/workflows/package.json deleted file mode 100644 index 340ac5c3c8c1..000000000000 --- a/.github/workflows/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "@js-temporal/polyfill": "^0.5.1" - } -} From dca5a121d6aaafc338833a5b2f4dc12a3daae425 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 10 Mar 2026 03:21:55 +0000 Subject: [PATCH 048/125] npm i --- .github/package-lock.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/package-lock.json b/.github/package-lock.json index 3bb7c9acc9fb..6bd95adfa0b1 100644 --- a/.github/package-lock.json +++ b/.github/package-lock.json @@ -16,6 +16,7 @@ "devDependencies": { "@actions/github-script": "github:actions/github-script", "@eslint/js": "^10.0.0", + "@js-temporal/polyfill": "0.5.1", "@octokit/endpoint": "^11.0.0", "@octokit/rest": "^22.0.0", "@octokit/types": "^16.0.0", @@ -923,6 +924,19 @@ "@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==", + "dev": true, + "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", @@ -2991,6 +3005,13 @@ "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==", + "dev": true, + "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", From 1f6870b5fbf2d4120b3ea99fc327df03066d4899 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 10 Mar 2026 03:25:37 +0000 Subject: [PATCH 049/125] move temporal to runtime dep --- .github/package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/package-lock.json b/.github/package-lock.json index 6bd95adfa0b1..3d337675df6b 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", @@ -16,7 +17,6 @@ "devDependencies": { "@actions/github-script": "github:actions/github-script", "@eslint/js": "^10.0.0", - "@js-temporal/polyfill": "0.5.1", "@octokit/endpoint": "^11.0.0", "@octokit/rest": "^22.0.0", "@octokit/types": "^16.0.0", From 0b978355b94078cdf2f03fa61be21dc10c610918 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 10 Mar 2026 03:25:56 +0000 Subject: [PATCH 050/125] npm i --- .github/package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/package-lock.json b/.github/package-lock.json index 3d337675df6b..6bd95adfa0b1 100644 --- a/.github/package-lock.json +++ b/.github/package-lock.json @@ -6,7 +6,6 @@ "": { "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", @@ -17,6 +16,7 @@ "devDependencies": { "@actions/github-script": "github:actions/github-script", "@eslint/js": "^10.0.0", + "@js-temporal/polyfill": "0.5.1", "@octokit/endpoint": "^11.0.0", "@octokit/rest": "^22.0.0", "@octokit/types": "^16.0.0", From 9237135e9abd8fc534d7a96a0676e9068537a403 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 10 Mar 2026 03:27:23 +0000 Subject: [PATCH 051/125] actually move temporal to runtime dep --- .github/package-lock.json | 4 +--- .github/package.json | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/package-lock.json b/.github/package-lock.json index 6bd95adfa0b1..6330764473fe 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", @@ -16,7 +17,6 @@ "devDependencies": { "@actions/github-script": "github:actions/github-script", "@eslint/js": "^10.0.0", - "@js-temporal/polyfill": "0.5.1", "@octokit/endpoint": "^11.0.0", "@octokit/rest": "^22.0.0", "@octokit/types": "^16.0.0", @@ -928,7 +928,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/@js-temporal/polyfill/-/polyfill-0.5.1.tgz", "integrity": "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==", - "dev": true, "license": "ISC", "dependencies": { "jsbi": "^4.3.0" @@ -3009,7 +3008,6 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.2.tgz", "integrity": "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==", - "dev": true, "license": "Apache-2.0" }, "node_modules/json-buffer": { diff --git a/.github/package.json b/.github/package.json index d1b6574af4d7..3034f38a2cab 100644 --- a/.github/package.json +++ b/.github/package.json @@ -7,6 +7,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", "marked": "^17.0.0", @@ -17,7 +18,6 @@ "devDependencies": { "@actions/github-script": "github:actions/github-script", "@eslint/js": "^10.0.0", - "@js-temporal/polyfill": "0.5.1", "@octokit/endpoint": "^11.0.0", "@octokit/rest": "^22.0.0", "@octokit/types": "^16.0.0", From 513f3366a0b34e79192383a140d7d1b38cec98f6 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 10 Mar 2026 03:32:58 +0000 Subject: [PATCH 052/125] sort deps --- .github/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/package.json b/.github/package.json index 3034f38a2cab..895c3aef52dc 100644 --- a/.github/package.json +++ b/.github/package.json @@ -10,8 +10,8 @@ "@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" }, From f5c0151bab5580022cb38b15c8532278b0ce8cae Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Mon, 9 Mar 2026 20:33:13 -0700 Subject: [PATCH 053/125] Apply suggestion from @mikeharder --- .github/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/package.json b/.github/package.json index 3a4503a88805..895c3aef52dc 100644 --- a/.github/package.json +++ b/.github/package.json @@ -7,7 +7,7 @@ }, "dependencies": { "@apidevtools/json-schema-ref-parser": "^15.1.3", - "@js-temporal/polyfill": "^0.5.1", + "@js-temporal/polyfill": "0.5.1", "debug": "^4.4.3", "js-yaml": "^4.1.0", "markdown-table": "^3.0.4", From a1c9624c796441c04a359c40b6dcc6904036bfb6 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 10 Mar 2026 03:33:37 +0000 Subject: [PATCH 054/125] npm i --- .github/package-lock.json | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/package-lock.json b/.github/package-lock.json index 51067fb5f71e..6330764473fe 100644 --- a/.github/package-lock.json +++ b/.github/package-lock.json @@ -6,7 +6,7 @@ "": { "dependencies": { "@apidevtools/json-schema-ref-parser": "^15.1.3", - "@js-temporal/polyfill": "^0.5.1", + "@js-temporal/polyfill": "0.5.1", "debug": "^4.4.3", "js-yaml": "^4.1.0", "markdown-table": "^3.0.4", @@ -967,7 +967,6 @@ "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", @@ -1283,7 +1282,6 @@ "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", @@ -1835,8 +1833,7 @@ "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", - "peer": true + "license": "MIT" }, "node_modules/@types/ms": { "version": "2.1.0", @@ -1851,7 +1848,6 @@ "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1908,7 +1904,6 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -2281,7 +2276,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2525,7 +2519,6 @@ "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -3294,7 +3287,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3347,7 +3339,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -3614,7 +3605,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3690,7 +3680,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -3766,7 +3755,6 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", From 8d2f6e4fa700e8123aec0f534834b1893e451b8d Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 11 Mar 2026 17:10:52 -0500 Subject: [PATCH 055/125] Addressed the comment --- .github/arm-leases/README.md | 22 +- .../testservice/Microsoft.TestRP/lease.yaml | 5 - .../widget/Microsoft.Widget/lease.yaml | 5 + .../arm-lease-validation.js | 82 ++++-- .../arm-lease-validation.test.js | 235 ++++++------------ 5 files changed, 153 insertions(+), 196 deletions(-) delete mode 100644 .github/arm-leases/testservice/Microsoft.TestRP/lease.yaml create mode 100644 .github/arm-leases/widget/Microsoft.Widget/lease.yaml diff --git a/.github/arm-leases/README.md b/.github/arm-leases/README.md index 23c18122f96b..a5fa799d0ff4 100644 --- a/.github/arm-leases/README.md +++ b/.github/arm-leases/README.md @@ -4,7 +4,7 @@ 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 in the lease file must also be a PM who has reviewed and approved the lease. +**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 @@ -36,8 +36,8 @@ The `lease.yaml` file must follow this format: 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 (e.g., P180D, P6M, P1Y2M3D) - reviewer: Evan Hissey # Name of the approving reviewer + duration: P180D # ISO 8601 duration (max 180 days, e.g., P180D, P90D, P5M) + reviewer: "@evanhissey" # GitHub alias of the approving reviewer ``` ### Copy-Paste Template @@ -49,7 +49,7 @@ lease: resource-provider: startdate: duration: P180D - reviewer: + reviewer: "@your-github-alias" ``` @@ -67,17 +67,17 @@ All lease files are automatically validated with the following requirements: ### 3. Start Date - Must be in ISO 8601 format: `YYYY-MM-DD` -- Cannot be in the past (must be today or a future date) +- Must be a valid calendar date ### 4. Duration - Required field that cannot be empty -- Must be a valid ISO 8601 duration (e.g., `P180D`, `P6M`, `P1Y2M3D`) -- Supports day-based (`P90D`), month-based (`P6M`), and combined formats (`P1Y2M3D`) +- 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 contain the name of the PM who approved the lease -- Only PMs can be listed as reviewers +- Must be a GitHub alias starting with `@` (e.g., `@octocat`) ## Troubleshooting @@ -85,8 +85,8 @@ If your PR check **"ARM Lease Validation"** is failing, review the error message - **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 today or a future date in `YYYY-MM-DD` format -- **Invalid duration**: Use a valid ISO 8601 duration (e.g., `P180D`, `P6M`, `P1Y2M3D`) +- **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/testservice/Microsoft.TestRP/lease.yaml b/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml deleted file mode 100644 index be8580da244e..000000000000 --- a/.github/arm-leases/testservice/Microsoft.TestRP/lease.yaml +++ /dev/null @@ -1,5 +0,0 @@ -lease: - resource-provider: Microsoft.TestRP - startdate: 2026-07-15 # ISO 8601 format (YYYY-MM-DD) - duration: P180D # ISO 8601 duration (e.g., P180D, P6M, P1Y2M3D) - reviewer: Tejaswi Salaigari diff --git a/.github/arm-leases/widget/Microsoft.Widget/lease.yaml b/.github/arm-leases/widget/Microsoft.Widget/lease.yaml new file mode 100644 index 000000000000..bc6e6d489681 --- /dev/null +++ b/.github/arm-leases/widget/Microsoft.Widget/lease.yaml @@ -0,0 +1,5 @@ +lease: + resource-provider: Microsoft.Widget + startdate: "2026-01-07" + duration: P180D + reviewer: "@pshao25" diff --git a/.github/workflows/src/arm-lease-validation/arm-lease-validation.js b/.github/workflows/src/arm-lease-validation/arm-lease-validation.js index 8d19c07861f3..28fdf96408e0 100644 --- a/.github/workflows/src/arm-lease-validation/arm-lease-validation.js +++ b/.github/workflows/src/arm-lease-validation/arm-lease-validation.js @@ -1,4 +1,4 @@ -import { readFile } from "fs/promises"; +import { readFile, stat } from "fs/promises"; import { resolve } from "path"; import { inspect } from "util"; import YAML from "js-yaml"; @@ -44,24 +44,34 @@ export const leaseSchema = z.object({ ), startdate: z .string() - .regex(/^\d{4}-\d{2}-\d{2}$/, "Invalid startdate format (expected: YYYY-MM-DD)"), + .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; + return Temporal.Duration.from(v).total({ unit: "days", relativeTo: Temporal.Now.plainDateISO() }) <= 180; } catch { return false; } }, - "duration must be a valid ISO 8601 duration (e.g. P180D, P6M, P1Y2M3D)", + "duration must be a valid ISO 8601 duration not exceeding 180 days (e.g. P180D, P6M)", ), reviewer: z .string() .min(1, "Reviewer is required and cannot be empty") .refine( - (r) => r.trim().length > 0, - "Reviewer is required and cannot be empty", + (r) => r.startsWith("@") && r.trim().length > 1, + "Reviewer must be a GitHub alias starting with @ (e.g., @octocat)", ), }), }); @@ -93,16 +103,17 @@ export function validateFolderStructure(files) { /** * Validate lease file contents using Zod schema. * @param {string} leaseFile - Full path to lease file - * @param {string} today - Today's date in YYYY-MM-DD format * @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, today, relativePath) { +export async function validateLeaseContent(leaseFile, relativePath, workspaceRoot) { const errors = []; - const pathForExtraction = relativePath || leaseFile; - // Extract rpNamespace from .github/arm-leases///lease.yaml + // Extract orgName and rpNamespace from .github/arm-leases///lease.yaml // or .github/arm-leases////lease.yaml - const folderRP = pathForExtraction.split("/")[3]; // rpNamespace is always at index 3 + 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; @@ -138,9 +149,47 @@ export async function validateLeaseContent(leaseFile, today, relativePath) { ); } - // Cross-field validation: startdate must not be in the past - if (lease.startdate < today) { - errors.push(`Startdate is in the past: ${lease.startdate} (today: ${today})`); + // 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})`, + ); + } + + // 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 (error) { + // Service folder doesn't exist + errors.push( + `Service folder not found: specification/${orgName}. The orgName in the lease path must match an existing service folder in specification/.`, + ); + } + + // 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 (error) { + // Directory doesn't exist - skip validation (new RP being registered) + } + } } return { file: leaseFile, errors }; @@ -156,7 +205,6 @@ export async function validateLeaseContent(leaseFile, today, relativePath) { * @returns {Promise<{ status: string, errors: number }>} Validation result */ export default async function validateArmLeases(core) { - const today = Temporal.Now.plainDateISO().toString(); const cwd = process.env.GITHUB_WORKSPACE; let hasErrors = false; @@ -266,7 +314,7 @@ export default async function validateArmLeases(core) { for (const leaseFile of validLeaseFiles) { const fullPath = resolve(cwd, leaseFile); - const result = await validateLeaseContent(fullPath, today, leaseFile); + const result = await validateLeaseContent(fullPath, leaseFile, cwd); if (result.errors.length > 0) { contentErrors.push({ file: leaseFile, errors: result.errors }); hasErrors = true; 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 index 9679451bdeae..e5bb0415c9f7 100644 --- a/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js +++ b/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js @@ -2,9 +2,12 @@ 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 { @@ -12,8 +15,6 @@ import { validateFolderStructure, validateLeaseContent, leaseSchema, - LEASE_FILE_PATTERN, - LEASE_FILE_WITH_GROUP_PATTERN, } from "../../src/arm-lease-validation/arm-lease-validation.js"; describe("validate-arm-leases", () => { @@ -28,151 +29,60 @@ describe("validate-arm-leases", () => { expect(isFileAllowed(".github/arm-leases/README.md")).toBe(true); }); - it("rejects files that do not match allowed patterns", () => { + 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/bad.test/noyaml.md")).toBe(false); - expect(isFileAllowed(".github/arm-leases/testservice/readme.md")).toBe(false); + expect(isFileAllowed(".github/arm-leases/badtest/No.Yaml/no.md")).toBe(false); }); }); describe("validateFolderStructure", () => { - it("accepts valid lease file paths", () => { - const validFiles = [ + it("accepts valid paths and rejects invalid ones", () => { + expect(validateFolderStructure([ ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", - ".github/arm-leases/widgetservice/Azure.Widget/lease.yaml", ".github/arm-leases/compute/Microsoft.Compute/DiskRP/lease.yaml", - ]; - expect(validateFolderStructure(validFiles)).toHaveLength(0); - }); - - it("rejects invalid folder structures", () => { - const invalidFiles = [ - ".github/arm-leases/TestService/Microsoft.Test/lease.yaml", - ".github/arm-leases/test-service/Microsoft.Test/lease.yaml", - ".github/arm-leases/testservice/Microsoft.Test/other.yaml", - ]; - expect(validateFolderStructure(invalidFiles)).toHaveLength(invalidFiles.length); - }); - - it("rejects stable or preview as service group", () => { - const invalidFiles = [ - ".github/arm-leases/compute/Microsoft.Compute/stable/lease.yaml", - ".github/arm-leases/compute/Microsoft.Compute/preview/lease.yaml", - ".github/arm-leases/compute/Microsoft.Compute/stable-2024-01-01/lease.yaml", - ".github/arm-leases/compute/Microsoft.Compute/preview-internal/lease.yaml", - ]; - expect(validateFolderStructure(invalidFiles)).toHaveLength(invalidFiles.length); - }); - }); - - describe("LEASE_FILE_PATTERN", () => { - it("matches valid patterns", () => { - expect(LEASE_FILE_PATTERN.test(".github/arm-leases/testservice/Microsoft.Test/lease.yaml")).toBe(true); - expect(LEASE_FILE_PATTERN.test(".github/arm-leases/service123/Contoso.Manager/lease.yaml")).toBe(true); - }); - - it("rejects invalid patterns", () => { - expect(LEASE_FILE_PATTERN.test(".github/arm-leases/TestService/Microsoft.Test/lease.yaml")).toBe(false); - expect(LEASE_FILE_PATTERN.test("arm-leases/testservice/Microsoft.Test/lease.yaml")).toBe(false); - }); - }); + ])).toHaveLength(0); - describe("LEASE_FILE_WITH_GROUP_PATTERN", () => { - it("matches valid service group patterns", () => { - expect(LEASE_FILE_WITH_GROUP_PATTERN.test(".github/arm-leases/compute/Microsoft.Compute/DiskRP/lease.yaml")).toBe(true); - expect(LEASE_FILE_WITH_GROUP_PATTERN.test(".github/arm-leases/storage/Azure.Storage/BlobRP/lease.yaml")).toBe(true); - }); - - it("rejects stable/preview as service group", () => { - expect(LEASE_FILE_WITH_GROUP_PATTERN.test(".github/arm-leases/compute/Microsoft.Compute/stable/lease.yaml")).toBe(false); - expect(LEASE_FILE_WITH_GROUP_PATTERN.test(".github/arm-leases/compute/Microsoft.Compute/preview/lease.yaml")).toBe(false); + 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", () => { - it("accepts valid lease data", () => { - const valid = { - lease: { - "resource-provider": "Microsoft.Test", - startdate: "2026-06-01", - duration: "P90D", - reviewer: "John Doe", - }, - }; - expect(leaseSchema.safeParse(valid).success).toBe(true); - }); - - it("rejects resource provider with lowercase parts", () => { - const invalid = { - lease: { - "resource-provider": "microsoft.Test", - startdate: "2026-06-01", - duration: "P90D", - reviewer: "John Doe", - }, - }; - expect(leaseSchema.safeParse(invalid).success).toBe(false); - }); + const validLease = { + lease: { + "resource-provider": "Microsoft.Test", + startdate: "2026-06-01", + duration: "P90D", + reviewer: "@johndoe", + }, + }; - it("rejects invalid startdate format", () => { - const invalid = { - lease: { - "resource-provider": "Microsoft.Test", - startdate: "01-15-2026", - duration: "P90D", - reviewer: "John Doe", - }, - }; - expect(leaseSchema.safeParse(invalid).success).toBe(false); + 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 duration format", () => { - const invalid = { - lease: { - "resource-provider": "Microsoft.Test", - startdate: "2026-06-01", - duration: "90 days", - reviewer: "John Doe", - }, - }; - expect(leaseSchema.safeParse(invalid).success).toBe(false); + it("rejects invalid resource-provider", () => { + expect(leaseSchema.safeParse({ lease: { ...validLease.lease, "resource-provider": "microsoft.Test" } }).success).toBe(false); }); - it("accepts month-based durations like P6M", () => { - const valid = { - lease: { - "resource-provider": "Microsoft.Test", - startdate: "2026-06-01", - duration: "P6M", - reviewer: "John Doe", - }, - }; - expect(leaseSchema.safeParse(valid).success).toBe(true); + 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("accepts combined durations like P1Y2M3D", () => { - const valid = { - lease: { - "resource-provider": "Microsoft.Test", - startdate: "2026-06-01", - duration: "P1Y2M3D", - reviewer: "John Doe", - }, - }; - expect(leaseSchema.safeParse(valid).success).toBe(true); + 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 empty reviewer", () => { - const invalid = { - lease: { - "resource-provider": "Microsoft.Test", - startdate: "2026-06-01", - duration: "P90D", - reviewer: "", - }, - }; - expect(leaseSchema.safeParse(invalid).success).toBe(false); + 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", () => { @@ -185,82 +95,81 @@ describe("validate-arm-leases", () => { resource-provider: Microsoft.Test startdate: "2027-06-01" duration: P90D - reviewer: John Doe + 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", - "2026-01-07", ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", ); - expect(result.errors).toHaveLength(0); - expect(mockReadFile).toHaveBeenCalledOnce(); }); it("detects resource provider mismatch", async () => { - const mismatchYaml = validYaml.replace("Microsoft.Test", "Microsoft.Other"); - mockReadFile.mockResolvedValue(mismatchYaml); - + mockReadFile.mockResolvedValue(validYaml.replace("Microsoft.Test", "Microsoft.Other")); const result = await validateLeaseContent( "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", - "2026-01-07", ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", ); - expect(result.errors.some((e) => e.includes("mismatch"))).toBe(true); }); - it("detects past startdate", async () => { - const pastYaml = validYaml.replace("2027-06-01", "2025-01-01"); - mockReadFile.mockResolvedValue(pastYaml); + 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); + }); - const result = await validateLeaseContent( - "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", - "2026-01-07", - ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", - ); + 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); - expect(result.errors.some((e) => e.includes("past"))).toBe(true); + // 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("returns error for non-existent file", async () => { - mockReadFile.mockRejectedValue(new Error("ENOENT: no such file or directory")); + it("validates specification folder structure", async () => { + mockReadFile.mockResolvedValue(validYaml); + mockStat.mockResolvedValue({ isDirectory: () => true }); const result = await validateLeaseContent( - "/nonexistent/lease.yaml", - "2026-01-07", - ".github/arm-leases/svc/NS/lease.yaml", + "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", + ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", + "/repo", ); - - expect(result.errors.some((e) => e.includes("Error reading file"))).toBe(true); + expect(result.errors).toHaveLength(0); + expect(mockStat).toHaveBeenCalledTimes(2); }); - it("detects invalid YAML content", async () => { - mockReadFile.mockResolvedValue("invalid yaml content without structure"); + it("fails when service folder does not exist", async () => { + mockReadFile.mockResolvedValue(validYaml); + mockStat.mockRejectedValue(new Error("ENOENT")); const result = await validateLeaseContent( - "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", - "2026-01-07", - ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", + "/repo/.github/arm-leases/invalidservice/Microsoft.Test/lease.yaml", + ".github/arm-leases/invalidservice/Microsoft.Test/lease.yaml", + "/repo", ); - - expect(result.errors.length).toBeGreaterThan(0); + expect(result.errors.some((e) => e.includes("Service folder not found"))).toBe(true); }); - it("accepts today as startdate", async () => { - const todayYaml = validYaml.replace("2027-06-01", "2026-01-07"); - mockReadFile.mockResolvedValue(todayYaml); + it("allows new RP when service exists but RP folder does not", async () => { + mockReadFile.mockResolvedValue(validYaml); + mockStat.mockResolvedValueOnce({ isDirectory: () => true }).mockRejectedValueOnce(new Error("ENOENT")); const result = await validateLeaseContent( "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", - "2026-01-07", ".github/arm-leases/testservice/Microsoft.Test/lease.yaml", + "/repo", ); - expect(result.errors).toHaveLength(0); }); }); From a29e9d972b6f849b0268ab6b0a94020d13ee3578 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 11 Mar 2026 18:02:47 -0500 Subject: [PATCH 056/125] Added service name --- .../arm-leases/widget/Microsoft.Widget/{ => Widget}/lease.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/arm-leases/widget/Microsoft.Widget/{ => Widget}/lease.yaml (76%) diff --git a/.github/arm-leases/widget/Microsoft.Widget/lease.yaml b/.github/arm-leases/widget/Microsoft.Widget/Widget/lease.yaml similarity index 76% rename from .github/arm-leases/widget/Microsoft.Widget/lease.yaml rename to .github/arm-leases/widget/Microsoft.Widget/Widget/lease.yaml index bc6e6d489681..e2ca2684a733 100644 --- a/.github/arm-leases/widget/Microsoft.Widget/lease.yaml +++ b/.github/arm-leases/widget/Microsoft.Widget/Widget/lease.yaml @@ -1,5 +1,5 @@ lease: resource-provider: Microsoft.Widget - startdate: "2026-01-07" + startdate: "2026-03-11" duration: P180D reviewer: "@pshao25" From ea9739e8e85b9c181e4e8d9fa5758a8db3cf106a Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 11 Mar 2026 18:14:59 -0500 Subject: [PATCH 057/125] tried to exclude arm-leases from spell check, reviewer names are triggering spell check errors --- cspell.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/cspell.yaml b/cspell.yaml index 422fcbccdcfe..583c9acba0c8 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -4,6 +4,7 @@ flagWords: - teh ignorePaths: - .github/CODEOWNERS* + - .github/arm-leases/** - .github/policies/** - .gitignore - '**/examples/**' From fda8efafaa16e9b0db2e54f5b12ada4365e07730 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 11 Mar 2026 18:28:23 -0500 Subject: [PATCH 058/125] check specifications folder format with the lease file --- .github/workflows/arm-lease-validation.yaml | 1 + package-lock.json | 39 ++++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/.github/workflows/arm-lease-validation.yaml b/.github/workflows/arm-lease-validation.yaml index 35510fc8bc97..ecd4dceabe13 100644 --- a/.github/workflows/arm-lease-validation.yaml +++ b/.github/workflows/arm-lease-validation.yaml @@ -31,6 +31,7 @@ jobs: fetch-depth: 2 sparse-checkout: | .github + specification - name: Install dependencies for github-script actions uses: ./.github/actions/install-deps-github-script diff --git a/package-lock.json b/package-lock.json index 945f1b02dde9..837e875810de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -714,6 +714,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -1079,6 +1080,7 @@ "integrity": "sha512-dYgHtt0CY0Q9AimdIsMV41jHKLmAT4r++TLwyxAHRbxdiRG+Sll1UKJzOIIoq45Bq64wCfEltu5OOnyPA01/sQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" }, @@ -1105,6 +1107,7 @@ "integrity": "sha512-3rvyGDIYSqraZ7jHfq5Bfet8u3ZeERWJWhwWMNvbShnrS/vVR3iuu/1z2M0p5mTRFuwUaSMlL/dbtBp1YqgGAg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "change-case": "~5.4.4", "pluralize": "^8.0.0" @@ -1173,6 +1176,7 @@ "integrity": "sha512-p+MZU/nEmU3ciLEuNbqQtAybPxUKo/fKeKT9feh+tZLVpDDFO5DTefYoN4cteZQkPu/xyzxhjeUnKKvyVQxd6A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "change-case": "~5.4.4", "pluralize": "^8.0.0", @@ -3881,6 +3885,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", @@ -4457,6 +4462,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4768,6 +4774,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4868,6 +4875,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5208,7 +5216,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/@ts-common/property-set": { "version": "0.1.0", @@ -5425,7 +5434,8 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/lodash": { "version": "4.17.23", @@ -5552,6 +5562,7 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -5782,6 +5793,7 @@ "integrity": "sha512-Rz9fFWQSTJSnhBfZvtA/bDIuO82fknYdtyMsL9lZNJE82rquC6JByHPFsnbGH1VXA0HhMj9L7Oqyp3f0m/BTOA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "~7.28.6", "@inquirer/prompts": "^8.0.1", @@ -5911,6 +5923,7 @@ "integrity": "sha512-41R2jA7k21uMArjyUdvnqYzVnPPaSEcGi40dLMiRVP79m6XgnD3INuTdlMblaS1i+5jJ1BtS1o4QhBBuS/5/qg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" }, @@ -5924,6 +5937,7 @@ "integrity": "sha512-agcwmbB/hK/o9KmM38UB8OGZwLgB17lJ7b4EjqYGpyshqcRMTESMRxnJIH7rRzUq4HJDTqal0tsb8z0K0zXuDg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" }, @@ -5943,6 +5957,7 @@ "integrity": "sha512-5ieXCWRLcyFLv3IFk26ena/RW/NxvT5KiHaoNVFRd79J0XZjFcE0Od6Lxxqj4dWmCo3C8oKtOwFoQuie18G3lQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" }, @@ -6017,6 +6032,7 @@ "integrity": "sha512-6QIX7oaUGy/z4rseUrC86LjHxZn8rAAY4fXvGnlPRce6GhEdTb9S9OQPmlPeWngXwCx/07P2+FCR915APqmZxg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" }, @@ -6031,6 +6047,7 @@ "integrity": "sha512-YQYlDWCNBza75S360jc51emwntWXMZfkvqXKng+etKP4iCuogJfTX1J8h1yd8tZwkuUNBcklEPCuz3O/+psopg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" }, @@ -6047,6 +6064,7 @@ "integrity": "sha512-nOXpLcEYNdWvLY/6WJ16rD6hGs7bKSmkH+WwgyVwdRON5KJ559quw56pns2DSANw+NaV0lJxJq/8ek5xKCGD6g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" }, @@ -6075,6 +6093,7 @@ "integrity": "sha512-mk65zpKNm+ARyHASnre/lp3o3FKzb0P8Nj96ji182JUy7ShrVCCF0u+bC+ZXQ8ZTRza1d0xBjRC/Xr4iM+Uwag==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" }, @@ -6088,6 +6107,7 @@ "integrity": "sha512-BqbbtkL9xuiAhehHKKUCMtRg0a1vjSvoiAOanvTIuoFq3N8PbKVV3dKTcyI/oS3iCCkJErdu11HQcAoD/VsIsA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" }, @@ -6256,6 +6276,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7512,6 +7533,7 @@ "integrity": "sha512-20MV9SUdeN6Jd84xESsKhRly+/vxI+hwvpBMA93s+9dAcjdCuCojn4IqUGS3lvVaqjVYGYHSRMCpeFtF2rQYxQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -9315,6 +9337,7 @@ "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 10.16.0" } @@ -10557,6 +10580,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10686,7 +10710,8 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -11595,6 +11620,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -11694,7 +11720,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tunnel": { "version": "0.0.6", @@ -11803,6 +11830,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11965,6 +11993,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -12058,6 +12087,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -12071,6 +12101,7 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", From 873f7db07a35adf0467de7726e84cc14a3a96461 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Thu, 12 Mar 2026 10:33:37 -0500 Subject: [PATCH 059/125] Fix error --- .../arm-lease-validation.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/src/arm-lease-validation/arm-lease-validation.js b/.github/workflows/src/arm-lease-validation/arm-lease-validation.js index 28fdf96408e0..35c7f4aa67ba 100644 --- a/.github/workflows/src/arm-lease-validation/arm-lease-validation.js +++ b/.github/workflows/src/arm-lease-validation/arm-lease-validation.js @@ -71,7 +71,7 @@ export const leaseSchema = z.object({ .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., @octocat)", + "Reviewer must be a GitHub alias starting with @ (e.g., @githubUser)", ), }), }); @@ -153,7 +153,7 @@ export async function validateLeaseContent(leaseFile, relativePath, workspaceRoo 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})`, + `Startdate is in the past: ${lease.startdate} (must be within 10 days of today: ${today.toString()})`, ); } @@ -170,7 +170,7 @@ export async function validateLeaseContent(leaseFile, relativePath, workspaceRoo } else { serviceExists = true; } - } catch (error) { + } catch { // Service folder doesn't exist errors.push( `Service folder not found: specification/${orgName}. The orgName in the lease path must match an existing service folder in specification/.`, @@ -186,7 +186,7 @@ export async function validateLeaseContent(leaseFile, relativePath, workspaceRoo ); } // Directory exists and matches - validation passes - } catch (error) { + } catch { // Directory doesn't exist - skip validation (new RP being registered) } } @@ -232,6 +232,8 @@ export default async function validateArmLeases(core) { core.info(` ... and ${disallowedFiles.length - 20} more files`); } hasErrors = true; + } else { + core.info("No disallowed files found"); } core.endGroup(); @@ -248,6 +250,8 @@ export default async function validateArmLeases(core) { } 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(); @@ -291,6 +295,8 @@ export default async function validateArmLeases(core) { 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(); @@ -329,6 +335,11 @@ export default async function validateArmLeases(core) { 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(); From e0104c77c27ff58ce613ee54b4758ee727dd686d Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Thu, 12 Mar 2026 12:39:35 -0500 Subject: [PATCH 060/125] Addressed comments --- .github/workflows/arm-modeling-review.yaml | 15 +- .../arm-lease-validation/detect-arm-leases.js | 2 +- .../detect-new-resource-provider.js | 39 ++-- .../detect-new-resource-types.js | 149 +++++--------- .../detect-arm-leases.test.js | 4 +- .../detect-new-resource-provider.test.js | 2 +- .../detect-new-resource-types.test.js | 186 +++++------------- 7 files changed, 124 insertions(+), 273 deletions(-) diff --git a/.github/workflows/arm-modeling-review.yaml b/.github/workflows/arm-modeling-review.yaml index 21aca07fd690..130ed7cb4faf 100644 --- a/.github/workflows/arm-modeling-review.yaml +++ b/.github/workflows/arm-modeling-review.yaml @@ -29,11 +29,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 with: - fetch-depth: 0 # Fetch all history for merge-base calculation and diffing - sparse-checkout: | - .github - specification - eng + fetch-depth: 2 - name: Install dependencies for github-script actions uses: ./.github/actions/install-deps-github-script @@ -45,8 +41,7 @@ jobs: script: | const { default: armModelingReview } = await import('${{ github.workspace }}/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js'); - const result = await armModelingReview({ context, core }); - return result; + 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' @@ -68,9 +63,3 @@ jobs: with: name: "ARMModelingAutoSignedOff" value: "${{ fromJson(steps.detect.outputs.result).labelActions['ARMModelingAutoSignedOff'] == 'add' }}" - - - name: Upload artifact for issue_number - uses: ./.github/actions/add-empty-artifact - with: - name: issue-number - value: "${{ github.event.pull_request.number }}" diff --git a/.github/workflows/src/arm-lease-validation/detect-arm-leases.js b/.github/workflows/src/arm-lease-validation/detect-arm-leases.js index 96e647d5a0bc..00d52397c442 100644 --- a/.github/workflows/src/arm-lease-validation/detect-arm-leases.js +++ b/.github/workflows/src/arm-lease-validation/detect-arm-leases.js @@ -84,7 +84,7 @@ export function parseLease(content) { const today = Temporal.Now.plainDateISO(); if (Temporal.PlainDate.compare(today, endDate) > 0) { - return { valid: false, reason: `Lease expired on ${endDate}` }; + return { valid: false, reason: `Lease expired on ${endDate.toString()}` }; } return { valid: true, reason: "Lease is valid" }; diff --git a/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js b/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js index 0c487c00a740..aad4168d7654 100644 --- a/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js +++ b/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js @@ -15,12 +15,13 @@ const ARM_OFFICE_HOURS_URL = // Match pattern: specification//resource-manager//... // Trailing slash ensures the match is a directory component, not a file like readme.md -const RESOURCE_MANAGER_PATTERN = /^specification\/[^\/]+\/resource-manager\/([^\/]+)\//; +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 = - /^specification\/[^\/]+\/resource-manager\/([^\/]+)\/(?!stable|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. @@ -131,31 +132,17 @@ function extractResourceProviders(files) { * Main detection logic for GitHub script action. * * @param {Object} params - Parameters from github-script - * @param {import('@actions/github-script').AsyncFunctionArguments['context']} params.context * @param {import('@actions/github-script').AsyncFunctionArguments['core']} params.core * @returns {Promise<{ status: string, labelActions: ManagedLabelActions }>} */ -export default async function detectNewResourceProvider({ context, core }) { - const repoRoot = process.env.GITHUB_WORKSPACE ?? process.cwd(); +export default async function detectNewResourceProvider({ core }) { + const repoRoot = process.env.GITHUB_WORKSPACE; const git = simpleGit(repoRoot); core.info("Detecting New Resource Providers"); - // Determine merge base between HEAD and the base branch (usually main) - // This ensures we detect all changes introduced in the PR, not just the last commit. - let mergeBase = "HEAD^"; - try { - const baseRef = context.payload.pull_request?.base?.sha || "main"; - mergeBase = await git.raw(["merge-base", baseRef, "HEAD"]); - mergeBase = mergeBase.trim(); - core.info(`Using merge-base: ${mergeBase} (against ${baseRef})`); - } catch (e) { - core.info(`Could not determine merge-base, falling back to HEAD^. Error: ${e.message}`); - } - const options = { cwd: process.env.GITHUB_WORKSPACE, - baseCommitish: "HEAD^", paths: ["specification"], logger: new CoreLogger(core), }; @@ -216,7 +203,7 @@ export default async function detectNewResourceProvider({ context, core }) { 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(repoRoot, mergeBase, rmFiles, core); + return await checkNewResourceTypes(repoRoot, rmFiles, core); } } @@ -237,7 +224,7 @@ export default async function detectNewResourceProvider({ context, core }) { if (newResourceProviders.length === 0) { core.info("No new resource providers detected."); core.info("Checking for new resource types in existing RPs..."); - return await checkNewResourceTypes(repoRoot, mergeBase, rmFiles, core); + return await checkNewResourceTypes(repoRoot, rmFiles, core); } core.info(`Detected ${newResourceProviders.length} new resource provider(s)`); @@ -275,12 +262,14 @@ export default async function detectNewResourceProvider({ context, core }) { } core.info("Checking for new resource types in existing RPs..."); - const newRtResult = await checkNewResourceTypes(repoRoot, mergeBase, rmFiles, core); + const newRtResult = await checkNewResourceTypes(repoRoot, 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 [label, action] of Object.entries(newRtResult.labelActions)) { + 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; @@ -297,15 +286,13 @@ export default async function detectNewResourceProvider({ context, core }) { * Check for new resource types in existing RPs and validate their leases. * * @param {string} repoRoot - Repository root directory - * @param {string} mergeBase - Git merge base SHA * @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(repoRoot, mergeBase, rmFiles, core) { +async function checkNewResourceTypes(repoRoot, rmFiles, core) { const newRtResults = await detectNewResourceTypes({ repoRoot, - mergeBase, rmFiles, core, }); diff --git a/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js b/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js index 1f69042bb05f..0a908dd2503e 100644 --- a/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js +++ b/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js @@ -1,23 +1,16 @@ /** - * Detect new ARM resource types in PRs using Azure OpenAPI Validator's ArmHelper. + * 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 { resolve } from "path"; import { simpleGit } from "simple-git"; -import { ArmHelper } from "@microsoft.azure/openapi-validator-rulesets/dist/native/utilities/arm-helper.js"; -import { SwaggerInventory } from "@microsoft.azure/openapi-validator-core"; -import debug from "debug"; - -// Disable simple-git debug logging to remove verbose [GitExecutor] logs from the output -debug.disable(); // Match pattern: specification//resource-manager//... -const RESOURCE_MANAGER_PATTERN = /^specification\/[^\/]+\/resource-manager\/([^\/]+)\//; +const RESOURCE_MANAGER_PATTERN = new RegExp(String.raw`^specification/[^/]+/resource-manager/([^/]+)/`); -const RESOURCE_TYPE_REGEX = /\/providers\/([^/?#]+)(\/[^?#]*)?/; +const RESOURCE_TYPE_REGEX = new RegExp(String.raw`/providers/([^/?#]+)(/[^?#]*)?`); /** * Extract resource type from API path (e.g. Microsoft.Compute/virtualMachines/extensions) @@ -45,88 +38,53 @@ function getResourceType(apiPath) { } /** - * Get all ARM resource types from a swagger document using openapi-validator's ArmHelper. + * Get all ARM resource types from a swagger document by parsing paths. * - * Primary: ArmHelper detects resources based on definitions with x-ms-azure-resource or - * ARM base types. Fallback: path-based detection for specs using inline schemas. + * Scans paths for ARM resource patterns like /providers/Microsoft.X/resourceTypes. * - * @param {Object} swaggerDoc - Parsed swagger JSON document - * @param {string} specPath - Absolute path to the swagger file + * @param {{paths?: Record>}} swaggerDoc - Parsed swagger JSON document * @returns {Map}>} */ -function getResourceTypesFromSwagger(swaggerDoc, specPath) { +function getResourceTypesFromSwagger(swaggerDoc) { + /** @type {Map} */ const resourceTypes = new Map(); - // ArmHelper requires a populated SwaggerInventory to resolve cross-file $refs. - // When used with a fresh empty inventory (as here), it throws "Node does not exist" - // during getAllResources(). Wrap in try/catch so we always fall through to the - // path-based fallback when ArmHelper cannot run. - try { - const armHelper = new ArmHelper( - swaggerDoc, - resolve(specPath), - new SwaggerInventory(), - ); - - for (const resource of armHelper.getAllResources()) { - const firstOp = resource.operations[0]; - const resourceType = getResourceType(firstOp.apiPath); - - if (resourceType && !resourceTypes.has(resourceType)) { - resourceTypes.set(resourceType, { - resourceType, - provider: resourceType.split("/")[0], - modelName: resource.modelName, - operations: resource.operations.map((op) => ({ - method: op.httpMethod, - apiPath: op.apiPath, - })), - }); - } - } - } catch { - // ArmHelper failed (e.g. empty SwaggerInventory throws "Node does not exist"). - // Fall through to the path-based fallback below. + if (!swaggerDoc.paths) { + return resourceTypes; } - // Fallback: when ArmHelper finds no resources (e.g. spec uses inline response schemas - // instead of $ref to named definitions, or ArmHelper threw), scan paths directly for - // ARM resource patterns. - if (resourceTypes.size === 0 && swaggerDoc.paths) { - /** @type {Record>} */ - const paths = /** @type {any} */ (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(/** @type {Record} */ (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); - } + 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); } } } @@ -144,7 +102,8 @@ function getResourceTypesFromSwagger(swaggerDoc, specPath) { * @param {string} repoRoot - Repository root directory * @returns {Promise | null>} Map of resource type to info, or null if path doesn't exist at this ref */ -async function getResourceTypesAtRef(git, commitish, namespacePath, repoRoot) { +async function getResourceTypesAtRef(git, commitish, namespacePath) { + /** @type {Map} */ const allTypes = new Map(); let output; @@ -180,19 +139,17 @@ async function getResourceTypesAtRef(git, commitish, namespacePath, repoRoot) { let content; try { content = await git.show([`${commitish}:${file.trim()}`]); - } catch (e) { + } catch { // Intentionally quiet to avoid log spam during comparison continue; } try { - const swaggerDoc = JSON.parse(content); + 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, - resolve(repoRoot, file), - ); + const types = getResourceTypesFromSwagger(swaggerDoc); for (const [type, info] of types) { if (!allTypes.has(type)) { allTypes.set(type, info); @@ -215,14 +172,12 @@ async function getResourceTypesAtRef(git, commitish, namespacePath, repoRoot) { * * @param {Object} params * @param {string} params.repoRoot - Repository root directory - * @param {string} params.mergeBase - Git ref for the base commit * @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({ repoRoot, - mergeBase, rmFiles, core, }) { @@ -258,9 +213,8 @@ export async function detectNewResourceTypes({ const baseTypes = await getResourceTypesAtRef( git, - mergeBase, + "HEAD^", namespacePath, - repoRoot, ); // Skip namespace if it doesn't exist in base (brand new RP — handled by RP-level detection) @@ -273,11 +227,12 @@ export async function detectNewResourceTypes({ git, "HEAD", namespacePath, - repoRoot, ); const newTypes = []; - for (const [type, info] of (headTypes ?? new Map())) { + /** @type {Map} */ + const typedHeadTypes = headTypes ?? new Map(); + for (const [type, info] of typedHeadTypes) { if (!baseTypes.has(type)) { newTypes.push({ resourceType: type, diff --git a/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js b/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js index f213240a7a10..aae2ac5e122b 100644 --- a/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js +++ b/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js @@ -22,7 +22,9 @@ function today() { return Temporal.Now.plainDateISO(); } -/** Subtract days from today and return YYYY-MM-DD string */ +/** 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(); } diff --git a/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js b/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js index 6a6c35ca60bf..d4866e5be596 100644 --- a/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js +++ b/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js @@ -99,7 +99,7 @@ describe("detectNewResourceProvider", () => { vi.mocked(mockRaw).mockResolvedValue(""); // SvcA valid, SvcB invalid - vi.mocked(checkLease).mockImplementation(async (orgName) => orgName === "svcA"); + vi.mocked(checkLease).mockImplementation((orgName) => orgName === "svcA"); const result = await detectNewResourceProvider({ context, core }); diff --git a/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js b/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js index ed017b2e0076..e519bdbbeeac 100644 --- a/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js +++ b/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js @@ -11,52 +11,33 @@ vi.mock("simple-git", () => ({ simpleGit: vi.fn().mockReturnValue({ raw: mockRaw, show: mockShow }), })); -const mockGetAllResources = vi.hoisted(() => vi.fn().mockReturnValue([])); - -vi.mock( - "@microsoft.azure/openapi-validator-rulesets/dist/native/utilities/arm-helper.js", - () => ({ - ArmHelper: vi.fn().mockImplementation(function () { - return { getAllResources: mockGetAllResources }; - }), - }), -); - -vi.mock("@microsoft.azure/openapi-validator-core", () => ({ - SwaggerInventory: vi.fn().mockImplementation(function () {}), -})); - import { detectNewResourceTypes } from "../../src/arm-lease-validation/detect-new-resource-types.js"; const core = createMockCore(); -const swaggerContent = JSON.stringify({ swagger: "2.0", paths: {} }); +const emptySwagger = JSON.stringify({ swagger: "2.0", paths: {} }); -// ── test data ─────────────────────────────────────────────────────────── - -const vmResource = { - modelName: "VirtualMachine", - operations: [ - { - httpMethod: "GET", - apiPath: "/subscriptions/{id}/providers/Microsoft.Compute/virtualMachines/{name}", +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 diskResource = { - modelName: "Disk", - operations: [ - { - httpMethod: "GET", - apiPath: "/subscriptions/{id}/providers/Microsoft.Compute/disks/{name}", + }, +}); + +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" } } }, }, - { - httpMethod: "PUT", - apiPath: "/subscriptions/{id}/providers/Microsoft.Compute/disks/{name}", + "/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 ───────────────────────────────────────────────────────────── @@ -73,7 +54,7 @@ function setupGit({ headFiles = new Map(), fileContents = new Map(), } = {}) { - mockRaw.mockImplementation(async (args) => { + mockRaw.mockImplementation((args) => { if (args[0] === "ls-tree" && args.includes("-r")) { const commitish = args[3]; const namespacePath = args[4]; @@ -86,12 +67,13 @@ function setupGit({ return ""; }); - mockShow.mockImplementation(async (args) => { - const key = args[0]; // "ref:filepath" + mockShow.mockImplementation((args) => { + const key = String(args[0]); // "ref:filepath" if (fileContents.has(key)) { return fileContents.get(key); } - throw new Error(`path does not exist: ${key}`); + const msg = `path does not exist: ${key}`; + throw new Error(msg); }); } @@ -105,7 +87,7 @@ describe("detectNewResourceTypes", () => { it("returns empty when rmFiles has no version-pattern matches", async () => { const result = await detectNewResourceTypes({ repoRoot: "/fake/repo", - mergeBase: "base123", + rmFiles: ["specification/compute/resource-manager/readme.md"], core, }); @@ -117,7 +99,7 @@ describe("detectNewResourceTypes", () => { it("returns empty when rmFiles is empty", async () => { const result = await detectNewResourceTypes({ repoRoot: "/fake/repo", - mergeBase: "base123", + rmFiles: [], core, }); @@ -133,7 +115,7 @@ describe("detectNewResourceTypes", () => { const result = await detectNewResourceTypes({ repoRoot: "/fake/repo", - mergeBase: "base123", + rmFiles: [rmFile], core, }); @@ -151,16 +133,14 @@ describe("detectNewResourceTypes", () => { baseFiles: new Map([[servicePath, [file]]]), headFiles: new Map([[servicePath, [file]]]), fileContents: new Map([ - [`base123:${file}`, swaggerContent], - [`HEAD:${file}`, swaggerContent], + [`HEAD^:${file}`, vmSwagger], + [`HEAD:${file}`, vmSwagger], ]), }); - mockGetAllResources.mockReturnValue([vmResource]); - const result = await detectNewResourceTypes({ repoRoot: "/fake/repo", - mergeBase: "base123", + rmFiles: [file], core, }); @@ -178,18 +158,14 @@ describe("detectNewResourceTypes", () => { baseFiles: new Map([[servicePath, [file]]]), headFiles: new Map([[servicePath, [file]]]), fileContents: new Map([ - [`base123:${file}`, swaggerContent], - [`HEAD:${file}`, swaggerContent], + [`HEAD^:${file}`, vmSwagger], // base: VM only + [`HEAD:${file}`, vmAndDiskSwagger], // HEAD: VM + Disk ]), }); - mockGetAllResources - .mockReturnValueOnce([vmResource]) // base: VM only - .mockReturnValueOnce([vmResource, diskResource]); // HEAD: VM + Disk - const result = await detectNewResourceTypes({ repoRoot: "/fake/repo", - mergeBase: "base123", + rmFiles: [file], core, }); @@ -225,18 +201,16 @@ describe("detectNewResourceTypes", () => { [networkServicePath, [networkFile]], ]), fileContents: new Map([ - [`base123:${computeFile}`, swaggerContent], - [`HEAD:${computeFile}`, swaggerContent], - [`base123:${networkFile}`, swaggerContent], - [`HEAD:${networkFile}`, swaggerContent], + [`HEAD^:${computeFile}`, emptySwagger], + [`HEAD:${computeFile}`, emptySwagger], + [`HEAD^:${networkFile}`, emptySwagger], + [`HEAD:${networkFile}`, emptySwagger], ]), }); - mockGetAllResources.mockReturnValue([]); - const result = await detectNewResourceTypes({ repoRoot: "/fake/repo", - mergeBase: "base123", + rmFiles: [computeFile, networkFile], core, }); @@ -256,16 +230,14 @@ describe("detectNewResourceTypes", () => { baseFiles: new Map([[servicePath, [file, exampleFile]]]), headFiles: new Map([[servicePath, [file, exampleFile]]]), fileContents: new Map([ - [`base123:${file}`, swaggerContent], - [`HEAD:${file}`, swaggerContent], + [`HEAD^:${file}`, emptySwagger], + [`HEAD:${file}`, emptySwagger], ]), }); - mockGetAllResources.mockReturnValue([]); - await detectNewResourceTypes({ repoRoot: "/fake/repo", - mergeBase: "base123", + rmFiles: [file], core, }); @@ -285,16 +257,14 @@ describe("detectNewResourceTypes", () => { baseFiles: new Map([[servicePath, [file, readme]]]), headFiles: new Map([[servicePath, [file, readme]]]), fileContents: new Map([ - [`base123:${file}`, swaggerContent], - [`HEAD:${file}`, swaggerContent], + [`HEAD^:${file}`, emptySwagger], + [`HEAD:${file}`, emptySwagger], ]), }); - mockGetAllResources.mockReturnValue([]); - await detectNewResourceTypes({ repoRoot: "/fake/repo", - mergeBase: "base123", + rmFiles: [file], core, }); @@ -304,7 +274,7 @@ describe("detectNewResourceTypes", () => { ); }); - it("detects new resource types via path-based fallback when ArmHelper finds nothing (inline schemas)", async () => { + 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`; @@ -326,85 +296,35 @@ describe("detectNewResourceTypes", () => { baseFiles: new Map([[`${ns}/DiskRP`, [existingFile]]]), headFiles: new Map([[`${ns}/DiskRP`, [existingFile, newFile]]]), fileContents: new Map([ - [`base123:${existingFile}`, existingSwagger], + [`HEAD^:${existingFile}`, existingSwagger], [`HEAD:${existingFile}`, existingSwagger], [`HEAD:${newFile}`, newSwagger], ]), }); - // ArmHelper returns nothing (inline schemas, no definitions) - mockGetAllResources.mockReturnValue([]); - const result = await detectNewResourceTypes({ repoRoot: "/fake/repo", - mergeBase: "base123", + rmFiles: [newFile], core, }); expect(result).toHaveLength(1); expect(result[0].namespace).toBe("Microsoft.Compute"); - // superDisks should be detected via path fallback + // 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 but child of superDisks + // superDisks/metrics is also new const metricsType = result[0].newResourceTypes.find((t) => t.resourceType === "Microsoft.Compute/superDisks/metrics", ); expect(metricsType).toBeDefined(); }); - it("detects new resource types via path-based fallback when ArmHelper throws (e.g. empty SwaggerInventory)", 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" } } }, - }, - }, - }); - - setupGit({ - baseFiles: new Map([[`${ns}/DiskRP`, [existingFile]]]), - headFiles: new Map([[`${ns}/DiskRP`, [existingFile, newFile]]]), - fileContents: new Map([ - [`base123:${existingFile}`, existingSwagger], - [`HEAD:${existingFile}`, existingSwagger], - [`HEAD:${newFile}`, newSwagger], - ]), - }); - - // ArmHelper throws (simulating the real "Node does not exist" error from an - // empty SwaggerInventory — the actual failure mode in production) - mockGetAllResources.mockImplementation(() => { - throw new Error("Node does not exist: file:///path/to/spec.json"); - }); - - const result = await detectNewResourceTypes({ - repoRoot: "/fake/repo", - mergeBase: "base123", - rmFiles: [newFile], - core, - }); - - expect(result).toHaveLength(1); - expect(result[0].namespace).toBe("Microsoft.Compute"); - const superDisksType = result[0].newResourceTypes.find((t) => - t.resourceType === "Microsoft.Compute/superDisks", - ); - expect(superDisksType).toBeDefined(); - expect(superDisksType.operations).toContain("GET"); - }); - - it("path-based fallback excludes operations-only paths", async () => { + 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`; @@ -423,17 +343,15 @@ describe("detectNewResourceTypes", () => { baseFiles: new Map([[`${ns}/DiskRP`, [existingPreviewFile]]]), headFiles: new Map([[`${ns}/DiskRP`, [existingPreviewFile, newFile]]]), fileContents: new Map([ - [`base123:${existingPreviewFile}`, existingSwagger], + [`HEAD^:${existingPreviewFile}`, existingSwagger], [`HEAD:${existingPreviewFile}`, existingSwagger], [`HEAD:${newFile}`, operationsOnlySwagger], ]), }); - mockGetAllResources.mockReturnValue([]); - const result = await detectNewResourceTypes({ repoRoot: "/fake/repo", - mergeBase: "base123", + rmFiles: [newFile], core, }); From b7e69246a1d60c9e0c2df26157f07a8afa4e43bd Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Thu, 12 Mar 2026 13:11:32 -0500 Subject: [PATCH 061/125] Addresses copilots comments --- .../detect-new-resource-types.js | 21 ++-- .../src/summarize-checks/labelling.js | 2 +- .../detect-arm-leases.test.js | 21 +++- .../detect-new-resource-types.test.js | 100 ++++++++++++++---- 4 files changed, 115 insertions(+), 29 deletions(-) diff --git a/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js b/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js index 0a908dd2503e..6628ad4d699a 100644 --- a/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js +++ b/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js @@ -99,10 +99,10 @@ function getResourceTypesFromSwagger(swaggerDoc) { * @param {import("simple-git").SimpleGit} git * @param {string} commitish - Git ref * @param {string} namespacePath - e.g. `specification/compute/resource-manager/Microsoft.Compute` - * @param {string} repoRoot - Repository root directory + * @param {string} label - Label 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, commitish, namespacePath) { +async function getResourceTypesAtRef(git, commitish, namespacePath, label) { /** @type {Map} */ const allTypes = new Map(); @@ -126,7 +126,7 @@ async function getResourceTypesAtRef(git, commitish, namespacePath) { const total = swaggerFiles.length; if (total > 0) { - console.log(`Analyzing ${total} swagger files in base branch for comparison...`); + console.log(`Analyzing ${total} swagger files in ${label} (${commitish}) for comparison...`); } // Iterate over files, but only log summary if it's too much @@ -196,10 +196,15 @@ export async function detectNewResourceTypes({ const parts = file.split("/"); const orgName = parts[1]; const namespace = parts[3]; - // Scope to the service subdirectory (e.g. DiskRP) to avoid scanning the entire namespace - const serviceGroup = parts[4]; - const namespacePath = `specification/${orgName}/resource-manager/${namespace}/${serviceGroup}`; - const serviceKey = `${namespace}/${serviceGroup}`; + // Check if parts[4] is an actual service group (e.g. DiskRP) vs lifecycle folder (stable/preview) + // If it's stable/preview, compare at namespace root to avoid false negatives when + // introducing the first preview/stable folder under an existing namespace + const potentialServiceGroup = parts[4]; + const isLifecycleFolder = potentialServiceGroup === "stable" || potentialServiceGroup === "preview"; + const namespacePath = isLifecycleFolder + ? `specification/${orgName}/resource-manager/${namespace}` + : `specification/${orgName}/resource-manager/${namespace}/${potentialServiceGroup}`; + const serviceKey = isLifecycleFolder ? namespace : `${namespace}/${potentialServiceGroup}`; if (!namespaceMap.has(serviceKey)) { namespaceMap.set(serviceKey, { orgName, namespacePath, namespace }); @@ -215,6 +220,7 @@ export async function detectNewResourceTypes({ git, "HEAD^", namespacePath, + "base", ); // Skip namespace if it doesn't exist in base (brand new RP — handled by RP-level detection) @@ -227,6 +233,7 @@ export async function detectNewResourceTypes({ git, "HEAD", namespacePath, + "head", ); const newTypes = []; diff --git a/.github/workflows/src/summarize-checks/labelling.js b/.github/workflows/src/summarize-checks/labelling.js index 81c3155dfb0f..78dc65c7fc91 100644 --- a/.github/workflows/src/summarize-checks/labelling.js +++ b/.github/workflows/src/summarize-checks/labelling.js @@ -893,7 +893,7 @@ const rulesPri0NotReadyForArmReview = [ }, { precedence: 0, - allPrerequisiteLabels: ["NotReadyForARMReview", "ARMModelingReviewRequired"], + anyPrerequisiteLabels: ["ARMModelingReviewRequired"], anyRequiredLabels: [], troubleshootingGuide: wrapInArmReviewMessage( "This PR has ARMModelingReviewRequired label. " + diff --git a/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js b/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js index aae2ac5e122b..0a4e503c1527 100644 --- a/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js +++ b/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { Temporal } from "@js-temporal/polyfill"; /** @type {import("vitest").MockedFunction} */ @@ -17,9 +17,13 @@ vi.mock("../../../shared/src/simple-git.js", () => ({ import { checkLease, parseLease } from "../../src/arm-lease-validation/detect-arm-leases.js"; -/** Get today's date string using Temporal (same as source code) */ +// 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 Temporal.Now.plainDateISO(); + return FIXED_PLAIN_DATE; } /** Subtract days from today and return YYYY-MM-DD string @@ -35,6 +39,17 @@ function leaseYaml(startdate, duration) { } 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(); }); diff --git a/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js b/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js index e519bdbbeeac..ca6ff0471158 100644 --- a/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js +++ b/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js @@ -127,11 +127,12 @@ describe("detectNewResourceTypes", () => { 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`; - const servicePath = `${ns}/stable`; + // When path contains stable/preview, compare at namespace root + const namespacePath = ns; setupGit({ - baseFiles: new Map([[servicePath, [file]]]), - headFiles: new Map([[servicePath, [file]]]), + baseFiles: new Map([[namespacePath, [file]]]), + headFiles: new Map([[namespacePath, [file]]]), fileContents: new Map([ [`HEAD^:${file}`, vmSwagger], [`HEAD:${file}`, vmSwagger], @@ -152,11 +153,12 @@ describe("detectNewResourceTypes", () => { 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`; - const servicePath = `${ns}/stable`; + // When path contains stable/preview, compare at namespace root + const namespacePath = ns; setupGit({ - baseFiles: new Map([[servicePath, [file]]]), - headFiles: new Map([[servicePath, [file]]]), + 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 @@ -188,17 +190,18 @@ describe("detectNewResourceTypes", () => { 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`; - const computeServicePath = `${computeNs}/stable`; - const networkServicePath = `${networkNs}/stable`; + // When path contains stable/preview, compare at namespace root + const computeNamespacePath = computeNs; + const networkNamespacePath = networkNs; setupGit({ baseFiles: new Map([ - [computeServicePath, [computeFile]], - [networkServicePath, [networkFile]], + [computeNamespacePath, [computeFile]], + [networkNamespacePath, [networkFile]], ]), headFiles: new Map([ - [computeServicePath, [computeFile]], - [networkServicePath, [networkFile]], + [computeNamespacePath, [computeFile]], + [networkNamespacePath, [networkFile]], ]), fileContents: new Map([ [`HEAD^:${computeFile}`, emptySwagger], @@ -224,11 +227,12 @@ describe("detectNewResourceTypes", () => { 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`; - const servicePath = `${ns}/stable`; + // When path contains stable/preview, compare at namespace root + const namespacePath = ns; setupGit({ - baseFiles: new Map([[servicePath, [file, exampleFile]]]), - headFiles: new Map([[servicePath, [file, exampleFile]]]), + baseFiles: new Map([[namespacePath, [file, exampleFile]]]), + headFiles: new Map([[namespacePath, [file, exampleFile]]]), fileContents: new Map([ [`HEAD^:${file}`, emptySwagger], [`HEAD:${file}`, emptySwagger], @@ -251,11 +255,12 @@ describe("detectNewResourceTypes", () => { const ns = "specification/compute/resource-manager/Microsoft.Compute"; const file = `${ns}/stable/2024-01-01/compute.json`; const readme = `${ns}/stable/readme.md`; - const servicePath = `${ns}/stable`; + // When path contains stable/preview, compare at namespace root + const namespacePath = ns; setupGit({ - baseFiles: new Map([[servicePath, [file, readme]]]), - headFiles: new Map([[servicePath, [file, readme]]]), + baseFiles: new Map([[namespacePath, [file, readme]]]), + headFiles: new Map([[namespacePath, [file, readme]]]), fileContents: new Map([ [`HEAD^:${file}`, emptySwagger], [`HEAD:${file}`, emptySwagger], @@ -274,6 +279,65 @@ describe("detectNewResourceTypes", () => { ); }); + 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({ + repoRoot: "/fake/repo", + rmFiles: [previewFile], + core, + }); + + // Should detect the new quantumVMs resource type from preview + expect(result).toHaveLength(1); + expect(result[0].namespace).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`; From 26dc21ebe519db409418f34123e74f9928ce5cac Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Thu, 12 Mar 2026 13:40:28 -0500 Subject: [PATCH 062/125] Naming convention --- .../detect-new-resource-types.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js b/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js index 6628ad4d699a..b64680120cf3 100644 --- a/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js +++ b/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js @@ -97,12 +97,12 @@ function getResourceTypesFromSwagger(swaggerDoc) { * Get resource types from swagger files at a specific git ref. * * @param {import("simple-git").SimpleGit} git - * @param {string} commitish - Git ref + * @param {string} gitRef - Git ref (e.g. "HEAD" or "HEAD^") * @param {string} namespacePath - e.g. `specification/compute/resource-manager/Microsoft.Compute` - * @param {string} label - Label for logging (e.g. "base" or "head") + * @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, commitish, namespacePath, label) { +async function getResourceTypesAtRef(git, gitRef, namespacePath, branchName) { /** @type {Map} */ const allTypes = new Map(); @@ -112,7 +112,7 @@ async function getResourceTypesAtRef(git, commitish, namespacePath, label) { "ls-tree", "-r", "--name-only", - commitish, + gitRef, namespacePath, ]); } catch { @@ -126,7 +126,7 @@ async function getResourceTypesAtRef(git, commitish, namespacePath, label) { const total = swaggerFiles.length; if (total > 0) { - console.log(`Analyzing ${total} swagger files in ${label} (${commitish}) for comparison...`); + console.log(`Analyzing ${total} swagger files in ${branchName} branch (${gitRef}) for comparison...`); } // Iterate over files, but only log summary if it's too much @@ -138,7 +138,7 @@ async function getResourceTypesAtRef(git, commitish, namespacePath, label) { let content; try { - content = await git.show([`${commitish}:${file.trim()}`]); + content = await git.show([`${gitRef}:${file.trim()}`]); } catch { // Intentionally quiet to avoid log spam during comparison continue; From 97909536379651ac15c1f6be50cb1d7504e35ab7 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Thu, 12 Mar 2026 16:33:10 -0500 Subject: [PATCH 063/125] Names --- .../detect-new-resource-provider.js | 8 ++-- .../detect-new-resource-types.js | 40 +++++++++---------- .../detect-new-resource-types.test.js | 6 +-- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js b/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js index aad4168d7654..b38f2834152a 100644 --- a/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js +++ b/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js @@ -305,18 +305,18 @@ async function checkNewResourceTypes(repoRoot, rmFiles, core) { }; } - core.info(`Detected new resource types in ${newRtResults.length} namespace(s)`); + 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.namespace, ""); + const leaseValid = await checkLease(ns.orgName, ns.rpNamespace, ""); if (leaseValid) { - core.info(` - ${ns.namespace}: valid ARM lease for new resource types`); + core.info(` - ${ns.rpNamespace}: valid ARM lease for new resource types`); } else { allLeasesValid = false; core.error( - `${ns.namespace}: new resource types detected without a valid ARM lease`, + `${ns.rpNamespace}: new resource types detected without a valid ARM lease`, ); } } diff --git a/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js b/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js index b64680120cf3..58770e9f12bb 100644 --- a/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js +++ b/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js @@ -183,37 +183,37 @@ export async function detectNewResourceTypes({ }) { const git = simpleGit(repoRoot); - // Group changed RM swagger files by service subdirectory (namespace + service group) - /** @type {Map} */ + // 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 namespace directory - // (the trailing "/" in the pattern requires at least one path component after the namespace) + // 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 namespace = parts[3]; - // Check if parts[4] is an actual service group (e.g. DiskRP) vs lifecycle folder (stable/preview) - // If it's stable/preview, compare at namespace root to avoid false negatives when - // introducing the first preview/stable folder under an existing namespace - const potentialServiceGroup = parts[4]; - const isLifecycleFolder = potentialServiceGroup === "stable" || potentialServiceGroup === "preview"; + 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 namespacePath = isLifecycleFolder - ? `specification/${orgName}/resource-manager/${namespace}` - : `specification/${orgName}/resource-manager/${namespace}/${potentialServiceGroup}`; - const serviceKey = isLifecycleFolder ? namespace : `${namespace}/${potentialServiceGroup}`; + ? `specification/${orgName}/resource-manager/${rpNamespace}` + : `specification/${orgName}/resource-manager/${rpNamespace}/${serviceName}`; + const serviceKey = isLifecycleFolder ? rpNamespace : `${rpNamespace}/${serviceName}`; if (!namespaceMap.has(serviceKey)) { - namespaceMap.set(serviceKey, { orgName, namespacePath, namespace }); + namespaceMap.set(serviceKey, { orgName, namespacePath, rpNamespace }); } } const results = []; - for (const [serviceKey, { orgName, namespacePath, namespace }] of namespaceMap) { + for (const [serviceKey, { orgName, namespacePath, rpNamespace }] of namespaceMap) { core.info(`Checking for new resource types in ${serviceKey}...`); const baseTypes = await getResourceTypesAtRef( @@ -223,9 +223,9 @@ export async function detectNewResourceTypes({ "base", ); - // Skip namespace if it doesn't exist in base (brand new RP — handled by RP-level detection) + // Skip rpNamespace if it doesn't exist in base (brand new RP — handled by RP-level detection) if (baseTypes === null) { - core.info(` ${namespace}: no resources in base (new namespace), skipping RT detection`); + core.info(` ${rpNamespace}: no resources in base (new rpNamespace), skipping RT detection`); continue; } @@ -252,14 +252,14 @@ export async function detectNewResourceTypes({ if (newTypes.length > 0) { core.info( - ` ${namespace}: ${newTypes.length} new resource type(s) detected`, + ` ${rpNamespace}: ${newTypes.length} new resource type(s) detected`, ); for (const t of newTypes) { core.info(` - ${t.resourceType} (${t.operations.join(", ")})`); } - results.push({ namespace, orgName, newResourceTypes: newTypes }); + results.push({ rpNamespace, orgName, newResourceTypes: newTypes }); } else { - core.info(` ${namespace}: no new resource types`); + core.info(` ${rpNamespace}: no new resource types`); } } diff --git a/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js b/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js index ca6ff0471158..ef21739cf2bf 100644 --- a/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js +++ b/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js @@ -174,7 +174,7 @@ describe("detectNewResourceTypes", () => { expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ - namespace: "Microsoft.Compute", + rpNamespace: "Microsoft.Compute", orgName: "compute", }); expect(result[0].newResourceTypes).toHaveLength(1); @@ -329,7 +329,7 @@ describe("detectNewResourceTypes", () => { // Should detect the new quantumVMs resource type from preview expect(result).toHaveLength(1); - expect(result[0].namespace).toBe("Microsoft.Compute"); + expect(result[0].rpNamespace).toBe("Microsoft.Compute"); const quantumType = result[0].newResourceTypes.find((t) => t.resourceType === "Microsoft.Compute/quantumVMs", ); @@ -374,7 +374,7 @@ describe("detectNewResourceTypes", () => { }); expect(result).toHaveLength(1); - expect(result[0].namespace).toBe("Microsoft.Compute"); + expect(result[0].rpNamespace).toBe("Microsoft.Compute"); // superDisks should be detected const superDisksType = result[0].newResourceTypes.find((t) => t.resourceType === "Microsoft.Compute/superDisks", From 99fda8195ded7e80dd15749108882cf577170c51 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Thu, 12 Mar 2026 20:18:41 -0500 Subject: [PATCH 064/125] Restore TextToSpeech files accidentally dropped in merge --- .../data-plane/TextToSpeech/client.tsp | 24 + .../examples/2026-01-01/create_consent.json | 54 + .../create_consent_with_multipart_form.json | 37 + .../examples/2026-01-01/create_endpoint.json | 53 + .../examples/2026-01-01/create_model.json | 68 + .../2026-01-01/create_multi_style_model.json | 111 + .../2026-01-01/create_personalvoice.json | 53 + ...ate_personalvoice_with_multipart_form.json | 32 + .../examples/2026-01-01/create_project.json | 28 + .../2026-01-01/create_trainingset.json | 32 + .../examples/2026-01-01/delete_consent.json | 15 + .../examples/2026-01-01/delete_endpoint.json | 15 + .../examples/2026-01-01/delete_model.json | 15 + .../2026-01-01/delete_personalvoice.json | 15 + .../examples/2026-01-01/delete_project.json | 15 + .../2026-01-01/delete_trainingset.json | 15 + .../examples/2026-01-01/get_base_models.json | 26 + .../examples/2026-01-01/get_consent.json | 26 + .../examples/2026-01-01/get_consents.json | 41 + .../examples/2026-01-01/get_endpoint.json | 27 + .../examples/2026-01-01/get_endpoints.json | 43 + .../examples/2026-01-01/get_model.json | 32 + .../examples/2026-01-01/get_models.json | 75 + .../examples/2026-01-01/get_operation.json | 19 + .../2026-01-01/get_personalvoice.json | 24 + .../2026-01-01/get_personalvoices.json | 37 + .../examples/2026-01-01/get_project.json | 22 + .../examples/2026-01-01/get_projects.json | 33 + .../examples/2026-01-01/get_recipes.json | 84 + .../examples/2026-01-01/get_trainingset.json | 30 + .../examples/2026-01-01/get_trainingsets.json | 49 + .../examples/2026-01-01/resume_endpoint.json | 31 + .../examples/2026-01-01/suspend_endpoint.json | 31 + .../2026-01-01/upload_trainingset.json | 38 + .../data-plane/TextToSpeech/main.tsp | 42 + .../data-plane/TextToSpeech/models.tsp | 751 ++++ .../data-plane/TextToSpeech/readme.md | 28 + .../data-plane/TextToSpeech/routes.tsp | 433 +++ .../2026-01-01/examples/create_consent.json | 54 + .../create_consent_with_multipart_form.json | 37 + .../2026-01-01/examples/create_endpoint.json | 53 + .../2026-01-01/examples/create_model.json | 68 + .../examples/create_multi_style_model.json | 111 + .../examples/create_personalvoice.json | 53 + ...ate_personalvoice_with_multipart_form.json | 32 + .../2026-01-01/examples/create_project.json | 28 + .../examples/create_trainingset.json | 32 + .../2026-01-01/examples/delete_consent.json | 15 + .../2026-01-01/examples/delete_endpoint.json | 15 + .../2026-01-01/examples/delete_model.json | 15 + .../examples/delete_personalvoice.json | 15 + .../2026-01-01/examples/delete_project.json | 15 + .../examples/delete_trainingset.json | 15 + .../2026-01-01/examples/get_base_models.json | 26 + .../2026-01-01/examples/get_consent.json | 26 + .../2026-01-01/examples/get_consents.json | 41 + .../2026-01-01/examples/get_endpoint.json | 27 + .../2026-01-01/examples/get_endpoints.json | 43 + .../stable/2026-01-01/examples/get_model.json | 32 + .../2026-01-01/examples/get_models.json | 75 + .../2026-01-01/examples/get_operation.json | 19 + .../examples/get_personalvoice.json | 24 + .../examples/get_personalvoices.json | 37 + .../2026-01-01/examples/get_project.json | 22 + .../2026-01-01/examples/get_projects.json | 33 + .../2026-01-01/examples/get_recipes.json | 84 + .../2026-01-01/examples/get_trainingset.json | 30 + .../2026-01-01/examples/get_trainingsets.json | 49 + .../2026-01-01/examples/resume_endpoint.json | 31 + .../2026-01-01/examples/suspend_endpoint.json | 31 + .../examples/upload_trainingset.json | 38 + .../stable/2026-01-01/texttospeech.json | 3075 +++++++++++++++++ .../data-plane/TextToSpeech/tspconfig.yaml | 39 + 73 files changed, 6844 insertions(+) create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/client.tsp create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_consent.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_consent_with_multipart_form.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_endpoint.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_model.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_multi_style_model.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_personalvoice.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_personalvoice_with_multipart_form.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_project.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_trainingset.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_consent.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_endpoint.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_model.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_personalvoice.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_project.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_trainingset.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_base_models.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_consent.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_consents.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_endpoint.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_endpoints.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_model.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_models.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_operation.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_personalvoice.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_personalvoices.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_project.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_projects.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_recipes.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_trainingset.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_trainingsets.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/resume_endpoint.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/suspend_endpoint.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/upload_trainingset.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/main.tsp create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/models.tsp create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/readme.md create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/routes.tsp create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_consent.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_consent_with_multipart_form.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_endpoint.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_model.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_multi_style_model.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_personalvoice.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_personalvoice_with_multipart_form.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_project.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_trainingset.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_consent.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_endpoint.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_model.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_personalvoice.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_project.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_trainingset.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_base_models.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_consent.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_consents.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_endpoint.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_endpoints.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_model.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_models.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_operation.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_personalvoice.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_personalvoices.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_project.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_projects.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_recipes.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_trainingset.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_trainingsets.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/resume_endpoint.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/suspend_endpoint.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/upload_trainingset.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/texttospeech.json create mode 100644 specification/cognitiveservices/data-plane/TextToSpeech/tspconfig.yaml diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/client.tsp b/specification/cognitiveservices/data-plane/TextToSpeech/client.tsp new file mode 100644 index 000000000000..e4de3df3bfdf --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/client.tsp @@ -0,0 +1,24 @@ +import "./main.tsp"; +import "@azure-tools/typespec-client-generator-core"; + +using Azure.ClientGenerator.Core; +using CustomVoiceApi; + +// AZC0012: Rename generic interface/client names for C# +@@clientName(Projects, "CustomVoiceProjects", "csharp"); +@@clientName(Consents, "CustomVoiceConsents", "csharp"); +@@clientName(Endpoints, "CustomVoiceEndpoints", "csharp"); +@@clientName(Operations, "CustomVoiceOperations", "csharp"); +@@clientName(Models, "CustomVoiceModels", "csharp"); + +// AZC0012: Rename generic model names for C# +@@clientName(Model, "CustomVoiceModel", "csharp"); +@@clientName(Endpoint, "CustomVoiceEndpoint", "csharp"); +@@clientName(Dataset, "CustomVoiceDataset", "csharp"); +@@clientName(Status, "CustomVoiceStatus", "csharp"); +@@clientName(Project, "CustomVoiceProject", "csharp"); +@@clientName(Recipe, "CustomVoiceRecipe", "csharp"); +@@clientName(Consent, "CustomVoiceConsent", "csharp"); + +// AZC0034: Operation conflicts with Azure.Operation from Azure.Core +@@clientName(Operation, "CustomVoiceOperation", "csharp"); diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_consent.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_consent.json new file mode 100644 index 000000000000..b5c6ccdac292 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_consent.json @@ -0,0 +1,54 @@ +{ + "parameters": { + "Content-Type": "application/json", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "070f7986-ef17-41d0-ba2b-907f0f28e314", + "api-version": "2026-01-01", + "resource": { + "description": "Consent for Jessica voice", + "audioUrl": "https://contoso.blob.core.windows.net/public/jessica-consent.wav?mySasToken", + "companyName": "Contoso", + "locale": "en-US", + "projectId": "Jessica", + "voiceTalentName": "Jessica Smith" + }, + "id": "Jessica" + }, + "responses": { + "200": { + "body": { + "description": "Consent for Jessica voice", + "companyName": "Contoso", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "status": "NotStarted", + "voiceTalentName": "Jessica Smith" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/070f7986-ef17-41d0-ba2b-907f0f28e314?api-version=2026-01-01" + } + }, + "201": { + "body": { + "description": "Consent for Jessica voice", + "companyName": "Contoso", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "status": "NotStarted", + "voiceTalentName": "Jessica Smith" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/070f7986-ef17-41d0-ba2b-907f0f28e314?api-version=2026-01-01" + } + } + }, + "operationId": "Consents_Create", + "title": "Create a consent" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_consent_with_multipart_form.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_consent_with_multipart_form.json new file mode 100644 index 000000000000..0ee65c43c5b9 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_consent_with_multipart_form.json @@ -0,0 +1,37 @@ +{ + "parameters": { + "description": "Consent for Jessica voice", + "Content-Type": "multipart/form-data", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "070f7986-ef17-41d0-ba2b-907f0f28e314", + "api-version": "2026-01-01", + "audioData": "{audio file}", + "companyName": "Contoso", + "id": "Jessica", + "locale": "en-US", + "projectId": "Jessica", + "voiceTalentName": "Jessica Smith" + }, + "responses": { + "201": { + "body": { + "description": "Consent for Jessica voice", + "companyName": "Contoso", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "status": "NotStarted", + "voiceTalentName": "Jessica Smith" + }, + "headers": { + "Operation-Id": "070f7986-ef17-41d0-ba2b-907f0f28e314", + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/070f7986-ef17-41d0-ba2b-907f0f28e314?api-version=2026-01-01" + } + } + }, + "operationId": "Consents_CreateFromAudioFile", + "title": "Create a consent" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_endpoint.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_endpoint.json new file mode 100644 index 000000000000..5bbe068d760b --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_endpoint.json @@ -0,0 +1,53 @@ +{ + "parameters": { + "Content-Type": "application/json", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "2595b58d-40d6-4032-a618-482dcce1c130", + "api-version": "2026-01-01", + "resource": { + "description": "Endpoint for Jessica voice", + "modelId": "Jessica", + "projectId": "Jessica" + }, + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb" + }, + "responses": { + "200": { + "body": { + "description": "Endpoint for Jessica voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "modelId": "Jessica", + "projectId": "Jessica", + "properties": { + "kind": "HighPerformance" + }, + "status": "NotStarted" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/2595b58d-40d6-4032-a618-482dcce1c130?api-version=2026-01-01" + } + }, + "201": { + "body": { + "description": "Endpoint for Jessica voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "modelId": "Jessica", + "projectId": "Jessica", + "properties": { + "kind": "HighPerformance" + }, + "status": "NotStarted" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/2595b58d-40d6-4032-a618-482dcce1c130?api-version=2026-01-01" + } + } + }, + "operationId": "Endpoints_Create", + "title": "Create an endpoint" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_model.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_model.json new file mode 100644 index 000000000000..e98466e04b52 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_model.json @@ -0,0 +1,68 @@ +{ + "parameters": { + "Content-Type": "application/json", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "1f4352df-f247-40c0-a7b1-a54d017933e1", + "api-version": "2026-01-01", + "id": "Jessica", + "resource": { + "description": "Jessica voice", + "consentId": "Jessica", + "projectId": "Jessica", + "recipe": { + "kind": "Default" + }, + "trainingSetId": "Jessica-300", + "voiceName": "JessicaNeural" + } + }, + "responses": { + "200": { + "body": { + "description": "Jessica voice", + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "engineVersion": "2023.07.04.0", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "recipe": { + "kind": "Default", + "version": "V7.2023.03" + }, + "status": "NotStarted", + "trainingSetId": "Jessica-300", + "voiceName": "JessicaNeural" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/1f4352df-f247-40c0-a7b1-a54d017933e1?api-version=2026-01-01" + } + }, + "201": { + "body": { + "description": "Jessica voice", + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "engineVersion": "2023.07.04.0", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "recipe": { + "kind": "Default", + "version": "V7.2023.03" + }, + "status": "NotStarted", + "trainingSetId": "Jessica-300", + "voiceName": "JessicaNeural" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/1f4352df-f247-40c0-a7b1-a54d017933e1?api-version=2026-01-01" + } + } + }, + "operationId": "Models_Create", + "title": "Create a model" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_multi_style_model.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_multi_style_model.json new file mode 100644 index 000000000000..990bba1c5db5 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_multi_style_model.json @@ -0,0 +1,111 @@ +{ + "parameters": { + "Content-Type": "application/json", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "a01a127a-c204-4e46-a8c1-fab01559b05b", + "api-version": "2026-01-01", + "id": "JessicaMultiStyle", + "resource": { + "description": "Jessica multi style voice", + "consentId": "Jessica", + "locale": "en-US", + "projectId": "Jessica", + "properties": { + "presetStyles": [ + "cheerful", + "sad" + ], + "styleTrainingSetIds": { + "happy": "JessicaHappy-300", + "myStyle2": "JessicaStyle2" + } + }, + "recipe": { + "kind": "MultiStyle" + }, + "trainingSetId": "Jessica-300", + "voiceName": "JessicaMultiStyleNeural" + } + }, + "responses": { + "200": { + "body": { + "description": "Jessica multi style voice", + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "engineVersion": "2023.07.04.0", + "id": "JessicaMultiStyle", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "properties": { + "presetStyles": [ + "cheerful", + "sad" + ], + "styleTrainingSetIds": { + "happy": "JessicaHappy-300", + "myStyle2": "JessicaStyle2" + }, + "voiceStyles": [ + "cheerful", + "sad", + "happy", + "myStyle2" + ] + }, + "recipe": { + "kind": "MultiStyle", + "version": "V3.2023.06" + }, + "status": "NotStarted", + "trainingSetId": "Jessica-300", + "voiceName": "JessicaMultiStyleNeural" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/a01a127a-c204-4e46-a8c1-fab01559b05b?api-version=2026-01-01" + } + }, + "201": { + "body": { + "description": "Jessica multi style voice", + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "engineVersion": "2023.07.04.0", + "id": "JessicaMultiStyle", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "properties": { + "presetStyles": [ + "cheerful", + "sad" + ], + "styleTrainingSetIds": { + "happy": "JessicaHappy-300", + "myStyle2": "JessicaStyle2" + }, + "voiceStyles": [ + "cheerful", + "sad", + "happy", + "myStyle2" + ] + }, + "recipe": { + "kind": "MultiStyle", + "version": "V3.2023.06" + }, + "status": "NotStarted", + "trainingSetId": "Jessica-300", + "voiceName": "JessicaMultiStyleNeural" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/a01a127a-c204-4e46-a8c1-fab01559b05b?api-version=2026-01-01" + } + } + }, + "operationId": "Models_Create", + "title": "Create a multi style model" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_personalvoice.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_personalvoice.json new file mode 100644 index 000000000000..a9829dad24b6 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_personalvoice.json @@ -0,0 +1,53 @@ +{ + "parameters": { + "Content-Type": "application/json", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "1321a2c0-9be4-471d-83bb-bc3be4f96a6f", + "api-version": "2026-01-01", + "id": "Jessica-PersonalVoice", + "resource": { + "audios": { + "containerUrl": "https://contoso.blob.core.windows.net/voicecontainer?mySasToken", + "prefix": "jessica/", + "extensions": [ + ".wav" + ] + }, + "consentId": "Jessica", + "projectId": "PersonalVoice" + } + }, + "responses": { + "200": { + "body": { + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-PersonalVoice", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "projectId": "PersonalVoice", + "speakerProfileId": "3059912f-a3dc-49e3-bdd0-02e449df1fe3", + "status": "NotStarted" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/1321a2c0-9be4-471d-83bb-bc3be4f96a6f?api-version=2026-01-01" + } + }, + "201": { + "body": { + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-PersonalVoice", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "projectId": "PersonalVoice", + "speakerProfileId": "3059912f-a3dc-49e3-bdd0-02e449df1fe3", + "status": "NotStarted" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/1321a2c0-9be4-471d-83bb-bc3be4f96a6f?api-version=2026-01-01" + } + } + }, + "operationId": "PersonalVoices_Create", + "title": "Create a personal voice" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_personalvoice_with_multipart_form.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_personalvoice_with_multipart_form.json new file mode 100644 index 000000000000..b29756624f51 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_personalvoice_with_multipart_form.json @@ -0,0 +1,32 @@ +{ + "parameters": { + "Content-Type": "multipart/form-data", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "1321a2c0-9be4-471d-83bb-bc3be4f96a6f", + "api-version": "2026-01-01", + "audioData": "{audio files}", + "consentId": "Jessica", + "id": "Jessica-PersonalVoice", + "projectId": "PersonalVoice" + }, + "responses": { + "201": { + "body": { + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-PersonalVoice", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "projectId": "PersonalVoice", + "speakerProfileId": "3059912f-a3dc-49e3-bdd0-02e449df1fe3", + "status": "NotStarted" + }, + "headers": { + "Operation-Id": "1321a2c0-9be4-471d-83bb-bc3be4f96a6f", + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/1321a2c0-9be4-471d-83bb-bc3be4f96a6f?api-version=2026-01-01" + } + } + }, + "operationId": "PersonalVoices_CreateFromAudioFile", + "title": "Create a personal voice" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_project.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_project.json new file mode 100644 index 000000000000..4b622e05c216 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_project.json @@ -0,0 +1,28 @@ +{ + "parameters": { + "Content-Type": "application/json", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica", + "project": { + "description": "Project for Jessica Voice", + "kind": "ProfessionalVoice", + "locale": "en-US" + } + }, + "responses": { + "201": { + "body": { + "description": "Project for Jessica Voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica", + "kind": "ProfessionalVoice", + "locale": "en-US" + }, + "headers": {} + } + }, + "operationId": "Projects_Create", + "title": "Create a project" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_trainingset.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_trainingset.json new file mode 100644 index 000000000000..6ce3d239bcf4 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/create_trainingset.json @@ -0,0 +1,32 @@ +{ + "parameters": { + "Content-Type": "application/json", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica-300", + "trainingset": { + "description": "300 sentences Jessica data in general style.", + "locale": "en-US", + "projectId": "Jessica", + "voiceKind": "Female" + } + }, + "responses": { + "201": { + "body": { + "description": "300 sentences Jessica data in general style.", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-300", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "status": "Succeeded", + "voiceKind": "Female" + }, + "headers": {} + } + }, + "operationId": "TrainingSets_Create", + "title": "Create a training set" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_consent.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_consent.json new file mode 100644 index 000000000000..69cbf22e047a --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_consent.json @@ -0,0 +1,15 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica" + }, + "responses": { + "204": { + "headers": {} + } + }, + "operationId": "Consents_Delete", + "title": "Delete a consent" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_endpoint.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_endpoint.json new file mode 100644 index 000000000000..3a59f658f580 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_endpoint.json @@ -0,0 +1,15 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb" + }, + "responses": { + "204": { + "headers": {} + } + }, + "operationId": "Endpoints_Delete", + "title": "Delete an endpoint" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_model.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_model.json new file mode 100644 index 000000000000..ae826b82facc --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_model.json @@ -0,0 +1,15 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica" + }, + "responses": { + "204": { + "headers": {} + } + }, + "operationId": "Models_Delete", + "title": "Delete a model" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_personalvoice.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_personalvoice.json new file mode 100644 index 000000000000..dbb311f86b8c --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_personalvoice.json @@ -0,0 +1,15 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica-PersonalVoice" + }, + "responses": { + "204": { + "headers": {} + } + }, + "operationId": "PersonalVoices_Delete", + "title": "Delete a personal voice" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_project.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_project.json new file mode 100644 index 000000000000..9cf3eee1456b --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_project.json @@ -0,0 +1,15 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica" + }, + "responses": { + "204": { + "headers": {} + } + }, + "operationId": "Projects_Delete", + "title": "Delete a project" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_trainingset.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_trainingset.json new file mode 100644 index 000000000000..f39eda3e8a94 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/delete_trainingset.json @@ -0,0 +1,15 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica-300" + }, + "responses": { + "204": { + "headers": {} + } + }, + "operationId": "TrainingSets_Delete", + "title": "Delete a training set" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_base_models.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_base_models.json new file mode 100644 index 000000000000..ea8bc54ec7fd --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_base_models.json @@ -0,0 +1,26 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "name": "PhoenixV2Neural", + "description": "Phonenix V2 base model", + "capabilities": [ + "PersonalVoice" + ], + "releaseDateTime": "2023-12-01T00:00:00.000Z" + } + ] + }, + "headers": {} + } + }, + "operationId": "BaseModels_List", + "title": "Get base models" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_consent.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_consent.json new file mode 100644 index 000000000000..d4f77bea11fb --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_consent.json @@ -0,0 +1,26 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica" + }, + "responses": { + "200": { + "body": { + "description": "Consent for Jessica voice", + "companyName": "Contoso", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "status": "Succeeded", + "voiceTalentName": "Jessica Smith" + }, + "headers": {} + } + }, + "operationId": "Consents_Get", + "title": "Get a consent" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_consents.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_consents.json new file mode 100644 index 000000000000..f052122c9c1a --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_consents.json @@ -0,0 +1,41 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01" + }, + "responses": { + "200": { + "body": { + "nextLink": "https://eastus.api.cognitive.microsoft.com/customvoice/consents?skip=2&maxpagesize=2&api-version=2026-01-01", + "value": [ + { + "description": "Consent for Jessica voice", + "companyName": "Contoso", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "status": "Succeeded", + "voiceTalentName": "Jessica Smith" + }, + { + "description": "Consent for Jessica voice", + "companyName": "Contoso", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "status": "Succeeded", + "voiceTalentName": "Jessica Smith" + } + ] + }, + "headers": {} + } + }, + "operationId": "Consents_List", + "title": "Get all consents" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_endpoint.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_endpoint.json new file mode 100644 index 000000000000..bc30b895aa8b --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_endpoint.json @@ -0,0 +1,27 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb" + }, + "responses": { + "200": { + "body": { + "description": "Endpoint for Jessica voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "modelId": "Jessica", + "projectId": "Jessica", + "properties": { + "kind": "HighPerformance" + }, + "status": "NotStarted" + }, + "headers": {} + } + }, + "operationId": "Endpoints_Get", + "title": "Get an endpoint" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_endpoints.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_endpoints.json new file mode 100644 index 000000000000..ecedfeb4121f --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_endpoints.json @@ -0,0 +1,43 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01" + }, + "responses": { + "200": { + "body": { + "nextLink": "https://eastus.api.cognitive.microsoft.com/customvoice/endpoints?skip=2&maxpagesize=2&api-version=2026-01-01", + "value": [ + { + "description": "Endpoint for Jessica voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "modelId": "Jessica", + "projectId": "Jessica", + "properties": { + "kind": "HighPerformance" + }, + "status": "NotStarted" + }, + { + "description": "Endpoint for Jessica voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "modelId": "Jessica", + "projectId": "Jessica", + "properties": { + "kind": "HighPerformance" + }, + "status": "NotStarted" + } + ] + }, + "headers": {} + } + }, + "operationId": "Endpoints_List", + "title": "Get all endpoints" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_model.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_model.json new file mode 100644 index 000000000000..c4df3786f669 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_model.json @@ -0,0 +1,32 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica" + }, + "responses": { + "200": { + "body": { + "description": "Jessica voice", + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "engineVersion": "2023.07.04.0", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "recipe": { + "kind": "Default", + "version": "V7.2023.03" + }, + "status": "NotStarted", + "trainingSetId": "Jessica-300", + "voiceName": "JessicaNeural" + }, + "headers": {} + } + }, + "operationId": "Models_Get", + "title": "Get a model" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_models.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_models.json new file mode 100644 index 000000000000..f3bfaceb8177 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_models.json @@ -0,0 +1,75 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01" + }, + "responses": { + "200": { + "body": { + "nextLink": "https://eastus.api.cognitive.microsoft.com/customvoice/models?skip=2&maxpagesize=2&api-version=2026-01-01", + "value": [ + { + "description": "Jessica voice", + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "engineVersion": "2023.07.04.0", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "properties": { + "secondaryLocales": [ + "en-GB", + "en-AU" + ] + }, + "recipe": { + "kind": "Default", + "version": "V7.2023.03" + }, + "status": "NotStarted", + "trainingSetId": "Jessica-300", + "voiceName": "JessicaNeural" + }, + { + "description": "Jessica multi style voice", + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "engineVersion": "2023.07.04.0", + "id": "JessicaMultiStyle", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "properties": { + "presetStyles": [ + "cheerful", + "sad" + ], + "styleTrainingSetIds": { + "happy": "JessicaHappy-300", + "myStyle2": "JessicaStyle2" + }, + "voiceStyles": [ + "cheerful", + "sad", + "happy", + "myStyle2" + ] + }, + "recipe": { + "kind": "MultiStyle", + "version": "V3.2023.06" + }, + "status": "NotStarted", + "trainingSetId": "Jessica-300", + "voiceName": "JessicaMultiStyleNeural" + } + ] + }, + "headers": {} + } + }, + "operationId": "Models_List", + "title": "Get all models" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_operation.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_operation.json new file mode 100644 index 000000000000..6fc6f61ecc79 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_operation.json @@ -0,0 +1,19 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "06c7f835-e07b-4ac8-b33c-5b6df4a4eeef" + }, + "responses": { + "200": { + "body": { + "id": "06c7f835-e07b-4ac8-b33c-5b6df4a4eeef", + "status": "Running" + }, + "headers": {} + } + }, + "operationId": "Operations_Get", + "title": "Get Operation" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_personalvoice.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_personalvoice.json new file mode 100644 index 000000000000..754c9e58cfc7 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_personalvoice.json @@ -0,0 +1,24 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica-PersonalVoice" + }, + "responses": { + "200": { + "body": { + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-PersonalVoice", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "projectId": "PersonalVoice", + "speakerProfileId": "3059912f-a3dc-49e3-bdd0-02e449df1fe3", + "status": "NotStarted" + }, + "headers": {} + } + }, + "operationId": "PersonalVoices_Get", + "title": "Get a personal voice" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_personalvoices.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_personalvoices.json new file mode 100644 index 000000000000..408c3df9bde9 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_personalvoices.json @@ -0,0 +1,37 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01" + }, + "responses": { + "200": { + "body": { + "nextLink": "https://eastus.api.cognitive.microsoft.com/customvoice/personalvoices?skip=2&maxpagesize=2&api-version=2026-01-01", + "value": [ + { + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-PersonalVoice", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "projectId": "PersonalVoice", + "speakerProfileId": "3059912f-a3dc-49e3-bdd0-02e449df1fe3", + "status": "NotStarted" + }, + { + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-PersonalVoice", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "projectId": "PersonalVoice", + "speakerProfileId": "3059912f-a3dc-49e3-bdd0-02e449df1fe3", + "status": "NotStarted" + } + ] + }, + "headers": {} + } + }, + "operationId": "PersonalVoices_List", + "title": "Get all personal voices" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_project.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_project.json new file mode 100644 index 000000000000..4dd019c3bd28 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_project.json @@ -0,0 +1,22 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica" + }, + "responses": { + "200": { + "body": { + "description": "Project for Jessica Voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica", + "kind": "ProfessionalVoice", + "locale": "en-US" + }, + "headers": {} + } + }, + "operationId": "Projects_Get", + "title": "Get a project" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_projects.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_projects.json new file mode 100644 index 000000000000..d3f9138157dc --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_projects.json @@ -0,0 +1,33 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01" + }, + "responses": { + "200": { + "body": { + "nextLink": "https://eastus.api.cognitive.microsoft.com/customvoice/projects?skip=2&maxpagesize=2&api-version=2026-01-01", + "value": [ + { + "description": "Project for Jessica Voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica", + "kind": "ProfessionalVoice", + "locale": "en-US" + }, + { + "description": "Project for personal voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "PersonalVoice", + "kind": "PersonalVoice", + "locale": "en-US" + } + ] + }, + "headers": {} + } + }, + "operationId": "Projects_List", + "title": "Get all projects" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_recipes.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_recipes.json new file mode 100644 index 000000000000..d96e582c721c --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_recipes.json @@ -0,0 +1,84 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "description": "Model updated with a robust vocoder for higher audio quality. 15-50 compute hours estimated for each training.", + "datasetLocales": [ + "en-US", + "en-GB", + "fr-FR", + "it-IT" + ], + "kind": "Default", + "minDurationInSeconds": 3600, + "minUtteranceCount": 300, + "version": "V7.2023.03" + }, + { + "description": "Basic version to adapt the voice to speak with multiple emotional styles, or create your own speaking style with custom style training data. 25-50 computer hours estimated for each training. Style degree tuning supported.", + "datasetLocales": [ + "en-US", + "ja-JP" + ], + "kind": "MultiStyle", + "maxCustomStyleNum": 5, + "minDurationInSeconds": 3600, + "minStyleUtteranceCount": 100, + "minUtteranceCount": 300, + "presetStyles": { + "en-US": { + "female": [ + "angry", + "excited", + "cheerful", + "friendly", + "hopeful", + "sad", + "shouting", + "terrified", + "unfriendly", + "whispering" + ], + "male": [ + "angry", + "excited", + "cheerful", + "friendly", + "hopeful", + "sad", + "shouting", + "terrified", + "unfriendly", + "whispering" + ] + }, + "ja-JP": { + "female": [ + "angry", + "cheerful", + "sad" + ], + "male": [ + "angry", + "cheerful", + "sad" + ] + } + }, + "version": "V3.2023.06" + } + ] + }, + "headers": {} + } + }, + "operationId": "Models_ListRecipes", + "title": "Get all recipes" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_trainingset.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_trainingset.json new file mode 100644 index 000000000000..8fa9156f31d6 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_trainingset.json @@ -0,0 +1,30 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica-300" + }, + "responses": { + "200": { + "body": { + "description": "300 sentences Jessica data in general style.", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-300", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "properties": { + "durationInSeconds": 3600.5, + "isContextual": false, + "utteranceCount": 300 + }, + "status": "Succeeded", + "voiceKind": "Female" + }, + "headers": {} + } + }, + "operationId": "TrainingSets_Get", + "title": "Get a training set" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_trainingsets.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_trainingsets.json new file mode 100644 index 000000000000..26df2af65fb2 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/get_trainingsets.json @@ -0,0 +1,49 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01" + }, + "responses": { + "200": { + "body": { + "nextLink": "https://eastus.api.cognitive.microsoft.com/customvoice/trainingsets?skip=2&maxpagesize=2&api-version=2026-01-01", + "value": [ + { + "description": "300 sentences Jessica data in general style.", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-300", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "properties": { + "durationInSeconds": 3600.5, + "isContextual": false, + "utteranceCount": 300 + }, + "status": "Succeeded", + "voiceKind": "Female" + }, + { + "description": "300 sentences Jessica data in general style.", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-300", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "properties": { + "durationInSeconds": 3600.5, + "isContextual": false, + "utteranceCount": 300 + }, + "status": "Succeeded", + "voiceKind": "Female" + } + ] + }, + "headers": {} + } + }, + "operationId": "TrainingSets_List", + "title": "Get all training sets" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/resume_endpoint.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/resume_endpoint.json new file mode 100644 index 000000000000..5907297f8298 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/resume_endpoint.json @@ -0,0 +1,31 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "15cc4e23-3cc7-4811-adcc-75e5804765cc", + "api-version": "2026-01-01", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb" + }, + "responses": { + "202": { + "body": { + "description": "Endpoint for Jessica voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "modelId": "Jessica", + "projectId": "Jessica", + "properties": { + "kind": "HighPerformance" + }, + "status": "Running" + }, + "headers": { + "Operation-Id": "15cc4e23-3cc7-4811-adcc-75e5804765cc", + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/15cc4e23-3cc7-4811-adcc-75e5804765cc?api-version=2026-01-01" + } + } + }, + "operationId": "Endpoints_Resume", + "title": "Resume an endpoint" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/suspend_endpoint.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/suspend_endpoint.json new file mode 100644 index 000000000000..c7b307e99c02 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/suspend_endpoint.json @@ -0,0 +1,31 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "23f53763-5f21-442a-a944-18f72cdcaa4f", + "api-version": "2026-01-01", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb" + }, + "responses": { + "202": { + "body": { + "description": "Endpoint for Jessica voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "modelId": "Jessica", + "projectId": "Jessica", + "properties": { + "kind": "HighPerformance" + }, + "status": "Disabling" + }, + "headers": { + "Operation-Id": "23f53763-5f21-442a-a944-18f72cdcaa4f", + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/23f53763-5f21-442a-a944-18f72cdcaa4f?api-version=2026-01-01" + } + } + }, + "operationId": "Endpoints_Suspend", + "title": "Suspend an endpoint" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/upload_trainingset.json b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/upload_trainingset.json new file mode 100644 index 000000000000..8d96d2195979 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/examples/2026-01-01/upload_trainingset.json @@ -0,0 +1,38 @@ +{ + "parameters": { + "Content-Type": "application/json", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "284b7e37-f42d-4054-8fa9-08523c3de345", + "api-version": "2026-01-01", + "dataset": { + "audios": { + "containerUrl": "https://contoso.blob.core.windows.net/voicecontainer?mySasToken", + "prefix": "jessica300/", + "extensions": [ + ".wav" + ] + }, + "kind": "AudioAndScript", + "processAs": "Segmented", + "scripts": { + "containerUrl": "https://contoso.blob.core.windows.net/voicecontainer?mySasToken", + "prefix": "jessica300/", + "extensions": [ + ".txt" + ] + } + }, + "id": "d6916a55-2cbc-4ed4-bd19-739e9a13b0ab" + }, + "responses": { + "202": { + "headers": { + "Operation-Id": "284b7e37-f42d-4054-8fa9-08523c3de345", + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/284b7e37-f42d-4054-8fa9-08523c3de345?api-version=2026-01-01" + } + } + }, + "operationId": "TrainingSets_UploadData", + "title": "Upload data to a training set" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/main.tsp b/specification/cognitiveservices/data-plane/TextToSpeech/main.tsp new file mode 100644 index 000000000000..9c6a9653777a --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/main.tsp @@ -0,0 +1,42 @@ +import "@typespec/rest"; +import "@typespec/http"; +import "@typespec/versioning"; +import "@azure-tools/typespec-azure-core"; +import "./routes.tsp"; + +using TypeSpec.Rest; +using TypeSpec.Http; +using TypeSpec.Versioning; + +/** + * Custom voice API to create and deploy your voice. + */ +@useAuth( + ApiKeyAuth | OAuth2Auth<[ + { + type: OAuth2FlowType.implicit, + authorizationUrl: "https://login.microsoftonline.com/common/oauth2/authorize", + scopes: ["https://cognitiveservices.azure.com/.default"], + } + ]> +) +@service(#{ title: "Custom voice API" }) +@versioned(Versions) +@server( + "{endpoint}/customvoice", + "Custom voice API to create and deploy your voice.", + { + /** + * Supported Cognitive Services endpoints (protocol and hostname, for example: + * https://eastus.api.cognitive.microsoft.com). + */ + endpoint: url, + } +) +namespace CustomVoiceApi; + +/** The available API versions. */ +enum Versions { + /** The 2026-01-01 API version. */ + v2026_01_01: "2026-01-01", +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/models.tsp b/specification/cognitiveservices/data-plane/TextToSpeech/models.tsp new file mode 100644 index 000000000000..5b48d7435cea --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/models.tsp @@ -0,0 +1,751 @@ +import "@typespec/rest"; +import "@typespec/http"; +import "@azure-tools/typespec-azure-core"; + +using TypeSpec.Rest; +using TypeSpec.Http; +using Azure.Core; + +namespace CustomVoiceApi; + +/** Resource ID */ +@pattern("^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$") +@minLength(3) +@maxLength(64) +scalar resourceId extends string; + +/** Project kind */ +union ProjectKind { + string, + + /** ProfessionalVoice */ + ProfessionalVoice: "ProfessionalVoice", + + /** PersonalVoice */ + PersonalVoice: "PersonalVoice", +} + +/** Consent creation failure reason */ +union ConsentFailureReason { + string, + + /** + * The consent audio mismatch with verbal statement. Please check + * [verbal-statement](https://github.com/Azure-Samples/Cognitive-Speech-TTS/blob/master/CustomVoice/script/verbal-statement-all-locales.txt). + */ + AudioAndScriptNotMatch: "AudioAndScriptNotMatch", + + /** Custom Voice Service error. */ + Internal: "Internal", +} + +/** Status of a resource. */ +union Status { + string, + + /** NotStarted */ + NotStarted: "NotStarted", + + /** Running */ + Running: "Running", + + /** Succeeded */ + Succeeded: "Succeeded", + + /** Failed */ + Failed: "Failed", + + /** Disabling */ + Disabling: "Disabling", + + /** Disabled */ + Disabled: "Disabled", +} + +/** Voice kind */ +union VoiceKind { + string, + + /** Male */ + Male: "Male", + + /** Female */ + Female: "Female", +} + +/** Dataset kind */ +union DatasetKind { + string, + + /** AudioAndScript */ + AudioAndScript: "AudioAndScript", + + /** LongAudio */ + LongAudio: "LongAudio", + + /** AudioOnly */ + AudioOnly: "AudioOnly", +} + +/** + * Dataset processing method. If not specified, the default processing method will + * be used. + */ +union DatasetProcessAs { + string, + + /** The default processing mode that works with all supported languages. */ + Segmented: "Segmented", + + /** + * An enhanced mode that retains the audio as a whole to keep the contextual + * information for more natural intonations. + */ + Contextual: "Contextual", +} + +/** Model training failure reason */ +union ModelFailureReason { + string, + + /** + * The customer uses Bring Your Own Storage in Speech Account. But the storage is + * not accessible now. Please check + * [doc](https://learn.microsoft.com/azure/ai-services/speech-service/bring-your-own-storage-speech-resource?tabs=portal#configure-byos-associated-storage-account). + */ + InaccessibleCustomerStorage: "InaccessibleCustomerStorage", + + /** The consent and training audio are not from the same speaker. */ + SpeakerVerificationFailed: "SpeakerVerificationFailed", + + /** The customer canceled model training. */ + TerminateByUser: "TerminateByUser", + + /** Custom Voice Service error. */ + Internal: "Internal", + + /** Training data is not ready for model training. */ + DataNotReady: "DataNotReady", + + /** Training data is not enough for model training. */ + DataNotEnough: "DataNotEnough", +} + +/** Endpoint kind */ +union EndpointKind { + string, + + /** HighPerformance */ + HighPerformance: "HighPerformance", + + /** FastResume */ + FastResume: "FastResume", +} + +/** Model capability */ +union ModelCapability { + string, + + /** PersonalVoice */ + PersonalVoice: "PersonalVoice", +} + +/** Status of an operation. */ +union OperationStatus { + string, + + /** NotStarted */ + NotStarted: "NotStarted", + + /** Running */ + Running: "Running", + + /** Succeeded */ + Succeeded: "Succeeded", + + /** Failed */ + Failed: "Failed", +} + +/** + * Project object. Consents, training sets, models, and endpoints are organized in + * a project. + */ +@resource("projects") +model Project { + /** Resource id */ + @key + @visibility(Lifecycle.Read) + id: resourceId; + + /** Project name */ + @minLength(1) + displayName?: string; + + /** Project description */ + description?: string; + + /** Project kind */ + kind: ProjectKind; + + /** + * The locale of this project. Locale code follows BCP-47. You can find the text + * to speech locale list here + * https://learn.microsoft.com/azure/ai-services/speech-service/language-support?tabs=tts. + */ + locale?: string; + + /** + * The timestamp when the object was created. The timestamp is encoded as ISO 8601 + * date and time format ("YYYY-MM-DDThh:mm:ssZ", see + * https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations). + */ + @visibility(Lifecycle.Read) + createdDateTime?: utcDateTime; +} + +/** Consent object */ +@resource("consents") +model Consent { + /** Resource id */ + @key + @visibility(Lifecycle.Read) + id: resourceId; + + /** Name of consent. */ + @minLength(1) + displayName?: string; + + /** Description of consent. */ + description?: string; + + /** Voice talent name. Must match the voice talent name in the consent audio file. */ + @minLength(1) + voiceTalentName: string; + + /** Company name. Must match the company name in the consent audio file. */ + @minLength(1) + companyName: string; + + /** + * The public accessible URL of the consent audio file. It's recommended to be an + * Azure blob URL with + * [SAS](https://learn.microsoft.com/azure/storage/common/storage-sas-overview). + * This property is only available in request. + */ + audioUrl?: url; + + /** + * The locale of this consent. Locale code follows BCP-47. You can find the text + * to speech locale list here + * https://learn.microsoft.com/azure/ai-services/speech-service/language-support?tabs=tts. + */ + locale: string; + + /** Resource id */ + projectId: resourceId; + + /** Consent properties */ + properties?: ConsentProperties; + + /** Status of a resource. */ + status?: Status; + + /** + * The timestamp when the object was created. The timestamp is encoded as ISO 8601 + * date and time format ("YYYY-MM-DDThh:mm:ssZ", see + * https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations) + */ + @visibility(Lifecycle.Read) + createdDateTime?: utcDateTime; + + /** + * The timestamp when the current status was entered. The timestamp is encoded as + * ISO 8601 date and time format ("YYYY-MM-DDThh:mm:ssZ", see + * https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations). + */ + @visibility(Lifecycle.Read) + lastActionDateTime?: utcDateTime; +} + +/** Consent properties */ +model ConsentProperties { + /** Consent creation failure reason */ + failureReason?: ConsentFailureReason; +} + +/** Training set */ +@resource("trainingsets") +model TrainingSet { + /** Resource id */ + @key + @visibility(Lifecycle.Read) + id: resourceId; + + /** Training set name */ + @minLength(1) + displayName?: string; + + /** Training set description */ + description?: string; + + /** + * The locale of the training dataset. Locale code follows BCP-47. You can find + * the text to speech locale list here + * https://learn.microsoft.com/azure/ai-services/speech-service/language-support?tabs=tts. + */ + locale: string; + + /** Voice kind */ + voiceKind?: VoiceKind; + + /** Training set properties */ + properties?: TrainingSetProperties; + + /** Resource id */ + projectId: resourceId; + + /** Status of a resource. */ + status?: Status; + + /** + * The timestamp when the object was created. The timestamp is encoded as ISO 8601 + * date and time format ("YYYY-MM-DDThh:mm:ssZ", see + * https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations). + */ + @visibility(Lifecycle.Read) + createdDateTime?: utcDateTime; + + /** + * The timestamp when the current status was entered. The timestamp is encoded as + * ISO 8601 date and time format ("YYYY-MM-DDThh:mm:ssZ", see + * https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations). + */ + @visibility(Lifecycle.Read) + lastActionDateTime?: utcDateTime; +} + +/** Training set properties */ +model TrainingSetProperties { + /** Utterance count in this training set */ + utteranceCount?: int32; + + /** Total duration of audio in seconds in this training set */ + durationInSeconds?: float64; + + /** Indicates whether the training set is composed of contextual data */ + isContextual?: boolean; +} + +/** Dataset object */ +model Dataset { + /** The name of this dataset. */ + displayName?: string; + + /** Optional description of this dataset. */ + description?: string; + + /** Dataset kind */ + kind: DatasetKind; + + /** + * Dataset processing method. If not specified, the default processing method will + * be used. + */ + processAs?: DatasetProcessAs; + + /** + * Azure Blob Storage content. With the examples below, it represents files + * https://contoso.blob.core.windows.net/voicecontainer/jessica/*.wav + */ + audios: AzureBlobContentSource; + + /** + * Azure Blob Storage content. With the examples below, it represents files + * https://contoso.blob.core.windows.net/voicecontainer/jessica/*.wav + */ + scripts?: AzureBlobContentSource; +} + +/** + * Azure Blob Storage content. With the examples below, it represents files + * https://contoso.blob.core.windows.net/voicecontainer/jessica/*.wav + */ +model AzureBlobContentSource { + /** + * Azure Blob Storage container URL with + * [SAS](https://learn.microsoft.com/azure/storage/common/storage-sas-overview). + * Need both read and list permissions. + */ + containerUrl: url; + + /** Blob name prefix. */ + prefix?: string; + + /** File name extensions. */ + extensions: string[]; +} + +/** Recipe for model building. Different recipes have different capability. */ +model Recipe { + /** Recipe kind */ + kind?: string; + + /** Recipe version */ + version?: string; + + /** Recipe description */ + @visibility(Lifecycle.Read) + description?: string; + + /** Minimum utterance count required to train a voice model with this recipe. */ + @visibility(Lifecycle.Read) + minUtteranceCount?: int32; + + /** Minimum utterance count required to train each customized style. */ + @visibility(Lifecycle.Read) + minStyleUtteranceCount?: int32; + + /** Maximum customized style number supported in one voice model. */ + @visibility(Lifecycle.Read) + maxCustomStyleNum?: int32; + + /** + * The locale of the training dataset. Locale code follows BCP-47. You can find + * the text to speech locale list here + * https://learn.microsoft.com/azure/ai-services/speech-service/language-support?tabs=tts. + */ + @visibility(Lifecycle.Read) + datasetLocales?: string[]; + + /** + * The locale that a voice model can speak with this recipe. Locale code follows + * BCP-47. You can find the text to speech locale list here + * https://learn.microsoft.com/azure/ai-services/speech-service/language-support?tabs=tts. + */ + @visibility(Lifecycle.Read) + modelLocales?: string[]; + + /** + * Preset styles supported by this recipe per locale. You can get these styles + * without any style training set. + */ + @visibility(Lifecycle.Read) + presetStyles?: Record; + + /** + * Minimum audio duration in seconds required to train a voice model with this + * recipe. + */ + @visibility(Lifecycle.Read) + minDurationInSeconds?: float64; +} + +/** + * Preset styles supported by the recipe. The voice model can support these styles + * without any style training set. + */ +model PresetStyleItem { + /** Preset styles supported on male voice model. */ + male?: string[]; + + /** Preset styles supported on female voice model. */ + female?: string[]; +} + +/** Model object */ +@resource("models") +model Model { + /** Resource id */ + @key + @visibility(Lifecycle.Read) + id: resourceId; + + /** Voice name */ + @minLength(1) + voiceName?: string; + + /** Model description */ + description?: string; + + /** Recipe for model building. Different recipes have different capability. */ + recipe: Recipe; + + /** + * The locale of this model. Locale code follows BCP-47. You can find the text to + * speech locale list here + * https://learn.microsoft.com/azure/ai-services/speech-service/language-support?tabs=tts. + */ + locale?: string; + + /** Resource id */ + projectId: resourceId; + + /** Resource id */ + consentId: resourceId; + + /** Resource id */ + trainingSetId: resourceId; + + /** Engine version. Update this version can get the latest pronunciation bug fixing. */ + @visibility(Lifecycle.Read) + engineVersion?: string; + + /** Model properties */ + properties?: ModelProperties; + + /** Status of a resource. */ + status?: Status; + + /** + * The timestamp when the object was created. The timestamp is encoded as ISO 8601 + * date and time format ("YYYY-MM-DDThh:mm:ssZ", see + * https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations). + */ + @visibility(Lifecycle.Read) + createdDateTime?: utcDateTime; + + /** + * The timestamp when the current status was entered. The timestamp is encoded as + * ISO 8601 date and time format ("YYYY-MM-DDThh:mm:ssZ", see + * https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations). + */ + @visibility(Lifecycle.Read) + lastActionDateTime?: utcDateTime; +} + +/** Model properties */ +model ModelProperties { + /** Preset styles of this model. */ + presetStyles?: string[]; + + /** Customized styles and associated training sets. */ + styleTrainingSetIds?: Record; + + /** All styles supported by this model. */ + @visibility(Lifecycle.Read) + voiceStyles?: string[]; + + /** Model training failure reason */ + failureReason?: ModelFailureReason; + + /** IDs of failed training sets. */ + @visibility(Lifecycle.Read) + failedTrainingsets?: string[]; + + /** Secondary locales that this model can speak. Locale code follows BCP-47. */ + @visibility(Lifecycle.Read) + secondaryLocales?: string[]; +} + +/** Endpoint object */ +@resource("endpoints") +model Endpoint { + /** Endpoint Id */ + @key + @visibility(Lifecycle.Read) + id: uuid; + + /** Endpoint name */ + @minLength(1) + displayName?: string; + + /** Endpoint description */ + description?: string; + + /** Resource id */ + projectId: resourceId; + + /** Resource id */ + modelId: resourceId; + + /** Endpoint properties */ + properties?: EndpointProperties; + + /** Status of a resource. */ + status?: Status; + + /** + * The timestamp when the object was created. The timestamp is encoded as ISO 8601 + * date and time format ("YYYY-MM-DDThh:mm:ssZ", see + * https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations). + */ + @visibility(Lifecycle.Read) + createdDateTime?: utcDateTime; + + /** + * The timestamp when the current status was entered. The timestamp is encoded as + * ISO 8601 date and time format ("YYYY-MM-DDThh:mm:ssZ", see + * https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations). + */ + @visibility(Lifecycle.Read) + lastActionDateTime?: utcDateTime; +} + +/** Endpoint properties */ +model EndpointProperties { + /** Endpoint kind */ + kind?: EndpointKind; +} + +/** Base model */ +@resource("basemodels") +model BaseModel { + /** Base model name */ + @key + @visibility(Lifecycle.Read) + @minLength(1) + name: string; + + /** Base model description */ + description?: string; + + /** + * The stamp when the base model was released. The timestamp is encoded as ISO + * 8601 date and time format ("YYYY-MM-DDThh:mm:ssZ", see + * https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations). + */ + releaseDateTime: utcDateTime; + + /** + * The timestamp when TTS service will stop serving this base model. The timestamp + * is encoded as ISO 8601 date and time format ("YYYY-MM-DDThh:mm:ssZ", see + * https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations). + */ + expirationDateTime?: utcDateTime; + + /** Capabilities of base model */ + capabilities?: ModelCapability[]; +} + +/** Personal voice object */ +@resource("personalvoices") +model PersonalVoice { + /** Resource id */ + @key + @visibility(Lifecycle.Read) + id: resourceId; + + /** Personal voice speaker profile id. Fill this property in SSML. */ + @visibility(Lifecycle.Read) + speakerProfileId?: uuid; + + /** Display name of personal voice */ + @minLength(1) + displayName?: string; + + /** Personal voice description */ + description?: string; + + /** Resource id */ + projectId: resourceId; + + /** Resource id */ + consentId: resourceId; + + /** + * Azure Blob Storage content. With the examples below, it represents files + * https://contoso.blob.core.windows.net/voicecontainer/jessica/*.wav + */ + audios?: AzureBlobContentSource; + + /** Personal voice properties */ + properties?: PersonalVoiceProperties; + + /** Status of a resource. */ + status?: Status; + + /** + * The timestamp when the object was created. The timestamp is encoded as ISO 8601 + * date and time format ("YYYY-MM-DDThh:mm:ssZ", see + * https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations). + */ + @visibility(Lifecycle.Read) + createdDateTime?: utcDateTime; + + /** + * The timestamp when the current status was entered. The timestamp is encoded as + * ISO 8601 date and time format ("YYYY-MM-DDThh:mm:ssZ", see + * https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations). + */ + @visibility(Lifecycle.Read) + lastActionDateTime?: utcDateTime; +} + +/** Personal voice properties */ +model PersonalVoiceProperties { + /** Model training failure reason */ + failureReason?: ModelFailureReason; +} + +/** Status of a long running operation. */ +@resource("operations") +model Operation { + /** Unique operation ID. */ + @key + @visibility(Lifecycle.Read) + id: resourceId; + + /** Status of an operation. */ + status?: OperationStatus; +} + +/** Operation-Id header for long-running operations. */ +model OperationIdHeader { + /** + * ID of the status monitor for the operation. If the Operation-Id header matches + * an existing operation and the request is not identical to the prior request, it + * will fail with a 400 Bad Request. + */ + @header("Operation-Id") + operationId?: resourceId; +} + +/** Form data for creating a consent with an audio file. */ +model ConsentPostFormData { + /** The project ID. */ + projectId: HttpPart; + + /** The display name of this consent. */ + displayName?: HttpPart; + + /** Optional description of this consent. */ + description?: HttpPart; + + /** The name of voice talent. */ + voiceTalentName: HttpPart; + + /** The name of company. */ + companyName: HttpPart; + + /** An audio file containing the audio data. */ + audioData: HttpPart; + + /** + * The locale of this consent. Locale code follows BCP-47. You can find the text + * to speech locale list here + * https://learn.microsoft.com/azure/ai-services/speech-service/language-support?tabs=tts. + */ + locale: HttpPart; + + /** properties. */ + properties?: HttpPart; +} + +/** Form data for creating a personal voice with audio files. */ +model PersonalVoicePostFormData { + /** The display name of this model. */ + displayName?: HttpPart; + + /** Optional description of this model. */ + description?: HttpPart; + + /** The project ID. */ + projectId: HttpPart; + + /** Audio files. Maximum file size is 30MB. */ + audioData?: HttpPart; + + /** properties. */ + properties?: HttpPart; +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/readme.md b/specification/cognitiveservices/data-plane/TextToSpeech/readme.md new file mode 100644 index 000000000000..4e8159c2e26a --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/readme.md @@ -0,0 +1,28 @@ +# Cognitive Services Speech TextToSpeech SDKs + +> see https://aka.ms/autorest + +Configuration for generating TextToSpeech SDK. + +The current release for the TextToSpeech is `release_2026_01_01`. + +> **Note:** Starting from version 2026-01-01, this specification is generated from TypeSpec. The source of truth is in the TypeSpec files (`main.tsp`, `models.tsp`, `routes.tsp`) in this directory. Do not edit `stable/2026-01-01/texttospeech.json` directly — regenerate it by running `tsp compile .` from this directory. +> +> For older API versions (2024-02-01-preview and earlier), see the [legacy readme](../Speech/TextToSpeech/readme.md). + +``` yaml +tag: release_2026_01_01 +add-credentials: true +openapi-type: data-plane +``` + +# Releases + +## TextToSpeech 2026-01-01 + +These settings apply only when `--tag=release_2026_01_01` is specified on the command line. + +```yaml $(tag) == 'release_2026_01_01' +input-file: + - stable/2026-01-01/texttospeech.json +``` diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/routes.tsp b/specification/cognitiveservices/data-plane/TextToSpeech/routes.tsp new file mode 100644 index 000000000000..f26afe0463ac --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/routes.tsp @@ -0,0 +1,433 @@ +import "@azure-tools/typespec-azure-core"; +import "@typespec/rest"; +import "./models.tsp"; + +using TypeSpec.Rest; +using TypeSpec.Http; +using Azure.Core; +using Azure.Core.Traits; + +namespace CustomVoiceApi; + +/** Service traits - suppress standard Azure headers not used by this API. */ +alias CustomVoiceServiceTraits = NoRepeatableRequests & + NoConditionalRequests & + NoClientRequestId; + +/** Standard resource operations with Custom Voice service traits. */ +alias ops = Azure.Core.ResourceOperations; + +/** + * Common list query parameters for resource list operations. + */ +model CustomListQueryParameters { + /** + * The number of result items to skip. + */ + @query("skip") + @offset + skip?: int32 = 0; + + /** + * The maximum number of items to include in a single response. + */ + @query("maxpagesize") + @pageSize + maxpagesize?: int32 = 100; + + /** + * Filter condition. + * - **Supported properties:** projectId, createdDateTime, locale, kind + * - **Operators:** + * - eq, ne are supported for all properties. + * - gt, ge, lt, le are supported for createdDateTime. + * - **Example:** + * - ```filter=projectId eq 'Jessica'``` (filter by project ID) + * - ```filter=kind eq 'ProfessionalVoice'``` (filter project by kind) + * - ```filter=locale eq 'en-US'``` (filter training set and model by locale) + * - ```filter=createdDateTime gt 2022-12-30T23:59:59.99Z``` (filter resource + * created time after 2023-11-01) + */ + @query("filter") + filter?: string; +} + +interface Projects { + /** + * Gets a list of projects for the authenticated Speech service resource. + */ + list is ops.ResourceList< + Project, + ListQueryParametersTrait + >; + + /** + * Gets the project identified by the given ID. + */ + get is ops.ResourceRead; + + /** + * Deletes the project identified by the given ID. All data (like consent, + * training set) in this project will be deleted automatically. + */ + @route("/projects/{id}") + @delete + delete is Azure.Core.Foundations.Operation< + { + /** The ID of the resource. */ + @path + id: resourceId; + + /** + * Set this to true if you want to delete a project with model and endpoint. + * Otherwise, the delete operation will fail. + */ + @query("forceDelete") + forceDelete?: boolean; + }, + void + >; + + /** + * Creates a new project. + */ + @route("/projects/{id}") + @put + create is Azure.Core.Foundations.Operation< + { + /** The ID of the resource. */ + @path + id: resourceId; + + /** + * project definition + */ + @bodyRoot + project: Project; + }, + { + @statusCode statusCode: 201; + @bodyRoot body: Project; + } + >; +} + +interface Consents { + /** + * Gets a list of consents for the authenticated Speech service resource. + */ + list is ops.ResourceList< + Consent, + ListQueryParametersTrait + >; + + /** + * Gets the consent identified by the given ID. + */ + get is ops.ResourceRead; + + /** + * Deletes the consent identified by the given ID. + */ + delete is ops.ResourceDelete; + + /** + * Creates a new voice talent consent with the provided audio URL. + */ + @pollingOperation(Operations.get) + create is ops.LongRunningResourceCreateOrReplace< + Consent, + RequestHeadersTrait + >; + + /** + * Creates a new voice talent consent with the provided audio file. + */ + @route("/consents/{id}") + @post + @pollingOperation(Operations.get) + @useFinalStateVia("operation-location") + createFromAudioFile is Azure.Core.Foundations.Operation< + { + ...OperationIdHeader; + + /** The ID of the resource. */ + @path + id: resourceId; + + /** Content type for multipart form data. */ + @header contentType: "multipart/form-data"; + + /** Consent form data */ + @multipartBody + body: ConsentPostFormData; + }, + { + @statusCode statusCode: 201; + @header("Operation-Location") operationLocation: string; + @header("Operation-Id") operationIdHeader: string; + @bodyRoot body: Consent; + } + >; +} + +interface TrainingSets { + /** + * Gets a list of training sets for the authenticated Speech service resource. + */ + list is ops.ResourceList< + TrainingSet, + ListQueryParametersTrait + >; + + /** + * Gets the training set identified by the given ID. + */ + get is ops.ResourceRead; + + /** + * Deletes the training set identified by the given ID. + */ + delete is ops.ResourceDelete; + + /** + * Creates a new training set. + */ + @route("/trainingsets/{id}") + @put + create is Azure.Core.Foundations.Operation< + { + /** The ID of the resource. */ + @path + id: resourceId; + + /** + * training set definition + */ + @bodyRoot + trainingset: TrainingSet; + }, + { + @statusCode statusCode: 201; + @bodyRoot body: TrainingSet; + } + >; + + /** + * Uploads data to the specified training set. + */ + @route("/trainingsets/{id}:upload") + @post + @pollingOperation(Operations.get) + @useFinalStateVia("operation-location") + uploadData is Azure.Core.Foundations.Operation< + { + ...OperationIdHeader; + + /** The ID of the resource. */ + @path + id: resourceId; + + /** + * Dataset to be uploaded + */ + @bodyRoot + dataset: Dataset; + }, + { + @statusCode statusCode: 202; + @header("Operation-Location") operationLocation: string; + @header("Operation-Id") operationIdHeader: string; + } + >; +} + +interface Models { + /** + * Get a list of supported recipes for model building. Different recipes have + * different capabilities such as support for multi-style voice models. + */ + @route("/modelrecipes") + @get + @list + listRecipes is Azure.Core.Foundations.Operation<{}, Azure.Core.Page>; + + /** + * Gets the list of models for the authenticated Speech service resource. + */ + list is ops.ResourceList< + Model, + ListQueryParametersTrait + >; + + /** + * Gets the model identified by the given ID. + */ + get is ops.ResourceRead; + + /** + * Deletes the model identified by the given ID. + */ + delete is ops.ResourceDelete; + + /** + * Creates a new voice model. + */ + @pollingOperation(Operations.get) + create is ops.LongRunningResourceCreateOrReplace< + Model, + RequestHeadersTrait + >; +} + +interface Endpoints { + /** + * Gets a list of endpoints for the authenticated Speech service resource. + */ + list is ops.ResourceList< + Endpoint, + ListQueryParametersTrait + >; + + /** + * Gets the endpoint identified by the given ID. + */ + get is ops.ResourceRead; + + /** + * Deletes the endpoint identified by the given ID. + */ + delete is ops.ResourceDelete; + + /** + * Creates a new endpoint. + */ + @pollingOperation(Operations.get) + create is ops.LongRunningResourceCreateOrReplace< + Endpoint, + RequestHeadersTrait + >; + + /** + * Resumes the endpoint identified by the given ID. + */ + @route("/endpoints/{id}:resume") + @post + @pollingOperation(Operations.get) + @useFinalStateVia("operation-location") + resume is Azure.Core.Foundations.Operation< + { + ...OperationIdHeader; + + /** + * The resource ID, which should be UUID. + */ + @path + id: uuid; + }, + { + @statusCode statusCode: 202; + @header("Operation-Location") operationLocation: string; + @header("Operation-Id") operationIdHeader: string; + @bodyRoot body: Endpoint; + } + >; + + /** + * Suspends the endpoint identified by the given ID. + */ + @route("/endpoints/{id}:suspend") + @post + @pollingOperation(Operations.get) + @useFinalStateVia("operation-location") + suspend is Azure.Core.Foundations.Operation< + { + ...OperationIdHeader; + + /** + * The resource ID, which should be UUID. + */ + @path + id: uuid; + }, + { + @statusCode statusCode: 202; + @header("Operation-Location") operationLocation: string; + @header("Operation-Id") operationIdHeader: string; + @bodyRoot body: Endpoint; + } + >; +} + +interface BaseModels { + /** + * Gets a list of base models. + */ + list is ops.ResourceList; +} + +interface PersonalVoices { + /** + * Gets a list of personal voices for the authenticated Speech service resource. + */ + list is ops.ResourceList< + PersonalVoice, + ListQueryParametersTrait + >; + + /** + * Gets the personal voice identified by the given ID. + */ + get is ops.ResourceRead; + + /** + * Deletes the personal voice identified by the given ID. + */ + delete is ops.ResourceDelete; + + /** + * Creates a new personal voice with audio files in Azure Blob Storage. + */ + @pollingOperation(Operations.get) + create is ops.LongRunningResourceCreateOrReplace< + PersonalVoice, + RequestHeadersTrait + >; + + /** + * Creates a new personal voice with audio files in the client. + */ + @route("/personalvoices/{id}") + @post + @pollingOperation(Operations.get) + @useFinalStateVia("operation-location") + createFromAudioFile is Azure.Core.Foundations.Operation< + { + ...OperationIdHeader; + + /** The ID of the resource. */ + @path + id: resourceId; + + /** Content type for multipart form data. */ + @header contentType: "multipart/form-data"; + + /** Personal voice form data */ + @multipartBody + body: PersonalVoicePostFormData; + }, + { + @statusCode statusCode: 201; + @header("Operation-Location") operationLocation: string; + @header("Operation-Id") operationIdHeader: string; + @bodyRoot body: PersonalVoice; + } + >; +} + +interface Operations { + /** + * Gets operation info. + */ + get is ops.ResourceRead; +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_consent.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_consent.json new file mode 100644 index 000000000000..b5c6ccdac292 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_consent.json @@ -0,0 +1,54 @@ +{ + "parameters": { + "Content-Type": "application/json", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "070f7986-ef17-41d0-ba2b-907f0f28e314", + "api-version": "2026-01-01", + "resource": { + "description": "Consent for Jessica voice", + "audioUrl": "https://contoso.blob.core.windows.net/public/jessica-consent.wav?mySasToken", + "companyName": "Contoso", + "locale": "en-US", + "projectId": "Jessica", + "voiceTalentName": "Jessica Smith" + }, + "id": "Jessica" + }, + "responses": { + "200": { + "body": { + "description": "Consent for Jessica voice", + "companyName": "Contoso", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "status": "NotStarted", + "voiceTalentName": "Jessica Smith" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/070f7986-ef17-41d0-ba2b-907f0f28e314?api-version=2026-01-01" + } + }, + "201": { + "body": { + "description": "Consent for Jessica voice", + "companyName": "Contoso", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "status": "NotStarted", + "voiceTalentName": "Jessica Smith" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/070f7986-ef17-41d0-ba2b-907f0f28e314?api-version=2026-01-01" + } + } + }, + "operationId": "Consents_Create", + "title": "Create a consent" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_consent_with_multipart_form.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_consent_with_multipart_form.json new file mode 100644 index 000000000000..0ee65c43c5b9 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_consent_with_multipart_form.json @@ -0,0 +1,37 @@ +{ + "parameters": { + "description": "Consent for Jessica voice", + "Content-Type": "multipart/form-data", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "070f7986-ef17-41d0-ba2b-907f0f28e314", + "api-version": "2026-01-01", + "audioData": "{audio file}", + "companyName": "Contoso", + "id": "Jessica", + "locale": "en-US", + "projectId": "Jessica", + "voiceTalentName": "Jessica Smith" + }, + "responses": { + "201": { + "body": { + "description": "Consent for Jessica voice", + "companyName": "Contoso", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "status": "NotStarted", + "voiceTalentName": "Jessica Smith" + }, + "headers": { + "Operation-Id": "070f7986-ef17-41d0-ba2b-907f0f28e314", + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/070f7986-ef17-41d0-ba2b-907f0f28e314?api-version=2026-01-01" + } + } + }, + "operationId": "Consents_CreateFromAudioFile", + "title": "Create a consent" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_endpoint.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_endpoint.json new file mode 100644 index 000000000000..5bbe068d760b --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_endpoint.json @@ -0,0 +1,53 @@ +{ + "parameters": { + "Content-Type": "application/json", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "2595b58d-40d6-4032-a618-482dcce1c130", + "api-version": "2026-01-01", + "resource": { + "description": "Endpoint for Jessica voice", + "modelId": "Jessica", + "projectId": "Jessica" + }, + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb" + }, + "responses": { + "200": { + "body": { + "description": "Endpoint for Jessica voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "modelId": "Jessica", + "projectId": "Jessica", + "properties": { + "kind": "HighPerformance" + }, + "status": "NotStarted" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/2595b58d-40d6-4032-a618-482dcce1c130?api-version=2026-01-01" + } + }, + "201": { + "body": { + "description": "Endpoint for Jessica voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "modelId": "Jessica", + "projectId": "Jessica", + "properties": { + "kind": "HighPerformance" + }, + "status": "NotStarted" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/2595b58d-40d6-4032-a618-482dcce1c130?api-version=2026-01-01" + } + } + }, + "operationId": "Endpoints_Create", + "title": "Create an endpoint" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_model.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_model.json new file mode 100644 index 000000000000..e98466e04b52 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_model.json @@ -0,0 +1,68 @@ +{ + "parameters": { + "Content-Type": "application/json", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "1f4352df-f247-40c0-a7b1-a54d017933e1", + "api-version": "2026-01-01", + "id": "Jessica", + "resource": { + "description": "Jessica voice", + "consentId": "Jessica", + "projectId": "Jessica", + "recipe": { + "kind": "Default" + }, + "trainingSetId": "Jessica-300", + "voiceName": "JessicaNeural" + } + }, + "responses": { + "200": { + "body": { + "description": "Jessica voice", + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "engineVersion": "2023.07.04.0", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "recipe": { + "kind": "Default", + "version": "V7.2023.03" + }, + "status": "NotStarted", + "trainingSetId": "Jessica-300", + "voiceName": "JessicaNeural" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/1f4352df-f247-40c0-a7b1-a54d017933e1?api-version=2026-01-01" + } + }, + "201": { + "body": { + "description": "Jessica voice", + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "engineVersion": "2023.07.04.0", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "recipe": { + "kind": "Default", + "version": "V7.2023.03" + }, + "status": "NotStarted", + "trainingSetId": "Jessica-300", + "voiceName": "JessicaNeural" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/1f4352df-f247-40c0-a7b1-a54d017933e1?api-version=2026-01-01" + } + } + }, + "operationId": "Models_Create", + "title": "Create a model" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_multi_style_model.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_multi_style_model.json new file mode 100644 index 000000000000..990bba1c5db5 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_multi_style_model.json @@ -0,0 +1,111 @@ +{ + "parameters": { + "Content-Type": "application/json", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "a01a127a-c204-4e46-a8c1-fab01559b05b", + "api-version": "2026-01-01", + "id": "JessicaMultiStyle", + "resource": { + "description": "Jessica multi style voice", + "consentId": "Jessica", + "locale": "en-US", + "projectId": "Jessica", + "properties": { + "presetStyles": [ + "cheerful", + "sad" + ], + "styleTrainingSetIds": { + "happy": "JessicaHappy-300", + "myStyle2": "JessicaStyle2" + } + }, + "recipe": { + "kind": "MultiStyle" + }, + "trainingSetId": "Jessica-300", + "voiceName": "JessicaMultiStyleNeural" + } + }, + "responses": { + "200": { + "body": { + "description": "Jessica multi style voice", + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "engineVersion": "2023.07.04.0", + "id": "JessicaMultiStyle", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "properties": { + "presetStyles": [ + "cheerful", + "sad" + ], + "styleTrainingSetIds": { + "happy": "JessicaHappy-300", + "myStyle2": "JessicaStyle2" + }, + "voiceStyles": [ + "cheerful", + "sad", + "happy", + "myStyle2" + ] + }, + "recipe": { + "kind": "MultiStyle", + "version": "V3.2023.06" + }, + "status": "NotStarted", + "trainingSetId": "Jessica-300", + "voiceName": "JessicaMultiStyleNeural" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/a01a127a-c204-4e46-a8c1-fab01559b05b?api-version=2026-01-01" + } + }, + "201": { + "body": { + "description": "Jessica multi style voice", + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "engineVersion": "2023.07.04.0", + "id": "JessicaMultiStyle", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "properties": { + "presetStyles": [ + "cheerful", + "sad" + ], + "styleTrainingSetIds": { + "happy": "JessicaHappy-300", + "myStyle2": "JessicaStyle2" + }, + "voiceStyles": [ + "cheerful", + "sad", + "happy", + "myStyle2" + ] + }, + "recipe": { + "kind": "MultiStyle", + "version": "V3.2023.06" + }, + "status": "NotStarted", + "trainingSetId": "Jessica-300", + "voiceName": "JessicaMultiStyleNeural" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/a01a127a-c204-4e46-a8c1-fab01559b05b?api-version=2026-01-01" + } + } + }, + "operationId": "Models_Create", + "title": "Create a multi style model" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_personalvoice.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_personalvoice.json new file mode 100644 index 000000000000..a9829dad24b6 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_personalvoice.json @@ -0,0 +1,53 @@ +{ + "parameters": { + "Content-Type": "application/json", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "1321a2c0-9be4-471d-83bb-bc3be4f96a6f", + "api-version": "2026-01-01", + "id": "Jessica-PersonalVoice", + "resource": { + "audios": { + "containerUrl": "https://contoso.blob.core.windows.net/voicecontainer?mySasToken", + "prefix": "jessica/", + "extensions": [ + ".wav" + ] + }, + "consentId": "Jessica", + "projectId": "PersonalVoice" + } + }, + "responses": { + "200": { + "body": { + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-PersonalVoice", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "projectId": "PersonalVoice", + "speakerProfileId": "3059912f-a3dc-49e3-bdd0-02e449df1fe3", + "status": "NotStarted" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/1321a2c0-9be4-471d-83bb-bc3be4f96a6f?api-version=2026-01-01" + } + }, + "201": { + "body": { + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-PersonalVoice", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "projectId": "PersonalVoice", + "speakerProfileId": "3059912f-a3dc-49e3-bdd0-02e449df1fe3", + "status": "NotStarted" + }, + "headers": { + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/1321a2c0-9be4-471d-83bb-bc3be4f96a6f?api-version=2026-01-01" + } + } + }, + "operationId": "PersonalVoices_Create", + "title": "Create a personal voice" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_personalvoice_with_multipart_form.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_personalvoice_with_multipart_form.json new file mode 100644 index 000000000000..b29756624f51 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_personalvoice_with_multipart_form.json @@ -0,0 +1,32 @@ +{ + "parameters": { + "Content-Type": "multipart/form-data", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "1321a2c0-9be4-471d-83bb-bc3be4f96a6f", + "api-version": "2026-01-01", + "audioData": "{audio files}", + "consentId": "Jessica", + "id": "Jessica-PersonalVoice", + "projectId": "PersonalVoice" + }, + "responses": { + "201": { + "body": { + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-PersonalVoice", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "projectId": "PersonalVoice", + "speakerProfileId": "3059912f-a3dc-49e3-bdd0-02e449df1fe3", + "status": "NotStarted" + }, + "headers": { + "Operation-Id": "1321a2c0-9be4-471d-83bb-bc3be4f96a6f", + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/1321a2c0-9be4-471d-83bb-bc3be4f96a6f?api-version=2026-01-01" + } + } + }, + "operationId": "PersonalVoices_CreateFromAudioFile", + "title": "Create a personal voice" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_project.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_project.json new file mode 100644 index 000000000000..4b622e05c216 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_project.json @@ -0,0 +1,28 @@ +{ + "parameters": { + "Content-Type": "application/json", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica", + "project": { + "description": "Project for Jessica Voice", + "kind": "ProfessionalVoice", + "locale": "en-US" + } + }, + "responses": { + "201": { + "body": { + "description": "Project for Jessica Voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica", + "kind": "ProfessionalVoice", + "locale": "en-US" + }, + "headers": {} + } + }, + "operationId": "Projects_Create", + "title": "Create a project" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_trainingset.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_trainingset.json new file mode 100644 index 000000000000..6ce3d239bcf4 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/create_trainingset.json @@ -0,0 +1,32 @@ +{ + "parameters": { + "Content-Type": "application/json", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica-300", + "trainingset": { + "description": "300 sentences Jessica data in general style.", + "locale": "en-US", + "projectId": "Jessica", + "voiceKind": "Female" + } + }, + "responses": { + "201": { + "body": { + "description": "300 sentences Jessica data in general style.", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-300", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "status": "Succeeded", + "voiceKind": "Female" + }, + "headers": {} + } + }, + "operationId": "TrainingSets_Create", + "title": "Create a training set" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_consent.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_consent.json new file mode 100644 index 000000000000..69cbf22e047a --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_consent.json @@ -0,0 +1,15 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica" + }, + "responses": { + "204": { + "headers": {} + } + }, + "operationId": "Consents_Delete", + "title": "Delete a consent" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_endpoint.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_endpoint.json new file mode 100644 index 000000000000..3a59f658f580 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_endpoint.json @@ -0,0 +1,15 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb" + }, + "responses": { + "204": { + "headers": {} + } + }, + "operationId": "Endpoints_Delete", + "title": "Delete an endpoint" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_model.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_model.json new file mode 100644 index 000000000000..ae826b82facc --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_model.json @@ -0,0 +1,15 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica" + }, + "responses": { + "204": { + "headers": {} + } + }, + "operationId": "Models_Delete", + "title": "Delete a model" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_personalvoice.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_personalvoice.json new file mode 100644 index 000000000000..dbb311f86b8c --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_personalvoice.json @@ -0,0 +1,15 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica-PersonalVoice" + }, + "responses": { + "204": { + "headers": {} + } + }, + "operationId": "PersonalVoices_Delete", + "title": "Delete a personal voice" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_project.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_project.json new file mode 100644 index 000000000000..9cf3eee1456b --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_project.json @@ -0,0 +1,15 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica" + }, + "responses": { + "204": { + "headers": {} + } + }, + "operationId": "Projects_Delete", + "title": "Delete a project" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_trainingset.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_trainingset.json new file mode 100644 index 000000000000..f39eda3e8a94 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/delete_trainingset.json @@ -0,0 +1,15 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica-300" + }, + "responses": { + "204": { + "headers": {} + } + }, + "operationId": "TrainingSets_Delete", + "title": "Delete a training set" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_base_models.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_base_models.json new file mode 100644 index 000000000000..ea8bc54ec7fd --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_base_models.json @@ -0,0 +1,26 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "name": "PhoenixV2Neural", + "description": "Phonenix V2 base model", + "capabilities": [ + "PersonalVoice" + ], + "releaseDateTime": "2023-12-01T00:00:00.000Z" + } + ] + }, + "headers": {} + } + }, + "operationId": "BaseModels_List", + "title": "Get base models" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_consent.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_consent.json new file mode 100644 index 000000000000..d4f77bea11fb --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_consent.json @@ -0,0 +1,26 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica" + }, + "responses": { + "200": { + "body": { + "description": "Consent for Jessica voice", + "companyName": "Contoso", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "status": "Succeeded", + "voiceTalentName": "Jessica Smith" + }, + "headers": {} + } + }, + "operationId": "Consents_Get", + "title": "Get a consent" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_consents.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_consents.json new file mode 100644 index 000000000000..f052122c9c1a --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_consents.json @@ -0,0 +1,41 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01" + }, + "responses": { + "200": { + "body": { + "nextLink": "https://eastus.api.cognitive.microsoft.com/customvoice/consents?skip=2&maxpagesize=2&api-version=2026-01-01", + "value": [ + { + "description": "Consent for Jessica voice", + "companyName": "Contoso", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "status": "Succeeded", + "voiceTalentName": "Jessica Smith" + }, + { + "description": "Consent for Jessica voice", + "companyName": "Contoso", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "status": "Succeeded", + "voiceTalentName": "Jessica Smith" + } + ] + }, + "headers": {} + } + }, + "operationId": "Consents_List", + "title": "Get all consents" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_endpoint.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_endpoint.json new file mode 100644 index 000000000000..bc30b895aa8b --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_endpoint.json @@ -0,0 +1,27 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb" + }, + "responses": { + "200": { + "body": { + "description": "Endpoint for Jessica voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "modelId": "Jessica", + "projectId": "Jessica", + "properties": { + "kind": "HighPerformance" + }, + "status": "NotStarted" + }, + "headers": {} + } + }, + "operationId": "Endpoints_Get", + "title": "Get an endpoint" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_endpoints.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_endpoints.json new file mode 100644 index 000000000000..ecedfeb4121f --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_endpoints.json @@ -0,0 +1,43 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01" + }, + "responses": { + "200": { + "body": { + "nextLink": "https://eastus.api.cognitive.microsoft.com/customvoice/endpoints?skip=2&maxpagesize=2&api-version=2026-01-01", + "value": [ + { + "description": "Endpoint for Jessica voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "modelId": "Jessica", + "projectId": "Jessica", + "properties": { + "kind": "HighPerformance" + }, + "status": "NotStarted" + }, + { + "description": "Endpoint for Jessica voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "modelId": "Jessica", + "projectId": "Jessica", + "properties": { + "kind": "HighPerformance" + }, + "status": "NotStarted" + } + ] + }, + "headers": {} + } + }, + "operationId": "Endpoints_List", + "title": "Get all endpoints" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_model.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_model.json new file mode 100644 index 000000000000..c4df3786f669 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_model.json @@ -0,0 +1,32 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica" + }, + "responses": { + "200": { + "body": { + "description": "Jessica voice", + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "engineVersion": "2023.07.04.0", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "recipe": { + "kind": "Default", + "version": "V7.2023.03" + }, + "status": "NotStarted", + "trainingSetId": "Jessica-300", + "voiceName": "JessicaNeural" + }, + "headers": {} + } + }, + "operationId": "Models_Get", + "title": "Get a model" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_models.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_models.json new file mode 100644 index 000000000000..f3bfaceb8177 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_models.json @@ -0,0 +1,75 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01" + }, + "responses": { + "200": { + "body": { + "nextLink": "https://eastus.api.cognitive.microsoft.com/customvoice/models?skip=2&maxpagesize=2&api-version=2026-01-01", + "value": [ + { + "description": "Jessica voice", + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "engineVersion": "2023.07.04.0", + "id": "Jessica", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "properties": { + "secondaryLocales": [ + "en-GB", + "en-AU" + ] + }, + "recipe": { + "kind": "Default", + "version": "V7.2023.03" + }, + "status": "NotStarted", + "trainingSetId": "Jessica-300", + "voiceName": "JessicaNeural" + }, + { + "description": "Jessica multi style voice", + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "engineVersion": "2023.07.04.0", + "id": "JessicaMultiStyle", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "properties": { + "presetStyles": [ + "cheerful", + "sad" + ], + "styleTrainingSetIds": { + "happy": "JessicaHappy-300", + "myStyle2": "JessicaStyle2" + }, + "voiceStyles": [ + "cheerful", + "sad", + "happy", + "myStyle2" + ] + }, + "recipe": { + "kind": "MultiStyle", + "version": "V3.2023.06" + }, + "status": "NotStarted", + "trainingSetId": "Jessica-300", + "voiceName": "JessicaMultiStyleNeural" + } + ] + }, + "headers": {} + } + }, + "operationId": "Models_List", + "title": "Get all models" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_operation.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_operation.json new file mode 100644 index 000000000000..6fc6f61ecc79 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_operation.json @@ -0,0 +1,19 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "06c7f835-e07b-4ac8-b33c-5b6df4a4eeef" + }, + "responses": { + "200": { + "body": { + "id": "06c7f835-e07b-4ac8-b33c-5b6df4a4eeef", + "status": "Running" + }, + "headers": {} + } + }, + "operationId": "Operations_Get", + "title": "Get Operation" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_personalvoice.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_personalvoice.json new file mode 100644 index 000000000000..754c9e58cfc7 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_personalvoice.json @@ -0,0 +1,24 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica-PersonalVoice" + }, + "responses": { + "200": { + "body": { + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-PersonalVoice", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "projectId": "PersonalVoice", + "speakerProfileId": "3059912f-a3dc-49e3-bdd0-02e449df1fe3", + "status": "NotStarted" + }, + "headers": {} + } + }, + "operationId": "PersonalVoices_Get", + "title": "Get a personal voice" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_personalvoices.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_personalvoices.json new file mode 100644 index 000000000000..408c3df9bde9 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_personalvoices.json @@ -0,0 +1,37 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01" + }, + "responses": { + "200": { + "body": { + "nextLink": "https://eastus.api.cognitive.microsoft.com/customvoice/personalvoices?skip=2&maxpagesize=2&api-version=2026-01-01", + "value": [ + { + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-PersonalVoice", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "projectId": "PersonalVoice", + "speakerProfileId": "3059912f-a3dc-49e3-bdd0-02e449df1fe3", + "status": "NotStarted" + }, + { + "consentId": "Jessica", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-PersonalVoice", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "projectId": "PersonalVoice", + "speakerProfileId": "3059912f-a3dc-49e3-bdd0-02e449df1fe3", + "status": "NotStarted" + } + ] + }, + "headers": {} + } + }, + "operationId": "PersonalVoices_List", + "title": "Get all personal voices" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_project.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_project.json new file mode 100644 index 000000000000..4dd019c3bd28 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_project.json @@ -0,0 +1,22 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica" + }, + "responses": { + "200": { + "body": { + "description": "Project for Jessica Voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica", + "kind": "ProfessionalVoice", + "locale": "en-US" + }, + "headers": {} + } + }, + "operationId": "Projects_Get", + "title": "Get a project" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_projects.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_projects.json new file mode 100644 index 000000000000..d3f9138157dc --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_projects.json @@ -0,0 +1,33 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01" + }, + "responses": { + "200": { + "body": { + "nextLink": "https://eastus.api.cognitive.microsoft.com/customvoice/projects?skip=2&maxpagesize=2&api-version=2026-01-01", + "value": [ + { + "description": "Project for Jessica Voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica", + "kind": "ProfessionalVoice", + "locale": "en-US" + }, + { + "description": "Project for personal voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "PersonalVoice", + "kind": "PersonalVoice", + "locale": "en-US" + } + ] + }, + "headers": {} + } + }, + "operationId": "Projects_List", + "title": "Get all projects" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_recipes.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_recipes.json new file mode 100644 index 000000000000..d96e582c721c --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_recipes.json @@ -0,0 +1,84 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "description": "Model updated with a robust vocoder for higher audio quality. 15-50 compute hours estimated for each training.", + "datasetLocales": [ + "en-US", + "en-GB", + "fr-FR", + "it-IT" + ], + "kind": "Default", + "minDurationInSeconds": 3600, + "minUtteranceCount": 300, + "version": "V7.2023.03" + }, + { + "description": "Basic version to adapt the voice to speak with multiple emotional styles, or create your own speaking style with custom style training data. 25-50 computer hours estimated for each training. Style degree tuning supported.", + "datasetLocales": [ + "en-US", + "ja-JP" + ], + "kind": "MultiStyle", + "maxCustomStyleNum": 5, + "minDurationInSeconds": 3600, + "minStyleUtteranceCount": 100, + "minUtteranceCount": 300, + "presetStyles": { + "en-US": { + "female": [ + "angry", + "excited", + "cheerful", + "friendly", + "hopeful", + "sad", + "shouting", + "terrified", + "unfriendly", + "whispering" + ], + "male": [ + "angry", + "excited", + "cheerful", + "friendly", + "hopeful", + "sad", + "shouting", + "terrified", + "unfriendly", + "whispering" + ] + }, + "ja-JP": { + "female": [ + "angry", + "cheerful", + "sad" + ], + "male": [ + "angry", + "cheerful", + "sad" + ] + } + }, + "version": "V3.2023.06" + } + ] + }, + "headers": {} + } + }, + "operationId": "Models_ListRecipes", + "title": "Get all recipes" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_trainingset.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_trainingset.json new file mode 100644 index 000000000000..8fa9156f31d6 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_trainingset.json @@ -0,0 +1,30 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01", + "id": "Jessica-300" + }, + "responses": { + "200": { + "body": { + "description": "300 sentences Jessica data in general style.", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-300", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "properties": { + "durationInSeconds": 3600.5, + "isContextual": false, + "utteranceCount": 300 + }, + "status": "Succeeded", + "voiceKind": "Female" + }, + "headers": {} + } + }, + "operationId": "TrainingSets_Get", + "title": "Get a training set" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_trainingsets.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_trainingsets.json new file mode 100644 index 000000000000..26df2af65fb2 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/get_trainingsets.json @@ -0,0 +1,49 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "api-version": "2026-01-01" + }, + "responses": { + "200": { + "body": { + "nextLink": "https://eastus.api.cognitive.microsoft.com/customvoice/trainingsets?skip=2&maxpagesize=2&api-version=2026-01-01", + "value": [ + { + "description": "300 sentences Jessica data in general style.", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-300", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "properties": { + "durationInSeconds": 3600.5, + "isContextual": false, + "utteranceCount": 300 + }, + "status": "Succeeded", + "voiceKind": "Female" + }, + { + "description": "300 sentences Jessica data in general style.", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "Jessica-300", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "locale": "en-US", + "projectId": "Jessica", + "properties": { + "durationInSeconds": 3600.5, + "isContextual": false, + "utteranceCount": 300 + }, + "status": "Succeeded", + "voiceKind": "Female" + } + ] + }, + "headers": {} + } + }, + "operationId": "TrainingSets_List", + "title": "Get all training sets" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/resume_endpoint.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/resume_endpoint.json new file mode 100644 index 000000000000..5907297f8298 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/resume_endpoint.json @@ -0,0 +1,31 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "15cc4e23-3cc7-4811-adcc-75e5804765cc", + "api-version": "2026-01-01", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb" + }, + "responses": { + "202": { + "body": { + "description": "Endpoint for Jessica voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "modelId": "Jessica", + "projectId": "Jessica", + "properties": { + "kind": "HighPerformance" + }, + "status": "Running" + }, + "headers": { + "Operation-Id": "15cc4e23-3cc7-4811-adcc-75e5804765cc", + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/15cc4e23-3cc7-4811-adcc-75e5804765cc?api-version=2026-01-01" + } + } + }, + "operationId": "Endpoints_Resume", + "title": "Resume an endpoint" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/suspend_endpoint.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/suspend_endpoint.json new file mode 100644 index 000000000000..c7b307e99c02 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/suspend_endpoint.json @@ -0,0 +1,31 @@ +{ + "parameters": { + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "23f53763-5f21-442a-a944-18f72cdcaa4f", + "api-version": "2026-01-01", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb" + }, + "responses": { + "202": { + "body": { + "description": "Endpoint for Jessica voice", + "createdDateTime": "2023-04-01T05:30:00.000Z", + "id": "9f50c644-2121-40e9-9ea7-544e48bfe3cb", + "lastActionDateTime": "2023-04-02T10:15:30.000Z", + "modelId": "Jessica", + "projectId": "Jessica", + "properties": { + "kind": "HighPerformance" + }, + "status": "Disabling" + }, + "headers": { + "Operation-Id": "23f53763-5f21-442a-a944-18f72cdcaa4f", + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/23f53763-5f21-442a-a944-18f72cdcaa4f?api-version=2026-01-01" + } + } + }, + "operationId": "Endpoints_Suspend", + "title": "Suspend an endpoint" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/upload_trainingset.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/upload_trainingset.json new file mode 100644 index 000000000000..8d96d2195979 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/examples/upload_trainingset.json @@ -0,0 +1,38 @@ +{ + "parameters": { + "Content-Type": "application/json", + "Endpoint": "https://eastus.api.cognitive.microsoft.com/", + "Ocp-Apim-Subscription-Key": "{API Key}", + "Operation-Id": "284b7e37-f42d-4054-8fa9-08523c3de345", + "api-version": "2026-01-01", + "dataset": { + "audios": { + "containerUrl": "https://contoso.blob.core.windows.net/voicecontainer?mySasToken", + "prefix": "jessica300/", + "extensions": [ + ".wav" + ] + }, + "kind": "AudioAndScript", + "processAs": "Segmented", + "scripts": { + "containerUrl": "https://contoso.blob.core.windows.net/voicecontainer?mySasToken", + "prefix": "jessica300/", + "extensions": [ + ".txt" + ] + } + }, + "id": "d6916a55-2cbc-4ed4-bd19-739e9a13b0ab" + }, + "responses": { + "202": { + "headers": { + "Operation-Id": "284b7e37-f42d-4054-8fa9-08523c3de345", + "Operation-Location": "https://eastus.api.cognitive.microsoft.com/customvoice/operations/284b7e37-f42d-4054-8fa9-08523c3de345?api-version=2026-01-01" + } + } + }, + "operationId": "TrainingSets_UploadData", + "title": "Upload data to a training set" +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/texttospeech.json b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/texttospeech.json new file mode 100644 index 000000000000..7d378a05a272 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/stable/2026-01-01/texttospeech.json @@ -0,0 +1,3075 @@ +{ + "swagger": "2.0", + "info": { + "title": "Custom voice API", + "version": "2026-01-01", + "description": "Custom voice API to create and deploy your voice.", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "x-ms-parameterized-host": { + "hostTemplate": "{endpoint}/customvoice", + "useSchemePrefix": false, + "parameters": [ + { + "name": "endpoint", + "in": "path", + "description": "Supported Cognitive Services endpoints (protocol and hostname, for example:\nhttps://eastus.api.cognitive.microsoft.com).", + "required": true, + "type": "string", + "format": "uri", + "x-ms-skip-url-encoding": true + } + ] + }, + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "security": [ + { + "ApiKeyAuth": [] + }, + { + "OAuth2Auth": [ + "https://cognitiveservices.azure.com/.default" + ] + } + ], + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "Ocp-Apim-Subscription-Key", + "in": "header" + }, + "OAuth2Auth": { + "type": "oauth2", + "flow": "implicit", + "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize", + "scopes": { + "https://cognitiveservices.azure.com/.default": "" + } + } + }, + "tags": [], + "paths": { + "/basemodels": { + "get": { + "operationId": "BaseModels_List", + "description": "Gets a list of base models.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/PagedBaseModel" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Get base models": { + "$ref": "./examples/get_base_models.json" + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/consents": { + "get": { + "operationId": "Consents_List", + "description": "Gets a list of consents for the authenticated Speech service resource.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "$ref": "#/parameters/CustomListQueryParameters.skip" + }, + { + "$ref": "#/parameters/CustomListQueryParameters.maxpagesize" + }, + { + "$ref": "#/parameters/CustomListQueryParameters.filter" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/PagedConsent" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Get all consents": { + "$ref": "./examples/get_consents.json" + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/consents/{id}": { + "get": { + "operationId": "Consents_Get", + "description": "Gets the consent identified by the given ID.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "Resource id", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/Consent" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Get a consent": { + "$ref": "./examples/get_consent.json" + } + } + }, + "put": { + "operationId": "Consents_Create", + "description": "Creates a new voice talent consent with the provided audio URL.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "Resource id", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + }, + { + "$ref": "#/parameters/OperationIdHeader" + }, + { + "name": "resource", + "in": "body", + "description": "The resource instance.", + "required": true, + "schema": { + "$ref": "#/definitions/Consent" + } + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/Consent" + }, + "headers": { + "Operation-Location": { + "type": "string", + "format": "uri", + "description": "The location for monitoring the operation state." + } + } + }, + "201": { + "description": "The request has succeeded and a new resource has been created as a result.", + "schema": { + "$ref": "#/definitions/Consent" + }, + "headers": { + "Operation-Location": { + "type": "string", + "format": "uri", + "description": "The location for monitoring the operation state." + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Create a consent": { + "$ref": "./examples/create_consent.json" + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "original-uri" + }, + "x-ms-long-running-operation": true + }, + "post": { + "operationId": "Consents_CreateFromAudioFile", + "description": "Creates a new voice talent consent with the provided audio file.", + "consumes": [ + "multipart/form-data" + ], + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "$ref": "#/parameters/OperationIdHeader" + }, + { + "name": "id", + "in": "path", + "description": "The ID of the resource.", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + }, + { + "name": "projectId", + "in": "formData", + "description": "The project ID.", + "required": true, + "type": "string" + }, + { + "name": "displayName", + "in": "formData", + "description": "The display name of this consent.", + "required": false, + "type": "string" + }, + { + "name": "description", + "in": "formData", + "description": "Optional description of this consent.", + "required": false, + "type": "string" + }, + { + "name": "voiceTalentName", + "in": "formData", + "description": "The name of voice talent.", + "required": true, + "type": "string" + }, + { + "name": "companyName", + "in": "formData", + "description": "The name of company.", + "required": true, + "type": "string" + }, + { + "name": "audioData", + "in": "formData", + "description": "An audio file containing the audio data.", + "required": true, + "type": "file" + }, + { + "name": "locale", + "in": "formData", + "description": "The locale of this consent. Locale code follows BCP-47. You can find the text\nto speech locale list here\nhttps://learn.microsoft.com/azure/ai-services/speech-service/language-support?tabs=tts.", + "required": true, + "type": "string" + }, + { + "name": "properties", + "in": "formData", + "description": "properties.", + "required": false, + "type": "string" + } + ], + "responses": { + "201": { + "description": "The request has succeeded and a new resource has been created as a result.", + "schema": { + "$ref": "#/definitions/Consent" + }, + "headers": { + "Operation-Id": { + "type": "string" + }, + "Operation-Location": { + "type": "string" + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Create a consent": { + "$ref": "./examples/create_consent_with_multipart_form.json" + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "operation-location" + }, + "x-ms-long-running-operation": true + }, + "delete": { + "operationId": "Consents_Delete", + "description": "Deletes the consent identified by the given ID.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "Resource id", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + } + ], + "responses": { + "204": { + "description": "There is no content to send for this request, but the headers may be useful. " + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Delete a consent": { + "$ref": "./examples/delete_consent.json" + } + } + } + }, + "/endpoints": { + "get": { + "operationId": "Endpoints_List", + "description": "Gets a list of endpoints for the authenticated Speech service resource.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "$ref": "#/parameters/CustomListQueryParameters.skip" + }, + { + "$ref": "#/parameters/CustomListQueryParameters.maxpagesize" + }, + { + "$ref": "#/parameters/CustomListQueryParameters.filter" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/PagedEndpoint" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Get all endpoints": { + "$ref": "./examples/get_endpoints.json" + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/endpoints/{id}": { + "get": { + "operationId": "Endpoints_Get", + "description": "Gets the endpoint identified by the given ID.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "Endpoint Id", + "required": true, + "type": "string", + "format": "uuid" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/Endpoint" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Get an endpoint": { + "$ref": "./examples/get_endpoint.json" + } + } + }, + "put": { + "operationId": "Endpoints_Create", + "description": "Creates a new endpoint.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "Endpoint Id", + "required": true, + "type": "string", + "format": "uuid" + }, + { + "$ref": "#/parameters/OperationIdHeader" + }, + { + "name": "resource", + "in": "body", + "description": "The resource instance.", + "required": true, + "schema": { + "$ref": "#/definitions/Endpoint" + } + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/Endpoint" + }, + "headers": { + "Operation-Location": { + "type": "string", + "format": "uri", + "description": "The location for monitoring the operation state." + } + } + }, + "201": { + "description": "The request has succeeded and a new resource has been created as a result.", + "schema": { + "$ref": "#/definitions/Endpoint" + }, + "headers": { + "Operation-Location": { + "type": "string", + "format": "uri", + "description": "The location for monitoring the operation state." + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Create an endpoint": { + "$ref": "./examples/create_endpoint.json" + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "original-uri" + }, + "x-ms-long-running-operation": true + }, + "delete": { + "operationId": "Endpoints_Delete", + "description": "Deletes the endpoint identified by the given ID.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "Endpoint Id", + "required": true, + "type": "string", + "format": "uuid" + } + ], + "responses": { + "204": { + "description": "There is no content to send for this request, but the headers may be useful. " + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Delete an endpoint": { + "$ref": "./examples/delete_endpoint.json" + } + } + } + }, + "/endpoints/{id}:resume": { + "post": { + "operationId": "Endpoints_Resume", + "description": "Resumes the endpoint identified by the given ID.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "$ref": "#/parameters/OperationIdHeader" + }, + { + "name": "id", + "in": "path", + "description": "The resource ID, which should be UUID.", + "required": true, + "type": "string", + "format": "uuid" + } + ], + "responses": { + "202": { + "description": "The request has been accepted for processing, but processing has not yet completed.", + "schema": { + "$ref": "#/definitions/Endpoint" + }, + "headers": { + "Operation-Id": { + "type": "string" + }, + "Operation-Location": { + "type": "string" + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Resume an endpoint": { + "$ref": "./examples/resume_endpoint.json" + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "operation-location" + }, + "x-ms-long-running-operation": true + } + }, + "/endpoints/{id}:suspend": { + "post": { + "operationId": "Endpoints_Suspend", + "description": "Suspends the endpoint identified by the given ID.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "$ref": "#/parameters/OperationIdHeader" + }, + { + "name": "id", + "in": "path", + "description": "The resource ID, which should be UUID.", + "required": true, + "type": "string", + "format": "uuid" + } + ], + "responses": { + "202": { + "description": "The request has been accepted for processing, but processing has not yet completed.", + "schema": { + "$ref": "#/definitions/Endpoint" + }, + "headers": { + "Operation-Id": { + "type": "string" + }, + "Operation-Location": { + "type": "string" + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Suspend an endpoint": { + "$ref": "./examples/suspend_endpoint.json" + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "operation-location" + }, + "x-ms-long-running-operation": true + } + }, + "/modelrecipes": { + "get": { + "operationId": "Models_ListRecipes", + "description": "Get a list of supported recipes for model building. Different recipes have\ndifferent capabilities such as support for multi-style voice models.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/PagedRecipe" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Get all recipes": { + "$ref": "./examples/get_recipes.json" + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/models": { + "get": { + "operationId": "Models_List", + "description": "Gets the list of models for the authenticated Speech service resource.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "$ref": "#/parameters/CustomListQueryParameters.skip" + }, + { + "$ref": "#/parameters/CustomListQueryParameters.maxpagesize" + }, + { + "$ref": "#/parameters/CustomListQueryParameters.filter" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/PagedModel" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Get all models": { + "$ref": "./examples/get_models.json" + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/models/{id}": { + "get": { + "operationId": "Models_Get", + "description": "Gets the model identified by the given ID.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "Resource id", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/Model" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Get a model": { + "$ref": "./examples/get_model.json" + } + } + }, + "put": { + "operationId": "Models_Create", + "description": "Creates a new voice model.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "Resource id", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + }, + { + "$ref": "#/parameters/OperationIdHeader" + }, + { + "name": "resource", + "in": "body", + "description": "The resource instance.", + "required": true, + "schema": { + "$ref": "#/definitions/Model" + } + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/Model" + }, + "headers": { + "Operation-Location": { + "type": "string", + "format": "uri", + "description": "The location for monitoring the operation state." + } + } + }, + "201": { + "description": "The request has succeeded and a new resource has been created as a result.", + "schema": { + "$ref": "#/definitions/Model" + }, + "headers": { + "Operation-Location": { + "type": "string", + "format": "uri", + "description": "The location for monitoring the operation state." + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Create a model": { + "$ref": "./examples/create_model.json" + }, + "Create a multi style model": { + "$ref": "./examples/create_multi_style_model.json" + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "original-uri" + }, + "x-ms-long-running-operation": true + }, + "delete": { + "operationId": "Models_Delete", + "description": "Deletes the model identified by the given ID.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "Resource id", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + } + ], + "responses": { + "204": { + "description": "There is no content to send for this request, but the headers may be useful. " + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Delete a model": { + "$ref": "./examples/delete_model.json" + } + } + } + }, + "/operations/{id}": { + "get": { + "operationId": "Operations_Get", + "description": "Gets operation info.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "Unique operation ID.", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/Operation" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Get Operation": { + "$ref": "./examples/get_operation.json" + } + } + } + }, + "/personalvoices": { + "get": { + "operationId": "PersonalVoices_List", + "description": "Gets a list of personal voices for the authenticated Speech service resource.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "$ref": "#/parameters/CustomListQueryParameters.skip" + }, + { + "$ref": "#/parameters/CustomListQueryParameters.maxpagesize" + }, + { + "$ref": "#/parameters/CustomListQueryParameters.filter" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/PagedPersonalVoice" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Get all personal voices": { + "$ref": "./examples/get_personalvoices.json" + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/personalvoices/{id}": { + "get": { + "operationId": "PersonalVoices_Get", + "description": "Gets the personal voice identified by the given ID.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "Resource id", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/PersonalVoice" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Get a personal voice": { + "$ref": "./examples/get_personalvoice.json" + } + } + }, + "put": { + "operationId": "PersonalVoices_Create", + "description": "Creates a new personal voice with audio files in Azure Blob Storage.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "Resource id", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + }, + { + "$ref": "#/parameters/OperationIdHeader" + }, + { + "name": "resource", + "in": "body", + "description": "The resource instance.", + "required": true, + "schema": { + "$ref": "#/definitions/PersonalVoice" + } + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/PersonalVoice" + }, + "headers": { + "Operation-Location": { + "type": "string", + "format": "uri", + "description": "The location for monitoring the operation state." + } + } + }, + "201": { + "description": "The request has succeeded and a new resource has been created as a result.", + "schema": { + "$ref": "#/definitions/PersonalVoice" + }, + "headers": { + "Operation-Location": { + "type": "string", + "format": "uri", + "description": "The location for monitoring the operation state." + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Create a personal voice": { + "$ref": "./examples/create_personalvoice.json" + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "original-uri" + }, + "x-ms-long-running-operation": true + }, + "post": { + "operationId": "PersonalVoices_CreateFromAudioFile", + "description": "Creates a new personal voice with audio files in the client.", + "consumes": [ + "multipart/form-data" + ], + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "$ref": "#/parameters/OperationIdHeader" + }, + { + "name": "id", + "in": "path", + "description": "The ID of the resource.", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + }, + { + "name": "displayName", + "in": "formData", + "description": "The display name of this model.", + "required": false, + "type": "string" + }, + { + "name": "description", + "in": "formData", + "description": "Optional description of this model.", + "required": false, + "type": "string" + }, + { + "name": "projectId", + "in": "formData", + "description": "The project ID.", + "required": true, + "type": "string" + }, + { + "name": "audioData", + "in": "formData", + "description": "Audio files. Maximum file size is 30MB.", + "required": false, + "type": "file" + }, + { + "name": "properties", + "in": "formData", + "description": "properties.", + "required": false, + "type": "string" + } + ], + "responses": { + "201": { + "description": "The request has succeeded and a new resource has been created as a result.", + "schema": { + "$ref": "#/definitions/PersonalVoice" + }, + "headers": { + "Operation-Id": { + "type": "string" + }, + "Operation-Location": { + "type": "string" + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Create a personal voice": { + "$ref": "./examples/create_personalvoice_with_multipart_form.json" + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "operation-location" + }, + "x-ms-long-running-operation": true + }, + "delete": { + "operationId": "PersonalVoices_Delete", + "description": "Deletes the personal voice identified by the given ID.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "Resource id", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + } + ], + "responses": { + "204": { + "description": "There is no content to send for this request, but the headers may be useful. " + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Delete a personal voice": { + "$ref": "./examples/delete_personalvoice.json" + } + } + } + }, + "/projects": { + "get": { + "operationId": "Projects_List", + "description": "Gets a list of projects for the authenticated Speech service resource.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "$ref": "#/parameters/CustomListQueryParameters.skip" + }, + { + "$ref": "#/parameters/CustomListQueryParameters.maxpagesize" + }, + { + "$ref": "#/parameters/CustomListQueryParameters.filter" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/PagedProject" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Get all projects": { + "$ref": "./examples/get_projects.json" + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/projects/{id}": { + "get": { + "operationId": "Projects_Get", + "description": "Gets the project identified by the given ID.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "Resource id", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/Project" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Get a project": { + "$ref": "./examples/get_project.json" + } + } + }, + "put": { + "operationId": "Projects_Create", + "description": "Creates a new project.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "The ID of the resource.", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + }, + { + "name": "project", + "in": "body", + "description": "project definition", + "required": true, + "schema": { + "$ref": "#/definitions/Project" + } + } + ], + "responses": { + "201": { + "description": "The request has succeeded and a new resource has been created as a result.", + "schema": { + "$ref": "#/definitions/Project" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Create a project": { + "$ref": "./examples/create_project.json" + } + } + }, + "delete": { + "operationId": "Projects_Delete", + "description": "Deletes the project identified by the given ID. All data (like consent,\ntraining set) in this project will be deleted automatically.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "The ID of the resource.", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + }, + { + "name": "forceDelete", + "in": "query", + "description": "Set this to true if you want to delete a project with model and endpoint.\nOtherwise, the delete operation will fail.", + "required": false, + "type": "boolean" + } + ], + "responses": { + "204": { + "description": "There is no content to send for this request, but the headers may be useful. " + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Delete a project": { + "$ref": "./examples/delete_project.json" + } + } + } + }, + "/trainingsets": { + "get": { + "operationId": "TrainingSets_List", + "description": "Gets a list of training sets for the authenticated Speech service resource.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "$ref": "#/parameters/CustomListQueryParameters.skip" + }, + { + "$ref": "#/parameters/CustomListQueryParameters.maxpagesize" + }, + { + "$ref": "#/parameters/CustomListQueryParameters.filter" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/PagedTrainingSet" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Get all training sets": { + "$ref": "./examples/get_trainingsets.json" + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/trainingsets/{id}": { + "get": { + "operationId": "TrainingSets_Get", + "description": "Gets the training set identified by the given ID.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "Resource id", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/TrainingSet" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Get a training set": { + "$ref": "./examples/get_trainingset.json" + } + } + }, + "put": { + "operationId": "TrainingSets_Create", + "description": "Creates a new training set.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "The ID of the resource.", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + }, + { + "name": "trainingset", + "in": "body", + "description": "training set definition", + "required": true, + "schema": { + "$ref": "#/definitions/TrainingSet" + } + } + ], + "responses": { + "201": { + "description": "The request has succeeded and a new resource has been created as a result.", + "schema": { + "$ref": "#/definitions/TrainingSet" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Create a training set": { + "$ref": "./examples/create_trainingset.json" + } + } + }, + "delete": { + "operationId": "TrainingSets_Delete", + "description": "Deletes the training set identified by the given ID.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "name": "id", + "in": "path", + "description": "Resource id", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + } + ], + "responses": { + "204": { + "description": "There is no content to send for this request, but the headers may be useful. " + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Delete a training set": { + "$ref": "./examples/delete_trainingset.json" + } + } + } + }, + "/trainingsets/{id}:upload": { + "post": { + "operationId": "TrainingSets_UploadData", + "description": "Uploads data to the specified training set.", + "parameters": [ + { + "$ref": "#/parameters/Azure.Core.Foundations.ApiVersionParameter" + }, + { + "$ref": "#/parameters/OperationIdHeader" + }, + { + "name": "id", + "in": "path", + "description": "The ID of the resource.", + "required": true, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + }, + { + "name": "dataset", + "in": "body", + "description": "Dataset to be uploaded", + "required": true, + "schema": { + "$ref": "#/definitions/Dataset" + } + } + ], + "responses": { + "202": { + "description": "The request has been accepted for processing, but processing has not yet completed.", + "headers": { + "Operation-Id": { + "type": "string" + }, + "Operation-Location": { + "type": "string" + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/Azure.Core.Foundations.ErrorResponse" + }, + "headers": { + "x-ms-error-code": { + "type": "string", + "description": "String error code indicating what went wrong." + } + } + } + }, + "x-ms-examples": { + "Upload data to a training set": { + "$ref": "./examples/upload_trainingset.json" + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "operation-location" + }, + "x-ms-long-running-operation": true + } + } + }, + "definitions": { + "Azure.Core.Foundations.Error": { + "type": "object", + "description": "The error object.", + "properties": { + "code": { + "type": "string", + "description": "One of a server-defined set of error codes." + }, + "message": { + "type": "string", + "description": "A human-readable representation of the error." + }, + "target": { + "type": "string", + "description": "The target of the error." + }, + "details": { + "type": "array", + "description": "An array of details about specific errors that led to this reported error.", + "items": { + "$ref": "#/definitions/Azure.Core.Foundations.Error" + } + }, + "innererror": { + "$ref": "#/definitions/Azure.Core.Foundations.InnerError", + "description": "An object containing more specific information than the current object about the error." + } + }, + "required": [ + "code", + "message" + ] + }, + "Azure.Core.Foundations.ErrorResponse": { + "type": "object", + "description": "A response containing error details.", + "properties": { + "error": { + "$ref": "#/definitions/Azure.Core.Foundations.Error", + "description": "The error object." + } + }, + "required": [ + "error" + ] + }, + "Azure.Core.Foundations.InnerError": { + "type": "object", + "description": "An object containing more specific information about the error. As per Azure REST API guidelines - https://aka.ms/AzureRestApiGuidelines#handling-errors.", + "properties": { + "code": { + "type": "string", + "description": "One of a server-defined set of error codes." + }, + "innererror": { + "$ref": "#/definitions/Azure.Core.Foundations.InnerError", + "description": "Inner error." + } + } + }, + "Azure.Core.uuid": { + "type": "string", + "format": "uuid", + "description": "Universally Unique Identifier" + }, + "AzureBlobContentSource": { + "type": "object", + "description": "Azure Blob Storage content. With the examples below, it represents files\nhttps://contoso.blob.core.windows.net/voicecontainer/jessica/*.wav", + "properties": { + "containerUrl": { + "type": "string", + "format": "uri", + "description": "Azure Blob Storage container URL with\n[SAS](https://learn.microsoft.com/azure/storage/common/storage-sas-overview).\nNeed both read and list permissions." + }, + "prefix": { + "type": "string", + "description": "Blob name prefix." + }, + "extensions": { + "type": "array", + "description": "File name extensions.", + "items": { + "type": "string" + } + } + }, + "required": [ + "containerUrl", + "extensions" + ] + }, + "BaseModel": { + "type": "object", + "description": "Base model", + "properties": { + "name": { + "type": "string", + "description": "Base model name", + "minLength": 1, + "readOnly": true + }, + "description": { + "type": "string", + "description": "Base model description" + }, + "releaseDateTime": { + "type": "string", + "format": "date-time", + "description": "The stamp when the base model was released. The timestamp is encoded as ISO\n8601 date and time format (\"YYYY-MM-DDThh:mm:ssZ\", see\nhttps://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations)." + }, + "expirationDateTime": { + "type": "string", + "format": "date-time", + "description": "The timestamp when TTS service will stop serving this base model. The timestamp\nis encoded as ISO 8601 date and time format (\"YYYY-MM-DDThh:mm:ssZ\", see\nhttps://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations)." + }, + "capabilities": { + "type": "array", + "description": "Capabilities of base model", + "items": { + "$ref": "#/definitions/ModelCapability" + } + } + }, + "required": [ + "name", + "releaseDateTime" + ] + }, + "Consent": { + "type": "object", + "description": "Consent object", + "properties": { + "id": { + "$ref": "#/definitions/resourceId", + "description": "Resource id", + "readOnly": true + }, + "displayName": { + "type": "string", + "description": "Name of consent.", + "minLength": 1 + }, + "description": { + "type": "string", + "description": "Description of consent." + }, + "voiceTalentName": { + "type": "string", + "description": "Voice talent name. Must match the voice talent name in the consent audio file.", + "minLength": 1 + }, + "companyName": { + "type": "string", + "description": "Company name. Must match the company name in the consent audio file.", + "minLength": 1 + }, + "audioUrl": { + "type": "string", + "format": "uri", + "description": "The public accessible URL of the consent audio file. It's recommended to be an\nAzure blob URL with\n[SAS](https://learn.microsoft.com/azure/storage/common/storage-sas-overview).\nThis property is only available in request." + }, + "locale": { + "type": "string", + "description": "The locale of this consent. Locale code follows BCP-47. You can find the text\nto speech locale list here\nhttps://learn.microsoft.com/azure/ai-services/speech-service/language-support?tabs=tts." + }, + "projectId": { + "$ref": "#/definitions/resourceId", + "description": "Resource id" + }, + "properties": { + "$ref": "#/definitions/ConsentProperties", + "description": "Consent properties" + }, + "status": { + "$ref": "#/definitions/Status", + "description": "Status of a resource." + }, + "createdDateTime": { + "type": "string", + "format": "date-time", + "description": "The timestamp when the object was created. The timestamp is encoded as ISO 8601\ndate and time format (\"YYYY-MM-DDThh:mm:ssZ\", see\nhttps://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations)", + "readOnly": true + }, + "lastActionDateTime": { + "type": "string", + "format": "date-time", + "description": "The timestamp when the current status was entered. The timestamp is encoded as\nISO 8601 date and time format (\"YYYY-MM-DDThh:mm:ssZ\", see\nhttps://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations).", + "readOnly": true + } + }, + "required": [ + "id", + "voiceTalentName", + "companyName", + "locale", + "projectId" + ] + }, + "ConsentFailureReason": { + "type": "string", + "description": "Consent creation failure reason", + "enum": [ + "AudioAndScriptNotMatch", + "Internal" + ], + "x-ms-enum": { + "name": "ConsentFailureReason", + "modelAsString": true, + "values": [ + { + "name": "AudioAndScriptNotMatch", + "value": "AudioAndScriptNotMatch", + "description": "The consent audio mismatch with verbal statement. Please check\n[verbal-statement](https://github.com/Azure-Samples/Cognitive-Speech-TTS/blob/master/CustomVoice/script/verbal-statement-all-locales.txt)." + }, + { + "name": "Internal", + "value": "Internal", + "description": "Custom Voice Service error." + } + ] + } + }, + "ConsentProperties": { + "type": "object", + "description": "Consent properties", + "properties": { + "failureReason": { + "$ref": "#/definitions/ConsentFailureReason", + "description": "Consent creation failure reason" + } + } + }, + "Dataset": { + "type": "object", + "description": "Dataset object", + "properties": { + "displayName": { + "type": "string", + "description": "The name of this dataset." + }, + "description": { + "type": "string", + "description": "Optional description of this dataset." + }, + "kind": { + "$ref": "#/definitions/DatasetKind", + "description": "Dataset kind" + }, + "processAs": { + "$ref": "#/definitions/DatasetProcessAs", + "description": "Dataset processing method. If not specified, the default processing method will\nbe used." + }, + "audios": { + "$ref": "#/definitions/AzureBlobContentSource", + "description": "Azure Blob Storage content. With the examples below, it represents files\nhttps://contoso.blob.core.windows.net/voicecontainer/jessica/*.wav" + }, + "scripts": { + "$ref": "#/definitions/AzureBlobContentSource", + "description": "Azure Blob Storage content. With the examples below, it represents files\nhttps://contoso.blob.core.windows.net/voicecontainer/jessica/*.wav" + } + }, + "required": [ + "kind", + "audios" + ] + }, + "DatasetKind": { + "type": "string", + "description": "Dataset kind", + "enum": [ + "AudioAndScript", + "LongAudio", + "AudioOnly" + ], + "x-ms-enum": { + "name": "DatasetKind", + "modelAsString": true, + "values": [ + { + "name": "AudioAndScript", + "value": "AudioAndScript", + "description": "AudioAndScript" + }, + { + "name": "LongAudio", + "value": "LongAudio", + "description": "LongAudio" + }, + { + "name": "AudioOnly", + "value": "AudioOnly", + "description": "AudioOnly" + } + ] + } + }, + "DatasetProcessAs": { + "type": "string", + "description": "Dataset processing method. If not specified, the default processing method will\nbe used.", + "enum": [ + "Segmented", + "Contextual" + ], + "x-ms-enum": { + "name": "DatasetProcessAs", + "modelAsString": true, + "values": [ + { + "name": "Segmented", + "value": "Segmented", + "description": "The default processing mode that works with all supported languages." + }, + { + "name": "Contextual", + "value": "Contextual", + "description": "An enhanced mode that retains the audio as a whole to keep the contextual\ninformation for more natural intonations." + } + ] + } + }, + "Endpoint": { + "type": "object", + "description": "Endpoint object", + "properties": { + "id": { + "$ref": "#/definitions/Azure.Core.uuid", + "description": "Endpoint Id", + "readOnly": true + }, + "displayName": { + "type": "string", + "description": "Endpoint name", + "minLength": 1 + }, + "description": { + "type": "string", + "description": "Endpoint description" + }, + "projectId": { + "$ref": "#/definitions/resourceId", + "description": "Resource id" + }, + "modelId": { + "$ref": "#/definitions/resourceId", + "description": "Resource id" + }, + "properties": { + "$ref": "#/definitions/EndpointProperties", + "description": "Endpoint properties" + }, + "status": { + "$ref": "#/definitions/Status", + "description": "Status of a resource." + }, + "createdDateTime": { + "type": "string", + "format": "date-time", + "description": "The timestamp when the object was created. The timestamp is encoded as ISO 8601\ndate and time format (\"YYYY-MM-DDThh:mm:ssZ\", see\nhttps://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations).", + "readOnly": true + }, + "lastActionDateTime": { + "type": "string", + "format": "date-time", + "description": "The timestamp when the current status was entered. The timestamp is encoded as\nISO 8601 date and time format (\"YYYY-MM-DDThh:mm:ssZ\", see\nhttps://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations).", + "readOnly": true + } + }, + "required": [ + "id", + "projectId", + "modelId" + ] + }, + "EndpointKind": { + "type": "string", + "description": "Endpoint kind", + "enum": [ + "HighPerformance", + "FastResume" + ], + "x-ms-enum": { + "name": "EndpointKind", + "modelAsString": true, + "values": [ + { + "name": "HighPerformance", + "value": "HighPerformance", + "description": "HighPerformance" + }, + { + "name": "FastResume", + "value": "FastResume", + "description": "FastResume" + } + ] + } + }, + "EndpointProperties": { + "type": "object", + "description": "Endpoint properties", + "properties": { + "kind": { + "$ref": "#/definitions/EndpointKind", + "description": "Endpoint kind" + } + } + }, + "Model": { + "type": "object", + "description": "Model object", + "properties": { + "id": { + "$ref": "#/definitions/resourceId", + "description": "Resource id", + "readOnly": true + }, + "voiceName": { + "type": "string", + "description": "Voice name", + "minLength": 1 + }, + "description": { + "type": "string", + "description": "Model description" + }, + "recipe": { + "$ref": "#/definitions/Recipe", + "description": "Recipe for model building. Different recipes have different capability." + }, + "locale": { + "type": "string", + "description": "The locale of this model. Locale code follows BCP-47. You can find the text to\nspeech locale list here\nhttps://learn.microsoft.com/azure/ai-services/speech-service/language-support?tabs=tts." + }, + "projectId": { + "$ref": "#/definitions/resourceId", + "description": "Resource id" + }, + "consentId": { + "$ref": "#/definitions/resourceId", + "description": "Resource id" + }, + "trainingSetId": { + "$ref": "#/definitions/resourceId", + "description": "Resource id" + }, + "engineVersion": { + "type": "string", + "description": "Engine version. Update this version can get the latest pronunciation bug fixing.", + "readOnly": true + }, + "properties": { + "$ref": "#/definitions/ModelProperties", + "description": "Model properties" + }, + "status": { + "$ref": "#/definitions/Status", + "description": "Status of a resource." + }, + "createdDateTime": { + "type": "string", + "format": "date-time", + "description": "The timestamp when the object was created. The timestamp is encoded as ISO 8601\ndate and time format (\"YYYY-MM-DDThh:mm:ssZ\", see\nhttps://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations).", + "readOnly": true + }, + "lastActionDateTime": { + "type": "string", + "format": "date-time", + "description": "The timestamp when the current status was entered. The timestamp is encoded as\nISO 8601 date and time format (\"YYYY-MM-DDThh:mm:ssZ\", see\nhttps://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations).", + "readOnly": true + } + }, + "required": [ + "id", + "recipe", + "projectId", + "consentId", + "trainingSetId" + ] + }, + "ModelCapability": { + "type": "string", + "description": "Model capability", + "enum": [ + "PersonalVoice" + ], + "x-ms-enum": { + "name": "ModelCapability", + "modelAsString": true, + "values": [ + { + "name": "PersonalVoice", + "value": "PersonalVoice", + "description": "PersonalVoice" + } + ] + } + }, + "ModelFailureReason": { + "type": "string", + "description": "Model training failure reason", + "enum": [ + "InaccessibleCustomerStorage", + "SpeakerVerificationFailed", + "TerminateByUser", + "Internal", + "DataNotReady", + "DataNotEnough" + ], + "x-ms-enum": { + "name": "ModelFailureReason", + "modelAsString": true, + "values": [ + { + "name": "InaccessibleCustomerStorage", + "value": "InaccessibleCustomerStorage", + "description": "The customer uses Bring Your Own Storage in Speech Account. But the storage is\nnot accessible now. Please check\n[doc](https://learn.microsoft.com/azure/ai-services/speech-service/bring-your-own-storage-speech-resource?tabs=portal#configure-byos-associated-storage-account)." + }, + { + "name": "SpeakerVerificationFailed", + "value": "SpeakerVerificationFailed", + "description": "The consent and training audio are not from the same speaker." + }, + { + "name": "TerminateByUser", + "value": "TerminateByUser", + "description": "The customer canceled model training." + }, + { + "name": "Internal", + "value": "Internal", + "description": "Custom Voice Service error." + }, + { + "name": "DataNotReady", + "value": "DataNotReady", + "description": "Training data is not ready for model training." + }, + { + "name": "DataNotEnough", + "value": "DataNotEnough", + "description": "Training data is not enough for model training." + } + ] + } + }, + "ModelProperties": { + "type": "object", + "description": "Model properties", + "properties": { + "presetStyles": { + "type": "array", + "description": "Preset styles of this model.", + "items": { + "type": "string" + } + }, + "styleTrainingSetIds": { + "type": "object", + "description": "Customized styles and associated training sets.", + "additionalProperties": { + "type": "string" + } + }, + "voiceStyles": { + "type": "array", + "description": "All styles supported by this model.", + "items": { + "type": "string" + }, + "readOnly": true + }, + "failureReason": { + "$ref": "#/definitions/ModelFailureReason", + "description": "Model training failure reason" + }, + "failedTrainingsets": { + "type": "array", + "description": "IDs of failed training sets.", + "items": { + "type": "string" + }, + "readOnly": true + }, + "secondaryLocales": { + "type": "array", + "description": "Secondary locales that this model can speak. Locale code follows BCP-47.", + "items": { + "type": "string" + }, + "readOnly": true + } + } + }, + "Operation": { + "type": "object", + "description": "Status of a long running operation.", + "properties": { + "id": { + "$ref": "#/definitions/resourceId", + "description": "Unique operation ID.", + "readOnly": true + }, + "status": { + "$ref": "#/definitions/OperationStatus", + "description": "Status of an operation." + } + }, + "required": [ + "id" + ] + }, + "OperationStatus": { + "type": "string", + "description": "Status of an operation.", + "enum": [ + "NotStarted", + "Running", + "Succeeded", + "Failed" + ], + "x-ms-enum": { + "name": "OperationStatus", + "modelAsString": true, + "values": [ + { + "name": "NotStarted", + "value": "NotStarted", + "description": "NotStarted" + }, + { + "name": "Running", + "value": "Running", + "description": "Running" + }, + { + "name": "Succeeded", + "value": "Succeeded", + "description": "Succeeded" + }, + { + "name": "Failed", + "value": "Failed", + "description": "Failed" + } + ] + } + }, + "PagedBaseModel": { + "type": "object", + "description": "Paged collection of BaseModel items", + "properties": { + "value": { + "type": "array", + "description": "The BaseModel items on this page", + "items": { + "$ref": "#/definitions/BaseModel" + } + }, + "nextLink": { + "type": "string", + "format": "uri", + "description": "The link to the next page of items" + } + }, + "required": [ + "value" + ] + }, + "PagedConsent": { + "type": "object", + "description": "Paged collection of Consent items", + "properties": { + "value": { + "type": "array", + "description": "The Consent items on this page", + "items": { + "$ref": "#/definitions/Consent" + } + }, + "nextLink": { + "type": "string", + "format": "uri", + "description": "The link to the next page of items" + } + }, + "required": [ + "value" + ] + }, + "PagedEndpoint": { + "type": "object", + "description": "Paged collection of Endpoint items", + "properties": { + "value": { + "type": "array", + "description": "The Endpoint items on this page", + "items": { + "$ref": "#/definitions/Endpoint" + } + }, + "nextLink": { + "type": "string", + "format": "uri", + "description": "The link to the next page of items" + } + }, + "required": [ + "value" + ] + }, + "PagedModel": { + "type": "object", + "description": "Paged collection of Model items", + "properties": { + "value": { + "type": "array", + "description": "The Model items on this page", + "items": { + "$ref": "#/definitions/Model" + } + }, + "nextLink": { + "type": "string", + "format": "uri", + "description": "The link to the next page of items" + } + }, + "required": [ + "value" + ] + }, + "PagedPersonalVoice": { + "type": "object", + "description": "Paged collection of PersonalVoice items", + "properties": { + "value": { + "type": "array", + "description": "The PersonalVoice items on this page", + "items": { + "$ref": "#/definitions/PersonalVoice" + } + }, + "nextLink": { + "type": "string", + "format": "uri", + "description": "The link to the next page of items" + } + }, + "required": [ + "value" + ] + }, + "PagedProject": { + "type": "object", + "description": "Paged collection of Project items", + "properties": { + "value": { + "type": "array", + "description": "The Project items on this page", + "items": { + "$ref": "#/definitions/Project" + } + }, + "nextLink": { + "type": "string", + "format": "uri", + "description": "The link to the next page of items" + } + }, + "required": [ + "value" + ] + }, + "PagedRecipe": { + "type": "object", + "description": "Paged collection of Recipe items", + "properties": { + "value": { + "type": "array", + "description": "The Recipe items on this page", + "items": { + "$ref": "#/definitions/Recipe" + } + }, + "nextLink": { + "type": "string", + "format": "uri", + "description": "The link to the next page of items" + } + }, + "required": [ + "value" + ] + }, + "PagedTrainingSet": { + "type": "object", + "description": "Paged collection of TrainingSet items", + "properties": { + "value": { + "type": "array", + "description": "The TrainingSet items on this page", + "items": { + "$ref": "#/definitions/TrainingSet" + } + }, + "nextLink": { + "type": "string", + "format": "uri", + "description": "The link to the next page of items" + } + }, + "required": [ + "value" + ] + }, + "PersonalVoice": { + "type": "object", + "description": "Personal voice object", + "properties": { + "id": { + "$ref": "#/definitions/resourceId", + "description": "Resource id", + "readOnly": true + }, + "speakerProfileId": { + "$ref": "#/definitions/Azure.Core.uuid", + "description": "Personal voice speaker profile id. Fill this property in SSML.", + "readOnly": true + }, + "displayName": { + "type": "string", + "description": "Display name of personal voice", + "minLength": 1 + }, + "description": { + "type": "string", + "description": "Personal voice description" + }, + "projectId": { + "$ref": "#/definitions/resourceId", + "description": "Resource id" + }, + "consentId": { + "$ref": "#/definitions/resourceId", + "description": "Resource id" + }, + "audios": { + "$ref": "#/definitions/AzureBlobContentSource", + "description": "Azure Blob Storage content. With the examples below, it represents files\nhttps://contoso.blob.core.windows.net/voicecontainer/jessica/*.wav" + }, + "properties": { + "$ref": "#/definitions/PersonalVoiceProperties", + "description": "Personal voice properties" + }, + "status": { + "$ref": "#/definitions/Status", + "description": "Status of a resource." + }, + "createdDateTime": { + "type": "string", + "format": "date-time", + "description": "The timestamp when the object was created. The timestamp is encoded as ISO 8601\ndate and time format (\"YYYY-MM-DDThh:mm:ssZ\", see\nhttps://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations).", + "readOnly": true + }, + "lastActionDateTime": { + "type": "string", + "format": "date-time", + "description": "The timestamp when the current status was entered. The timestamp is encoded as\nISO 8601 date and time format (\"YYYY-MM-DDThh:mm:ssZ\", see\nhttps://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations).", + "readOnly": true + } + }, + "required": [ + "id", + "projectId", + "consentId" + ] + }, + "PersonalVoiceProperties": { + "type": "object", + "description": "Personal voice properties", + "properties": { + "failureReason": { + "$ref": "#/definitions/ModelFailureReason", + "description": "Model training failure reason" + } + } + }, + "PresetStyleItem": { + "type": "object", + "description": "Preset styles supported by the recipe. The voice model can support these styles\nwithout any style training set.", + "properties": { + "male": { + "type": "array", + "description": "Preset styles supported on male voice model.", + "items": { + "type": "string" + } + }, + "female": { + "type": "array", + "description": "Preset styles supported on female voice model.", + "items": { + "type": "string" + } + } + } + }, + "Project": { + "type": "object", + "description": "Project object. Consents, training sets, models, and endpoints are organized in\na project.", + "properties": { + "id": { + "$ref": "#/definitions/resourceId", + "description": "Resource id", + "readOnly": true + }, + "displayName": { + "type": "string", + "description": "Project name", + "minLength": 1 + }, + "description": { + "type": "string", + "description": "Project description" + }, + "kind": { + "$ref": "#/definitions/ProjectKind", + "description": "Project kind" + }, + "locale": { + "type": "string", + "description": "The locale of this project. Locale code follows BCP-47. You can find the text\nto speech locale list here\nhttps://learn.microsoft.com/azure/ai-services/speech-service/language-support?tabs=tts." + }, + "createdDateTime": { + "type": "string", + "format": "date-time", + "description": "The timestamp when the object was created. The timestamp is encoded as ISO 8601\ndate and time format (\"YYYY-MM-DDThh:mm:ssZ\", see\nhttps://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations).", + "readOnly": true + } + }, + "required": [ + "id", + "kind" + ] + }, + "ProjectKind": { + "type": "string", + "description": "Project kind", + "enum": [ + "ProfessionalVoice", + "PersonalVoice" + ], + "x-ms-enum": { + "name": "ProjectKind", + "modelAsString": true, + "values": [ + { + "name": "ProfessionalVoice", + "value": "ProfessionalVoice", + "description": "ProfessionalVoice" + }, + { + "name": "PersonalVoice", + "value": "PersonalVoice", + "description": "PersonalVoice" + } + ] + } + }, + "Recipe": { + "type": "object", + "description": "Recipe for model building. Different recipes have different capability.", + "properties": { + "kind": { + "type": "string", + "description": "Recipe kind" + }, + "version": { + "type": "string", + "description": "Recipe version" + }, + "description": { + "type": "string", + "description": "Recipe description", + "readOnly": true + }, + "minUtteranceCount": { + "type": "integer", + "format": "int32", + "description": "Minimum utterance count required to train a voice model with this recipe.", + "readOnly": true + }, + "minStyleUtteranceCount": { + "type": "integer", + "format": "int32", + "description": "Minimum utterance count required to train each customized style.", + "readOnly": true + }, + "maxCustomStyleNum": { + "type": "integer", + "format": "int32", + "description": "Maximum customized style number supported in one voice model.", + "readOnly": true + }, + "datasetLocales": { + "type": "array", + "description": "The locale of the training dataset. Locale code follows BCP-47. You can find\nthe text to speech locale list here\nhttps://learn.microsoft.com/azure/ai-services/speech-service/language-support?tabs=tts.", + "items": { + "type": "string" + }, + "readOnly": true + }, + "modelLocales": { + "type": "array", + "description": "The locale that a voice model can speak with this recipe. Locale code follows\nBCP-47. You can find the text to speech locale list here\nhttps://learn.microsoft.com/azure/ai-services/speech-service/language-support?tabs=tts.", + "items": { + "type": "string" + }, + "readOnly": true + }, + "presetStyles": { + "type": "object", + "description": "Preset styles supported by this recipe per locale. You can get these styles\nwithout any style training set.", + "additionalProperties": { + "$ref": "#/definitions/PresetStyleItem" + }, + "readOnly": true + }, + "minDurationInSeconds": { + "type": "number", + "format": "double", + "description": "Minimum audio duration in seconds required to train a voice model with this\nrecipe.", + "readOnly": true + } + } + }, + "Status": { + "type": "string", + "description": "Status of a resource.", + "enum": [ + "NotStarted", + "Running", + "Succeeded", + "Failed", + "Disabling", + "Disabled" + ], + "x-ms-enum": { + "name": "Status", + "modelAsString": true, + "values": [ + { + "name": "NotStarted", + "value": "NotStarted", + "description": "NotStarted" + }, + { + "name": "Running", + "value": "Running", + "description": "Running" + }, + { + "name": "Succeeded", + "value": "Succeeded", + "description": "Succeeded" + }, + { + "name": "Failed", + "value": "Failed", + "description": "Failed" + }, + { + "name": "Disabling", + "value": "Disabling", + "description": "Disabling" + }, + { + "name": "Disabled", + "value": "Disabled", + "description": "Disabled" + } + ] + } + }, + "TrainingSet": { + "type": "object", + "description": "Training set", + "properties": { + "id": { + "$ref": "#/definitions/resourceId", + "description": "Resource id", + "readOnly": true + }, + "displayName": { + "type": "string", + "description": "Training set name", + "minLength": 1 + }, + "description": { + "type": "string", + "description": "Training set description" + }, + "locale": { + "type": "string", + "description": "The locale of the training dataset. Locale code follows BCP-47. You can find\nthe text to speech locale list here\nhttps://learn.microsoft.com/azure/ai-services/speech-service/language-support?tabs=tts." + }, + "voiceKind": { + "$ref": "#/definitions/VoiceKind", + "description": "Voice kind" + }, + "properties": { + "$ref": "#/definitions/TrainingSetProperties", + "description": "Training set properties" + }, + "projectId": { + "$ref": "#/definitions/resourceId", + "description": "Resource id" + }, + "status": { + "$ref": "#/definitions/Status", + "description": "Status of a resource." + }, + "createdDateTime": { + "type": "string", + "format": "date-time", + "description": "The timestamp when the object was created. The timestamp is encoded as ISO 8601\ndate and time format (\"YYYY-MM-DDThh:mm:ssZ\", see\nhttps://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations).", + "readOnly": true + }, + "lastActionDateTime": { + "type": "string", + "format": "date-time", + "description": "The timestamp when the current status was entered. The timestamp is encoded as\nISO 8601 date and time format (\"YYYY-MM-DDThh:mm:ssZ\", see\nhttps://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations).", + "readOnly": true + } + }, + "required": [ + "id", + "locale", + "projectId" + ] + }, + "TrainingSetProperties": { + "type": "object", + "description": "Training set properties", + "properties": { + "utteranceCount": { + "type": "integer", + "format": "int32", + "description": "Utterance count in this training set" + }, + "durationInSeconds": { + "type": "number", + "format": "double", + "description": "Total duration of audio in seconds in this training set" + }, + "isContextual": { + "type": "boolean", + "description": "Indicates whether the training set is composed of contextual data" + } + } + }, + "VoiceKind": { + "type": "string", + "description": "Voice kind", + "enum": [ + "Male", + "Female" + ], + "x-ms-enum": { + "name": "VoiceKind", + "modelAsString": true, + "values": [ + { + "name": "Male", + "value": "Male", + "description": "Male" + }, + { + "name": "Female", + "value": "Female", + "description": "Female" + } + ] + } + }, + "resourceId": { + "type": "string", + "description": "Resource ID", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$" + } + }, + "parameters": { + "Azure.Core.Foundations.ApiVersionParameter": { + "name": "api-version", + "in": "query", + "description": "The API version to use for this operation.", + "required": true, + "type": "string", + "minLength": 1, + "x-ms-parameter-location": "method", + "x-ms-client-name": "apiVersion" + }, + "CustomListQueryParameters.filter": { + "name": "filter", + "in": "query", + "description": "Filter condition.\n- **Supported properties:** projectId, createdDateTime, locale, kind\n- **Operators:**\n- eq, ne are supported for all properties.\n- gt, ge, lt, le are supported for createdDateTime.\n- **Example:**\n- ```filter=projectId eq 'Jessica'``` (filter by project ID)\n- ```filter=kind eq 'ProfessionalVoice'``` (filter project by kind)\n- ```filter=locale eq 'en-US'``` (filter training set and model by locale)\n- ```filter=createdDateTime gt 2022-12-30T23:59:59.99Z``` (filter resource\ncreated time after 2023-11-01)", + "required": false, + "type": "string", + "x-ms-parameter-location": "method" + }, + "CustomListQueryParameters.maxpagesize": { + "name": "maxpagesize", + "in": "query", + "description": "The maximum number of items to include in a single response.", + "required": false, + "type": "integer", + "format": "int32", + "default": 100, + "x-ms-parameter-location": "method" + }, + "CustomListQueryParameters.skip": { + "name": "skip", + "in": "query", + "description": "The number of result items to skip.", + "required": false, + "type": "integer", + "format": "int32", + "default": 0, + "x-ms-parameter-location": "method" + }, + "OperationIdHeader": { + "name": "Operation-Id", + "in": "header", + "description": "ID of the status monitor for the operation. If the Operation-Id header matches\nan existing operation and the request is not identical to the prior request, it\nwill fail with a 400 Bad Request.", + "required": false, + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{1,62}[a-zA-Z0-9]$", + "x-ms-parameter-location": "method", + "x-ms-client-name": "operationId" + } + } +} diff --git a/specification/cognitiveservices/data-plane/TextToSpeech/tspconfig.yaml b/specification/cognitiveservices/data-plane/TextToSpeech/tspconfig.yaml new file mode 100644 index 000000000000..4178b6b54f78 --- /dev/null +++ b/specification/cognitiveservices/data-plane/TextToSpeech/tspconfig.yaml @@ -0,0 +1,39 @@ +parameters: + "service-dir": + default: "sdk/cognitiveservices" + "dependencies": + default: "" +emit: + - "@azure-tools/typespec-autorest" +linter: + extends: + - "@azure-tools/typespec-azure-rulesets/data-plane" + disable: + "@azure-tools/typespec-azure-core/use-standard-operations": "PUT create and POST action operations use Foundations.Operation to preserve exact response headers and status codes" + "@azure-tools/typespec-azure-core/use-standard-names": "Migrated from swagger; operation names match existing API conventions" + "@azure-tools/typespec-azure-core/byos": "Existing API uses multipart/form-data upload; BYOS not applicable" + "@azure-tools/typespec-client-generator-core/property-name-conflict": "Fixed via @@clientName in client.tsp; warning only appears in main compile" +options: + "@azure-tools/typespec-autorest": + emitter-output-dir: "{project-root}" + output-file: "{version-status}/{version}/texttospeech.json" + examples-dir: "{project-root}/examples" + "@azure-tools/typespec-python": + emitter-output-dir: "{output-dir}/{service-dir}/azure-cognitiveservices-speech-texttospeech" + namespace: "azure.cognitiveservices.speech.texttospeech" + generate-test: true + generate-sample: true + flavor: azure + "@azure-typespec/http-client-csharp": + namespace: "Azure.CognitiveServices.Speech.TextToSpeech" + emitter-output-dir: "{output-dir}/{service-dir}/{namespace}" + "@azure-tools/typespec-ts": + emitter-output-dir: "{output-dir}/{service-dir}/cognitiveservices-speech-texttospeech" + package-details: + name: "@azure/cognitiveservices-speech-texttospeech" + experimental-extensible-enums: true + flavor: azure + "@azure-tools/typespec-java": + emitter-output-dir: "{output-dir}/{service-dir}/azure-cognitiveservices-speech-texttospeech" + namespace: com.azure.cognitiveservices.speech.texttospeech + flavor: azure From f7e8d3375f9163320d343c7bb931f22c7e47d34e Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Thu, 12 Mar 2026 21:15:16 -0500 Subject: [PATCH 065/125] Addressed copilots review comments --- .../detect-new-resource-provider.js | 4 ++-- .../detect-new-resource-types.js | 24 ++++++++++--------- .../src/summarize-checks/labelling.js | 2 +- .../detect-new-resource-provider.test.js | 4 ++-- .../detect-new-resource-types.test.js | 1 + 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js b/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js index b38f2834152a..921152115302 100644 --- a/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js +++ b/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js @@ -165,7 +165,7 @@ export default async function detectNewResourceProvider({ core }) { } return null; }) - .filter(Boolean), + .filter((p) => p !== null), ); if (changedNamespacePaths.size > 0) { @@ -309,7 +309,7 @@ async function checkNewResourceTypes(repoRoot, rmFiles, core) { let allLeasesValid = true; for (const ns of newRtResults) { - const leaseValid = await checkLease(ns.orgName, ns.rpNamespace, ""); + const leaseValid = await checkLease(ns.orgName, ns.rpNamespace, ns.serviceName); if (leaseValid) { core.info(` - ${ns.rpNamespace}: valid ARM lease for new resource types`); diff --git a/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js b/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js index 58770e9f12bb..012e294869e3 100644 --- a/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js +++ b/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js @@ -98,11 +98,11 @@ function getResourceTypesFromSwagger(swaggerDoc) { * * @param {import("simple-git").SimpleGit} git * @param {string} gitRef - Git ref (e.g. "HEAD" or "HEAD^") - * @param {string} namespacePath - e.g. `specification/compute/resource-manager/Microsoft.Compute` + * @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, namespacePath, branchName) { +async function getResourceTypesAtRef(git, gitRef, specPath, branchName) { /** @type {Map} */ const allTypes = new Map(); @@ -113,7 +113,7 @@ async function getResourceTypesAtRef(git, gitRef, namespacePath, branchName) { "-r", "--name-only", gitRef, - namespacePath, + specPath, ]); } catch { return null; // path doesn't exist at this ref @@ -174,7 +174,7 @@ async function getResourceTypesAtRef(git, gitRef, namespacePath, branchName) { * @param {string} params.repoRoot - Repository root directory * @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}>>} + * @returns {Promise}>>} */ export async function detectNewResourceTypes({ repoRoot, @@ -184,7 +184,7 @@ export async function detectNewResourceTypes({ const git = simpleGit(repoRoot); // Group changed RM swagger files by service subdirectory (rpNamespace + serviceName) - /** @type {Map} */ + /** @type {Map} */ const namespaceMap = new Map(); for (const file of rmFiles) { @@ -201,25 +201,27 @@ export async function detectNewResourceTypes({ // introducing the first preview/stable folder under an existing rpNamespace const serviceName = parts[4]; const isLifecycleFolder = serviceName === "stable" || serviceName === "preview"; - const namespacePath = isLifecycleFolder + 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, namespacePath, rpNamespace }); + namespaceMap.set(serviceKey, { orgName, specPath, rpNamespace, serviceName: actualServiceName }); } } const results = []; - for (const [serviceKey, { orgName, namespacePath, rpNamespace }] of namespaceMap) { + for (const [serviceKey, { orgName, specPath, rpNamespace, serviceName }] of namespaceMap) { core.info(`Checking for new resource types in ${serviceKey}...`); const baseTypes = await getResourceTypesAtRef( git, "HEAD^", - namespacePath, + specPath, "base", ); @@ -232,7 +234,7 @@ export async function detectNewResourceTypes({ const headTypes = await getResourceTypesAtRef( git, "HEAD", - namespacePath, + specPath, "head", ); @@ -257,7 +259,7 @@ export async function detectNewResourceTypes({ for (const t of newTypes) { core.info(` - ${t.resourceType} (${t.operations.join(", ")})`); } - results.push({ rpNamespace, orgName, newResourceTypes: newTypes }); + results.push({ rpNamespace, orgName, serviceName, newResourceTypes: newTypes }); } else { core.info(` ${rpNamespace}: no new resource types`); } diff --git a/.github/workflows/src/summarize-checks/labelling.js b/.github/workflows/src/summarize-checks/labelling.js index 78dc65c7fc91..b40ccd7a5b2d 100644 --- a/.github/workflows/src/summarize-checks/labelling.js +++ b/.github/workflows/src/summarize-checks/labelling.js @@ -898,7 +898,7 @@ const rulesPri0NotReadyForArmReview = [ 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 Modelling Review team before merging.
" + + "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")}.`, ), diff --git a/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js b/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js index d4866e5be596..04c02dddbbce 100644 --- a/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js +++ b/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js @@ -202,7 +202,7 @@ describe("detectNewResourceProvider", () => { // detectNewResourceTypes returns new RT vi.mocked(detectNewResourceTypes).mockResolvedValue([ - { namespace: "Microsoft.Compute", orgName: "compute", newResourceTypes: [{ resourceType: "Microsoft.Compute/disks" }] }, + { rpNamespace: "Microsoft.Compute", orgName: "compute", serviceName: "", newResourceTypes: [{ resourceType: "Microsoft.Compute/disks" }] }, ]); vi.mocked(checkLease).mockResolvedValue(true); @@ -223,7 +223,7 @@ describe("detectNewResourceProvider", () => { vi.mocked(mockRaw).mockResolvedValue(rmFile); vi.mocked(detectNewResourceTypes).mockResolvedValue([ - { namespace: "Microsoft.Compute", orgName: "compute", newResourceTypes: [{ resourceType: "Microsoft.Compute/disks" }] }, + { rpNamespace: "Microsoft.Compute", orgName: "compute", serviceName: "", newResourceTypes: [{ resourceType: "Microsoft.Compute/disks" }] }, ]); vi.mocked(checkLease).mockResolvedValue(false); diff --git a/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js b/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js index ef21739cf2bf..a6d906cef298 100644 --- a/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js +++ b/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js @@ -176,6 +176,7 @@ describe("detectNewResourceTypes", () => { expect(result[0]).toMatchObject({ rpNamespace: "Microsoft.Compute", orgName: "compute", + serviceName: "", }); expect(result[0].newResourceTypes).toHaveLength(1); expect(result[0].newResourceTypes[0]).toMatchObject({ From ed0c0140827a979785a4d6489ead85cd8b5a344c Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Thu, 12 Mar 2026 21:20:35 -0500 Subject: [PATCH 066/125] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../arm-lease-validation.test.js | 68 +++++++++++++++---- 1 file changed, 56 insertions(+), 12 deletions(-) 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 index e5bb0415c9f7..38c5ae515801 100644 --- a/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js +++ b/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js @@ -24,14 +24,20 @@ describe("validate-arm-leases", () => { 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/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/testservice/Microsoft.Test/other.yaml"), + ).toBe(false); expect(isFileAllowed(".github/arm-leases/badtest/No.Yaml/no.md")).toBe(false); }); }); @@ -62,27 +68,65 @@ describe("validate-arm-leases", () => { 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); + 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); + 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 + 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 @ + 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", () => { From 34a871125665ef993e09b185d6d809a8371900ea Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Thu, 12 Mar 2026 21:56:37 -0500 Subject: [PATCH 067/125] Addressed copilots review comments --- .github/arm-leases/README.md | 2 +- .../widget/Microsoft.Widget/Widget/lease.yaml | 2 +- .../arm-lease-validation.js | 99 ++++++++++++------- .../arm-lease-validation.test.js | 93 ++++++++++++----- 4 files changed, 130 insertions(+), 66 deletions(-) diff --git a/.github/arm-leases/README.md b/.github/arm-leases/README.md index a5fa799d0ff4..c381004ae5db 100644 --- a/.github/arm-leases/README.md +++ b/.github/arm-leases/README.md @@ -77,7 +77,7 @@ All lease files are automatically validated with the following requirements: ### 5. Reviewer - Required field that cannot be empty -- Must be a GitHub alias starting with `@` (e.g., `@octocat`) +- Must be a GitHub alias starting with `@` (e.g., `@githubUsername`) ## Troubleshooting diff --git a/.github/arm-leases/widget/Microsoft.Widget/Widget/lease.yaml b/.github/arm-leases/widget/Microsoft.Widget/Widget/lease.yaml index e2ca2684a733..4948142a6a3a 100644 --- a/.github/arm-leases/widget/Microsoft.Widget/Widget/lease.yaml +++ b/.github/arm-leases/widget/Microsoft.Widget/Widget/lease.yaml @@ -1,5 +1,5 @@ lease: resource-provider: Microsoft.Widget - startdate: "2026-03-11" + startdate: "2026-03-20" duration: P180D reviewer: "@pshao25" diff --git a/.github/workflows/src/arm-lease-validation/arm-lease-validation.js b/.github/workflows/src/arm-lease-validation/arm-lease-validation.js index 35c7f4aa67ba..ed73b97844b7 100644 --- a/.github/workflows/src/arm-lease-validation/arm-lease-validation.js +++ b/.github/workflows/src/arm-lease-validation/arm-lease-validation.js @@ -1,9 +1,9 @@ +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 YAML from "js-yaml"; import * as z from "zod"; -import { Temporal } from "@js-temporal/polyfill"; import { getChangedFiles } from "../../../shared/src/changed-files.js"; import { CoreLogger } from "../core-logger.js"; @@ -11,9 +11,9 @@ 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_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$/; + /^\.github\/arm-leases\/[a-z0-9-]+\/[a-zA-Z0-9.]+\/(?!stable|preview)([^/]+)\/lease\.yaml$/; export const ALLOWED_FILE_PATTERNS = [ LEASE_FILE_PATTERN, @@ -30,50 +30,62 @@ export const ALLOWED_FILE_PATTERNS = [ * resource-provider: Microsoft.Compute * startdate: "2025-06-01" * duration: "P180D" - * reviewer: alias + * 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) => { + 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) => { + }, "startdate must be a valid calendar date"), + duration: z.string().refine((v) => { try { - return Temporal.Duration.from(v).total({ unit: "days", relativeTo: Temporal.Now.plainDateISO() }) <= 180; + Temporal.Duration.from(v); + return true; } catch { return false; } - }, - "duration must be a valid ISO 8601 duration not exceeding 180 days (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)", - ), - }), + }, "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. + } + }), }); // ============================================ @@ -151,7 +163,12 @@ export async function validateLeaseContent(leaseFile, relativePath, workspaceRoo // 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) { + 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()})`, ); @@ -180,7 +197,13 @@ export async function validateLeaseContent(leaseFile, relativePath, workspaceRoo // 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()) { + 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}`, ); @@ -205,7 +228,7 @@ export async function validateLeaseContent(leaseFile, relativePath, workspaceRoo * @returns {Promise<{ status: string, errors: number }>} Validation result */ export default async function validateArmLeases(core) { - const cwd = process.env.GITHUB_WORKSPACE; + const cwd = process.env.GITHUB_WORKSPACE || process.cwd(); let hasErrors = false; core.info("Running ARM Lease File Validation"); 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 index e5bb0415c9f7..309594acfd7b 100644 --- a/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js +++ b/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js @@ -12,9 +12,9 @@ vi.mock("fs/promises", () => ({ import { isFileAllowed, + leaseSchema, validateFolderStructure, validateLeaseContent, - leaseSchema, } from "../../src/arm-lease-validation/arm-lease-validation.js"; describe("validate-arm-leases", () => { @@ -25,7 +25,9 @@ describe("validate-arm-leases", () => { 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/compute/Microsoft.Compute/DiskRP/lease.yaml")).toBe( + true, + ); expect(isFileAllowed(".github/arm-leases/README.md")).toBe(true); }); @@ -38,15 +40,19 @@ describe("validate-arm-leases", () => { 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); + 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); }); }); @@ -62,27 +68,49 @@ describe("validate-arm-leases", () => { 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); + 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); + 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 + 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 @ + 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", () => { @@ -118,7 +146,10 @@ describe("validate-arm-leases", () => { 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"); + 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); }); @@ -126,19 +157,27 @@ describe("validate-arm-leases", () => { // 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"); + 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"); + 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({ isDirectory: () => true }); + mockStat.mockResolvedValue(/** @type {any} */ ({ isDirectory: () => true })); const result = await validateLeaseContent( "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", @@ -163,7 +202,9 @@ describe("validate-arm-leases", () => { it("allows new RP when service exists but RP folder does not", async () => { mockReadFile.mockResolvedValue(validYaml); - mockStat.mockResolvedValueOnce({ isDirectory: () => true }).mockRejectedValueOnce(new Error("ENOENT")); + mockStat + .mockResolvedValueOnce(/** @type {any} */ ({ isDirectory: () => true })) + .mockRejectedValueOnce(new Error("ENOENT")); const result = await validateLeaseContent( "/repo/.github/arm-leases/testservice/Microsoft.Test/lease.yaml", From d29ef17a06200e3816c6788aad043e6052c35554 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Thu, 12 Mar 2026 22:48:22 -0500 Subject: [PATCH 068/125] Addressed copilots suggestions --- .../detect-new-resource-provider.js | 21 ++++----- .../detect-new-resource-types.js | 4 +- .../detect-arm-leases.test.js | 5 +- .../detect-new-resource-provider.test.js | 35 +++++++------- .../detect-new-resource-types.test.js | 47 ++++++------------- 5 files changed, 45 insertions(+), 67 deletions(-) diff --git a/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js b/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js index 921152115302..1c30afd3de46 100644 --- a/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js +++ b/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js @@ -136,17 +136,16 @@ function extractResourceProviders(files) { * @returns {Promise<{ status: string, labelActions: ManagedLabelActions }>} */ export default async function detectNewResourceProvider({ core }) { - const repoRoot = process.env.GITHUB_WORKSPACE; - const git = simpleGit(repoRoot); - - core.info("Detecting New Resource Providers"); - 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); @@ -161,7 +160,7 @@ export default async function detectNewResourceProvider({ core }) { 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, match.index + match[0].length - 1); + return f.substring(0, /** @type {number} */ (match.index) + match[0].length - 1); } return null; }) @@ -203,7 +202,7 @@ export default async function detectNewResourceProvider({ core }) { 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(repoRoot, rmFiles, core); + return await checkNewResourceTypes(rmFiles, core); } } @@ -224,7 +223,7 @@ export default async function detectNewResourceProvider({ core }) { if (newResourceProviders.length === 0) { core.info("No new resource providers detected."); core.info("Checking for new resource types in existing RPs..."); - return await checkNewResourceTypes(repoRoot, rmFiles, core); + return await checkNewResourceTypes(rmFiles, core); } core.info(`Detected ${newResourceProviders.length} new resource provider(s)`); @@ -262,7 +261,7 @@ export default async function detectNewResourceProvider({ core }) { } core.info("Checking for new resource types in existing RPs..."); - const newRtResult = await checkNewResourceTypes(repoRoot, rmFiles, core); + const newRtResult = await checkNewResourceTypes(rmFiles, core); // Merge label actions: 'add' wins over 'remove' wins over 'none' /** @type {ManagedLabelActions} */ @@ -285,14 +284,12 @@ export default async function detectNewResourceProvider({ core }) { /** * Check for new resource types in existing RPs and validate their leases. * - * @param {string} repoRoot - Repository root directory * @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(repoRoot, rmFiles, core) { +async function checkNewResourceTypes(rmFiles, core) { const newRtResults = await detectNewResourceTypes({ - repoRoot, rmFiles, core, }); diff --git a/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js b/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js index 012e294869e3..cd42183e69bd 100644 --- a/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js +++ b/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js @@ -171,17 +171,15 @@ async function getResourceTypesAtRef(git, gitRef, specPath, branchName) { * present in HEAD but absent from base is new. * * @param {Object} params - * @param {string} params.repoRoot - Repository root directory * @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({ - repoRoot, rmFiles, core, }) { - const git = simpleGit(repoRoot); + const git = simpleGit(process.env.GITHUB_WORKSPACE); // Group changed RM swagger files by service subdirectory (rpNamespace + serviceName) /** @type {Map} */ diff --git a/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js b/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js index 0a4e503c1527..9043b99535c8 100644 --- a/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js +++ b/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js @@ -33,7 +33,10 @@ function daysAgo(n) { return today().subtract({ days: n }).toString(); } -/** Build a valid lease YAML string */ +/** 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`; } diff --git a/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js b/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js index 04c02dddbbce..59c44e17257a 100644 --- a/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js +++ b/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { createMockCore, createMockContext } from "../mocks.js"; +import { createMockCore } from "../mocks.js"; /** @type {import("vitest").MockedFunction} */ const mockRaw = vi.hoisted(() => vi.fn().mockResolvedValue("")); @@ -22,7 +22,6 @@ import { detectNewResourceTypes } from "../../src/arm-lease-validation/detect-ne import detectNewResourceProvider from "../../src/arm-lease-validation/detect-new-resource-provider.js"; const core = createMockCore(); -const context = createMockContext(); describe("detectNewResourceProvider", () => { afterEach(() => { @@ -40,7 +39,7 @@ describe("detectNewResourceProvider", () => { // Pre-check: file exists in base → not brand new vi.mocked(mockRaw).mockResolvedValue(rmFile); - const result = await detectNewResourceProvider({ context, core }); + const result = await detectNewResourceProvider({ core }); expect(result.status).toBe("no-new-rp"); expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); @@ -58,7 +57,7 @@ describe("detectNewResourceProvider", () => { vi.mocked(mockRaw).mockResolvedValue(""); vi.mocked(checkLease).mockResolvedValue(true); - const result = await detectNewResourceProvider({ context, core }); + const result = await detectNewResourceProvider({ core }); expect(result.status).toBe("new-rp-all-leases-valid"); expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); @@ -77,7 +76,7 @@ describe("detectNewResourceProvider", () => { vi.mocked(mockRaw).mockResolvedValue(""); vi.mocked(checkLease).mockResolvedValue(false); - const result = await detectNewResourceProvider({ context, core }); + const result = await detectNewResourceProvider({ core }); expect(result.status).toBe("new-rp-invalid-lease"); expect(result.labelActions.ARMModelingReviewRequired).toBe("add"); @@ -99,9 +98,9 @@ describe("detectNewResourceProvider", () => { vi.mocked(mockRaw).mockResolvedValue(""); // SvcA valid, SvcB invalid - vi.mocked(checkLease).mockImplementation((orgName) => orgName === "svcA"); + vi.mocked(checkLease).mockImplementation((orgName) => Promise.resolve(orgName === "svcA")); - const result = await detectNewResourceProvider({ context, core }); + const result = await detectNewResourceProvider({ core }); expect(result.status).toBe("new-rp-invalid-lease"); expect(result.labelActions.ARMModelingReviewRequired).toBe("add"); @@ -121,7 +120,7 @@ describe("detectNewResourceProvider", () => { vi.mocked(mockRaw).mockResolvedValue(""); vi.mocked(checkLease).mockResolvedValue(true); - const result = await detectNewResourceProvider({ context, core }); + const result = await detectNewResourceProvider({ core }); expect(result.status).toBe("new-rp-all-leases-valid"); expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); @@ -138,7 +137,7 @@ describe("detectNewResourceProvider", () => { vi.mocked(mockRaw).mockResolvedValue(""); vi.mocked(checkLease).mockResolvedValue(true); - await detectNewResourceProvider({ context, core }); + await detectNewResourceProvider({ core }); expect(checkLease).toHaveBeenCalledWith("svc", "Microsoft.Svc", "ComputeRP"); }); @@ -151,7 +150,7 @@ describe("detectNewResourceProvider", () => { vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([rmFile]); vi.mocked(mockRaw).mockResolvedValue(rmFile); - const result = await detectNewResourceProvider({ context, core }); + const result = await detectNewResourceProvider({ core }); expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); expect(result.labelActions.ARMModelingSignedOff).toBe("remove"); @@ -167,7 +166,7 @@ describe("detectNewResourceProvider", () => { vi.mocked(mockRaw).mockResolvedValue(""); vi.mocked(checkLease).mockResolvedValue(false); - const result = await detectNewResourceProvider({ context, core }); + const result = await detectNewResourceProvider({ core }); expect(result.labelActions.ARMModelingReviewRequired).toBe("add"); expect(result.labelActions.ARMModelingSignedOff).toBe("remove"); @@ -183,7 +182,7 @@ describe("detectNewResourceProvider", () => { vi.mocked(mockRaw).mockResolvedValue(""); vi.mocked(checkLease).mockResolvedValue(true); - const result = await detectNewResourceProvider({ context, core }); + const result = await detectNewResourceProvider({ core }); expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); expect(result.labelActions.ARMModelingSignedOff).toBe("remove"); @@ -202,11 +201,11 @@ describe("detectNewResourceProvider", () => { // detectNewResourceTypes returns new RT vi.mocked(detectNewResourceTypes).mockResolvedValue([ - { rpNamespace: "Microsoft.Compute", orgName: "compute", serviceName: "", newResourceTypes: [{ resourceType: "Microsoft.Compute/disks" }] }, + { 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 detectNewResourceProvider({ context, core }); + const result = await detectNewResourceProvider({ core }); expect(result.status).toBe("new-rt-all-leases-valid"); expect(result.labelActions.ARMModelingAutoSignedOff).toBe("add"); @@ -223,11 +222,11 @@ describe("detectNewResourceProvider", () => { vi.mocked(mockRaw).mockResolvedValue(rmFile); vi.mocked(detectNewResourceTypes).mockResolvedValue([ - { rpNamespace: "Microsoft.Compute", orgName: "compute", serviceName: "", newResourceTypes: [{ resourceType: "Microsoft.Compute/disks" }] }, + { 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 detectNewResourceProvider({ context, core }); + const result = await detectNewResourceProvider({ core }); expect(result.status).toBe("new-rt-invalid-lease"); expect(result.labelActions.ARMModelingReviewRequired).toBe("add"); @@ -244,10 +243,10 @@ describe("detectNewResourceProvider", () => { vi.mocked(mockRaw).mockResolvedValue(rmFile); vi.mocked(detectNewResourceTypes).mockResolvedValue([]); - const result = await detectNewResourceProvider({ context, core }); + const result = await detectNewResourceProvider({ core }); expect(result.status).toBe("no-new-rp"); expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); expect(result.labelActions.ARMModelingAutoSignedOff).toBe("remove"); }); -}); \ No newline at end of file +}); diff --git a/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js b/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js index a6d906cef298..0c7b0d2b125c 100644 --- a/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js +++ b/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js @@ -54,27 +54,29 @@ function setupGit({ headFiles = new Map(), fileContents = new Map(), } = {}) { - mockRaw.mockImplementation((args) => { + 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; - if (filesMap.has(namespacePath)) { - return filesMap.get(namespacePath).join("\n"); + const files = filesMap.get(namespacePath); + if (files) { + return files.join("\n"); } throw new Error(`path '${namespacePath}' does not exist`); } return ""; - }); + })); - mockShow.mockImplementation((args) => { - const key = String(args[0]); // "ref:filepath" - if (fileContents.has(key)) { - return fileContents.get(key); + 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 ─────────────────────────────────────────────────────────────── @@ -86,8 +88,6 @@ describe("detectNewResourceTypes", () => { it("returns empty when rmFiles has no version-pattern matches", async () => { const result = await detectNewResourceTypes({ - repoRoot: "/fake/repo", - rmFiles: ["specification/compute/resource-manager/readme.md"], core, }); @@ -98,8 +98,6 @@ describe("detectNewResourceTypes", () => { it("returns empty when rmFiles is empty", async () => { const result = await detectNewResourceTypes({ - repoRoot: "/fake/repo", - rmFiles: [], core, }); @@ -114,8 +112,6 @@ describe("detectNewResourceTypes", () => { setupGit({ baseFiles: new Map() }); // ls-tree throws → no base const result = await detectNewResourceTypes({ - repoRoot: "/fake/repo", - rmFiles: [rmFile], core, }); @@ -140,8 +136,6 @@ describe("detectNewResourceTypes", () => { }); const result = await detectNewResourceTypes({ - repoRoot: "/fake/repo", - rmFiles: [file], core, }); @@ -166,8 +160,6 @@ describe("detectNewResourceTypes", () => { }); const result = await detectNewResourceTypes({ - repoRoot: "/fake/repo", - rmFiles: [file], core, }); @@ -213,8 +205,6 @@ describe("detectNewResourceTypes", () => { }); const result = await detectNewResourceTypes({ - repoRoot: "/fake/repo", - rmFiles: [computeFile, networkFile], core, }); @@ -241,8 +231,6 @@ describe("detectNewResourceTypes", () => { }); await detectNewResourceTypes({ - repoRoot: "/fake/repo", - rmFiles: [file], core, }); @@ -269,8 +257,6 @@ describe("detectNewResourceTypes", () => { }); await detectNewResourceTypes({ - repoRoot: "/fake/repo", - rmFiles: [file], core, }); @@ -323,7 +309,6 @@ describe("detectNewResourceTypes", () => { }); const result = await detectNewResourceTypes({ - repoRoot: "/fake/repo", rmFiles: [previewFile], core, }); @@ -335,8 +320,8 @@ describe("detectNewResourceTypes", () => { t.resourceType === "Microsoft.Compute/quantumVMs", ); expect(quantumType).toBeDefined(); - expect(quantumType.operations).toContain("GET"); - expect(quantumType.operations).toContain("PUT"); + expect(quantumType?.operations).toContain("GET"); + expect(quantumType?.operations).toContain("PUT"); }); it("detects new resource types from path-based detection", async () => { @@ -368,8 +353,6 @@ describe("detectNewResourceTypes", () => { }); const result = await detectNewResourceTypes({ - repoRoot: "/fake/repo", - rmFiles: [newFile], core, }); @@ -381,7 +364,7 @@ describe("detectNewResourceTypes", () => { t.resourceType === "Microsoft.Compute/superDisks", ); expect(superDisksType).toBeDefined(); - expect(superDisksType.operations).toContain("GET"); + expect(superDisksType?.operations).toContain("GET"); // superDisks/metrics is also new const metricsType = result[0].newResourceTypes.find((t) => t.resourceType === "Microsoft.Compute/superDisks/metrics", @@ -415,8 +398,6 @@ describe("detectNewResourceTypes", () => { }); const result = await detectNewResourceTypes({ - repoRoot: "/fake/repo", - rmFiles: [newFile], core, }); From ef14528a3ed72e4c21a7af652f16634110bfab47 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Mon, 16 Mar 2026 11:47:38 -0500 Subject: [PATCH 069/125] Revise readme for TextToSpeech SDK configuration Updated readme to reflect the new TypeSpec-based project and current release version. --- .../data-plane/Speech/TextToSpeech/readme.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md b/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md index 1b07fe199981..82544e068d7b 100644 --- a/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md +++ b/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md @@ -4,6 +4,8 @@ Configuration for generating TextToSpeech SDK. +> **Note:** Starting from version 2026-01-01, the specification has moved to a [TypeSpec-based project](../../TextToSpeech/readme.md). + The current release for the TextToSpeech is `release_2024_02_01_preview`. ``` yaml @@ -30,4 +32,4 @@ These settings apply only when `--tag=release_2024_02_01_preview` is specified o ```yaml $(tag) == 'release_2024_02_01_preview' input-file: - preview/2024-02-01-preview/texttospeech.json -``` \ No newline at end of file +``` From bc75a89947f13670a852d15008be2fed70b547c5 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Mon, 16 Mar 2026 11:49:04 -0500 Subject: [PATCH 070/125] Fix YAML formatting in readme.md --- .../cognitiveservices/data-plane/Speech/TextToSpeech/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md b/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md index 82544e068d7b..50fe8517d783 100644 --- a/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md +++ b/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md @@ -32,4 +32,4 @@ These settings apply only when `--tag=release_2024_02_01_preview` is specified o ```yaml $(tag) == 'release_2024_02_01_preview' input-file: - preview/2024-02-01-preview/texttospeech.json -``` +-``` From ac3b48077c601ed697d2e8cbea43575047c08a63 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Mon, 16 Mar 2026 11:50:37 -0500 Subject: [PATCH 071/125] Fix YAML formatting in TextToSpeech readme --- .../cognitiveservices/data-plane/Speech/TextToSpeech/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md b/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md index 50fe8517d783..3aec2887de4a 100644 --- a/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md +++ b/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md @@ -32,4 +32,4 @@ These settings apply only when `--tag=release_2024_02_01_preview` is specified o ```yaml $(tag) == 'release_2024_02_01_preview' input-file: - preview/2024-02-01-preview/texttospeech.json --``` +- ``` From ecea007b8ae753c7183342091d52bc01fdcf38b0 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Mon, 16 Mar 2026 11:51:54 -0500 Subject: [PATCH 072/125] Fix YAML formatting in TextToSpeech readme --- .../cognitiveservices/data-plane/Speech/TextToSpeech/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md b/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md index 3aec2887de4a..82544e068d7b 100644 --- a/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md +++ b/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md @@ -32,4 +32,4 @@ These settings apply only when `--tag=release_2024_02_01_preview` is specified o ```yaml $(tag) == 'release_2024_02_01_preview' input-file: - preview/2024-02-01-preview/texttospeech.json -- ``` +``` From 21834137b7bae4745f452563e7def0be68fc6222 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Mon, 16 Mar 2026 11:59:30 -0500 Subject: [PATCH 073/125] Update readme.md From dba190b202c31af95a99524ce324f27ddb820274 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Mon, 16 Mar 2026 12:01:35 -0500 Subject: [PATCH 074/125] Revise readme for TextToSpeech SDK updates Updated readme to reflect the new TypeSpec-based project and current release. --- .../cognitiveservices/data-plane/Speech/TextToSpeech/readme.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md b/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md index 82544e068d7b..05bada9bfed8 100644 --- a/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md +++ b/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md @@ -4,8 +4,6 @@ Configuration for generating TextToSpeech SDK. -> **Note:** Starting from version 2026-01-01, the specification has moved to a [TypeSpec-based project](../../TextToSpeech/readme.md). - The current release for the TextToSpeech is `release_2024_02_01_preview`. ``` yaml From 5200de5b49b9908296b098fa0c2989d40802470a Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Mon, 16 Mar 2026 15:13:42 -0500 Subject: [PATCH 075/125] Revise TextToSpeech readme for version updates Updated readme to reflect changes in specification versioning and project structure. --- .../cognitiveservices/data-plane/Speech/TextToSpeech/readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md b/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md index 05bada9bfed8..82544e068d7b 100644 --- a/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md +++ b/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md @@ -4,6 +4,8 @@ Configuration for generating TextToSpeech SDK. +> **Note:** Starting from version 2026-01-01, the specification has moved to a [TypeSpec-based project](../../TextToSpeech/readme.md). + The current release for the TextToSpeech is `release_2024_02_01_preview`. ``` yaml From 036702eb8f17fbb21a8ac11febbccc303bb7d3d0 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Mon, 16 Mar 2026 16:20:29 -0500 Subject: [PATCH 076/125] Remove unintended trailing newline from TextToSpeech readme.md --- .../cognitiveservices/data-plane/Speech/TextToSpeech/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md b/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md index 82544e068d7b..9cf183ce8ee0 100644 --- a/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md +++ b/specification/cognitiveservices/data-plane/Speech/TextToSpeech/readme.md @@ -32,4 +32,4 @@ These settings apply only when `--tag=release_2024_02_01_preview` is specified o ```yaml $(tag) == 'release_2024_02_01_preview' input-file: - preview/2024-02-01-preview/texttospeech.json -``` +``` \ No newline at end of file From a60d668e4b886325dfac5d065b6ba8426ae43b6b Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Mar 2026 05:47:05 +0000 Subject: [PATCH 077/125] format --- .../arm-lease-validation/detect-arm-leases.js | 23 ++-- .../detect-new-resource-provider.js | 36 ++--- .../detect-new-resource-types.js | 46 +++---- .../src/summarize-checks/labelling.js | 3 +- .../detect-arm-leases.test.js | 2 +- .../detect-new-resource-provider.test.js | 30 +++- .../detect-new-resource-types.test.js | 129 ++++++++++-------- 7 files changed, 143 insertions(+), 126 deletions(-) diff --git a/.github/workflows/src/arm-lease-validation/detect-arm-leases.js b/.github/workflows/src/arm-lease-validation/detect-arm-leases.js index 00d52397c442..52170522cf5d 100644 --- a/.github/workflows/src/arm-lease-validation/detect-arm-leases.js +++ b/.github/workflows/src/arm-lease-validation/detect-arm-leases.js @@ -1,8 +1,8 @@ +import { Temporal } from "@js-temporal/polyfill"; import { readFile } from "fs/promises"; -import { resolve } from "path"; import yaml from "js-yaml"; +import { resolve } from "path"; import * as z from "zod"; -import { Temporal } from "@js-temporal/polyfill"; import { getRootFolder } from "../../../shared/src/simple-git.js"; /** @@ -18,17 +18,14 @@ import { getRootFolder } from "../../../shared/src/simple-git.js"; 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)", - ), + 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)"), }), }); diff --git a/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js b/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js index 1c30afd3de46..b37299277490 100644 --- a/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js +++ b/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js @@ -1,21 +1,19 @@ import { simpleGit } from "simple-git"; -import { - getChangedFiles, - resourceManager, - swagger, -} from "../../../shared/src/changed-files.js"; -import { checkLease } from "./detect-arm-leases.js"; -import { detectNewResourceTypes } from "./detect-new-resource-types.js"; +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/([^/]+)/`); +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" @@ -47,14 +45,7 @@ async function resourceProviderExistsInCommit(git, commitish, namespace, core) { /** @type {string} */ let output; try { - output = await git.raw([ - "ls-tree", - "-d", - "--name-only", - "-r", - commitish, - "specification/", - ]); + 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`); @@ -265,12 +256,17 @@ export default async function detectNewResourceProvider({ core }) { // Merge label actions: 'add' wins over 'remove' wins over 'none' /** @type {ManagedLabelActions} */ - const combinedLabelActions = { ...getLabelActions(allLeasesValid ? "auto-signed-off" : "review-required") }; + 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)) { + if ( + action === LabelAction.Add || + (action === LabelAction.Remove && currentAction === LabelAction.None) + ) { combinedLabelActions[label] = action; } } @@ -312,9 +308,7 @@ async function checkNewResourceTypes(rmFiles, core) { 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`, - ); + core.error(`${ns.rpNamespace}: new resource types detected without a valid ARM lease`); } } diff --git a/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js b/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js index cd42183e69bd..15a71f2b2221 100644 --- a/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js +++ b/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js @@ -8,7 +8,9 @@ import { simpleGit } from "simple-git"; // Match pattern: specification//resource-manager//... -const RESOURCE_MANAGER_PATTERN = new RegExp(String.raw`^specification/[^/]+/resource-manager/([^/]+)/`); +const RESOURCE_MANAGER_PATTERN = new RegExp( + String.raw`^specification/[^/]+/resource-manager/([^/]+)/`, +); const RESOURCE_TYPE_REGEX = new RegExp(String.raw`/providers/([^/?#]+)(/[^?#]*)?`); @@ -108,13 +110,7 @@ async function getResourceTypesAtRef(git, gitRef, specPath, branchName) { let output; try { - output = await git.raw([ - "ls-tree", - "-r", - "--name-only", - gitRef, - specPath, - ]); + output = await git.raw(["ls-tree", "-r", "--name-only", gitRef, specPath]); } catch { return null; // path doesn't exist at this ref } @@ -126,7 +122,9 @@ async function getResourceTypesAtRef(git, gitRef, specPath, branchName) { const total = swaggerFiles.length; if (total > 0) { - console.log(`Analyzing ${total} swagger files in ${branchName} branch (${gitRef}) for comparison...`); + console.log( + `Analyzing ${total} swagger files in ${branchName} branch (${gitRef}) for comparison...`, + ); } // Iterate over files, but only log summary if it's too much @@ -175,10 +173,7 @@ async function getResourceTypesAtRef(git, gitRef, specPath, branchName) { * @param {import("@actions/core")} params.core - GitHub Actions core for logging * @returns {Promise}>>} */ -export async function detectNewResourceTypes({ - rmFiles, - core, -}) { +export async function detectNewResourceTypes({ rmFiles, core }) { const git = simpleGit(process.env.GITHUB_WORKSPACE); // Group changed RM swagger files by service subdirectory (rpNamespace + serviceName) @@ -207,7 +202,12 @@ export async function detectNewResourceTypes({ const actualServiceName = isLifecycleFolder ? "" : serviceName; if (!namespaceMap.has(serviceKey)) { - namespaceMap.set(serviceKey, { orgName, specPath, rpNamespace, serviceName: actualServiceName }); + namespaceMap.set(serviceKey, { + orgName, + specPath, + rpNamespace, + serviceName: actualServiceName, + }); } } @@ -216,12 +216,7 @@ export async function detectNewResourceTypes({ 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", - ); + 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) { @@ -229,12 +224,7 @@ export async function detectNewResourceTypes({ continue; } - const headTypes = await getResourceTypesAtRef( - git, - "HEAD", - specPath, - "head", - ); + const headTypes = await getResourceTypesAtRef(git, "HEAD", specPath, "head"); const newTypes = []; /** @type {Map} */ @@ -251,9 +241,7 @@ export async function detectNewResourceTypes({ } if (newTypes.length > 0) { - core.info( - ` ${rpNamespace}: ${newTypes.length} new resource type(s) detected`, - ); + core.info(` ${rpNamespace}: ${newTypes.length} new resource type(s) detected`); for (const t of newTypes) { core.info(` - ${t.resourceType} (${t.operations.join(", ")})`); } diff --git a/.github/workflows/src/summarize-checks/labelling.js b/.github/workflows/src/summarize-checks/labelling.js index b40ccd7a5b2d..566289daaf91 100644 --- a/.github/workflows/src/summarize-checks/labelling.js +++ b/.github/workflows/src/summarize-checks/labelling.js @@ -654,7 +654,8 @@ function processARMReviewWorkflowLabels( // 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 blockedOnArmModeling = + armModelingReviewLabel.present || labelContext.toAdd.has("ARMModelingReviewRequired"); const blocked = blockedOnRpaas || blockedOnVersioningPolicy || blockedOnArmModeling; diff --git a/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js b/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js index 9043b99535c8..d80aa908da82 100644 --- a/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js +++ b/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js @@ -1,5 +1,5 @@ -import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; 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()); diff --git a/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js b/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js index 59c44e17257a..25a73a25268d 100644 --- a/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js +++ b/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js @@ -18,8 +18,8 @@ vi.mock("../../src/arm-lease-validation/detect-new-resource-types.js", () => ({ import * as changedFiles from "../../../shared/src/changed-files.js"; import { checkLease } from "../../src/arm-lease-validation/detect-arm-leases.js"; -import { detectNewResourceTypes } from "../../src/arm-lease-validation/detect-new-resource-types.js"; import detectNewResourceProvider from "../../src/arm-lease-validation/detect-new-resource-provider.js"; +import { detectNewResourceTypes } from "../../src/arm-lease-validation/detect-new-resource-types.js"; const core = createMockCore(); @@ -201,7 +201,19 @@ describe("detectNewResourceProvider", () => { // 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"] }] }, + { + rpNamespace: "Microsoft.Compute", + orgName: "compute", + serviceName: "", + newResourceTypes: [ + { + resourceType: "Microsoft.Compute/disks", + provider: "Microsoft.Compute", + modelName: null, + operations: ["GET"], + }, + ], + }, ]); vi.mocked(checkLease).mockResolvedValue(true); @@ -222,7 +234,19 @@ describe("detectNewResourceProvider", () => { 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"] }] }, + { + rpNamespace: "Microsoft.Compute", + orgName: "compute", + serviceName: "", + newResourceTypes: [ + { + resourceType: "Microsoft.Compute/disks", + provider: "Microsoft.Compute", + modelName: null, + operations: ["GET"], + }, + ], + }, ]); vi.mocked(checkLease).mockResolvedValue(false); diff --git a/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js b/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js index 0c7b0d2b125c..7e8bbd583d0f 100644 --- a/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js +++ b/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js @@ -20,22 +20,25 @@ 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" } } }, - }, + "/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" } } }, - }, + "/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" } } }, + }, }, }); @@ -49,34 +52,38 @@ const vmAndDiskSwagger = JSON.stringify({ * @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"); +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 ""; } - 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); - })); + ), + ); + + 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 ─────────────────────────────────────────────────────────────── @@ -280,19 +287,24 @@ describe("detectNewResourceTypes", () => { 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" } } }, - }, + "/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" } } }, - }, + "/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" } }, + }, + }, }, }); @@ -316,8 +328,8 @@ describe("detectNewResourceTypes", () => { // 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", + const quantumType = result[0].newResourceTypes.find( + (t) => t.resourceType === "Microsoft.Compute/quantumVMs", ); expect(quantumType).toBeDefined(); expect(quantumType?.operations).toContain("GET"); @@ -333,12 +345,14 @@ describe("detectNewResourceTypes", () => { 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" } } }, - }, + "/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" } } }, + }, }, }); @@ -360,14 +374,14 @@ describe("detectNewResourceTypes", () => { 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", + 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", + const metricsType = result[0].newResourceTypes.find( + (t) => t.resourceType === "Microsoft.Compute/superDisks/metrics", ); expect(metricsType).toBeDefined(); }); @@ -382,7 +396,7 @@ describe("detectNewResourceTypes", () => { swagger: "2.0", paths: { "/providers/Microsoft.Compute/operations": { - get: { operationId: "Operations_List", responses: { "200": { description: "OK" } } }, + get: { operationId: "Operations_List", responses: { 200: { description: "OK" } } }, }, }, }); @@ -404,5 +418,4 @@ describe("detectNewResourceTypes", () => { expect(result).toEqual([]); }); - }); From 12aee19ee221427aaefd6540011f86c808498bf0 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Mar 2026 06:38:12 +0000 Subject: [PATCH 078/125] make if-false pass actionlint --- .github/workflows/arm-modeling-review.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/arm-modeling-review.yaml b/.github/workflows/arm-modeling-review.yaml index 130ed7cb4faf..5994ee8951fa 100644 --- a/.github/workflows/arm-modeling-review.yaml +++ b/.github/workflows/arm-modeling-review.yaml @@ -22,8 +22,8 @@ jobs: arm-modeling-review: name: ARM Modeling Review runs-on: ubuntu-slim - # TODO: Set to true to enable new RP detection - if: false + # TODO: Remove to enable new RP detection + if: fromJson('false') steps: - name: Checkout repository From b976f2ce235b7fa430f62a7de417d2e78e0ce54d Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Mar 2026 07:12:20 +0000 Subject: [PATCH 079/125] format --- .github/arm-leases/README.md | 17 +++++++++++------ .../arm-lease-validation.test.js | 4 +--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/arm-leases/README.md b/.github/arm-leases/README.md index c381004ae5db..327e201ec8d3 100644 --- a/.github/arm-leases/README.md +++ b/.github/arm-leases/README.md @@ -19,11 +19,13 @@ This directory is intended to be governed via CODEOWNERS to ensure proper govern ## 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" @@ -34,10 +36,10 @@ 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 + 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 @@ -50,7 +52,6 @@ lease: startdate: duration: P180D reviewer: "@your-github-alias" - ``` ## Validation Rules @@ -58,24 +59,29 @@ lease: 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`) @@ -89,4 +95,3 @@ If your PR check **"ARM Lease Validation"** is failing, review the error message - **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/workflows/test/arm-lease-validation/arm-lease-validation.test.js b/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js index 229c62cae8c1..eadaa613fc41 100644 --- a/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js +++ b/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js @@ -33,9 +33,7 @@ describe("validate-arm-leases", () => { 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/testservice/Microsoft.Test/other.yaml")).toBe(false); expect(isFileAllowed(".github/arm-leases/badtest/No.Yaml/no.md")).toBe(false); }); }); From ec02f7014d1070b6e4bd02f3cd1826b320eb6a6b Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Tue, 17 Mar 2026 13:06:33 -0500 Subject: [PATCH 080/125] Address Mike's review comments: revert package-lock.json and remove sparse-checkout --- .github/workflows/arm-lease-validation.yaml | 3 - package-lock.json | 212 +++++++++++++------- 2 files changed, 143 insertions(+), 72 deletions(-) diff --git a/.github/workflows/arm-lease-validation.yaml b/.github/workflows/arm-lease-validation.yaml index ecd4dceabe13..cbb308665dbc 100644 --- a/.github/workflows/arm-lease-validation.yaml +++ b/.github/workflows/arm-lease-validation.yaml @@ -29,9 +29,6 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 2 - sparse-checkout: | - .github - specification - name: Install dependencies for github-script actions uses: ./.github/actions/install-deps-github-script diff --git a/package-lock.json b/package-lock.json index b4e2af3549d5..8533541c3122 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@azure-tools/typespec-azure-rulesets": "0.65.0", "@azure-tools/typespec-client-generator-cli": "0.31.0", "@azure-tools/typespec-client-generator-core": "0.65.4", - "@azure-tools/typespec-liftr-base": "0.13.0", + "@azure-tools/typespec-liftr-base": "0.12.0", "@azure-tools/typespec-metadata": "0.1.0", "@azure/avocado": "0.10.5", "@azure/oad": "0.12.3", @@ -714,7 +714,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -1080,7 +1079,6 @@ "integrity": "sha512-dYgHtt0CY0Q9AimdIsMV41jHKLmAT4r++TLwyxAHRbxdiRG+Sll1UKJzOIIoq45Bq64wCfEltu5OOnyPA01/sQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" }, @@ -1107,7 +1105,6 @@ "integrity": "sha512-3rvyGDIYSqraZ7jHfq5Bfet8u3ZeERWJWhwWMNvbShnrS/vVR3iuu/1z2M0p5mTRFuwUaSMlL/dbtBp1YqgGAg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "change-case": "~5.4.4", "pluralize": "^8.0.0" @@ -1176,7 +1173,6 @@ "integrity": "sha512-p+MZU/nEmU3ciLEuNbqQtAybPxUKo/fKeKT9feh+tZLVpDDFO5DTefYoN4cteZQkPu/xyzxhjeUnKKvyVQxd6A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "change-case": "~5.4.4", "pluralize": "^8.0.0", @@ -1199,9 +1195,9 @@ } }, "node_modules/@azure-tools/typespec-liftr-base": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@azure-tools/typespec-liftr-base/-/typespec-liftr-base-0.13.0.tgz", - "integrity": "sha512-MF40K/IHwyy3N606DTZSPhFSwA/84ekR9RqvmtJXsXD0SdYcQSoAHOJp4o5qw8OAPSC+aKZ5A+TXRx333V/3PQ==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@azure-tools/typespec-liftr-base/-/typespec-liftr-base-0.12.0.tgz", + "integrity": "sha512-jM5jjhiWUsqRvzYNiDc+7q4UXs02EbA3m8LRaUiLUkF61n7Vzz2Y0Jw2pW88zD4feCyyXoixRjpqvmurIY8K2w==", "dev": true }, "node_modules/@azure-tools/typespec-metadata": { @@ -3885,7 +3881,6 @@ "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", @@ -4462,7 +4457,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4774,7 +4768,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4875,7 +4868,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5042,6 +5034,16 @@ "dev": true, "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/@ts-common/add-position": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/@ts-common/add-position/-/add-position-0.0.2.tgz", @@ -5216,8 +5218,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true, - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/@ts-common/property-set": { "version": "0.1.0", @@ -5434,8 +5435,7 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/lodash": { "version": "4.17.23", @@ -5562,7 +5562,6 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -5793,7 +5792,6 @@ "integrity": "sha512-Rz9fFWQSTJSnhBfZvtA/bDIuO82fknYdtyMsL9lZNJE82rquC6JByHPFsnbGH1VXA0HhMj9L7Oqyp3f0m/BTOA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "~7.28.6", "@inquirer/prompts": "^8.0.1", @@ -5923,7 +5921,6 @@ "integrity": "sha512-41R2jA7k21uMArjyUdvnqYzVnPPaSEcGi40dLMiRVP79m6XgnD3INuTdlMblaS1i+5jJ1BtS1o4QhBBuS/5/qg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" }, @@ -5937,7 +5934,6 @@ "integrity": "sha512-agcwmbB/hK/o9KmM38UB8OGZwLgB17lJ7b4EjqYGpyshqcRMTESMRxnJIH7rRzUq4HJDTqal0tsb8z0K0zXuDg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" }, @@ -5957,7 +5953,6 @@ "integrity": "sha512-5ieXCWRLcyFLv3IFk26ena/RW/NxvT5KiHaoNVFRd79J0XZjFcE0Od6Lxxqj4dWmCo3C8oKtOwFoQuie18G3lQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" }, @@ -6032,7 +6027,6 @@ "integrity": "sha512-6QIX7oaUGy/z4rseUrC86LjHxZn8rAAY4fXvGnlPRce6GhEdTb9S9OQPmlPeWngXwCx/07P2+FCR915APqmZxg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" }, @@ -6047,7 +6041,6 @@ "integrity": "sha512-YQYlDWCNBza75S360jc51emwntWXMZfkvqXKng+etKP4iCuogJfTX1J8h1yd8tZwkuUNBcklEPCuz3O/+psopg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" }, @@ -6064,7 +6057,6 @@ "integrity": "sha512-nOXpLcEYNdWvLY/6WJ16rD6hGs7bKSmkH+WwgyVwdRON5KJ559quw56pns2DSANw+NaV0lJxJq/8ek5xKCGD6g==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" }, @@ -6093,7 +6085,6 @@ "integrity": "sha512-mk65zpKNm+ARyHASnre/lp3o3FKzb0P8Nj96ji182JUy7ShrVCCF0u+bC+ZXQ8ZTRza1d0xBjRC/Xr4iM+Uwag==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" }, @@ -6107,7 +6098,6 @@ "integrity": "sha512-BqbbtkL9xuiAhehHKKUCMtRg0a1vjSvoiAOanvTIuoFq3N8PbKVV3dKTcyI/oS3iCCkJErdu11HQcAoD/VsIsA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" }, @@ -6276,7 +6266,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6553,16 +6542,6 @@ "dev": true, "license": "MIT" }, - "node_modules/basic-ftp": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", - "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/before-after-hook": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", @@ -6977,6 +6956,13 @@ "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==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-env": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", @@ -7011,13 +6997,13 @@ } }, "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14" + "node": ">= 6" } }, "node_modules/data-view-buffer": { @@ -7533,7 +7519,6 @@ "integrity": "sha512-20MV9SUdeN6Jd84xESsKhRly+/vxI+hwvpBMA93s+9dAcjdCuCojn4IqUGS3lvVaqjVYGYHSRMCpeFtF2rQYxQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -7990,6 +7975,16 @@ "node": "*" } }, + "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==", + "dev": true, + "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", @@ -8188,6 +8183,21 @@ "js-yaml": "bin/js-yaml.js" } }, + "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==", + "dev": true, + "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", @@ -8203,6 +8213,19 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/ftp": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha512-faFVML1aBx2UoDStmLwv2Wptt4vw5x03xxX172nhA5Y5HBshW5JweqQ2W4xL4dezQTG8inJsuYcpPHHU3X5OTQ==", + "dev": true, + "dependencies": { + "readable-stream": "1.1.x", + "xregexp": "2.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -8365,18 +8388,21 @@ } }, "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-3.0.2.tgz", + "integrity": "sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg==", "dev": true, "license": "MIT", "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" + "@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": ">= 14" + "node": ">= 6" } }, "node_modules/glob": { @@ -9265,6 +9291,13 @@ "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==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -9337,7 +9370,6 @@ "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 10.16.0" } @@ -9455,6 +9487,16 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonpath-plus": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz", @@ -9785,9 +9827,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -10580,7 +10622,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10705,13 +10746,25 @@ ], "license": "MIT" }, + "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==", + "dev": true, + "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-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "dev": true, - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -11190,9 +11243,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": { @@ -11290,6 +11343,13 @@ "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==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -11502,9 +11562,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": { @@ -11620,7 +11680,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11720,8 +11779,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tunnel": { "version": "0.0.6", @@ -11830,7 +11888,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11943,6 +12000,16 @@ "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==", + "dev": true, + "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", @@ -11993,7 +12060,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -12087,7 +12153,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12101,7 +12166,6 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -12599,6 +12663,16 @@ "node": ">=4.0" } }, + "node_modules/xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", From 48bf3b6d055faddf54c60d04e56a3ed91ac68171 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Tue, 17 Mar 2026 13:18:05 -0500 Subject: [PATCH 081/125] revert --- .github/package-lock.json | 2616 ++++++++++++++++++++++++++++++++++++- 1 file changed, 2577 insertions(+), 39 deletions(-) diff --git a/.github/package-lock.json b/.github/package-lock.json index 21eca00b5ee5..4d9f23e60698 100644 --- a/.github/package-lock.json +++ b/.github/package-lock.json @@ -6,7 +6,9 @@ "": { "dependencies": { "@apidevtools/json-schema-ref-parser": "^15.1.3", - "@js-temporal/polyfill": "0.5.1", + "@js-temporal/polyfill": "^0.5.1", + "@microsoft.azure/openapi-validator-core": "^1.0.7", + "@microsoft.azure/openapi-validator-rulesets": "^2.2.4", "debug": "^4.4.3", "js-yaml": "^4.1.0", "markdown-table": "^3.0.4", @@ -144,9 +146,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" @@ -158,6 +160,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", @@ -936,6 +975,48 @@ "node": ">=12" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/ternary": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/ternary/-/ternary-1.1.4.tgz", + "integrity": "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -951,6 +1032,54 @@ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", "license": "MIT" }, + "node_modules/@microsoft.azure/openapi-validator-core": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@microsoft.azure/openapi-validator-core/-/openapi-validator-core-1.0.7.tgz", + "integrity": "sha512-7hSz1571z2Efx86AnXCuV7jEKw88lKhBWHBdThPBvdGsEEkNTNvXnGMdae/eNRO9a//gqowQPuD0A5oQWimFwQ==", + "license": "MIT", + "dependencies": { + "@azure-tools/uri": "^3.1.1", + "dependency-graph": "^1.0.0", + "jsonc-parser": "^3.2.1", + "jsonpath-plus": "^10.3.0", + "lodash": "^4.17.21", + "tslib": "^2.3.1" + } + }, + "node_modules/@microsoft.azure/openapi-validator-rulesets": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@microsoft.azure/openapi-validator-rulesets/-/openapi-validator-rulesets-2.2.4.tgz", + "integrity": "sha512-a9tik+dkZTQzI8aRUYxRK4Cm8tz2YNB7fRpva1hfAmS9lHS57dBhUm3d9A/B+Wc+R7zwEOIw6WW/Nzte+D0eKg==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.9", + "@microsoft.azure/openapi-validator-core": "^1.0.0", + "@stoplight/json-ref-resolver": "^3.1.6", + "@stoplight/spectral-core": "^1.18.3", + "@stoplight/spectral-formats": "^1.6.0", + "@stoplight/spectral-functions": "^1.7.2", + "@stoplight/types": "^14.1.1", + "jsonpath-plus": "^10.3.0", + "lodash": "^4.17.21", + "string.prototype.matchall": "^4.0.11", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@microsoft.azure/openapi-validator-rulesets/node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, "node_modules/@octokit/auth-token": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", @@ -967,6 +1096,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", @@ -1282,6 +1412,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", @@ -1773,6 +1904,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", @@ -1808,6 +2373,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", @@ -1833,7 +2407,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", @@ -1846,7 +2421,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" @@ -1859,6 +2433,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.56.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", @@ -1904,6 +2484,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -2270,12 +2851,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" }, @@ -2310,15 +2904,91 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "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", + "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/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": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", @@ -2338,11 +3008,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": { @@ -2363,13 +3065,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", @@ -2384,7 +3138,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/cross-env": { @@ -2420,6 +3179,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", @@ -2444,6 +3263,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", @@ -2451,6 +3313,128 @@ "dev": true, "license": "ISC" }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-aggregate-error": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.14.tgz", + "integrity": "sha512-3YxX6rVb07B5TV11AV5wsL7nQCHXNwoHPsQC8S4AmBiqYhyNCJ5BRKXkXyDJvs8QzXN20NgRtxe3dEEQD9NLHA==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "globalthis": "^1.0.4", + "has-property-descriptors": "^1.0.2", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -2458,6 +3442,50 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", @@ -2519,6 +3547,7 @@ "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -2714,6 +3743,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", @@ -2745,7 +3783,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": { @@ -2762,6 +3799,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", @@ -2800,6 +3859,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", @@ -2838,6 +3915,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", @@ -2853,10 +3959,140 @@ "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==", + "node_modules/ftp": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha512-faFVML1aBx2UoDStmLwv2Wptt4vw5x03xxX172nhA5Y5HBshW5JweqQ2W4xL4dezQTG8inJsuYcpPHHU3X5OTQ==", + "dependencies": { + "readable-stream": "1.1.x", + "xregexp": "2.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "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": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.4" + } + }, + "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.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": { + "@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": ">= 6" + } + }, + "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": { @@ -2867,9 +4103,9 @@ } }, "node_modules/globals": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", - "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", + "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", "dev": true, "license": "MIT", "engines": { @@ -2879,6 +4115,52 @@ "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", @@ -2889,6 +4171,72 @@ "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", @@ -2906,6 +4254,16 @@ "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", @@ -2916,6 +4274,138 @@ "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", @@ -2926,6 +4416,40 @@ "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", @@ -2939,6 +4463,188 @@ "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", @@ -3010,6 +4716,16 @@ "integrity": "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==", "license": "Apache-2.0" }, + "node_modules/jsep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 10.16.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -3031,6 +4747,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", @@ -3041,6 +4799,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", @@ -3071,6 +4838,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", @@ -3120,9 +4899,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" @@ -3131,6 +4910,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", @@ -3176,6 +4964,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", @@ -3215,6 +5083,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", @@ -3287,6 +5172,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3294,6 +5180,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.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -3339,6 +5243,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -3366,14 +5271,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/rollup": { @@ -3421,6 +5411,76 @@ "fsevents": "~2.3.2" } }, + "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", @@ -3434,6 +5494,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", @@ -3457,6 +5563,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", @@ -3464,10 +5642,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", @@ -3503,6 +5693,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", @@ -3563,6 +5855,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", @@ -3576,6 +5874,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", @@ -3599,12 +5903,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" @@ -3637,6 +6016,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": "5.29.0", "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", @@ -3654,7 +6051,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": { @@ -3664,6 +6060,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", @@ -3674,12 +6079,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": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -3755,6 +6176,7 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -3834,6 +6256,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", @@ -3850,6 +6288,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", @@ -3884,6 +6413,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", From 8681feb7f02ef443a805522a14a974aaab231bad Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Tue, 17 Mar 2026 13:36:03 -0500 Subject: [PATCH 082/125] Revert From c9caecc8201d21d06da944255674a632eb2ac22e Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Tue, 17 Mar 2026 13:46:04 -0500 Subject: [PATCH 083/125] Revert --- package-lock.json | 173 +++++++++------------------------------------- 1 file changed, 34 insertions(+), 139 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8533541c3122..19e3ca7747ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@azure-tools/typespec-azure-rulesets": "0.65.0", "@azure-tools/typespec-client-generator-cli": "0.31.0", "@azure-tools/typespec-client-generator-core": "0.65.4", - "@azure-tools/typespec-liftr-base": "0.12.0", + "@azure-tools/typespec-liftr-base": "0.13.0", "@azure-tools/typespec-metadata": "0.1.0", "@azure/avocado": "0.10.5", "@azure/oad": "0.12.3", @@ -1195,9 +1195,9 @@ } }, "node_modules/@azure-tools/typespec-liftr-base": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@azure-tools/typespec-liftr-base/-/typespec-liftr-base-0.12.0.tgz", - "integrity": "sha512-jM5jjhiWUsqRvzYNiDc+7q4UXs02EbA3m8LRaUiLUkF61n7Vzz2Y0Jw2pW88zD4feCyyXoixRjpqvmurIY8K2w==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@azure-tools/typespec-liftr-base/-/typespec-liftr-base-0.13.0.tgz", + "integrity": "sha512-MF40K/IHwyy3N606DTZSPhFSwA/84ekR9RqvmtJXsXD0SdYcQSoAHOJp4o5qw8OAPSC+aKZ5A+TXRx333V/3PQ==", "dev": true }, "node_modules/@azure-tools/typespec-metadata": { @@ -5034,16 +5034,6 @@ "dev": true, "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/@ts-common/add-position": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/@ts-common/add-position/-/add-position-0.0.2.tgz", @@ -6542,6 +6532,16 @@ "dev": true, "license": "MIT" }, + "node_modules/basic-ftp": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/before-after-hook": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", @@ -6956,13 +6956,6 @@ "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==", - "dev": true, - "license": "MIT" - }, "node_modules/cross-env": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", @@ -6997,13 +6990,13 @@ } }, "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==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/data-view-buffer": { @@ -7975,16 +7968,6 @@ "node": "*" } }, - "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==", - "dev": true, - "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", @@ -8183,21 +8166,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "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==", - "dev": true, - "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", @@ -8213,19 +8181,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/ftp": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", - "integrity": "sha512-faFVML1aBx2UoDStmLwv2Wptt4vw5x03xxX172nhA5Y5HBshW5JweqQ2W4xL4dezQTG8inJsuYcpPHHU3X5OTQ==", - "dev": true, - "dependencies": { - "readable-stream": "1.1.x", - "xregexp": "2.0.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -8388,21 +8343,18 @@ } }, "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==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", "dev": true, "license": "MIT", "dependencies": { - "@tootallnate/once": "1", - "data-uri-to-buffer": "3", - "debug": "4", - "file-uri-to-path": "2", - "fs-extra": "^8.1.0", - "ftp": "^0.3.10" + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/glob": { @@ -9291,13 +9243,6 @@ "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==", - "dev": true, - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -9487,16 +9432,6 @@ "dev": true, "license": "MIT" }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/jsonpath-plus": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz", @@ -9827,9 +9762,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -10746,19 +10681,6 @@ ], "license": "MIT" }, - "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==", - "dev": true, - "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-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -11243,9 +11165,9 @@ } }, "node_modules/simple-git": { - "version": "3.30.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.30.0.tgz", - "integrity": "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==", + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz", + "integrity": "sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng==", "dev": true, "license": "MIT", "dependencies": { @@ -11343,13 +11265,6 @@ "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==", - "dev": true, - "license": "MIT" - }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -11562,9 +11477,9 @@ "license": "MIT" }, "node_modules/tar": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", - "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", + "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -12000,16 +11915,6 @@ "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==", - "dev": true, - "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", @@ -12663,16 +12568,6 @@ "node": ">=4.0" } }, - "node_modules/xregexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", - "integrity": "sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", From 504adc25c49f28e05a6307ffc9e309663e00b9df Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Tue, 17 Mar 2026 14:32:02 -0500 Subject: [PATCH 084/125] Updated and aligned with this name arm-modeling-review --- .github/workflows/arm-modeling-review.yaml | 2 +- .../arm-lease-validation-labels.js | 0 .../arm-modeling-review.js} | 2 +- .../detect-arm-leases.js | 0 .../detect-new-resource-types.js | 0 .../arm-modeling-review.test.js} | 36 +++++++++---------- .../detect-arm-leases.test.js | 2 +- .../detect-new-resource-types.test.js | 2 +- 8 files changed, 22 insertions(+), 22 deletions(-) rename .github/workflows/src/{arm-lease-validation => arm-modeling-review}/arm-lease-validation-labels.js (100%) rename .github/workflows/src/{arm-lease-validation/detect-new-resource-provider.js => arm-modeling-review/arm-modeling-review.js} (99%) rename .github/workflows/src/{arm-lease-validation => arm-modeling-review}/detect-arm-leases.js (100%) rename .github/workflows/src/{arm-lease-validation => arm-modeling-review}/detect-new-resource-types.js (100%) rename .github/workflows/test/{arm-lease-validation/detect-new-resource-provider.test.js => arm-modeling-review/arm-modeling-review.test.js} (89%) rename .github/workflows/test/{arm-lease-validation => arm-modeling-review}/detect-arm-leases.test.js (98%) rename .github/workflows/test/{arm-lease-validation => arm-modeling-review}/detect-new-resource-types.test.js (99%) diff --git a/.github/workflows/arm-modeling-review.yaml b/.github/workflows/arm-modeling-review.yaml index 5994ee8951fa..ef7f87070479 100644 --- a/.github/workflows/arm-modeling-review.yaml +++ b/.github/workflows/arm-modeling-review.yaml @@ -40,7 +40,7 @@ jobs: with: script: | const { default: armModelingReview } = - await import('${{ github.workspace }}/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js'); + 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 diff --git a/.github/workflows/src/arm-lease-validation/arm-lease-validation-labels.js b/.github/workflows/src/arm-modeling-review/arm-lease-validation-labels.js similarity index 100% rename from .github/workflows/src/arm-lease-validation/arm-lease-validation-labels.js rename to .github/workflows/src/arm-modeling-review/arm-lease-validation-labels.js diff --git a/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js b/.github/workflows/src/arm-modeling-review/arm-modeling-review.js similarity index 99% rename from .github/workflows/src/arm-lease-validation/detect-new-resource-provider.js rename to .github/workflows/src/arm-modeling-review/arm-modeling-review.js index b37299277490..89ba1b3124a2 100644 --- a/.github/workflows/src/arm-lease-validation/detect-new-resource-provider.js +++ b/.github/workflows/src/arm-modeling-review/arm-modeling-review.js @@ -126,7 +126,7 @@ function extractResourceProviders(files) { * @param {import('@actions/github-script').AsyncFunctionArguments['core']} params.core * @returns {Promise<{ status: string, labelActions: ManagedLabelActions }>} */ -export default async function detectNewResourceProvider({ core }) { +export default async function armModelingReview({ core }) { const options = { cwd: process.env.GITHUB_WORKSPACE, paths: ["specification"], diff --git a/.github/workflows/src/arm-lease-validation/detect-arm-leases.js b/.github/workflows/src/arm-modeling-review/detect-arm-leases.js similarity index 100% rename from .github/workflows/src/arm-lease-validation/detect-arm-leases.js rename to .github/workflows/src/arm-modeling-review/detect-arm-leases.js diff --git a/.github/workflows/src/arm-lease-validation/detect-new-resource-types.js b/.github/workflows/src/arm-modeling-review/detect-new-resource-types.js similarity index 100% rename from .github/workflows/src/arm-lease-validation/detect-new-resource-types.js rename to .github/workflows/src/arm-modeling-review/detect-new-resource-types.js diff --git a/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js b/.github/workflows/test/arm-modeling-review/arm-modeling-review.test.js similarity index 89% rename from .github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js rename to .github/workflows/test/arm-modeling-review/arm-modeling-review.test.js index 25a73a25268d..ed6bdae1aeda 100644 --- a/.github/workflows/test/arm-lease-validation/detect-new-resource-provider.test.js +++ b/.github/workflows/test/arm-modeling-review/arm-modeling-review.test.js @@ -8,22 +8,22 @@ vi.mock("simple-git", () => ({ simpleGit: vi.fn().mockReturnValue({ raw: mockRaw }), })); -vi.mock("../../src/arm-lease-validation/detect-arm-leases.js", () => ({ +vi.mock("../../src/arm-modeling-review/detect-arm-leases.js", () => ({ checkLease: vi.fn().mockResolvedValue(false), })); -vi.mock("../../src/arm-lease-validation/detect-new-resource-types.js", () => ({ +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-lease-validation/detect-arm-leases.js"; -import detectNewResourceProvider from "../../src/arm-lease-validation/detect-new-resource-provider.js"; -import { detectNewResourceTypes } from "../../src/arm-lease-validation/detect-new-resource-types.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("detectNewResourceProvider", () => { +describe("armModelingReview", () => { afterEach(() => { vi.clearAllMocks(); delete process.env.GITHUB_WORKSPACE; @@ -39,7 +39,7 @@ describe("detectNewResourceProvider", () => { // Pre-check: file exists in base → not brand new vi.mocked(mockRaw).mockResolvedValue(rmFile); - const result = await detectNewResourceProvider({ core }); + const result = await armModelingReview({ core }); expect(result.status).toBe("no-new-rp"); expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); @@ -57,7 +57,7 @@ describe("detectNewResourceProvider", () => { vi.mocked(mockRaw).mockResolvedValue(""); vi.mocked(checkLease).mockResolvedValue(true); - const result = await detectNewResourceProvider({ core }); + const result = await armModelingReview({ core }); expect(result.status).toBe("new-rp-all-leases-valid"); expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); @@ -76,7 +76,7 @@ describe("detectNewResourceProvider", () => { vi.mocked(mockRaw).mockResolvedValue(""); vi.mocked(checkLease).mockResolvedValue(false); - const result = await detectNewResourceProvider({ core }); + const result = await armModelingReview({ core }); expect(result.status).toBe("new-rp-invalid-lease"); expect(result.labelActions.ARMModelingReviewRequired).toBe("add"); @@ -100,7 +100,7 @@ describe("detectNewResourceProvider", () => { // SvcA valid, SvcB invalid vi.mocked(checkLease).mockImplementation((orgName) => Promise.resolve(orgName === "svcA")); - const result = await detectNewResourceProvider({ core }); + const result = await armModelingReview({ core }); expect(result.status).toBe("new-rp-invalid-lease"); expect(result.labelActions.ARMModelingReviewRequired).toBe("add"); @@ -120,7 +120,7 @@ describe("detectNewResourceProvider", () => { vi.mocked(mockRaw).mockResolvedValue(""); vi.mocked(checkLease).mockResolvedValue(true); - const result = await detectNewResourceProvider({ core }); + const result = await armModelingReview({ core }); expect(result.status).toBe("new-rp-all-leases-valid"); expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); @@ -137,7 +137,7 @@ describe("detectNewResourceProvider", () => { vi.mocked(mockRaw).mockResolvedValue(""); vi.mocked(checkLease).mockResolvedValue(true); - await detectNewResourceProvider({ core }); + await armModelingReview({ core }); expect(checkLease).toHaveBeenCalledWith("svc", "Microsoft.Svc", "ComputeRP"); }); @@ -150,7 +150,7 @@ describe("detectNewResourceProvider", () => { vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([rmFile]); vi.mocked(mockRaw).mockResolvedValue(rmFile); - const result = await detectNewResourceProvider({ core }); + const result = await armModelingReview({ core }); expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); expect(result.labelActions.ARMModelingSignedOff).toBe("remove"); @@ -166,7 +166,7 @@ describe("detectNewResourceProvider", () => { vi.mocked(mockRaw).mockResolvedValue(""); vi.mocked(checkLease).mockResolvedValue(false); - const result = await detectNewResourceProvider({ core }); + const result = await armModelingReview({ core }); expect(result.labelActions.ARMModelingReviewRequired).toBe("add"); expect(result.labelActions.ARMModelingSignedOff).toBe("remove"); @@ -182,7 +182,7 @@ describe("detectNewResourceProvider", () => { vi.mocked(mockRaw).mockResolvedValue(""); vi.mocked(checkLease).mockResolvedValue(true); - const result = await detectNewResourceProvider({ core }); + const result = await armModelingReview({ core }); expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); expect(result.labelActions.ARMModelingSignedOff).toBe("remove"); @@ -217,7 +217,7 @@ describe("detectNewResourceProvider", () => { ]); vi.mocked(checkLease).mockResolvedValue(true); - const result = await detectNewResourceProvider({ core }); + const result = await armModelingReview({ core }); expect(result.status).toBe("new-rt-all-leases-valid"); expect(result.labelActions.ARMModelingAutoSignedOff).toBe("add"); @@ -250,7 +250,7 @@ describe("detectNewResourceProvider", () => { ]); vi.mocked(checkLease).mockResolvedValue(false); - const result = await detectNewResourceProvider({ core }); + const result = await armModelingReview({ core }); expect(result.status).toBe("new-rt-invalid-lease"); expect(result.labelActions.ARMModelingReviewRequired).toBe("add"); @@ -267,7 +267,7 @@ describe("detectNewResourceProvider", () => { vi.mocked(mockRaw).mockResolvedValue(rmFile); vi.mocked(detectNewResourceTypes).mockResolvedValue([]); - const result = await detectNewResourceProvider({ core }); + const result = await armModelingReview({ core }); expect(result.status).toBe("no-new-rp"); expect(result.labelActions.ARMModelingReviewRequired).toBe("remove"); diff --git a/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js b/.github/workflows/test/arm-modeling-review/detect-arm-leases.test.js similarity index 98% rename from .github/workflows/test/arm-lease-validation/detect-arm-leases.test.js rename to .github/workflows/test/arm-modeling-review/detect-arm-leases.test.js index d80aa908da82..a7727841aa0b 100644 --- a/.github/workflows/test/arm-lease-validation/detect-arm-leases.test.js +++ b/.github/workflows/test/arm-modeling-review/detect-arm-leases.test.js @@ -15,7 +15,7 @@ vi.mock("../../../shared/src/simple-git.js", () => ({ getRootFolder: mockGetRootFolder, })); -import { checkLease, parseLease } from "../../src/arm-lease-validation/detect-arm-leases.js"; +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"); diff --git a/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js b/.github/workflows/test/arm-modeling-review/detect-new-resource-types.test.js similarity index 99% rename from .github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js rename to .github/workflows/test/arm-modeling-review/detect-new-resource-types.test.js index 7e8bbd583d0f..5de3b17c019e 100644 --- a/.github/workflows/test/arm-lease-validation/detect-new-resource-types.test.js +++ b/.github/workflows/test/arm-modeling-review/detect-new-resource-types.test.js @@ -11,7 +11,7 @@ vi.mock("simple-git", () => ({ simpleGit: vi.fn().mockReturnValue({ raw: mockRaw, show: mockShow }), })); -import { detectNewResourceTypes } from "../../src/arm-lease-validation/detect-new-resource-types.js"; +import { detectNewResourceTypes } from "../../src/arm-modeling-review/detect-new-resource-types.js"; const core = createMockCore(); From cfc0a920dd1f3f7de87fd6a5818a484c4fb454fe Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Mar 2026 15:50:07 -0700 Subject: [PATCH 085/125] Add arm-leases to ignorePaths in cspell config --- .github/cspell.yaml | 1 + 1 file changed, 1 insertion(+) 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 From b4d8a83884c9470d449c5c608ab69a0432eeefad Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Wed, 18 Mar 2026 13:18:07 -0500 Subject: [PATCH 086/125] Merge conflict - package-lock --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 19e3ca7747ce..37ecebb1eb94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11165,9 +11165,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": { @@ -11477,9 +11477,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": { From 6fc8869933d8ee9b61f54608e1bec99d12e66147 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 21:38:13 +0000 Subject: [PATCH 087/125] Initial plan From 02a916d90a83473a5f2118bdf37dd038547c7003 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 21:42:07 +0000 Subject: [PATCH 088/125] Enable ARM Modeling Review workflow by removing disabled condition Co-authored-by: tejaswiMinnu <14865963+tejaswiMinnu@users.noreply.github.com> --- .github/workflows/arm-modeling-review.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/arm-modeling-review.yaml b/.github/workflows/arm-modeling-review.yaml index ef7f87070479..441c9bcda24c 100644 --- a/.github/workflows/arm-modeling-review.yaml +++ b/.github/workflows/arm-modeling-review.yaml @@ -22,9 +22,6 @@ jobs: arm-modeling-review: name: ARM Modeling Review runs-on: ubuntu-slim - # TODO: Remove to enable new RP detection - if: fromJson('false') - steps: - name: Checkout repository uses: actions/checkout@v6 From e723370f60af49c975623f0366cda4b68a8a81f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:39:33 +0000 Subject: [PATCH 089/125] Initial plan From 5891061277a7580c228d8d82e516cfef37e69821 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:45:20 +0000 Subject: [PATCH 090/125] Add ARM Modeling Review to update-labels.yaml workflow triggers Co-authored-by: tejaswiMinnu <14865963+tejaswiMinnu@users.noreply.github.com> --- .github/workflows/update-labels.yaml | 1 + 1 file changed, 1 insertion(+) 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", From 6256554b618994fed85cf750c832343bc8c4b792 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:38:57 +0000 Subject: [PATCH 091/125] Initial plan From a049a0afb96c365b4472f55e47af5cb9d08d6538 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:45:11 +0000 Subject: [PATCH 092/125] Fix ARM Lease Validation to allow new RPs without existing specification folder Co-authored-by: tejaswiMinnu <14865963+tejaswiMinnu@users.noreply.github.com> --- .../src/arm-lease-validation/arm-lease-validation.js | 5 +---- .../arm-lease-validation/arm-lease-validation.test.js | 8 ++++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/src/arm-lease-validation/arm-lease-validation.js b/.github/workflows/src/arm-lease-validation/arm-lease-validation.js index ed73b97844b7..224462687c2a 100644 --- a/.github/workflows/src/arm-lease-validation/arm-lease-validation.js +++ b/.github/workflows/src/arm-lease-validation/arm-lease-validation.js @@ -188,10 +188,7 @@ export async function validateLeaseContent(leaseFile, relativePath, workspaceRoo serviceExists = true; } } catch { - // Service folder doesn't exist - errors.push( - `Service folder not found: specification/${orgName}. The orgName in the lease path must match an existing service folder in specification/.`, - ); + // 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) 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 index eadaa613fc41..b38895e0292b 100644 --- a/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js +++ b/.github/workflows/test/arm-lease-validation/arm-lease-validation.test.js @@ -193,16 +193,16 @@ describe("validate-arm-leases", () => { expect(mockStat).toHaveBeenCalledTimes(2); }); - it("fails when service folder does not exist", async () => { + 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/invalidservice/Microsoft.Test/lease.yaml", - ".github/arm-leases/invalidservice/Microsoft.Test/lease.yaml", + "/repo/.github/arm-leases/newservice/Microsoft.Test/lease.yaml", + ".github/arm-leases/newservice/Microsoft.Test/lease.yaml", "/repo", ); - expect(result.errors.some((e) => e.includes("Service folder not found"))).toBe(true); + expect(result.errors).toHaveLength(0); }); it("allows new RP when service exists but RP folder does not", async () => { From 687815057dda805e8771feb9af58fe91b222f60c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:24:42 +0000 Subject: [PATCH 093/125] Initial plan From c19e1a36e44cc6024626c049e3f235ccbb8a59fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:33:28 +0000 Subject: [PATCH 094/125] Add ARM lease file for new Microsoft.XYZ RP Co-authored-by: tejaswiMinnu <14865963+tejaswiMinnu@users.noreply.github.com> --- .github/arm-leases/xyz/Microsoft.XYZ/XYZ/lease.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/arm-leases/xyz/Microsoft.XYZ/XYZ/lease.yaml 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" From 5590c05e42cfaa71da3a5ad26646b3d19484215b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:00:22 +0000 Subject: [PATCH 095/125] Initial plan From 8afeedec026594cae1830561e378bdfd425964b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:18:05 +0000 Subject: [PATCH 096/125] Fix: ensure ARMModelingSignedOff label exists via workflow step in update-labels.yaml Co-authored-by: tejaswiMinnu <14865963+tejaswiMinnu@users.noreply.github.com> --- .github/workflows/update-labels.yaml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/update-labels.yaml b/.github/workflows/update-labels.yaml index e615e3eb00e1..6d5bfaab6516 100644 --- a/.github/workflows/update-labels.yaml +++ b/.github/workflows/update-labels.yaml @@ -20,6 +20,7 @@ on: permissions: actions: read contents: read + issues: write pull-requests: write jobs: @@ -43,6 +44,33 @@ jobs: await import('${{ github.workspace }}/.github/workflows/src/update-labels.js'); await updateLabels({ github, context, core }); + - name: Ensure ARM Modeling labels exist + if: github.event.workflow_run.name == 'ARM Modeling Review' + uses: actions/github-script@v8 + with: + script: | + const labels = [ + "ARMModelingReviewRequired", + "ARMModelingSignedOff", + "ARMModelingAutoSignedOff", + ]; + for (const name of labels) { + try { + // Check whether the label already exists; result is intentionally unused. + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name }); + core.info(`Label '${name}' already exists`); + } catch (error) { + if (error.status === 404) { + // Use GitHub's default "light gray" color so the label is neutral and + // distinguishable from labels that carry semantic color coding. + await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name, color: "ededed" }); + core.info(`Created label '${name}'`); + } else { + throw error; + } + } + } + - if: ${{ always() && steps.update-labels.outputs.head_sha }} name: Upload artifact with head SHA uses: ./.github/actions/add-empty-artifact From 3f7d8ae943bd3aa52590f465514909ee2fa759dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:33:24 +0000 Subject: [PATCH 097/125] Fix: trigger ARM Modeling Review workflow when ARMModelingSignedOff label is added or removed Co-authored-by: tejaswiMinnu <14865963+tejaswiMinnu@users.noreply.github.com> --- .github/workflows/arm-modeling-review.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/arm-modeling-review.yaml b/.github/workflows/arm-modeling-review.yaml index 441c9bcda24c..afc71456a200 100644 --- a/.github/workflows/arm-modeling-review.yaml +++ b/.github/workflows/arm-modeling-review.yaml @@ -12,6 +12,9 @@ on: - 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/**" @@ -21,6 +24,12 @@ permissions: 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 From f7cdb9823c68a7cc1a806aa437d8ad1e87438b47 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:48:16 +0000 Subject: [PATCH 098/125] Revert update-labels.yaml to original state Co-authored-by: tejaswiMinnu <14865963+tejaswiMinnu@users.noreply.github.com> --- .github/workflows/update-labels.yaml | 29 ---------------------------- 1 file changed, 29 deletions(-) diff --git a/.github/workflows/update-labels.yaml b/.github/workflows/update-labels.yaml index 6d5bfaab6516..2cb27384a79b 100644 --- a/.github/workflows/update-labels.yaml +++ b/.github/workflows/update-labels.yaml @@ -9,7 +9,6 @@ on: workflows: [ "ARM Auto SignOff - Set Status", - "ARM Modeling Review", "SDK Breaking Change Labels", "SDK Suppressions", "TypeSpec Requirement", @@ -20,7 +19,6 @@ on: permissions: actions: read contents: read - issues: write pull-requests: write jobs: @@ -44,33 +42,6 @@ jobs: await import('${{ github.workspace }}/.github/workflows/src/update-labels.js'); await updateLabels({ github, context, core }); - - name: Ensure ARM Modeling labels exist - if: github.event.workflow_run.name == 'ARM Modeling Review' - uses: actions/github-script@v8 - with: - script: | - const labels = [ - "ARMModelingReviewRequired", - "ARMModelingSignedOff", - "ARMModelingAutoSignedOff", - ]; - for (const name of labels) { - try { - // Check whether the label already exists; result is intentionally unused. - await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name }); - core.info(`Label '${name}' already exists`); - } catch (error) { - if (error.status === 404) { - // Use GitHub's default "light gray" color so the label is neutral and - // distinguishable from labels that carry semantic color coding. - await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name, color: "ededed" }); - core.info(`Created label '${name}'`); - } else { - throw error; - } - } - } - - if: ${{ always() && steps.update-labels.outputs.head_sha }} name: Upload artifact with head SHA uses: ./.github/actions/add-empty-artifact From 436afe5f57c0f2a8cf570fe2c90ad4dc675a900f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:56:02 +0000 Subject: [PATCH 099/125] Restore update-labels.yaml to match origin/main (no diff) Co-authored-by: tejaswiMinnu <14865963+tejaswiMinnu@users.noreply.github.com> --- .github/workflows/update-labels.yaml | 1 + 1 file changed, 1 insertion(+) 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", From 283cf521b44c910f7bd91415a3c309697a1d983b Mon Sep 17 00:00:00 2001 From: Vikeshi Tiwari Date: Thu, 19 Mar 2026 15:43:21 -0400 Subject: [PATCH 100/125] Add ARM lease for Microsoft.Barbie RP --- .github/arm-leases/barbie/Microsoft.Barbie/lease.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/arm-leases/barbie/Microsoft.Barbie/lease.yaml diff --git a/.github/arm-leases/barbie/Microsoft.Barbie/lease.yaml b/.github/arm-leases/barbie/Microsoft.Barbie/lease.yaml new file mode 100644 index 000000000000..bc6272e57a85 --- /dev/null +++ b/.github/arm-leases/barbie/Microsoft.Barbie/lease.yaml @@ -0,0 +1,5 @@ +lease: + resource-provider: Microsoft.Barbie + startdate: 2026-03-19 + duration: P180D + reviewer: "@vikeshi26" From 9403df16f31665658280511a518fd6be3ff5a1db Mon Sep 17 00:00:00 2001 From: Vikeshi Tiwari Date: Thu, 19 Mar 2026 16:07:37 -0400 Subject: [PATCH 101/125] Fix: move Barbie lease to correct path with service name --- .../arm-leases/barbie/Microsoft.Barbie/{ => Barbie}/lease.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/arm-leases/barbie/Microsoft.Barbie/{ => Barbie}/lease.yaml (100%) diff --git a/.github/arm-leases/barbie/Microsoft.Barbie/lease.yaml b/.github/arm-leases/barbie/Microsoft.Barbie/Barbie/lease.yaml similarity index 100% rename from .github/arm-leases/barbie/Microsoft.Barbie/lease.yaml rename to .github/arm-leases/barbie/Microsoft.Barbie/Barbie/lease.yaml From b13b198096bb5949752372d20d7cee6d66102f42 Mon Sep 17 00:00:00 2001 From: Vikeshi Tiwari Date: Thu, 19 Mar 2026 17:00:53 -0400 Subject: [PATCH 102/125] Add ARM lease for Microsoft.Batman --- .github/arm-leases/batman/Microsoft.Batman/Batman/lease.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/arm-leases/batman/Microsoft.Batman/Batman/lease.yaml 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 From 2823d2dbe8abff706478dbc94ab6c106e6c630de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:15:34 +0000 Subject: [PATCH 103/125] Initial plan From f965d462f40d24ba25743b6d02dd4b448e9bebfa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:25:28 +0000 Subject: [PATCH 104/125] fix(arm-modeling-review): always read lease from HEAD^ via git show, not as fallback Co-authored-by: tejaswiMinnu <14865963+tejaswiMinnu@users.noreply.github.com> --- .../arm-modeling-review/detect-arm-leases.js | 27 ++++++------ .../detect-arm-leases.test.js | 42 ++++++++++++++----- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/.github/workflows/src/arm-modeling-review/detect-arm-leases.js b/.github/workflows/src/arm-modeling-review/detect-arm-leases.js index 52170522cf5d..a2821d7d7448 100644 --- a/.github/workflows/src/arm-modeling-review/detect-arm-leases.js +++ b/.github/workflows/src/arm-modeling-review/detect-arm-leases.js @@ -1,7 +1,6 @@ import { Temporal } from "@js-temporal/polyfill"; -import { readFile } from "fs/promises"; import yaml from "js-yaml"; -import { resolve } from "path"; +import { simpleGit } from "simple-git"; import * as z from "zod"; import { getRootFolder } from "../../../shared/src/simple-git.js"; @@ -30,25 +29,24 @@ const leaseSchema = z.object({ }); /** - * Build the lease path based on service information. + * Build the relative 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 + * @returns {string} Relative path to lease.yaml file (e.g., ".github/arm-leases/compute/Microsoft.Compute/lease.yaml") */ -function buildLeasePath(repoRoot, orgName, rpNamespace, serviceName = "") { - const leasePathParts = [repoRoot, ".github", "arm-leases", orgName, rpNamespace]; +function buildLeaseRelativePath(orgName, rpNamespace, serviceName = "") { + const parts = [".github", "arm-leases", orgName, rpNamespace]; if (serviceName) { - leasePathParts.push(serviceName); + parts.push(serviceName); } - leasePathParts.push("lease.yaml"); - return resolve(...leasePathParts); + parts.push("lease.yaml"); + return parts.join("/"); } /** @@ -90,7 +88,9 @@ export function parseLease(content) { /** * Check if ARM lease exists and is valid. * - * Looks for a lease file at the appropriate path (see buildLeasePath for path structure). + * Reads the lease file directly from HEAD^ (the base-branch parent of the merge commit) + * via git show. This is the same pattern used by trivial-changes-check.js and + * arm-incremental-typespec.js and requires no extra git fetch or GitHub API calls. * * @param {string} orgName - Organization name (e.g., "compute") * @param {string} rpNamespace - Resource provider namespace (e.g., "Microsoft.Compute") @@ -99,11 +99,12 @@ export function parseLease(content) { */ export async function checkLease(orgName, rpNamespace, serviceName = "") { const repoRoot = await getRootFolder(process.cwd()); - const leasePath = buildLeasePath(repoRoot, orgName, rpNamespace, serviceName); + const relLeasePath = buildLeaseRelativePath(orgName, rpNamespace, serviceName); let content; try { - content = await readFile(leasePath, "utf-8"); + const git = simpleGit(repoRoot); + content = await git.show([`HEAD^:${relLeasePath}`]); } catch { return false; } 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 index a7727841aa0b..ffc4dea2e1cf 100644 --- a/.github/workflows/test/arm-modeling-review/detect-arm-leases.test.js +++ b/.github/workflows/test/arm-modeling-review/detect-arm-leases.test.js @@ -1,11 +1,11 @@ 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()); +/** @type {{ show: import("vitest").MockedFunction<() => Promise> }} */ +const mockGitInstance = vi.hoisted(() => ({ show: vi.fn() })); -vi.mock("fs/promises", () => ({ - readFile: mockReadFile, +vi.mock("simple-git", () => ({ + simpleGit: vi.fn(() => mockGitInstance), })); /** @type {import("vitest").MockedFunction} */ @@ -121,44 +121,66 @@ describe("detect-arm-leases", () => { describe("checkLease", () => { it("returns false when lease file does not exist", async () => { - mockReadFile.mockRejectedValue(new Error("ENOENT")); + mockGitInstance.show.mockRejectedValue(new Error("does not exist in HEAD^")); 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")); + mockGitInstance.show.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")); + mockGitInstance.show.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"); + mockGitInstance.show.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")); + mockGitInstance.show.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")); + mockGitInstance.show.mockRejectedValue(new Error("does not exist in HEAD^")); expect(await checkLease("storage", "Microsoft.Storage")).toBe(false); }); + + it("reads lease from HEAD^ with correct path (with serviceName)", async () => { + mockGitInstance.show.mockResolvedValue(leaseYaml(daysAgo(30), "P90D")); + + const result = await checkLease("xyz", "Microsoft.XYZ", "XYZ"); + + expect(result).toBe(true); + expect(mockGitInstance.show).toHaveBeenCalledWith([ + "HEAD^:.github/arm-leases/xyz/Microsoft.XYZ/XYZ/lease.yaml", + ]); + }); + + it("reads lease from HEAD^ with correct path (without serviceName)", async () => { + mockGitInstance.show.mockResolvedValue(leaseYaml(daysAgo(30), "P90D")); + + const result = await checkLease("xyz", "Microsoft.XYZ"); + + expect(result).toBe(true); + expect(mockGitInstance.show).toHaveBeenCalledWith([ + "HEAD^:.github/arm-leases/xyz/Microsoft.XYZ/lease.yaml", + ]); + }); }); }); From afb676b1ed7c730475150d2baf3e249bea5952d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:55:13 +0000 Subject: [PATCH 105/125] Initial plan From f9e0452f55d6c0abdd331ba8198496e75b4117ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:57:33 +0000 Subject: [PATCH 106/125] Add lease file for Microsoft.XYZ/XYZAnalytics Co-authored-by: tejaswiMinnu <14865963+tejaswiMinnu@users.noreply.github.com> --- .github/arm-leases/xyz/Microsoft.XYZ/XYZAnalytics/lease.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/arm-leases/xyz/Microsoft.XYZ/XYZAnalytics/lease.yaml diff --git a/.github/arm-leases/xyz/Microsoft.XYZ/XYZAnalytics/lease.yaml b/.github/arm-leases/xyz/Microsoft.XYZ/XYZAnalytics/lease.yaml new file mode 100644 index 000000000000..b0e77d307b75 --- /dev/null +++ b/.github/arm-leases/xyz/Microsoft.XYZ/XYZAnalytics/lease.yaml @@ -0,0 +1,5 @@ +lease: + resource-provider: Microsoft.XYZ + startdate: "2026-03-20" + duration: P180D + reviewer: "@tejaswiMinnu" From fb079ba1b91d68c738a1a4ae4e65b4b776c94406 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:39:27 +0000 Subject: [PATCH 107/125] Initial plan From c74d5a6327daed8a5476bd3f1545e98e3d0381cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:43:11 +0000 Subject: [PATCH 108/125] Add lease for XYZInsights service under XYZ RP Co-authored-by: tejaswiMinnu <14865963+tejaswiMinnu@users.noreply.github.com> Agent-Logs-Url: https://github.com/tejaswiMinnu/azure-rest-api-specs/sessions/99dad3f3-268c-48a7-a31b-2bdade222ad4 --- .github/arm-leases/xyz/Microsoft.XYZ/XYZInsights/lease.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/arm-leases/xyz/Microsoft.XYZ/XYZInsights/lease.yaml diff --git a/.github/arm-leases/xyz/Microsoft.XYZ/XYZInsights/lease.yaml b/.github/arm-leases/xyz/Microsoft.XYZ/XYZInsights/lease.yaml new file mode 100644 index 000000000000..b0e77d307b75 --- /dev/null +++ b/.github/arm-leases/xyz/Microsoft.XYZ/XYZInsights/lease.yaml @@ -0,0 +1,5 @@ +lease: + resource-provider: Microsoft.XYZ + startdate: "2026-03-20" + duration: P180D + reviewer: "@tejaswiMinnu" From 46beb684e51b9c228fc5a61ed631ca59ece17d30 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:03:33 +0000 Subject: [PATCH 109/125] Initial plan From 21f94f67c439ca79e02f9474e92a25b69f4d6ce7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:19:24 +0000 Subject: [PATCH 110/125] Fix ARM Modeling Review failure when lease is merged after PR's merge commit is generated The checkLease function was reading the lease file from HEAD^ (the base branch parent of the merge commit). This failed when: 1. A spec PR is pushed (generating a merge commit based on main at that point) 2. The lease PR is merged to main AFTER the merge commit was generated 3. The CI runs - HEAD^ still points to the old main WITHOUT the lease file Fix: Add fallback to origin/ in checkLease, which reflects the current state of the base branch. The workflow also now fetches the base branch to ensure origin/ is available for this fallback check. Co-authored-by: tejaswiMinnu <14865963+tejaswiMinnu@users.noreply.github.com> Agent-Logs-Url: https://github.com/tejaswiMinnu/azure-rest-api-specs/sessions/ed82f09d-11d5-46f9-a81b-674badd9b966 --- .github/workflows/arm-modeling-review.yaml | 5 ++ .../arm-modeling-review/detect-arm-leases.js | 26 ++++++- .../detect-arm-leases.test.js | 77 ++++++++++++++++++- 3 files changed, 104 insertions(+), 4 deletions(-) diff --git a/.github/workflows/arm-modeling-review.yaml b/.github/workflows/arm-modeling-review.yaml index afc71456a200..848f1fcd22c1 100644 --- a/.github/workflows/arm-modeling-review.yaml +++ b/.github/workflows/arm-modeling-review.yaml @@ -37,6 +37,11 @@ jobs: with: fetch-depth: 2 + - name: Fetch latest base branch for lease verification + # Ensures origin/ is available so checkLease can fall back to it when the + # PR's merge commit is stale (i.e., the lease was merged after the last PR push). + run: git fetch origin ${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }} --depth=1 + - name: Install dependencies for github-script actions uses: ./.github/actions/install-deps-github-script diff --git a/.github/workflows/src/arm-modeling-review/detect-arm-leases.js b/.github/workflows/src/arm-modeling-review/detect-arm-leases.js index a2821d7d7448..bff0e71fdc4f 100644 --- a/.github/workflows/src/arm-modeling-review/detect-arm-leases.js +++ b/.github/workflows/src/arm-modeling-review/detect-arm-leases.js @@ -89,8 +89,10 @@ export function parseLease(content) { * Check if ARM lease exists and is valid. * * Reads the lease file directly from HEAD^ (the base-branch parent of the merge commit) - * via git show. This is the same pattern used by trivial-changes-check.js and - * arm-incremental-typespec.js and requires no extra git fetch or GitHub API calls. + * via git show. If the lease is not found in HEAD^ (which can happen when the PR's merge + * commit is stale — i.e., the lease was merged to the base branch after the merge commit + * was last generated), falls back to checking origin/ which is pre-fetched + * by the workflow to reflect the current state of the base branch. * * @param {string} orgName - Organization name (e.g., "compute") * @param {string} rpNamespace - Resource provider namespace (e.g., "Microsoft.Compute") @@ -100,11 +102,29 @@ export function parseLease(content) { export async function checkLease(orgName, rpNamespace, serviceName = "") { const repoRoot = await getRootFolder(process.cwd()); const relLeasePath = buildLeaseRelativePath(orgName, rpNamespace, serviceName); + const git = simpleGit(repoRoot); + // Try reading from HEAD^ (the base-branch parent of the merge commit). + // This is the common case when the lease was merged before the PR's merge commit was generated. let content; try { - const git = simpleGit(repoRoot); content = await git.show([`HEAD^:${relLeasePath}`]); + return parseLease(content).valid; + } catch { + // HEAD^ doesn't have the lease file. This can happen when the PR's merge commit is stale: + // the lease was merged to the base branch after the last push to this PR triggered CI. + // Fall back to checking origin/, which is pre-fetched by the workflow. + } + + // Fall back to origin/ to handle the race condition where the lease was merged + // to the base branch after this PR's merge commit was generated but before this check runs. + const baseBranch = process.env.GITHUB_BASE_REF; + if (!baseBranch) { + return false; + } + + try { + content = await git.show([`origin/${baseBranch}:${relLeasePath}`]); } catch { return false; } 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 index ffc4dea2e1cf..e3ecd045b37b 100644 --- a/.github/workflows/test/arm-modeling-review/detect-arm-leases.test.js +++ b/.github/workflows/test/arm-modeling-review/detect-arm-leases.test.js @@ -1,5 +1,5 @@ import { Temporal } from "@js-temporal/polyfill"; -import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; /** @type {{ show: import("vitest").MockedFunction<() => Promise> }} */ const mockGitInstance = vi.hoisted(() => ({ show: vi.fn() })); @@ -182,5 +182,80 @@ describe("detect-arm-leases", () => { "HEAD^:.github/arm-leases/xyz/Microsoft.XYZ/lease.yaml", ]); }); + + describe("fallback to origin/ when HEAD^ is stale", () => { + /** @type {string | undefined} */ + let origEnv; + + beforeEach(() => { + origEnv = process.env.GITHUB_BASE_REF; + process.env.GITHUB_BASE_REF = "main"; + }); + + afterEach(() => { + if (origEnv === undefined) { + delete process.env.GITHUB_BASE_REF; + } else { + process.env.GITHUB_BASE_REF = origEnv; + } + }); + + it("returns true when origin/ has a valid lease", async () => { + mockGitInstance.show + .mockRejectedValueOnce(new Error("does not exist in HEAD^")) + .mockResolvedValueOnce(leaseYaml(daysAgo(30), "P90D")); + + const result = await checkLease("xyz", "Microsoft.XYZ", "XYZInsights"); + expect(result).toBe(true); + expect(mockGitInstance.show).toHaveBeenCalledTimes(2); + expect(mockGitInstance.show).toHaveBeenNthCalledWith(1, [ + "HEAD^:.github/arm-leases/xyz/Microsoft.XYZ/XYZInsights/lease.yaml", + ]); + expect(mockGitInstance.show).toHaveBeenNthCalledWith(2, [ + "origin/main:.github/arm-leases/xyz/Microsoft.XYZ/XYZInsights/lease.yaml", + ]); + }); + + it("returns false when both HEAD^ and origin/ do not have the lease", async () => { + mockGitInstance.show.mockRejectedValue(new Error("does not exist")); + + const result = await checkLease("testservice", "Microsoft.Test"); + expect(result).toBe(false); + }); + + it("returns false when origin/ has an expired lease", async () => { + mockGitInstance.show + .mockRejectedValueOnce(new Error("does not exist in HEAD^")) + .mockResolvedValueOnce(leaseYaml(daysAgo(100), "P90D")); + + const result = await checkLease("testservice", "Microsoft.Test"); + expect(result).toBe(false); + }); + }); + + describe("without GITHUB_BASE_REF set", () => { + /** @type {string | undefined} */ + let origEnv; + + beforeEach(() => { + origEnv = process.env.GITHUB_BASE_REF; + delete process.env.GITHUB_BASE_REF; + }); + + afterEach(() => { + if (origEnv !== undefined) { + process.env.GITHUB_BASE_REF = origEnv; + } + }); + + it("returns false without attempting origin fallback", async () => { + mockGitInstance.show.mockRejectedValue(new Error("does not exist in HEAD^")); + + const result = await checkLease("testservice", "Microsoft.Test"); + expect(result).toBe(false); + // Only one call (HEAD^), no fallback attempted + expect(mockGitInstance.show).toHaveBeenCalledTimes(1); + }); + }); }); }); From ddd41a0b4cc6040e5e060b83a6448111f524c180 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:34:01 +0000 Subject: [PATCH 111/125] Refactor detect-arm-leases tests: use vi.stubEnv() instead of nested describe+beforeEach Replace nested describe blocks with beforeEach/afterEach for GITHUB_BASE_REF management with the canonical vi.stubEnv() pattern (consistent with avocado-code.test.js). Also clean up detect-arm-leases.js: use const for content variables in each try block, remove redundant comment, and clarify the empty catch block comment. Co-authored-by: tejaswiMinnu <14865963+tejaswiMinnu@users.noreply.github.com> Agent-Logs-Url: https://github.com/tejaswiMinnu/azure-rest-api-specs/sessions/c1e968c0-88b7-483c-85d6-8d997973e8e7 --- .../arm-modeling-review/detect-arm-leases.js | 18 ++- .../detect-arm-leases.test.js | 116 +++++++----------- 2 files changed, 52 insertions(+), 82 deletions(-) diff --git a/.github/workflows/src/arm-modeling-review/detect-arm-leases.js b/.github/workflows/src/arm-modeling-review/detect-arm-leases.js index bff0e71fdc4f..922c004915f7 100644 --- a/.github/workflows/src/arm-modeling-review/detect-arm-leases.js +++ b/.github/workflows/src/arm-modeling-review/detect-arm-leases.js @@ -102,32 +102,30 @@ export function parseLease(content) { export async function checkLease(orgName, rpNamespace, serviceName = "") { const repoRoot = await getRootFolder(process.cwd()); const relLeasePath = buildLeaseRelativePath(orgName, rpNamespace, serviceName); + + // git is initialized once and reused for both lookups (HEAD^ and origin/). const git = simpleGit(repoRoot); // Try reading from HEAD^ (the base-branch parent of the merge commit). // This is the common case when the lease was merged before the PR's merge commit was generated. - let content; try { - content = await git.show([`HEAD^:${relLeasePath}`]); + const content = await git.show([`HEAD^:${relLeasePath}`]); return parseLease(content).valid; } catch { - // HEAD^ doesn't have the lease file. This can happen when the PR's merge commit is stale: - // the lease was merged to the base branch after the last push to this PR triggered CI. - // Fall back to checking origin/, which is pre-fetched by the workflow. + // Expected when the lease file is absent from HEAD^ — this happens when the PR's merge + // commit is stale (the lease was merged to the base branch after the last PR push). + // Fall back to origin/, which is pre-fetched by the workflow. } - // Fall back to origin/ to handle the race condition where the lease was merged - // to the base branch after this PR's merge commit was generated but before this check runs. const baseBranch = process.env.GITHUB_BASE_REF; if (!baseBranch) { return false; } try { - content = await git.show([`origin/${baseBranch}:${relLeasePath}`]); + const content = await git.show([`origin/${baseBranch}:${relLeasePath}`]); + return parseLease(content).valid; } catch { return false; } - - return parseLease(content).valid; } 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 index e3ecd045b37b..022cb3cd7310 100644 --- a/.github/workflows/test/arm-modeling-review/detect-arm-leases.test.js +++ b/.github/workflows/test/arm-modeling-review/detect-arm-leases.test.js @@ -1,5 +1,5 @@ import { Temporal } from "@js-temporal/polyfill"; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; /** @type {{ show: import("vitest").MockedFunction<() => Promise> }} */ const mockGitInstance = vi.hoisted(() => ({ show: vi.fn() })); @@ -55,6 +55,7 @@ describe("detect-arm-leases", () => { afterEach(() => { vi.clearAllMocks(); + vi.unstubAllEnvs(); }); describe("parseLease", () => { @@ -183,79 +184,50 @@ describe("detect-arm-leases", () => { ]); }); - describe("fallback to origin/ when HEAD^ is stale", () => { - /** @type {string | undefined} */ - let origEnv; - - beforeEach(() => { - origEnv = process.env.GITHUB_BASE_REF; - process.env.GITHUB_BASE_REF = "main"; - }); - - afterEach(() => { - if (origEnv === undefined) { - delete process.env.GITHUB_BASE_REF; - } else { - process.env.GITHUB_BASE_REF = origEnv; - } - }); - - it("returns true when origin/ has a valid lease", async () => { - mockGitInstance.show - .mockRejectedValueOnce(new Error("does not exist in HEAD^")) - .mockResolvedValueOnce(leaseYaml(daysAgo(30), "P90D")); - - const result = await checkLease("xyz", "Microsoft.XYZ", "XYZInsights"); - expect(result).toBe(true); - expect(mockGitInstance.show).toHaveBeenCalledTimes(2); - expect(mockGitInstance.show).toHaveBeenNthCalledWith(1, [ - "HEAD^:.github/arm-leases/xyz/Microsoft.XYZ/XYZInsights/lease.yaml", - ]); - expect(mockGitInstance.show).toHaveBeenNthCalledWith(2, [ - "origin/main:.github/arm-leases/xyz/Microsoft.XYZ/XYZInsights/lease.yaml", - ]); - }); - - it("returns false when both HEAD^ and origin/ do not have the lease", async () => { - mockGitInstance.show.mockRejectedValue(new Error("does not exist")); - - const result = await checkLease("testservice", "Microsoft.Test"); - expect(result).toBe(false); - }); - - it("returns false when origin/ has an expired lease", async () => { - mockGitInstance.show - .mockRejectedValueOnce(new Error("does not exist in HEAD^")) - .mockResolvedValueOnce(leaseYaml(daysAgo(100), "P90D")); - - const result = await checkLease("testservice", "Microsoft.Test"); - expect(result).toBe(false); - }); + it("falls back to origin/ when HEAD^ does not have the lease", async () => { + vi.stubEnv("GITHUB_BASE_REF", "main"); + + // First call (HEAD^) fails, second call (origin/main) succeeds + mockGitInstance.show + .mockRejectedValueOnce(new Error("does not exist in HEAD^")) + .mockResolvedValueOnce(leaseYaml(daysAgo(30), "P90D")); + + const result = await checkLease("xyz", "Microsoft.XYZ", "XYZInsights"); + expect(result).toBe(true); + expect(mockGitInstance.show).toHaveBeenCalledTimes(2); + expect(mockGitInstance.show).toHaveBeenNthCalledWith(1, [ + "HEAD^:.github/arm-leases/xyz/Microsoft.XYZ/XYZInsights/lease.yaml", + ]); + expect(mockGitInstance.show).toHaveBeenNthCalledWith(2, [ + "origin/main:.github/arm-leases/xyz/Microsoft.XYZ/XYZInsights/lease.yaml", + ]); + }); + + it("returns false when both HEAD^ and origin/ do not have the lease", async () => { + vi.stubEnv("GITHUB_BASE_REF", "main"); + mockGitInstance.show.mockRejectedValue(new Error("does not exist")); + + const result = await checkLease("testservice", "Microsoft.Test"); + expect(result).toBe(false); }); - describe("without GITHUB_BASE_REF set", () => { - /** @type {string | undefined} */ - let origEnv; - - beforeEach(() => { - origEnv = process.env.GITHUB_BASE_REF; - delete process.env.GITHUB_BASE_REF; - }); - - afterEach(() => { - if (origEnv !== undefined) { - process.env.GITHUB_BASE_REF = origEnv; - } - }); - - it("returns false without attempting origin fallback", async () => { - mockGitInstance.show.mockRejectedValue(new Error("does not exist in HEAD^")); - - const result = await checkLease("testservice", "Microsoft.Test"); - expect(result).toBe(false); - // Only one call (HEAD^), no fallback attempted - expect(mockGitInstance.show).toHaveBeenCalledTimes(1); - }); + it("returns false when HEAD^ fails and GITHUB_BASE_REF is not set", async () => { + mockGitInstance.show.mockRejectedValue(new Error("does not exist in HEAD^")); + + const result = await checkLease("testservice", "Microsoft.Test"); + expect(result).toBe(false); + // Only one call (HEAD^), no fallback attempted + expect(mockGitInstance.show).toHaveBeenCalledTimes(1); + }); + + it("returns false when origin/ has an expired lease", async () => { + vi.stubEnv("GITHUB_BASE_REF", "main"); + mockGitInstance.show + .mockRejectedValueOnce(new Error("does not exist in HEAD^")) + .mockResolvedValueOnce(leaseYaml(daysAgo(100), "P90D")); + + const result = await checkLease("testservice", "Microsoft.Test"); + expect(result).toBe(false); }); }); }); From c53a54352e28a1a4a4cef0c91502b9200397be86 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:56:17 +0000 Subject: [PATCH 112/125] Initial plan From cb0d3fcfd550996197f2cd3cdad7812c548904ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:58:30 +0000 Subject: [PATCH 113/125] Add ARM lease for Microsoft.XYZ/XYZMonitor service group Co-authored-by: tejaswiMinnu <14865963+tejaswiMinnu@users.noreply.github.com> Agent-Logs-Url: https://github.com/tejaswiMinnu/azure-rest-api-specs/sessions/2fb5d474-67e2-4675-93e8-9e56d8e46779 --- .github/arm-leases/xyz/Microsoft.XYZ/XYZMonitor/lease.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/arm-leases/xyz/Microsoft.XYZ/XYZMonitor/lease.yaml diff --git a/.github/arm-leases/xyz/Microsoft.XYZ/XYZMonitor/lease.yaml b/.github/arm-leases/xyz/Microsoft.XYZ/XYZMonitor/lease.yaml new file mode 100644 index 000000000000..b0e77d307b75 --- /dev/null +++ b/.github/arm-leases/xyz/Microsoft.XYZ/XYZMonitor/lease.yaml @@ -0,0 +1,5 @@ +lease: + resource-provider: Microsoft.XYZ + startdate: "2026-03-20" + duration: P180D + reviewer: "@tejaswiMinnu" From 4ef55f3c281727d9d6ba0d9e82e23b847aa44ebb Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Fri, 20 Mar 2026 16:32:14 -0500 Subject: [PATCH 114/125] Resolve --- .../src/arm-modeling-review/detect-arm-leases.js | 7 +++---- .../test/arm-modeling-review/detect-arm-leases.test.js | 9 +++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/src/arm-modeling-review/detect-arm-leases.js b/.github/workflows/src/arm-modeling-review/detect-arm-leases.js index 922c004915f7..c406ac07c65a 100644 --- a/.github/workflows/src/arm-modeling-review/detect-arm-leases.js +++ b/.github/workflows/src/arm-modeling-review/detect-arm-leases.js @@ -1,3 +1,4 @@ +import { resolve } from "path"; import { Temporal } from "@js-temporal/polyfill"; import yaml from "js-yaml"; import { simpleGit } from "simple-git"; @@ -41,12 +42,10 @@ const leaseSchema = z.object({ * @returns {string} Relative path to lease.yaml file (e.g., ".github/arm-leases/compute/Microsoft.Compute/lease.yaml") */ function buildLeaseRelativePath(orgName, rpNamespace, serviceName = "") { - const parts = [".github", "arm-leases", orgName, rpNamespace]; if (serviceName) { - parts.push(serviceName); + return resolve(".github", "arm-leases", orgName, rpNamespace, serviceName, "lease.yaml"); } - parts.push("lease.yaml"); - return parts.join("/"); + return resolve(".github", "arm-leases", orgName, rpNamespace, "lease.yaml"); } /** 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 index 022cb3cd7310..1157e5504e91 100644 --- a/.github/workflows/test/arm-modeling-review/detect-arm-leases.test.js +++ b/.github/workflows/test/arm-modeling-review/detect-arm-leases.test.js @@ -1,4 +1,5 @@ import { Temporal } from "@js-temporal/polyfill"; +import { resolve } from "path"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; /** @type {{ show: import("vitest").MockedFunction<() => Promise> }} */ @@ -169,7 +170,7 @@ describe("detect-arm-leases", () => { expect(result).toBe(true); expect(mockGitInstance.show).toHaveBeenCalledWith([ - "HEAD^:.github/arm-leases/xyz/Microsoft.XYZ/XYZ/lease.yaml", + `HEAD^:${resolve(".github", "arm-leases", "xyz", "Microsoft.XYZ", "XYZ", "lease.yaml")}`, ]); }); @@ -180,7 +181,7 @@ describe("detect-arm-leases", () => { expect(result).toBe(true); expect(mockGitInstance.show).toHaveBeenCalledWith([ - "HEAD^:.github/arm-leases/xyz/Microsoft.XYZ/lease.yaml", + `HEAD^:${resolve(".github", "arm-leases", "xyz", "Microsoft.XYZ", "lease.yaml")}`, ]); }); @@ -196,10 +197,10 @@ describe("detect-arm-leases", () => { expect(result).toBe(true); expect(mockGitInstance.show).toHaveBeenCalledTimes(2); expect(mockGitInstance.show).toHaveBeenNthCalledWith(1, [ - "HEAD^:.github/arm-leases/xyz/Microsoft.XYZ/XYZInsights/lease.yaml", + `HEAD^:${resolve(".github", "arm-leases", "xyz", "Microsoft.XYZ", "XYZInsights", "lease.yaml")}`, ]); expect(mockGitInstance.show).toHaveBeenNthCalledWith(2, [ - "origin/main:.github/arm-leases/xyz/Microsoft.XYZ/XYZInsights/lease.yaml", + `origin/main:${resolve(".github", "arm-leases", "xyz", "Microsoft.XYZ", "XYZInsights", "lease.yaml")}`, ]); }); From 9f8f462248d54219bc471849a05fc02f57a9ee68 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Fri, 20 Mar 2026 17:35:15 -0500 Subject: [PATCH 115/125] Test --- .github/workflows/arm-modeling-review.yaml | 5 ----- .../src/arm-modeling-review/detect-arm-leases.js | 9 +++++---- .../arm-modeling-review/detect-arm-leases.test.js | 13 +++++++++++-- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/workflows/arm-modeling-review.yaml b/.github/workflows/arm-modeling-review.yaml index 848f1fcd22c1..afc71456a200 100644 --- a/.github/workflows/arm-modeling-review.yaml +++ b/.github/workflows/arm-modeling-review.yaml @@ -37,11 +37,6 @@ jobs: with: fetch-depth: 2 - - name: Fetch latest base branch for lease verification - # Ensures origin/ is available so checkLease can fall back to it when the - # PR's merge commit is stale (i.e., the lease was merged after the last PR push). - run: git fetch origin ${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }} --depth=1 - - name: Install dependencies for github-script actions uses: ./.github/actions/install-deps-github-script diff --git a/.github/workflows/src/arm-modeling-review/detect-arm-leases.js b/.github/workflows/src/arm-modeling-review/detect-arm-leases.js index c406ac07c65a..ee85cdd2976e 100644 --- a/.github/workflows/src/arm-modeling-review/detect-arm-leases.js +++ b/.github/workflows/src/arm-modeling-review/detect-arm-leases.js @@ -90,8 +90,8 @@ export function parseLease(content) { * Reads the lease file directly from HEAD^ (the base-branch parent of the merge commit) * via git show. If the lease is not found in HEAD^ (which can happen when the PR's merge * commit is stale — i.e., the lease was merged to the base branch after the merge commit - * was last generated), falls back to checking origin/ which is pre-fetched - * by the workflow to reflect the current state of the base branch. + * was last generated), falls back to checking origin/ after fetching + * the latest state of the base branch. * * @param {string} orgName - Organization name (e.g., "compute") * @param {string} rpNamespace - Resource provider namespace (e.g., "Microsoft.Compute") @@ -102,7 +102,6 @@ export async function checkLease(orgName, rpNamespace, serviceName = "") { const repoRoot = await getRootFolder(process.cwd()); const relLeasePath = buildLeaseRelativePath(orgName, rpNamespace, serviceName); - // git is initialized once and reused for both lookups (HEAD^ and origin/). const git = simpleGit(repoRoot); // Try reading from HEAD^ (the base-branch parent of the merge commit). @@ -113,7 +112,7 @@ export async function checkLease(orgName, rpNamespace, serviceName = "") { } catch { // Expected when the lease file is absent from HEAD^ — this happens when the PR's merge // commit is stale (the lease was merged to the base branch after the last PR push). - // Fall back to origin/, which is pre-fetched by the workflow. + // Fall back to origin/ after fetching latest. } const baseBranch = process.env.GITHUB_BASE_REF; @@ -122,6 +121,8 @@ export async function checkLease(orgName, rpNamespace, serviceName = "") { } try { + // Fetch the latest base branch to get any leases merged after the PR was created + await git.fetch(["origin", `${baseBranch}:refs/remotes/origin/${baseBranch}`, "--depth=1"]); const content = await git.show([`origin/${baseBranch}:${relLeasePath}`]); return parseLease(content).valid; } catch { 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 index 1157e5504e91..917dfce56a56 100644 --- a/.github/workflows/test/arm-modeling-review/detect-arm-leases.test.js +++ b/.github/workflows/test/arm-modeling-review/detect-arm-leases.test.js @@ -2,8 +2,8 @@ import { Temporal } from "@js-temporal/polyfill"; import { resolve } from "path"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; -/** @type {{ show: import("vitest").MockedFunction<() => Promise> }} */ -const mockGitInstance = vi.hoisted(() => ({ show: vi.fn() })); +/** @type {{ show: import("vitest").MockedFunction<() => Promise>, fetch: import("vitest").MockedFunction<() => Promise> }} */ +const mockGitInstance = vi.hoisted(() => ({ show: vi.fn(), fetch: vi.fn() })); vi.mock("simple-git", () => ({ simpleGit: vi.fn(() => mockGitInstance), @@ -192,9 +192,15 @@ describe("detect-arm-leases", () => { mockGitInstance.show .mockRejectedValueOnce(new Error("does not exist in HEAD^")) .mockResolvedValueOnce(leaseYaml(daysAgo(30), "P90D")); + mockGitInstance.fetch.mockResolvedValue(); const result = await checkLease("xyz", "Microsoft.XYZ", "XYZInsights"); expect(result).toBe(true); + expect(mockGitInstance.fetch).toHaveBeenCalledWith([ + "origin", + "main:refs/remotes/origin/main", + "--depth=1", + ]); expect(mockGitInstance.show).toHaveBeenCalledTimes(2); expect(mockGitInstance.show).toHaveBeenNthCalledWith(1, [ `HEAD^:${resolve(".github", "arm-leases", "xyz", "Microsoft.XYZ", "XYZInsights", "lease.yaml")}`, @@ -206,6 +212,7 @@ describe("detect-arm-leases", () => { it("returns false when both HEAD^ and origin/ do not have the lease", async () => { vi.stubEnv("GITHUB_BASE_REF", "main"); + mockGitInstance.fetch.mockResolvedValue(); mockGitInstance.show.mockRejectedValue(new Error("does not exist")); const result = await checkLease("testservice", "Microsoft.Test"); @@ -219,10 +226,12 @@ describe("detect-arm-leases", () => { expect(result).toBe(false); // Only one call (HEAD^), no fallback attempted expect(mockGitInstance.show).toHaveBeenCalledTimes(1); + expect(mockGitInstance.fetch).not.toHaveBeenCalled(); }); it("returns false when origin/ has an expired lease", async () => { vi.stubEnv("GITHUB_BASE_REF", "main"); + mockGitInstance.fetch.mockResolvedValue(); mockGitInstance.show .mockRejectedValueOnce(new Error("does not exist in HEAD^")) .mockResolvedValueOnce(leaseYaml(daysAgo(100), "P90D")); From 6415674ba6d92ead5a3b84067d3e67ece034c375 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Fri, 20 Mar 2026 18:49:57 -0500 Subject: [PATCH 116/125] Test Lease --- .github/arm-leases/testrop/Microsoft.TestROP/lease.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/arm-leases/testrop/Microsoft.TestROP/lease.yaml diff --git a/.github/arm-leases/testrop/Microsoft.TestROP/lease.yaml b/.github/arm-leases/testrop/Microsoft.TestROP/lease.yaml new file mode 100644 index 000000000000..8ec41004e095 --- /dev/null +++ b/.github/arm-leases/testrop/Microsoft.TestROP/lease.yaml @@ -0,0 +1,5 @@ +lease: + resource-provider: Microsoft.TestROP + startdate: "2026-03-20" + duration: P180D + reviewer: "@pshao25" From 425932c9dbc89be7c4bcb26747db265374ca6d9d Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Fri, 20 Mar 2026 18:59:30 -0500 Subject: [PATCH 117/125] Rename .github/arm-leases/testrop/Microsoft.TestROP/lease.yaml to .github/arm-leases/testrop/Microsoft.TestROP/arm-leases/testrop/Microsoft.TestROP/TestROP/lease.yaml --- .../{ => arm-leases/testrop/Microsoft.TestROP/TestROP}/lease.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/arm-leases/testrop/Microsoft.TestROP/{ => arm-leases/testrop/Microsoft.TestROP/TestROP}/lease.yaml (100%) diff --git a/.github/arm-leases/testrop/Microsoft.TestROP/lease.yaml b/.github/arm-leases/testrop/Microsoft.TestROP/arm-leases/testrop/Microsoft.TestROP/TestROP/lease.yaml similarity index 100% rename from .github/arm-leases/testrop/Microsoft.TestROP/lease.yaml rename to .github/arm-leases/testrop/Microsoft.TestROP/arm-leases/testrop/Microsoft.TestROP/TestROP/lease.yaml From 303bf8f9f889425fed98f3810b47dbd9551d3fb0 Mon Sep 17 00:00:00 2001 From: Tejaswi Salaigari Date: Fri, 20 Mar 2026 19:06:30 -0500 Subject: [PATCH 118/125] Rename .github/arm-leases/testrop/Microsoft.TestROP/arm-leases/testrop/Microsoft.TestROP/TestROP/lease.yaml to .github/arm-leases/testrop/Microsoft.TestROP/TestROP/lease.yaml --- .../{arm-leases/testrop/Microsoft.TestROP => }/TestROP/lease.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/arm-leases/testrop/Microsoft.TestROP/{arm-leases/testrop/Microsoft.TestROP => }/TestROP/lease.yaml (100%) diff --git a/.github/arm-leases/testrop/Microsoft.TestROP/arm-leases/testrop/Microsoft.TestROP/TestROP/lease.yaml b/.github/arm-leases/testrop/Microsoft.TestROP/TestROP/lease.yaml similarity index 100% rename from .github/arm-leases/testrop/Microsoft.TestROP/arm-leases/testrop/Microsoft.TestROP/TestROP/lease.yaml rename to .github/arm-leases/testrop/Microsoft.TestROP/TestROP/lease.yaml From d03190d2f771d0b6dea1bcb38b118f4e67b54dc8 Mon Sep 17 00:00:00 2001 From: Vikeshi Tiwari Date: Mon, 23 Mar 2026 10:52:09 -0700 Subject: [PATCH 119/125] Add ARM lease for Microsoft.Polarbear --- .../polarbear/Microsoft.Polarbear/Polarbear/lease.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/arm-leases/polarbear/Microsoft.Polarbear/Polarbear/lease.yaml diff --git a/.github/arm-leases/polarbear/Microsoft.Polarbear/Polarbear/lease.yaml b/.github/arm-leases/polarbear/Microsoft.Polarbear/Polarbear/lease.yaml new file mode 100644 index 000000000000..498555650f0b --- /dev/null +++ b/.github/arm-leases/polarbear/Microsoft.Polarbear/Polarbear/lease.yaml @@ -0,0 +1,5 @@ +lease: + resource-provider: Microsoft.Polarbear + startdate: 2026-03-23 + duration: P180D + reviewer: "@vikeshi26" From 0ac42efbf15f218c63a4e5d4c287f5ed468a7015 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:06:27 +0000 Subject: [PATCH 120/125] Initial plan From f1180e26659c564c3c8c5e4fc7b02118ebe7b677 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:11:33 +0000 Subject: [PATCH 121/125] Fix detect-arm-leases: use path.join() instead of path.resolve() for git-compatible relative paths Co-authored-by: tejaswiMinnu <14865963+tejaswiMinnu@users.noreply.github.com> Agent-Logs-Url: https://github.com/tejaswiMinnu/azure-rest-api-specs/sessions/96e18844-e7ab-48c0-b318-007904014bfc --- .../src/arm-modeling-review/detect-arm-leases.js | 6 +++--- .../test/arm-modeling-review/detect-arm-leases.test.js | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/src/arm-modeling-review/detect-arm-leases.js b/.github/workflows/src/arm-modeling-review/detect-arm-leases.js index ee85cdd2976e..3672b7e3cc34 100644 --- a/.github/workflows/src/arm-modeling-review/detect-arm-leases.js +++ b/.github/workflows/src/arm-modeling-review/detect-arm-leases.js @@ -1,4 +1,4 @@ -import { resolve } from "path"; +import { join } from "path"; import { Temporal } from "@js-temporal/polyfill"; import yaml from "js-yaml"; import { simpleGit } from "simple-git"; @@ -43,9 +43,9 @@ const leaseSchema = z.object({ */ function buildLeaseRelativePath(orgName, rpNamespace, serviceName = "") { if (serviceName) { - return resolve(".github", "arm-leases", orgName, rpNamespace, serviceName, "lease.yaml"); + return join(".github", "arm-leases", orgName, rpNamespace, serviceName, "lease.yaml"); } - return resolve(".github", "arm-leases", orgName, rpNamespace, "lease.yaml"); + return join(".github", "arm-leases", orgName, rpNamespace, "lease.yaml"); } /** 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 index 917dfce56a56..0c3ea635f003 100644 --- a/.github/workflows/test/arm-modeling-review/detect-arm-leases.test.js +++ b/.github/workflows/test/arm-modeling-review/detect-arm-leases.test.js @@ -1,5 +1,5 @@ import { Temporal } from "@js-temporal/polyfill"; -import { resolve } from "path"; +import { join } from "path"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; /** @type {{ show: import("vitest").MockedFunction<() => Promise>, fetch: import("vitest").MockedFunction<() => Promise> }} */ @@ -170,7 +170,7 @@ describe("detect-arm-leases", () => { expect(result).toBe(true); expect(mockGitInstance.show).toHaveBeenCalledWith([ - `HEAD^:${resolve(".github", "arm-leases", "xyz", "Microsoft.XYZ", "XYZ", "lease.yaml")}`, + `HEAD^:${join(".github", "arm-leases", "xyz", "Microsoft.XYZ", "XYZ", "lease.yaml")}`, ]); }); @@ -181,7 +181,7 @@ describe("detect-arm-leases", () => { expect(result).toBe(true); expect(mockGitInstance.show).toHaveBeenCalledWith([ - `HEAD^:${resolve(".github", "arm-leases", "xyz", "Microsoft.XYZ", "lease.yaml")}`, + `HEAD^:${join(".github", "arm-leases", "xyz", "Microsoft.XYZ", "lease.yaml")}`, ]); }); @@ -203,10 +203,10 @@ describe("detect-arm-leases", () => { ]); expect(mockGitInstance.show).toHaveBeenCalledTimes(2); expect(mockGitInstance.show).toHaveBeenNthCalledWith(1, [ - `HEAD^:${resolve(".github", "arm-leases", "xyz", "Microsoft.XYZ", "XYZInsights", "lease.yaml")}`, + `HEAD^:${join(".github", "arm-leases", "xyz", "Microsoft.XYZ", "XYZInsights", "lease.yaml")}`, ]); expect(mockGitInstance.show).toHaveBeenNthCalledWith(2, [ - `origin/main:${resolve(".github", "arm-leases", "xyz", "Microsoft.XYZ", "XYZInsights", "lease.yaml")}`, + `origin/main:${join(".github", "arm-leases", "xyz", "Microsoft.XYZ", "XYZInsights", "lease.yaml")}`, ]); }); From c6058e04e83731afd463a097be018f3187466c86 Mon Sep 17 00:00:00 2001 From: Vikeshi Tiwari Date: Mon, 23 Mar 2026 15:21:41 -0700 Subject: [PATCH 122/125] Add ARM lease for Microsoft.Tiwari --- .github/arm-leases/tiwari/Microsoft.Tiwari/Tiwari/lease.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/arm-leases/tiwari/Microsoft.Tiwari/Tiwari/lease.yaml diff --git a/.github/arm-leases/tiwari/Microsoft.Tiwari/Tiwari/lease.yaml b/.github/arm-leases/tiwari/Microsoft.Tiwari/Tiwari/lease.yaml new file mode 100644 index 000000000000..d7ecd4844e02 --- /dev/null +++ b/.github/arm-leases/tiwari/Microsoft.Tiwari/Tiwari/lease.yaml @@ -0,0 +1,5 @@ +lease: + resource-provider: Microsoft.Tiwari + startdate: 2026-03-23 + duration: P180D + reviewer: "@vikeshi26" From 9d957264a00b0a16c5c1e1014d5e6cfb87120b8a Mon Sep 17 00:00:00 2001 From: Vikeshi Tiwari Date: Mon, 23 Mar 2026 15:22:54 -0700 Subject: [PATCH 123/125] Add Microsoft.Tiwari resource provider spec --- specification/tiwari/cspell.yaml | 2 + .../Microsoft.Tiwari/Tiwari/main.tsp | 61 +++ .../preview/2024-01-01-preview/tiwari.json | 411 ++++++++++++++++++ .../Microsoft.Tiwari/Tiwari/readme.md | 26 ++ .../Microsoft.Tiwari/Tiwari/tspconfig.yaml | 14 + .../tiwari/resource-manager/readme.md | 3 + 6 files changed, 517 insertions(+) create mode 100644 specification/tiwari/cspell.yaml create mode 100644 specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/main.tsp create mode 100644 specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/preview/2024-01-01-preview/tiwari.json create mode 100644 specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/readme.md create mode 100644 specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/tspconfig.yaml create mode 100644 specification/tiwari/resource-manager/readme.md diff --git a/specification/tiwari/cspell.yaml b/specification/tiwari/cspell.yaml new file mode 100644 index 000000000000..99761fa26c01 --- /dev/null +++ b/specification/tiwari/cspell.yaml @@ -0,0 +1,2 @@ +words: + - tiwari diff --git a/specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/main.tsp b/specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/main.tsp new file mode 100644 index 000000000000..6ca6b785d04d --- /dev/null +++ b/specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/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.Tiwari Resource Provider management API. */ +@armProviderNamespace +@service(#{ title: "Tiwari" }) +@versioned(Microsoft.Tiwari.Versions) +namespace Microsoft.Tiwari; + +/** 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 Tiwari resource */ +model TiwariResource is TrackedResource { + ...ResourceNameParameter; +} + +/** Tiwari resource properties */ +model TiwariProperties { + /** 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 TiwariResources { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + delete is ArmResourceDeleteWithoutOkAsync; + listByResourceGroup is ArmResourceListByParent; + listBySubscription is ArmListBySubscription; +} diff --git a/specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/preview/2024-01-01-preview/tiwari.json b/specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/preview/2024-01-01-preview/tiwari.json new file mode 100644 index 000000000000..b1c660453699 --- /dev/null +++ b/specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/preview/2024-01-01-preview/tiwari.json @@ -0,0 +1,411 @@ +{ + "swagger": "2.0", + "info": { + "title": "Tiwari", + "version": "2024-01-01-preview", + "description": "Microsoft.Tiwari 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": "TiwariResources" + } + ], + "paths": { + "/providers/Microsoft.Tiwari/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.Tiwari/TiwariResources": { + "get": { + "operationId": "TiwariResources_ListBySubscription", + "tags": [ + "TiwariResources" + ], + "description": "List TiwariResource 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/TiwariResourceListResult" + } + }, + "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.Tiwari/TiwariResources": { + "get": { + "operationId": "TiwariResources_ListByResourceGroup", + "tags": [ + "TiwariResources" + ], + "description": "List TiwariResource 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/TiwariResourceListResult" + } + }, + "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.Tiwari/TiwariResources/{TiwariResourceName}": { + "get": { + "operationId": "TiwariResources_Get", + "tags": [ + "TiwariResources" + ], + "description": "Get a TiwariResource", + "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": "TiwariResourceName", + "in": "path", + "description": "The name of the TiwariResource", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9-]{3,24}$" + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "#/definitions/TiwariResource" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "operationId": "TiwariResources_CreateOrUpdate", + "tags": [ + "TiwariResources" + ], + "description": "Create a TiwariResource", + "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": "TiwariResourceName", + "in": "path", + "description": "The name of the TiwariResource", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9-]{3,24}$" + }, + { + "name": "resource", + "in": "body", + "description": "Resource create parameters.", + "required": true, + "schema": { + "$ref": "#/definitions/TiwariResource" + } + } + ], + "responses": { + "200": { + "description": "Resource 'TiwariResource' update operation succeeded", + "schema": { + "$ref": "#/definitions/TiwariResource" + } + }, + "201": { + "description": "Resource 'TiwariResource' create operation succeeded", + "schema": { + "$ref": "#/definitions/TiwariResource" + }, + "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": "TiwariResources_Delete", + "tags": [ + "TiwariResources" + ], + "description": "Delete a TiwariResource", + "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": "TiwariResourceName", + "in": "path", + "description": "The name of the TiwariResource", + "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": { + "TiwariProperties": { + "type": "object", + "description": "Tiwari resource properties", + "properties": { + "displayName": { + "type": "string", + "description": "Display name" + }, + "provisioningState": { + "$ref": "#/definitions/ProvisioningState", + "description": "The provisioning state", + "readOnly": true + } + } + }, + "TiwariResource": { + "type": "object", + "description": "A Tiwari resource", + "properties": { + "properties": { + "$ref": "#/definitions/TiwariProperties", + "description": "The resource-specific properties for this resource." + } + }, + "allOf": [ + { + "$ref": "../../../../../../common-types/resource-management/v5/types.json#/definitions/TrackedResource" + } + ] + }, + "TiwariResourceListResult": { + "type": "object", + "description": "The response of a TiwariResource list operation.", + "properties": { + "value": { + "type": "array", + "description": "The TiwariResource items on this page", + "items": { + "$ref": "#/definitions/TiwariResource" + } + }, + "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/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/readme.md b/specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/readme.md new file mode 100644 index 000000000000..be8c43dd4cc5 --- /dev/null +++ b/specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/readme.md @@ -0,0 +1,26 @@ +# Tiwari + +> see https://aka.ms/autorest + +This is the AutoRest configuration file for Tiwari. + +## 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/tiwari.json +``` + +--- diff --git a/specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/tspconfig.yaml b/specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/tspconfig.yaml new file mode 100644 index 000000000000..cc5c9a645eca --- /dev/null +++ b/specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/tspconfig.yaml @@ -0,0 +1,14 @@ +parameters: + "service-dir": + default: "sdk/tiwari" +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}/tiwari.json" + arm-types-dir: "{project-root}/../../../../common-types/resource-management" +linter: + extends: + - "@azure-tools/typespec-azure-rulesets/resource-manager" diff --git a/specification/tiwari/resource-manager/readme.md b/specification/tiwari/resource-manager/readme.md new file mode 100644 index 000000000000..e02b53bab7c6 --- /dev/null +++ b/specification/tiwari/resource-manager/readme.md @@ -0,0 +1,3 @@ +# Microsoft.Tiwari + +> see https://aka.ms/autorest From a1412948643c21b5960b85ca57a95f39d29ae238 Mon Sep 17 00:00:00 2001 From: Vikeshi Tiwari Date: Mon, 23 Mar 2026 15:50:05 -0700 Subject: [PATCH 124/125] Add ARM lease for Microsoft.Addons --- .github/arm-leases/addons/Microsoft.Addons/Addons/lease.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/arm-leases/addons/Microsoft.Addons/Addons/lease.yaml diff --git a/.github/arm-leases/addons/Microsoft.Addons/Addons/lease.yaml b/.github/arm-leases/addons/Microsoft.Addons/Addons/lease.yaml new file mode 100644 index 000000000000..d662c83a1179 --- /dev/null +++ b/.github/arm-leases/addons/Microsoft.Addons/Addons/lease.yaml @@ -0,0 +1,5 @@ +lease: + resource-provider: Microsoft.Addons + startdate: 2026-03-23 + duration: P180D + reviewer: "@vikeshi26" From 38d0c379070ba57041d9cbba6b0f0c6d35b05960 Mon Sep 17 00:00:00 2001 From: Vikeshi Tiwari Date: Mon, 23 Mar 2026 15:58:35 -0700 Subject: [PATCH 125/125] Add new resource type tiwariInsights to Microsoft.Tiwari --- .../preview/2025-01-01-preview/tiwari.json | 231 ++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/preview/2025-01-01-preview/tiwari.json diff --git a/specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/preview/2025-01-01-preview/tiwari.json b/specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/preview/2025-01-01-preview/tiwari.json new file mode 100644 index 000000000000..2dcef7bf90de --- /dev/null +++ b/specification/tiwari/resource-manager/Microsoft.Tiwari/Tiwari/preview/2025-01-01-preview/tiwari.json @@ -0,0 +1,231 @@ +{ + "swagger": "2.0", + "info": { + "title": "Tiwari", + "version": "2025-01-01-preview", + "description": "Microsoft.Tiwari 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" + } + } + }, + "paths": { + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Tiwari/tiwariInsights/{insightName}": { + "get": { + "operationId": "TiwariInsights_Get", + "tags": [ + "TiwariInsights" + ], + "description": "Get a TiwariInsight resource.", + "parameters": [ + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "#/parameters/ResourceGroupNameParameter" + }, + { + "name": "insightName", + "in": "path", + "required": true, + "description": "The name of the TiwariInsight resource.", + "type": "string" + } + ], + "responses": { + "200": { + "description": "ARM operation completed successfully.", + "schema": { + "$ref": "#/definitions/TiwariInsightResource" + } + } + } + }, + "put": { + "operationId": "TiwariInsights_CreateOrUpdate", + "tags": [ + "TiwariInsights" + ], + "description": "Create or update a TiwariInsight resource.", + "parameters": [ + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "#/parameters/ResourceGroupNameParameter" + }, + { + "name": "insightName", + "in": "path", + "required": true, + "description": "The name of the TiwariInsight resource.", + "type": "string" + }, + { + "name": "resource", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/TiwariInsightResource" + } + } + ], + "responses": { + "200": { + "description": "Resource updated successfully.", + "schema": { + "$ref": "#/definitions/TiwariInsightResource" + } + }, + "201": { + "description": "Resource created successfully.", + "schema": { + "$ref": "#/definitions/TiwariInsightResource" + } + } + } + }, + "delete": { + "operationId": "TiwariInsights_Delete", + "tags": [ + "TiwariInsights" + ], + "description": "Delete a TiwariInsight resource.", + "parameters": [ + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "#/parameters/ResourceGroupNameParameter" + }, + { + "name": "insightName", + "in": "path", + "required": true, + "description": "The name of the TiwariInsight resource.", + "type": "string" + } + ], + "responses": { + "200": { + "description": "Resource deleted successfully." + }, + "204": { + "description": "Resource does not exist." + } + } + } + } + }, + "definitions": { + "TiwariInsightResource": { + "type": "object", + "description": "A TiwariInsight resource.", + "properties": { + "id": { + "type": "string", + "readOnly": true, + "description": "Resource ID." + }, + "name": { + "type": "string", + "readOnly": true, + "description": "Resource name." + }, + "type": { + "type": "string", + "readOnly": true, + "description": "Resource type." + }, + "location": { + "type": "string", + "description": "Resource location." + }, + "properties": { + "$ref": "#/definitions/TiwariInsightProperties" + } + }, + "x-ms-azure-resource": true + }, + "TiwariInsightProperties": { + "type": "object", + "description": "TiwariInsight resource properties.", + "properties": { + "displayName": { + "type": "string", + "description": "Display name of the insight." + }, + "severity": { + "type": "string", + "description": "Severity level of the insight." + }, + "provisioningState": { + "type": "string", + "readOnly": true, + "description": "The provisioning state." + } + } + } + }, + "parameters": { + "ApiVersionParameter": { + "name": "api-version", + "in": "query", + "required": true, + "type": "string", + "description": "The API version to use for this operation." + }, + "SubscriptionIdParameter": { + "name": "subscriptionId", + "in": "path", + "required": true, + "type": "string", + "description": "The ID of the target subscription." + }, + "ResourceGroupNameParameter": { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + } + } +}