Skip to content

Commit f48dea7

Browse files
committed
adds UsePythonVersionV1
1 parent 28ce3bd commit f48dea7

34 files changed

+3733
-0
lines changed

Tasks/UsePythonVersionV1/.npmrc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
scripts-prepend-node-path=true
2+
3+
registry=https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/
4+
5+
always-auth=true
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"loc.friendlyName": "Use Python version",
3+
"loc.helpMarkDown": "[Learn more about this task](https://go.microsoft.com/fwlink/?linkid=871498) or [see the Python documentation](https://www.python.org/doc/)",
4+
"loc.description": "Use the specified version of Python from the tool cache, optionally adding it to the PATH",
5+
"loc.instanceNameFormat": "Use Python $(versionSpec)",
6+
"loc.group.displayName.advanced": "Advanced",
7+
"loc.input.label.versionSpec": "Version spec",
8+
"loc.input.help.versionSpec": "Version range or exact version of a Python version to use, using semver's version range syntax. [More information](https://go.microsoft.com/fwlink/?LinkID=2006180)",
9+
"loc.input.label.disableDownloadFromRegistry": "Disable downloading releases from the GitHub registry",
10+
"loc.input.help.disableDownloadFromRegistry": "Whether to disable downloading missing python version from the [Github Actions registry](https://github.com/actions/python-versions). Check this if you only want to use local pre-installed python.",
11+
"loc.input.label.allowUnstable": "Allow downloading unstable releases",
12+
"loc.input.help.allowUnstable": "Allow downloading unstable python versions from [Github Actions python versions registry](https://github.com/actions/python-versions).",
13+
"loc.input.label.githubTokenAuthType": "GitHub authentication type",
14+
"loc.input.help.githubTokenAuthType": "Specify the type of authentication to use for GitHub. Use 'Token' to provide a GitHub token directly, or 'Service Connection' to use a GitHub service connection.",
15+
"loc.input.label.githubToken": "GitHub token for GitHub Actions python registry",
16+
"loc.input.help.githubToken": "GitHub token is needed to prevent anonymous requests limit to the [Github Actions python versions registry](https://github.com/actions/python-versions). Leaving this empty may cause download failures. Not needed if you only use locally installed python.",
17+
"loc.input.label.githubServiceConnection": "GitHub service connection",
18+
"loc.input.help.githubServiceConnection": "GitHub service connection to use for authentication to the [Github Actions python versions registry](https://github.com/actions/python-versions). The connection must be based on a GitHub user's OAuth, a GitHub personal access token, or GitHub App installation token. Leaving this empty may cause download failures. Not needed if you only use locally installed python.",
19+
"loc.input.label.addToPath": "Add to PATH",
20+
"loc.input.help.addToPath": "Whether to prepend the retrieved Python version to the PATH environment variable to make it available in subsequent tasks or scripts without using the output variable.",
21+
"loc.input.label.architecture": "Architecture",
22+
"loc.input.help.architecture": "The target architecture (x86, x64, arm64) of the Python interpreter.",
23+
"loc.messages.ListAvailableVersions": "Versions in %s:",
24+
"loc.messages.PlatformNotRecognized": "Platform not recognized",
25+
"loc.messages.PrependPath": "Prepending PATH environment variable with directory: %s",
26+
"loc.messages.PyPyNotFound": "Could not find an installation of PyPy%d in Agent.ToolsDirectory.",
27+
"loc.messages.ToolNotFoundMicrosoftHosted": "If this is a Microsoft-hosted agent, check that this image supports side-by-side versions of %s at %s.",
28+
"loc.messages.ToolNotFoundSelfHosted": "If this is a self-hosted agent, see how to configure side-by-side %s versions at %s.",
29+
"loc.messages.VersionNotFound": "Version spec %s for architecture %s did not match any version in Agent.ToolsDirectory.",
30+
"loc.messages.ExactVersionNotRecommended": "Specifying an exact version is not recommended on Microsoft-Hosted agents. Patch versions of Python can be replaced by new ones on Hosted agents without notice, which will cause builds to fail unexpectedly. It is recommended to specify only major or major and minor version (Example: `3` or `3.9`)",
31+
"loc.messages.ExactVersionPyPyNotRecommended": "Specifying an exact version is not recommended on Microsoft-Hosted agents. Patch versions of PyPy can be replaced by new ones on Hosted agents without notice, which will cause builds to fail unexpectedly. It is recommended to specify only major or major and minor version (Example: `3` or `3.9`)",
32+
"loc.messages.PythonVersionRetirement": "Python 3.5 has reached its end of life and will be removed from the ms-hosted images on January, 24. Please update Python version if you use ms-hosted agents. For more information about this - https://github.com/actions/virtual-environments",
33+
"loc.messages.MissingGithubToken": "You should provide GitHub token if you want to download a python release. Otherwise you may hit the GitHub anonymous download limit.",
34+
"loc.messages.DownloadNotFound": "Could not find Python matching spec %s (%s) in the python-versions registry. Beware that only systems listed in the Github Actions python versions manifest (https://github.com/actions/python-versions/blob/main/versions-manifest.json) are fit for downloading python on-flight. Also, proxy is not supported.",
35+
"loc.messages.ManifestDownloadFailed": "Failed to download Python versions manifest from the Github Actions python registry (https://github.com/actions/python-versions). Invalid GitHub token could cause this.",
36+
"loc.messages.DownloadFailed": "Failed to download Python from the Github Actions python registry (https://github.com/actions/python-versions). Error: %s",
37+
"loc.messages.ServiceEndpointNotDefined": "Could not find the GitHub service connection. Make sure the selected service connection still exists.",
38+
"loc.messages.AuthSchemeNotSupported": "The authentication scheme '%s' is not supported for the GitHub service connection."
39+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
registry=https://pkgs.dev.azure.com/mseng/PipelineTools/_packaging/PipelineTools_PublicPackages/npm/registry/
2+
3+
always-auth=true
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import * as assert from 'assert';
2+
import * as os from 'os';
3+
import * as path from 'path';
4+
import * as task from 'azure-pipelines-task-lib/task';
5+
6+
import { MockTestRunner } from 'azure-pipelines-task-lib/mock-test';
7+
8+
function didSetVariable(testRunner: MockTestRunner, variableName: string, variableValue: string): boolean {
9+
return testRunner.stdOutContained(`##vso[task.setvariable variable=${variableName};isOutput=false;issecret=false;]${variableValue}`);
10+
}
11+
12+
function didPrependPath(testRunner: MockTestRunner, toolPath: string): boolean {
13+
return testRunner.stdOutContained(`##vso[task.prependpath]${toolPath}`);
14+
}
15+
16+
describe('UsePythonVersion L0 Suite', function () {
17+
this.timeout(parseInt(process.env.TASK_TEST_TIMEOUT) || 5000);
18+
19+
describe('usepythonversion.ts', function () {
20+
require('./L0_usepythonversion');
21+
});
22+
23+
describe('versionspec.ts', function () {
24+
require('./L0_versionspec');
25+
});
26+
27+
it('succeeds when version is found', async function () {
28+
const testFile = path.join(__dirname, 'L0SucceedsWhenVersionIsFound.js');
29+
const testRunner = new MockTestRunner(testFile);
30+
31+
await testRunner.runAsync();
32+
33+
const pythonDir = path.join('/', 'Python', '3.6.4', 'x64');
34+
const pythonBinDir = task.getPlatform() === task.Platform.Windows
35+
? path.join(pythonDir, 'Scripts')
36+
: path.join(pythonDir, 'bin');
37+
38+
assert(didSetVariable(testRunner, 'pythonLocation', pythonDir));
39+
assert(didPrependPath(testRunner, pythonDir));
40+
assert(didPrependPath(testRunner, pythonBinDir));
41+
assert.strictEqual(testRunner.stderr.length, 0, 'should not have written to stderr');
42+
assert(testRunner.succeeded, 'task should have succeeded');
43+
});
44+
45+
it('downloads python from registry on Windows', async function () {
46+
const testFile = path.join(__dirname, 'L0DownloadsFromRegistryWindows.js');
47+
const testRunner = new MockTestRunner(testFile);
48+
49+
await testRunner.runAsync();
50+
51+
const pythonDir = path.join('C', 'tools', 'Python', '3.10.1', 'x64');
52+
const pythonBinDir = path.join(pythonDir, 'Scripts');
53+
const pythonAppdataDir = path.join('testappdata', 'Python', 'Python310', 'Scripts');
54+
55+
assert(didSetVariable(testRunner, 'pythonLocation', pythonDir));
56+
assert(didPrependPath(testRunner, pythonDir));
57+
assert(didPrependPath(testRunner, pythonBinDir));
58+
assert(didPrependPath(testRunner, pythonAppdataDir));
59+
assert.strictEqual(testRunner.stderr.length, 0, 'should not have written to stderr');
60+
assert(testRunner.succeeded, 'task should have succeeded');
61+
});
62+
63+
it('downloads python from registry on Ubuntu', async function () {
64+
const testFile = path.join(__dirname, 'L0DownloadsFromRegistryUbuntu.js');
65+
const testRunner = new MockTestRunner(testFile);
66+
67+
await testRunner.runAsync();
68+
69+
const pythonDir = path.join('opt', 'hostedtoolcache', 'Python', '3.10.1', 'x64');
70+
const pythonBinDir = path.join(pythonDir, 'bin');
71+
72+
assert(didSetVariable(testRunner, 'pythonLocation', pythonDir));
73+
assert(didPrependPath(testRunner, pythonDir));
74+
assert(didPrependPath(testRunner, pythonBinDir));
75+
assert.strictEqual(testRunner.stderr.length, 0, 'should not have written to stderr');
76+
assert(testRunner.succeeded, 'task should have succeeded');
77+
});
78+
79+
it('downloads unstable python from registry', async function () {
80+
const testFile = path.join(__dirname, 'L0DownloadsUnstable.js');
81+
const testRunner = new MockTestRunner(testFile);
82+
83+
await testRunner.runAsync();
84+
85+
const pythonDir = path.join('opt', 'hostedtoolcache', 'Python', '3.11.1', 'x64');
86+
const pythonBinDir = path.join(pythonDir, 'bin');
87+
88+
assert(didSetVariable(testRunner, 'pythonLocation', pythonDir));
89+
assert(didPrependPath(testRunner, pythonDir));
90+
assert(didPrependPath(testRunner, pythonBinDir));
91+
assert.strictEqual(testRunner.stderr.length, 0, 'should not have written to stderr');
92+
assert(testRunner.succeeded, 'task should have succeeded');
93+
});
94+
95+
it('fails when version is not found', async function () {
96+
const testFile = path.join(__dirname, 'L0FailsWhenVersionIsMissing.js');
97+
const testRunner = new MockTestRunner(testFile);
98+
99+
await testRunner.runAsync();
100+
101+
assert(testRunner.createdErrorIssue('loc_mock_DownloadFailed Error: loc_mock_DownloadNotFound 3.11.x x64'));
102+
103+
const errorMessage = [
104+
'loc_mock_VersionNotFound 3.11.x x64',
105+
'loc_mock_ListAvailableVersions $(Agent.ToolsDirectory)',
106+
'2.6.0 (x86)',
107+
'2.7.13 (x86)',
108+
'2.6.0 (x64)',
109+
'2.7.13 (x64)',
110+
'loc_mock_ToolNotFoundMicrosoftHosted Python https://aka.ms/hosted-agent-software',
111+
'loc_mock_ToolNotFoundSelfHosted Python https://go.microsoft.com/fwlink/?linkid=871498',
112+
].join('\r\n');
113+
114+
assert(testRunner.createdErrorIssue(errorMessage));
115+
assert(testRunner.failed, 'task should have failed');
116+
});
117+
118+
it('selects architecture passed as input', async function () {
119+
const testFile = path.join(__dirname, 'L0SelectsArchitecture.js');
120+
const testRunner = new MockTestRunner(testFile);
121+
122+
await testRunner.runAsync();
123+
124+
assert(didSetVariable(testRunner, 'pythonLocation', 'x86ToolPath'));
125+
assert.strictEqual(testRunner.stderr.length, 0, 'should not have written to stderr');
126+
assert(testRunner.succeeded, 'task should have succeeded');
127+
});
128+
129+
it('finds PyPy2', async function () {
130+
const testFile = path.join(__dirname, 'L0PyPy2.js');
131+
const testRunner = new MockTestRunner(testFile);
132+
133+
await testRunner.runAsync();
134+
135+
const pypyDir = path.join('/', 'PyPy', '2.7.9', 'x64');
136+
const pypyBinDir = path.join(pypyDir, 'bin');
137+
const pythonLocation = task.getPlatform() === task.Platform.Windows
138+
? pypyDir
139+
: pypyBinDir;
140+
141+
assert(didSetVariable(testRunner, 'pythonLocation', pythonLocation));
142+
assert(didPrependPath(testRunner, pypyDir));
143+
assert(didPrependPath(testRunner, pypyBinDir));
144+
assert.strictEqual(testRunner.stderr.length, 0, 'should not have written to stderr');
145+
assert(testRunner.succeeded, 'task should have succeeded');
146+
});
147+
148+
it('finds PyPy3', async function () {
149+
const testFile = path.join(__dirname, 'L0PyPy3.js');
150+
const testRunner = new MockTestRunner(testFile);
151+
152+
await testRunner.runAsync();
153+
154+
const pypyDir = path.join('/', 'PyPy', '3.5.2', 'x64');
155+
const pypyBinDir = path.join(pypyDir, 'bin');
156+
const pythonLocation = task.getPlatform() === task.Platform.Windows
157+
? pypyDir
158+
: pypyBinDir;
159+
160+
assert(didSetVariable(testRunner, 'pythonLocation', pythonLocation));
161+
assert(didPrependPath(testRunner, pypyDir));
162+
assert(didPrependPath(testRunner, pypyBinDir));
163+
assert.strictEqual(testRunner.stderr.length, 0, 'should not have written to stderr');
164+
assert(testRunner.succeeded, 'task should have succeeded');
165+
});
166+
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import * as path from 'path';
2+
import * as fs from 'fs';
3+
4+
import { TaskMockRunner } from 'azure-pipelines-task-lib/mock-run';
5+
6+
const taskPath = path.join(__dirname, '..', 'main.js');
7+
const taskRunner = new TaskMockRunner(taskPath);
8+
9+
const TEST_GITHUB_TOKEN = 'testtoken';
10+
11+
taskRunner.setInput('versionSpec', '3.10.x');
12+
taskRunner.setInput('disableDownloadFromRegistry', 'false');
13+
taskRunner.setInput('addToPath', 'true');
14+
taskRunner.setInput('architecture', 'x64');
15+
taskRunner.setInput('githubToken', TEST_GITHUB_TOKEN);
16+
17+
// `getVariable` is not supported by `TaskLibAnswers`
18+
process.env['AGENT_TOOLSDIRECTORY'] = '$(Agent.ToolsDirectory)';
19+
20+
let pythonWasInstalled = false;
21+
22+
// Mock azure-pipelines-tool-lib
23+
taskRunner.registerMock('azure-pipelines-tool-lib/tool', {
24+
findLocalTool() {
25+
if (!pythonWasInstalled) {
26+
return null;
27+
}
28+
29+
return path.join('opt', 'hostedtoolcache', 'Python', '3.10.1', 'x64');
30+
},
31+
findLocalToolVersions: () => ['2.6.0', '2.7.13'],
32+
downloadTool: () => Promise.resolve('/home/python.tar.gz'),
33+
extractTar: () => Promise.resolve('/home/extracted/python'),
34+
extractZip() {
35+
throw new Error('This should never be called');
36+
},
37+
});
38+
39+
taskRunner.registerMock('os', {
40+
platform() {
41+
return 'linux';
42+
},
43+
arch() {
44+
return 'x64';
45+
},
46+
EOL: '\r\n'
47+
});
48+
49+
// Can't mock process, so have to mock taskutil instead
50+
enum Platform {
51+
Windows,
52+
MacOS,
53+
Linux
54+
}
55+
56+
taskRunner.registerMock('./taskutil', {
57+
Platform,
58+
getPlatform() {
59+
return Platform.Linux;
60+
}
61+
});
62+
63+
const tl = require('azure-pipelines-task-lib/mock-task');
64+
const tlClone = Object.assign({}, tl);
65+
tlClone.exec = function(command, args, options) {
66+
if (command !== 'bash' || args !== './setup.sh') {
67+
throw new Error(`Invalid command and arguments: ${command} ${args}`);
68+
}
69+
70+
if (options.cwd !== '/home/extracted/python') {
71+
throw new Error(`Invalid python installer dir path: ${options.cwd}`);
72+
}
73+
74+
pythonWasInstalled = true;
75+
};
76+
taskRunner.registerMock('azure-pipelines-task-lib/mock-task', tlClone);
77+
78+
// Test manifest contains stable python 3.10.1, so the task should find it
79+
taskRunner.registerMock('typed-rest-client', {
80+
RestClient: class {
81+
get(_url: string) {
82+
return Promise.resolve({
83+
result: JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'versions-manifest.json')).toString())
84+
});
85+
}
86+
}
87+
});
88+
89+
const lsbPath = '/etc/lsb-release';
90+
taskRunner.registerMock('fs', {
91+
...fs,
92+
existsSync(filePath) {
93+
if (filePath !== lsbPath) {
94+
throw new Error(`Tried to check the wrong file for existance: ${filePath}`);
95+
}
96+
97+
return true;
98+
},
99+
100+
readFileSync(filePath) {
101+
if (filePath !== lsbPath) {
102+
throw new Error(`Tried to read the wrong file: ${filePath}`);
103+
}
104+
105+
return 'DISTRIB_RELEASE=18.04';
106+
}
107+
});
108+
109+
taskRunner.run();

0 commit comments

Comments
 (0)