Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
70 changes: 68 additions & 2 deletions tools/tsp-client/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ export async function initCommand(argv: any) {
export async function syncCommand(argv: any) {
let outputDir = argv["output-dir"];
let localSpecRepo = argv["local-spec-repo"];
const batch = argv["batch"] ?? false;

const tempRoot = await createTempDirectory(outputDir);
const repoRoot = await getRepoRoot(outputDir);
Expand All @@ -312,6 +313,11 @@ export async function syncCommand(argv: any) {
throw new Error("Could not find repo root");
}
const tspLocation: TspLocation = await readTspLocation(outputDir);
if (!tspLocation.directory || !tspLocation.commit || !tspLocation.repo) {
throw new Error(
"tsp-location.yaml is missing required field(s) for sync operation: directory, commit, repo",
);
}
const emitterPackageJsonPath = getEmitterPackageJsonPath(repoRoot, tspLocation);
const dirSplit = tspLocation.directory.split("/");
let projectName = dirSplit[dirSplit.length - 1];
Expand All @@ -323,6 +329,12 @@ export async function syncCommand(argv: any) {
await mkdir(srcDir, { recursive: true });

if (localSpecRepo) {
if (batch) {
localSpecRepo = resolve(localSpecRepo, tspLocation.directory);
Logger.info(
`Resolved local spec repo path using tsp-location.yaml directory: ${localSpecRepo}`,
);
}
if (localSpecRepo.endsWith("tspconfig.yaml")) {
// If the path is to tspconfig.yaml, we need to remove it to get the spec directory
localSpecRepo = localSpecRepo.split("tspconfig.yaml")[0];
Expand Down Expand Up @@ -405,6 +417,11 @@ export async function generateCommand(argv: any) {

const tempRoot = joinPaths(outputDir, "TempTypeSpecFiles");
const tspLocation = await readTspLocation(outputDir);
if (!tspLocation.directory) {
throw new Error(
"tsp-location.yaml is missing required field(s) for generate operation: directory",
);
}
const dirSplit = tspLocation.directory.split("/");
let projectName = dirSplit[dirSplit.length - 1];
if (!projectName) {
Expand Down Expand Up @@ -492,24 +509,73 @@ export async function generateCommand(argv: any) {
}
}

/**
* Processes batch updates for multiple directories specified in the tsp-location.yaml file.
*
* Iterates over each directory listed in the `batch` property of the provided TspLocation object,
* updating each by invoking the updateCommand with the appropriate output directory.
* If any batch directory fails to process, the function logs the error and immediately throws,
* halting further batch processing.
*
* @param tspLocation - The TspLocation object containing batch directory information.
* @param outputDir - The base output directory where batch directories are located.
* @param argv - Command line arguments object, which will be updated for each batch directory.
* @returns Promise that resolves when all batch directories have been processed successfully.
* @throws Error if processing any batch directory fails.
*/
async function processBatchUpdate(tspLocation: TspLocation, outputDir: string, argv: any) {
// Process each directory in the batch
for (const batchDir of tspLocation.batch ?? []) {
const fullBatchPath = resolve(outputDir, batchDir);
Logger.info(`Processing batch directory: ${batchDir}`);

try {
argv["output-dir"] = fullBatchPath;
await updateCommand(argv);
Logger.info(`Successfully processed batch directory: ${batchDir}`);
} catch (error) {
Logger.error(`Failed to process batch directory ${batchDir}: ${error}`);
throw error; // Stop processing and propagate the error immediately
}
}

Logger.info("All batch directories processed successfully");
}

export async function updateCommand(argv: any) {
const outputDir = argv["output-dir"];
const repo = argv["repo"];
const commit = argv["commit"];
let tspConfig = argv["tsp-config"];

const tspLocation: TspLocation = await readTspLocation(outputDir);

// Check if this is a batch configuration
if (tspLocation.batch) {
Logger.info(`Found batch configuration with ${tspLocation.batch.length} directories`);
if (argv["local-spec-repo"]) {
const specRepoRoot = await getRepoRoot(argv["local-spec-repo"]);
Logger.info(
`During batch processing will use local spec repo root with child library tsp-location.yaml data to resolve path to typespec project directory: ${specRepoRoot}`,
);
argv["local-spec-repo"] = specRepoRoot;
argv["batch"] = true;
}
await processBatchUpdate(tspLocation, outputDir, argv);
return;
}

// Original non-batch logic
if (repo && !commit) {
throw new Error(
"Commit SHA is required when specifying `--repo`; please specify a commit using `--commit`",
);
}
if (commit) {
const tspLocation: TspLocation = await readTspLocation(outputDir);
tspLocation.commit = commit ?? tspLocation.commit;
tspLocation.repo = repo ?? tspLocation.repo;
await writeTspLocationYaml(tspLocation, outputDir);
} else if (tspConfig) {
const tspLocation: TspLocation = await readTspLocation(outputDir);
tspConfig = resolveTspConfigUrl(tspConfig);
tspLocation.commit = tspConfig.commit ?? tspLocation.commit;
tspLocation.repo = tspConfig.repo ?? tspLocation.repo;
Expand Down
22 changes: 19 additions & 3 deletions tools/tsp-client/src/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,31 @@ export async function readTspLocation(rootDir: string): Promise<TspLocation> {
if (fileStat.isFile()) {
const fileContents = await readFile(yamlPath, "utf8");
const tspLocation: TspLocation = parseYaml(fileContents);
if (!tspLocation.directory || !tspLocation.commit || !tspLocation.repo) {
throw new Error("Invalid tsp-location.yaml");

if (
!tspLocation.batch &&
(!tspLocation.directory || !tspLocation.commit || !tspLocation.repo)
) {
// For non-batch configurations, require the standard fields
throw new Error(
"Invalid tsp-location.yaml, missing required fields: directory, commit, repo",
);
} else if (tspLocation.batch) {
if (!Array.isArray(tspLocation.batch)) {
throw new Error("Invalid tsp-location.yaml: batch must be an array of directory paths");
}
if (tspLocation.directory || tspLocation.commit || tspLocation.repo) {
throw new Error(
"Invalid tsp-location.yaml: batch configuration cannot have directory, commit, or repo fields",
);
}
}
if (!tspLocation.additionalDirectories) {
tspLocation.additionalDirectories = [];
}

// Normalize the directory path and remove trailing slash
tspLocation.directory = normalizeDirectory(tspLocation.directory);
tspLocation.directory = normalizeDirectory(tspLocation.directory ?? "");
if (typeof tspLocation.additionalDirectories === "string") {
tspLocation.additionalDirectories = [normalizeDirectory(tspLocation.additionalDirectories)];
} else {
Expand Down
7 changes: 4 additions & 3 deletions tools/tsp-client/src/typespec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import { readFile, readdir, realpath, stat } from "fs/promises";
import { pathToFileURL } from "url";

export interface TspLocation {
directory: string;
commit: string;
repo: string;
directory?: string;
commit?: string;
repo?: string;
additionalDirectories?: string[];
entrypointFile?: string;
emitterPackageJsonPath?: string;
batch?: string[];
}

export function resolveTspConfigUrl(configUrl: string): {
Expand Down
62 changes: 61 additions & 1 deletion tools/tsp-client/test/commands.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
generateLockFileCommand,
generateConfigFilesCommand,
} from "../src/commands.js";
import { afterAll, afterEach, beforeAll, describe, it } from "vitest";
import { afterAll, afterEach, beforeAll, describe, it, expect } from "vitest";
import { assert } from "chai";
import { getRepoRoot } from "../src/git.js";
import { cwd } from "node:process";
Expand Down Expand Up @@ -53,6 +53,7 @@ describe.sequential("Verify commands", () => {
"./test/examples/sdk/contosowidgetmanager/contosowidgetmanager-rest/TempTypeSpecFiles/",
{ recursive: true },
);
await rm(joinPaths(repoRoot, "sdk/keyvault"), { recursive: true, force: true });
});

it("Generate lock file", async () => {
Expand Down Expand Up @@ -767,4 +768,63 @@ describe.sequential("Verify commands", () => {
assert.fail("Failed to generate tsp-client config files. Error: " + error);
}
}, 360000);

it("should read batch configuration from tsp-location.yaml", async () => {
const tspLocation = await readTspLocation("test/examples/batch");

expect(tspLocation.batch).toBeDefined();
expect(Array.isArray(tspLocation.batch)).toBe(true);
expect(tspLocation.batch).toHaveLength(3);
expect(tspLocation.batch).toContain("./rbac");
expect(tspLocation.batch).toContain("./settings");
expect(tspLocation.batch).toContain("./restore");
});

it("process batch directories in updateCommand", async () => {
const argv = {
"output-dir": "./test/examples/batch",
};

try {
await updateCommand(argv);

// Verify that output directories were created for each batch item
assert.isTrue(
(await stat(joinPaths(repoRoot, "sdk/keyvault/keyvault-admin/rbac"))).isDirectory(),
);
assert.isTrue(
(await stat(joinPaths(repoRoot, "sdk/keyvault/keyvault-admin/settings"))).isDirectory(),
);
assert.isTrue(
(await stat(joinPaths(repoRoot, "sdk/keyvault/keyvault-admin/restore"))).isDirectory(),
);
} finally {
await removeDirectory(joinPaths(repoRoot, "sdk/keyvault"));
}
}, 360000);

it("process batch directories in updateCommand with local spec path", async () => {
const argv = {
"output-dir": "./test/examples/batch",
"local-spec-repo": "./test/examples/batch/service",
"emitter-package-json-path": "tools/tsp-client/test/examples/batch/service/package.json",
};

try {
await updateCommand(argv);

// Verify that output directories were created for each batch item
assert.isTrue(
(await stat(joinPaths(repoRoot, "sdk/keyvault/keyvault-admin/rbac"))).isDirectory(),
);
assert.isTrue(
(await stat(joinPaths(repoRoot, "sdk/keyvault/keyvault-admin/settings"))).isDirectory(),
);
assert.isTrue(
(await stat(joinPaths(repoRoot, "sdk/keyvault/keyvault-admin/restore"))).isDirectory(),
);
} finally {
await removeDirectory(joinPaths(repoRoot, "sdk/keyvault"));
}
});
});
5 changes: 5 additions & 0 deletions tools/tsp-client/test/examples/batch/rbac/tsp-location.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
directory: tools/tsp-client/test/examples/batch/service/rbac
commit: 2496bc9d427ab109ebee2c1b8fd38f4dcf000cda
repo: Azure/azure-sdk-tools
additionalDirectories:
emitterPackageJsonPath: tools/tsp-client/test/examples/batch/service/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
directory: tools/tsp-client/test/examples/batch/service/restore
commit: 2496bc9d427ab109ebee2c1b8fd38f4dcf000cda
repo: Azure/azure-sdk-tools
additionalDirectories:
emitterPackageJsonPath: tools/tsp-client/test/examples/batch/service/package.json
17 changes: 17 additions & 0 deletions tools/tsp-client/test/examples/batch/service/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "typescript-emitter-package",
"main": "dist/src/index.js",
"dependencies": {
"@azure-tools/typespec-ts": "0.46.1",
"@azure-tools/typespec-azure-core": "0.62.0",
"@azure-tools/typespec-autorest": "0.62.0",
"@azure-tools/typespec-client-generator-core": "0.62.0",
"@azure-tools/typespec-azure-resource-manager": "0.62.0",
"@azure-tools/typespec-azure-rulesets": "0.62.0",
"@azure-tools/typespec-liftr-base": "0.11.0",
"@typespec/compiler": "1.6.0",
"@typespec/http": "1.6.0",
"@typespec/rest": "0.76.0",
"@typespec/versioning": "0.76.0"
}
}
42 changes: 42 additions & 0 deletions tools/tsp-client/test/examples/batch/service/rbac/main.tsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import "@typespec/rest";
import "@typespec/http";
import "@typespec/versioning";
import "@azure-tools/typespec-azure-core";

using TypeSpec.Http;
using TypeSpec.Rest;
using TypeSpec.Versioning;
using Azure.Core;

/**
* The key vault client performs cryptographic key operations and vault operations against the Key Vault service.
*/
@useAuth(
OAuth2Auth<[
{
type: OAuth2FlowType.implicit,
authorizationUrl: "https://login.microsoftonline.com/common/oauth2/authorize",
scopes: ["https://vault.azure.net/.default"],
}
]>
)
@service(#{ title: "KeyVaultClient" })
@versioned(Versions)
@server(
"{vaultBaseUrl}",
"The key vault client performs cryptographic key operations and vault operations against the Key Vault service.",
{
vaultBaseUrl: url,
}
)
namespace KeyVault.RBAC;

/**
* The available API versions.
*/
enum Versions {
/**
* The 2025-07-01 API version.
*/
v2025_07_01: "2025-07-01",
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
parameters:
"service-dir":
default: "sdk/keyvault"
"dependencies":
default: ""
emit:
# - "@azure-tools/typespec-autorest"
options:
"@azure-tools/typespec-autorest":
azure-resource-provider-folder: "data-plane"
emitter-output-dir: "{project-root}/.."
output-file: "{azure-resource-provider-folder}/Microsoft.KeyVault/{version-status}/{version}/rbac.json"
"@azure-tools/typespec-ts":
emitter-output-dir: "{output-dir}/{service-dir}/keyvault-admin/rbac"
generate-metadata: true
src-folder: "src/generated/rbac"
experimental-extensible-enums: true
is-modular-library: true
package-details:
name: "@azure/keyvault-admin"
description: "Azure Key Vault Administration"
flavor: azure
"@azure-tools/typespec-go":
containing-module: "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azadmin"
service-dir: "sdk/security/keyvault"
emitter-output-dir: "{output-dir}/{service-dir}/azadmin/rbac"
go-generate: build.go
inject-spans: true
single-client: true
generate-fakes: true
omit-constructors: true
"@azure-tools/typespec-client-generator-cli":
additionalDirectories:
- "specification/keyvault/Security.KeyVault.Common/"
Loading