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
14 changes: 4 additions & 10 deletions eng/tools/spec-gen-sdk-runner/src/command-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import fs from "node:fs";

Check failure on line 1 in eng/tools/spec-gen-sdk-runner/src/command-helpers.ts

View workflow job for this annotation

GitHub Actions / Protected Files

File 'eng/tools/spec-gen-sdk-runner/src/command-helpers.ts' should only be updated by the Azure SDK team. If intentional, the PR may be merged by the Azure SDK team via bypassing the branch protections.
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
Expand Down Expand Up @@ -263,24 +263,21 @@
* Process the breaking change label artifacts.
*
* @param executionReport - The spec-gen-sdk execution report.
* @returns [flag of lable breaking change, breaking change label].
* @returns flag of lable breaking change.
*/
export function getBreakingChangeInfo(executionReport: any): [boolean, string] {
let breakingChangeLabel = "";
export function getBreakingChangeInfo(executionReport: any): boolean {
for (const packageInfo of executionReport.packages) {
breakingChangeLabel = packageInfo.breakingChangeLabel;
if (packageInfo.shouldLabelBreakingChange) {
return [true, breakingChangeLabel];
return true;
}
}
return [false, breakingChangeLabel];
return false;
}

/**
* Generate the spec-gen-sdk artifacts.
* @param commandInput - The command input.
* @param result - The spec-gen-sdk execution result.
* @param breakingChangeLabel - The breaking change label.
* @param hasBreakingChange - A flag indicating whether there are breaking changes.
* @param hasManagementPlaneSpecs - A flag indicating whether there are management plane specs.
* @param stagedArtifactsFolder - The staged artifacts folder.
Expand All @@ -291,7 +288,6 @@
export function generateArtifact(
commandInput: SpecGenSdkCmdInput,
result: string,
breakingChangeLabel: string,
hasBreakingChange: boolean,
hasManagementPlaneSpecs: boolean,
stagedArtifactsFolder: string,
Expand Down Expand Up @@ -333,8 +329,6 @@
setVsoVariable("SpecGenSdkArtifactName", specGenSdkArtifactName);
setVsoVariable("SpecGenSdkArtifactPath", specGenSdkArtifactPath);
setVsoVariable("StagedArtifactsFolder", stagedArtifactsFolder);
setVsoVariable("BreakingChangeLabelAction", hasBreakingChange ? "add" : "remove");
setVsoVariable("BreakingChangeLabel", breakingChangeLabel);
setVsoVariable("HasAPIViewArtifact", apiViewRequestData.length > 0 ? "true" : "false");
} catch (error) {
logMessage("Runner: errors occurred while processing breaking change", LogLevel.Group);
Expand Down
4 changes: 1 addition & 3 deletions eng/tools/spec-gen-sdk-runner/src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import fs from "node:fs";

Check failure on line 1 in eng/tools/spec-gen-sdk-runner/src/commands.ts

View workflow job for this annotation

GitHub Actions / Protected Files

File 'eng/tools/spec-gen-sdk-runner/src/commands.ts' should only be updated by the Azure SDK team. If intentional, the PR may be merged by the Azure SDK team via bypassing the branch protections.
import path from "node:path";
import { runSpecGenSdkCommand, resetGitRepo, SpecConfigs } from "./utils.js";
import { LogLevel, logMessage, vsoAddAttachment, vsoLogIssue } from "./log.js";
Expand Down Expand Up @@ -84,7 +84,6 @@

let statusCode = 0;
let pushedSpecConfigCount;
let breakingChangeLabel = "";
let executionReport;
let changedSpecPathText = "";
let hasManagementPlaneSpecs = false;
Expand Down Expand Up @@ -164,7 +163,7 @@
if (overallExecutionResult !== "failed") {
overallExecutionResult = currentExecutionResult;
}
[currentRunHasBreakingChange, breakingChangeLabel] = getBreakingChangeInfo(executionReport);
currentRunHasBreakingChange = getBreakingChangeInfo(executionReport);
overallRunHasBreakingChange = overallRunHasBreakingChange || currentRunHasBreakingChange;
logMessage(`Runner command execution result:${currentExecutionResult}`);
} catch (error) {
Expand All @@ -180,7 +179,6 @@
generateArtifact(
commandInput,
overallExecutionResult,
breakingChangeLabel,
overallRunHasBreakingChange,
hasManagementPlaneSpecs,
stagedArtifactsFolder,
Expand Down
286 changes: 209 additions & 77 deletions eng/tools/spec-gen-sdk-runner/src/spec-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import path from "node:path";

Check failure on line 1 in eng/tools/spec-gen-sdk-runner/src/spec-helpers.ts

View workflow job for this annotation

GitHub Actions / Protected Files

File 'eng/tools/spec-gen-sdk-runner/src/spec-helpers.ts' should only be updated by the Azure SDK team. If intentional, the PR may be merged by the Azure SDK team via bypassing the branch protections.
import {
getChangedFiles,
searchRelatedParentFolders,
Expand All @@ -18,16 +18,131 @@
export const typespecProjectRegex = /^tspconfig.yaml$/;
export const typespecProjectSharedLibraryRegex = /[^/]+\.Shared/;

/**
* Processes typespec projects that follow the resource-manager or data-plane folder structure
* and matches them with corresponding readme files if they exist in the same folder.
* @param readmeMDResult - Object mapping folder paths to readme file paths
* @param typespecProjectResult - Object mapping folder paths to typespec project file paths
* @returns An array of ChangedSpecs objects containing the paths to the readme and TypeSpec config files
*/
export function processTypeSpecProjectsV2FolderStructure(
readmeMDResult: { [folderPath: string]: string[] },
typespecProjectResult: { [folderPath: string]: string[] },
): ChangedSpecs[] {
const changedSpecs: ChangedSpecs[] = [];

// Iterate through each typespec project folder
for (const folderPath of Object.keys(typespecProjectResult)) {
// Split the path into segments to check for specific components
const segments = folderPath.split(/[/\\]/);
// Check if the folder path contains resource-manager or data-plane segments
if (segments.includes("resource-manager") || segments.includes("data-plane")) {
const cs: ChangedSpecs = {
specs: [],
};

// Set the typespec project path
cs.typespecProject = path.join(folderPath, "tspconfig.yaml");
// Initialize the specs array with typespec project files
cs.specs = [...typespecProjectResult[folderPath]]; // Check if the same folder has a readme.md file
if (readmeMDResult[folderPath]) {
cs.readmeMd = path.join(folderPath, "readme.md");
// Merge the specs arrays, removing duplicates
cs.specs = [...new Set([...cs.specs, ...readmeMDResult[folderPath]])];
// Remove the processed entry from readmeMDResult
delete readmeMDResult[folderPath];
}

// Add the ChangedSpecs object to the result array
changedSpecs.push(cs);
// Remove the processed entry from typespecProjectResult
delete typespecProjectResult[folderPath];

// Delete readme entries that match specific folder structure patterns and are in the same parent folder hierarchy
// such as:
// "specification/service/data-plane"
// "specification/service/resource-manager"
// "specification/service/resource-manager/Microsoft.Service"
for (const readmePath of Object.keys(readmeMDResult)) {
// Split the paths into segments to work with path components rather than raw strings with separators
const folderSegments = folderPath.split(/[/\\]/); // Split on either / or \
const readmeSegments = readmePath.split(/[/\\]/);

// Find the position of "resource-manager" or "data-plane" in folder segments
const rmIndex = folderSegments.indexOf("resource-manager");
const dpIndex = folderSegments.indexOf("data-plane");

// For resource-manager paths
if (rmIndex !== -1) {
// Get the service path segments (everything before resource-manager)
const serviceSegments = folderSegments.slice(0, rmIndex);
// Check if readmePath shares the same service prefix
const isRelatedService = serviceSegments.every(
(segment, i) => i < readmeSegments.length && readmeSegments[i] === segment,
);

if (isRelatedService) {
// Case 1: Readme ends with resource-manager
// Example: specification/service/resource-manager
if (
readmeSegments.length === rmIndex + 1 &&
readmeSegments[rmIndex] === "resource-manager"
) {
logMessage(`\t Removing related readme: ${readmePath} for folder: ${folderPath}`);
delete readmeMDResult[readmePath];
continue;
}

// Case 2: Readme is one level down from resource-manager
// Example: specification/service/resource-manager/Microsoft.Service
if (
readmeSegments.length === rmIndex + 2 &&
readmeSegments[rmIndex] === "resource-manager" &&
folderSegments.length > rmIndex + 1 &&
folderSegments[rmIndex + 1] === readmeSegments[rmIndex + 1]
) {
logMessage(`\t Removing related readme: ${readmePath} for folder: ${folderPath}`);
delete readmeMDResult[readmePath];
continue;
}
}
}
// For data-plane paths
else if (dpIndex !== -1) {
// Get the service path segments (everything before data-plane)
const serviceSegments = folderSegments.slice(0, dpIndex);
// Check if readmePath shares the same service prefix and ends with data-plane
const isRelatedService = serviceSegments.every(
(segment, i) => i < readmeSegments.length && readmeSegments[i] === segment,
);

if (
isRelatedService &&
readmeSegments.length === dpIndex + 1 &&
readmeSegments[dpIndex] === "data-plane"
) {
logMessage(`\t Removing related readme: ${readmePath} for folder: ${folderPath}`);
delete readmeMDResult[readmePath];
}
}
}
}
}

return changedSpecs;
}

export function detectChangedSpecConfigFiles(commandInput: SpecGenSdkCmdInput): ChangedSpecs[] {
const prChangedFiles: string[] = getChangedFiles(commandInput.localSpecRepoPath) ?? [];
if (prChangedFiles.length === 0) {
logMessage("No files changed in the PR");
}
logMessage(`Changed files in the PR: ${prChangedFiles.length}`);
for (const file of prChangedFiles) {
const normalizedChangedFiles = prChangedFiles.map((f) => f.replaceAll("\\", "/"));
logMessage(`Changed files in the PR: ${normalizedChangedFiles.length}`);
for (const file of normalizedChangedFiles) {
logMessage(`\t${file}`);
}
const fileList = prChangedFiles
const fileList = normalizedChangedFiles
.filter((p) => p.startsWith("specification/"))
.filter((p) => !p.includes("/scenarios/"));

Expand Down Expand Up @@ -75,88 +190,105 @@
}
}

// Group paths by service
const serviceMap = groupPathsByService(readmeMDResult, typespecProjectResult);
// Process TypeSpec projects with the V2 folder structure
const newFolderStructureSpecs = processTypeSpecProjectsV2FolderStructure(
readmeMDResult,
typespecProjectResult,
);

const results: SpecResults = { readmeMDResult, typespecProjectResult };
if (newFolderStructureSpecs.length > 0) {
logMessage(`Found ${newFolderStructureSpecs.length} specs with the new folder structure`);
changedSpecs.push(...newFolderStructureSpecs);
for (const spec of newFolderStructureSpecs) {
logMessage(`\t\t tspconfig: ${spec.typespecProject}, readme: ${spec.readmeMd}`);
}
}

// Process each service
for (const [, info] of serviceMap) {
// Case: Resource Manager with .Management
if (info.managementPaths.length > 0) {
if (info.resourceManagerPaths.length === 1) {
// Single resource-manager path - match with all Management paths
const newSpecs = createCombinedSpecs(
info.resourceManagerPaths[0].path,
info.managementPaths,
results,
);
changedSpecs.push(...newSpecs);
logMessage(
`\t readme folders: ${info.resourceManagerPaths[0].path}, tspconfig folders: ${info.managementPaths}`,
);
for (const p of info.managementPaths) {
delete typespecProjectResult[p];
}
delete readmeMDResult[info.resourceManagerPaths[0].path];
} else {
// Multiple resource-manager paths - match by subfolder name
for (const rmPath of info.resourceManagerPaths) {
const matchingManagements = info.managementPaths.filter((mPath) => {
const rmSubPath = rmPath.subPath;
const managementName = getLastPathSegment(mPath).replace(".Management", "");
return rmSubPath && rmSubPath === managementName;
});
if (matchingManagements.length > 0) {
const newSpecs = createCombinedSpecs(rmPath.path, matchingManagements, results);
changedSpecs.push(...newSpecs);
logMessage(
`\t readme folders: ${rmPath.path}, tspconfig folders: ${matchingManagements}`,
);
for (const p of matchingManagements) {
delete typespecProjectResult[p];
// Process TypeSpec projects with the old folder structure
if (Object.keys(readmeMDResult).length > 0 && Object.keys(typespecProjectResult).length > 0) {
// Group paths by service
const serviceMap = groupPathsByService(readmeMDResult, typespecProjectResult);

const results: SpecResults = { readmeMDResult, typespecProjectResult };

// Process each service
for (const [, info] of serviceMap) {
// Case: Resource Manager with .Management
if (info.managementPaths.length > 0) {
if (info.resourceManagerPaths.length === 1) {
// Single resource-manager path - match with all Management paths
const newSpecs = createCombinedSpecs(
info.resourceManagerPaths[0].path,
info.managementPaths,
results,
);
changedSpecs.push(...newSpecs);
logMessage(
`\t readme folders: ${info.resourceManagerPaths[0].path}, tspconfig folders: ${info.managementPaths}`,
);
for (const p of info.managementPaths) {
delete typespecProjectResult[p];
}
delete readmeMDResult[info.resourceManagerPaths[0].path];
} else {
// Multiple resource-manager paths - match by subfolder name
for (const rmPath of info.resourceManagerPaths) {
const matchingManagements = info.managementPaths.filter((mPath) => {
const rmSubPath = rmPath.subPath;
const managementName = getLastPathSegment(mPath).replace(".Management", "");
return rmSubPath && rmSubPath === managementName;
});
if (matchingManagements.length > 0) {
const newSpecs = createCombinedSpecs(rmPath.path, matchingManagements, results);
changedSpecs.push(...newSpecs);
logMessage(
`\t readme folders: ${rmPath.path}, tspconfig folders: ${matchingManagements}`,
);
for (const p of matchingManagements) {
delete typespecProjectResult[p];
}
delete readmeMDResult[rmPath.path];
}
delete readmeMDResult[rmPath.path];
}
}
}
}

// Case: Data Plane matching
if (info.dataPlanePaths.length > 0 && info.otherTypeSpecPaths.length > 0) {
if (info.dataPlanePaths.length === 1) {
// Single data-plane path - match with all non-Management TypeSpec paths
const newSpecs = createCombinedSpecs(
info.dataPlanePaths[0].path,
info.otherTypeSpecPaths,
results,
);
changedSpecs.push(...newSpecs);
logMessage(
`\t readme folders: ${info.dataPlanePaths[0].path}, tspconfig folders: ${info.otherTypeSpecPaths}`,
);
for (const p of info.otherTypeSpecPaths) {
delete typespecProjectResult[p];
}
delete readmeMDResult[info.dataPlanePaths[0].path];
} else {
// Multiple data-plane paths - match by subfolder name
for (const dpPath of info.dataPlanePaths) {
const matchingTypeSpecs = info.otherTypeSpecPaths.filter((tsPath) => {
const dpSubFolder = dpPath.subFolder;
const tsLastSegment = getLastPathSegment(tsPath);
return dpSubFolder && dpSubFolder === tsLastSegment;
});
if (matchingTypeSpecs.length > 0) {
const newSpecs = createCombinedSpecs(dpPath.path, matchingTypeSpecs, results);
changedSpecs.push(...newSpecs);
logMessage(
`\t readme folders: ${dpPath.path}, tspconfig folders: ${matchingTypeSpecs}`,
);
for (const p of matchingTypeSpecs) {
delete typespecProjectResult[p];
// Case: Data Plane matching
if (info.dataPlanePaths.length > 0 && info.otherTypeSpecPaths.length > 0) {
if (info.dataPlanePaths.length === 1) {
// Single data-plane path - match with all non-Management TypeSpec paths
const newSpecs = createCombinedSpecs(
info.dataPlanePaths[0].path,
info.otherTypeSpecPaths,
results,
);
changedSpecs.push(...newSpecs);
logMessage(
`\t readme folders: ${info.dataPlanePaths[0].path}, tspconfig folders: ${info.otherTypeSpecPaths}`,
);
for (const p of info.otherTypeSpecPaths) {
delete typespecProjectResult[p];
}
delete readmeMDResult[info.dataPlanePaths[0].path];
} else {
// Multiple data-plane paths - match by subfolder name
for (const dpPath of info.dataPlanePaths) {
const matchingTypeSpecs = info.otherTypeSpecPaths.filter((tsPath) => {
const dpSubFolder = dpPath.subFolder;
const tsLastSegment = getLastPathSegment(tsPath);
return dpSubFolder && dpSubFolder === tsLastSegment;
});
if (matchingTypeSpecs.length > 0) {
const newSpecs = createCombinedSpecs(dpPath.path, matchingTypeSpecs, results);
changedSpecs.push(...newSpecs);
logMessage(
`\t readme folders: ${dpPath.path}, tspconfig folders: ${matchingTypeSpecs}`,
);
for (const p of matchingTypeSpecs) {
delete typespecProjectResult[p];
}
delete readmeMDResult[dpPath.path];
}
delete readmeMDResult[dpPath.path];
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion eng/tools/spec-gen-sdk-runner/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { spawn, spawnSync, exec } from "node:child_process";

Check failure on line 1 in eng/tools/spec-gen-sdk-runner/src/utils.ts

View workflow job for this annotation

GitHub Actions / Protected Files

File 'eng/tools/spec-gen-sdk-runner/src/utils.ts' should only be updated by the Azure SDK team. If intentional, the PR may be merged by the Azure SDK team via bypassing the branch protections.
import path from "node:path";
import fs from "node:fs";
import { LogLevel, logMessage } from "./log.js";
Expand Down Expand Up @@ -223,7 +223,9 @@
return undefined;
}
currentPath = path.dirname(currentPath);
if (stopAtFolder && currentPath === stopAtFolder) {
// Check if we've reached the root of the path (stopAtFolder) or
// if we've reached '.' which prevents infinite loops with path.dirname('.')
if ((stopAtFolder && currentPath === stopAtFolder) || currentPath === ".") {
return undefined;
}
}
Expand Down
Loading
Loading