Compare commits

...

3 Commits

15 changed files with 1304 additions and 91 deletions

View File

@@ -1,18 +1,26 @@
import * as tl from 'azure-pipelines-task-lib/task';
import {
buildOidcUrl,
exchangeOidcForScopedToken,
getServiceConnectionMetadata,
requestOidcToken
} from './oidc';
import { requireVariable } from './devops-helpers';
export const STORAGE_SCOPE = 'https://storage.azure.com/.default';
export async function requestStorageAccessToken(
endpointId: string
): Promise<string> {
const oidcBaseUrl = requireVariable('System.OidcRequestUri');
const systemAccessToken = requireVariable('System.AccessToken');
const oidcBaseUrl = tl.getVariable('System.OidcRequestUri');
const systemAccessToken = tl.getVariable('System.AccessToken');
if (oidcBaseUrl === undefined) {
throw new Error('Missing required pipeline variable: System.OidcRequestUri.');
}
if (systemAccessToken === undefined) {
throw new Error('Missing required pipeline variable: System.AccessToken.');
}
const metadata = getServiceConnectionMetadata(endpointId);

View File

@@ -1,28 +0,0 @@
type TaskLibBridge = {
getInput: (name: string, required?: boolean) => string | undefined;
getVariable: (name: string) => string | undefined;
};
function getTaskLibBridge(): TaskLibBridge {
return require('azure-pipelines-task-lib/task') as TaskLibBridge;
}
export function requireInput(name: string): string {
const taskLib = getTaskLibBridge();
const value = taskLib.getInput(name, true);
if (!value) {
throw new Error(`Task input ${name} is required.`);
}
return value.trim();
}
export function requireVariable(name: string): string {
const taskLib = getTaskLibBridge();
const value = taskLib.getVariable(name);
if (!value) {
throw new Error(`Missing required pipeline variable: ${name}.`);
}
return value.trim();
}

View File

@@ -1,3 +1,2 @@
export * from './devops-helpers';
export * from './oidc';
export * from './blob';

View File

@@ -1,3 +1,5 @@
import * as tl from 'azure-pipelines-task-lib/task';
export type ServiceConnectionMetadata = {
tenantId: string;
clientId: string;
@@ -11,40 +13,25 @@ export type TokenResponse = {
export const CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
type TaskLibEndpointBridge = {
getEndpointAuthorizationParameter: (
endpointId: string,
key: string,
optional: boolean
) => string | undefined;
getEndpointDataParameter: (endpointId: string, key: string, optional: boolean) => string | undefined;
};
type OidcResponse = {
oidcToken?: string;
};
function getTaskLibEndpointBridge(): TaskLibEndpointBridge {
return require('azure-pipelines-task-lib/task') as TaskLibEndpointBridge;
}
export function getServiceConnectionMetadata(endpointId: string): ServiceConnectionMetadata {
const taskLib = getTaskLibEndpointBridge();
const tenantId =
taskLib.getEndpointAuthorizationParameter(endpointId, 'tenantid', true) ||
taskLib.getEndpointDataParameter(endpointId, 'tenantid', true);
tl.getEndpointAuthorizationParameter(endpointId, 'tenantid', true) ||
tl.getEndpointDataParameter(endpointId, 'tenantid', true);
const clientId =
taskLib.getEndpointAuthorizationParameter(endpointId, 'serviceprincipalid', true) ||
taskLib.getEndpointAuthorizationParameter(endpointId, 'clientid', true) ||
taskLib.getEndpointDataParameter(endpointId, 'serviceprincipalid', true);
tl.getEndpointAuthorizationParameter(endpointId, 'serviceprincipalid', true) ||
tl.getEndpointAuthorizationParameter(endpointId, 'clientid', true) ||
tl.getEndpointDataParameter(endpointId, 'serviceprincipalid', true);
if (!tenantId) {
if (tenantId === undefined) {
throw new Error('Could not resolve tenant ID from the selected AzureRM service connection.');
}
if (!clientId) {
if (clientId === undefined) {
throw new Error('Could not resolve client ID from the selected AzureRM service connection.');
}

View File

@@ -4,21 +4,27 @@ import {
buildOidcUrl,
exchangeOidcForScopedToken,
getServiceConnectionMetadata,
requestOidcToken,
requireInput,
requireVariable
requestOidcToken
} from '@skoszewski/ado-sk-toolkit-shared';
const AZDO_APP_SCOPE = '499b84ac-1321-427f-aa17-267ca6975798/.default';
async function run(): Promise<void> {
try {
const endpointId = requireInput('serviceConnectionARM');
const endpointId = tl.getInputRequired('serviceConnectionARM');
const setGitAccessToken = tl.getBoolInput('setGitAccessToken', false);
const printTokenHashes = tl.getBoolInput('printTokenHashes', false);
const oidcBaseUrl = requireVariable('System.OidcRequestUri');
const accessToken = requireVariable('System.AccessToken');
const oidcBaseUrl = tl.getVariable('System.OidcRequestUri');
const accessToken = tl.getVariable('System.AccessToken');
if (oidcBaseUrl === undefined) {
throw new Error('Missing required pipeline variable: System.OidcRequestUri.');
}
if (accessToken === undefined) {
throw new Error('Missing required pipeline variable: System.AccessToken.');
}
console.log('Requesting OIDC token for ARM authentication...');

View File

@@ -1,8 +1,7 @@
import * as tl from 'azure-pipelines-task-lib/task';
import {
buildBlobUrl,
requestStorageAccessToken,
requireInput
requestStorageAccessToken
} from '@skoszewski/ado-sk-toolkit-shared';
async function copyBlob(
@@ -38,12 +37,12 @@ async function copyBlob(
async function run(): Promise<void> {
try {
const endpointId = requireInput('serviceConnectionARM');
const srcStorageAccountName = requireInput('srcStorageAccountName');
const dstStorageAccountName = requireInput('dstStorageAccountName');
const srcContainerName = requireInput('srcContainerName');
const dstContainerNameInput = tl.getInput('dstContainerName', false)?.trim() || '';
const blobName = requireInput('blobName');
const endpointId = tl.getInputRequired('serviceConnectionARM');
const srcStorageAccountName = tl.getInputRequired('srcStorageAccountName');
const dstStorageAccountName = tl.getInputRequired('dstStorageAccountName');
const srcContainerName = tl.getInputRequired('srcContainerName');
const dstContainerNameInput = tl.getInput('dstContainerName', false) || '';
const blobName = tl.getInputRequired('blobName');
console.log('Requesting storage access token from Microsoft Entra ID...');
const accessToken = await requestStorageAccessToken(endpointId);

View File

@@ -3,8 +3,7 @@ import * as path from 'node:path';
import * as tl from 'azure-pipelines-task-lib/task';
import {
buildBlobUrl,
requestStorageAccessToken,
requireInput
requestStorageAccessToken
} from '@skoszewski/ado-sk-toolkit-shared';
async function downloadBlob(blobUrl: string, bearerToken: string): Promise<Buffer> {
@@ -28,11 +27,11 @@ async function downloadBlob(blobUrl: string, bearerToken: string): Promise<Buffe
async function run(): Promise<void> {
try {
const endpointId = requireInput('serviceConnectionARM');
const storageAccountName = requireInput('storageAccountName');
const containerName = requireInput('containerName');
const blobName = requireInput('blobName');
const destinationPath = requireInput('destinationPath');
const endpointId = tl.getInputRequired('serviceConnectionARM');
const storageAccountName = tl.getInputRequired('storageAccountName');
const containerName = tl.getInputRequired('containerName');
const blobName = tl.getInputRequired('blobName');
const destinationPath = tl.getInputRequired('destinationPath');
console.log('Requesting storage access token from Microsoft Entra ID...');
const accessToken = await requestStorageAccessToken(endpointId);

View File

@@ -1,7 +1,6 @@
import * as tl from 'azure-pipelines-task-lib/task';
import {
requestStorageAccessToken,
requireInput
requestStorageAccessToken
} from '@skoszewski/ado-sk-toolkit-shared';
function decodeXmlValue(value: string): string {
@@ -60,11 +59,11 @@ async function listBlobs(listUrl: string, bearerToken: string): Promise<string[]
async function run(): Promise<void> {
try {
const endpointId = requireInput('serviceConnectionARM');
const storageAccountName = requireInput('storageAccountName');
const containerName = requireInput('containerName');
const prefix = tl.getInput('prefix', false)?.trim() || '';
const maxResultsRaw = tl.getInput('maxResults', false)?.trim() || '1000';
const endpointId = tl.getInputRequired('serviceConnectionARM');
const storageAccountName = tl.getInputRequired('storageAccountName');
const containerName = tl.getInputRequired('containerName');
const prefix = tl.getInput('prefix', false) || '';
const maxResultsRaw = tl.getInput('maxResults', false) || '1000';
const maxResults = Number.parseInt(maxResultsRaw, 10);
if (!Number.isInteger(maxResults) || maxResults <= 0) {

View File

@@ -2,8 +2,7 @@ import * as fs from 'node:fs/promises';
import * as tl from 'azure-pipelines-task-lib/task';
import {
buildBlobUrl,
requestStorageAccessToken,
requireInput
requestStorageAccessToken
} from '@skoszewski/ado-sk-toolkit-shared';
async function uploadBlob(
@@ -35,12 +34,12 @@ async function uploadBlob(
async function run(): Promise<void> {
try {
const endpointId = requireInput('serviceConnectionARM');
const storageAccountName = requireInput('storageAccountName');
const containerName = requireInput('containerName');
const blobName = requireInput('blobName');
const sourcePath = requireInput('sourcePath');
const contentType = tl.getInput('contentType', false)?.trim() || 'application/octet-stream';
const endpointId = tl.getInputRequired('serviceConnectionARM');
const storageAccountName = tl.getInputRequired('storageAccountName');
const containerName = tl.getInputRequired('containerName');
const blobName = tl.getInputRequired('blobName');
const sourcePath = tl.getInputRequired('sourcePath');
const contentType = tl.getInput('contentType', false) || 'application/octet-stream';
console.log('Requesting storage access token from Microsoft Entra ID...');
const accessToken = await requestStorageAccessToken(endpointId);

680
task/SetupGitHubRelease/package-lock.json generated Normal file
View File

@@ -0,0 +1,680 @@
{
"name": "setup-github-release-task",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "setup-github-release-task",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"azure-pipelines-task-lib": "^5.2.6"
},
"devDependencies": {
"@types/node": "^20.11.30",
"typescript": "^5.4.5"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.stat": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.walk": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@types/node": {
"version": "20.19.33",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
"integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/adm-zip": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
"license": "MIT",
"engines": {
"node": ">=12.0"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/azure-pipelines-task-lib": {
"version": "5.2.6",
"resolved": "https://registry.npmjs.org/azure-pipelines-task-lib/-/azure-pipelines-task-lib-5.2.6.tgz",
"integrity": "sha512-YWEJFAY+Imk5nWwPd5ao6h/J8BgW2Dtzt+M5QiUWDV5cB10n4zywQ5Dulj2OcO1B9tGfdV5KDKVnQRKfJ72YmA==",
"license": "MIT",
"dependencies": {
"adm-zip": "^0.5.10",
"minimatch": "3.0.5",
"nodejs-file-downloader": "^4.11.1",
"q": "^1.5.1",
"semver": "^5.7.2",
"shelljs": "^0.10.0",
"uuid": "^3.0.1"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.3",
"get-stream": "^6.0.0",
"human-signals": "^2.1.0",
"is-stream": "^2.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^4.0.1",
"onetime": "^5.1.2",
"signal-exit": "^3.0.3",
"strip-final-newline": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2",
"merge2": "^1.3.0",
"micromatch": "^4.0.8"
},
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
"license": "Apache-2.0",
"engines": {
"node": ">=10.17.0"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"license": "MIT"
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/minimatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/nodejs-file-downloader": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/nodejs-file-downloader/-/nodejs-file-downloader-4.13.0.tgz",
"integrity": "sha512-nI2fKnmJWWFZF6SgMPe1iBodKhfpztLKJTtCtNYGhm/9QXmWa/Pk9Sv00qHgzEvNLe1x7hjGDRor7gcm/ChaIQ==",
"license": "ISC",
"dependencies": {
"follow-redirects": "^1.15.6",
"https-proxy-agent": "^5.0.0",
"mime-types": "^2.1.27",
"sanitize-filename": "^1.6.3"
}
},
"node_modules/npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
"license": "MIT",
"dependencies": {
"path-key": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"license": "MIT",
"dependencies": {
"mimic-fn": "^2.1.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/q": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
"integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
"deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)",
"license": "MIT",
"engines": {
"node": ">=0.6.0",
"teleport": ">=0.2.0"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"queue-microtask": "^1.2.2"
}
},
"node_modules/sanitize-filename": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
"integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
"license": "WTFPL OR ISC",
"dependencies": {
"truncate-utf8-bytes": "^1.0.0"
}
},
"node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"license": "ISC",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/shelljs": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.10.0.tgz",
"integrity": "sha512-Jex+xw5Mg2qMZL3qnzXIfaxEtBaC4n7xifqaqtrZDdlheR70OGkydrPJWT0V1cA1k3nanC86x9FwAmQl6w3Klw==",
"license": "BSD-3-Clause",
"dependencies": {
"execa": "^5.1.1",
"fast-glob": "^3.3.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
"node_modules/strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/truncate-utf8-bytes": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
"integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==",
"license": "WTFPL",
"dependencies": {
"utf8-byte-length": "^1.0.1"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/utf8-byte-length": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
"integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==",
"license": "(WTFPL OR MIT)"
},
"node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"license": "MIT",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
}
}
}

View File

@@ -0,0 +1,23 @@
{
"name": "setup-github-release-task",
"version": "1.0.0",
"private": true,
"author": "Slawomir Koszewski",
"license": "MIT",
"description": "Azure DevOps task to download and install the latest release asset from GitHub.",
"main": "dist/index.js",
"scripts": {
"build": "tsc -p tsconfig.json",
"clean": "rm -rf dist"
},
"dependencies": {
"azure-pipelines-task-lib": "^5.2.6"
},
"overrides": {
"minimatch": "3.1.3"
},
"devDependencies": {
"@types/node": "^20.11.30",
"typescript": "^5.4.5"
}
}

View File

@@ -0,0 +1,430 @@
import * as fs from 'node:fs';
import * as fsp from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import * as tl from 'azure-pipelines-task-lib/task';
type ReleaseAsset = {
name: string;
browser_download_url: string;
};
type ReleaseInfo = {
tag_name: string;
assets: ReleaseAsset[];
};
type PlatformInfo = {
system: string;
arch: string;
systemPattern: string;
archPattern: string;
};
type MatchOptions = {
fileName?: string;
fileType?: string;
};
const systemPatterns: Record<string, string> = {
linux: 'linux',
darwin: '(darwin|macos|mac|osx)',
win32: '(windows|win)'
};
const archPatterns: Record<string, string> = {
x64: '(x86_64|x64|amd64)',
arm64: '(aarch64|arm64)'
};
function getPlatformInfo(): PlatformInfo {
const system = os.platform();
const arch = os.arch();
return {
system,
arch,
systemPattern: systemPatterns[system] || system,
archPattern: archPatterns[arch] || arch
};
}
function getExtensionPattern(fileType: string): string {
if (fileType === 'archive') {
return '\\.(zip|tar\\.gz|tar|tgz|7z)';
}
if (fileType === 'package') {
return '\\.(deb|rpm|pkg)';
}
return fileType;
}
function getMatchingAsset(assets: ReleaseAsset[], platform: PlatformInfo, options: MatchOptions): ReleaseAsset {
const fileName = options.fileName;
const extPattern = getExtensionPattern(options.fileType || 'archive');
if (!fileName) {
const pattern = `${platform.systemPattern}[_-]${platform.archPattern}.*${extPattern}$`;
const regex = new RegExp(pattern, 'i');
const matches = assets.filter((asset) => regex.test(asset.name));
if (matches.length === 0) {
throw new Error(`No assets matched the default criteria: ${pattern}`);
}
if (matches.length > 1) {
throw new Error(`Multiple assets matched the default criteria: ${matches.map((asset) => asset.name).join(', ')}`);
}
return matches[0];
}
if (fileName.startsWith('~')) {
let pattern = fileName.substring(1);
const hasSystem = pattern.includes('{{SYSTEM}}');
const hasArch = pattern.includes('{{ARCH}}');
const hasExt = pattern.includes('{{EXT_PATTERN}}');
const hasEnd = pattern.endsWith('$');
if (!hasSystem && !hasArch && !hasExt && !hasEnd) {
pattern += '.*{{SYSTEM}}[_-]{{ARCH}}.*{{EXT_PATTERN}}$';
} else if (hasSystem && hasArch && !hasExt && !hasEnd) {
pattern += '.*{{EXT_PATTERN}}$';
}
const finalPattern = pattern
.replace(/{{SYSTEM}}/g, platform.systemPattern)
.replace(/{{ARCH}}/g, platform.archPattern)
.replace(/{{EXT_PATTERN}}/g, extPattern);
const regex = new RegExp(finalPattern, 'i');
const matches = assets.filter((asset) => regex.test(asset.name));
if (matches.length === 0) {
throw new Error(`No assets matched the regex: ${finalPattern}`);
}
if (matches.length > 1) {
throw new Error(`Multiple assets matched the criteria: ${matches.map((asset) => asset.name).join(', ')}`);
}
return matches[0];
}
const exact = assets.find((asset) => asset.name === fileName);
if (!exact) {
throw new Error(`No asset found matching the exact name: ${fileName}`);
}
return exact;
}
async function fetchLatestRelease(repository: string, token?: string): Promise<ReleaseInfo> {
const url = `https://api.github.com/repos/${repository}/releases/latest`;
const headers: Record<string, string> = {
Accept: 'application/vnd.github.v3+json',
'User-Agent': 'setup-github-release-ado-task'
};
if (token) {
headers.Authorization = `token ${token}`;
}
const response = await fetch(url, { headers });
if (!response.ok) {
const body = await response.text();
throw new Error(`Failed to fetch latest release for ${repository}: ${response.status} ${response.statusText}. ${body}`);
}
return (await response.json()) as ReleaseInfo;
}
async function downloadAsset(url: string, destinationPath: string, token?: string): Promise<void> {
const headers: Record<string, string> = {
'User-Agent': 'setup-github-release-ado-task'
};
if (token) {
headers.Authorization = `token ${token}`;
}
const response = await fetch(url, { headers });
if (!response.ok) {
const body = await response.text();
throw new Error(`Failed to download asset from ${url}: ${response.status} ${response.statusText}. ${body}`);
}
const arrayBuffer = await response.arrayBuffer();
await fsp.writeFile(destinationPath, Buffer.from(arrayBuffer));
}
async function extractAsset(filePath: string, destinationDirectory: string): Promise<void> {
const lowerName = path.basename(filePath).toLowerCase();
await fsp.mkdir(destinationDirectory, { recursive: true });
if (lowerName.endsWith('.tar.gz') || lowerName.endsWith('.tgz') || lowerName.endsWith('.tar')) {
const result = spawnSync('tar', ['-xf', filePath, '-C', destinationDirectory]);
if (result.status !== 0) {
throw new Error(`tar failed with status ${result.status}: ${result.stderr.toString()}`);
}
return;
}
if (lowerName.endsWith('.zip')) {
if (process.platform === 'win32') {
const tarResult = spawnSync('tar', ['-xf', filePath, '-C', destinationDirectory]);
if (tarResult.status === 0) {
return;
}
const escapedFilePath = filePath.replace(/'/g, "''");
const escapedDestinationDirectory = destinationDirectory.replace(/'/g, "''");
const command = `Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('${escapedFilePath}', '${escapedDestinationDirectory}')`;
for (const shell of ['pwsh', 'powershell']) {
const result = spawnSync(shell, ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', command]);
if (result.status === 0) {
return;
}
}
throw new Error('ZIP extraction failed on Windows.');
}
const unzipResult = spawnSync('unzip', ['-q', filePath, '-d', destinationDirectory]);
if (unzipResult.status !== 0) {
throw new Error(`unzip failed with status ${unzipResult.status}: ${unzipResult.stderr.toString()}`);
}
return;
}
if (lowerName.endsWith('.7z')) {
const sevenZipResult = spawnSync('7z', ['x', filePath, `-o${destinationDirectory}`, '-y']);
if (sevenZipResult.status !== 0) {
throw new Error(`7z failed with status ${sevenZipResult.status}: ${sevenZipResult.stderr.toString()}`);
}
return;
}
if (lowerName.endsWith('.pkg') || lowerName.endsWith('.xar')) {
const xarResult = spawnSync('xar', ['-xf', filePath], { cwd: destinationDirectory });
if (xarResult.status !== 0) {
throw new Error(`xar failed with status ${xarResult.status}: ${xarResult.stderr.toString()}`);
}
return;
}
const destinationPath = path.join(destinationDirectory, path.basename(filePath));
await fsp.copyFile(filePath, destinationPath);
}
function findBinary(directory: string, pattern: string | RegExp, debug: boolean): string | undefined {
const items = fs.readdirSync(directory);
if (debug) {
tl.debug(`Searching for binary in ${directory}`);
for (const item of items) {
tl.debug(`- ${item}`);
}
}
for (const item of items) {
const fullPath = path.join(directory, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
const nested = findBinary(fullPath, pattern, debug);
if (nested) {
return nested;
}
continue;
}
let match = false;
if (pattern instanceof RegExp) {
match = pattern.test(item);
} else {
match = item === pattern;
if (!match && process.platform === 'win32' && !pattern.toLowerCase().endsWith('.exe')) {
match = item.toLowerCase() === `${pattern.toLowerCase()}.exe`;
}
}
if (match) {
return fullPath;
}
}
return undefined;
}
async function copyDirectory(sourceDirectory: string, destinationDirectory: string): Promise<void> {
await fsp.mkdir(destinationDirectory, { recursive: true });
const entries = await fsp.readdir(sourceDirectory, { withFileTypes: true });
for (const entry of entries) {
const sourcePath = path.join(sourceDirectory, entry.name);
const destinationPath = path.join(destinationDirectory, entry.name);
if (entry.isDirectory()) {
await copyDirectory(sourcePath, destinationPath);
} else if (entry.isSymbolicLink()) {
const linkTarget = await fsp.readlink(sourcePath);
await fsp.symlink(linkTarget, destinationPath);
} else {
await fsp.copyFile(sourcePath, destinationPath);
}
}
}
function getToolsRoot(): string {
const toolsDirectory = tl.getVariable('Agent.ToolsDirectory');
if (toolsDirectory !== undefined) {
return toolsDirectory;
}
return path.join(os.homedir(), '.ado-sk-tools');
}
async function findAnyCachedVersion(toolName: string, arch: string): Promise<{ version: string; toolDirectory: string } | undefined> {
const archRoot = path.join(getToolsRoot(), toolName);
if (!fs.existsSync(archRoot)) {
return undefined;
}
const entries = await fsp.readdir(archRoot, { withFileTypes: true });
const versions = entries
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort();
for (let index = versions.length - 1; index >= 0; index -= 1) {
const version = versions[index];
const candidate = path.join(archRoot, version, arch);
if (fs.existsSync(candidate)) {
return {
version,
toolDirectory: candidate
};
}
}
return undefined;
}
function getSpecificCacheDirectory(toolName: string, version: string, arch: string): string {
return path.join(getToolsRoot(), toolName, version, arch);
}
async function cacheTool(sourceDirectory: string, toolName: string, version: string, arch: string): Promise<string> {
const destinationDirectory = getSpecificCacheDirectory(toolName, version, arch);
await fsp.rm(destinationDirectory, { recursive: true, force: true });
await copyDirectory(sourceDirectory, destinationDirectory);
return destinationDirectory;
}
function setExecutable(filePath: string): void {
if (process.platform !== 'win32') {
fs.chmodSync(filePath, 0o755);
}
}
async function run(): Promise<void> {
try {
const repository = tl.getInputRequired('repository');
const fileNameInput = tl.getInput('fileName', false) || '';
const binaryInput = tl.getInput('binaryName', false) || '';
const fileType = tl.getInput('fileType', false) || 'archive';
const updateCache = (tl.getInput('updateCache', false) || 'false').toLowerCase();
const debug = tl.getBoolInput('debug', false);
const token = tl.getInput('token', false) || process.env.GITHUB_TOKEN;
if (!/^[^/\s]+\/[^/\s]+$/.test(repository)) {
throw new Error('Input repository must be in owner/repo format.');
}
if (!['false', 'true', 'always'].includes(updateCache)) {
throw new Error('Input updateCache must be one of: false, true, always.');
}
const platformInfo = getPlatformInfo();
const toolName = repository.split('/').pop() || repository;
if (updateCache === 'false') {
const cached = await findAnyCachedVersion(toolName, platformInfo.arch);
if (cached) {
tl.debug(`Using cached ${toolName} version ${cached.version}`);
tl.prependPath(cached.toolDirectory);
tl.setResult(tl.TaskResult.Succeeded, `Using cached ${toolName} version ${cached.version}.`);
return;
}
}
tl.debug(`Fetching latest release for ${repository}`);
const release = await fetchLatestRelease(repository, token);
const asset = getMatchingAsset(release.assets, platformInfo, {
fileName: fileNameInput,
fileType
});
const version = release.tag_name.replace(/^v/, '');
const binaryName = binaryInput || toolName;
if (updateCache !== 'always') {
const cachedDirectory = getSpecificCacheDirectory(toolName, version, platformInfo.arch);
if (fs.existsSync(cachedDirectory)) {
tl.debug(`Using cached ${toolName} version ${version}`);
tl.prependPath(cachedDirectory);
tl.setResult(tl.TaskResult.Succeeded, `Using cached ${toolName} version ${version}.`);
return;
}
}
const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'setup-github-release-'));
const downloadPath = path.join(tempRoot, asset.name);
tl.debug(`Downloading asset ${asset.name}`);
await downloadAsset(asset.browser_download_url, downloadPath, token);
let extractionRoot = path.join(tempRoot, 'extract');
const lowerName = asset.name.toLowerCase();
if (
/\.(tar\.gz|tar|tgz)$/i.test(lowerName) ||
/\.zip$/i.test(lowerName) ||
/\.7z$/i.test(lowerName) ||
/\.(xar|pkg)$/i.test(lowerName)
) {
await extractAsset(downloadPath, extractionRoot);
} else {
extractionRoot = path.join(tempRoot, 'bin');
await fsp.mkdir(extractionRoot, { recursive: true });
const destinationPath = path.join(extractionRoot, asset.name);
await fsp.rename(downloadPath, destinationPath);
setExecutable(destinationPath);
}
const binaryPattern = binaryName.startsWith('~')
? new RegExp(binaryName.substring(1), 'i')
: binaryName;
const binaryPath = findBinary(extractionRoot, binaryPattern, debug);
if (!binaryPath) {
throw new Error(`Could not find binary ${binaryName} in extracted asset.`);
}
setExecutable(binaryPath);
const binaryDirectory = path.dirname(binaryPath);
const cachedDirectory = await cacheTool(binaryDirectory, toolName, version, platformInfo.arch);
tl.prependPath(cachedDirectory);
tl.setResult(tl.TaskResult.Succeeded, `Installed ${toolName} ${version} from ${repository}.`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
tl.setResult(tl.TaskResult.Failed, message);
}
}
void run();

View File

@@ -0,0 +1,85 @@
{
"$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json",
"id": "950311ab-f037-4f4f-a875-8a4251e2dbd5",
"name": "SetupGitHubRelease",
"friendlyName": "Setup GitHub Release",
"description": "Downloads and installs a binary from the latest GitHub release and adds it to PATH.",
"helpMarkDown": "Matches release assets by platform and architecture, downloads, extracts, locates the binary, caches it, and prepends its directory to PATH.",
"category": "Utility",
"author": "Slawomir Koszewski",
"version": {
"Major": 1,
"Minor": 0,
"Patch": 0
},
"instanceNameFormat": "Setup GitHub release from $(repository)",
"inputs": [
{
"name": "repository",
"type": "string",
"label": "Repository",
"defaultValue": "",
"required": true,
"helpMarkDown": "GitHub repository in owner/repo format."
},
{
"name": "fileName",
"type": "string",
"label": "File Name",
"defaultValue": "",
"required": false,
"helpMarkDown": "Asset name or regex prefixed with ~."
},
{
"name": "binaryName",
"type": "string",
"label": "Binary Name",
"defaultValue": "",
"required": false,
"helpMarkDown": "Binary name or regex prefixed with ~. Defaults to repository name."
},
{
"name": "fileType",
"type": "string",
"label": "File Type",
"defaultValue": "archive",
"required": false,
"helpMarkDown": "archive, package, or custom regex pattern for file extension matching."
},
{
"name": "updateCache",
"type": "pickList",
"label": "Update Cache",
"defaultValue": "false",
"required": false,
"helpMarkDown": "false: use any cached version. true: use current latest release and cache if needed. always: always download latest and refresh cache.",
"options": {
"false": "false",
"true": "true",
"always": "always"
}
},
{
"name": "debug",
"type": "boolean",
"label": "Debug",
"defaultValue": "false",
"required": false,
"helpMarkDown": "Logs discovered files while searching for the binary."
},
{
"name": "token",
"type": "string",
"label": "GitHub Token",
"defaultValue": "",
"required": false,
"helpMarkDown": "Optional GitHub token for private repos or higher API limits. If empty, uses GITHUB_TOKEN if available."
}
],
"execution": {
"Node20_1": {
"target": "dist/SetupGitHubRelease/src/index.js"
}
},
"minimumAgentVersion": "3.225.0"
}

View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"outDir": "dist",
"rootDir": ".."
},
"include": ["src/**/*.ts"]
}

View File

@@ -2,7 +2,7 @@
"manifestVersion": 1,
"id": "sk-azure-devops-toolkit",
"name": "SK Azure DevOps Toolkit",
"version": "1.0.5",
"version": "1.1.0",
"publisher": "skoszewski-lab",
"targets": [
{
@@ -38,6 +38,9 @@
{
"path": "task/PutBlob"
},
{
"path": "task/SetupGitHubRelease"
},
{
"path": "images",
"addressable": true
@@ -97,6 +100,16 @@
"properties": {
"name": "task/PutBlob"
}
},
{
"id": "289ace53-1fe2-45c7-abf2-a8e8fb57552d",
"type": "ms.vss-distributed-task.task",
"targets": [
"ms.vss-distributed-task.tasks"
],
"properties": {
"name": "task/SetupGitHubRelease"
}
}
]
}