From 442edf5a7a19da4b6d84dfa3da0523446d14eb71 Mon Sep 17 00:00:00 2001 From: Andrew Walsh Date: Sat, 4 Jul 2026 11:24:27 -0700 Subject: [PATCH] docs: add examples and field docs in action module --- src/action.ts | 256 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) diff --git a/src/action.ts b/src/action.ts index 7a5b204..d0366e9 100644 --- a/src/action.ts +++ b/src/action.ts @@ -15,40 +15,108 @@ import { readManifestAsCsv, writeManifest } from "./manifest.js"; import * as tar from "tar"; import winston from "winston"; +/** + * Tar module contract used by {@link ActionRunner}. + * + * @example + * const tarModule = await import("tar"); + */ type TarModule = typeof import("tar"); +/** + * Strategy for handling empty package inputs. + * + * @example + * const behavior: EmptyPackageBehavior = "warn"; + */ type EmptyPackageBehavior = "error" | "warn" | "ignore"; const FORCE_UPDATE_INCREMENT = "4"; const CACHE_DIRNAME = "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 { + /** Raw package input from action YAML. */ readonly packages: string; + /** User-controlled cache salt/version. */ readonly version: string; + /** Whether preinst/postinst scripts are executed on restore. */ readonly executeInstallScripts: boolean; + /** Behavior when normalized package input is empty. */ readonly emptyPackagesBehavior: EmptyPackageBehavior; + /** Enables verbose logging when true. */ 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 { + /** True when cache restore key exactly matches requested key. */ readonly cacheHit: boolean; + /** CSV list of requested package versions. */ readonly packageVersionList: string; + /** CSV list of all installed package versions including dependencies. */ 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 { constructor( + /** Package name, for example "curl". */ readonly name: string, + /** Optional version pin, for example "8.5.0". */ readonly version?: string, + /** Optional distro qualifier used by ts-apt types. */ readonly distro?: string, ) {} + /** + * Serializes the package descriptor for ts-apt APIs. + * + * @returns Serialized package specifier, including version when provided. + */ serialize(): string { 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 { const [name, version] = packageSpecifier.split("="); if (!name) { @@ -58,6 +126,17 @@ function toPackageName(packageSpecifier: string): ActionPackageName { 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 { if (value === "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[] { return inputPackages .replace(/[,\\]/g, " ") @@ -82,9 +171,19 @@ export function normalizeInputPackages(inputPackages: string): string[] { .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 { + /** Shell command runner used for architecture and script execution. */ private readonly commandRunner: CommandRunner; + /** Tar adapter used for archive create/extract operations. */ private readonly tar: TarModule; + /** Logger used for restore/install script tracing. */ private readonly logger: winston.Logger; constructor( @@ -97,14 +196,44 @@ export class ActionRunner { 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 { 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[] { 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( packageManager: PackageManager, packageName: string, @@ -122,6 +251,16 @@ export class ActionRunner { 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( packageManager: PackageManager, inputPackages: string, @@ -140,6 +279,17 @@ export class ActionRunner { 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( behavior: EmptyPackageBehavior, packages: string[], @@ -160,10 +310,28 @@ export class ActionRunner { 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 { 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( normalizedPackages: string[], version: string, @@ -179,6 +347,17 @@ export class ActionRunner { 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( packageName: string, extension: "preinst" | "postinst", @@ -202,10 +381,29 @@ export class ActionRunner { 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 { 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( packageManager: PackageManager, packageName: string, @@ -236,6 +434,15 @@ export class ActionRunner { 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 { if (isAptListsFresh()) { return; @@ -244,10 +451,30 @@ export class ActionRunner { 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 { 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( cacheDir: string, packages: string[], @@ -296,6 +523,16 @@ export class ActionRunner { 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( cacheDir: string, 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 { if (/\s/.test(inputs.version)) { 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( inputs: ActionInputs, logger: winston.Logger,