Use op-cli-installed as local package

This commit is contained in:
Volodymyr Zotov
2025-12-15 13:45:09 -06:00
parent c96389a7ae
commit 2f243ca4fa
25 changed files with 1894 additions and 3268 deletions

View File

@@ -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;
}
}

View File

@@ -0,0 +1 @@
export { type Installer, newCliInstaller } from "./installer";

View File

@@ -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",
);
});
});

View File

@@ -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}`);
}
};

View File

@@ -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);
});
});

View 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));
}
}

View File

@@ -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);
});
});

View 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");
}
}

View File

@@ -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);
});
});

View 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));
}
}