diff --git a/ext/azuredevops/CHANGELOG.md b/ext/azuredevops/CHANGELOG.md index 2f867292c29..a5faa81abd5 100644 --- a/ext/azuredevops/CHANGELOG.md +++ b/ext/azuredevops/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## 1.2.1 (2026-08-03) + +- Validates requested versions and runs installer scripts with explicit arguments. + ## 1.2.0 (2026-01-14) - Moving release from manual to pipeline. diff --git a/ext/azuredevops/README.md b/ext/azuredevops/README.md index 0fcc71cfb3a..00582b871ab 100644 --- a/ext/azuredevops/README.md +++ b/ext/azuredevops/README.md @@ -4,7 +4,7 @@ This Azure DevOps task allows you to provision resources and deploy your applica The task installs the Azure Developer CLI on a user-defined Azure Developer CLI version. If the user does not specify a version, latest CLI version is used. Read more about various Azure Developer CLI versions [here](https://github.com/Azure/azure-dev/releases). -- `version` – **Optional** Example: 1.13.0, Default: set to latest azd cli version. +- `version` – **Optional** Use `latest`, `stable`, `daily`, or a semantic version such as `1.13.0` or `1.14.0-beta.1`. Defaults to `latest`. ## Sample pipeline install latest `azd` version diff --git a/ext/azuredevops/setupAzd/.gitignore b/ext/azuredevops/setupAzd/.gitignore index d9222a57c25..02cd7b499f9 100644 --- a/ext/azuredevops/setupAzd/.gitignore +++ b/ext/azuredevops/setupAzd/.gitignore @@ -1,4 +1,5 @@ index.js +version.js node_modules .taskkey index.js diff --git a/ext/azuredevops/setupAzd/index.ts b/ext/azuredevops/setupAzd/index.ts index ebe075d1e83..698c71ee37b 100644 --- a/ext/azuredevops/setupAzd/index.ts +++ b/ext/azuredevops/setupAzd/index.ts @@ -1,13 +1,24 @@ +import { mkdtemp, rm } from 'fs/promises' +import * as os from 'os' +import * as path from 'path' import * as task from 'azure-pipelines-task-lib/task' import * as toolRunner from 'azure-pipelines-task-lib/toolrunner' +import { isValidVersion } from './version' + +function errorMessage(err: unknown): string { + return err instanceof Error ? err.message : String(err) +} + export async function runMain(): Promise { + let tempDirectory: string | undefined + try { task.setTaskVariable('hasRunMain', 'true') - const os = process.platform + const platform = os.platform() const localAppData = process.env.LocalAppData const envPath = process.env.PATH - if (os === 'win32' && !localAppData) { + if (platform === 'win32' && !localAppData) { task.setResult(task.TaskResult.Failed, 'LocalAppData environment variable is not defined.') return } @@ -16,20 +27,53 @@ export async function runMain(): Promise { return } const version = task.getInput('version') || 'latest' + if (!isValidVersion(version)) { + task.setResult( + task.TaskResult.Failed, + 'Version must be latest, stable, daily, or a semantic version such as 1.2.3.', + ) + return + } - console.log(`Installing azd version ${version} on ${os}.`) + console.log(`Installing azd version ${version} on ${platform}.`) + tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'setup-azd-')) - if (os === 'win32') { + if (platform === 'win32') { + const installScriptPath = path.join(tempDirectory, 'install-azd.ps1') const powershellPath = task.which('powershell', true) - const powershell: toolRunner.ToolRunner = task.tool(powershellPath) - const installScript = `$scriptPath = "$($env:TEMP)\\install-azd.ps1"; Invoke-RestMethod 'https://aka.ms/install-azd.ps1' -OutFile $scriptPath; . $scriptPath -Version '${version}' -Verbose:$true; Remove-Item $scriptPath` - powershell.arg('-NoLogo') - powershell.arg('-NoProfile') - powershell.arg('-NonInteractive') - powershell.arg('-Command') - powershell.arg(installScript) - - const installResult = await powershell.exec() + const download: toolRunner.ToolRunner = task.tool(powershellPath) + download.arg('-NoLogo') + download.arg('-NoProfile') + download.arg('-NonInteractive') + download.arg('-Command') + download.arg( + "$ErrorActionPreference = 'Stop'; " + + "Invoke-RestMethod -Uri 'https://aka.ms/install-azd.ps1' -OutFile $env:AZD_INSTALL_SCRIPT", + ) + + const downloadResult = await download.exec({ + env: { + ...process.env, + AZD_INSTALL_SCRIPT: installScriptPath, + }, + ignoreReturnCode: true, + }) + if (downloadResult !== 0) { + task.setResult(task.TaskResult.Failed, `Failed to download the azd installer. Exit code: ${downloadResult}`) + return + } + + const installer: toolRunner.ToolRunner = task.tool(powershellPath) + installer.arg('-NoLogo') + installer.arg('-NoProfile') + installer.arg('-NonInteractive') + installer.arg('-File') + installer.arg(installScriptPath) + installer.arg('-Version') + installer.arg(version) + installer.arg('-Verbose') + + const installResult = await installer.exec({ ignoreReturnCode: true }) if (installResult !== 0) { task.setResult(task.TaskResult.Failed, `Failed to install azd. Exit code: ${installResult}`) return @@ -42,27 +86,53 @@ export async function runMain(): Promise { const azdPath = `${localAppData}\\Programs\\Azure Dev CLI\\azd.exe` const azd: toolRunner.ToolRunner = task.tool(azdPath) azd.arg('version') - const versionResult = await azd.exec() + const versionResult = await azd.exec({ ignoreReturnCode: true }) if (versionResult !== 0) { task.setResult(task.TaskResult.Failed, `azd version check failed. Exit code: ${versionResult}`) return } } else { const bashPath = task.which('bash', true) - const bash: toolRunner.ToolRunner = task.tool(bashPath) - bash.arg('-c') - bash.arg(`curl -fsSL https://aka.ms/install-azd.sh | sudo bash -s -- --version ${version} --verbose`) - - const installResult = await bash.exec() + const curlPath = task.which('curl', true) + const sudoPath = task.which('sudo', true) + const installScriptPath = path.join(tempDirectory, 'install-azd.sh') + + const download: toolRunner.ToolRunner = task.tool(curlPath) + download.arg('-fsSL') + download.arg('https://aka.ms/install-azd.sh') + download.arg('-o') + download.arg(installScriptPath) + const downloadResult = await download.exec({ ignoreReturnCode: true }) + if (downloadResult !== 0) { + task.setResult(task.TaskResult.Failed, `Failed to download the azd installer. Exit code: ${downloadResult}`) + return + } + + const installer: toolRunner.ToolRunner = task.tool(sudoPath) + installer.arg(bashPath) + installer.arg(installScriptPath) + installer.arg('--version') + installer.arg(version) + installer.arg('--verbose') + + const installResult = await installer.exec({ ignoreReturnCode: true }) if (installResult !== 0) { task.setResult(task.TaskResult.Failed, `Failed to install azd. Exit code: ${installResult}`) return } } - + console.log(`Successfully installed azd version ${version}.`) - } catch (err: any) { - task.setResult(task.TaskResult.Failed, err.message) + } catch (err: unknown) { + task.setResult(task.TaskResult.Failed, errorMessage(err)) + } finally { + if (tempDirectory) { + try { + await rm(tempDirectory, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }) + } catch (err: unknown) { + task.warning(`Failed to clean up installer files: ${errorMessage(err)}`) + } + } } } diff --git a/ext/azuredevops/setupAzd/package-lock.json b/ext/azuredevops/setupAzd/package-lock.json index c95c17dcc13..3acd80412c6 100644 --- a/ext/azuredevops/setupAzd/package-lock.json +++ b/ext/azuredevops/setupAzd/package-lock.json @@ -1,12 +1,12 @@ { "name": "setup-azd", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "setup-azd", - "version": "1.2.0", + "version": "1.2.1", "license": "ISC", "dependencies": { "azure-pipelines-task-lib": "^5.276.0" diff --git a/ext/azuredevops/setupAzd/package.json b/ext/azuredevops/setupAzd/package.json index c31d39e0fe7..ffaa9274987 100644 --- a/ext/azuredevops/setupAzd/package.json +++ b/ext/azuredevops/setupAzd/package.json @@ -1,6 +1,6 @@ { "name": "setup-azd", - "version": "1.2.0", + "version": "1.2.1", "description": "", "main": "index.js", "scripts": { diff --git a/ext/azuredevops/setupAzd/task.json b/ext/azuredevops/setupAzd/task.json index 6a8f5c0e6f7..0cbf91b1726 100644 --- a/ext/azuredevops/setupAzd/task.json +++ b/ext/azuredevops/setupAzd/task.json @@ -10,7 +10,7 @@ "version": { "Major": 1, "Minor": 2, - "Patch": 0 + "Patch": 1 }, "instanceNameFormat": "Installs azd: $(rootFolder)", "inputs": [ @@ -18,7 +18,8 @@ "name": "version", "type": "string", "label": "The version of azd to install (default: latest)", - "required": false + "required": false, + "helpMarkDown": "Use `latest`, `stable`, `daily`, or a semantic version such as `1.13.0` or `1.14.0-beta.1`." } ], "execution": { diff --git a/ext/azuredevops/setupAzd/tests/_suite.ts b/ext/azuredevops/setupAzd/tests/_suite.ts index a14a0ae6d97..9fe73fe9ae6 100644 --- a/ext/azuredevops/setupAzd/tests/_suite.ts +++ b/ext/azuredevops/setupAzd/tests/_suite.ts @@ -1,6 +1,7 @@ import * as path from 'path'; import * as assert from 'assert'; import * as ttm from 'azure-pipelines-task-lib/mock-test'; +import { isValidVersion } from '../version'; describe('Setup azd task tests', function () { @@ -62,7 +63,119 @@ describe('Setup azd task tests', function () { tr.runAsync().then(() => { assert.equal(tr.succeeded, false, 'should have failed'); assert.equal(tr.warningIssues.length, 0, 'should have no warnings'); - assert.ok(tr.errorIssues.length > 0, 'should have at least one error'); + assert.ok( + tr.errorIssues.some((issue) => issue.includes('Failed to install azd. Exit code: 1')), + 'should report the installer exit code', + ); + done(); + }).catch((error) => { + done(error); + }); + }); + + it('should reject an invalid version format', function(done: Mocha.Done) { + this.timeout(30000); + + const tp: string = path.join(__dirname, 'invalidVersionFormat.js'); + const tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + + tr.runAsync().then(() => { + assert.equal(tr.succeeded, false, 'should have failed'); + assert.equal(tr.warningIssues.length, 0, 'should have no warnings'); + assert.ok(tr.errorIssues.some((issue) => issue.includes('Version must be')), 'should report an invalid version'); + assert.equal(tr.stdout.indexOf('Installing azd version'), -1, 'should fail before installation'); + done(); + }).catch((error) => { + done(error); + }); + }); + + it('should accept supported version formats', function() { + const versions = [ + 'latest', + 'stable', + 'daily', + '1.0.0', + '1.14.0-beta.1', + '1.14.0-beta.1+build.5', + ]; + + for (const version of versions) { + assert.equal(isValidVersion(version), true, `should accept ${version}`); + } + }); + + it('should reject unsupported version formats', function() { + const versions = [ + '01.2.3', + '1.02.3', + '1.2.03', + '1.2.3-01', + '1.2', + '1.2.3.4', + 'latest ', + 'latest\n', + ]; + + for (const version of versions) { + assert.equal(isValidVersion(version), false, `should reject ${version}`); + } + }); + + it('should warn when installer cleanup fails', function(done: Mocha.Done) { + this.timeout(30000); + + const tp: string = path.join(__dirname, 'cleanupFailure.js'); + const tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + + tr.runAsync().then(() => { + assert.equal(tr.succeeded, true, 'should have succeeded'); + assert.equal(tr.errorIssues.length, 0, 'should have no errors'); + assert.ok( + tr.warningIssues.some((issue) => issue.includes('Failed to clean up installer files')), + 'should report the cleanup warning', + ); + done(); + }).catch((error) => { + done(error); + }); + }); + + it('should preserve installer errors when cleanup also fails', function(done: Mocha.Done) { + this.timeout(30000); + + const tp: string = path.join(__dirname, 'installAndCleanupFailure.js'); + const tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + + tr.runAsync().then(() => { + assert.equal(tr.succeeded, false, 'should have failed'); + assert.ok( + tr.errorIssues.some((issue) => issue.includes('Failed to install azd. Exit code: 1')), + 'should preserve the installer error', + ); + assert.ok( + tr.warningIssues.some((issue) => issue.includes('Failed to clean up installer files')), + 'should report the cleanup warning', + ); + done(); + }).catch((error) => { + done(error); + }); + }); + + it('should report download failures with the exit code', function(done: Mocha.Done) { + this.timeout(30000); + + const tp: string = path.join(__dirname, 'downloadFailure.js'); + const tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + + tr.runAsync().then(() => { + assert.equal(tr.succeeded, false, 'should have failed'); + assert.equal(tr.warningIssues.length, 0, 'should have no warnings'); + assert.ok( + tr.errorIssues.some((issue) => issue.includes('Failed to download the azd installer. Exit code: 1')), + 'should report the download exit code', + ); done(); }).catch((error) => { done(error); diff --git a/ext/azuredevops/setupAzd/tests/cleanupFailure.ts b/ext/azuredevops/setupAzd/tests/cleanupFailure.ts new file mode 100644 index 00000000000..ded4776e454 --- /dev/null +++ b/ext/azuredevops/setupAzd/tests/cleanupFailure.ts @@ -0,0 +1,47 @@ +import ma = require('azure-pipelines-task-lib/mock-answer'); +import tmrm = require('azure-pipelines-task-lib/mock-run'); +import os = require('os'); +import path = require('path'); + +const taskPath = path.join(__dirname, '..', 'index.js'); +const tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath); +const tempDirectory = '/tmp/setup-azd-test'; +const installScriptPath = path.join(tempDirectory, 'install-azd.sh'); + +tmr.registerMock('os', { + ...os, + platform: () => 'linux', +}); +tmr.registerMock('fs/promises', { + mkdtemp: async () => tempDirectory, + rm: async () => { + throw new Error('directory is busy'); + }, +}); +tmr.setInput('version', 'stable'); + +const answers: ma.TaskLibAnswers = { + which: { + 'bash': '/bin/bash', + 'curl': '/usr/bin/curl', + 'sudo': '/usr/bin/sudo', + }, + checkPath: { + '/bin/bash': true, + '/usr/bin/curl': true, + '/usr/bin/sudo': true, + }, + exec: { + [`/usr/bin/curl -fsSL https://aka.ms/install-azd.sh -o ${installScriptPath}`]: { + code: 0, + stdout: 'Downloaded azd installer', + }, + [`/usr/bin/sudo /bin/bash ${installScriptPath} --version stable --verbose`]: { + code: 0, + stdout: 'azd installed successfully', + }, + }, +}; + +tmr.setAnswers(answers); +tmr.run(); diff --git a/ext/azuredevops/setupAzd/tests/downloadFailure.ts b/ext/azuredevops/setupAzd/tests/downloadFailure.ts new file mode 100644 index 00000000000..67a9ba42472 --- /dev/null +++ b/ext/azuredevops/setupAzd/tests/downloadFailure.ts @@ -0,0 +1,41 @@ +import ma = require('azure-pipelines-task-lib/mock-answer'); +import tmrm = require('azure-pipelines-task-lib/mock-run'); +import os = require('os'); +import path = require('path'); + +const taskPath = path.join(__dirname, '..', 'index.js'); +const tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath); +const tempDirectory = '/tmp/setup-azd-test'; +const installScriptPath = path.join(tempDirectory, 'install-azd.sh'); + +tmr.registerMock('os', { + ...os, + platform: () => 'linux', +}); +tmr.registerMock('fs/promises', { + mkdtemp: async () => tempDirectory, + rm: async () => undefined, +}); +tmr.setInput('version', 'latest'); + +const answers: ma.TaskLibAnswers = { + which: { + 'bash': '/bin/bash', + 'curl': '/usr/bin/curl', + 'sudo': '/usr/bin/sudo', + }, + checkPath: { + '/bin/bash': true, + '/usr/bin/curl': true, + '/usr/bin/sudo': true, + }, + exec: { + [`/usr/bin/curl -fsSL https://aka.ms/install-azd.sh -o ${installScriptPath}`]: { + code: 1, + stdout: 'Download failed', + }, + }, +}; + +tmr.setAnswers(answers); +tmr.run(); diff --git a/ext/azuredevops/setupAzd/tests/installAndCleanupFailure.ts b/ext/azuredevops/setupAzd/tests/installAndCleanupFailure.ts new file mode 100644 index 00000000000..3179a7cba37 --- /dev/null +++ b/ext/azuredevops/setupAzd/tests/installAndCleanupFailure.ts @@ -0,0 +1,47 @@ +import ma = require('azure-pipelines-task-lib/mock-answer'); +import tmrm = require('azure-pipelines-task-lib/mock-run'); +import os = require('os'); +import path = require('path'); + +const taskPath = path.join(__dirname, '..', 'index.js'); +const tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath); +const tempDirectory = '/tmp/setup-azd-test'; +const installScriptPath = path.join(tempDirectory, 'install-azd.sh'); + +tmr.registerMock('os', { + ...os, + platform: () => 'linux', +}); +tmr.registerMock('fs/promises', { + mkdtemp: async () => tempDirectory, + rm: async () => { + throw new Error('directory is busy'); + }, +}); +tmr.setInput('version', '1.9999999.0'); + +const answers: ma.TaskLibAnswers = { + which: { + 'bash': '/bin/bash', + 'curl': '/usr/bin/curl', + 'sudo': '/usr/bin/sudo', + }, + checkPath: { + '/bin/bash': true, + '/usr/bin/curl': true, + '/usr/bin/sudo': true, + }, + exec: { + [`/usr/bin/curl -fsSL https://aka.ms/install-azd.sh -o ${installScriptPath}`]: { + code: 0, + stdout: 'Downloaded azd installer', + }, + [`/usr/bin/sudo /bin/bash ${installScriptPath} --version 1.9999999.0 --verbose`]: { + code: 1, + stdout: 'Could not download azd version 1.9999999.0', + }, + }, +}; + +tmr.setAnswers(answers); +tmr.run(); diff --git a/ext/azuredevops/setupAzd/tests/invalidVersion.ts b/ext/azuredevops/setupAzd/tests/invalidVersion.ts index 64e1524236c..766adea5545 100644 --- a/ext/azuredevops/setupAzd/tests/invalidVersion.ts +++ b/ext/azuredevops/setupAzd/tests/invalidVersion.ts @@ -1,35 +1,47 @@ import ma = require('azure-pipelines-task-lib/mock-answer'); import tmrm = require('azure-pipelines-task-lib/mock-run'); +import os = require('os'); import path = require('path'); const taskPath = path.join(__dirname, '..', 'index.js'); const tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath); +const tempDirectory = '/tmp/setup-azd-test'; +const installScriptPath = path.join(tempDirectory, 'install-azd.sh'); + +tmr.registerMock('os', { + ...os, + platform: () => 'linux', +}); +tmr.registerMock('fs/promises', { + mkdtemp: async () => tempDirectory, + rm: async () => undefined, +}); // Set input with an invalid version tmr.setInput('version', '1.9999999.0'); -// Mock answers - simulate failure for invalid version (Windows and Linux/Mac) +// Mock answers - simulate failure for an unavailable version const answers: ma.TaskLibAnswers = { which: { - 'powershell': 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', - 'bash': '/bin/bash' + 'bash': '/bin/bash', + 'curl': '/usr/bin/curl', + 'sudo': '/usr/bin/sudo', }, checkPath: { - 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe': true, - '/bin/bash': true + '/bin/bash': true, + '/usr/bin/curl': true, + '/usr/bin/sudo': true, }, exec: { - // Windows PowerShell install command - fails with invalid version - [`C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoLogo -NoProfile -NonInteractive -Command $scriptPath = "$($env:TEMP)\\install-azd.ps1"; Invoke-RestMethod 'https://aka.ms/install-azd.ps1' -OutFile $scriptPath; . $scriptPath -Version '1.9999999.0' -Verbose:$true; Remove-Item $scriptPath`]: { - code: 1, - stdout: 'Could not download - version 1.9999999.0 not found' + [`/usr/bin/curl -fsSL https://aka.ms/install-azd.sh -o ${installScriptPath}`]: { + code: 0, + stdout: 'Downloaded azd installer', }, - // Linux/Mac bash install command - fails with invalid version - '/bin/bash -c curl -fsSL https://aka.ms/install-azd.sh | sudo bash -s -- --version 1.9999999.0 --verbose': { + [`/usr/bin/sudo /bin/bash ${installScriptPath} --version 1.9999999.0 --verbose`]: { code: 1, - stdout: 'Could not download from https://aka.ms/install-azd.sh - version 1.9999999.0 not found' - } - } + stdout: 'Could not download azd version 1.9999999.0', + }, + }, }; tmr.setAnswers(answers); diff --git a/ext/azuredevops/setupAzd/tests/invalidVersionFormat.ts b/ext/azuredevops/setupAzd/tests/invalidVersionFormat.ts new file mode 100644 index 00000000000..e0e8f8a3622 --- /dev/null +++ b/ext/azuredevops/setupAzd/tests/invalidVersionFormat.ts @@ -0,0 +1,13 @@ +import tmrm = require('azure-pipelines-task-lib/mock-run'); +import os = require('os'); +import path = require('path'); + +const taskPath = path.join(__dirname, '..', 'index.js'); +const tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath); + +tmr.registerMock('os', { + ...os, + platform: () => 'linux', +}); +tmr.setInput('version', "latest'; Write-Output unexpected #"); +tmr.run(); diff --git a/ext/azuredevops/setupAzd/tests/success.ts b/ext/azuredevops/setupAzd/tests/success.ts index 1397131bf0d..d6adf7f6e6f 100644 --- a/ext/azuredevops/setupAzd/tests/success.ts +++ b/ext/azuredevops/setupAzd/tests/success.ts @@ -1,44 +1,47 @@ import ma = require('azure-pipelines-task-lib/mock-answer'); import tmrm = require('azure-pipelines-task-lib/mock-run'); +import os = require('os'); import path = require('path'); const taskPath = path.join(__dirname, '..', 'index.js'); const tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath); +const tempDirectory = '/tmp/setup-azd-test'; +const installScriptPath = path.join(tempDirectory, 'install-azd.sh'); + +tmr.registerMock('os', { + ...os, + platform: () => 'linux', +}); +tmr.registerMock('fs/promises', { + mkdtemp: async () => tempDirectory, + rm: async () => undefined, +}); // Set input for success scenario (empty version = latest) tmr.setInput('version', ''); -// Get the mocked LocalAppData path for Windows -const mockLocalAppData = process.env.LocalAppData || 'C:\\Users\\test\\AppData\\Local'; -const azdExePath = `${mockLocalAppData}\\Programs\\Azure Dev CLI\\azd.exe`; - -// Mock answers for tool lookups and executions (Windows and Linux/Mac) +// Mock answers for tool lookups and executions const answers: ma.TaskLibAnswers = { which: { - 'powershell': 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', - 'bash': '/bin/bash' + 'bash': '/bin/bash', + 'curl': '/usr/bin/curl', + 'sudo': '/usr/bin/sudo', }, checkPath: { - 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe': true, - '/bin/bash': true + '/bin/bash': true, + '/usr/bin/curl': true, + '/usr/bin/sudo': true, }, exec: { - // Windows PowerShell install command - [`C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoLogo -NoProfile -NonInteractive -Command $scriptPath = "$($env:TEMP)\\install-azd.ps1"; Invoke-RestMethod 'https://aka.ms/install-azd.ps1' -OutFile $scriptPath; . $scriptPath -Version 'latest' -Verbose:$true; Remove-Item $scriptPath`]: { + [`/usr/bin/curl -fsSL https://aka.ms/install-azd.sh -o ${installScriptPath}`]: { code: 0, - stdout: 'azd installed successfully' + stdout: 'Downloaded azd installer', }, - // Windows azd version check - [`${azdExePath} version`]: { + [`/usr/bin/sudo /bin/bash ${installScriptPath} --version latest --verbose`]: { code: 0, - stdout: 'azd version 1.0.0' + stdout: 'azd installed successfully', }, - // Linux/Mac bash install command - '/bin/bash -c curl -fsSL https://aka.ms/install-azd.sh | sudo bash -s -- --version latest --verbose': { - code: 0, - stdout: 'azd installed successfully' - } - } + }, }; tmr.setAnswers(answers); diff --git a/ext/azuredevops/setupAzd/tests/successVersion.ts b/ext/azuredevops/setupAzd/tests/successVersion.ts index da7a1fd86c1..75ab954c9ba 100644 --- a/ext/azuredevops/setupAzd/tests/successVersion.ts +++ b/ext/azuredevops/setupAzd/tests/successVersion.ts @@ -1,44 +1,52 @@ import ma = require('azure-pipelines-task-lib/mock-answer'); import tmrm = require('azure-pipelines-task-lib/mock-run'); +import os = require('os'); import path = require('path'); const taskPath = path.join(__dirname, '..', 'index.js'); const tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath); +const tempDirectory = '/tmp/setup-azd-test'; +const installScriptPath = path.join(tempDirectory, 'install-azd.ps1'); + +tmr.registerMock('os', { + ...os, + platform: () => 'win32', +}); +tmr.registerMock('fs/promises', { + mkdtemp: async () => tempDirectory, + rm: async () => undefined, +}); // Set input for success with specific version tmr.setInput('version', '1.0.0'); // Get the mocked LocalAppData path for Windows const mockLocalAppData = process.env.LocalAppData || 'C:\\Users\\test\\AppData\\Local'; +process.env.LocalAppData = mockLocalAppData; const azdExePath = `${mockLocalAppData}\\Programs\\Azure Dev CLI\\azd.exe`; -// Mock answers for tool lookups and executions (Windows and Linux/Mac) +// Mock answers for tool lookups and executions const answers: ma.TaskLibAnswers = { which: { 'powershell': 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', - 'bash': '/bin/bash' }, checkPath: { 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe': true, - '/bin/bash': true }, exec: { - // Windows PowerShell install command - [`C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoLogo -NoProfile -NonInteractive -Command $scriptPath = "$($env:TEMP)\\install-azd.ps1"; Invoke-RestMethod 'https://aka.ms/install-azd.ps1' -OutFile $scriptPath; . $scriptPath -Version '1.0.0' -Verbose:$true; Remove-Item $scriptPath`]: { + [`C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoLogo -NoProfile -NonInteractive -Command $ErrorActionPreference = 'Stop'; Invoke-RestMethod -Uri 'https://aka.ms/install-azd.ps1' -OutFile $env:AZD_INSTALL_SCRIPT`]: { code: 0, - stdout: 'azd version 1.0.0 installed successfully' + stdout: 'Downloaded azd installer', }, - // Windows azd version check - [`${azdExePath} version`]: { + [`C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoLogo -NoProfile -NonInteractive -File ${installScriptPath} -Version 1.0.0 -Verbose`]: { code: 0, - stdout: 'azd version 1.0.0' + stdout: 'azd version 1.0.0 installed successfully', }, - // Linux/Mac bash install command - '/bin/bash -c curl -fsSL https://aka.ms/install-azd.sh | sudo bash -s -- --version 1.0.0 --verbose': { + [`${azdExePath} version`]: { code: 0, - stdout: 'azd version 1.0.0 installed successfully' - } - } + stdout: 'azd version 1.0.0', + }, + }, }; tmr.setAnswers(answers); diff --git a/ext/azuredevops/setupAzd/version.ts b/ext/azuredevops/setupAzd/version.ts new file mode 100644 index 00000000000..cb7937b0a93 --- /dev/null +++ b/ext/azuredevops/setupAzd/version.ts @@ -0,0 +1,12 @@ +const numericIdentifier = '(?:0|[1-9]\\d*)' +const prereleaseIdentifier = `(?:${numericIdentifier}|\\d*[A-Za-z-][0-9A-Za-z-]*)` +const semanticVersion = + `${numericIdentifier}\\.${numericIdentifier}\\.${numericIdentifier}` + + `(?:-${prereleaseIdentifier}(?:\\.${prereleaseIdentifier})*)?` + + '(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?' +// Keep aliases aligned with the versions supported by cli/installer/install-azd.*. +const validVersionPattern = new RegExp(`^(?:latest|stable|daily|${semanticVersion})$`) + +export function isValidVersion(version: string): boolean { + return version.length <= 128 && validVersionPattern.exec(version)?.[0] === version +} diff --git a/ext/azuredevops/vss-extension.json b/ext/azuredevops/vss-extension.json index 58a0d8fccac..f605204558a 100644 --- a/ext/azuredevops/vss-extension.json +++ b/ext/azuredevops/vss-extension.json @@ -2,7 +2,7 @@ "manifestVersion": 1, "id": "azd", "name": "Install azd", - "version": "1.2.0", + "version": "1.2.1", "publisher": "ms-azuretools", "targets": [ {