mirror of
https://github.com/awalsh128/cache-apt-pkgs-action.git
synced 2026-10-24 18:37:05 +00:00
docs: add examples and field docs in action module
This commit is contained in:
parent
effb2413be
commit
442edf5a7a
256
src/action.ts
256
src/action.ts
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue