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
92 changes: 74 additions & 18 deletions app/exec/extension/_lib/merger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,14 @@ export class Merger {
return Promise.all(manifestPromises).then(partials => {
// Determine the targets so we can construct the builders
let targets: TargetDeclaration[] = [];
const taskJsonValidationPromises: Promise<any>[] = [];
let allContributions: any[] = [];
partials.forEach(partial => {
if (_.isArray(partial["targets"])) {
targets = targets.concat(partial["targets"]);
}
if (_.isArray(partial["contributions"])) {
allContributions = allContributions.concat(partial["contributions"]);
}
});
this.extensionComposer = ComposerFactory.GetComposer(this.settings, targets);
this.manifestBuilders = this.extensionComposer.getBuilders();
Expand Down Expand Up @@ -223,9 +226,6 @@ export class Merger {
absolutePath = path.join(path.dirname(partial.__origin), asset.path);
}
asset.path = path.relative(this.settings.root, absolutePath);

const taskJsonPattern: string = path.join(absolutePath, '**', "task.json");
taskJsonValidationPromises.push(this.validateTaskJson(taskJsonPattern));
});
}
// Transform icon paths as above
Expand Down Expand Up @@ -263,6 +263,9 @@ export class Merger {
});
});

// Validate task.json files based on build task contributions
const taskJsonValidationPromise = this.validateBuildTaskContributions(allContributions);

// Generate localization resources
const locPrepper = new loc.LocPrep.LocKeyGenerator(this.manifestBuilders);
const resources = locPrepper.generateLocalizationKeys();
Expand All @@ -283,9 +286,8 @@ export class Merger {

// Finalize each builder
return Promise.all(
[updateVersionPromise].concat(
this.manifestBuilders.map(b => b.finalize(packageFiles, resourceData, this.manifestBuilders)),
taskJsonValidationPromises
[updateVersionPromise, taskJsonValidationPromise].concat(
this.manifestBuilders.map(b => b.finalize(packageFiles, resourceData, this.manifestBuilders))
),
).then(() => {
// const the composer do validation
Expand Down Expand Up @@ -408,27 +410,81 @@ export class Merger {
return files;
}

private async validateTaskJson(taskJsonSearchPattern: string): Promise<TaskJson> {
private async validateBuildTaskContributions(contributions: any[]): Promise<void> {
try {
const matches: string[] = await promisify(glob)(taskJsonSearchPattern);

if (matches.length === 0) {
trace.debug(`No task.json file found for validation in ${taskJsonSearchPattern}`);
// Filter contributions to only build tasks
const buildTaskContributions = contributions.filter(contrib =>
contrib.type === "ms.vss-distributed-task.task" &&
contrib.properties &&
contrib.properties.name
);

if (buildTaskContributions.length === 0) {
trace.debug("No build task contributions found, skipping task.json validation");
return;
}

const taskJsonPath = matches[0];
const taskJsonExists = await exists(taskJsonPath);

if (taskJsonExists) {
return validate(taskJsonPath, "no task.json in specified directory");
const allTaskJsonPaths: string[] = [];

// For each build task contribution, look for task.json files and validate them
for (const contrib of buildTaskContributions) {
const taskPath = contrib.properties.name;
const absoluteTaskPath = path.join(this.settings.root, taskPath);
const contributionTaskJsonPaths: string[] = [];

// Check for task.json in the main directory
const mainTaskJsonPath = path.join(absoluteTaskPath, "task.json");
if (fs.existsSync(mainTaskJsonPath)) {
contributionTaskJsonPaths.push(mainTaskJsonPath);
trace.debug(`Found task.json: ${mainTaskJsonPath}`);
}

// Check for task.json in direct child directories (version folders)
if (fs.existsSync(absoluteTaskPath) && fs.lstatSync(absoluteTaskPath).isDirectory()) {
try {
const childDirs = fs.readdirSync(absoluteTaskPath);
for (const childDir of childDirs) {
const childPath = path.join(absoluteTaskPath, childDir);
if (fs.lstatSync(childPath).isDirectory()) {
const childTaskJsonPath = path.join(childPath, "task.json");
if (fs.existsSync(childTaskJsonPath)) {
contributionTaskJsonPaths.push(childTaskJsonPath);
trace.debug(`Found task.json: ${childTaskJsonPath}`);
}
}
}
} catch (err) {
trace.warn(`Error reading task directory ${absoluteTaskPath}: ${err}`);
}
}

// Validate task.json files for this contribution with backwards compatibility checking
if (contributionTaskJsonPaths.length > 0) {
trace.debug(`Validating ${contributionTaskJsonPaths.length} task.json files for contribution ${contrib.id || taskPath}`);

for (const taskJsonPath of contributionTaskJsonPaths) {
validate(taskJsonPath, "no task.json in specified directory", contributionTaskJsonPaths);
}

// Also collect for global tracking if needed
allTaskJsonPaths.push(...contributionTaskJsonPaths);
} else {
trace.warn(`Build task contribution '${contrib.id || taskPath}' does not have a task.json file. Expected task.json in ${absoluteTaskPath} or its subdirectories.`);
}
}

if (allTaskJsonPaths.length === 0) {
trace.debug("No task.json files found in build task contributions");
return;
}

trace.debug(`Successfully validated ${allTaskJsonPaths.length} task.json files across ${buildTaskContributions.length} build task contributions`);

} catch (err) {
const warningMessage = "Please, make sure the task.json file is correct. In the future, this warning will be treated as an error.\n";
trace.warn(err && err instanceof Error
? warningMessage + err.message
: `Error occurred while validating task.json. ${warningMessage}`);
: `Error occurred while validating build task contributions. ${warningMessage}`);
}
}
}
52 changes: 40 additions & 12 deletions app/lib/jsonvalidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ export interface TaskJson {
/*
* Checks a json file for correct formatting against some validation function
* @param jsonFilePath path to the json file being validated
* @param jsonValidationFunction function that validates parsed json data against some criteria
* @param jsonMissingErrorMessage error message if json file doesn't exist
* @param allMatchedPaths optional array of all matched task.json paths for backwards compat detection
* @return the parsed json file
* @throws InvalidDirectoryException if json file doesn't exist, InvalidJsonException on failed parse or *first* invalid field in json
*/
export function validate(jsonFilePath: string, jsonMissingErrorMessage?: string): TaskJson {
export function validate(jsonFilePath: string, jsonMissingErrorMessage?: string, allMatchedPaths?: string[]): TaskJson {
trace.debug("Validating task json...");
var jsonMissingErrorMsg: string = jsonMissingErrorMessage || "specified json file does not exist.";
this.exists(jsonFilePath, jsonMissingErrorMsg);
exists(jsonFilePath, jsonMissingErrorMsg);

var taskJson;
try {
Expand All @@ -29,7 +30,7 @@ export function validate(jsonFilePath: string, jsonMissingErrorMessage?: string)
throw new Error("Invalid task json: " + jsonError);
}

var issues: string[] = this.validateTask(jsonFilePath, taskJson);
var issues: string[] = validateTask(jsonFilePath, taskJson);
if (issues.length > 0) {
var output: string = "Invalid task json:";
for (var i = 0; i < issues.length; i++) {
Expand All @@ -40,7 +41,7 @@ export function validate(jsonFilePath: string, jsonMissingErrorMessage?: string)
}

trace.debug("Json is valid.");
validateRunner(taskJson);
validateRunner(taskJson, allMatchedPaths);
return taskJson;
}

Expand All @@ -55,24 +56,51 @@ export function exists(path: string, errorMessage: string) {
}

/*
* Validates a task against deprecated runner
* Counts the number of non-deprecated runners in a task's execution configuration
* @param taskData the parsed json file
* @return number of valid (non-deprecated) runners, or 0 if no execution is defined
*/
export function validateRunner(taskData: any) {
function countValidRunners(taskData: any): number {
if (taskData == undefined || taskData.execution == undefined)
return
return 0;

return Object.keys(taskData.execution).filter(itm => deprecatedRunners.indexOf(itm) == -1).length;
}

const validRunnerCount = Object.keys(taskData.execution).filter(itm => deprecatedRunners.indexOf(itm) == -1) || 0;
if (validRunnerCount == 0) {
trace.warn("Task %s is dependent on a task runner that is end-of-life and will be removed in the future. Please visit https://aka.ms/node-runner-guidance to learn how to upgrade the task.", taskData.name)
/*
* Validates a task against deprecated runner
* @param taskData the parsed json file
* @param allMatchedPaths optional array of all matched task.json paths for backwards compat detection
*/
export function validateRunner(taskData: any, allMatchedPaths?: string[]) {
if (countValidRunners(taskData) == 0) {
if (allMatchedPaths) {
for (const matchedPath of allMatchedPaths) {
let matchedTaskData;
try {
matchedTaskData = require(matchedPath);
} catch {
continue;
}
if (taskData.name == matchedTaskData.name && taskData.id == matchedTaskData.id && matchedTaskData.version?.Major > taskData.version?.Major) {
// Return if the other task is using a non-deprecated task runner
const otherValidRunnerCount = countValidRunners(matchedTaskData);
if (otherValidRunnerCount > 0) {
return;
}
}
}
}

trace.warn("Task %s@%s is dependent on a task runner that is end-of-life and will be removed in the future. Please visit https://aka.ms/node-runner-guidance to learn how to upgrade the task.", taskData.name, taskData.version?.Major || "?")
}
}

/*
* Validates a parsed json file describing a build task
* @param taskPath the path to the original json file
* @param taskData the parsed json file
* @return list of issues with the json file
* @return list of issues with the json file
*/
export function validateTask(taskPath: string, taskData: any): string[] {
var vn = taskData.name || taskPath;
Expand Down
23 changes: 6 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tfx-cli",
"version": "0.21.2",
"version": "0.21.3",
"description": "CLI for Azure DevOps Services and Team Foundation Server",
"repository": {
"type": "git",
Expand Down Expand Up @@ -35,7 +35,7 @@
"prompt": "^1.3.0",
"read": "^1.0.6",
"shelljs": "^0.8.5",
"tmp": "0.0.26",
"tmp": "^0.2.4",
"tracer": "0.7.4",
"util.promisify": "^1.0.0",
"uuid": "^3.0.1",
Expand Down
Loading