Skip to content

Commit d70c0cc

Browse files
rm-dmirisb464fdagoodnagarajkumanolerazvan
authored
Extend GoToolV0 task with optional GoDownloadUrl parameter and env variable GOTOOL_GODOWNLOADURL
* Configure gotools task to pass custom url from customer * Update task to fetch latest patch version * Add caching to aka.ms go version * Refactor to address revision from microsoft go build * Update Tasks/GoToolV0/gotool.ts Co-authored-by: Davis Goodin <dagood@users.noreply.github.com> * Unify baseurl parsing * Update comment * bump the version to current sprint * Use download URL as optional parameter instead of download source and recreate unit tests * Add unit tests and fix download official URL * Try to fix unit test * Purify unit tests * Add anti SSRF check unit test * Update input parameter type and readme * Implement PR comments * Address PR comments * Implement PR comments and do minor refactoring in gotool.ts * Add environment variable * Code polishing * Rename env variable * Extract URL resolution to separate method * Remove base from names of task parameter and environment variable * Update readme * Update param description in task.json and resources * Update readme * Reorder functions in gotool.ts --------- Co-authored-by: sb464f <32311730+sb464f@users.noreply.github.com> Co-authored-by: Davis Goodin <dagood@users.noreply.github.com> Co-authored-by: nagarajku <151506811+nagarajku@users.noreply.github.com> Co-authored-by: Razvan Manole <razvanmanole@microsoft.com>
1 parent 4ed801f commit d70c0cc

31 files changed

+1898
-89
lines changed

Tasks/GoToolV0/README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ This task can run on Windows, Linux, or Mac machines.
2424

2525
For more details about the versions, see [Go Language Release Page](https://golang.org/doc/devel/release.html).
2626

27-
* **GOPATH\*:** Specify a new value for the GOPATH environment variable if you want to modify it.
28-
* **GOBIN\*:** Specify a new value for the GOBIN environment variable if you want to modify it.
27+
* **GOPATH:** Specify a new value for the GOPATH environment variable if you want to modify it.
28+
29+
* **GOBIN:** Specify a new value for the GOBIN environment variable if you want to modify it.
30+
31+
* **Go download URL:** URL for downloading Go binaries. Leave empty to use the default (https://go.dev/dl). Supported URLs:
32+
- `https://go.dev/dl` - [Official Go distribution](https://go.dev/dl/). (default)
33+
- `https://aka.ms/golang/release/latest` - the [Microsoft build of Go](https://github.com/microsoft/go), a fork of the official Go distribution. See [the Migration Guide](https://github.com/microsoft/go/blob/microsoft/main/eng/doc/MigrationGuide.md) for an introduction to the Microsoft build of Go.
2934

Tasks/GoToolV0/Strings/resources.resjson/en-US/resources.resjson

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
"loc.input.help.goPath": "A custom value for the GOPATH environment variable.",
1111
"loc.input.label.goBin": "GOBIN",
1212
"loc.input.help.goBin": "A custom value for the GOBIN environment variable.",
13+
"loc.input.label.goDownloadUrl": "Go download URL",
14+
"loc.input.help.goDownloadUrl": "URL for downloading Go binaries. Only https://go.dev/dl (official) and https://aka.ms/golang/release/latest (Microsoft build) are supported. If omitted, the official Go download URL (https://go.dev/dl) will be used. This parameter takes priority over the GOTOOL_GODOWNLOADURL environment variable if both are set.",
1315
"loc.messages.FailedToDownload": "Failed to download Go version %s. Verify that the version is valid and resolve any other issues. Error: %s",
1416
"loc.messages.TempDirNotSet": "The 'Agent.TempDirectory' environment variable was expected to be set."
1517
}

Tasks/GoToolV0/Tests/L0.ts

Lines changed: 220 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,227 @@
1-
import fs = require('fs');
2-
import assert = require('assert');
3-
import path = require('path');
1+
import * as path from "path";
2+
import * as assert from "assert";
3+
import { MockTestRunner } from "azure-pipelines-task-lib/mock-test";
4+
import tl = require('azure-pipelines-task-lib');
45

5-
describe('GoToolV0 Suite', function () {
6-
before(() => {
6+
describe('GoToolV0 Suite', function() {
7+
this.timeout(60000);
8+
9+
before((done) => {
10+
done();
711
});
812

9-
after(() => {
13+
after(function () {
14+
// Cleanup if needed
1015
});
1116

12-
it('Does a basic hello world test', function(done: MochaDone) {
13-
// TODO - add real tests
14-
done();
17+
// Official Go (go.dev) tests
18+
it('Should install official Go with full patch version', async () => {
19+
let tp = path.join(__dirname, 'L0OfficialGoPatch.js');
20+
let tr: MockTestRunner = new MockTestRunner(tp);
21+
await tr.runAsync();
22+
assert(tr.succeeded, 'Should have succeeded');
23+
assert(tr.stdOutContained('https://go.dev/dl/go1.22.3'), 'Should use official Go storage URL');
24+
assert(tr.stdOutContained('Caching tool: go version: 1.22.3'), 'Should cache with toolName "go"');
25+
});
26+
27+
it('Should resolve official Go major.minor to latest patch', async () => {
28+
let tp = path.join(__dirname, 'L0OfficialGoMinor.js');
29+
let tr: MockTestRunner = new MockTestRunner(tp);
30+
await tr.runAsync();
31+
assert(tr.succeeded, 'Should have succeeded');
32+
assert(tr.stdOutContained('Resolved 1.21 to 1.21.5'), 'Should resolve to latest patch via go.dev API');
33+
assert(tr.stdOutContained('https://go.dev/dl/go1.21.5'), 'Should download resolved version');
34+
});
35+
36+
// Microsoft Go (aka.ms) tests
37+
it('Should install Microsoft Go with major.minor version', async () => {
38+
let tp = path.join(__dirname, 'L0MicrosoftGoMinor.js');
39+
let tr: MockTestRunner = new MockTestRunner(tp);
40+
await tr.runAsync();
41+
assert(tr.succeeded, 'Should have succeeded');
42+
assert(tr.stdOutContained('https://aka.ms/golang/release/latest/go1.25.0'), 'Should use Microsoft aka.ms URL');
43+
assert(tr.stdOutContained('Caching tool: go-aka version: 1.25.0'), 'Should cache with toolName "go-aka"');
44+
});
45+
46+
it('Should install Microsoft Go with patch version', async () => {
47+
let tp = path.join(__dirname, 'L0MicrosoftGoPatch.js');
48+
let tr: MockTestRunner = new MockTestRunner(tp);
49+
await tr.runAsync();
50+
assert(tr.succeeded, 'Should have succeeded');
51+
assert(tr.stdOutContained('https://aka.ms/golang/release/latest/go1.24.7'), 'Should use Microsoft URL');
52+
assert(tr.stdOutContained('Caching tool: go-aka version: 1.24.7-2'), 'Should resolve to latest revision from manifest');
53+
});
54+
55+
it('Should install Microsoft Go with revision format', async () => {
56+
let tp = path.join(__dirname, 'L0MicrosoftGoRevision.js');
57+
let tr: MockTestRunner = new MockTestRunner(tp);
58+
await tr.runAsync();
59+
assert(tr.succeeded, 'Should have succeeded');
60+
assert(tr.stdOutContained('https://aka.ms/golang/release/latest/go1.24.7-1'), 'Should use exact revision');
61+
assert(tr.stdOutContained('Caching tool: go-aka version: 1.24.7-1'), 'Should cache with specified revision');
62+
});
63+
64+
// Caching tests
65+
it('Should use cached official Go version', async () => {
66+
let tp = path.join(__dirname, 'L0CachedOfficial.js');
67+
let tr: MockTestRunner = new MockTestRunner(tp);
68+
await tr.runAsync();
69+
assert(tr.succeeded, 'Should have succeeded');
70+
assert(tr.stdOutContained('Found cached tool: go version 1.22.3'), 'Should find cached version');
71+
assert(!tr.stdOutContained('Downloading Go from'), 'Should not download when cached');
72+
});
73+
74+
it('Should use cached Microsoft Go version', async () => {
75+
let tp = path.join(__dirname, 'L0CachedMicrosoft.js');
76+
let tr: MockTestRunner = new MockTestRunner(tp);
77+
await tr.runAsync();
78+
assert(tr.succeeded, 'Should have succeeded');
79+
assert(tr.stdOutContained('Found cached tool: go-aka version 1.25.0-1'), 'Should find cached Microsoft build with resolved version');
80+
assert(!tr.stdOutContained('Downloading Go from'), 'Should not download when cached');
81+
});
82+
83+
// Environment variable tests
84+
it('Should set environment variables correctly', async () => {
85+
let tp = path.join(__dirname, 'L0EnvironmentVariables.js');
86+
let tr: MockTestRunner = new MockTestRunner(tp);
87+
await tr.runAsync();
88+
assert(tr.succeeded, 'Should have succeeded');
89+
assert(tr.stdOutContained('##vso[task.setvariable variable=GOROOT'), 'Should set GOROOT');
90+
assert(tr.stdOutContained('##vso[task.setvariable variable=GOPATH'), 'Should set GOPATH');
91+
assert(tr.stdOutContained('##vso[task.setvariable variable=GOBIN'), 'Should set GOBIN');
92+
});
93+
94+
// Cross-platform tests
95+
it('Should generate correct filename for Windows', async () => {
96+
let tp = path.join(__dirname, 'L0FilenameWindows.js');
97+
let tr: MockTestRunner = new MockTestRunner(tp);
98+
await tr.runAsync();
99+
assert(tr.succeeded, 'Should have succeeded');
100+
assert(tr.stdOutContained('go1.22.3.windows-amd64.zip'), 'Should generate Windows zip filename');
101+
});
102+
103+
it('Should generate correct filename for Linux', async () => {
104+
let tp = path.join(__dirname, 'L0FilenameLinux.js');
105+
let tr: MockTestRunner = new MockTestRunner(tp);
106+
await tr.runAsync();
107+
assert(tr.succeeded, 'Should have succeeded');
108+
assert(tr.stdOutContained('go1.22.3.linux-amd64.tar.gz'), 'Should generate Linux tar.gz filename');
109+
});
110+
111+
it('Should generate correct filename for Darwin/macOS', async () => {
112+
let tp = path.join(__dirname, 'L0FilenameDarwin.js');
113+
let tr: MockTestRunner = new MockTestRunner(tp);
114+
await tr.runAsync();
115+
assert(tr.succeeded, 'Should have succeeded');
116+
assert(tr.stdOutContained('go1.22.3.darwin-amd64.tar.gz'), 'Should generate Darwin tar.gz filename');
117+
});
118+
119+
it('Should generate correct filename for ARM64 architecture', async () => {
120+
let tp = path.join(__dirname, 'L0FilenameArm64.js');
121+
let tr: MockTestRunner = new MockTestRunner(tp);
122+
await tr.runAsync();
123+
assert(tr.succeeded, 'Should have succeeded');
124+
assert(tr.stdOutContained('go1.22.3.linux-arm64.tar.gz'), 'Should generate ARM64 filename');
125+
});
126+
127+
// Error handling tests
128+
it('Should fail with empty version input', async () => {
129+
let tp = path.join(__dirname, 'L0InvalidVersionEmpty.js');
130+
let tr: MockTestRunner = new MockTestRunner(tp);
131+
await tr.runAsync();
132+
assert(tr.failed, 'Should have failed');
133+
assert(tr.stdOutContained('Input required: version'), 'Should show validation error');
134+
});
135+
136+
it('Should fail with null version input', async () => {
137+
let tp = path.join(__dirname, 'L0InvalidVersionNull.js');
138+
let tr: MockTestRunner = new MockTestRunner(tp);
139+
await tr.runAsync();
140+
assert(tr.failed, 'Should have failed');
141+
assert(tr.stdOutContained('Input required: version') || tr.stdOutContained('Input \'version\' is required'), 'Should reject null version');
142+
});
143+
144+
it('Should fail with undefined version input', async () => {
145+
let tp = path.join(__dirname, 'L0InvalidVersionUndefined.js');
146+
let tr: MockTestRunner = new MockTestRunner(tp);
147+
await tr.runAsync();
148+
assert(tr.failed, 'Should have failed');
149+
assert(tr.stdOutContained('Input required: version'), 'Should reject undefined version');
150+
});
151+
152+
it('Should fail with unsupported base URL', async () => {
153+
let tp = path.join(__dirname, 'L0InvalidBaseUrl.js');
154+
let tr: MockTestRunner = new MockTestRunner(tp);
155+
await tr.runAsync();
156+
assert(tr.failed, 'Should have failed');
157+
assert(tr.stdOutContained('Invalid download URL'), 'Should reject unsupported URLs');
158+
});
159+
160+
it('Should fail with unparseable version format', async () => {
161+
let tp = path.join(__dirname, 'L0InvalidVersionFormatParseError.js');
162+
let tr: MockTestRunner = new MockTestRunner(tp);
163+
await tr.runAsync();
164+
assert(tr.failed, 'Should have failed');
165+
assert(tr.stdOutContained('Invalid version format'), 'Should reject version that cannot be parsed');
166+
});
167+
168+
it('Should fail when official Go version includes revision', async () => {
169+
let tp = path.join(__dirname, 'L0InvalidVersionFormatOfficialWithRevision.js');
170+
let tr: MockTestRunner = new MockTestRunner(tp);
171+
await tr.runAsync();
172+
assert(tr.failed, 'Should have failed');
173+
assert(tr.stdOutContained('Official Go version must be'), 'Should reject revision syntax for official Go');
174+
});
175+
176+
it('Should fail on download errors', async () => {
177+
let tp = path.join(__dirname, 'L0DownloadFailure.js');
178+
let tr: MockTestRunner = new MockTestRunner(tp);
179+
await tr.runAsync();
180+
assert(tr.failed, 'Should have failed');
181+
assert(tr.stdOutContained('Failed to download version'), 'Should show download failure');
182+
});
183+
184+
it('Should fail when go.dev API returns no matching version', async () => {
185+
let tp = path.join(__dirname, 'L0NoMatchingVersion.js');
186+
let tr: MockTestRunner = new MockTestRunner(tp);
187+
await tr.runAsync();
188+
assert(tr.failed, 'Should have failed');
189+
assert(tr.stdOutContained('has no stable patch release yet'), 'Should indicate no stable release');
190+
});
191+
192+
// Security tests for URL validation against SSRF attacks
193+
it('Should block URL parser confusion attack (@-based)', async () => {
194+
let tp = path.join(__dirname, 'L0SecurityURLValidation.js');
195+
let tr: MockTestRunner = new MockTestRunner(tp);
196+
await tr.runAsync();
197+
assert(tr.failed, 'Should have failed with malicious URL');
198+
assert(tr.stdOutContained('Invalid download URL'), 'Should reject malicious URL with validation error');
199+
});
200+
201+
// Environment variable tests for goDownloadUrl
202+
it('Should use GoTool.GoDownloadUrl environment variable when parameter is not set', async () => {
203+
let tp = path.join(__dirname, 'L0EnvVarOnly.js');
204+
let tr: MockTestRunner = new MockTestRunner(tp);
205+
await tr.runAsync();
206+
assert(tr.succeeded, 'Should have succeeded');
207+
assert(tr.stdOutContained('Using GoTool.GoDownloadUrl environment variable'), 'Should log environment variable usage');
208+
assert(tr.stdOutContained('go.dev/dl'), 'Should use URL from environment variable');
209+
});
210+
211+
it('Should use parameter value when both parameter and environment variable are set', async () => {
212+
let tp = path.join(__dirname, 'L0BothParamAndEnvVar.js');
213+
let tr: MockTestRunner = new MockTestRunner(tp);
214+
await tr.runAsync();
215+
assert(tr.succeeded, 'Should have succeeded');
216+
assert(tr.stdOutContained('Both goDownloadUrl parameter and GoTool.GoDownloadUrl environment variable are set'), 'Should log precedence decision');
217+
assert(tr.stdOutContained('Correctly using parameter URL over environment variable'), 'Should use parameter URL');
218+
});
219+
220+
it('Should fail with invalid URL in GoTool.GoDownloadUrl environment variable', async () => {
221+
let tp = path.join(__dirname, 'L0InvalidEnvVarUrl.js');
222+
let tr: MockTestRunner = new MockTestRunner(tp);
223+
await tr.runAsync();
224+
assert(tr.failed, 'Should have failed');
225+
assert(tr.stdOutContained('Invalid download URL'), 'Should reject unsupported URL from environment variable');
15226
});
16227
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import tmrm = require('azure-pipelines-task-lib/mock-run');
2+
import path = require('path');
3+
import * as fs from 'fs';
4+
5+
// Create temporary directory for test
6+
const tempDir = path.join(__dirname, '_temp');
7+
if (!fs.existsSync(tempDir)) {
8+
fs.mkdirSync(tempDir, { recursive: true });
9+
}
10+
11+
// Mock environment variables BEFORE creating TaskMockRunner
12+
process.env['AGENT_TEMPDIRECTORY'] = tempDir;
13+
process.env['GOTOOL_GODOWNLOADURL'] = 'https://example.com/alternate';
14+
15+
let taskPath = path.join(__dirname, '..', 'gotool.js');
16+
let tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath);
17+
18+
// Set inputs - both parameter and environment variable
19+
tmr.setInput('version', '1.21.3');
20+
tmr.setInput('goDownloadUrl', 'https://go.dev/dl');
21+
22+
// Mock tool-lib
23+
tmr.registerMock('azure-pipelines-tool-lib/tool', {
24+
findLocalTool: function(toolName: string, versionSpec: string): string | null {
25+
return null;
26+
},
27+
downloadTool: function(url: string): Promise<string> {
28+
console.log(`Download URL: ${url}`);
29+
// Verify it uses the parameter URL (go.dev/dl), not the env var (example.com)
30+
if (url.includes('go.dev/dl')) {
31+
console.log('Correctly using parameter URL over environment variable');
32+
}
33+
return Promise.resolve('/mock/download/path');
34+
},
35+
extractTar: function(file: string): Promise<string> {
36+
return Promise.resolve('/mock/extract/path');
37+
},
38+
extractZip: function(file: string): Promise<string> {
39+
return Promise.resolve('/mock/extract/path');
40+
},
41+
cacheDir: function(sourceDir: string, tool: string, version: string): Promise<string> {
42+
return Promise.resolve('/mock/cache/path');
43+
},
44+
prependPath: function(toolPath: string): void {
45+
console.log(`Adding to PATH: ${toolPath}`);
46+
}
47+
});
48+
49+
// Mock os module
50+
tmr.registerMock('os', {
51+
platform: (): string => 'linux',
52+
arch: (): string => 'x64'
53+
});
54+
55+
// Mock telemetry
56+
tmr.registerMock('azure-pipelines-tasks-utility-common/telemetry', {
57+
emitTelemetry: function(area: string, feature: string, properties: any): void {
58+
console.log(`Telemetry: ${area}.${feature}`);
59+
}
60+
});
61+
62+
tmr.run();
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import tmrm = require('azure-pipelines-task-lib/mock-run');
2+
import path = require('path');
3+
4+
let taskPath = path.join(__dirname, '..', 'gotool.js');
5+
let tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath);
6+
7+
// Set inputs for cached Microsoft Go version
8+
tmr.setInput('version', '1.25.0');
9+
tmr.setInput('goDownloadUrl', 'https://aka.ms/golang/release/latest');
10+
11+
// Mock tool lib functions
12+
tmr.registerMock('azure-pipelines-tool-lib/tool', {
13+
findLocalTool: function(toolName: string, version: string) {
14+
console.log(`Found cached tool: ${toolName} version ${version}`);
15+
// Return cached path to simulate found Microsoft build
16+
// Note: version should be the fully qualified version (1.25.0-1) after manifest resolution
17+
return '/mock/cache/go-aka/1.25.0-1';
18+
},
19+
downloadTool: function(url: string) {
20+
// Microsoft Go needs to download manifest even when version is cached
21+
console.log(`Downloading manifest from: ${url}`);
22+
if (url.includes('go1.25.0.assets.json')) {
23+
return Promise.resolve('/mock/manifest.json');
24+
}
25+
throw new Error(`Unexpected download URL: ${url}`);
26+
},
27+
prependPath: function(toolPath: string) {
28+
console.log(`Adding to PATH: ${toolPath}`);
29+
}
30+
});
31+
32+
// Mock os module
33+
tmr.registerMock('os', {
34+
platform: () => 'linux',
35+
arch: () => 'x64'
36+
});
37+
38+
// Mock telemetry
39+
tmr.registerMock('azure-pipelines-tasks-utility-common/telemetry', {
40+
emitTelemetry: function(area: string, feature: string, properties: any) {
41+
console.log(`Telemetry: ${area}.${feature} - version: ${properties.version}`);
42+
}
43+
});
44+
45+
// Mock fs to return manifest data
46+
tmr.registerMock('fs', {
47+
readFileSync: function(filePath: string, encoding: string) {
48+
console.log(`Reading file: ${filePath}`);
49+
if (filePath.includes('manifest.json')) {
50+
// Return Microsoft Go manifest with version field (lowercase)
51+
return JSON.stringify({
52+
version: "1.25.0-1",
53+
files: [
54+
{
55+
filename: "go1.25.0-1.linux-amd64.tar.gz",
56+
os: "linux",
57+
arch: "amd64"
58+
}
59+
]
60+
});
61+
}
62+
return JSON.stringify({});
63+
}
64+
});
65+
66+
tmr.run();

0 commit comments

Comments
 (0)