Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
58 changes: 53 additions & 5 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 @@ -313,7 +314,7 @@ export async function syncCommand(argv: any) {
}
const tspLocation: TspLocation = await readTspLocation(outputDir);
const emitterPackageJsonPath = getEmitterPackageJsonPath(repoRoot, tspLocation);
const dirSplit = tspLocation.directory.split("/");
const dirSplit = tspLocation.directory!.split("/");
let projectName = dirSplit[dirSplit.length - 1];
Logger.debug(`Using project name: ${projectName}`);
if (!projectName) {
Expand All @@ -323,6 +324,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 @@ -360,12 +367,12 @@ export async function syncCommand(argv: any) {
Logger.debug(`Cloning repo to ${cloneDir}`);
await cloneRepo(tempRoot, cloneDir, `https://github.com/${tspLocation.repo}.git`);
await sparseCheckout(cloneDir);
await addSpecFiles(cloneDir, tspLocation.directory);
await addSpecFiles(cloneDir, tspLocation.directory!);
for (const dir of tspLocation.additionalDirectories ?? []) {
Logger.info(`Processing additional directory: ${dir}`);
await addSpecFiles(cloneDir, dir);
}
await checkoutCommit(cloneDir, tspLocation.commit);
await checkoutCommit(cloneDir, tspLocation.commit!);
await cp(joinPaths(cloneDir, tspLocation.directory), srcDir, { recursive: true });
for (const dir of tspLocation.additionalDirectories!) {
Logger.info(`Syncing additional directory: ${dir}`);
Expand Down Expand Up @@ -405,6 +412,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 +504,60 @@ export async function generateCommand(argv: any) {
}
}

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");
return;
}

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
13 changes: 11 additions & 2 deletions tools/tsp-client/src/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,24 @@ 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) {

if (
!tspLocation.batch &&
(!tspLocation.directory || !tspLocation.commit || !tspLocation.repo)
) {
// For non-batch configurations, require the standard fields
throw new Error("Invalid tsp-location.yaml");
} 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.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
22 changes: 22 additions & 0 deletions tools/tsp-client/src/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,25 @@ export async function nodeCommand(workingDir: string, args: string[]): Promise<v
});
});
}

export async function tspClientCommand(workingDir: string, args: string[]): Promise<void> {
Logger.debug("tsp-client " + args.join(" "));

return new Promise((resolve, reject) => {
const tspClient = spawn("tsp-client", args, {
cwd: workingDir,
stdio: "inherit",
shell: true,
});
tspClient.once("exit", (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`tsp-client ${args[0]} failed with exit code ${code}`));
}
});
tspClient.once("error", (err) => {
reject(new Error(`tsp-client ${args[0]} failed with error: ${err}`));
});
});
}
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
56 changes: 55 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, beforeAll, describe, it } from "vitest";
import { afterAll, 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 @@ -47,6 +47,7 @@ describe.sequential("Verify commands", () => {
await rm("./test/examples/initGlobalConfig/", { recursive: true });
await rm("./test/examples/initGlobalConfigNoMatch/", { recursive: true });
await rm(joinPaths(repoRoot, "sdk/contosowidgetmanager"), { recursive: true });
await rm(joinPaths(repoRoot, "sdk/keyvault"), { recursive: true });
});

it("Generate lock file", async () => {
Expand Down Expand Up @@ -775,4 +776,57 @@ 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",
};

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(),
);
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",
};

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(),
);
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
Loading