Files
api/plugin/builder/cli/setup-plugin-environment.ts
Pujit Mehrotra 3cfe9fe9ee build: replace hash with build increment in slackware txz pkg (#1449)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced support for specifying and propagating a build number
throughout the build process, including command-line options and
workflow inputs.
* TXZ package naming and URLs now include the build number for improved
traceability.

* **Improvements**
* Enhanced robustness in locating TXZ files with improved fallback
logic, especially in CI environments.
* Improved flexibility and validation of environment schema for plugin
builds.

* **Style**
  * Minor formatting corrections for consistency and readability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210677942019563
2025-07-01 10:57:39 -04:00

228 lines
6.7 KiB
TypeScript

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";
import { existsSync } from "node:fs";
import { join } from "node:path";
import { baseEnvSchema, addCommonOptions } from "./common-environment";
const basePluginSchema = baseEnvSchema.extend({
txzPath: z
.string()
.refine((val) => val.endsWith(".txz"), {
message: "TXZ Path must end with .txz",
})
.optional(),
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 safeParseEnvSchema = basePluginSchema.transform((data) => ({
...data,
txzPath:
data.txzPath ||
getTxzPath({
startingDir: process.cwd(),
version: data.apiVersion,
build: data.buildNumber.toString(),
}),
}));
const pluginEnvSchema = basePluginSchema
.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",
}),
})
.transform((data) => ({
...data,
txzPath:
data.txzPath ||
getTxzPath({
startingDir: process.cwd(),
version: data.apiVersion,
build: data.buildNumber.toString(),
}),
}));
export type PluginEnv = z.infer<typeof pluginEnvSchema>;
/**
* Resolves the txz path, trying multiple possible locations based on apiVersion
* Also verifies the file exists and is accessible
* @param txzPath Initial txz path to check
* @param apiVersion API version to use for alternative path
* @param isCi Whether we're running in CI
* @returns Object containing the resolved txz path and SHA256 hash
* @throws Error if no valid txz file can be found
*/
export const resolveTxzPath = async (
txzPath: string,
apiVersion: string,
isCi?: boolean
): Promise<{ path: string; sha256: string }> => {
if (existsSync(txzPath)) {
await access(txzPath, constants.F_OK);
console.log("Reading txz file from:", txzPath);
const txzFile = await readFile(txzPath);
if (!txzFile || txzFile.length === 0) {
throw new Error(`TXZ file is empty: ${txzPath}`);
}
return {
path: txzPath,
sha256: getSha256(txzFile),
};
}
console.log(`TXZ path not found at: ${txzPath}`);
console.log(`Attempting to find TXZ using apiVersion: ${apiVersion}`);
// Try different formats of generated TXZ name
const deployDir = join(process.cwd(), "deploy");
// Try with exact apiVersion format
const alternativePaths = [
join(deployDir, `dynamix.unraid.net-${apiVersion}-x86_64-1.txz`),
];
// In CI, we sometimes see unusual filenames, so try a glob-like approach
if (isCi) {
console.log("Checking for possible TXZ files in deploy directory");
try {
// Using node's filesystem APIs to scan the directory
const fs = require("fs");
const deployFiles = fs.readdirSync(deployDir);
// Find any txz file that contains the apiVersion
for (const file of deployFiles) {
if (
file.endsWith(".txz") &&
file.includes("dynamix.unraid.net") &&
file.includes(apiVersion.split("+")[0])
) {
alternativePaths.push(join(deployDir, file));
}
}
} catch (error) {
console.log(`Error scanning deploy directory: ${error}`);
}
}
// Check each path
for (const path of alternativePaths) {
if (existsSync(path)) {
console.log(`Found TXZ at: ${path}`);
await access(path, constants.F_OK);
console.log("Reading txz file from:", path);
const txzFile = await readFile(path);
if (!txzFile || txzFile.length === 0) {
console.log(`TXZ file is empty: ${path}, trying next alternative`);
continue;
}
return {
path,
sha256: getSha256(txzFile),
};
}
console.log(`Could not find TXZ at: ${path}`);
}
// If we get here, we couldn't find a valid txz file
throw new Error(
`Could not find any valid TXZ file. Tried original path: ${txzPath} and alternatives.`
);
};
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);
}
// Resolve and validate the txz path
const { path, sha256 } = await resolveTxzPath(
safeEnv.txzPath,
safeEnv.apiVersion,
safeEnv.ci
);
envArgs.txzPath = path;
envArgs.txzSha256 = sha256;
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();
// Add common options
addCommonOptions(program);
// Add plugin-specific options
program
.option(
"--txz-path <path>",
"Path to built package, will be used to generate the SHA256 and renamed with the plugin version"
)
.option(
"--plugin-version <version>",
"Plugin Version in the format YYYY.MM.DD.HHMM",
getPluginVersion()
)
.option("--release-notes-path <path>", "Path to release notes file")
.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");
}