Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions ext/azuredevops/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion ext/azuredevops/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions ext/azuredevops/setupAzd/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
index.js
version.js
node_modules
.taskkey
index.js
Expand Down
114 changes: 92 additions & 22 deletions ext/azuredevops/setupAzd/index.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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
}
Expand All @@ -16,20 +27,53 @@ export async function runMain(): Promise<void> {
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
Expand All @@ -42,27 +86,53 @@ export async function runMain(): Promise<void> {
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)}`)
}
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions ext/azuredevops/setupAzd/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ext/azuredevops/setupAzd/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "setup-azd",
"version": "1.2.0",
"version": "1.2.1",
"description": "",
"main": "index.js",
"scripts": {
Expand Down
5 changes: 3 additions & 2 deletions ext/azuredevops/setupAzd/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@
"version": {
"Major": 1,
"Minor": 2,
"Patch": 0
"Patch": 1
},
"instanceNameFormat": "Installs azd: $(rootFolder)",
"inputs": [
{
"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": {
Expand Down
115 changes: 114 additions & 1 deletion ext/azuredevops/setupAzd/tests/_suite.ts
Original file line number Diff line number Diff line change
@@ -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 () {

Expand Down Expand Up @@ -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);
Expand Down
47 changes: 47 additions & 0 deletions ext/azuredevops/setupAzd/tests/cleanupFailure.ts
Original file line number Diff line number Diff line change
@@ -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();
Loading