Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
150cc4e
Configure gotools task to pass custom url from customer
sb464f Sep 2, 2025
3006e62
Update task to fetch latest patch version
sb464f Sep 3, 2025
b66a77e
Add caching to aka.ms go version
sb464f Sep 18, 2025
a2efe21
Refactor to address revision from microsoft go build
sb464f Oct 1, 2025
dc45ef6
Update Tasks/GoToolV0/gotool.ts
sb464f Oct 1, 2025
05888ff
Unify baseurl parsing
sb464f Oct 1, 2025
6c186a8
Update comment
sb464f Oct 8, 2025
6bd7e7d
Merge branch 'master' into feature/gotool
sb464f Oct 13, 2025
a1d36b9
Merge branch 'master' into feature/gotool
nagarajku Oct 16, 2025
2fdf131
Merge branch 'pr-21285' into users/razvanmanole/GoToolDownloadLink
manolerazvan Oct 23, 2025
c75ad2f
bump the version to current sprint
manolerazvan Oct 23, 2025
384661d
Use download URL as optional parameter instead of download source and…
rm-dmiri Oct 24, 2025
637ebfd
Add unit tests and fix download official URL
rm-dmiri Oct 24, 2025
2755f2f
Try to fix unit test
rm-dmiri Oct 24, 2025
334ce1a
Purify unit tests
rm-dmiri Oct 24, 2025
c7c8e0a
Add anti SSRF check unit test
rm-dmiri Oct 24, 2025
8334c75
Update input parameter type and readme
rm-dmiri Oct 24, 2025
a9f5a5a
Implement PR comments
rm-dmiri Oct 24, 2025
972ffa6
Address PR comments
rm-dmiri Oct 27, 2025
b8aa075
Implement PR comments and do minor refactoring in gotool.ts
rm-dmiri Oct 28, 2025
de838ab
Add environment variable
rm-dmiri Oct 29, 2025
a9af79a
Code polishing
rm-dmiri Oct 29, 2025
da8e362
Rename env variable
rm-dmiri Oct 29, 2025
f578599
Extract URL resolution to separate method
rm-dmiri Oct 29, 2025
30ba1c8
Remove base from names of task parameter and environment variable
rm-dmiri Oct 29, 2025
6ee1ded
Update readme
rm-dmiri Oct 29, 2025
2aaf96d
Update param description in task.json and resources
rm-dmiri Oct 29, 2025
234683e
Update readme
rm-dmiri Oct 30, 2025
a1a3469
Reorder functions in gotool.ts
rm-dmiri Oct 30, 2025
e6cd1b2
Merge branch 'master' into users/razvanmanole/GoToolDownloadLink
rm-dmiri Oct 30, 2025
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
9 changes: 7 additions & 2 deletions Tasks/GoToolV0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ This task can run on Windows, Linux, or Mac machines.

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

* **GOPATH\*:** Specify a new value for the GOPATH environment variable if you want to modify it.
* **GOBIN\*:** Specify a new value for the GOBIN environment variable if you want to modify it.
* **GOPATH:** Specify a new value for the GOPATH environment variable if you want to modify it.

* **GOBIN:** Specify a new value for the GOBIN environment variable if you want to modify it.

* **Go download URL:** URL for downloading Go binaries. Leave empty to use the default (https://go.dev/dl). Supported URLs:
- `https://go.dev/dl` - [Official Go distribution](https://go.dev/dl/). (default)
- `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.

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"loc.input.help.goPath": "A custom value for the GOPATH environment variable.",
"loc.input.label.goBin": "GOBIN",
"loc.input.help.goBin": "A custom value for the GOBIN environment variable.",
"loc.input.label.goDownloadUrl": "Go download URL",
"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.",
"loc.messages.FailedToDownload": "Failed to download Go version %s. Verify that the version is valid and resolve any other issues. Error: %s",
"loc.messages.TempDirNotSet": "The 'Agent.TempDirectory' environment variable was expected to be set."
}
229 changes: 220 additions & 9 deletions Tasks/GoToolV0/Tests/L0.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,227 @@
import fs = require('fs');
import assert = require('assert');
import path = require('path');
import * as path from "path";
import * as assert from "assert";
import { MockTestRunner } from "azure-pipelines-task-lib/mock-test";
import tl = require('azure-pipelines-task-lib');

describe('GoToolV0 Suite', function () {
before(() => {
describe('GoToolV0 Suite', function() {
this.timeout(60000);

before((done) => {
done();
});

after(() => {
after(function () {
// Cleanup if needed
});

it('Does a basic hello world test', function(done: MochaDone) {
// TODO - add real tests
done();
// Official Go (go.dev) tests
it('Should install official Go with full patch version', async () => {
let tp = path.join(__dirname, 'L0OfficialGoPatch.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.succeeded, 'Should have succeeded');
assert(tr.stdOutContained('https://go.dev/dl/go1.22.3'), 'Should use official Go storage URL');
assert(tr.stdOutContained('Caching tool: go version: 1.22.3'), 'Should cache with toolName "go"');
});

it('Should resolve official Go major.minor to latest patch', async () => {
let tp = path.join(__dirname, 'L0OfficialGoMinor.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.succeeded, 'Should have succeeded');
assert(tr.stdOutContained('Resolved 1.21 to 1.21.5'), 'Should resolve to latest patch via go.dev API');
assert(tr.stdOutContained('https://go.dev/dl/go1.21.5'), 'Should download resolved version');
});

// Microsoft Go (aka.ms) tests
it('Should install Microsoft Go with major.minor version', async () => {
let tp = path.join(__dirname, 'L0MicrosoftGoMinor.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.succeeded, 'Should have succeeded');
assert(tr.stdOutContained('https://aka.ms/golang/release/latest/go1.25.0'), 'Should use Microsoft aka.ms URL');
assert(tr.stdOutContained('Caching tool: go-aka version: 1.25.0'), 'Should cache with toolName "go-aka"');
});

it('Should install Microsoft Go with patch version', async () => {
let tp = path.join(__dirname, 'L0MicrosoftGoPatch.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.succeeded, 'Should have succeeded');
assert(tr.stdOutContained('https://aka.ms/golang/release/latest/go1.24.7'), 'Should use Microsoft URL');
assert(tr.stdOutContained('Caching tool: go-aka version: 1.24.7-2'), 'Should resolve to latest revision from manifest');
});

it('Should install Microsoft Go with revision format', async () => {
let tp = path.join(__dirname, 'L0MicrosoftGoRevision.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.succeeded, 'Should have succeeded');
assert(tr.stdOutContained('https://aka.ms/golang/release/latest/go1.24.7-1'), 'Should use exact revision');
assert(tr.stdOutContained('Caching tool: go-aka version: 1.24.7-1'), 'Should cache with specified revision');
});

// Caching tests
it('Should use cached official Go version', async () => {
let tp = path.join(__dirname, 'L0CachedOfficial.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.succeeded, 'Should have succeeded');
assert(tr.stdOutContained('Found cached tool: go version 1.22.3'), 'Should find cached version');
assert(!tr.stdOutContained('Downloading Go from'), 'Should not download when cached');
});

it('Should use cached Microsoft Go version', async () => {
let tp = path.join(__dirname, 'L0CachedMicrosoft.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.succeeded, 'Should have succeeded');
assert(tr.stdOutContained('Found cached tool: go-aka version 1.25.0-1'), 'Should find cached Microsoft build with resolved version');
assert(!tr.stdOutContained('Downloading Go from'), 'Should not download when cached');
});

// Environment variable tests
it('Should set environment variables correctly', async () => {
let tp = path.join(__dirname, 'L0EnvironmentVariables.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.succeeded, 'Should have succeeded');
assert(tr.stdOutContained('##vso[task.setvariable variable=GOROOT'), 'Should set GOROOT');
assert(tr.stdOutContained('##vso[task.setvariable variable=GOPATH'), 'Should set GOPATH');
assert(tr.stdOutContained('##vso[task.setvariable variable=GOBIN'), 'Should set GOBIN');
});

// Cross-platform tests
it('Should generate correct filename for Windows', async () => {
let tp = path.join(__dirname, 'L0FilenameWindows.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.succeeded, 'Should have succeeded');
assert(tr.stdOutContained('go1.22.3.windows-amd64.zip'), 'Should generate Windows zip filename');
});

it('Should generate correct filename for Linux', async () => {
let tp = path.join(__dirname, 'L0FilenameLinux.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.succeeded, 'Should have succeeded');
assert(tr.stdOutContained('go1.22.3.linux-amd64.tar.gz'), 'Should generate Linux tar.gz filename');
});

it('Should generate correct filename for Darwin/macOS', async () => {
let tp = path.join(__dirname, 'L0FilenameDarwin.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.succeeded, 'Should have succeeded');
assert(tr.stdOutContained('go1.22.3.darwin-amd64.tar.gz'), 'Should generate Darwin tar.gz filename');
});

it('Should generate correct filename for ARM64 architecture', async () => {
let tp = path.join(__dirname, 'L0FilenameArm64.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.succeeded, 'Should have succeeded');
assert(tr.stdOutContained('go1.22.3.linux-arm64.tar.gz'), 'Should generate ARM64 filename');
});

// Error handling tests
it('Should fail with empty version input', async () => {
let tp = path.join(__dirname, 'L0InvalidVersionEmpty.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.failed, 'Should have failed');
assert(tr.stdOutContained('Input required: version'), 'Should show validation error');
});

it('Should fail with null version input', async () => {
let tp = path.join(__dirname, 'L0InvalidVersionNull.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.failed, 'Should have failed');
assert(tr.stdOutContained('Input required: version') || tr.stdOutContained('Input \'version\' is required'), 'Should reject null version');
});

it('Should fail with undefined version input', async () => {
let tp = path.join(__dirname, 'L0InvalidVersionUndefined.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.failed, 'Should have failed');
assert(tr.stdOutContained('Input required: version'), 'Should reject undefined version');
});

it('Should fail with unsupported base URL', async () => {
let tp = path.join(__dirname, 'L0InvalidBaseUrl.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.failed, 'Should have failed');
assert(tr.stdOutContained('Invalid download URL'), 'Should reject unsupported URLs');
});

it('Should fail with unparseable version format', async () => {
let tp = path.join(__dirname, 'L0InvalidVersionFormatParseError.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.failed, 'Should have failed');
assert(tr.stdOutContained('Invalid version format'), 'Should reject version that cannot be parsed');
});

it('Should fail when official Go version includes revision', async () => {
let tp = path.join(__dirname, 'L0InvalidVersionFormatOfficialWithRevision.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.failed, 'Should have failed');
assert(tr.stdOutContained('Official Go version must be'), 'Should reject revision syntax for official Go');
});

it('Should fail on download errors', async () => {
let tp = path.join(__dirname, 'L0DownloadFailure.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.failed, 'Should have failed');
assert(tr.stdOutContained('Failed to download version'), 'Should show download failure');
});

it('Should fail when go.dev API returns no matching version', async () => {
let tp = path.join(__dirname, 'L0NoMatchingVersion.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.failed, 'Should have failed');
assert(tr.stdOutContained('has no stable patch release yet'), 'Should indicate no stable release');
});

// Security tests for URL validation against SSRF attacks
it('Should block URL parser confusion attack (@-based)', async () => {
let tp = path.join(__dirname, 'L0SecurityURLValidation.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.failed, 'Should have failed with malicious URL');
assert(tr.stdOutContained('Invalid download URL'), 'Should reject malicious URL with validation error');
});

// Environment variable tests for goDownloadUrl
it('Should use GoTool.GoDownloadUrl environment variable when parameter is not set', async () => {
let tp = path.join(__dirname, 'L0EnvVarOnly.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.succeeded, 'Should have succeeded');
assert(tr.stdOutContained('Using GoTool.GoDownloadUrl environment variable'), 'Should log environment variable usage');
assert(tr.stdOutContained('go.dev/dl'), 'Should use URL from environment variable');
});

it('Should use parameter value when both parameter and environment variable are set', async () => {
let tp = path.join(__dirname, 'L0BothParamAndEnvVar.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.succeeded, 'Should have succeeded');
assert(tr.stdOutContained('Both goDownloadUrl parameter and GoTool.GoDownloadUrl environment variable are set'), 'Should log precedence decision');
assert(tr.stdOutContained('Correctly using parameter URL over environment variable'), 'Should use parameter URL');
});

it('Should fail with invalid URL in GoTool.GoDownloadUrl environment variable', async () => {
let tp = path.join(__dirname, 'L0InvalidEnvVarUrl.js');
let tr: MockTestRunner = new MockTestRunner(tp);
await tr.runAsync();
assert(tr.failed, 'Should have failed');
assert(tr.stdOutContained('Invalid download URL'), 'Should reject unsupported URL from environment variable');
});
});
62 changes: 62 additions & 0 deletions Tasks/GoToolV0/Tests/L0BothParamAndEnvVar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import tmrm = require('azure-pipelines-task-lib/mock-run');
import path = require('path');
import * as fs from 'fs';

// Create temporary directory for test
const tempDir = path.join(__dirname, '_temp');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}

// Mock environment variables BEFORE creating TaskMockRunner
process.env['AGENT_TEMPDIRECTORY'] = tempDir;
process.env['GOTOOL_GODOWNLOADURL'] = 'https://example.com/alternate';

let taskPath = path.join(__dirname, '..', 'gotool.js');
let tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath);

// Set inputs - both parameter and environment variable
tmr.setInput('version', '1.21.3');
tmr.setInput('goDownloadUrl', 'https://go.dev/dl');

// Mock tool-lib
tmr.registerMock('azure-pipelines-tool-lib/tool', {
findLocalTool: function(toolName: string, versionSpec: string): string | null {
return null;
},
downloadTool: function(url: string): Promise<string> {
console.log(`Download URL: ${url}`);
// Verify it uses the parameter URL (go.dev/dl), not the env var (example.com)
if (url.includes('go.dev/dl')) {
console.log('Correctly using parameter URL over environment variable');
}
return Promise.resolve('/mock/download/path');
},
extractTar: function(file: string): Promise<string> {
return Promise.resolve('/mock/extract/path');
},
extractZip: function(file: string): Promise<string> {
return Promise.resolve('/mock/extract/path');
},
cacheDir: function(sourceDir: string, tool: string, version: string): Promise<string> {
return Promise.resolve('/mock/cache/path');
},
prependPath: function(toolPath: string): void {
console.log(`Adding to PATH: ${toolPath}`);
}
});

// Mock os module
tmr.registerMock('os', {
platform: (): string => 'linux',
arch: (): string => 'x64'
});

// Mock telemetry
tmr.registerMock('azure-pipelines-tasks-utility-common/telemetry', {
emitTelemetry: function(area: string, feature: string, properties: any): void {
console.log(`Telemetry: ${area}.${feature}`);
}
});

tmr.run();
66 changes: 66 additions & 0 deletions Tasks/GoToolV0/Tests/L0CachedMicrosoft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import tmrm = require('azure-pipelines-task-lib/mock-run');
import path = require('path');

let taskPath = path.join(__dirname, '..', 'gotool.js');
let tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath);

// Set inputs for cached Microsoft Go version
tmr.setInput('version', '1.25.0');
tmr.setInput('goDownloadUrl', 'https://aka.ms/golang/release/latest');

// Mock tool lib functions
tmr.registerMock('azure-pipelines-tool-lib/tool', {
findLocalTool: function(toolName: string, version: string) {
console.log(`Found cached tool: ${toolName} version ${version}`);
// Return cached path to simulate found Microsoft build
// Note: version should be the fully qualified version (1.25.0-1) after manifest resolution
return '/mock/cache/go-aka/1.25.0-1';
},
downloadTool: function(url: string) {
// Microsoft Go needs to download manifest even when version is cached
console.log(`Downloading manifest from: ${url}`);
if (url.includes('go1.25.0.assets.json')) {
return Promise.resolve('/mock/manifest.json');
}
throw new Error(`Unexpected download URL: ${url}`);
},
prependPath: function(toolPath: string) {
console.log(`Adding to PATH: ${toolPath}`);
}
});

// Mock os module
tmr.registerMock('os', {
platform: () => 'linux',
arch: () => 'x64'
});

// Mock telemetry
tmr.registerMock('azure-pipelines-tasks-utility-common/telemetry', {
emitTelemetry: function(area: string, feature: string, properties: any) {
console.log(`Telemetry: ${area}.${feature} - version: ${properties.version}`);
}
});

// Mock fs to return manifest data
tmr.registerMock('fs', {
readFileSync: function(filePath: string, encoding: string) {
console.log(`Reading file: ${filePath}`);
if (filePath.includes('manifest.json')) {
// Return Microsoft Go manifest with version field (lowercase)
return JSON.stringify({
version: "1.25.0-1",
files: [
{
filename: "go1.25.0-1.linux-amd64.tar.gz",
os: "linux",
arch: "amd64"
}
]
});
}
return JSON.stringify({});
}
});

tmr.run();
Loading