Use op-cli-installed as local package
This commit is contained in:
4404
dist/index.js
vendored
4404
dist/index.js
vendored
File diff suppressed because it is too large
Load Diff
26
package-lock.json
generated
26
package-lock.json
generated
@@ -12,8 +12,8 @@
|
||||
"@1password/op-js": "^0.1.11",
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/exec": "^1.1.1",
|
||||
"dotenv": "^17.2.2",
|
||||
"op-cli-installer": "github:1Password/op-cli-installer#e6c1c758bc3339e5fe9b06255728039f688f73fa"
|
||||
"@actions/tool-cache": "^2.0.2",
|
||||
"dotenv": "^17.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@1password/eslint-config": "^4.3.1",
|
||||
@@ -6399,28 +6399,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/op-cli-installer": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "git+ssh://git@github.com/1Password/op-cli-installer.git#e6c1c758bc3339e5fe9b06255728039f688f73fa",
|
||||
"integrity": "sha512-ueyYQAgtbIHP2QWx9iCiztoPov01GdxPZNZ4+Qg3IaAyC4khMIk4/vYPagRhRKta+HI6fWV6jO3/ajBj27KBZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/tool-cache": "^2.0.2",
|
||||
"semver": "^7.7.2"
|
||||
}
|
||||
},
|
||||
"node_modules/op-cli-installer/node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
"@1password/op-js": "^0.1.11",
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/exec": "^1.1.1",
|
||||
"dotenv": "^17.2.2",
|
||||
"op-cli-installer": "github:1Password/op-cli-installer#e6c1c758bc3339e5fe9b06255728039f688f73fa"
|
||||
"@actions/tool-cache": "^2.0.2",
|
||||
"dotenv": "^17.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@1password/eslint-config": "^4.3.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import dotenv from "dotenv";
|
||||
import * as core from "@actions/core";
|
||||
import { validateCli } from "@1password/op-js";
|
||||
import { installCliOnGithubActionRunner } from "op-cli-installer";
|
||||
import dotenv from "dotenv";
|
||||
import { installCliOnGithubActionRunner } from "./op-cli-installer";
|
||||
import { loadSecrets, unsetPrevious, validateAuth } from "./utils";
|
||||
import { envFilePath } from "./constants";
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import os from "os";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import * as tc from "@actions/tool-cache";
|
||||
|
||||
export type SupportedPlatform = Extract<
|
||||
NodeJS.Platform,
|
||||
"linux" | "darwin" | "win32"
|
||||
>;
|
||||
|
||||
// maps OS architecture names to 1Password CLI installer architecture names
|
||||
export const archMap: Record<string, string> = {
|
||||
ia32: "386",
|
||||
x64: "amd64",
|
||||
arm: "arm",
|
||||
arm64: "arm64",
|
||||
};
|
||||
|
||||
// Builds the download URL for the 1Password CLI based on the platform and version.
|
||||
export const cliUrlBuilder: Record<
|
||||
SupportedPlatform,
|
||||
(version: string, arch?: string) => string
|
||||
> = {
|
||||
linux: (version, arch) =>
|
||||
`https://cache.agilebits.com/dist/1P/op2/pkg/${version}/op_linux_${arch}_${version}.zip`,
|
||||
darwin: (version) =>
|
||||
`https://cache.agilebits.com/dist/1P/op2/pkg/${version}/op_apple_universal_${version}.pkg`,
|
||||
win32: (version, arch) =>
|
||||
`https://cache.agilebits.com/dist/1P/op2/pkg/${version}/op_windows_${arch}_${version}.zip`,
|
||||
};
|
||||
|
||||
export class CliInstaller {
|
||||
public readonly version: string;
|
||||
public readonly arch: string;
|
||||
|
||||
public constructor(version: string) {
|
||||
this.version = version;
|
||||
this.arch = this.getArch();
|
||||
}
|
||||
|
||||
public async install(url: string): Promise<void> {
|
||||
console.info(`Downloading 1Password CLI from: ${url}`);
|
||||
const downloadPath = await tc.downloadTool(url);
|
||||
console.info("Installing 1Password CLI");
|
||||
const extractedPath = await tc.extractZip(downloadPath);
|
||||
core.addPath(extractedPath);
|
||||
core.info("1Password CLI installed");
|
||||
}
|
||||
|
||||
private getArch(): string {
|
||||
const arch = archMap[os.arch()];
|
||||
if (!arch) {
|
||||
throw new Error("Unsupported architecture");
|
||||
}
|
||||
|
||||
return arch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { type Installer, newCliInstaller } from "./installer";
|
||||
@@ -0,0 +1,43 @@
|
||||
import os from "os";
|
||||
|
||||
import { newCliInstaller } from "./installer";
|
||||
import { LinuxInstaller } from "./linux";
|
||||
import { MacOsInstaller } from "./macos";
|
||||
import { WindowsInstaller } from "./windows";
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("newCliInstaller", () => {
|
||||
const version = "1.0.0";
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should return LinuxInstaller for linux platform", () => {
|
||||
jest.spyOn(os, "platform").mockReturnValue("linux");
|
||||
const installer = newCliInstaller(version);
|
||||
expect(installer).toBeInstanceOf(LinuxInstaller);
|
||||
});
|
||||
|
||||
it("should return MacOsInstaller for darwin platform", () => {
|
||||
jest.spyOn(os, "platform").mockReturnValue("darwin");
|
||||
const installer = newCliInstaller(version);
|
||||
expect(installer).toBeInstanceOf(MacOsInstaller);
|
||||
});
|
||||
|
||||
it("should return WindowsInstaller for win32 platform", () => {
|
||||
jest.spyOn(os, "platform").mockReturnValue("win32");
|
||||
const installer = newCliInstaller(version);
|
||||
expect(installer).toBeInstanceOf(WindowsInstaller);
|
||||
});
|
||||
|
||||
it("should throw error for unsupported platform", () => {
|
||||
jest.spyOn(os, "platform").mockReturnValue("sunos");
|
||||
expect(() => newCliInstaller(version)).toThrow(
|
||||
"Unsupported platform: sunos",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import os from "os";
|
||||
|
||||
import { LinuxInstaller } from "./linux";
|
||||
import { MacOsInstaller } from "./macos";
|
||||
import { WindowsInstaller } from "./windows";
|
||||
|
||||
export interface Installer {
|
||||
installCli(): Promise<void>;
|
||||
}
|
||||
|
||||
export const newCliInstaller = (version: string): Installer => {
|
||||
const platform = os.platform();
|
||||
switch (platform) {
|
||||
case "linux":
|
||||
return new LinuxInstaller(version);
|
||||
case "darwin":
|
||||
return new MacOsInstaller(version);
|
||||
case "win32":
|
||||
return new WindowsInstaller(version);
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import os from "os";
|
||||
|
||||
import {
|
||||
archMap,
|
||||
CliInstaller,
|
||||
cliUrlBuilder,
|
||||
type SupportedPlatform,
|
||||
} from "./cli-installer";
|
||||
import { LinuxInstaller } from "./linux";
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("LinuxInstaller", () => {
|
||||
const version = "1.2.3";
|
||||
const arch: NodeJS.Architecture = "arm64";
|
||||
|
||||
it("should construct with given version and architecture", () => {
|
||||
jest.spyOn(os, "arch").mockReturnValue(arch);
|
||||
const installer = new LinuxInstaller(version);
|
||||
expect(installer.version).toEqual(version);
|
||||
expect(installer.arch).toEqual(archMap[arch]);
|
||||
});
|
||||
|
||||
it("should call install with correct URL", async () => {
|
||||
const installer = new LinuxInstaller(version);
|
||||
const installMock = jest
|
||||
.spyOn(CliInstaller.prototype, "install")
|
||||
.mockResolvedValue();
|
||||
|
||||
await installer.installCli();
|
||||
|
||||
const builder = cliUrlBuilder["linux" as SupportedPlatform];
|
||||
const url = builder(version, installer.arch);
|
||||
expect(installMock).toHaveBeenCalledWith(url);
|
||||
});
|
||||
});
|
||||
19
src/op-cli-installer/github-action/cli-installer/linux.ts
Normal file
19
src/op-cli-installer/github-action/cli-installer/linux.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
CliInstaller,
|
||||
cliUrlBuilder,
|
||||
type SupportedPlatform,
|
||||
} from "./cli-installer";
|
||||
import type { Installer } from "./installer";
|
||||
|
||||
export class LinuxInstaller extends CliInstaller implements Installer {
|
||||
private readonly platform: SupportedPlatform = "linux"; // Node.js platform identifier for Linux
|
||||
|
||||
public constructor(version: string) {
|
||||
super(version);
|
||||
}
|
||||
|
||||
public async installCli(): Promise<void> {
|
||||
const urlBuilder = cliUrlBuilder[this.platform];
|
||||
await super.install(urlBuilder(this.version, this.arch));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import os from "os";
|
||||
|
||||
import {
|
||||
archMap,
|
||||
cliUrlBuilder,
|
||||
type SupportedPlatform,
|
||||
} from "./cli-installer";
|
||||
import { MacOsInstaller } from "./macos";
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("MacOsInstaller", () => {
|
||||
const version = "1.2.3";
|
||||
const arch: NodeJS.Architecture = "x64";
|
||||
|
||||
it("should construct with given version and architecture", () => {
|
||||
jest.spyOn(os, "arch").mockReturnValue(arch);
|
||||
const installer = new MacOsInstaller(version);
|
||||
expect(installer.version).toEqual(version);
|
||||
expect(installer.arch).toEqual(archMap[arch]);
|
||||
});
|
||||
|
||||
it("should call install with correct URL", async () => {
|
||||
const installer = new MacOsInstaller(version);
|
||||
const installMock = jest.spyOn(installer, "install").mockResolvedValue();
|
||||
|
||||
await installer.installCli();
|
||||
|
||||
const builder = cliUrlBuilder["darwin" as SupportedPlatform];
|
||||
const url = builder(version, installer.arch);
|
||||
expect(installMock).toHaveBeenCalledWith(url);
|
||||
});
|
||||
});
|
||||
49
src/op-cli-installer/github-action/cli-installer/macos.ts
Normal file
49
src/op-cli-installer/github-action/cli-installer/macos.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { exec } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { promisify } from "util";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import * as tc from "@actions/tool-cache";
|
||||
|
||||
import {
|
||||
CliInstaller,
|
||||
cliUrlBuilder,
|
||||
type SupportedPlatform,
|
||||
} from "./cli-installer";
|
||||
import { type Installer } from "./installer";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export class MacOsInstaller extends CliInstaller implements Installer {
|
||||
private readonly platform: SupportedPlatform = "darwin"; // Node.js platform identifier for macOS
|
||||
|
||||
public constructor(version: string) {
|
||||
super(version);
|
||||
}
|
||||
|
||||
public async installCli(): Promise<void> {
|
||||
const urlBuilder = cliUrlBuilder[this.platform];
|
||||
await this.install(urlBuilder(this.version));
|
||||
}
|
||||
|
||||
// @actions/tool-cache package does not support .pkg files, so we need to handle the installation manually
|
||||
public override async install(downloadUrl: string): Promise<void> {
|
||||
console.info(`Downloading 1Password CLI from: ${downloadUrl}`);
|
||||
const pkgPath = await tc.downloadTool(downloadUrl);
|
||||
const pkgWithExtension = `${pkgPath}.pkg`;
|
||||
fs.renameSync(pkgPath, pkgWithExtension);
|
||||
|
||||
const expandDir = "temp-pkg";
|
||||
await execAsync(`pkgutil --expand "${pkgWithExtension}" "${expandDir}"`);
|
||||
const payloadPath = path.join(expandDir, "op.pkg", "Payload");
|
||||
console.info("Installing 1Password CLI");
|
||||
const cliPath = await tc.extractTar(payloadPath);
|
||||
core.addPath(cliPath);
|
||||
|
||||
fs.rmSync(expandDir, { recursive: true, force: true });
|
||||
fs.rmSync(pkgPath, { force: true });
|
||||
|
||||
core.info("1Password CLI installed");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import os from "os";
|
||||
|
||||
import {
|
||||
archMap,
|
||||
CliInstaller,
|
||||
cliUrlBuilder,
|
||||
type SupportedPlatform,
|
||||
} from "./cli-installer";
|
||||
import { WindowsInstaller } from "./windows";
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("WindowsInstaller", () => {
|
||||
const version = "1.2.3";
|
||||
const arch: NodeJS.Architecture = "x64";
|
||||
|
||||
it("should construct with given version and architecture", () => {
|
||||
jest.spyOn(os, "arch").mockReturnValue(arch);
|
||||
const installer = new WindowsInstaller(version);
|
||||
expect(installer.version).toEqual(version);
|
||||
expect(installer.arch).toEqual(archMap[arch]);
|
||||
});
|
||||
|
||||
it("should call install with correct URL", async () => {
|
||||
const installer = new WindowsInstaller(version);
|
||||
const installMock = jest
|
||||
.spyOn(CliInstaller.prototype, "install")
|
||||
.mockResolvedValue();
|
||||
|
||||
await installer.installCli();
|
||||
|
||||
const builder = cliUrlBuilder["win32" as SupportedPlatform];
|
||||
const url = builder(version, installer.arch);
|
||||
expect(installMock).toHaveBeenCalledWith(url);
|
||||
});
|
||||
});
|
||||
19
src/op-cli-installer/github-action/cli-installer/windows.ts
Normal file
19
src/op-cli-installer/github-action/cli-installer/windows.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
CliInstaller,
|
||||
cliUrlBuilder,
|
||||
type SupportedPlatform,
|
||||
} from "./cli-installer";
|
||||
import type { Installer } from "./installer";
|
||||
|
||||
export class WindowsInstaller extends CliInstaller implements Installer {
|
||||
private readonly platform: SupportedPlatform = "win32"; // Node.js platform identifier for Windows
|
||||
|
||||
public constructor(version: string) {
|
||||
super(version);
|
||||
}
|
||||
|
||||
public async installCli(): Promise<void> {
|
||||
const urlBuilder = cliUrlBuilder[this.platform];
|
||||
await super.install(urlBuilder(this.version, this.arch));
|
||||
}
|
||||
}
|
||||
18
src/op-cli-installer/github-action/index.ts
Normal file
18
src/op-cli-installer/github-action/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { ReleaseChannel, VersionResolver } from "../version";
|
||||
|
||||
import { newCliInstaller } from "./cli-installer";
|
||||
|
||||
// Installs the 1Password CLI on a GitHub Action runner.
|
||||
export const installCliOnGithubActionRunner = async (
|
||||
version?: string,
|
||||
): Promise<void> => {
|
||||
// Get the version from parameter, if not passed - from the job input. Defaults to latest if no version is provided
|
||||
const providedVersion =
|
||||
version || core.getInput("version") || ReleaseChannel.latest;
|
||||
const versionResolver = new VersionResolver(providedVersion);
|
||||
await versionResolver.resolve();
|
||||
const installer = newCliInstaller(versionResolver.get());
|
||||
await installer.installCli();
|
||||
};
|
||||
81
src/op-cli-installer/index.test.ts
Normal file
81
src/op-cli-installer/index.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { newCliInstaller } from "./github-action/cli-installer";
|
||||
import {
|
||||
installCliOnGithubActionRunner,
|
||||
ReleaseChannel,
|
||||
VersionResolver,
|
||||
} from "./index";
|
||||
|
||||
jest.mock("./github-action/cli-installer", () => ({
|
||||
newCliInstaller: jest.fn().mockImplementation((_resolved: string) => ({
|
||||
installCli: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("installCliOnGithubActionRunner", () => {
|
||||
it("should defaults to `latest` when nothing is passed", async () => {
|
||||
jest.spyOn(core, "getInput").mockReturnValue("");
|
||||
jest.spyOn(VersionResolver.prototype, "resolve").mockResolvedValue();
|
||||
jest
|
||||
.spyOn(VersionResolver.prototype, "get")
|
||||
.mockReturnValue(ReleaseChannel.latest);
|
||||
|
||||
await installCliOnGithubActionRunner();
|
||||
|
||||
expect(newCliInstaller).toHaveBeenCalledWith(ReleaseChannel.latest);
|
||||
});
|
||||
|
||||
it("should defaults to `latest` when undefined is passed", async () => {
|
||||
jest.spyOn(core, "getInput").mockReturnValue("");
|
||||
jest.spyOn(VersionResolver.prototype, "resolve").mockResolvedValue();
|
||||
jest
|
||||
.spyOn(VersionResolver.prototype, "get")
|
||||
.mockReturnValue(ReleaseChannel.latest);
|
||||
|
||||
await installCliOnGithubActionRunner(undefined);
|
||||
|
||||
expect(newCliInstaller).toHaveBeenCalledWith(ReleaseChannel.latest);
|
||||
});
|
||||
|
||||
it("should set provided explicit version", async () => {
|
||||
const providedVersion = "1.2.3";
|
||||
jest.spyOn(core, "getInput").mockReturnValue("");
|
||||
jest.spyOn(VersionResolver.prototype, "resolve").mockResolvedValue();
|
||||
jest
|
||||
.spyOn(VersionResolver.prototype, "get")
|
||||
.mockReturnValue(providedVersion);
|
||||
|
||||
await installCliOnGithubActionRunner(providedVersion);
|
||||
|
||||
expect(newCliInstaller).toHaveBeenCalledWith(providedVersion);
|
||||
});
|
||||
|
||||
it("should set version provided as job input", async () => {
|
||||
const providedVersion = "3.0.0";
|
||||
jest.spyOn(core, "getInput").mockReturnValue(providedVersion);
|
||||
jest.spyOn(VersionResolver.prototype, "resolve").mockResolvedValue();
|
||||
jest
|
||||
.spyOn(VersionResolver.prototype, "get")
|
||||
.mockReturnValue(providedVersion);
|
||||
|
||||
await installCliOnGithubActionRunner();
|
||||
|
||||
expect(newCliInstaller).toHaveBeenCalledWith(providedVersion);
|
||||
});
|
||||
|
||||
it("should throw error for invalid version", async () => {
|
||||
const providedVersion = "invalid";
|
||||
jest.spyOn(core, "getInput").mockReturnValue(providedVersion);
|
||||
jest.spyOn(VersionResolver.prototype, "resolve").mockResolvedValue();
|
||||
jest
|
||||
.spyOn(VersionResolver.prototype, "get")
|
||||
.mockReturnValue(providedVersion);
|
||||
|
||||
await expect(installCliOnGithubActionRunner()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
2
src/op-cli-installer/index.ts
Normal file
2
src/op-cli-installer/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { installCliOnGithubActionRunner } from "./github-action";
|
||||
export { ReleaseChannel, VersionResolver } from "./version";
|
||||
13
src/op-cli-installer/version/constants.ts
Normal file
13
src/op-cli-installer/version/constants.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export enum ReleaseChannel {
|
||||
latest = "latest",
|
||||
latestBeta = "latest-beta",
|
||||
}
|
||||
|
||||
export interface VersionResponse {
|
||||
// eslint disabled next line as CLI2 is expected in getting CLI versions response
|
||||
/* eslint-disable-next-line @typescript-eslint/naming-convention */
|
||||
CLI2: {
|
||||
release: { version: string };
|
||||
beta: { version: string };
|
||||
};
|
||||
}
|
||||
91
src/op-cli-installer/version/helper.test.ts
Normal file
91
src/op-cli-installer/version/helper.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { ReleaseChannel } from "./constants";
|
||||
import { getLatestVersion } from "./helper";
|
||||
|
||||
describe("getLatestVersion", () => {
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should return latest stable version", async () => {
|
||||
const mockResponse = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
CLI2: {
|
||||
release: { version: "2.31.0" },
|
||||
beta: { version: "2.32.0-beta.01" },
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(global, "fetch").mockResolvedValueOnce({
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
json: async () => mockResponse,
|
||||
} as Response);
|
||||
|
||||
const version = await getLatestVersion(ReleaseChannel.latest);
|
||||
expect(version).toBe("2.31.0");
|
||||
});
|
||||
|
||||
it("should return latest beta version", async () => {
|
||||
const mockResponse = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
CLI2: {
|
||||
release: { version: "2.31.0" },
|
||||
beta: { version: "2.32.0-beta.01" },
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(global, "fetch").mockResolvedValueOnce({
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
json: async () => mockResponse,
|
||||
} as Response);
|
||||
|
||||
const version = await getLatestVersion(ReleaseChannel.latestBeta);
|
||||
expect(version).toBe("2.32.0-beta.01");
|
||||
});
|
||||
|
||||
it("should throw if no CLI2 field", async () => {
|
||||
jest.spyOn(global, "fetch").mockResolvedValueOnce({
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
json: async () => ({}),
|
||||
} as Response);
|
||||
|
||||
await expect(getLatestVersion(ReleaseChannel.latest)).rejects.toThrow(
|
||||
`No ${ReleaseChannel.latest} versions found`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if no stable version found", async () => {
|
||||
const mockResponse = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
CLI2: {
|
||||
beta: { version: "2.32.0-beta.01" },
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(global, "fetch").mockResolvedValueOnce({
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
json: async () => mockResponse,
|
||||
} as Response);
|
||||
|
||||
await expect(getLatestVersion(ReleaseChannel.latest)).rejects.toThrow(
|
||||
`No ${ReleaseChannel.latest} versions found`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if no beta version found", async () => {
|
||||
const mockResponse = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
CLI2: {
|
||||
release: { version: "2.32.0" },
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(global, "fetch").mockResolvedValueOnce({
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
json: async () => mockResponse,
|
||||
} as Response);
|
||||
|
||||
await expect(getLatestVersion(ReleaseChannel.latestBeta)).rejects.toThrow(
|
||||
`No ${ReleaseChannel.latestBeta} versions found`,
|
||||
);
|
||||
});
|
||||
});
|
||||
23
src/op-cli-installer/version/helper.ts
Normal file
23
src/op-cli-installer/version/helper.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { ReleaseChannel, type VersionResponse } from "./constants";
|
||||
|
||||
// Returns the latest version of the 1Password CLI based on the specified channel.
|
||||
export const getLatestVersion = async (
|
||||
channel: ReleaseChannel,
|
||||
): Promise<string> => {
|
||||
core.info(`Getting ${channel} version number`);
|
||||
const res = await fetch("https://app-updates.agilebits.com/latest");
|
||||
const json = (await res.json()) as VersionResponse;
|
||||
const latestStable = json?.CLI2?.release?.version;
|
||||
const latestBeta = json?.CLI2?.beta?.version;
|
||||
const version =
|
||||
channel === ReleaseChannel.latestBeta ? latestBeta : latestStable;
|
||||
|
||||
if (!version) {
|
||||
core.error(`No ${channel} versions found`);
|
||||
throw new Error(`No ${channel} versions found`);
|
||||
}
|
||||
|
||||
return version;
|
||||
};
|
||||
2
src/op-cli-installer/version/index.ts
Normal file
2
src/op-cli-installer/version/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { VersionResolver } from "./version-resolver";
|
||||
export { ReleaseChannel } from "./constants";
|
||||
45
src/op-cli-installer/version/validate.test.ts
Normal file
45
src/op-cli-installer/version/validate.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from "@jest/globals";
|
||||
|
||||
import { validateVersion } from "./validate";
|
||||
|
||||
describe("validateVersion", () => {
|
||||
it('should not throw for "latest"', () => {
|
||||
expect(() => validateVersion("latest")).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for "latest-beta"', () => {
|
||||
expect(() => validateVersion("latest-beta")).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for valid semver version "2.18.0"', () => {
|
||||
expect(() => validateVersion("2.18.0")).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw for partial version "2"', () => {
|
||||
expect(() => validateVersion("2")).toThrow();
|
||||
});
|
||||
|
||||
it('should throw for partial version "2.1"', () => {
|
||||
expect(() => validateVersion("2.1")).toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for valid beta "2.19.0-beta.01"', () => {
|
||||
expect(() => validateVersion("2.19.0-beta.01")).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for valid beta "2.19.3-beta.12"', () => {
|
||||
expect(() => validateVersion("2.19.3-beta.12")).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for coerced version "v2.19.0"', () => {
|
||||
expect(() => validateVersion("v2.19.0")).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw for invalid version "latest-abc"', () => {
|
||||
expect(() => validateVersion("latest-abc")).toThrow();
|
||||
});
|
||||
|
||||
it("should throw for empty string", () => {
|
||||
expect(() => validateVersion("")).toThrow();
|
||||
});
|
||||
});
|
||||
23
src/op-cli-installer/version/validate.ts
Normal file
23
src/op-cli-installer/version/validate.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import semver from "semver";
|
||||
|
||||
import { ReleaseChannel } from "./constants";
|
||||
|
||||
// Validates if the provided version type is a valid enum value or a valid semver version.
|
||||
export const validateVersion = (input: string): void => {
|
||||
if (Object.values(ReleaseChannel).includes(input as ReleaseChannel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1Password beta releases (aka 2.19.0-beta.01) are not semver compliant.
|
||||
// According to semver, it should be "2.19.0-beta.1".
|
||||
// That's why we need to normalize them before validating.
|
||||
// Accepts valid semver versions like "2.18.0" or beta-releases like "2.19.0-beta.01"
|
||||
// or versions with 'v' prefix like "v2.19.0"
|
||||
const normalized = input.replace(/-beta\.0*(\d+)/, "-beta.$1");
|
||||
const normInput = new semver.SemVer(normalized);
|
||||
if (semver.valid(normInput)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid version input: ${input}`);
|
||||
};
|
||||
58
src/op-cli-installer/version/version-resolver.test.ts
Normal file
58
src/op-cli-installer/version/version-resolver.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { expect } from "@jest/globals";
|
||||
|
||||
import { ReleaseChannel } from "./constants";
|
||||
import { VersionResolver } from "./version-resolver";
|
||||
|
||||
describe("VersionResolver", () => {
|
||||
test("should throw error when invalid version provided", () => {
|
||||
expect(() => new VersionResolver("vv")).toThrow();
|
||||
});
|
||||
|
||||
test("should throw error when version is empty", () => {
|
||||
expect(() => new VersionResolver("")).toThrow();
|
||||
});
|
||||
|
||||
test("should throw error for major version only", () => {
|
||||
expect(() => new VersionResolver("1")).toThrow();
|
||||
});
|
||||
|
||||
test("should throw error for major and minor version only", () => {
|
||||
expect(() => new VersionResolver("1.0")).toThrow();
|
||||
});
|
||||
|
||||
test("should resolve latest stable version", async () => {
|
||||
const versionResolver = new VersionResolver(ReleaseChannel.latest);
|
||||
await versionResolver.resolve();
|
||||
expect(versionResolver.get()).toBeDefined();
|
||||
});
|
||||
|
||||
test("should resolve latest beta version", async () => {
|
||||
const versionResolver = new VersionResolver(ReleaseChannel.latestBeta);
|
||||
await versionResolver.resolve();
|
||||
expect(versionResolver.get()).toBeDefined();
|
||||
});
|
||||
|
||||
test("should resolve version without 'v' prefix", async () => {
|
||||
const versionResolver = new VersionResolver("1.0.0");
|
||||
await versionResolver.resolve();
|
||||
expect(versionResolver.get()).toBe("v1.0.0");
|
||||
});
|
||||
|
||||
test("should resolve version with 'v' prefix", async () => {
|
||||
const versionResolver = new VersionResolver("v1.0.0");
|
||||
await versionResolver.resolve();
|
||||
expect(versionResolver.get()).toBe("v1.0.0");
|
||||
});
|
||||
|
||||
test("should resolve beta version without 'v' prefix", async () => {
|
||||
const versionResolver = new VersionResolver("2.19.0-beta.01");
|
||||
await versionResolver.resolve();
|
||||
expect(versionResolver.get()).toBe("v2.19.0-beta.01");
|
||||
});
|
||||
|
||||
test("should resolve beta version with 'v' prefix", async () => {
|
||||
const versionResolver = new VersionResolver("v2.19.0-beta.01");
|
||||
await versionResolver.resolve();
|
||||
expect(versionResolver.get()).toBe("v2.19.0-beta.01");
|
||||
});
|
||||
});
|
||||
45
src/op-cli-installer/version/version-resolver.ts
Normal file
45
src/op-cli-installer/version/version-resolver.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { ReleaseChannel } from "./constants";
|
||||
import { getLatestVersion } from "./helper";
|
||||
import { validateVersion } from "./validate";
|
||||
|
||||
export class VersionResolver {
|
||||
private version: string;
|
||||
|
||||
public constructor(version: string) {
|
||||
this.validate(version);
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public get(): string {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public async resolve(): Promise<void> {
|
||||
core.info(`Resolving version: ${this.version}`);
|
||||
if (!this.version) {
|
||||
core.error("Version is not provided");
|
||||
throw new Error("Version is not provided");
|
||||
}
|
||||
|
||||
if (this.isReleaseChannel(this.version)) {
|
||||
this.version = await getLatestVersion(this.version);
|
||||
}
|
||||
|
||||
// add `v` prefix if not already present
|
||||
this.version = this.version.startsWith("v")
|
||||
? this.version
|
||||
: `v${this.version}`;
|
||||
}
|
||||
|
||||
private validate(version: string) {
|
||||
core.info(`Validating version number: '${version}'`);
|
||||
validateVersion(version);
|
||||
core.info(`Version number '${version}' is valid`);
|
||||
}
|
||||
|
||||
private isReleaseChannel(value: string): value is ReleaseChannel {
|
||||
return Object.values(ReleaseChannel).includes(value as ReleaseChannel);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user