feat: split plugin builds

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced containerized plugin deployment support with updated Docker
Compose configurations.
- Added continuous build watch modes for API, web, and UI components for
smoother development iterations.
  - Added a new job for API testing in the CI/CD workflow.
- Added a new shell script to determine the local host's IP address for
Docker configurations.
- Introduced a new entry point and HTTP server setup in the plugin's
Docker environment.
- Added new scripts for building and watching plugin changes in
real-time.
- Added a new script for building the project in watch mode for the API
and UI components.

- **Improvements**
- Streamlined the plugin installation process and refined release
workflows for a more reliable user experience.
- Enhanced overall CI/CD pipelines to ensure efficient, production-ready
deployments.
- Updated artifact upload paths and job definitions for clarity and
efficiency.
- Implemented new utility functions for better URL management and
changelog generation.
- Modified the `.dockerignore` file to ignore all contents within the
`node_modules` directory.
- Added new constants and functions for managing plugin paths and
configurations.
- Updated the build process in the Dockerfile to focus on release
operations.

- **Tests**
- Expanded automated testing to validate environment setups and build
stability, ensuring higher reliability during updates.
- Introduced new test suites for validating plugin environment setups
and configurations.
- Added tests for validating environment variables and handling of
manifest files.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Datelle <mdatelle@icloud.com>
Co-authored-by: mdatelle <mike@datelle.net>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Pujit Mehrotra <pujit@lime-technology.com>
This commit is contained in:
Eli Bosley
2025-03-04 15:18:04 -05:00
committed by GitHub
parent 270072266a
commit d63e54bdbc
37 changed files with 6885 additions and 678 deletions
@@ -0,0 +1,172 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
validatePluginEnv,
setupPluginEnv,
} from "../../cli/setup-plugin-environment";
import { access, readFile } from "node:fs/promises";
// Mock fs/promises
vi.mock("node:fs/promises", () => ({
access: vi.fn(),
readFile: vi.fn(),
constants: {
F_OK: 0,
},
}));
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(readFile).mockImplementation((path, encoding) => {
console.log("Mock readFile called with:", path, encoding);
// If called with encoding parameter (for release notes)
if (encoding === "utf8") {
if (path.toString().includes("valid-release-notes.txt")) {
return Promise.resolve("Release notes content");
}
}
// If called without encoding (for txz file)
if (path.toString().includes("test.txz")) {
return Promise.resolve(Buffer.from("test content"));
}
return Promise.reject(new Error(`File not found: ${path}`));
});
vi.mocked(access).mockResolvedValue(undefined);
});
describe("validatePluginEnv", () => {
afterEach(() => {
vi.clearAllMocks();
});
it("validates required fields", async () => {
const validEnv = {
baseUrl: "https://example.com",
txzPath: "./test.txz",
pluginVersion: "2024.05.05.1232",
};
const result = await validatePluginEnv(validEnv);
expect(result).toMatchObject(validEnv);
});
it("throws on invalid URL", async () => {
const invalidEnv = {
baseUrl: "not-a-url",
txzPath: "./test.txz",
pluginVersion: "2024.05.05.1232",
};
await expect(validatePluginEnv(invalidEnv)).rejects.toThrow();
});
it("handles tag option in non-CI mode", async () => {
const envWithTag = {
baseUrl: "https://example.com",
txzPath: "./test.txz",
pluginVersion: "2024.05.05.1232",
tag: "v1.0.0",
};
const result = await validatePluginEnv(envWithTag);
expect(result.releaseNotes).toBe("FAST_TEST_CHANGELOG");
expect(result.tag).toBe("v1.0.0");
});
it("reads release notes when release-notes-path is provided", async () => {
const envWithNotes = {
baseUrl: "https://example.com",
txzPath: "./test.txz",
pluginVersion: "2024.05.05.1232",
releaseNotesPath: "valid-release-notes.txt",
};
const result = await validatePluginEnv(envWithNotes);
expect(access).toHaveBeenCalledWith("valid-release-notes.txt", 0);
expect(readFile).toHaveBeenCalledWith("valid-release-notes.txt", "utf8");
expect(result.releaseNotes).toBe("Release notes content");
});
it("throws when release notes file is empty", async () => {
// Instead of overwriting the entire mock, just mock this specific case
vi.mocked(readFile).mockImplementationOnce((path, encoding) => {
if (path === "/path/to/notes.md" && encoding === "utf8") {
return Promise.resolve("");
}
return Promise.reject(new Error("Unexpected mock call"));
});
const envWithEmptyNotes = {
baseUrl: "https://example.com",
txzPath: "./test.txz",
pluginVersion: "2024.05.05.1232",
releaseNotesPath: "/path/to/notes.md",
};
await expect(validatePluginEnv(envWithEmptyNotes)).rejects.toThrow(
"Release notes file is empty: /path/to/notes.md"
);
});
});
describe("setupPluginEnv", () => {
it("sets up environment from CLI arguments", async () => {
const argv = [
"node",
"script.js",
"--plugin-version",
"2024.05.05.1232",
"--txz-path",
"./test.txz",
"--base-url",
"https://example.com",
];
const result = await setupPluginEnv(argv);
expect(result).toMatchObject({
pluginVersion: "2024.05.05.1232",
txzPath: "./test.txz",
baseUrl: "https://example.com",
});
});
it("throws when required options are missing", async () => {
const argv = ["node", "script.js"]; // Missing required options
await expect(setupPluginEnv(argv)).rejects.toThrow();
});
it("handles optional CLI arguments", async () => {
const argv = [
"node",
"script.js",
"--txz-path",
"./test.txz",
"--base-url",
"https://example.com",
"--tag",
"PR1203",
"--ci",
"--plugin-version",
"2024.05.05.1232",
];
try {
const result = await setupPluginEnv(argv);
expect(result).toMatchObject({
pluginVersion: "2024.05.05.1232",
txzPath: "./test.txz",
txzSha256:
"6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72",
baseUrl: "https://example.com",
tag: "PR1203",
ci: true,
});
} catch (error) {
console.error("Error:", error);
throw error;
}
});
});
@@ -0,0 +1,47 @@
import { join } from "path";
import { validateTxzEnv, TxzEnv } from "../../cli/setup-txz-environment";
import { describe, it, expect, vi } from "vitest";
import { startingDir } from "../../utils/consts";
import { deployDir } from "../../utils/paths";
describe("setupTxzEnvironment", () => {
it("should return default values when no arguments are provided", async () => {
const envArgs = {};
const expected: TxzEnv = { ci: false, skipValidation: "false", compress: "1", txzOutputDir: join(startingDir, deployDir) };
const result = await validateTxzEnv(envArgs);
expect(result).toEqual(expected);
});
it("should parse and return provided environment arguments", async () => {
const envArgs = { ci: true, skipValidation: "true", txzOutputDir: join(startingDir, "deploy/release/test"), compress: '8' };
const expected: TxzEnv = { ci: true, skipValidation: "true", compress: "8", txzOutputDir: join(startingDir, "deploy/release/test") };
const result = await validateTxzEnv(envArgs);
expect(result).toEqual(expected);
});
it("should warn and skip validation when skipValidation is true", async () => {
const envArgs = { skipValidation: "true" };
const consoleWarnSpy = vi
.spyOn(console, "warn")
.mockImplementation(() => {});
await validateTxzEnv(envArgs);
expect(consoleWarnSpy).toHaveBeenCalledWith(
"skipValidation is true, skipping validation"
);
consoleWarnSpy.mockRestore();
});
it("should throw an error for invalid SKIP_VALIDATION value", async () => {
const envArgs = { skipValidation: "invalid" };
await expect(validateTxzEnv(envArgs)).rejects.toThrow(
"Must be true or false"
);
});
});
+108
View File
@@ -0,0 +1,108 @@
import { readFile, writeFile, mkdir, rename } from "fs/promises";
import { $ } from "zx";
import { escape as escapeHtml } from "html-sloppy-escaper";
import { dirname, join } from "node:path";
import { getTxzName, pluginName, startingDir } from "./utils/consts";
import { getPluginUrl } from "./utils/bucket-urls";
import { getMainTxzUrl } from "./utils/bucket-urls";
import {
deployDir,
getDeployPluginPath,
getRootPluginPath,
} from "./utils/paths";
import { PluginEnv, setupPluginEnv } from "./cli/setup-plugin-environment";
import { cleanupPluginFiles } from "./utils/cleanup";
/**
* Check if git is available
*/
const checkGit = async () => {
try {
await $`git log -1 --pretty=%B`;
} catch (err) {
console.error(`Error: git not available: ${err}`);
throw new Error(`Git not available: ${err}`);
}
};
const moveTxzFile = async (txzPath: string, pluginVersion: string) => {
const txzName = getTxzName(pluginVersion);
await rename(txzPath, join(deployDir, txzName));
};
function updateEntityValue(
xmlString: string,
entityName: string,
newValue: string
) {
console.log("Updating entity:", entityName, "with value:", newValue);
const regex = new RegExp(`<!ENTITY ${entityName} "[^"]*">`);
if (regex.test(xmlString)) {
return xmlString.replace(regex, `<!ENTITY ${entityName} "${newValue}">`);
}
throw new Error(`Entity ${entityName} not found in XML`);
}
const buildPlugin = async ({
pluginVersion,
baseUrl,
tag,
txzSha256,
releaseNotes,
}: PluginEnv) => {
// Update plg file
let plgContent = await readFile(getRootPluginPath({ startingDir }), "utf8");
// Update entity values
const entities: Record<string, string> = {
name: pluginName,
version: pluginVersion,
pluginURL: getPluginUrl({ baseUrl, tag }),
MAIN_TXZ: getMainTxzUrl({ baseUrl, pluginVersion, tag }),
TXZ_SHA256: txzSha256,
...(tag ? { TAG: tag } : {}),
};
console.log("Entities:", entities);
// Iterate over entities and update them
Object.entries(entities).forEach(([key, value]) => {
if (!value) {
throw new Error(`Entity ${key} not set in entities: ${JSON.stringify(entities)}`);
}
plgContent = updateEntityValue(plgContent, key, value);
});
if (releaseNotes) {
// Update the CHANGES section with release notes
plgContent = plgContent.replace(
/<CHANGES>.*?<\/CHANGES>/s,
`<CHANGES>\n${escapeHtml(releaseNotes)}\n</CHANGES>`
);
}
await mkdir(dirname(getDeployPluginPath({ startingDir })), {
recursive: true,
});
console.log("Writing plg file to:", getDeployPluginPath({ startingDir }));
await writeFile(getDeployPluginPath({ startingDir }), plgContent);
};
/**
* Main build script
*/
const main = async () => {
try {
const validatedEnv = await setupPluginEnv(process.argv);
await checkGit();
await cleanupPluginFiles();
await buildPlugin(validatedEnv);
await moveTxzFile(validatedEnv.txzPath, validatedEnv.pluginVersion);
} catch (error) {
console.error(error);
process.exit(1);
}
};
await main();
+110
View File
@@ -0,0 +1,110 @@
import { join } from "path";
import { $, cd } from "zx";
import { existsSync } from "node:fs";
import { readdir } from "node:fs/promises";
import { getTxzName, pluginName, startingDir } from "./utils/consts";
import { setupTxzEnv, TxzEnv } from "./cli/setup-txz-environment";
import { cleanupTxzFiles } from "./utils/cleanup";
// Recursively search for manifest files
const findManifestFiles = async (dir: string): Promise<string[]> => {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await findManifestFiles(fullPath)));
} else if (
entry.isFile() &&
(entry.name === "manifest.json" || entry.name === "ui.manifest.json")
) {
files.push(entry.name);
}
}
return files;
};
const validateSourceDir = async (validatedEnv: TxzEnv) => {
if (!validatedEnv.ci) {
console.log("Validating TXZ source directory");
}
const sourceDir = join(startingDir, "source");
if (!existsSync(sourceDir)) {
throw new Error(`Source directory ${sourceDir} does not exist`);
}
// Validate existence of webcomponent files:
// source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
const webcomponentDir = join(
sourceDir,
"dynamix.unraid.net",
"usr",
"local",
"emhttp",
"plugins",
"dynamix.my.servers",
"unraid-components"
);
if (!existsSync(webcomponentDir)) {
throw new Error(`Webcomponent directory ${webcomponentDir} does not exist`);
}
const manifestFiles = await findManifestFiles(webcomponentDir);
const hasManifest = manifestFiles.includes("manifest.json");
const hasUiManifest = manifestFiles.includes("ui.manifest.json");
if (!hasManifest || !hasUiManifest) {
console.log("Existing Manifest Files:", manifestFiles);
throw new Error(
`Webcomponents must contain both "ui.manifest.json" and "manifest.json" - be sure to have run pnpm build:wc in unraid-ui`
);
}
const apiDir = join(
startingDir,
"source",
pluginName,
"usr",
"local",
"unraid-api"
);
if (!existsSync(apiDir)) {
throw new Error(`API directory ${apiDir} does not exist`);
}
const packageJson = join(apiDir, "package.json");
if (!existsSync(packageJson)) {
throw new Error(`API package.json file ${packageJson} does not exist`);
}
// Now CHMOD the api/dist directory files to allow execution
await $`chmod +x ${apiDir}/dist/*.js`;
};
const buildTxz = async (validatedEnv: TxzEnv) => {
await validateSourceDir(validatedEnv);
const txzPath = join(validatedEnv.txzOutputDir, getTxzName());
// Create package - must be run from within the pre-pack directory
// Use cd option to run command from prePackDir
await cd(join(startingDir, "source", pluginName));
$.verbose = true;
await $`${join(startingDir, "scripts/makepkg")} --chown y --compress -${
validatedEnv.compress
} --linkadd y ${txzPath}`;
$.verbose = false;
await cd(startingDir);
};
const main = async () => {
try {
const validatedEnv = await setupTxzEnv(process.argv);
await cleanupTxzFiles();
await buildTxz(validatedEnv);
} catch (error) {
console.error(error);
process.exit(1);
}
};
await main();
@@ -0,0 +1,122 @@
import { z } from "zod";
import { access, constants, readFile } from "node:fs/promises";
import { Command } from "commander";
import { getStagingChangelogFromGit } from "../utils/changelog";
import { createHash } from "node:crypto";
import { getTxzPath } from "../utils/paths";
const safeParseEnvSchema = z.object({
ci: z.boolean().optional(),
baseUrl: z.string().url(),
tag: z.string().optional().default(''),
txzPath: z.string().refine((val) => val.endsWith(".txz"), {
message: "TXZ Path must end with .txz",
}),
pluginVersion: z.string().regex(/^\d{4}\.\d{2}\.\d{2}\.\d{4}$/, {
message: "Plugin version must be in the format YYYY.MM.DD.HHMM",
}),
releaseNotesPath: z.string().optional(),
});
const pluginEnvSchema = safeParseEnvSchema.extend({
releaseNotes: z.string().nonempty("Release notes are required"),
txzSha256: z.string().refine((val) => val.length === 64, {
message: "TXZ SHA256 must be 64 characters long",
}),
});
export type PluginEnv = z.infer<typeof pluginEnvSchema>;
export const validatePluginEnv = async (
envArgs: Record<string, any>
): Promise<PluginEnv> => {
const safeEnv = safeParseEnvSchema.parse(envArgs);
if (safeEnv.releaseNotesPath) {
await access(safeEnv.releaseNotesPath, constants.F_OK);
const releaseNotes = await readFile(safeEnv.releaseNotesPath, "utf8");
if (!releaseNotes || releaseNotes.length === 0) {
throw new Error(
`Release notes file is empty: ${safeEnv.releaseNotesPath}`
);
}
envArgs.releaseNotes = releaseNotes;
} else {
envArgs.releaseNotes =
process.env.TEST === "true"
? "FAST_TEST_CHANGELOG"
: await getStagingChangelogFromGit(safeEnv);
}
if (safeEnv.txzPath) {
await access(safeEnv.txzPath, constants.F_OK);
console.log("Reading txz file from:", safeEnv.txzPath);
const txzFile = await readFile(safeEnv.txzPath);
if (!txzFile || txzFile.length === 0) {
throw new Error(`TXZ Path is empty: ${safeEnv.txzPath}`);
}
envArgs.txzSha256 = getSha256(txzFile);
}
const validatedEnv = pluginEnvSchema.parse(envArgs);
if (validatedEnv.tag) {
console.warn("Tag is set, will generate a TAGGED build");
}
return validatedEnv;
};
export const getPluginVersion = () => {
const now = new Date();
const formatUtcComponent = (component: number) => String(component).padStart(2, '0');
const year = now.getUTCFullYear();
const month = formatUtcComponent(now.getUTCMonth() + 1);
const day = formatUtcComponent(now.getUTCDate());
const hour = formatUtcComponent(now.getUTCHours());
const minute = formatUtcComponent(now.getUTCMinutes());
const version = `${year}.${month}.${day}.${hour}${minute}`;
console.log("Plugin version:", version);
return version;
};
export const setupPluginEnv = async (argv: string[]): Promise<PluginEnv> => {
// CLI setup for plugin environment
const program = new Command();
program
.requiredOption(
"--base-url <url>",
"Base URL - will be used to determine the bucket, and combined with the tag (if set) to form the final URL",
process.env.CI === "true"
? "This is a CI build, please set the base URL manually"
: `http://${process.env.HOST_LAN_IP}:8080`
)
.option(
"--txz-path <path>",
"Path to built package, will be used to generate the SHA256 and renamed with the plugin version",
getTxzPath({ startingDir: process.cwd() })
)
.option(
"--plugin-version <version>",
"Plugin Version in the format YYYY.MM.DD.HHMM",
getPluginVersion()
)
.option("--tag <tag>", "Tag (used for PR and staging builds)", process.env.TAG)
.option("--release-notes-path <path>", "Path to release notes file")
.option("--ci", "CI mode", process.env.CI === "true")
.parse(argv);
const options = program.opts();
console.log("Options:", options);
const env = await validatePluginEnv(options);
console.log("Plugin environment setup successfully:", env);
return env;
};
function getSha256(txzBlob: Buffer): string {
return createHash("sha256").update(txzBlob).digest("hex");
}
@@ -0,0 +1,48 @@
import { join } from "path";
import { z } from "zod";
import { Command } from "commander";
import { startingDir } from "../utils/consts";
import { deployDir } from "../utils/paths";
const txzEnvSchema = z.object({
ci: z.boolean().optional().default(false),
skipValidation: z
.string()
.optional()
.default("false")
.refine((v) => v === "true" || v === "false", "Must be true or false"),
compress: z.string().optional().default("1"),
txzOutputDir: z.string().optional().default(join(startingDir, deployDir)),
});
export type TxzEnv = z.infer<typeof txzEnvSchema>;
export const validateTxzEnv = async (
envArgs: Record<string, any>
): Promise<TxzEnv> => {
const validatedEnv = txzEnvSchema.parse(envArgs);
if ("skipValidation" in validatedEnv) {
console.warn("skipValidation is true, skipping validation");
}
return validatedEnv;
};
export const setupTxzEnv = async (argv: string[]): Promise<TxzEnv> => {
// CLI setup for TXZ environment
const program = new Command();
program
.option("--skip-validation", "Skip validation", "false")
.option("--ci", "CI mode", process.env.CI === "true")
.option("--compress, -z", "Compress level", "1")
.parse(argv);
const options = program.opts();
const env = await validateTxzEnv(options);
console.log("TXZ environment setup successfully:", env);
return env;
};
+49
View File
@@ -0,0 +1,49 @@
import { getTxzName, LOCAL_BUILD_TAG, pluginNameWithExt } from "./consts";
// Define a common interface for URL parameters
interface UrlParams {
baseUrl: string;
tag?: string;
}
interface TxzUrlParams extends UrlParams {
pluginVersion: string;
}
/**
* Get the bucket path for the given tag
* ex. baseUrl = https://stable.dl.unraid.net/unraid-api
* ex. tag = PR123
* ex. returns = https://stable.dl.unraid.net/unraid-api/tag/PR123
*/
const getRootBucketPath = ({ baseUrl, tag }: UrlParams): URL => {
// Append tag to the baseUrl if tag is set, otherwise return the baseUrl
const url = new URL(baseUrl);
if (tag && tag !== LOCAL_BUILD_TAG) {
// Ensure the path ends with a trailing slash before adding the tag
url.pathname = url.pathname.replace(/\/?$/, "/") + "tag/" + tag;
}
return url;
};
/**
* Get the URL for the plugin file
* ex. returns = BASE_URL/TAG/dynamix.unraid.net.plg
*/
export const getPluginUrl = (params: UrlParams): string => {
const rootUrl = getRootBucketPath(params);
// Ensure the path ends with a slash and join with the plugin name
rootUrl.pathname = rootUrl.pathname.replace(/\/?$/, "/") + pluginNameWithExt;
return rootUrl.toString();
};
/**
* Get the URL for the main TXZ file
* ex. returns = BASE_URL/TAG/dynamix.unraid.net-4.1.3.txz
*/
export const getMainTxzUrl = (params: TxzUrlParams): string => {
const rootUrl = getRootBucketPath(params);
// Ensure the path ends with a slash and join with the txz name
rootUrl.pathname = rootUrl.pathname.replace(/\/?$/, "/") + getTxzName(params.pluginVersion);
return rootUrl.toString();
};
+39
View File
@@ -0,0 +1,39 @@
import conventionalChangelog from "conventional-changelog";
import { PluginEnv } from "../cli/setup-plugin-environment";
export const getStagingChangelogFromGit = async ({
pluginVersion,
tag,
}: Pick<PluginEnv, "pluginVersion" | "tag">): Promise<string> => {
try {
const changelogStream = conventionalChangelog(
{
preset: "conventionalcommits",
},
{
version: pluginVersion,
},
tag
? {
from: "origin/main",
to: "HEAD",
}
: {},
undefined,
tag
? {
headerPartial: `## [${tag}](https://github.com/unraid/api/${tag})\n\n`,
}
: undefined
);
let changelog = "";
for await (const chunk of changelogStream) {
changelog += chunk;
}
// Encode HTML entities using the 'he' library
return changelog ?? "";
} catch (err) {
throw new Error(`Failed to get changelog from git: ${err}`);
}
};
+18
View File
@@ -0,0 +1,18 @@
import { execSync } from "node:child_process";
import { deployDir } from "./paths";
import { mkdir } from "node:fs/promises";
import { startingDir } from "./consts";
import { join } from "node:path";
export const cleanupTxzFiles = async () => {
await mkdir(deployDir, { recursive: true });
const txzFiles = join(startingDir, deployDir, "*.txz");
await execSync(`rm -rf ${txzFiles}`);
};
export const cleanupPluginFiles = async () => {
await mkdir(deployDir, { recursive: true });
const pluginFiles = join(startingDir, deployDir, "*.plg");
await execSync(`rm -rf ${pluginFiles}`);
};
+13
View File
@@ -0,0 +1,13 @@
export const pluginName = "dynamix.unraid.net" as const;
export const pluginNameWithExt = `${pluginName}.plg` as const;
export const getTxzName = (version?: string) =>
version ? `${pluginName}-${version}.txz` : `${pluginName}.txz`;
export const startingDir = process.cwd();
export const BASE_URLS = {
STABLE: "https://stable.dl.unraid.net/unraid-api",
PREVIEW: "https://preview.dl.unraid.net/unraid-api",
} as const;
export const LOCAL_BUILD_TAG = "LOCAL_PLUGIN_BUILD" as const;
+43
View File
@@ -0,0 +1,43 @@
import { join } from "path";
import { getTxzName, pluginNameWithExt } from "./consts";
export interface PathConfig {
startingDir: string;
}
export interface TxzPathConfig extends PathConfig {
pluginVersion?: string;
}
export const deployDir = "deploy" as const;
/**
* Get the path to the root plugin directory
* @param startingDir - The starting directory
* @returns The path to the root plugin directory
*/
export function getRootPluginPath({ startingDir }: PathConfig): string {
return join(startingDir, "/plugins/", pluginNameWithExt);
}
/**
* Get the path to the deploy plugin directory
* @param startingDir - The starting directory
* @returns The path to the deploy plugin directory
*/
export function getDeployPluginPath({ startingDir }: PathConfig): string {
return join(startingDir, deployDir, pluginNameWithExt);
}
/**
* Get the path to the TXZ file
* @param startingDir - The starting directory
* @param pluginVersion - The plugin version
* @returns The path to the TXZ file
*/
export function getTxzPath({
startingDir,
pluginVersion,
}: TxzPathConfig): string {
return join(startingDir, deployDir, getTxzName(pluginVersion));
}