Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
11c5ed0
Initial plan
Copilot Mar 19, 2026
49f1761
Sync lease file generation script with ARM lease validation checks
Copilot Mar 19, 2026
d86dc57
Merge branch 'main' into copilot/update-lease-files-script
tejaswiMinnu Apr 16, 2026
29e42d3
Few manual tweaks
tejaswiMinnu Apr 16, 2026
94ae3cf
Merge branch 'main' into copilot/update-lease-files-script
tejaswiMinnu Apr 16, 2026
2ad87da
Prettier check
tejaswiMinnu Apr 16, 2026
62ce804
Merge branch 'main' into copilot/update-lease-files-script
tejaswiMinnu Apr 16, 2026
9c30115
Merge branch 'main' into copilot/update-lease-files-script
tejaswiMinnu Apr 27, 2026
9fde3aa
Add trailing line
tejaswiMinnu Apr 27, 2026
7200e1f
Merge branch 'main' into copilot/update-lease-files-script
tejaswiMinnu Apr 27, 2026
68bae9e
Merge branch 'main' into copilot/update-lease-files-script
tejaswiMinnu Apr 28, 2026
860061e
Merge branch 'main' into copilot/update-lease-files-script
tejaswiMinnu Apr 28, 2026
77504c7
Merge branch 'main' into copilot/update-lease-files-script
tejaswiMinnu Apr 30, 2026
2ed8223
Moved under arm-leases
tejaswiMinnu Apr 30, 2026
edf63c2
Merge branch 'main' into copilot/update-lease-files-script
tejaswiMinnu Apr 30, 2026
4b0a984
Exclude script file from arm-lease validation
tejaswiMinnu Apr 30, 2026
5e54a87
Add test for scripts folder allowance
tejaswiMinnu May 1, 2026
4051262
Exclude scripts folder from non-lease file check
tejaswiMinnu May 1, 2026
3af4dfb
Exclude scripts folder from folder structure validation
tejaswiMinnu May 1, 2026
c1723c8
Convert arm-lease scripts to CJS and tests to vitest format
tejaswiMinnu May 1, 2026
05e4745
Fix findRepoRoot to use __dirname in tests
tejaswiMinnu May 1, 2026
4d4841a
Moved to cmd folder
tejaswiMinnu May 1, 2026
ce82a02
Merge branch 'main' into copilot/update-lease-files-script
tejaswiMinnu May 1, 2026
6b5cdb3
Clean code
tejaswiMinnu May 1, 2026
d0e617a
Fix find repo root failure
tejaswiMinnu May 1, 2026
d6a161a
Fix tests
tejaswiMinnu May 1, 2026
5018220
Merge branch 'main' into copilot/update-lease-files-script
tejaswiMinnu May 1, 2026
a5f009c
Merge branch 'main' into copilot/update-lease-files-script
tejaswiMinnu May 1, 2026
4190869
Fix WF failure
tejaswiMinnu May 1, 2026
f5925ed
Prettier check
tejaswiMinnu May 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/arm-leases/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# ARM Leases - Design Discussion Period

## Overview
Expand Down Expand Up @@ -95,3 +95,56 @@
- **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/`

## Automation Scripts

Scripts are located in `.github/workflows/cmd/`. Run from the repository root.

### Single Resource Provider

**Syntax:**

```bash
# Without service groups
node .github/workflows/cmd/arm-lease-generate-lease-files.js --orgName <orgName> --rpNamespace <rpNamespace> --reviewer "@youralias" --startdate <YYYY-MM-DD> --duration <P#D>

# With service groups
node .github/workflows/cmd/arm-lease-generate-lease-files.js --orgName <orgName> --rpNamespace <rpNamespace> --reviewer "@youralias" --startdate <YYYY-MM-DD> --duration <P#D> --serviceName "<serviceName1>,<serviceName2>"
```

**Examples:**

```bash
# Without service groups
node .github/workflows/cmd/arm-lease-generate-lease-files.js --orgName storage --rpNamespace Microsoft.Storage --reviewer "@johndoe" --startdate 2026-05-01 --duration P90D

# With service groups
node .github/workflows/cmd/arm-lease-generate-lease-files.js --orgName compute --rpNamespace Microsoft.Compute --reviewer "@janesmith" --startdate 2026-05-01 --duration P180D --serviceName "DiskRP,ComputeRP,GalleryRP"
```

### Bulk Generation

**Syntax:**

```bash
# Generate resource provider list (without service groups)
node .github/workflows/cmd/arm-lease-fetch-resource-providers.js --output <filename>

# Generate resource provider list (with service groups)
node .github/workflows/cmd/arm-lease-fetch-resource-providers.js --with-service-groups --output <filename>

# Generate lease files from list
node .github/workflows/cmd/arm-lease-generate-lease-files.js --input <filename> --reviewer "@youralias" --startdate <YYYY-MM-DD> --duration <P#D>
```

**Examples:**

```bash
# RPs without service groups (e.g., Microsoft.Storage)
node .github/workflows/cmd/arm-lease-fetch-resource-providers.js --output rps-simple.txt
node .github/workflows/cmd/arm-lease-generate-lease-files.js --input rps-simple.txt --reviewer "@johndoe" --startdate 2026-04-16 --duration P180D

# RPs with service groups (e.g., Microsoft.Compute with DiskRP, ComputeRP)
node .github/workflows/cmd/arm-lease-fetch-resource-providers.js --with-service-groups --output rps-groups.txt
node .github/workflows/cmd/arm-lease-generate-lease-files.js --input rps-groups.txt --reviewer "@johndoe" --startdate 2026-04-16 --duration P180D
```
249 changes: 249 additions & 0 deletions .github/workflows/cmd/arm-lease-fetch-resource-providers.js

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, please move these scripts under .github/workflows/cmd, either grouped under a folder like arm-leases, or you could use a prefix like arm-lease-*.js. follow the conventions in this file:

https://github.com/Azure/azure-rest-api-specs/blob/main/.github/workflows/cmd/api-doc-preview.js

Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import { existsSync, readdirSync, statSync, writeFileSync } from "fs";
import { basename, dirname, join, relative, resolve } from "path";
import { fileURLToPath } from "url";
import { parseArgs } from "util";

/**
* Check if a directory is a service name directory (not stable, preview, common-types, or examples)
* @param {string} dirPath - Path to the directory
* @returns {boolean} True if it's a valid service name directory
*/
export function isServiceNameDirectory(dirPath) {
const excludeNames = new Set(["stable", "preview", "common-types", "examples"]);
try {
return !excludeNames.has(basename(dirPath)) && statSync(dirPath).isDirectory();
} catch {
return false;
}
}

/**
* Check if a resource provider path has version directories (stable or preview)
* @param {string} rpPath - Path to the resource provider
* @returns {boolean} True if it has version directories
*/
export function hasVersionDirectories(rpPath) {
return existsSync(join(rpPath, "stable")) || existsSync(join(rpPath, "preview"));
}

/**
* Check if a directory looks like the repository root
* @param {string} dir - Directory to check
* @returns {boolean} True if it looks like repo root
*/
function looksLikeRepoRoot(dir) {
return (
existsSync(join(dir, ".git")) ||
existsSync(join(dir, "specification")) ||
existsSync(join(dir, ".github"))
);
}

/**
* Find the repository root by looking for repo markers (.git, specification, .github)
* Supports sparse checkouts in CI by checking GITHUB_WORKSPACE first
* @param {string} [startPath] - Starting path to search from
* @returns {string} The repository root path
* @throws {Error} If repository root cannot be found
*/
export function findRepoRoot(startPath = process.cwd()) {
const workspace = process.env.GITHUB_WORKSPACE;
if (workspace && looksLikeRepoRoot(workspace)) {
return workspace;
}

let current = resolve(startPath);
for (let i = 0; i < 20; i++) {
if (looksLikeRepoRoot(current)) return current;
const parent = dirname(current);
if (parent === current) break;
current = parent;
}
throw new Error("Could not find repository root. Run from within azure-rest-api-specs.");
}

/**
* @typedef {Object} ResourceProvider
* @property {string} rpNamespace - Resource provider namespace (e.g., Microsoft.Storage)
* @property {string} path - Relative path to the resource provider
* @property {string} orgName - Organization name from specification folder
* @property {string[]} [serviceNames] - Service names if withServiceNames is true
*/

/**
* Find all resource providers in the specification directory
* @param {string} repoRoot - Repository root path
* @param {boolean} [withServiceNames=false] - Whether to include only RPs with service names
* @returns {ResourceProvider[]} Array of resource provider objects
*/
export function findResourceProviders(repoRoot, withServiceNames = false) {
const results = [];
const specDir = join(repoRoot, "specification");
if (!existsSync(specDir)) {
// In sparse checkouts (CI), specification/ may not exist. Return empty list.
return [];
}

for (const orgName of readdirSync(specDir)) {
const orgDir = join(specDir, orgName);
if (!statSync(orgDir).isDirectory()) continue;

const rmDir = join(orgDir, "resource-manager");
if (!existsSync(rmDir)) continue;

for (const rpNamespace of readdirSync(rmDir)) {
const rpPath = join(rmDir, rpNamespace);
if (!statSync(rpPath).isDirectory() || !rpNamespace.startsWith("Microsoft.")) {
continue;
}

const serviceNames = readdirSync(rpPath)
.filter((sn) => isServiceNameDirectory(join(rpPath, sn)))
.sort();

if (withServiceNames && serviceNames.length > 0) {
results.push({
rpNamespace: rpNamespace,
path: relative(repoRoot, rpPath),
orgName: orgName,
serviceNames: serviceNames,
});
} else if (!withServiceNames && serviceNames.length === 0 && hasVersionDirectories(rpPath)) {
results.push({
rpNamespace: rpNamespace,
path: relative(repoRoot, rpPath),
orgName: orgName,
});
}
}
}
return results.sort((a, b) => a.rpNamespace.localeCompare(b.rpNamespace));
}

/**
* Format resource providers for output
* @param {ResourceProvider[]} rps - Array of resource providers
* @param {"list"|"json"|"table"} fmt - Output format
* @param {boolean} withSN - Whether resource providers have service names
* @returns {string} Formatted output string
*/
export function formatOutput(rps, fmt, withSN) {
if (fmt === "json") return JSON.stringify(rps, null, 2);
if (rps.length === 0) {
return `No resource providers ${withSN ? "with" : "without"} serviceNames found.`;
}

if (fmt === "table") {
const maxOrg = Math.max(...rps.map((r) => r.orgName.length));
const maxRp = Math.max(...rps.map((r) => r.rpNamespace.length));
const header = withSN
? `${"orgName".padEnd(maxOrg)} ${"rpNamespace".padEnd(maxRp)} serviceNames`
: `${"orgName".padEnd(maxOrg)} ${"rpNamespace".padEnd(maxRp)} Path`;
const sep = `${"-".repeat(maxOrg)} ${"-".repeat(maxRp)} ${"-".repeat(60)}`;
const rows = withSN
? rps.map(
(r) =>
`${r.orgName.padEnd(maxOrg)} ${r.rpNamespace.padEnd(maxRp)} ${/** @type {string[]} */ (r.serviceNames).join(", ")}`,
)
: rps.map((r) => `${r.orgName.padEnd(maxOrg)} ${r.rpNamespace.padEnd(maxRp)} ${r.path}`);
return [header, sep, ...rows].join("\n");
}

return withSN
? rps
.map(
(r) =>
`${r.orgName}, ${r.rpNamespace}, [${/** @type {string[]} */ (r.serviceNames).join(", ")}]`,
)
.join("\n")
: rps.map((r) => `${r.orgName}, ${r.rpNamespace}`).join("\n");
}

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

function usage() {
console.log(`Usage:
npx arm-lease-fetch-resource-providers [options]

Options:
--with-service-groups Include only RPs with serviceNames (service groups)
--format <format> Output format: list, json, table (default: list)
--count Output only the count
--output <file> Write output to file instead of stdout
--repo-root <path> Repository root path (auto-detected if not provided)
--help Show this help message

Examples:
# RPs without service names
npx arm-lease-fetch-resource-providers

# RPs with service names
npx arm-lease-fetch-resource-providers --with-service-groups

# Output to file
npx arm-lease-fetch-resource-providers --output rps.txt

# JSON format
npx arm-lease-fetch-resource-providers --format json`);
}

// Only run CLI if this file is executed directly (not imported)
if (process.argv[1] === __filename) {
const {
values: {
"with-service-groups": withServiceGroups,
format,
count,
output,
"repo-root": repoRootArg,
help,
},
} = parseArgs({
options: {
"with-service-groups": { type: "boolean", default: false },
format: { type: "string", default: "list" },
count: { type: "boolean", default: false },
output: { type: "string", default: "" },
"repo-root": { type: "string", default: "" },
help: { type: "boolean", default: false },
},
allowPositionals: false,
});

if (help) {
usage();
process.exit(0);
}

try {
const repoRoot = repoRootArg
? resolve(repoRootArg)
: findRepoRoot(resolve(__dirname, "../../../"));
const rps = findResourceProviders(repoRoot, withServiceGroups);

let outputText;
if (count) {
outputText = String(rps.length);
} else {
outputText = formatOutput(
rps,
/** @type {"list"|"json"|"table"} */ (format),
withServiceGroups,
);
if (format !== "json") {
outputText += `\n\nTotal: ${rps.length} resource provider(s) ${withServiceGroups ? "with" : "without"} serviceNames`;
}
}

if (output) {
writeFileSync(output, outputText + "\n");
console.log(`Output written to ${output}`);
} else {
console.log(outputText);
}
} catch (error) {
console.error(`Error: ${/** @type {Error} */ (error).message}`);
process.exit(1);
}
}
Loading
Loading