From 7b5820be7055c1015cbba8c387bb2f7412ad3090 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 May 2025 12:46:07 +0000 Subject: [PATCH 1/4] Initial plan for issue From f3399b17f587fc92c76e818118680e8c44c41f7a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 May 2025 12:56:53 +0000 Subject: [PATCH 2/4] Fix DotNetCoreCLI@2 zipAfterPublish issue to prevent empty directories Co-authored-by: sanjuyadav24 <185911972+sanjuyadav24@users.noreply.github.com> --- Tasks/DotNetCoreCLIV2/Tests/L0.ts | 12 ++- .../Tests/zipAfterPublishTests.ts | 95 +++++++++++++++++++ Tasks/DotNetCoreCLIV2/dotnetcore.ts | 7 +- 3 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishTests.ts diff --git a/Tasks/DotNetCoreCLIV2/Tests/L0.ts b/Tasks/DotNetCoreCLIV2/Tests/L0.ts index eec3247855e1..b4b483c2fc71 100644 --- a/Tasks/DotNetCoreCLIV2/Tests/L0.ts +++ b/Tasks/DotNetCoreCLIV2/Tests/L0.ts @@ -321,8 +321,16 @@ describe('DotNetCoreExe Suite', function () { assert(tr.succeeded, 'task should have succeeded'); }); - it('publish works with zipAfterPublish option', () => { - // TODO + it('publish works with zipAfterPublish option', async () => { + process.env["__projects__"] = "web/project.json"; + process.env["__publishWebProjects__"] = "false"; + process.env["__arguments__"] = "--configuration release --output /usr/out"; + let tp = path.join(__dirname, 'zipAfterPublishTests.js'); + let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + await tr.runAsync(); + + assert(tr.invokedToolCount == 1, 'should have invoked tool once'); + assert(tr.succeeded, 'task should have succeeded'); }); it('publish fails with zipAfterPublish and publishWebProjects option with no project file specified', async () => { diff --git a/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishTests.ts b/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishTests.ts new file mode 100644 index 000000000000..1bcad6dafccb --- /dev/null +++ b/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishTests.ts @@ -0,0 +1,95 @@ +import ma = require('azure-pipelines-task-lib/mock-answer'); +import tmrm = require('azure-pipelines-task-lib/mock-run'); +import path = require('path'); +import fs = require('fs'); +import assert = require('assert'); + +let taskPath = path.join(__dirname, '..', 'dotnetcore.js'); +let tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath); + +tmr.setInput('command', "publish"); +tmr.setInput('projects', "web/project.json"); +tmr.setInput('publishWebProjects', "false"); +tmr.setInput('arguments', "--configuration release --output /usr/out"); +tmr.setInput('zipAfterPublish', "true"); +tmr.setInput('modifyOutputPath', "false"); + +// Mock file system operations for testing zip functionality +const mockFs = { + createWriteStream: function(filePath) { + console.log("Creating write stream for: " + filePath); + const events = {}; + return { + on: (event, callback) => { + events[event] = callback; + return this; + }, + end: () => { + console.log("Closing write stream for: " + filePath); + events['close'](); + } + }; + }, + mkdirSync: function(p) { + console.log("Creating directory: " + p); + }, + renameSync: function(oldPath, newPath) { + console.log("Moving file from: " + oldPath + " to: " + newPath); + }, + existsSync: function(filePath) { + return true; + }, + readFileSync: function() { + return ""; + }, + statSync: function() { + return { + isFile: () => false, + isDirectory: () => true + }; + }, + lstatSync: function() { + return { + isDirectory: () => true + }; + } +}; + +// Mock archiver +const mockArchiver = function() { + return { + pipe: function() { return this; }, + directory: function() { return this; }, + finalize: function() { return this; } + }; +}; + +let a: ma.TaskLibAnswers = { + "which": { "dotnet": "dotnet" }, + "checkPath": { "dotnet": true }, + "exist": { + "/usr/out": true + }, + "exec": { + "dotnet publish web/project.json --configuration release --output /usr/out": { + "code": 0, + "stdout": "published web without adding project name to path\n", + "stderr": "" + } + }, + "findMatch": { + "web/project.json": ["web/project.json"] + }, + "rmRF": { + "/usr/out": { + "success": true + } + } +}; + +tmr.setAnswers(a); +tmr.registerMock('fs', Object.assign({}, fs, mockFs)); +tmr.registerMock('archiver', mockArchiver); +tmr.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner')); + +tmr.run(); \ No newline at end of file diff --git a/Tasks/DotNetCoreCLIV2/dotnetcore.ts b/Tasks/DotNetCoreCLIV2/dotnetcore.ts index 4cc5aa783429..4c0eddec61b0 100644 --- a/Tasks/DotNetCoreCLIV2/dotnetcore.ts +++ b/Tasks/DotNetCoreCLIV2/dotnetcore.ts @@ -286,10 +286,9 @@ export class dotNetExe { var outputTarget = outputSource + ".zip"; await this.zip(outputSource, outputTarget); tl.rmRF(outputSource); - if (moveZipToOutputSource) { - fs.mkdirSync(outputSource); - fs.renameSync(outputTarget, path.join(outputSource, path.basename(outputTarget))); - } + // When moveZipToOutputSource is true and zipAfterPublish is true, + // we should leave the zip file where it was created and not move it into a subdirectory + // This way the artifact will be the zip file directly instead of a directory containing the zip } else { throw tl.loc("noPublishFolderFoundToZip", projectFile); From aab9dd21a75765524ad3b5b28e46ae5997c56898 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Jun 2025 06:59:16 +0000 Subject: [PATCH 3/4] Add feature flag for zipAfterPublish directory creation behavior Co-authored-by: sanjuyadav24 <185911972+sanjuyadav24@users.noreply.github.com> --- Tasks/DotNetCoreCLIV2/Tests/L0.ts | 12 +++ .../Tests/zipAfterPublishLegacyTests.ts | 96 +++++++++++++++++++ .../Tests/zipAfterPublishTests.ts | 1 + Tasks/DotNetCoreCLIV2/dotnetcore.ts | 13 ++- Tasks/DotNetCoreCLIV2/task.json | 9 ++ 5 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishLegacyTests.ts diff --git a/Tasks/DotNetCoreCLIV2/Tests/L0.ts b/Tasks/DotNetCoreCLIV2/Tests/L0.ts index b4b483c2fc71..0d83b801a416 100644 --- a/Tasks/DotNetCoreCLIV2/Tests/L0.ts +++ b/Tasks/DotNetCoreCLIV2/Tests/L0.ts @@ -333,6 +333,18 @@ describe('DotNetCoreExe Suite', function () { assert(tr.succeeded, 'task should have succeeded'); }); + it('publish works with zipAfterPublish and legacy directory creation option', async () => { + process.env["__projects__"] = "web/project.json"; + process.env["__publishWebProjects__"] = "false"; + process.env["__arguments__"] = "--configuration release --output /usr/out"; + let tp = path.join(__dirname, 'zipAfterPublishLegacyTests.js'); + let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + await tr.runAsync(); + + assert(tr.invokedToolCount == 1, 'should have invoked tool once'); + assert(tr.succeeded, 'task should have succeeded'); + }); + it('publish fails with zipAfterPublish and publishWebProjects option with no project file specified', async () => { process.env["__projects__"] = ""; process.env["__publishWebProjects__"] = "false"; diff --git a/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishLegacyTests.ts b/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishLegacyTests.ts new file mode 100644 index 000000000000..3841c5dfa79f --- /dev/null +++ b/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishLegacyTests.ts @@ -0,0 +1,96 @@ +import ma = require('azure-pipelines-task-lib/mock-answer'); +import tmrm = require('azure-pipelines-task-lib/mock-run'); +import path = require('path'); +import fs = require('fs'); +import assert = require('assert'); + +let taskPath = path.join(__dirname, '..', 'dotnetcore.js'); +let tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath); + +tmr.setInput('command', "publish"); +tmr.setInput('projects', "web/project.json"); +tmr.setInput('publishWebProjects', "false"); +tmr.setInput('arguments', "--configuration release --output /usr/out"); +tmr.setInput('zipAfterPublish', "true"); +tmr.setInput('modifyOutputPath', "false"); +tmr.setInput('zipAfterPublishCreateDirectory', "true"); // Test legacy behavior with directory creation + +// Mock file system operations for testing zip functionality +const mockFs = { + createWriteStream: function(filePath) { + console.log("Creating write stream for: " + filePath); + const events = {}; + return { + on: (event, callback) => { + events[event] = callback; + return this; + }, + end: () => { + console.log("Closing write stream for: " + filePath); + events['close'](); + } + }; + }, + mkdirSync: function(p) { + console.log("Creating directory: " + p); + }, + renameSync: function(oldPath, newPath) { + console.log("Moving file from: " + oldPath + " to: " + newPath); + }, + existsSync: function(filePath) { + return true; + }, + readFileSync: function() { + return ""; + }, + statSync: function() { + return { + isFile: () => false, + isDirectory: () => true + }; + }, + lstatSync: function() { + return { + isDirectory: () => true + }; + } +}; + +// Mock archiver +const mockArchiver = function() { + return { + pipe: function() { return this; }, + directory: function() { return this; }, + finalize: function() { return this; } + }; +}; + +let a: ma.TaskLibAnswers = { + "which": { "dotnet": "dotnet" }, + "checkPath": { "dotnet": true }, + "exist": { + "/usr/out": true + }, + "exec": { + "dotnet publish web/project.json --configuration release --output /usr/out": { + "code": 0, + "stdout": "published web without adding project name to path\n", + "stderr": "" + } + }, + "findMatch": { + "web/project.json": ["web/project.json"] + }, + "rmRF": { + "/usr/out": { + "success": true + } + } +}; + +tmr.setAnswers(a); +tmr.registerMock('fs', Object.assign({}, fs, mockFs)); +tmr.registerMock('archiver', mockArchiver); +tmr.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner')); + +tmr.run(); \ No newline at end of file diff --git a/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishTests.ts b/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishTests.ts index 1bcad6dafccb..2c663d5c5d94 100644 --- a/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishTests.ts +++ b/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishTests.ts @@ -13,6 +13,7 @@ tmr.setInput('publishWebProjects', "false"); tmr.setInput('arguments', "--configuration release --output /usr/out"); tmr.setInput('zipAfterPublish', "true"); tmr.setInput('modifyOutputPath', "false"); +tmr.setInput('zipAfterPublishCreateDirectory', "false"); // Test new simplified behavior // Mock file system operations for testing zip functionality const mockFs = { diff --git a/Tasks/DotNetCoreCLIV2/dotnetcore.ts b/Tasks/DotNetCoreCLIV2/dotnetcore.ts index 4c0eddec61b0..8ea23c914eb4 100644 --- a/Tasks/DotNetCoreCLIV2/dotnetcore.ts +++ b/Tasks/DotNetCoreCLIV2/dotnetcore.ts @@ -28,6 +28,7 @@ export class dotNetExe { private arguments: string; private publishWebProjects: boolean; private zipAfterPublish: boolean; + private zipAfterPublishCreateDirectory: boolean; private outputArgument: string = ""; private outputArgumentIndex: number = 0; private workingDirectory: string; @@ -39,6 +40,7 @@ export class dotNetExe { this.arguments = tl.getInput("arguments", false) || ""; this.publishWebProjects = tl.getBoolInput("publishWebProjects", false); this.zipAfterPublish = tl.getBoolInput("zipAfterPublish", false); + this.zipAfterPublishCreateDirectory = tl.getBoolInput("zipAfterPublishCreateDirectory", true); this.workingDirectory = tl.getPathInput("workingDirectory", false); } @@ -286,9 +288,14 @@ export class dotNetExe { var outputTarget = outputSource + ".zip"; await this.zip(outputSource, outputTarget); tl.rmRF(outputSource); - // When moveZipToOutputSource is true and zipAfterPublish is true, - // we should leave the zip file where it was created and not move it into a subdirectory - // This way the artifact will be the zip file directly instead of a directory containing the zip + + // Check if we should create directory for ZIP output (legacy behavior) + if (moveZipToOutputSource && this.zipAfterPublishCreateDirectory) { + // Legacy behavior: create directory and move ZIP file into it + fs.mkdirSync(outputSource); + fs.renameSync(outputTarget, path.join(outputSource, path.basename(outputTarget))); + } + // If zipAfterPublishCreateDirectory is false, leave ZIP file at original location (new simplified behavior) } else { throw tl.loc("noPublishFolderFoundToZip", projectFile); diff --git a/Tasks/DotNetCoreCLIV2/task.json b/Tasks/DotNetCoreCLIV2/task.json index b862ace9480f..7c7a596e3700 100644 --- a/Tasks/DotNetCoreCLIV2/task.json +++ b/Tasks/DotNetCoreCLIV2/task.json @@ -179,6 +179,15 @@ "required": false, "helpMarkDown": "If true, folders created by the publish command will have project's folder name prefixed to their folder names when output path is specified explicitly in arguments. This is useful if you want to publish multiple projects to the same folder." }, + { + "name": "zipAfterPublishCreateDirectory", + "type": "boolean", + "visibleRule": "command = publish && zipAfterPublish = true", + "label": "Create directory for ZIP output", + "defaultValue": "true", + "required": false, + "helpMarkDown": "If true, the ZIP file will be placed in a directory with the same name as the output path (legacy behavior). If false, the ZIP file will remain at the original output location (simplified behavior)." + }, { "name": "selectOrConfig", "aliases": [ From 9d3ea7d6076df4426138579635f974b8aded5fb3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Jun 2025 08:56:52 +0000 Subject: [PATCH 4/4] Replace customer input with feature flag for zipAfterPublish directory behavior Co-authored-by: sanjuyadav24 <185911972+sanjuyadav24@users.noreply.github.com> --- .../Tests/zipAfterPublishLegacyTests.ts | 15 ++++++++++++++- .../DotNetCoreCLIV2/Tests/zipAfterPublishTests.ts | 15 ++++++++++++++- Tasks/DotNetCoreCLIV2/dotnetcore.ts | 10 ++++++---- Tasks/DotNetCoreCLIV2/task.json | 10 +--------- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishLegacyTests.ts b/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishLegacyTests.ts index 3841c5dfa79f..e189fea7591c 100644 --- a/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishLegacyTests.ts +++ b/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishLegacyTests.ts @@ -13,7 +13,7 @@ tmr.setInput('publishWebProjects', "false"); tmr.setInput('arguments', "--configuration release --output /usr/out"); tmr.setInput('zipAfterPublish', "true"); tmr.setInput('modifyOutputPath', "false"); -tmr.setInput('zipAfterPublishCreateDirectory', "true"); // Test legacy behavior with directory creation +// tmr.setInput('zipAfterPublishCreateDirectory', "true"); // Removed: now controlled by feature flag // Mock file system operations for testing zip functionality const mockFs = { @@ -89,6 +89,19 @@ let a: ma.TaskLibAnswers = { }; tmr.setAnswers(a); + +// Mock getPipelineFeature to return false for legacy behavior (create directory) +const mockTl = { + ...require('azure-pipelines-task-lib/task'), + getPipelineFeature: function(feature: string): boolean { + if (feature === 'DotNetCoreCLIZipAfterPublishSimplified') { + return false; // Disable simplified behavior for this test (use legacy behavior) + } + return false; + } +}; + +tmr.registerMock('azure-pipelines-task-lib/task', mockTl); tmr.registerMock('fs', Object.assign({}, fs, mockFs)); tmr.registerMock('archiver', mockArchiver); tmr.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner')); diff --git a/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishTests.ts b/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishTests.ts index 2c663d5c5d94..39082ffd0ba8 100644 --- a/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishTests.ts +++ b/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishTests.ts @@ -13,7 +13,7 @@ tmr.setInput('publishWebProjects', "false"); tmr.setInput('arguments', "--configuration release --output /usr/out"); tmr.setInput('zipAfterPublish', "true"); tmr.setInput('modifyOutputPath', "false"); -tmr.setInput('zipAfterPublishCreateDirectory', "false"); // Test new simplified behavior +// tmr.setInput('zipAfterPublishCreateDirectory', "false"); // Removed: now controlled by feature flag // Mock file system operations for testing zip functionality const mockFs = { @@ -89,6 +89,19 @@ let a: ma.TaskLibAnswers = { }; tmr.setAnswers(a); + +// Mock getPipelineFeature to return true for simplified behavior (no directory creation) +const mockTl = { + ...require('azure-pipelines-task-lib/task'), + getPipelineFeature: function(feature: string): boolean { + if (feature === 'DotNetCoreCLIZipAfterPublishSimplified') { + return true; // Enable simplified behavior for this test + } + return false; + } +}; + +tmr.registerMock('azure-pipelines-task-lib/task', mockTl); tmr.registerMock('fs', Object.assign({}, fs, mockFs)); tmr.registerMock('archiver', mockArchiver); tmr.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner')); diff --git a/Tasks/DotNetCoreCLIV2/dotnetcore.ts b/Tasks/DotNetCoreCLIV2/dotnetcore.ts index 8ea23c914eb4..794ceba8c997 100644 --- a/Tasks/DotNetCoreCLIV2/dotnetcore.ts +++ b/Tasks/DotNetCoreCLIV2/dotnetcore.ts @@ -28,7 +28,7 @@ export class dotNetExe { private arguments: string; private publishWebProjects: boolean; private zipAfterPublish: boolean; - private zipAfterPublishCreateDirectory: boolean; + private outputArgument: string = ""; private outputArgumentIndex: number = 0; private workingDirectory: string; @@ -40,7 +40,7 @@ export class dotNetExe { this.arguments = tl.getInput("arguments", false) || ""; this.publishWebProjects = tl.getBoolInput("publishWebProjects", false); this.zipAfterPublish = tl.getBoolInput("zipAfterPublish", false); - this.zipAfterPublishCreateDirectory = tl.getBoolInput("zipAfterPublishCreateDirectory", true); + this.workingDirectory = tl.getPathInput("workingDirectory", false); } @@ -290,12 +290,14 @@ export class dotNetExe { tl.rmRF(outputSource); // Check if we should create directory for ZIP output (legacy behavior) - if (moveZipToOutputSource && this.zipAfterPublishCreateDirectory) { + // Feature flag controls this: when enabled, uses simplified behavior (no directory creation) + const useSimplifiedZipBehavior = tl.getPipelineFeature('DotNetCoreCLIZipAfterPublishSimplified'); + if (moveZipToOutputSource && !useSimplifiedZipBehavior) { // Legacy behavior: create directory and move ZIP file into it fs.mkdirSync(outputSource); fs.renameSync(outputTarget, path.join(outputSource, path.basename(outputTarget))); } - // If zipAfterPublishCreateDirectory is false, leave ZIP file at original location (new simplified behavior) + // If feature flag is enabled, leave ZIP file at original location (simplified behavior) } else { throw tl.loc("noPublishFolderFoundToZip", projectFile); diff --git a/Tasks/DotNetCoreCLIV2/task.json b/Tasks/DotNetCoreCLIV2/task.json index 7c7a596e3700..3e28318e805c 100644 --- a/Tasks/DotNetCoreCLIV2/task.json +++ b/Tasks/DotNetCoreCLIV2/task.json @@ -179,15 +179,7 @@ "required": false, "helpMarkDown": "If true, folders created by the publish command will have project's folder name prefixed to their folder names when output path is specified explicitly in arguments. This is useful if you want to publish multiple projects to the same folder." }, - { - "name": "zipAfterPublishCreateDirectory", - "type": "boolean", - "visibleRule": "command = publish && zipAfterPublish = true", - "label": "Create directory for ZIP output", - "defaultValue": "true", - "required": false, - "helpMarkDown": "If true, the ZIP file will be placed in a directory with the same name as the output path (legacy behavior). If false, the ZIP file will remain at the original output location (simplified behavior)." - }, + { "name": "selectOrConfig", "aliases": [