docs: add examples and field docs in action module

This commit is contained in:
Andrew Walsh 2026-07-04 11:24:27 -07:00
parent effb2413be
commit 442edf5a7a

View file

@ -15,40 +15,108 @@ import { readManifestAsCsv, writeManifest } from "./manifest.js";
import * as tar from "tar"; import * as tar from "tar";
import winston from "winston"; import winston from "winston";
/**
* Tar module contract used by {@link ActionRunner}.
*
* @example
* const tarModule = await import("tar");
*/
type TarModule = typeof import("tar"); type TarModule = typeof import("tar");
/**
* Strategy for handling empty package inputs.
*
* @example
* const behavior: EmptyPackageBehavior = "warn";
*/
type EmptyPackageBehavior = "error" | "warn" | "ignore"; type EmptyPackageBehavior = "error" | "warn" | "ignore";
const FORCE_UPDATE_INCREMENT = "4"; const FORCE_UPDATE_INCREMENT = "4";
const CACHE_DIRNAME = "cache-apt-pkgs"; const CACHE_DIRNAME = "cache-apt-pkgs";
const CACHE_PREFIX = "cache-apt-pkgs_"; const CACHE_PREFIX = "cache-apt-pkgs_";
/**
* Inputs accepted by the GitHub Action runtime.
*
* @example
* const inputs: ActionInputs = {
* packages: "curl git",
* version: "v1",
* executeInstallScripts: false,
* emptyPackagesBehavior: "error",
* debug: false,
* };
*/
export interface ActionInputs { export interface ActionInputs {
/** Raw package input from action YAML. */
readonly packages: string; readonly packages: string;
/** User-controlled cache salt/version. */
readonly version: string; readonly version: string;
/** Whether preinst/postinst scripts are executed on restore. */
readonly executeInstallScripts: boolean; readonly executeInstallScripts: boolean;
/** Behavior when normalized package input is empty. */
readonly emptyPackagesBehavior: EmptyPackageBehavior; readonly emptyPackagesBehavior: EmptyPackageBehavior;
/** Enables verbose logging when true. */
readonly debug: boolean; readonly debug: boolean;
} }
/**
* Outputs emitted by the GitHub Action runtime.
*
* @example
* const outputs: ActionOutputs = {
* cacheHit: true,
* packageVersionList: "curl=8.5.0",
* allPackageVersionList: "curl=8.5.0,libcurl4=8.5.0",
* };
*/
export interface ActionOutputs { export interface ActionOutputs {
/** True when cache restore key exactly matches requested key. */
readonly cacheHit: boolean; readonly cacheHit: boolean;
/** CSV list of requested package versions. */
readonly packageVersionList: string; readonly packageVersionList: string;
/** CSV list of all installed package versions including dependencies. */
readonly allPackageVersionList: string; readonly allPackageVersionList: string;
} }
/**
* Serializable package specifier understood by ts-apt.
*
* @example
* const p = new ActionPackageName("curl", "8.5.0");
* const value = p.serialize(); // curl=8.5.0
*/
export class ActionPackageName implements PackageName { export class ActionPackageName implements PackageName {
constructor( constructor(
/** Package name, for example "curl". */
readonly name: string, readonly name: string,
/** Optional version pin, for example "8.5.0". */
readonly version?: string, readonly version?: string,
/** Optional distro qualifier used by ts-apt types. */
readonly distro?: string, readonly distro?: string,
) {} ) {}
/**
* Serializes the package descriptor for ts-apt APIs.
*
* @returns Serialized package specifier, including version when provided.
*/
serialize(): string { serialize(): string {
return this.version ? `${this.name}=${this.version}` : this.name; return this.version ? `${this.name}=${this.version}` : this.name;
} }
} }
/**
* Converts a user package specifier into a typed package descriptor.
*
* @param packageSpecifier Specifier in name or name=version format.
* @returns Typed package descriptor.
* @throws Error when package name is empty.
*
* @example
* const pkg = toPackageName("curl=8.5.0");
* const value = pkg.serialize(); // curl=8.5.0
*/
function toPackageName(packageSpecifier: string): ActionPackageName { function toPackageName(packageSpecifier: string): ActionPackageName {
const [name, version] = packageSpecifier.split("="); const [name, version] = packageSpecifier.split("=");
if (!name) { if (!name) {
@ -58,6 +126,17 @@ function toPackageName(packageSpecifier: string): ActionPackageName {
return new ActionPackageName(name, version); return new ActionPackageName(name, version);
} }
/**
* Parses a strict boolean string used by Action inputs.
*
* @param value Raw input value.
* @param fieldName Input field name used in validation errors.
* @returns Parsed boolean value.
* @throws Error when value is not "true" or "false".
*
* @example
* const debug = parseBoolean("true", "debug"); // true
*/
export function parseBoolean(value: string, fieldName: string): boolean { export function parseBoolean(value: string, fieldName: string): boolean {
if (value === "true") { if (value === "true") {
return true; return true;
@ -71,6 +150,16 @@ export function parseBoolean(value: string, fieldName: string): boolean {
); );
} }
/**
* Normalizes package input by splitting on comma, backslash, and whitespace.
*
* @param inputPackages Raw package input from action configuration.
* @returns Sorted list of normalized package specifiers.
*
* @example
* const packages = normalizeInputPackages("git, curl\\vim");
* // ["curl", "git", "vim"]
*/
export function normalizeInputPackages(inputPackages: string): string[] { export function normalizeInputPackages(inputPackages: string): string[] {
return inputPackages return inputPackages
.replace(/[,\\]/g, " ") .replace(/[,\\]/g, " ")
@ -82,9 +171,19 @@ export function normalizeInputPackages(inputPackages: string): string[] {
.sort((a, b) => a.localeCompare(b)); .sort((a, b) => a.localeCompare(b));
} }
/**
* Orchestrates package normalization, cache restore/save, install, and outputs.
*
* @example
* const runner = new ActionRunner(commandRunner, tar, logger);
* const outputs = await runner.runAction(inputs);
*/
export class ActionRunner { export class ActionRunner {
/** Shell command runner used for architecture and script execution. */
private readonly commandRunner: CommandRunner; private readonly commandRunner: CommandRunner;
/** Tar adapter used for archive create/extract operations. */
private readonly tar: TarModule; private readonly tar: TarModule;
/** Logger used for restore/install script tracing. */
private readonly logger: winston.Logger; private readonly logger: winston.Logger;
constructor( constructor(
@ -97,14 +196,44 @@ export class ActionRunner {
this.logger = logger; this.logger = logger;
} }
/**
* Instance wrapper over {@link parseBoolean} for easier unit testing.
*
* @param value Raw input value.
* @param fieldName Input field name used in validation errors.
* @returns Parsed boolean value.
*
* @example
* const enabled = runner.parseBoolean("false", "debug");
*/
parseBoolean(value: string, fieldName: string): boolean { parseBoolean(value: string, fieldName: string): boolean {
return parseBoolean(value, fieldName); return parseBoolean(value, fieldName);
} }
/**
* Instance wrapper over {@link normalizeInputPackages}.
*
* @param inputPackages Raw package input from action configuration.
* @returns Sorted list of normalized package specifiers.
*
* @example
* const normalized = runner.normalizeInputPackages("curl git");
*/
normalizeInputPackages(inputPackages: string): string[] { normalizeInputPackages(inputPackages: string): string[] {
return normalizeInputPackages(inputPackages); return normalizeInputPackages(inputPackages);
} }
/**
* Resolves a concrete version for an unpinned package name.
*
* @param packageManager ts-apt package manager instance used for metadata lookup.
* @param packageName Package name without a version pin.
* @returns Resolved package version.
* @throws Error when no version can be resolved.
*
* @example
* const version = await runner.resolvePackageVersion(manager, "curl");
*/
async resolvePackageVersion( async resolvePackageVersion(
packageManager: PackageManager, packageManager: PackageManager,
packageName: string, packageName: string,
@ -122,6 +251,16 @@ export class ActionRunner {
return version; return version;
} }
/**
* Ensures each package has a pinned version to make cache behavior deterministic.
*
* @param packageManager ts-apt package manager instance used for version resolution.
* @param inputPackages Raw package input string.
* @returns Sorted list of package specifiers in name=version form.
*
* @example
* const packages = await runner.normalizePackagesWithVersions(manager, "curl git=2.39");
*/
async normalizePackagesWithVersions( async normalizePackagesWithVersions(
packageManager: PackageManager, packageManager: PackageManager,
inputPackages: string, inputPackages: string,
@ -140,6 +279,17 @@ export class ActionRunner {
return packages.sort((a, b) => a.localeCompare(b)); return packages.sort((a, b) => a.localeCompare(b));
} }
/**
* Handles behavior when package input resolves to an empty list.
*
* @param behavior Empty-package handling strategy.
* @param packages Normalized package list.
* @returns Nothing.
* @throws Error when behavior is "error" and packages is empty.
*
* @example
* runner.validateEmptyPackages("warn", []);
*/
validateEmptyPackages( validateEmptyPackages(
behavior: EmptyPackageBehavior, behavior: EmptyPackageBehavior,
packages: string[], packages: string[],
@ -160,10 +310,28 @@ export class ActionRunner {
throw new Error("Packages argument is empty."); throw new Error("Packages argument is empty.");
} }
/**
* Returns the local cache root used by this action.
*
* @returns Absolute cache directory path.
*
* @example
* const cacheRoot = runner.getCacheRoot();
*/
getCacheRoot(): string { getCacheRoot(): string {
return path.join(os.homedir(), CACHE_DIRNAME); return path.join(os.homedir(), CACHE_DIRNAME);
} }
/**
* Builds a stable cache key from packages, user version, force bump, and arch.
*
* @param normalizedPackages Sorted package specifiers.
* @param version User-provided cache version salt.
* @returns Cache key with action-specific prefix.
*
* @example
* const key = await runner.getCacheKey(["curl=8.5.0"], "v1");
*/
async getCacheKey( async getCacheKey(
normalizedPackages: string[], normalizedPackages: string[],
version: string, version: string,
@ -179,6 +347,17 @@ export class ActionRunner {
return `${CACHE_PREFIX}${hash}`; return `${CACHE_PREFIX}${hash}`;
} }
/**
* Finds dpkg lifecycle scripts for a package when present.
*
* @param packageName Package name.
* @param extension Script extension to resolve.
* @param root Root filesystem path used to resolve dpkg metadata.
* @returns Absolute script path when found.
*
* @example
* const preinst = runner.findInstallScript("curl", "preinst", "/");
*/
findInstallScript( findInstallScript(
packageName: string, packageName: string,
extension: "preinst" | "postinst", extension: "preinst" | "postinst",
@ -202,10 +381,29 @@ export class ActionRunner {
return path.join(scriptsDir, candidate); return path.join(scriptsDir, candidate);
} }
/**
* Converts absolute paths to tar-relative paths.
*
* @param filePath Absolute or relative file path.
* @returns Path relative to tar root.
*
* @example
* const relative = runner.tarRelativePath("/usr/bin/curl"); // usr/bin/curl
*/
tarRelativePath(filePath: string): string { tarRelativePath(filePath: string): string {
return filePath.startsWith("/") ? filePath.slice(1) : filePath; return filePath.startsWith("/") ? filePath.slice(1) : filePath;
} }
/**
* Collects installed file and lifecycle script paths for archive creation.
*
* @param packageManager ts-apt package manager instance.
* @param packageName Package name.
* @returns Sorted unique file list suitable for tar archiving.
*
* @example
* const files = await runner.buildFileListForPackage(manager, "curl");
*/
async buildFileListForPackage( async buildFileListForPackage(
packageManager: PackageManager, packageManager: PackageManager,
packageName: string, packageName: string,
@ -236,6 +434,15 @@ export class ActionRunner {
return Array.from(new Set(files)).sort((a, b) => a.localeCompare(b)); return Array.from(new Set(files)).sort((a, b) => a.localeCompare(b));
} }
/**
* Updates apt lists only when the local lists directory appears stale.
*
* @param packageManager ts-apt package manager instance.
* @returns Nothing.
*
* @example
* await runner.updateAptLists(manager);
*/
async updateAptLists(packageManager: PackageManager): Promise<void> { async updateAptLists(packageManager: PackageManager): Promise<void> {
if (isAptListsFresh()) { if (isAptListsFresh()) {
return; return;
@ -244,10 +451,30 @@ export class ActionRunner {
await packageManager.update(); await packageManager.update();
} }
/**
* Converts a versioned specifier like "pkg=1.2" into "pkg".
*
* @param packageSpecifier Versioned or unversioned package specifier.
* @returns Package name only.
*
* @example
* const name = runner.packageSpecifierToName("curl=8.5.0"); // curl
*/
packageSpecifierToName(packageSpecifier: string): string { packageSpecifierToName(packageSpecifier: string): string {
return packageSpecifier.split("=")[0] ?? packageSpecifier; return packageSpecifier.split("=")[0] ?? packageSpecifier;
} }
/**
* Installs packages, archives installed files, and writes manifest files.
*
* @param cacheDir Cache directory path.
* @param packages Package names to install.
* @param packageManager ts-apt package manager instance configured for install.
* @returns Nothing.
*
* @example
* await runner.installAndCachePackages("/tmp/cache", ["curl"], installManager);
*/
async installAndCachePackages( async installAndCachePackages(
cacheDir: string, cacheDir: string,
packages: string[], packages: string[],
@ -296,6 +523,16 @@ export class ActionRunner {
writeManifest(path.join(cacheDir, "manifest_all.log"), manifestAll); writeManifest(path.join(cacheDir, "manifest_all.log"), manifestAll);
} }
/**
* Restores archived files and optionally executes package install scripts.
*
* @param cacheDir Cache directory path.
* @param executeInstallScripts Whether to execute preinst/postinst scripts.
* @returns Nothing.
*
* @example
* await runner.restorePackages("/tmp/cache", false);
*/
async restorePackages( async restorePackages(
cacheDir: string, cacheDir: string,
executeInstallScripts: boolean, executeInstallScripts: boolean,
@ -341,6 +578,15 @@ export class ActionRunner {
} }
} }
/**
* Executes the end-to-end action flow and returns action outputs.
*
* @param inputs Validated action inputs.
* @returns Action outputs consumed by the workflow runtime.
*
* @example
* const outputs = await runner.runAction(inputs);
*/
async runAction(inputs: ActionInputs): Promise<ActionOutputs> { async runAction(inputs: ActionInputs): Promise<ActionOutputs> {
if (/\s/.test(inputs.version)) { if (/\s/.test(inputs.version)) {
throw new Error( throw new Error(
@ -408,6 +654,16 @@ export class ActionRunner {
} }
} }
/**
* Public entrypoint used by src/index.ts and tests.
*
* @param inputs Validated action inputs.
* @param logger Logger instance used for command and restore diagnostics.
* @returns Action outputs consumed by the workflow runtime.
*
* @example
* const outputs = await runAction(inputs, logger);
*/
export async function runAction( export async function runAction(
inputs: ActionInputs, inputs: ActionInputs,
logger: winston.Logger, logger: winston.Logger,