Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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
24 changes: 22 additions & 2 deletions Tasks/DotNetCoreCLIV2/Tests/L0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
96 changes: 96 additions & 0 deletions Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishLegacyTests.ts
Original file line number Diff line number Diff line change
@@ -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 = <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();
96 changes: 96 additions & 0 deletions Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishTests.ts
Original file line number Diff line number Diff line change
@@ -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', "false"); // Test new simplified behavior

// 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 = <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();
8 changes: 7 additions & 1 deletion Tasks/DotNetCoreCLIV2/dotnetcore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}

Expand Down Expand Up @@ -286,10 +288,14 @@ 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)
if (moveZipToOutputSource && this.zipAfterPublishCreateDirectory) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot this change is asking customer to provide this input, make it a feature flag based check tl.getPipelineFeature

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've implemented the change to use a pipeline feature flag instead of customer input. The zipAfterPublishCreateDirectory parameter has been removed and replaced with tl.getPipelineFeature('DotNetCoreCLIZipAfterPublishSimplified'). When the feature flag is enabled, it uses the simplified behavior (no directory creation), and when disabled it uses the legacy behavior. This maintains backward compatibility while allowing service-level control. Commit: 9d3ea7d

// 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);
Expand Down
9 changes: 9 additions & 0 deletions Tasks/DotNetCoreCLIV2/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down