Skip to content

Fix DotNetCoreCLI@2 zipAfterPublish to preserve ZIP file location #21045

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
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
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
109 changes: 109 additions & 0 deletions Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishLegacyTests.ts
Original file line number Diff line number Diff line change
@@ -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 = <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();
109 changes: 109 additions & 0 deletions Tasks/DotNetCoreCLIV2/Tests/zipAfterPublishTests.ts
Original file line number Diff line number Diff line change
@@ -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 = <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();
10 changes: 9 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 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.workingDirectory = tl.getPathInput("workingDirectory", false);
}

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