mirror of
https://github.com/awalsh128/cache-apt-pkgs-action.git
synced 2026-11-14 05:46:50 +00:00
* fix: find install scripts for package names with regex metacharacters findInstallScript built a regex from the raw package name, so names like g++ or libstdc++6 threw "Nothing to repeat" on cache save and on restore with install scripts, and a '.' in a name could match another package's script. Compare file names as plain strings instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf: speed up cache restores - Upload the diagnostic artifact only when debug is on or the action fails. The upload took ~1.4s of a ~3.7s restore in CI. - Extract all package archives in one sudo shell when install scripts are off, instead of a sudo and tar process per package. Per-package extraction stays when scripts run, to keep install ordering (#196). - Skip the apt-fast availability probe on a cache hit, where nothing is installed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: document Manifest and ManifestEntry public members Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
220 lines
7.3 KiB
TypeScript
220 lines
7.3 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { createPackageName } from "ts-apt";
|
|
import {
|
|
ActionPackageNames,
|
|
updateAptLists,
|
|
buildFileList,
|
|
findInstallScript,
|
|
} from "../src/packages.js";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
const PACKAGE_NAME = createPackageName("test-package", "1.0.0", "amd64");
|
|
|
|
function makeTempRoot(prefix: string): string {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
}
|
|
|
|
function ensureScriptsDir(root: string): string {
|
|
const scriptsDir = path.join(root, "var", "lib", "dpkg", "info");
|
|
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
return scriptsDir;
|
|
}
|
|
|
|
describe("packages", () => {
|
|
it("buildFileList returns an array of package files", async () => {
|
|
const root = makeTempRoot("pkg-files-");
|
|
const scriptsDir = ensureScriptsDir(root);
|
|
const packageFiles = [
|
|
path.join(scriptsDir, "test-package.list"),
|
|
path.join(scriptsDir, "test-package.md5sums"),
|
|
path.join(scriptsDir, "test-package.postinst"),
|
|
path.join(scriptsDir, "test-package.preinst"),
|
|
];
|
|
for (const filePath of packageFiles) {
|
|
fs.writeFileSync(filePath, "", "utf8");
|
|
}
|
|
|
|
const manager = {
|
|
listInstalledFiles: vi.fn().mockResolvedValue(packageFiles),
|
|
};
|
|
const result = await buildFileList(PACKAGE_NAME, manager as any, root);
|
|
|
|
expect(result).toEqual(packageFiles.map((item) => item.slice(1)).sort());
|
|
});
|
|
|
|
it("buildFileList returns an empty array when no files are found", async () => {
|
|
const root = makeTempRoot("pkg-empty-");
|
|
|
|
const manager = {
|
|
listInstalledFiles: vi.fn().mockResolvedValue([]),
|
|
};
|
|
|
|
const result = await buildFileList(PACKAGE_NAME, manager as any, root);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("findInstallScript returns undefined when scripts directory does not exist", async () => {
|
|
const missingRoot = makeTempRoot("pkg-missing-");
|
|
|
|
const result = await findInstallScript(
|
|
PACKAGE_NAME,
|
|
"preinst",
|
|
missingRoot,
|
|
);
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("findInstallScript returns undefined when no matching scripts are found", async () => {
|
|
const root = makeTempRoot("pkg-no-match-");
|
|
const scriptsDir = ensureScriptsDir(root);
|
|
fs.writeFileSync(
|
|
path.join(scriptsDir, "other-package.preinst"),
|
|
"",
|
|
"utf8",
|
|
);
|
|
|
|
const result = await findInstallScript(PACKAGE_NAME, "preinst", root);
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("findInstallScript returns the first matching script when multiple are found", async () => {
|
|
const root = makeTempRoot("pkg-match-");
|
|
const scriptsDir = ensureScriptsDir(root);
|
|
fs.writeFileSync(
|
|
path.join(scriptsDir, "test-package.postinst"),
|
|
"",
|
|
"utf8",
|
|
);
|
|
fs.writeFileSync(path.join(scriptsDir, "test-package.preinst"), "", "utf8");
|
|
fs.writeFileSync(
|
|
path.join(scriptsDir, "test-package.preinst.backup"),
|
|
"",
|
|
"utf8",
|
|
);
|
|
|
|
const result = await findInstallScript(PACKAGE_NAME, "preinst", root);
|
|
expect(result).toBe(path.join(scriptsDir, "test-package.preinst"));
|
|
});
|
|
|
|
it.each([
|
|
["g++", "g++.postinst"],
|
|
["libstdc++6", "libstdc++6:amd64.postinst"],
|
|
])(
|
|
"findInstallScript name %s with regex metacharacters returns its script",
|
|
async (name, scriptFilename) => {
|
|
const root = makeTempRoot("pkg-metachar-");
|
|
const scriptsDir = ensureScriptsDir(root);
|
|
fs.writeFileSync(path.join(scriptsDir, scriptFilename), "", "utf8");
|
|
|
|
const result = await findInstallScript(
|
|
createPackageName(name),
|
|
"postinst",
|
|
root,
|
|
);
|
|
expect(result).toBe(path.join(scriptsDir, scriptFilename));
|
|
},
|
|
);
|
|
|
|
it("findInstallScript name with dot does not match a similarly named package", async () => {
|
|
const root = makeTempRoot("pkg-dot-");
|
|
const scriptsDir = ensureScriptsDir(root);
|
|
fs.writeFileSync(path.join(scriptsDir, "libaXb.postinst"), "", "utf8");
|
|
|
|
const result = await findInstallScript(
|
|
createPackageName("liba.b"),
|
|
"postinst",
|
|
root,
|
|
);
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("updateAptLists calls the package manager's update method", async () => {
|
|
const aptListsPath = makeTempRoot("apt-lists-");
|
|
|
|
const mockUpdate = vi.fn().mockResolvedValue(undefined);
|
|
const mockLogger = {
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
};
|
|
const packageManager = {
|
|
update: mockUpdate,
|
|
};
|
|
|
|
await updateAptLists(
|
|
packageManager as any,
|
|
mockLogger as any,
|
|
aptListsPath,
|
|
);
|
|
|
|
expect(mockUpdate).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
describe("ActionPackageNames", () => {
|
|
it("fromInput parses APT serialized input", () => {
|
|
const input = "test-package=1.0.0 amd64 other-package=2.0.0 i386";
|
|
const result = ActionPackageNames.fromInput(input);
|
|
expect(result.toArray()).toEqual([
|
|
createPackageName("other-package", "2.0.0", "i386"),
|
|
createPackageName("test-package", "1.0.0", "amd64"),
|
|
]);
|
|
});
|
|
|
|
it("fromInput returns an empty list when input is empty", () => {
|
|
const result = ActionPackageNames.fromInput("");
|
|
expect(result.toArray()).toEqual([]);
|
|
});
|
|
|
|
it("fromInput returns an empty list when input is whitespace", () => {
|
|
const result = ActionPackageNames.fromInput(" ");
|
|
expect(result.toArray()).toEqual([]);
|
|
});
|
|
|
|
it("fromJSON parses an array of serialized package names", () => {
|
|
const json = [
|
|
{ name: "test-package", version: "1.0.0", arch: "amd64" },
|
|
{ name: "other-package", version: "2.0.0", arch: "i386" },
|
|
];
|
|
const result = ActionPackageNames.fromJSON(json);
|
|
expect(result.toArray()).toEqual([
|
|
createPackageName("other-package", "2.0.0", "i386"),
|
|
createPackageName("test-package", "1.0.0", "amd64"),
|
|
]);
|
|
});
|
|
|
|
it("fromJSON returns an empty list when input is empty", () => {
|
|
const result = ActionPackageNames.fromJSON([]);
|
|
expect(result.toArray()).toEqual([]);
|
|
});
|
|
|
|
it("length returns the number of package names", () => {
|
|
const input = "test-package=1.0.0 amd64 other-package=2.0.0 i386";
|
|
const result = ActionPackageNames.fromInput(input);
|
|
expect(result.length).toBe(2);
|
|
});
|
|
|
|
it("toArray returns a sorted array of package names", () => {
|
|
const input = "test-package=1.0.0 amd64 other-package=2.0.0 i386";
|
|
const result = ActionPackageNames.fromInput(input);
|
|
expect(result.toArray()).toEqual([
|
|
createPackageName("other-package", "2.0.0", "i386"),
|
|
createPackageName("test-package", "1.0.0", "amd64"),
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
["same name without arch returns true", "xdot", createPackageName("xdot", "1.3-1"), true],
|
|
["requested without arch returns true", "xdot", createPackageName("xdot", "1.3-1", "amd64"), true],
|
|
["installed without arch returns true", "xdot:amd64", createPackageName("xdot", "1.3-1"), true],
|
|
["matching arch returns true", "xdot:amd64", createPackageName("xdot", "1.3-1", "amd64"), true],
|
|
["different arch returns false", "xdot:i386", createPackageName("xdot", "1.3-1", "amd64"), false],
|
|
["different name returns false", "xdot", createPackageName("rolldice", "1.16"), false],
|
|
])("includes %s", (_case, input, installed, expected) => {
|
|
expect(ActionPackageNames.fromInput(input).includes(installed)).toBe(
|
|
expected,
|
|
);
|
|
});
|
|
});
|
|
});
|