Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 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/arm-leases/scripts/`. Run from the repository root.

### Single Resource Provider

**Syntax:**

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

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

**Examples:**

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

# With service groups
node .github/arm-leases/scripts/generate-lease-files.cjs --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/arm-leases/scripts/fetch-resource-providers.cjs --output <filename>

# Generate resource provider list (with service groups)
node .github/arm-leases/scripts/fetch-resource-providers.cjs --with-service-groups --output <filename>

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

**Examples:**

```bash
# RPs without service groups (e.g., Microsoft.Storage)
node .github/arm-leases/scripts/fetch-resource-providers.cjs --output rps-simple.txt
node .github/arm-leases/scripts/generate-lease-files.cjs --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/arm-leases/scripts/fetch-resource-providers.cjs --with-service-groups --output rps-groups.txt
node .github/arm-leases/scripts/generate-lease-files.cjs --input rps-groups.txt --reviewer "@johndoe" --startdate 2026-04-16 --duration P180D
```
220 changes: 220 additions & 0 deletions .github/arm-leases/scripts/fetch-resource-providers.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
#!/usr/bin/env node
// Fetch Azure resource providers with or without service names (service groups)
// Usage: node fetch-resource-providers.js [--with-service-groups] [--format list|json|table] [--count] [--output FILE]

const fs = require("fs");
const path = require("path");

function isServiceNameDirectory(dirPath) {
const excludeNames = new Set([
"stable",
"preview",
"common-types",
"examples",
]);
try {
return (
!excludeNames.has(path.basename(dirPath)) &&
fs.statSync(dirPath).isDirectory()
);
} catch {
return false;
}
}

function hasVersionDirectories(rpPath) {
return (
fs.existsSync(path.join(rpPath, "stable")) ||
fs.existsSync(path.join(rpPath, "preview"))
);
}

function findRepoRoot(startPath = process.cwd()) {
let current = path.resolve(startPath);
for (let i = 0; i < 6; i++) {
if (fs.existsSync(path.join(current, "specification"))) return current;
const parent = path.dirname(current);
if (parent === current) break;
current = parent;
}
throw new Error(
"Could not find repository root. Run from within azure-rest-api-specs.",
);
}

function findResourceProviders(repoRoot, withServiceNames = false) {
const results = [];
const specDir = path.join(repoRoot, "specification");
if (!fs.existsSync(specDir))
throw new Error(`Specification directory not found: ${specDir}`);

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

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

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

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

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

function formatOutput(rps, format, withSN) {
if (format === "json") return JSON.stringify(rps, null, 2);
if (rps.length === 0)
return `No resource providers ${withSN ? "with" : "without"} serviceNames found.`;

if (format === "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)} ${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}, [${r.serviceNames.join(", ")}]`,
)
.join("\n")
: rps.map((r) => `${r.orgName}, ${r.rpNamespace}`).join("\n");
}

function printHelp() {
console.log(`
Fetch Azure resource providers with or without service names (service groups)

Usage:
node fetch-resource-providers.js [options]

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

Examples:
# RPs without service names
node fetch-resource-providers.js

# RPs with service names
node fetch-resource-providers.js --with-service-groups

# Output to file
node fetch-resource-providers.js --output rps.txt

# JSON format
node fetch-resource-providers.js --format json
`);
}

function main() {
const args = {
repoRoot: null,
format: "list",
count: false,
withSN: false,
output: null,
};

for (let i = 2; i < process.argv.length; i++) {
const arg = process.argv[i];
if (arg === "--help" || arg === "-h") {
printHelp();
return 0;
}
if (arg === "--repo-root") args.repoRoot = process.argv[++i];
else if (arg === "--format") args.format = process.argv[++i];
else if (arg === "--output" || arg === "-o")
args.output = process.argv[++i];
else if (arg === "--count") args.count = true;
else if (arg === "--with-service-groups") args.withSN = true;
}

try {
const repoRoot = args.repoRoot
? path.resolve(args.repoRoot)
: findRepoRoot();
const rps = findResourceProviders(repoRoot, args.withSN);

let outputText;
if (args.count) {
outputText = String(rps.length);
} else {
outputText = formatOutput(rps, args.format, args.withSN);
if (args.format !== "json") {
outputText += `\n\nTotal: ${rps.length} resource provider(s) ${args.withSN ? "with" : "without"} serviceNames`;
}
}

if (args.output) {
fs.writeFileSync(args.output, outputText + "\n");
console.log(`Output written to ${args.output}`);
} else {
console.log(outputText);
}
return 0;
} catch (error) {
console.error(`Error: ${error.message}`);
return 1;
}
}

if (require.main === module) {
process.exit(main());
}

module.exports = {
findRepoRoot,
findResourceProviders,
formatOutput,
isServiceNameDirectory,
hasVersionDirectories,
};
Loading
Loading