diff --git a/Tasks/DotNetCoreCLIV2/Tests/L0.ts b/Tasks/DotNetCoreCLIV2/Tests/L0.ts index eec3247855e1..0d83b801a416 100644 --- a/Tasks/DotNetCoreCLIV2/Tests/L0.ts +++ b/Tasks/DotNetCoreCLIV2/Tests/L0.ts @@ -321,8 +321,28 @@ 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 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 () => { diff --git a/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishLegacyTests.ts b/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishLegacyTests.ts new file mode 100644 index 000000000000..e189fea7591c --- /dev/null +++ b/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishLegacyTests.ts @@ -0,0 +1,109 @@ +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"); // Removed: now controlled by feature flag + +// 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); + +// 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')); + +tmr.run(); \ No newline at end of file diff --git a/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishTests.ts b/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishTests.ts new file mode 100644 index 000000000000..39082ffd0ba8 --- /dev/null +++ b/Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishTests.ts @@ -0,0 +1,109 @@ +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', "false"); // Removed: now controlled by feature flag + +// 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); + +// 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')); + +tmr.run(); \ No newline at end of file diff --git a/Tasks/DotNetCoreCLIV2/dotnetcore.ts b/Tasks/DotNetCoreCLIV2/dotnetcore.ts index 4cc5aa783429..794ceba8c997 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 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.workingDirectory = tl.getPathInput("workingDirectory", false); } @@ -286,10 +288,16 @@ export class dotNetExe { var outputTarget = outputSource + ".zip"; await this.zip(outputSource, outputTarget); tl.rmRF(outputSource); - if (moveZipToOutputSource) { + + // Check if we should create directory for ZIP output (legacy behavior) + // 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 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 b862ace9480f..3e28318e805c 100644 --- a/Tasks/DotNetCoreCLIV2/task.json +++ b/Tasks/DotNetCoreCLIV2/task.json @@ -179,6 +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": "selectOrConfig", "aliases": [