Files
api/plugin/builder/cli/setup-plugin-environment.ts
Eli Bosley 4f63b4cf3b feat: native slackware package (#1381)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added detailed versioning for plugin packages incorporating
architecture and build identifiers.
- Simplified and improved install/uninstall scripts with backup and
dynamic package detection.
- Introduced comprehensive setup, verification, patching, and cleanup
scripts for the Unraid API environment.
- Enhanced service control with explicit start, stop, restart, and
status commands.
- Added robust dependency management scripts for restoring and archiving
Node.js modules.
- Implemented vendor archive metadata storage and dynamic handling
during build and runtime.
- Added new CLI options and environment schemas for consistent build
configuration.
- Introduced new shutdown scripts to gracefully stop flash-backup and
unraid-api services.
- Added utility scripts for API version detection and vendor archive
configuration.
- Added a new package description file detailing Unraid API features and
homepage link.

- **Bug Fixes**
- Improved validation and error reporting for missing manifests,
dependencies, and configuration files.
  - Enhanced fallback logic for locating and creating vendor archives.
- Fixed iframe compatibility in UI by updating HTML and Firefox
preference files.

- **Chores**
- Updated .gitignore with generated file patterns for Node.js binaries
and archives.
  - Removed obsolete internal documentation and legacy cleanup scripts.
- Refined Docker Compose and CI workflows to pass precise API versioning
and manage build artifacts.
- Centralized common environment validation and CLI option definitions
across build tools.
- Cleaned up plugin manifest by removing Node.js and PNPM-related
entities and legacy logic.
- Improved logging and error handling in build and installation scripts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-08 22:54:10 -04:00

191 lines
6.3 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 safeParseEnvSchema = baseEnvSchema.extend({
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>;
/**
* 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",
getTxzPath({ startingDir: process.cwd(), pluginVersion: process.env.API_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");
}