diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9f88003e..e16e7fbf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,9 +21,9 @@ on: required: true env: - PACKAGE_VERSION: 5.2.${{ github.run_number }} - OCTOPUS_CLI_SERVER: ${{ secrets.OCTOPUS_URL }} - OCTOPUS_CLI_API_KEY: ${{ secrets.INTEGRATIONS_API_KEY }} + PACKAGE_VERSION: 6.0.${{ github.run_number }} + OCTOPUS_URL: ${{ secrets.OCTOPUS_URL }} + OCTOPUS_API_KEY: ${{ secrets.INTEGRATIONS_API_KEY }} jobs: build: @@ -32,9 +32,9 @@ jobs: outputs: package_version: ${{ steps.build.outputs.package_version }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: "16" cache: "npm" @@ -45,7 +45,7 @@ jobs: npm ci npm run build -- --extensionVersion $PACKAGE_VERSION echo "::set-output name=package_version::$PACKAGE_VERSION" - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: dist path: dist/ @@ -62,9 +62,9 @@ jobs: matrix: os: [windows-2022, ubuntu-20.04, macos-11] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: "16" cache: "npm" @@ -88,21 +88,21 @@ jobs: if: github.event_name == 'release' || (github.actor != 'dependabot[bot]' && github.actor != 'dependabot-preview[bot]') steps: - uses: actions/checkout@v3 - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v3 with: name: dist path: dist - name: Use Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: "16" cache: "npm" - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v3 with: - go-version: "^1.17.7" + go-version: "^1.19.5" - name: Embed octo portable run: | @@ -120,13 +120,8 @@ jobs: tar -czf OctoTFS.vsix.${{ needs.build.outputs.package_version }}.tar.gz ./Artifacts/**/*.vsix tar -czf OctoTFS.publish.${{ needs.build.outputs.package_version }}.tar.gz ./publish.ps1 ./dist/extension-manifest*.json - - name: Install Octopus CLI 🐙 - uses: OctopusDeploy/install-octopus-cli-action@v1.2.0 - with: - version: "*" - - name: Push Package 🐙 - uses: OctopusDeploy/push-package-action@v1.1.2 + uses: OctopusDeploy/push-package-action@v3 with: space: "Integrations" packages: | @@ -143,7 +138,7 @@ jobs: echo "::set-output name=release-note-file::$OUTPUT_FILE" - name: Create a release in Octopus Deploy 🐙 - uses: OctopusDeploy/create-release-action@v1.1.1 + uses: OctopusDeploy/create-release-action@v3 with: space: "Integrations" project: "Azure DevOps Extension" diff --git a/source/img/octopus_installer.png b/source/img/octopus_installer.png index d54551d7..37dd16ae 100644 Binary files a/source/img/octopus_installer.png and b/source/img/octopus_installer.png differ diff --git a/source/tasks/OctoInstaller/OctoInstallerV5/downloadVersion.ts b/source/tasks/OctoInstaller/OctoInstallerV5/downloadEndpointRetriever.ts similarity index 93% rename from source/tasks/OctoInstaller/OctoInstallerV5/downloadVersion.ts rename to source/tasks/OctoInstaller/OctoInstallerV5/downloadEndpointRetriever.ts index 65678edb..758a8f7f 100644 --- a/source/tasks/OctoInstaller/OctoInstallerV5/downloadVersion.ts +++ b/source/tasks/OctoInstaller/OctoInstallerV5/downloadEndpointRetriever.ts @@ -36,7 +36,7 @@ export interface Endpoint { export class DownloadEndpointRetriever { private osPlat: string = os.platform(); - constructor(readonly octopusUrl: string) {} + constructor(readonly octopurlsUrl: string) {} public async getEndpoint(versionSpec: string): Promise { const octopurls = this.restClient(); @@ -48,7 +48,7 @@ export class DownloadEndpointRetriever { const version = new OctopusCLIVersionFetcher(versionsResponse.result.versions).getVersion(versionSpec); - tasks.debug(`Attempting to contact ${this.octopusUrl} to find Octopus CLI tool version ${version}`); + tasks.debug(`Attempting to contact ${this.octopurlsUrl} to find Octopus CLI tool version ${version}`); const response = await octopurls.get("LatestTools"); @@ -79,7 +79,7 @@ export class DownloadEndpointRetriever { } private restClient() { - const proxyConfiguration = tasks.getHttpProxyConfiguration(this.octopusUrl); + const proxyConfiguration = tasks.getHttpProxyConfiguration(this.octopurlsUrl); let proxySettings: IProxyConfiguration | undefined = undefined; if (proxyConfiguration) { @@ -94,7 +94,7 @@ export class DownloadEndpointRetriever { }; } - return new TypedRestClient.RestClient("OctoTFS/Tasks", this.octopusUrl, undefined, { proxy: proxySettings }); + return new TypedRestClient.RestClient("OctoTFS/Tasks", this.octopurlsUrl, undefined, { proxy: proxySettings }); } private applyTemplate(dictionary: Dictionary, template: string) { diff --git a/source/tasks/OctoInstaller/OctoInstallerV5/installer.ts b/source/tasks/OctoInstaller/OctoInstallerV5/installer.ts index e0746a2b..2933f147 100644 --- a/source/tasks/OctoInstaller/OctoInstallerV5/installer.ts +++ b/source/tasks/OctoInstaller/OctoInstallerV5/installer.ts @@ -3,19 +3,19 @@ import * as tasks from "azure-pipelines-task-lib"; import os from "os"; import path from "path"; import { executeWithSetResult } from "../../Utils/octopusTasks"; -import { DownloadEndpointRetriever, Endpoint } from "./downloadVersion"; +import { DownloadEndpointRetriever, Endpoint } from "./downloadEndpointRetriever"; const TOOL_NAME = "octo"; const osPlat: string = os.platform(); export class Installer { - constructor(readonly octopusUrl: string) {} + constructor(readonly octopurlsUrl: string) {} public async run(versionSpec: string) { await executeWithSetResult( async () => { - const endpoint = await new DownloadEndpointRetriever(this.octopusUrl).getEndpoint(versionSpec); + const endpoint = await new DownloadEndpointRetriever(this.octopurlsUrl).getEndpoint(versionSpec); let toolPath = tools.findLocalTool(TOOL_NAME, endpoint.version); if (!toolPath) { diff --git a/source/tasks/OctoInstaller/OctoInstallerV5/task.json b/source/tasks/OctoInstaller/OctoInstallerV5/task.json index 0637b40a..f84501ce 100644 --- a/source/tasks/OctoInstaller/OctoInstallerV5/task.json +++ b/source/tasks/OctoInstaller/OctoInstallerV5/task.json @@ -2,8 +2,8 @@ "id": "57342b23-3a76-490a-8e78-25d4ade2f2e3", "name": "OctoInstaller", "friendlyName": "Octopus CLI Installer", - "description": "Install a specific version of the Octopus CLI", - "helpMarkDown": "Install a specific version of the Octopus CLI", + "description": "Install a specific version of the Octopus CLI (C#)", + "helpMarkDown": "Install a specific version of the Octopus CLI (C#)", "category": "Tool", "runsOn": [ "Agent", diff --git a/source/tasks/OctoInstaller/OctoInstallerV6/downloadEndpointRetriever.test.ts b/source/tasks/OctoInstaller/OctoInstallerV6/downloadEndpointRetriever.test.ts new file mode 100644 index 00000000..1e3ecad1 --- /dev/null +++ b/source/tasks/OctoInstaller/OctoInstallerV6/downloadEndpointRetriever.test.ts @@ -0,0 +1,124 @@ +import { DownloadEndpointRetriever } from "./downloadEndpointRetriever"; +import { mkdtemp, rm } from "fs/promises"; +import * as path from "path"; +import os from "os"; +import express from "express"; +import { Server } from "http"; +import { AddressInfo } from "net"; +import { Logger } from "@octopusdeploy/api-client"; + +describe("OctopusInstaller", () => { + let tempOutDir: string; + let releasesUrl: string; + let server: Server; + + const msgs: string[] = []; + const logger: Logger = { + debug: (message) => { + msgs.push(message); + }, + info: (message) => { + msgs.push(message); + }, + warn: (message) => { + msgs.push(message); + }, + error: (message, err) => { + if (err !== undefined) { + msgs.push(err.message); + } else { + msgs.push(message); + } + }, + }; + + jest.setTimeout(100000); + + beforeEach(async () => { + tempOutDir = await mkdtemp(path.join(os.tmpdir(), "octopus_")); + process.env["AGENT_TOOLSDIRECTORY"] = tempOutDir; + process.env["AGENT_TEMPDIRECTORY"] = tempOutDir; + + const app = express(); + + app.get("/repos/OctopusDeploy/cli/releases", (_, res) => { + const latestToolsPayload = `[ + { + "tag_name": "v7.4.1", + "assets": [ + { + "name": "octopus_7.4.1_windows_amd64.zip", + "browser_download_url": "http://localhost:${address.port}/OctopusDeploy/cli/releases/download/v7.4.1/octopus_7.4.1_windows_amd64.zip" + } + ] + }, + { + "tag_name": "v8.0.0", + "assets": [ + { + "name": "octopus_8.0.0_windows_amd64.zip", + "browser_download_url": "http://localhost:${address.port}/OctopusDeploy/cli/releases/download/v8.0.0/octopus_8.0.0_windows_amd64.zip" + } + ] + }, + { + "tag_name": "v8.2.0", + "assets": [ + { + "name": "octopus_8.2.0_windows_amd64.zip", + "browser_download_url": "http://localhost:${address.port}/OctopusDeploy/cli/releases/download/v8.2.0/octopus_8.2.0_windows_amd64.zip" + } + ] + } + ]`; + + res.send(latestToolsPayload); + }); + + app.get("/OctopusDeploy/cli/releases/download/v7.4.1/octopus_7.4.1_windows_amd64.zip", (_, res) => { + res.statusCode = 200; + }); + + app.get("/OctopusDeploy/cli/releases/download/v8.0.0/octopus_8.0.0_windows_amd64.zip", (_, res) => { + res.statusCode = 200; + }); + + app.get("/OctopusDeploy/cli/releases/download/v8.2.0/octopus_8.2.0_windows_amd64.zip", (_, res) => { + res.statusCode = 200; + }); + + server = await new Promise((resolve) => { + const r = app.listen(() => { + resolve(r); + }); + }); + + const address = server.address() as AddressInfo; + releasesUrl = `http://localhost:${address.port}/repos/OctopusDeploy/cli/releases`; + }); + + afterEach(async () => { + await new Promise((resolve) => { + server.close(() => { + resolve(); + }); + }); + + await rm(tempOutDir, { recursive: true }); + }); + + test("Installs specific version", async () => { + const result = await new DownloadEndpointRetriever(releasesUrl, "win32", "amd64", logger).getEndpoint("8.0.0"); + expect(result.version).toBe("8.0.0"); + }); + + test("Installs wildcard version", async () => { + const result = await new DownloadEndpointRetriever(releasesUrl, "win32", "amd64", logger).getEndpoint("7.*"); + expect(result.version).toBe("7.4.1"); + }); + + test("Installs latest of latest", async () => { + const result = await new DownloadEndpointRetriever(releasesUrl, "win32", "amd64", logger).getEndpoint("*"); + expect(result.version).toBe("8.2.0"); + }); +}); diff --git a/source/tasks/OctoInstaller/OctoInstallerV6/downloadEndpointRetriever.ts b/source/tasks/OctoInstaller/OctoInstallerV6/downloadEndpointRetriever.ts new file mode 100644 index 00000000..a7b5515e --- /dev/null +++ b/source/tasks/OctoInstaller/OctoInstallerV6/downloadEndpointRetriever.ts @@ -0,0 +1,152 @@ +import * as tasks from "azure-pipelines-task-lib/task"; +import * as TypedRestClient from "typed-rest-client"; +import { IProxyConfiguration } from "typed-rest-client/Interfaces"; +import { OctopusCLIVersionResolver } from "./octopusCLIVersionResolver"; +import { Logger } from "@octopusdeploy/api-client"; + +const downloadsRegEx = + /^.*_(?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)_(?linux|macOS|windows)_(?arm64|amd64).(?tar.gz|zip)$/i; + +type DownloadOption = { + version: string; + location: string; + extension: string; + platform?: string; + architecture?: string; +}; + +export interface Endpoint { + downloadUrl: string; + version: string; + architecture: string; +} + +interface VersionsResponse { + versions: string[]; + downloads: DownloadOption[]; +} + +interface GitHubRelease { + tag_name: string; + assets: GitHubReleaseAsset[]; +} + +interface GitHubReleaseAsset { + version: string; + name: string; + browser_download_url: string; +} + +export class DownloadEndpointRetriever { + constructor(readonly releasesUrl: string, readonly osPlatform: string, readonly osArch: string, readonly logger: Logger) {} + + public async getEndpoint(versionSpec: string): Promise { + this.logger.debug?.(`Attempting to contact ${this.releasesUrl} to find Octopus CLI tool version ${versionSpec}`); + + const versionsResponse: VersionsResponse | null = await this.getVersions(); + if (versionsResponse === null) { + throw Error(`Unable to get versions...`); + } + + const version = new OctopusCLIVersionResolver(versionsResponse.versions).getVersion(versionSpec); + if (version === null) { + throw Error(`The version specified (${version}) is not available to download.`); + } + + this.logger.debug?.(`Attempting to find Octopus CLI version ${version}`); + + let platform = "linux"; + switch (this.osPlatform) { + case "darwin": + platform = "macOS"; + break; + case "win32": + platform = "windows"; + break; + } + + let arch = "amd64"; + switch (this.osArch) { + case "arm": + case "arm64": + arch = "arm64"; + break; + } + + this.logger.debug?.(`Attempting download for platform '${platform}' and architecture ${this.osArch}`); + + let downloadUrl: string | undefined; + + for (const download of versionsResponse.downloads) { + if (download.version === version && download.platform === platform && download.architecture === arch) { + downloadUrl = download.location; + } + } + + if (downloadUrl === undefined || downloadUrl === null) { + throw Error(`Failed to resolve endpoint URL to download: ${downloadUrl}`); + } + + this.logger.debug?.(`Checking status of download url '${downloadUrl}'`); + + const http = this.restClient(); + const statusCode = (await http.client.head(downloadUrl)).message.statusCode; + if (statusCode !== 200) { + throw Error(`Octopus CLI version not found: ${version}`); + } + + this.logger.info?.(`✓ Octopus CLI version found: ${version}`); + return { downloadUrl, version, architecture: arch }; + } + + async getVersions(): Promise { + const githubReleasesClient = this.restClient(); + + const releasesResponse = (await githubReleasesClient.get(this.releasesUrl)).result; + if (releasesResponse === null) { + return null; + } + + const ext: string = this.osPlatform === "win32" ? "zip" : "tar.gz"; + + const downloads = releasesResponse.flatMap((v) => + v.assets + .filter((a) => downloadsRegEx.test(a.name)) + .map((a) => { + const matches = downloadsRegEx.exec(a.name); + + return { + version: matches?.groups?.version || v.tag_name.slice(1), + location: a.browser_download_url, + extension: matches?.groups?.extension || `.${ext}`, + platform: matches?.groups?.platform || undefined, + architecture: matches?.groups?.architecture || undefined, + }; + }) + ); + const versions = downloads.map((d) => d.version); + return { + versions, + downloads, + }; + } + + private restClient() { + const proxyConfiguration = tasks.getHttpProxyConfiguration(this.releasesUrl); + let proxySettings: IProxyConfiguration | undefined = undefined; + + if (proxyConfiguration) { + this.logger.debug?.( + "Using agent configured proxy. If this command should not be sent via the agent's proxy, you might need to add or modify the agent's .proxybypass file. See https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/proxy#specify-proxy-bypass-urls." + ); + proxySettings = { + proxyUrl: proxyConfiguration.proxyUrl, + proxyUsername: proxyConfiguration.proxyUsername, + proxyPassword: proxyConfiguration.proxyPassword, + proxyBypassHosts: proxyConfiguration.proxyBypassHosts, + }; + } + + return new TypedRestClient.RestClient("OctoTFS/Tasks", this.releasesUrl, undefined, { proxy: proxySettings }); + } +} diff --git a/source/tasks/OctoInstaller/OctoInstallerV6/icon.png b/source/tasks/OctoInstaller/OctoInstallerV6/icon.png new file mode 100644 index 00000000..37dd16ae Binary files /dev/null and b/source/tasks/OctoInstaller/OctoInstallerV6/icon.png differ diff --git a/source/tasks/OctoInstaller/OctoInstallerV6/icon.svg b/source/tasks/OctoInstaller/OctoInstallerV6/icon.svg new file mode 100644 index 00000000..b144a032 --- /dev/null +++ b/source/tasks/OctoInstaller/OctoInstallerV6/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/source/tasks/OctoInstaller/OctoInstallerV6/index.ts b/source/tasks/OctoInstaller/OctoInstallerV6/index.ts new file mode 100644 index 00000000..0027c8e4 --- /dev/null +++ b/source/tasks/OctoInstaller/OctoInstallerV6/index.ts @@ -0,0 +1,35 @@ +import * as tasks from "azure-pipelines-task-lib/task"; +import { Logger } from "@octopusdeploy/api-client"; +import { Installer } from "./installer"; +import os from "os"; + +async function run() { + try { + const version = tasks.getInput("octopusVersion", true) || ""; + + const logger: Logger = { + debug: (message) => { + tasks.debug(message); + }, + info: (message) => console.log(message), + warn: (message) => tasks.warning(message), + error: (message, err) => { + if (err !== undefined) { + tasks.error(err.message); + } else { + tasks.error(message); + } + }, + }; + + new Installer("https://api.github.com/repos/OctopusDeploy/cli/releases", os.platform(), os.arch(), logger).run(version); + } catch (error: unknown) { + if (error instanceof Error) { + tasks.setResult(tasks.TaskResult.Failed, `"Failed to execute pack. ${error.message}${os.EOL}${error.stack}`, true); + } else { + tasks.setResult(tasks.TaskResult.Failed, `"Failed to execute pack. ${error}`, true); + } + } +} + +run(); diff --git a/source/tasks/OctoInstaller/OctoInstallerV6/installer.ts b/source/tasks/OctoInstaller/OctoInstallerV6/installer.ts new file mode 100644 index 00000000..1ed24bcc --- /dev/null +++ b/source/tasks/OctoInstaller/OctoInstallerV6/installer.ts @@ -0,0 +1,56 @@ +import * as tools from "azure-pipelines-tool-lib"; +import * as tasks from "azure-pipelines-task-lib"; +import path from "path"; +import { executeWithSetResult } from "../../Utils/octopusTasks"; +import { DownloadEndpointRetriever, Endpoint } from "./downloadEndpointRetriever"; +import { Logger } from "@octopusdeploy/api-client"; + +const TOOL_NAME = "octopus"; + +export class Installer { + constructor(readonly releasesUrl: string, readonly osPlatform: string, readonly osArch: string, readonly logger: Logger) {} + + public async run(versionSpec: string) { + await executeWithSetResult( + async () => { + const endpoint = await new DownloadEndpointRetriever(this.releasesUrl, this.osPlatform, this.osArch, this.logger).getEndpoint(versionSpec); + let toolPath = tools.findLocalTool(TOOL_NAME, endpoint.version); + + if (!toolPath) { + toolPath = await this.installTool(endpoint); + toolPath = tools.findLocalTool(TOOL_NAME, endpoint.version); + } + + tools.prependPath(toolPath); + }, + `Installed octopus v${versionSpec}.`, + `Failed to install octopus v${versionSpec}.` + ); + } + + private async installTool(endpoint: Endpoint): Promise { + if (!endpoint.downloadUrl) { + throw Error(`Failed to download Octopus CLI tool version ${endpoint.version}.`); + } + + const downloadPath = await tools.downloadTool(endpoint.downloadUrl); + + // + // Extract + // + let extPath: string; + if (this.osPlatform == "win32") { + extPath = tasks.getVariable("Agent.TempDirectory") || ""; + if (!extPath) { + throw new Error("Expected Agent.TempDirectory to be set"); + } + + extPath = path.join(extPath, "n"); // use as short a path as possible due to nested node_modules folders + extPath = await tools.extractZip(downloadPath, extPath); + } else { + extPath = await tools.extractTar(downloadPath); + } + + return await tools.cacheDir(extPath, TOOL_NAME, endpoint.version); + } +} diff --git a/source/tasks/OctoInstaller/OctoInstallerV6/octopusCLIVersionResolver.test.ts b/source/tasks/OctoInstaller/OctoInstallerV6/octopusCLIVersionResolver.test.ts new file mode 100644 index 00000000..eb33e38f --- /dev/null +++ b/source/tasks/OctoInstaller/OctoInstallerV6/octopusCLIVersionResolver.test.ts @@ -0,0 +1,67 @@ +import { OctopusCLIVersionResolver } from "./octopusCLIVersionResolver"; + +describe("OctopusCLIVersionFetcher tests", () => { + test("Gets latest", () => { + const fetcher = new OctopusCLIVersionResolver(["1.0.0", "2.0.0", "2.1.0"]); + + const version = fetcher.getVersion("*"); + + expect(version).toBe("2.1.0"); + }); + + test("Fixed returns fixed version", () => { + const fetcher = new OctopusCLIVersionResolver(["1.0.0", "2.0.0", "2.1.0"]); + + const version = fetcher.getVersion("1.0.0"); + + expect(version).toBe("1.0.0"); + }); + + test("When version no exists", () => { + const fetcher = new OctopusCLIVersionResolver(["1.0.0", "2.0.0", "2.1.0"]); + + expect(() => fetcher.getVersion("5.0.0")).toThrow(); + }); + + test("Get latest minor", () => { + const fetcher = new OctopusCLIVersionResolver(["1.0.0", "2.0.0", "2.1.0", "3.0.0"]); + + const version = fetcher.getVersion("2.*"); + + expect(version).toBe("2.1.0"); + }); + + test("Get latest patch", () => { + const fetcher = new OctopusCLIVersionResolver(["1.0.0", "1.0.3", "2.1.0", "3.0.0"]); + + const version = fetcher.getVersion("1.0.*"); + + expect(version).toBe("1.0.3"); + }); + + test("When version spec if invalid", () => { + const fetcher = new OctopusCLIVersionResolver(["1.0.0", "2.0.0", "2.1.0"]); + + expect(() => fetcher.getVersion("*.*")).toThrow(); + + expect(() => fetcher.getVersion("*.2")).toThrow(); + + expect(() => fetcher.getVersion("sdfs")).toThrow(); + }); + + test("Get latest major", () => { + const fetcher = new OctopusCLIVersionResolver(["1.0.0", "2.0.0", "2.1.0", "3.0.0"]); + + const version = fetcher.getVersion("2"); + + expect(version).toBe("2.1.0"); + }); + + test("Get latest not pre-release", () => { + const fetcher = new OctopusCLIVersionResolver(["1.0.0", "1.0.3", "2.1.0", "3.0.0", "4.0.0-pre"]); + + const version = fetcher.getVersion("*"); + + expect(version).toBe("3.0.0"); + }); +}); diff --git a/source/tasks/OctoInstaller/OctoInstallerV6/octopusCLIVersionResolver.ts b/source/tasks/OctoInstaller/OctoInstallerV6/octopusCLIVersionResolver.ts new file mode 100644 index 00000000..9cca5ca6 --- /dev/null +++ b/source/tasks/OctoInstaller/OctoInstallerV6/octopusCLIVersionResolver.ts @@ -0,0 +1,72 @@ +import { maxSatisfying, valid } from "semver"; + +export class OctopusCLIVersionResolver { + constructor(readonly versions: string[]) {} + + getVersion(versionSpec: string): string { + if (versionSpec === "*") { + const version = maxSatisfying(this.versions, versionSpec); + if (!version) { + throw new Error(`A version satisfying '${versionSpec}' could not be found.`); + } + + return version; + } + + if (valid(versionSpec) === null) { + const parts = versionSpec.split("."); + if (parts.length > 3) { + throw new Error(`The '${versionSpec}' is an invalid version, a version needs to be a maximum of three parts.`); + } + + if (parts.length === 1) { + const majorVersion = parts[0]; + if (Number.isNaN(Number.parseInt(majorVersion))) { + throw new Error(`The '${versionSpec}' version needs to specify a number or '*' for its major part.`); + } + } + + if (parts.length === 2) { + const majorVersion = parts[0]; + const minorVersion = parts[1]; + + // the major version number must be a number + if (Number.isNaN(Number.parseInt(majorVersion))) { + throw new Error(`The '${versionSpec}' version needs to specify a number for its major part.`); + } + + if (minorVersion !== "*" && Number.isNaN(Number.parseInt(minorVersion))) { + throw new Error(`The '${versionSpec}' version needs to specify a number or '*' for its minor part.`); + } + } + + if (parts.length === 3) { + const majorVersion = parts[0]; + const minorVersion = parts[1]; + const patchVersion = parts[2]; + + // the major version number must be a number + if (Number.isNaN(Number.parseInt(majorVersion))) { + throw new Error(`The '${versionSpec}' version needs to specify a number for its major part.`); + } + + // the minor version number must be a number + if (Number.isNaN(Number.parseInt(minorVersion))) { + throw new Error(`The '${versionSpec}' version needs to specify a number for its minor part.`); + } + + if (patchVersion !== "*" && Number.isNaN(Number.parseInt(patchVersion))) { + throw new Error(`The '${versionSpec}' version needs to specify a number or '*' for its patch part.`); + } + } + } + + const version = maxSatisfying(this.versions, versionSpec); + + if (!version) { + throw new Error(`A version satisfying '${versionSpec}' could not be found.`); + } + + return version; + } +} diff --git a/source/tasks/OctoInstaller/OctoInstallerV6/task.json b/source/tasks/OctoInstaller/OctoInstallerV6/task.json new file mode 100644 index 00000000..4558eb64 --- /dev/null +++ b/source/tasks/OctoInstaller/OctoInstallerV6/task.json @@ -0,0 +1,47 @@ +{ + "id": "57342b23-3a76-490a-8e78-25d4ade2f2e3", + "name": "OctoInstaller", + "friendlyName": "Octopus CLI Installer", + "description": "Install a specific version of the Octopus CLI (Go)", + "helpMarkDown": "Install a specific version of the Octopus CLI (Go)", + "category": "Tool", + "runsOn": [ + "Agent", + "DeploymentGroup" + ], + "visibility": [ + "Build", + "Release" + ], + "author": "Octopus Deploy", + "version": { + "Major": 6, + "Minor": 0, + "Patch": 0 + }, + "satisfies": ["octopus"], + "demands": [], + "minimumAgentVersion": "2.144.0", + "groups": [ + { + "name": "advanced", + "displayName": "Advanced Options", + "isExpanded": false + } + ], + "inputs": [ + { + "name": "octopusVersion", + "type": "string", + "label": "Octopus CLI Version", + "required": true, + "helpMarkDown": "Specify version of Octopus CLI to install.
Versions can be given in the following formats
  • `1.*` => Install latest in major version.
  • `7.3.*` => Install latest in major and minor version.
  • `8.0.1` => Install exact version.
  • `*` => Install whatever is latest.

  • Find the value of `version` for installing Octopus CLI, from the [this link](https://github.com/OctopusDeploy/cli/releases)." + } + ], + "instanceNameFormat": "Install Octopus CLI tool version $(version)", + "execution": { + "Node16": { + "target": "index.js" + } + } +} diff --git a/source/vsts.md b/source/vsts.md index dd81a2f2..1368e9cd 100644 --- a/source/vsts.md +++ b/source/vsts.md @@ -48,7 +48,7 @@ For example, if your build needs to create a Release for Project A, the user who This extension adds the following tasks: -- Octopus CLI Installer +- [Octopus CLI Installer](#tools-installer) - [Package Application - Zip](#pack-zip) - [Package Application - NuGet](#pack-nuget) - [Push Package(s) to Octopus](#push-packages-to-octopus) @@ -77,7 +77,7 @@ Alternatively, you can supply the tool using the system `PATH` environment varia Options include: -- **Octopus CLI Version**: Specific a version number like `8.0.0`, that version will be downloaded and supplied to the other tasks. +- **Octopus CLI Version**: Specific a version number like `1.0.0`, that version will be downloaded and supplied to the other tasks. ## ![Package Icon](img/octopus_package-03.png) Package Application - Zip