import { APIViewRequestData, SdkName, SdkNameSchema, SpecGenSdkArtifactInfo, } from "@azure-tools/specs-shared/sdk-types"; import { spawnSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { inspect } from "node:util"; import { LogIssueType, LogLevel, logMessage, setVsoVariable, vsoLogIssue } from "./log.js"; import { groupSpecConfigPaths } from "./spec-helpers.js"; import { ExecutionReport, SpecGenSdkCmdInput, SpecGenSdkRequiredSettings, VsoLogs, } from "./types.js"; import { execAsync, findReadmeFiles, getAllTypeSpecPaths, getArgumentValue, objectToMap, SpecConfigs, } from "./utils.js"; /** * Load execution-report.json. * @param commandInput - The command input. * @returns the execution report JSON */ export function getExecutionReport(commandInput: SpecGenSdkCmdInput): ExecutionReport { // Read the execution report to determine if the generation was successful const executionReportPath = path.join( commandInput.workingFolder, `${commandInput.sdkRepoName}_tmp/execution-report.json`, ); return JSON.parse(fs.readFileSync(executionReportPath, "utf8")) as ExecutionReport; } /** * Set the pipeline variables for the SDK pull request. * @param stagedArtifactsFolder - The staged artifacts folder. * @param skipPrVariables - A flag indicating whether to skip setting PR variables. * @param packageName - The package name. * @param installationInstructions - The installation instructions. */ export function setPipelineVariables( stagedArtifactsFolder: string, skipPrVariables: boolean = true, packageName: string = "", installationInstructions: string = "", ): void { if (!skipPrVariables) { const branchName = `sdkauto/${packageName?.replace("/", "-")}`; const prTitle = `[AutoPR ${packageName}]`; const prBody = installationInstructions; setVsoVariable("PrBranch", branchName); setVsoVariable("PrTitle", prTitle); setVsoVariable("PrBody", prBody); } if (stagedArtifactsFolder) { setVsoVariable("StagedArtifactsFolder", stagedArtifactsFolder); } } /** * Parse the arguments. * @returns The spec-gen-sdk command input. */ export function parseArguments(): SpecGenSdkCmdInput { const __filename: string = fileURLToPath(import.meta.url); const __dirname: string = path.dirname(__filename); // Get the arguments passed to the script const args: string[] = process.argv.slice(2); const localSpecRepoPath: string = path.resolve( getArgumentValue(args, "--scp", path.join(__dirname, "..", "..")), ); const sdkRepoName: string = getArgumentValue(args, "--lang", "azure-sdk-for-net"); const localSdkRepoPath: string = path.resolve( getArgumentValue(args, "--sdp", path.join(localSpecRepoPath, "..", sdkRepoName)), ); const workingFolder: string = path.resolve( getArgumentValue(args, "--wf", path.join(localSpecRepoPath, "..")), ); // Set runMode to "release" by default let runMode = "release"; const batchType: string = getArgumentValue(args, "--batch-type", ""); const pullRequestNumber: string = getArgumentValue(args, "--pr-number", ""); if (batchType) { runMode = "batch"; } else if (pullRequestNumber) { runMode = "spec-pull-request"; } return { workingFolder, localSpecRepoPath, localSdkRepoPath, sdkRepoName, sdkLanguage: SdkNameSchema.parse(sdkRepoName.replace("-pr", "")), runMode, tspConfigPath: getArgumentValue(args, "--tsp-config-relative-path", ""), readmePath: getArgumentValue(args, "--readme-relative-path", ""), prNumber: getArgumentValue(args, "--pr-number", ""), apiVersion: getArgumentValue(args, "--api-version", ""), sdkReleaseType: getArgumentValue(args, "--sdk-release-type", ""), specCommitSha: getArgumentValue(args, "--commit", "HEAD"), specRepoHttpsUrl: getArgumentValue(args, "--spec-repo-url", ""), headRepoHttpsUrl: getArgumentValue(args, "--head-repo-url", ""), headBranch: getArgumentValue(args, "--head-branch", ""), }; } /** * Prepare the spec-gen-sdk command. * @param commandInput The command input. * @returns The spec-gen-sdk command. */ export function prepareSpecGenSdkCommand(commandInput: SpecGenSdkCmdInput): string[] { const specGenSdkCommand = []; specGenSdkCommand.push( "spec-gen-sdk", "--scp", commandInput.localSpecRepoPath, "--sdp", commandInput.localSdkRepoPath, "--wf", commandInput.workingFolder, "-l", commandInput.sdkRepoName, "-c", commandInput.specCommitSha, "--rm", commandInput.runMode, ); if (commandInput.specRepoHttpsUrl) { specGenSdkCommand.push("--spec-repo-https-url", commandInput.specRepoHttpsUrl); } if (commandInput.prNumber) { specGenSdkCommand.push("--pr-number", commandInput.prNumber); } if (commandInput.tspConfigPath) { specGenSdkCommand.push("--tsp-config-relative-path", commandInput.tspConfigPath); } if (commandInput.readmePath) { specGenSdkCommand.push("--readme-relative-path", commandInput.readmePath); } if (commandInput.headRepoHttpsUrl) { specGenSdkCommand.push("--head-repo-url", commandInput.headRepoHttpsUrl); } if (commandInput.headBranch) { specGenSdkCommand.push("--head-branch", commandInput.headBranch); } if (commandInput.apiVersion) { specGenSdkCommand.push("--api-version", commandInput.apiVersion); } if (commandInput.sdkReleaseType) { specGenSdkCommand.push("--sdk-release-type", commandInput.sdkReleaseType); } return specGenSdkCommand; } /** * Get the spec paths based on the batch run type. * @param batchType The batch run type. * @param specRepoPath The specification repository path. * @returns The specConfigs array. */ export function getSpecPaths(batchType: string, specRepoPath: string): SpecConfigs[] { let tspconfigs: string[] = []; let readmes: string[] = []; let skipUnmatchedReadmes = false; switch (batchType) { case "all-specs": { tspconfigs = getAllTypeSpecPaths(specRepoPath); readmes = findReadmeFiles(path.join(specRepoPath, "specification")); break; } case "all-typespecs": { tspconfigs = getAllTypeSpecPaths(specRepoPath); readmes = findReadmeFiles(path.join(specRepoPath, "specification")); skipUnmatchedReadmes = true; break; } case "all-mgmtplane-typespecs": { tspconfigs = getAllTypeSpecPaths(specRepoPath).filter((p) => p.includes(".Management")); readmes = findReadmeFiles(path.join(specRepoPath, "specification")).filter((p) => p.includes("resource-manager"), ); skipUnmatchedReadmes = true; break; } case "all-dataplane-typespecs": { tspconfigs = getAllTypeSpecPaths(specRepoPath).filter((p) => !p.includes(".Management")); readmes = findReadmeFiles(path.join(specRepoPath, "specification")).filter((p) => p.includes("data-plane"), ); skipUnmatchedReadmes = true; break; } case "all-openapis": { readmes = findReadmeFiles(path.join(specRepoPath, "specification")); break; } case "all-mgmtplane-openapis": { readmes = findReadmeFiles(path.join(specRepoPath, "specification")).filter((p) => p.includes("resource-manager"), ); break; } case "all-dataplane-openapis": { readmes = findReadmeFiles(path.join(specRepoPath, "specification")).filter((p) => p.includes("data-plane"), ); break; } case "sample-typespecs": { tspconfigs = [ "specification/contosowidgetmanager/Contoso.Management/tspconfig.yaml", "specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", ]; } } return groupSpecConfigPaths(tspconfigs, readmes, skipUnmatchedReadmes); } /** * Logs issues to Azure DevOps Pipeline * * @param logPath - The vso log file path. * @param specConfigDisplayText - The display text for the spec configuration. */ export function logIssuesToPipeline(logPath: string, specConfigDisplayText: string): void { let vsoLogs: VsoLogs; try { const logContent = JSON.parse(fs.readFileSync(logPath, "utf8")) as Record< string, { errors?: string[]; warnings?: string[] } >; vsoLogs = objectToMap(logContent); } catch (error) { throw new Error(`Runner: error reading log at ${logPath}:${inspect(error)}`, { cause: error }); } if (vsoLogs) { const errors = [...vsoLogs.values()].flatMap((entry) => entry.errors ?? []); const warnings = [...vsoLogs.values()].flatMap((entry) => entry.warnings ?? []); if (errors.length > 0) { const errorTitle = `Errors occurred while generating SDK from ${specConfigDisplayText}. ` + `Follow the steps at https://aka.ms/azsdk/sdk-automation-faq#how-to-view-the-detailed-sdk-generation-errors to view detailed errors.`; logMessage(errorTitle, LogLevel.Group); const errorsWithTitle = [errorTitle, ...errors]; vsoLogIssue(errorsWithTitle.join("%0D%0A")); logMessage("ending group logging", LogLevel.EndGroup); } if (warnings.length > 0) { const warningTitle = `Warnings occurred while generating SDK from ${specConfigDisplayText}. ` + `Follow the steps at https://aka.ms/azsdk/sdk-automation-faq#how-to-view-the-detailed-sdk-generation-errors to view detailed warnings.`; logMessage(warningTitle, LogLevel.Group); const warningsWithTitle = [warningTitle, ...warnings]; vsoLogIssue(warningsWithTitle.join("%0D%0A"), LogIssueType.Warning); logMessage("ending group logging", LogLevel.EndGroup); } } } /** * Process the breaking change label artifacts. * * @param executionReport - The spec-gen-sdk execution report. * @returns flag of lable breaking change. */ export function getBreakingChangeInfo(executionReport: ExecutionReport): boolean { for (const packageInfo of executionReport.packages) { if (packageInfo.shouldLabelBreakingChange) { return true; } } return false; } /** * Generate the spec-gen-sdk artifacts. * @param commandInput - The command input. * @param result - The spec-gen-sdk execution result. * @param hasBreakingChange - A flag indicating whether there are breaking changes. * @param hasManagementPlaneSpecs - A flag indicating whether there are management plane specs. * @param hasTypeSpecProjects - A flag indicating whether there are TypeSpec projects. * @param stagedArtifactsFolder - The staged artifacts folder. * @param apiViewRequestData - The API view request data. * @param sdkGenerationExecuted - A flag indicating whether the SDK generation was executed. * @returns the run status code. */ export function generateArtifact( commandInput: SpecGenSdkCmdInput, result: string, hasBreakingChange: boolean, hasManagementPlaneSpecs: boolean, hasTypeSpecProjects: boolean, stagedArtifactsFolder: string, apiViewRequestData: APIViewRequestData[], sdkGenerationExecuted: boolean = true, ): number { const specGenSdkArtifactName = "spec-gen-sdk-artifact"; const specGenSdkArtifactFileName = specGenSdkArtifactName + ".json"; const specGenSdkArtifactPath = "out/spec-gen-sdk-artifact"; const specGenSdkArtifactAbsoluteFolder = path.join( commandInput.workingFolder, specGenSdkArtifactPath, ); try { if (!fs.existsSync(specGenSdkArtifactAbsoluteFolder)) { fs.mkdirSync(specGenSdkArtifactAbsoluteFolder, { recursive: true }); } let isSpecGenSdkCheckRequired = false; if (sdkGenerationExecuted) { isSpecGenSdkCheckRequired = getRequiredSettingValue( hasManagementPlaneSpecs, hasTypeSpecProjects, commandInput.sdkLanguage, ); } // Write artifact const artifactInfo: SpecGenSdkArtifactInfo = { language: commandInput.sdkLanguage, result, headSha: commandInput.specCommitSha, prNumber: commandInput.prNumber, labelAction: hasBreakingChange, isSpecGenSdkCheckRequired, apiViewRequestData: apiViewRequestData, }; fs.writeFileSync( path.join(commandInput.workingFolder, specGenSdkArtifactPath, specGenSdkArtifactFileName), JSON.stringify(artifactInfo, undefined, 2), ); setVsoVariable("SpecGenSdkArtifactName", specGenSdkArtifactName); setVsoVariable("SpecGenSdkArtifactPath", specGenSdkArtifactPath); setVsoVariable("StagedArtifactsFolder", stagedArtifactsFolder); setVsoVariable("HasAPIViewArtifact", apiViewRequestData.length > 0 ? "true" : "false"); } catch (error) { logMessage("Runner: errors occurred while processing breaking change", LogLevel.Group); vsoLogIssue(`Runner: errors writing breaking change label artifacts:${inspect(error)}`); logMessage("ending group logging", LogLevel.EndGroup); return 1; } return 0; } /** * Get the service folder path from the spec config path. * @param specConfigPath * @returns The service folder path. */ export function getServiceFolderPath(specConfigPath: string): string { if (!specConfigPath || specConfigPath.length === 0) { return ""; } const segments = specConfigPath.split("/"); if (segments.length > 2) { return `${segments[0]}/${segments[1]}`; } return specConfigPath; } /** * Get the required setting value for the SDK check based on the spec PR types. * @param hasManagementPlaneSpecs - A flag indicating whether there are management plane specs. * @param hasTypeSpecProjects - A flag indicating whether there are TypeSpec projects. * @param sdkName - The SDK name. * @returns boolean indicating whether the SDK check is required. */ export function getRequiredSettingValue( hasManagementPlaneSpecs: boolean, hasTypeSpecProjects: boolean, sdkName: SdkName, ): boolean { // If the SDK is azure-sdk-for-net, return false if there are no TypeSpec projects. if (sdkName === "azure-sdk-for-net" && !hasTypeSpecProjects) { return false; } if (hasManagementPlaneSpecs) { return SpecGenSdkRequiredSettings[sdkName].managementPlane; } else { return SpecGenSdkRequiredSettings[sdkName].dataPlane; } } /** * Indicates which generation tool should be used for a given spec. * - "azsdk-cli": Use azsdk-cli for generation (Rust TypeSpec specs) * - "spec-gen-sdk": Use existing spec-gen-sdk tool * - "unsupported": The required tool is not available (e.g., Rust without azsdk-cli) */ export type GenerationTool = "azsdk-cli" | "spec-gen-sdk" | "unsupported"; /** * Determines whether to use azsdk-cli or spec-gen-sdk for a given spec. * Currently only uses azsdk-cli for Rust TypeSpec specs when the CLI is * available. All other languages and OpenAPI specs use spec-gen-sdk. * Returns "unsupported" if Rust is requested but azsdk-cli is not installed. */ export function selectGenerationTool( tspConfigPath?: string, _readmePath?: string, sdkLanguage?: SdkName, ): GenerationTool { if (tspConfigPath && sdkLanguage === "azure-sdk-for-rust") { return isAzsdkCliAvailable() ? "azsdk-cli" : "unsupported"; } return "spec-gen-sdk"; } /** * Checks whether the azsdk CLI tool is available on the system. * Returns true if the tool can be found via the AZSDK env variable or on PATH. */ function isAzsdkCliAvailable(): boolean { try { // Pipeline sets AZSDK variable to the installed path const azsdkPath = process.env.AZSDK; const executable = azsdkPath || "azsdk"; const result = spawnSync(executable, ["--version"], { shell: false, stdio: "ignore", timeout: 5000, }); return result.status === 0; } catch { return false; } } /** * Prepare the azsdk pkg generate command arguments. * * @param commandInput - The spec-gen-sdk command input. * @param tspConfigRelativePath - Relative path to tspconfig.yaml within the spec repo. * @returns Array of arguments for the azsdk command. */ export function prepareAzsdkGenerateCommand( commandInput: SpecGenSdkCmdInput, tspConfigRelativePath: string, ): string[] { const tspConfigFullPath = path.join(commandInput.localSpecRepoPath, tspConfigRelativePath); return [ "pkg", "generate", "-r", commandInput.localSdkRepoPath, "-t", tspConfigFullPath, "--output", "json", ]; } /** * Prepare the azsdk pkg build command arguments. * * @param packagePath - Absolute path to the generated SDK package directory. * @returns Array of arguments for the azsdk build command. */ export function prepareAzsdkBuildCommand(packagePath: string): string[] { return ["pkg", "build", "--package-path", packagePath, "--output", "json"]; } /** * Prepare the azsdk pkg pack command arguments. * * @param packagePath - Absolute path to the generated SDK package directory. * @param outputPath - Optional output directory for artifacts. * @returns Array of arguments for the azsdk pack command. */ export function prepareAzsdkPackCommand(packagePath: string, outputPath?: string): string[] { const args = ["pkg", "pack", "--package-path", packagePath, "--output", "json"]; if (outputPath) { args.push("--output-path", outputPath); } return args; } /** * Resolves the generated package directory path from typespec-metadata output. * The outputDir in metadata uses `{output-dir}` placeholder which resolves to the SDK repo root. * * @param outputDir - The outputDir from typespec-metadata (e.g., "{output-dir}/sdk/keyvault/azure-keyvault-keys") * @param sdkRepoPath - Absolute path to the SDK repository root. * @returns Absolute path to the generated package directory. */ export function resolvePackagePath(outputDir: string, sdkRepoPath: string): string { // Replace {output-dir} placeholder with the SDK repo path const resolved = outputDir.replace("{output-dir}", sdkRepoPath); return path.resolve(resolved); } /** * Installs language-specific toolchains required before SDK generation. * Currently only Rust needs this — runs `rustup install` in the SDK repo root * to pick up the version from rust-toolchain.toml. */ export async function installLanguageToolchain(commandInput: SpecGenSdkCmdInput): Promise { if (commandInput.sdkLanguage === "azure-sdk-for-rust") { logMessage(`Installing Rust toolchain from SDK repo`, LogLevel.Info); await execAsync("rustup install", { cwd: commandInput.localSdkRepoPath }); } }