mirror of
https://github.com/unraid/api.git
synced 2025-12-31 05:29:48 -06:00
<!-- 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
228 lines
6.7 KiB
TypeScript
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");
|
|
}
|