Skip to content

Commit a6451bc

Browse files
committed
Robocopy trailing slash issue
1 parent 5c0ad26 commit a6451bc

File tree

6 files changed

+112
-24
lines changed

6 files changed

+112
-24
lines changed

Tasks/PublishBuildArtifacts/publishbuildartifacts.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,43 @@ var process = require('process');
77
import tl = require('vsts-task-lib/task');
88
import tr = require('vsts-task-lib/toolrunner');
99

10-
// used for escaping file paths that are passed into the powershell command
11-
let pathToPSString = (filePath: string) => {
12-
let result: string =
13-
filePath.replace(/"/g, '') // remove double quotes
14-
.replace(/'/g, "''"); // double-up single quotes
15-
return `'${result}'`; // enclose in single quotes
10+
// used for escaping the path to the Invoke-Robocopy.ps1 script that is passed to the powershell command
11+
let pathToScriptPSString = (filePath: string) => {
12+
// remove double quotes
13+
let result: string = filePath.replace(/"/g, '');
14+
15+
// double-up single quotes and enclose in single quotes. this is to create a single-quoted string in powershell.
16+
result = result.replace(/'/g, "''");
17+
return `'${result}'`;
18+
}
19+
20+
// used for escaping file paths that are ultimately passed to robocopy (via the powershell command)
21+
let pathToRobocopyPSString = (filePath: string) => {
22+
// the path needs to be fixed-up due to a robocopy quirk handling trailing backslashes.
23+
//
24+
// according to http://ss64.com/nt/robocopy.html:
25+
// If either the source or desination are a "quoted long foldername" do not include a
26+
// trailing backslash as this will be treated as an escape character, i.e. "C:\some path\"
27+
// will fail but "C:\some path\\" or "C:\some path\." or "C:\some path" will work.
28+
//
29+
// furthermore, PowerShell implicitly double-quotes arguments to external commands when the
30+
// argument contains unquoted spaces.
31+
//
32+
// note, details on PowerShell quoting rules for external commands can be found in the
33+
// source code here:
34+
// https://github.com/PowerShell/PowerShell/blob/v0.6.0/src/System.Management.Automation/engine/NativeCommandParameterBinder.cs
35+
36+
// remove double quotes
37+
let result: string = filePath.replace(/"/g, '');
38+
39+
// append a "." if the path ends with a backslash. e.g. "C:\some path\" -> "C:\some path\."
40+
if (result.endsWith('\\')) {
41+
result += '.';
42+
}
43+
44+
// double-up single quotes and enclose in single quotes. this is to create a single-quoted string in powershell.
45+
result = result.replace(/'/g, "''");
46+
return `'${result}'`;
1647
}
1748

1849
async function run() {
@@ -53,7 +84,7 @@ async function run() {
5384

5485
// copy the files
5586
let script: string = path.join(__dirname, 'Invoke-Robocopy.ps1');
56-
let command: string = `& ${pathToPSString(script)} -Source ${pathToPSString(pathtoPublish)} -Target ${pathToPSString(artifactPath)}`
87+
let command: string = `& ${pathToScriptPSString(script)} -Source ${pathToRobocopyPSString(pathtoPublish)} -Target ${pathToRobocopyPSString(artifactPath)}`
5788
let powershell = new tr.ToolRunner('powershell.exe');
5889
powershell.arg('-NoLogo');
5990
powershell.arg('-Sta');

Tasks/PublishBuildArtifacts/task.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"version": {
1313
"Major": 1,
1414
"Minor": 0,
15-
"Patch": 38
15+
"Patch": 39
1616
},
1717
"demands": [],
1818
"minimumAgentVersion": "1.91.0",

Tasks/PublishBuildArtifacts/task.loc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"version": {
1313
"Major": 1,
1414
"Minor": 0,
15-
"Patch": 38
15+
"Patch": 39
1616
},
1717
"demands": [],
1818
"minimumAgentVersion": "1.91.0",

Tests/L0/PublishBuildArtifacts/_suite.ts

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,62 @@ describe('Publish Build Artifacts Suite', function () {
4545
if (os.platform() == 'win32') {
4646
it('Publish to UNC', (done: MochaDone) => {
4747
setResponseFile('publishBuildArtifactsGood.json');
48-
48+
4949
let tr = new trm.TaskRunner('PublishBuildArtifacts', false, true);
50-
tr.setInput('PathtoPublish', '/bin/release');
50+
tr.setInput('PathtoPublish', 'C:\\bin\\release');
51+
tr.setInput('ArtifactName', 'drop');
52+
tr.setInput('ArtifactType', 'FilePath');
53+
tr.setInput('TargetPath', '\\\\UNCShare\\subdir');
54+
55+
tr.run()
56+
.then(() => {
57+
assert(!tr.stderr, 'should not have written to stderr. error: ' + tr.stderr);
58+
assert(tr.succeeded, 'task should have succeeded');
59+
assert(tr.stdout.indexOf('test stdout from robocopy (no trailing slashes)') >= 0, 'should copy files.');
60+
assert(tr.stdout.search(/artifact.associate/gi) >= 0, 'should associate artifact.');
61+
done();
62+
})
63+
.fail((err) => {
64+
done(err);
65+
});
66+
})
67+
68+
it('Appends . to robocopy source with trailing slash', (done: MochaDone) => {
69+
setResponseFile('publishBuildArtifactsGood.json');
70+
71+
let tr = new trm.TaskRunner('PublishBuildArtifacts', false, true);
72+
tr.setInput('PathtoPublish', 'C:\\bin\\release\\');
73+
tr.setInput('ArtifactName', 'drop');
74+
tr.setInput('ArtifactType', 'FilePath');
75+
tr.setInput('TargetPath', '\\\\UNCShare\\subdir');
76+
77+
tr.run()
78+
.then(() => {
79+
assert(!tr.stderr, 'should not have written to stderr. error: ' + tr.stderr);
80+
assert(tr.succeeded, 'task should have succeeded');
81+
assert(tr.stdout.indexOf('test stdout from robocopy (source with trailing slash)') >= 0, 'should copy files.');
82+
assert(tr.stdout.search(/artifact.associate/gi) >= 0, 'should associate artifact.');
83+
done();
84+
})
85+
.fail((err) => {
86+
done(err);
87+
});
88+
})
89+
90+
it('Appends . to robocopy target with trailing slash', (done: MochaDone) => {
91+
setResponseFile('publishBuildArtifactsGood.json');
92+
93+
let tr = new trm.TaskRunner('PublishBuildArtifacts', false, true);
94+
tr.setInput('PathtoPublish', 'C:\\bin\\release');
5195
tr.setInput('ArtifactName', 'drop');
5296
tr.setInput('ArtifactType', 'FilePath');
5397
tr.setInput('TargetPath', '\\\\UNCShare');
54-
98+
5599
tr.run()
56100
.then(() => {
57101
assert(!tr.stderr, 'should not have written to stderr. error: ' + tr.stderr);
58102
assert(tr.succeeded, 'task should have succeeded');
59-
assert(tr.stdout.match(/test stdout from robocopy/gi).length === 1, 'should copy files.');
103+
assert(tr.stdout.indexOf('test stdout from robocopy (target with trailing slash)') >= 0, 'should copy files.');
60104
assert(tr.stdout.search(/artifact.associate/gi) >= 0, 'should associate artifact.');
61105
done();
62106
})
@@ -69,10 +113,10 @@ describe('Publish Build Artifacts Suite', function () {
69113
setResponseFile('publishBuildArtifactsBad.json');
70114

71115
let tr = new trm.TaskRunner('PublishBuildArtifacts', false, true);
72-
tr.setInput('PathtoPublish', '/bin/release');
116+
tr.setInput('PathtoPublish', 'C:\\bin\\release');
73117
tr.setInput('ArtifactName', 'drop');
74118
tr.setInput('ArtifactType', 'FilePath');
75-
tr.setInput('TargetPath', '\\\\UNCShare');
119+
tr.setInput('TargetPath', '\\\\UNCShare\\subdir');
76120

77121
tr.run()
78122
.then(() => {
@@ -90,16 +134,16 @@ describe('Publish Build Artifacts Suite', function () {
90134
setResponseFile('publishBuildArtifactsGood.json');
91135

92136
let tr = new trm.TaskRunner('PublishBuildArtifacts', false, true);
93-
tr.setInput('PathtoPublish', '/bin/release');
137+
tr.setInput('PathtoPublish', 'C:\\bin\\release');
94138
tr.setInput('ArtifactName', 'drop');
95139
tr.setInput('ArtifactType', 'FilePath');
96-
tr.setInput('TargetPath', '\\\\UNCShare');
140+
tr.setInput('TargetPath', '\\\\UNCShare\\subdir');
97141

98142
tr.run()
99143
.then(() => {
100144
assert(!tr.stderr, 'should not have written to stderr. error: ' + tr.stderr);
101145
assert(tr.succeeded, 'task should have succeeded');
102-
assert(tr.stdout.indexOf('##vso[artifact.associate artifacttype=filepath;artifactname=drop;artifactlocation=\\\\UNCShare;]\\\\UNCShare') >= 0, 'should associate artifact.');
146+
assert(tr.stdout.indexOf('##vso[artifact.associate artifacttype=filepath;artifactname=drop;artifactlocation=\\\\UNCShare\\subdir;]\\\\UNCShare\\subdir') >= 0, 'should associate artifact.');
103147
done();
104148
})
105149
.fail((err) => {
@@ -115,7 +159,7 @@ describe('Publish Build Artifacts Suite', function () {
115159
tr.setInput('PathtoPublish', '/bin/release');
116160
tr.setInput('ArtifactName', 'drop');
117161
tr.setInput('ArtifactType', 'FilePath');
118-
tr.setInput('TargetPath', '\\\\UNCShare');
162+
tr.setInput('TargetPath', '\\\\UNCShare\\subdir');
119163

120164
tr.run()
121165
.then(() => {

Tests/L0/PublishBuildArtifacts/publishBuildArtifactsBad.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
{
22
"checkPath": {
3-
"/bin/release": true
3+
"/bin/release": true,
4+
"C:\\bin\\release": true
45
},
56
"exec": {
6-
"powershell.exe -NoLogo -Sta -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command & '\\PublishBuildArtifacts\\Invoke-Robocopy.ps1' -Source '/bin/release' -Target '\\\\UNCShare\\drop\\'": {
7+
"powershell.exe -NoLogo -Sta -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command & '\\PublishBuildArtifacts\\Invoke-Robocopy.ps1' -Source 'C:\\bin\\release' -Target '\\\\UNCShare\\subdir\\drop'": {
78
"stdout": "test stdout from robocopy middle-man",
89
"stderr": "",
910
"code": 1

Tests/L0/PublishBuildArtifacts/publishBuildArtifactsGood.json

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
{
22
"checkPath": {
3-
"/bin/release": true
3+
"/bin/release": true,
4+
"C:\\bin\\release\\": true,
5+
"C:\\bin\\release": true
46
},
57
"exec": {
6-
"powershell.exe -NoLogo -Sta -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command & '\\PublishBuildArtifacts\\Invoke-Robocopy.ps1' -Source '/bin/release' -Target '\\\\UNCShare\\drop\\'": {
7-
"stdout": "test stdout from robocopy",
8+
"powershell.exe -NoLogo -Sta -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command & '\\PublishBuildArtifacts\\Invoke-Robocopy.ps1' -Source 'C:\\bin\\release' -Target '\\\\UNCShare\\subdir\\drop'": {
9+
"stdout": "test stdout from robocopy (no trailing slashes)",
10+
"stderr": "",
11+
"code": 0
12+
},
13+
"powershell.exe -NoLogo -Sta -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command & '\\PublishBuildArtifacts\\Invoke-Robocopy.ps1' -Source 'C:\\bin\\release\\.' -Target '\\\\UNCShare\\subdir\\drop'": {
14+
"stdout": "test stdout from robocopy (source with trailing slash)",
15+
"stderr": "",
16+
"code": 0
17+
},
18+
"powershell.exe -NoLogo -Sta -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command & '\\PublishBuildArtifacts\\Invoke-Robocopy.ps1' -Source 'C:\\bin\\release' -Target '\\\\UNCShare\\drop\\.'": {
19+
"stdout": "test stdout from robocopy (target with trailing slash)",
820
"stderr": "",
921
"code": 0
1022
}

0 commit comments

Comments
 (0)