mirror of
https://github.com/awalsh128/cache-apt-pkgs-action.git
synced 2026-11-13 21:36:56 +00:00
Merge branch 'main-2' into build/bump-ts-apt-v1.1.0-rc.1
This commit is contained in:
commit
eeb6a4d9b1
|
|
@ -1,6 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
# Blocks pushes while ts-apt is locally linked or not pinned to a commit SHA.
|
||||
# Installed by the npm "prepare" script via core.hooksPath.
|
||||
# Blocks pushes while a pinned dependency (e.g. ts-apt) is locally linked or
|
||||
# not pinned to a commit SHA. Installed by the npm "prepare" script via
|
||||
# core.hooksPath.
|
||||
set -euo pipefail
|
||||
|
||||
exec "$(git rev-parse --show-toplevel)/scripts/ops/check_tsapt_pin.sh"
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
exec npm run -s check:deps-pin
|
||||
|
|
|
|||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
|
@ -125,9 +125,9 @@ jobs:
|
|||
run: npm run check:latest-action-pins
|
||||
|
||||
# Fails if ts-apt is linked locally or not pinned to a pushed commit SHA.
|
||||
- name: Check ts-apt Pin
|
||||
- name: Check Dependency Pins
|
||||
shell: bash
|
||||
run: npm run check:tsapt-pin
|
||||
run: npm run check:deps-pin
|
||||
|
||||
testing:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ You are an expert developer focused on TypeScript, Bash, DevOps, Git, and GitHub
|
|||
- `package.json` pins ts-apt to a commit SHA; local ts-apt changes are not visible to CI.
|
||||
- MUST push the ts-apt change first, then bump the ts-apt SHA pin in the same change that uses it.
|
||||
- NEVER push action code that depends on an unpushed ts-apt change.
|
||||
- `npm run check:tsapt-pin` enforces the pin; it runs in CI and in the `.githooks/pre-push` hook installed by `npm install`.
|
||||
- `npm run check:deps-pin` enforces the pin; it runs in CI and in the `.githooks/pre-push` hook installed by `npm install`.
|
||||
- NEVER bypass the pre-push hook with `--no-verify`; unlink ts-apt instead.
|
||||
|
||||
## Quick Commands
|
||||
|
|
|
|||
53
dist/action.d.ts
vendored
53
dist/action.d.ts
vendored
|
|
@ -1,53 +0,0 @@
|
|||
import { type PackageManager, type CommandRunner } from "../../ts-apt/dist/index.js";
|
||||
import winston from "winston";
|
||||
type TarModule = typeof import("tar");
|
||||
type EmptyPackageBehavior = "error" | "warn" | "ignore";
|
||||
interface PackageName {
|
||||
readonly name: string;
|
||||
readonly version?: string;
|
||||
readonly distro?: string;
|
||||
serialize(): string;
|
||||
}
|
||||
export interface ActionInputs {
|
||||
readonly packages: string;
|
||||
readonly version: string;
|
||||
readonly executeInstallScripts: boolean;
|
||||
readonly emptyPackagesBehavior: EmptyPackageBehavior;
|
||||
readonly debug: boolean;
|
||||
}
|
||||
export interface ActionOutputs {
|
||||
readonly cacheHit: boolean;
|
||||
readonly packageVersionList: string;
|
||||
readonly allPackageVersionList: string;
|
||||
}
|
||||
export declare class ActionPackageName implements PackageName {
|
||||
readonly name: string;
|
||||
readonly version?: string | undefined;
|
||||
constructor(name: string, version?: string | undefined);
|
||||
serialize(): string;
|
||||
}
|
||||
export declare function parseBoolean(value: string, fieldName: string): boolean;
|
||||
export declare function normalizeInputPackages(inputPackages: string): string[];
|
||||
export declare class ActionRunner {
|
||||
private readonly commandRunner;
|
||||
private readonly tar;
|
||||
private readonly logger;
|
||||
constructor(commandRunner: CommandRunner, tarModule: TarModule, logger: winston.Logger);
|
||||
parseBoolean(value: string, fieldName: string): boolean;
|
||||
normalizeInputPackages(inputPackages: string): string[];
|
||||
resolvePackageVersion(packageManager: PackageManager, packageName: string): Promise<string>;
|
||||
normalizePackagesWithVersions(packageManager: PackageManager, inputPackages: string): Promise<string[]>;
|
||||
validateEmptyPackages(behavior: EmptyPackageBehavior, packages: string[]): void;
|
||||
getCacheRoot(): string;
|
||||
getCacheKey(normalizedPackages: string[], version: string): Promise<string>;
|
||||
findInstallScript(packageName: string, extension: "preinst" | "postinst", root: string): string | undefined;
|
||||
tarRelativePath(filePath: string): string;
|
||||
buildFileListForPackage(packageManager: PackageManager, packageName: string): Promise<string[]>;
|
||||
updateAptLists(packageManager: PackageManager): Promise<void>;
|
||||
packageSpecifierToName(packageSpecifier: string): string;
|
||||
installAndCachePackages(cacheDir: string, packages: string[], packageManager: PackageManager): Promise<void>;
|
||||
restorePackages(cacheDir: string, executeInstallScripts: boolean): Promise<void>;
|
||||
runAction(inputs: ActionInputs): Promise<ActionOutputs>;
|
||||
}
|
||||
export declare function runAction(inputs: ActionInputs, logger: winston.Logger): Promise<ActionOutputs>;
|
||||
export {};
|
||||
265
dist/action.js
vendored
265
dist/action.js
vendored
|
|
@ -1,265 +0,0 @@
|
|||
import * as cache from "@actions/cache";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { createPackageManager, DefaultCommandRunner, } from "../../ts-apt/dist/index.js";
|
||||
import { isAptListsFresh } from "./io.js";
|
||||
import { readManifestAsCsv, writeManifest } from "./manifest.js";
|
||||
import * as tar from "tar";
|
||||
const FORCE_UPDATE_INCREMENT = "3";
|
||||
const CACHE_DIRNAME = "cache-apt-pkgs";
|
||||
const CACHE_PREFIX = "cache-apt-pkgs_";
|
||||
export class ActionPackageName {
|
||||
name;
|
||||
version;
|
||||
constructor(name, version) {
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
}
|
||||
serialize() {
|
||||
return this.version ? `${this.name}=${this.version}` : this.name;
|
||||
}
|
||||
}
|
||||
function toPackageName(packageSpecifier) {
|
||||
const [name, version] = packageSpecifier.split("=");
|
||||
if (!name) {
|
||||
throw new Error("Package name cannot be empty.");
|
||||
}
|
||||
return new ActionPackageName(name, version);
|
||||
}
|
||||
export function parseBoolean(value, fieldName) {
|
||||
if (value === "true") {
|
||||
return true;
|
||||
}
|
||||
if (value === "false") {
|
||||
return false;
|
||||
}
|
||||
throw new Error(`${fieldName} value '${value}' must be either true or false.`);
|
||||
}
|
||||
export function normalizeInputPackages(inputPackages) {
|
||||
return inputPackages
|
||||
.replace(/[,\\]/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.split(" ")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
export class ActionRunner {
|
||||
commandRunner;
|
||||
tar;
|
||||
logger;
|
||||
constructor(commandRunner, tarModule, logger) {
|
||||
this.commandRunner = commandRunner;
|
||||
this.tar = tarModule;
|
||||
this.logger = logger;
|
||||
}
|
||||
parseBoolean(value, fieldName) {
|
||||
return parseBoolean(value, fieldName);
|
||||
}
|
||||
normalizeInputPackages(inputPackages) {
|
||||
return normalizeInputPackages(inputPackages);
|
||||
}
|
||||
async resolvePackageVersion(packageManager, packageName) {
|
||||
const packageInfo = await packageManager.getPackageInfo([
|
||||
toPackageName(packageName),
|
||||
]);
|
||||
const version = packageInfo[0]?.version;
|
||||
if (!version) {
|
||||
throw new Error(`Unable to resolve package version for '${packageName}'.`);
|
||||
}
|
||||
return version;
|
||||
}
|
||||
async normalizePackagesWithVersions(packageManager, inputPackages) {
|
||||
const raw = this.normalizeInputPackages(inputPackages);
|
||||
const packages = await Promise.all(raw.map(async (pkg) => {
|
||||
if (pkg.includes("=")) {
|
||||
return pkg;
|
||||
}
|
||||
return `${pkg}=${await this.resolvePackageVersion(packageManager, pkg)}`;
|
||||
}));
|
||||
return packages.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
validateEmptyPackages(behavior, packages) {
|
||||
if (packages.length > 0) {
|
||||
return;
|
||||
}
|
||||
if (behavior === "ignore") {
|
||||
return;
|
||||
}
|
||||
if (behavior === "warn") {
|
||||
process.stdout.write("::warning::Packages argument is empty.\n");
|
||||
return;
|
||||
}
|
||||
throw new Error("Packages argument is empty.");
|
||||
}
|
||||
getCacheRoot() {
|
||||
return path.join(os.homedir(), CACHE_DIRNAME);
|
||||
}
|
||||
async getCacheKey(normalizedPackages, version) {
|
||||
const architecture = (await this.commandRunner.run("arch")).stdout.trim();
|
||||
let value = `${normalizedPackages.join(" ")} @ ${version} ${FORCE_UPDATE_INCREMENT}`;
|
||||
if (architecture !== "x86_64") {
|
||||
value = `${value} ${architecture}`;
|
||||
}
|
||||
const hash = crypto.createHash("md5").update(value).digest("hex");
|
||||
return `${CACHE_PREFIX}${hash}`;
|
||||
}
|
||||
findInstallScript(packageName, extension, root) {
|
||||
const scriptsDir = path.join(root, "var", "lib", "dpkg", "info");
|
||||
if (!fs.existsSync(scriptsDir)) {
|
||||
return undefined;
|
||||
}
|
||||
const pattern = new RegExp(`^${packageName}(:.*)?\\.${extension}$`);
|
||||
const matches = fs
|
||||
.readdirSync(scriptsDir)
|
||||
.filter((entry) => pattern.test(entry))
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
const candidate = matches[0];
|
||||
if (!candidate) {
|
||||
return undefined;
|
||||
}
|
||||
return path.join(scriptsDir, candidate);
|
||||
}
|
||||
tarRelativePath(filePath) {
|
||||
return filePath.startsWith("/") ? filePath.slice(1) : filePath;
|
||||
}
|
||||
async buildFileListForPackage(packageManager, packageName) {
|
||||
const files = (await packageManager.listInstalledFiles(toPackageName(packageName)))
|
||||
.filter((filePath) => {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return false;
|
||||
}
|
||||
const stat = fs.lstatSync(filePath);
|
||||
return stat.isFile() || stat.isSymbolicLink();
|
||||
})
|
||||
.map(this.tarRelativePath);
|
||||
const preinst = this.findInstallScript(packageName, "preinst", "/");
|
||||
const postinst = this.findInstallScript(packageName, "postinst", "/");
|
||||
if (preinst) {
|
||||
files.push(this.tarRelativePath(preinst));
|
||||
}
|
||||
if (postinst) {
|
||||
files.push(this.tarRelativePath(postinst));
|
||||
}
|
||||
return Array.from(new Set(files)).sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
async updateAptLists(packageManager) {
|
||||
if (isAptListsFresh()) {
|
||||
return;
|
||||
}
|
||||
await packageManager.update();
|
||||
}
|
||||
packageSpecifierToName(packageSpecifier) {
|
||||
return packageSpecifier.split("=")[0] ?? packageSpecifier;
|
||||
}
|
||||
async installAndCachePackages(cacheDir, packages, packageManager) {
|
||||
await this.updateAptLists(packageManager);
|
||||
writeManifest(path.join(cacheDir, "manifest_main.log"), packages);
|
||||
const installedPackages = await packageManager.install(packages.map(toPackageName));
|
||||
const manifestAll = [];
|
||||
for (const pkg of installedPackages) {
|
||||
const packageName = pkg.name;
|
||||
const packageVersion = pkg.version;
|
||||
if (!packageName || !packageVersion) {
|
||||
continue;
|
||||
}
|
||||
const archivePath = path.join(cacheDir, `${packageName}=${packageVersion}.tar`);
|
||||
if (!fs.existsSync(archivePath)) {
|
||||
const filesToArchive = await this.buildFileListForPackage(packageManager, packageName);
|
||||
await this.tar.create({
|
||||
cwd: "/",
|
||||
file: archivePath,
|
||||
portable: false,
|
||||
preservePaths: false,
|
||||
follow: false,
|
||||
noDirRecurse: false,
|
||||
}, filesToArchive);
|
||||
}
|
||||
manifestAll.push(`${packageName}=${packageVersion}`);
|
||||
}
|
||||
writeManifest(path.join(cacheDir, "manifest_all.log"), manifestAll);
|
||||
}
|
||||
async restorePackages(cacheDir, executeInstallScripts) {
|
||||
const archives = fs
|
||||
.readdirSync(cacheDir)
|
||||
.filter((entry) => entry.endsWith(".tar"))
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
for (const archive of archives) {
|
||||
const archivePath = path.join(cacheDir, archive);
|
||||
await this.tar.extract({
|
||||
cwd: "/",
|
||||
file: archivePath,
|
||||
preservePaths: true,
|
||||
});
|
||||
if (!executeInstallScripts) {
|
||||
continue;
|
||||
}
|
||||
const packageName = archive.split("=")[0] ?? "";
|
||||
if (!packageName) {
|
||||
continue;
|
||||
}
|
||||
const preinst = this.findInstallScript(packageName, "preinst", "/");
|
||||
if (preinst) {
|
||||
this.logger.info(`Running pre-install script for ${packageName}`);
|
||||
await this.commandRunner.run("sudo", ["sh", "-x", preinst, "install"]);
|
||||
}
|
||||
const postinst = this.findInstallScript(packageName, "postinst", "/");
|
||||
if (postinst) {
|
||||
this.logger.info(`Running post-install script for ${packageName}`);
|
||||
await this.commandRunner.run("sudo", [
|
||||
"sh",
|
||||
"-x",
|
||||
postinst,
|
||||
"configure",
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
async runAction(inputs) {
|
||||
if (/\s/.test(inputs.version)) {
|
||||
throw new Error(`Version value '${inputs.version}' cannot contain spaces.`);
|
||||
}
|
||||
const packageInfoManager = await createPackageManager(false);
|
||||
const normalizedPackages = await this.normalizePackagesWithVersions(packageInfoManager, inputs.packages);
|
||||
this.validateEmptyPackages(inputs.emptyPackagesBehavior, normalizedPackages);
|
||||
const cacheDir = this.getCacheRoot();
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
if (normalizedPackages.length === 0) {
|
||||
writeManifest(path.join(cacheDir, "manifest_main.log"), []);
|
||||
writeManifest(path.join(cacheDir, "manifest_all.log"), []);
|
||||
return {
|
||||
cacheHit: false,
|
||||
packageVersionList: "",
|
||||
allPackageVersionList: "",
|
||||
};
|
||||
}
|
||||
const key = await this.getCacheKey(normalizedPackages, inputs.version);
|
||||
fs.writeFileSync(path.join(cacheDir, "cache_key.md5"), key.replace(CACHE_PREFIX, ""), "utf8");
|
||||
const restoredKey = await cache.restoreCache([cacheDir], key);
|
||||
const cacheHit = restoredKey === key;
|
||||
if (cacheHit) {
|
||||
await this.restorePackages(cacheDir, inputs.executeInstallScripts);
|
||||
}
|
||||
else {
|
||||
const installManager = await createPackageManager(true);
|
||||
const installTargets = normalizedPackages.map((packageSpecifier) => this.packageSpecifierToName(packageSpecifier));
|
||||
await this.installAndCachePackages(cacheDir, installTargets, installManager);
|
||||
await cache.saveCache([cacheDir], key);
|
||||
}
|
||||
return {
|
||||
cacheHit,
|
||||
packageVersionList: readManifestAsCsv(path.join(cacheDir, "manifest_main.log")),
|
||||
allPackageVersionList: readManifestAsCsv(path.join(cacheDir, "manifest_all.log")),
|
||||
};
|
||||
}
|
||||
}
|
||||
export async function runAction(inputs, logger) {
|
||||
const packageManager = (await createPackageManager(true, logger, logger));
|
||||
const commandRunner = new DefaultCommandRunner(logger, logger);
|
||||
const actionRunner = new ActionRunner(commandRunner, tar, logger);
|
||||
return await actionRunner.runAction(inputs);
|
||||
}
|
||||
//# sourceMappingURL=action.js.map
|
||||
1
dist/action.js.map
vendored
1
dist/action.js.map
vendored
File diff suppressed because one or more lines are too long
1
dist/index.d.ts
vendored
1
dist/index.d.ts
vendored
|
|
@ -1 +0,0 @@
|
|||
export {};
|
||||
41
dist/index.js
vendored
41
dist/index.js
vendored
|
|
@ -1,41 +0,0 @@
|
|||
import * as core from "@actions/core";
|
||||
import { parseBoolean, runAction } from "./action.js";
|
||||
import winston from "winston";
|
||||
function parseEmptyPackagesBehavior(value) {
|
||||
if (value === "error" || value === "warn" || value === "ignore") {
|
||||
return value;
|
||||
}
|
||||
throw new Error(`empty_packages_behavior value '${value}' must be one of: error, warn, ignore.`);
|
||||
}
|
||||
function getInputs() {
|
||||
const executeInstallScriptsRaw = core.getInput("execute_install_scripts");
|
||||
const debugRaw = core.getInput("debug");
|
||||
const emptyPackagesBehaviorRaw = core.getInput("empty_packages_behavior") || "error";
|
||||
return {
|
||||
packages: core.getInput("packages", { required: true }),
|
||||
version: core.getInput("version"),
|
||||
executeInstallScripts: parseBoolean(executeInstallScriptsRaw, "execute_install_scripts"),
|
||||
emptyPackagesBehavior: parseEmptyPackagesBehavior(emptyPackagesBehaviorRaw),
|
||||
debug: parseBoolean(debugRaw, "debug"),
|
||||
};
|
||||
}
|
||||
async function main() {
|
||||
try {
|
||||
const inputs = getInputs();
|
||||
const logger = winston.createLogger({
|
||||
level: inputs.debug ? "debug" : "info",
|
||||
format: winston.format.combine(winston.format.colorize(), winston.format.printf(({ level, message }) => `${level}: ${message}`)),
|
||||
transports: [new winston.transports.Console()],
|
||||
});
|
||||
const outputs = await runAction(inputs, logger);
|
||||
core.setOutput("cache-hit", String(outputs.cacheHit));
|
||||
core.setOutput("package-version-list", outputs.packageVersionList);
|
||||
core.setOutput("all-package-version-list", outputs.allPackageVersionList);
|
||||
}
|
||||
catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
core.setFailed(message);
|
||||
}
|
||||
}
|
||||
void main();
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
dist/index.js.map
vendored
1
dist/index.js.map
vendored
|
|
@ -1 +0,0 @@
|
|||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAqB,MAAM,aAAa,CAAC;AACzE,OAAO,OAAO,MAAM,SAAS,CAAC;AAE9B,SAAS,0BAA0B,CACjC,KAAa;IAEb,IAAI,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChE,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,IAAI,KAAK,CACb,kCAAkC,KAAK,wCAAwC,CAChF,CAAC;AACJ,CAAC;AAED,SAAS,SAAS;IAChB,MAAM,wBAAwB,GAAG,IAAI,CAAC,QAAQ,CAAC,yBAAyB,CAAC,CAAC;IAC1E,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACxC,MAAM,wBAAwB,GAC5B,IAAI,CAAC,QAAQ,CAAC,yBAAyB,CAAC,IAAI,OAAO,CAAC;IAEtD,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QACvD,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;QACjC,qBAAqB,EAAE,YAAY,CACjC,wBAAwB,EACxB,yBAAyB,CAC1B;QACD,qBAAqB,EAAE,0BAA0B,CAAC,wBAAwB,CAAC;QAC3E,KAAK,EAAE,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC;KACvC,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;YAClC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM;YACtC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,CAC5B,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,EACzB,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,GAAG,KAAK,KAAK,OAAO,EAAE,CAAC,CACtE;YACD,UAAU,EAAE,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;SAC/C,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAEhD,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;QACtD,IAAI,CAAC,SAAS,CAAC,sBAAsB,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAC;QACnE,IAAI,CAAC,SAAS,CAAC,0BAA0B,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC5E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACvE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAC1B,CAAC;AACH,CAAC;AAED,KAAK,IAAI,EAAE,CAAC"}
|
||||
23
dist/io.d.ts
vendored
23
dist/io.d.ts
vendored
|
|
@ -1,23 +0,0 @@
|
|||
import { type CommandRunner } from "../../ts-apt/dist/index.js";
|
||||
export declare function isAptListsFresh(): boolean;
|
||||
export declare class Package {
|
||||
readonly name: string;
|
||||
readonly version: string;
|
||||
constructor(name: string, version: string);
|
||||
serialize(): string;
|
||||
}
|
||||
export declare class CacheKey {
|
||||
readonly version: string;
|
||||
readonly forceUpdateIncrement: string;
|
||||
readonly arch: string;
|
||||
readonly normalizedPackages: string[];
|
||||
constructor(version: string, forceUpdateIncrement: string, arch: string, normalizedPackages: string[]);
|
||||
serialize(): string;
|
||||
}
|
||||
export declare function deserializeCacheKey(serialized: string): CacheKey;
|
||||
export declare class Cache {
|
||||
private readonly cachePath;
|
||||
private readonly commandRunner;
|
||||
constructor(cacheDir: string | undefined, commandRunner: CommandRunner);
|
||||
getKey(normalizedPackages: string[], version: string): Promise<string>;
|
||||
}
|
||||
88
dist/io.js
vendored
88
dist/io.js
vendored
|
|
@ -1,88 +0,0 @@
|
|||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as crypto from "crypto";
|
||||
import * as os from "os";
|
||||
export function isAptListsFresh() {
|
||||
const aptListsPath = "/var/lib/apt/lists";
|
||||
const maxDepth = 5;
|
||||
function search(currentPath, currentDepth) {
|
||||
if (currentDepth > maxDepth)
|
||||
return false;
|
||||
try {
|
||||
const stats = fs.statSync(currentPath);
|
||||
if (stats.isDirectory()) {
|
||||
const entries = fs.readdirSync(currentPath);
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentPath, entry);
|
||||
if (search(fullPath, currentDepth + 1)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
// Ignore permission errors or inaccessible paths
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return search(aptListsPath, 0);
|
||||
}
|
||||
export class Package {
|
||||
name;
|
||||
version;
|
||||
constructor(name, version) {
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
}
|
||||
serialize() {
|
||||
return `${this.name}@${this.version}`;
|
||||
}
|
||||
}
|
||||
const FORCE_UPDATE_INCREMENT = "4";
|
||||
const CACHE_DIRNAME = "cache-apt-pkgs";
|
||||
const CACHE_PREFIX = "cache-apt-pkgs_";
|
||||
export class CacheKey {
|
||||
version;
|
||||
forceUpdateIncrement;
|
||||
arch;
|
||||
normalizedPackages;
|
||||
constructor(version, forceUpdateIncrement, arch, normalizedPackages) {
|
||||
this.version = version;
|
||||
this.forceUpdateIncrement = forceUpdateIncrement;
|
||||
this.arch = arch;
|
||||
this.normalizedPackages = normalizedPackages;
|
||||
}
|
||||
serialize() {
|
||||
return `${this.version}|${this.forceUpdateIncrement}|${this.arch}|${this.normalizedPackages.join(",")}`;
|
||||
}
|
||||
}
|
||||
export function deserializeCacheKey(serialized) {
|
||||
const parts = serialized.split("|");
|
||||
if (parts.length !== 4) {
|
||||
throw new Error(`Invalid serialized cache key: ${serialized}`);
|
||||
}
|
||||
const [version, forceUpdateIncrement, arch, normalizedPackagesStr] = parts;
|
||||
const normalizedPackages = normalizedPackagesStr.split(",");
|
||||
return new CacheKey(version, forceUpdateIncrement, arch, normalizedPackages);
|
||||
}
|
||||
export class Cache {
|
||||
cachePath;
|
||||
commandRunner;
|
||||
constructor(cacheDir = CACHE_DIRNAME, commandRunner) {
|
||||
this.cachePath = path.join(os.homedir(), cacheDir);
|
||||
this.commandRunner = commandRunner;
|
||||
}
|
||||
async getKey(normalizedPackages, version) {
|
||||
const architecture = await (await this.commandRunner.run("arch")).stdout.trim();
|
||||
let value = `${normalizedPackages.join(" ")} @ ${version} ${FORCE_UPDATE_INCREMENT}`;
|
||||
if (architecture !== "x86_64") {
|
||||
value = `${value} ${architecture}`;
|
||||
}
|
||||
const hash = crypto.createHash("md5").update(value).digest("hex");
|
||||
return `${CACHE_PREFIX}${hash}`;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=io.js.map
|
||||
1
dist/io.js.map
vendored
1
dist/io.js.map
vendored
|
|
@ -1 +0,0 @@
|
|||
{"version":3,"file":"io.js","sourceRoot":"","sources":["../src/io.ts"],"names":[],"mappings":"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"}
|
||||
2
dist/manifest.d.ts
vendored
2
dist/manifest.d.ts
vendored
|
|
@ -1,2 +0,0 @@
|
|||
export declare function writeManifest(filePath: string, entries: string[]): void;
|
||||
export declare function readManifestAsCsv(filePath: string): string;
|
||||
17
dist/manifest.js
vendored
17
dist/manifest.js
vendored
|
|
@ -1,17 +0,0 @@
|
|||
import fs from "node:fs";
|
||||
export function writeManifest(filePath, entries) {
|
||||
const normalized = [...entries].filter(Boolean).sort((a, b) => a.localeCompare(b));
|
||||
fs.writeFileSync(filePath, normalized.join("\n"), "utf8");
|
||||
}
|
||||
export function readManifestAsCsv(filePath) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return "";
|
||||
}
|
||||
return fs
|
||||
.readFileSync(filePath, "utf8")
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.join(",");
|
||||
}
|
||||
//# sourceMappingURL=manifest.js.map
|
||||
1
dist/manifest.js.map
vendored
1
dist/manifest.js.map
vendored
|
|
@ -1 +0,0 @@
|
|||
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,OAAiB;IAC/D,MAAM,UAAU,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IACnF,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,QAAgB;IAChD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,OAAO,EAAE;SACN,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC;SAC9B,KAAK,CAAC,OAAO,CAAC;SACd,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SAC1B,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC"}
|
||||
9
dist/shell.d.ts
vendored
9
dist/shell.d.ts
vendored
|
|
@ -1,9 +0,0 @@
|
|||
import { type SpawnSyncOptions } from "node:child_process";
|
||||
export interface CommandResult {
|
||||
readonly status: number;
|
||||
readonly stdout: string;
|
||||
readonly stderr: string;
|
||||
}
|
||||
export declare function runCommand(command: string, args?: string[], options?: SpawnSyncOptions): CommandResult;
|
||||
export declare function runCommandOrThrow(command: string, args?: string[], options?: SpawnSyncOptions): CommandResult;
|
||||
export declare function fileIsFresh(path: string, withinMinutes: number): boolean;
|
||||
46
dist/shell.js
vendored
46
dist/shell.js
vendored
|
|
@ -1,46 +0,0 @@
|
|||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
export function runCommand(command, args = [], options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
encoding: "utf8",
|
||||
...options,
|
||||
});
|
||||
const stdout = typeof result.stdout === "string"
|
||||
? result.stdout
|
||||
: result.stdout
|
||||
? result.stdout.toString("utf8")
|
||||
: "";
|
||||
const stderr = typeof result.stderr === "string"
|
||||
? result.stderr
|
||||
: result.stderr
|
||||
? result.stderr.toString("utf8")
|
||||
: "";
|
||||
return {
|
||||
status: result.status ?? 1,
|
||||
stdout,
|
||||
stderr,
|
||||
};
|
||||
}
|
||||
export function runCommandOrThrow(command, args = [], options = {}) {
|
||||
const result = runCommand(command, args, options);
|
||||
if (result.status !== 0) {
|
||||
throw new Error([
|
||||
`Command failed: ${command} ${args.join(" ")}`,
|
||||
`exit: ${result.status}`,
|
||||
result.stdout ? `stdout:\n${result.stdout.trim()}` : "",
|
||||
result.stderr ? `stderr:\n${result.stderr.trim()}` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export function fileIsFresh(path, withinMinutes) {
|
||||
if (!fs.existsSync(path)) {
|
||||
return false;
|
||||
}
|
||||
const stat = fs.statSync(path);
|
||||
const maxAgeMs = withinMinutes * 60 * 1000;
|
||||
return Date.now() - stat.mtimeMs <= maxAgeMs;
|
||||
}
|
||||
//# sourceMappingURL=shell.js.map
|
||||
1
dist/shell.js.map
vendored
1
dist/shell.js.map
vendored
|
|
@ -1 +0,0 @@
|
|||
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAyB,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,MAAM,SAAS,CAAC;AAQzB,MAAM,UAAU,UAAU,CACxB,OAAe,EACf,OAAiB,EAAE,EACnB,UAA4B,EAAE;IAE9B,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE;QACtC,QAAQ,EAAE,MAAM;QAChB,GAAG,OAAO;KACX,CAAC,CAAC;IAEH,MAAM,MAAM,GACV,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ;QAC/B,CAAC,CAAC,MAAM,CAAC,MAAM;QACf,CAAC,CAAC,MAAM,CAAC,MAAM;YACb,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YAChC,CAAC,CAAC,EAAE,CAAC;IACX,MAAM,MAAM,GACV,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ;QAC/B,CAAC,CAAC,MAAM,CAAC,MAAM;QACf,CAAC,CAAC,MAAM,CAAC,MAAM;YACb,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YAChC,CAAC,CAAC,EAAE,CAAC;IAEX,OAAO;QACL,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC;QAC1B,MAAM;QACN,MAAM;KACP,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,OAAe,EACf,OAAiB,EAAE,EACnB,UAA4B,EAAE;IAE9B,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAClD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACb;YACE,mBAAmB,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;YAC9C,SAAS,MAAM,CAAC,MAAM,EAAE;YACxB,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE;YACvD,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE;SACxD;aACE,MAAM,CAAC,OAAO,CAAC;aACf,IAAI,CAAC,IAAI,CAAC,CACd,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,aAAqB;IAC7D,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,QAAQ,GAAG,aAAa,GAAG,EAAE,GAAG,IAAI,CAAC;IAC3C,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,OAAO,IAAI,QAAQ,CAAC;AAC/C,CAAC"}
|
||||
3445
package-lock.json
generated
3445
package-lock.json
generated
File diff suppressed because it is too large
Load diff
29
package.json
29
package.json
|
|
@ -55,8 +55,8 @@
|
|||
"check:latest-action-pins": "node --experimental-strip-types ./scripts/ops/check_latest_action_pin.mts --scan",
|
||||
"check:pr": "node --experimental-strip-types ./scripts/ops/pr_checks.mts",
|
||||
"check:configs": "node --experimental-strip-types ./scripts/ops/check_configs.mts",
|
||||
"check:tsapt-pin": "./scripts/ops/check_tsapt_pin.sh",
|
||||
"check": "npm run -s runall -- check:latest-action-pins check:pr check:configs check:tsapt-pin",
|
||||
"check:deps-pin": "node --experimental-strip-types ./scripts/ops/check_deps_pin.mts ts-apt",
|
||||
"check": "npm run -s runall -- check:latest-action-pins check:pr check:configs check:deps-pin",
|
||||
"dev:tsapt:link": "npm link ../ts-apt --no-save; echo \"NOTE: npm run dev:tsapt:unlink when you are finished.\"",
|
||||
"dev:tsapt:unlink": "npm unlink ts-apt --no-save && npm ci",
|
||||
"gh:auth": "gh auth status >/dev/null 2>&1 || gh auth login --web && gh auth setup-git",
|
||||
|
|
@ -72,7 +72,6 @@
|
|||
"repo:tags:upload": "npm run repo::sync -- upload --target tags",
|
||||
"repo:all:download": "npm run repo::sync -- download --target all",
|
||||
"repo:all:upload": "npm run repo::sync -- upload --target all",
|
||||
"docs:api": "typedoc --options typedoc.json",
|
||||
"release": "semantic-release",
|
||||
"release:dry-run": "semantic-release --dry-run",
|
||||
"lint": "eslint src test scripts --ext .ts,.mts",
|
||||
|
|
@ -85,14 +84,10 @@
|
|||
"@actions/artifact": "^6.2.1",
|
||||
"@actions/cache": "^6.1.0",
|
||||
"@actions/core": "^3.0.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"rollup": "^4.62.2",
|
||||
"tar": "^7.4.3",
|
||||
"ts-apt": "git+https://github.com/awalsh128/ts-apt.git#67d450ee524f1a6145e5d30eaf5d3d6615e3d0ef",
|
||||
"ts-apt": "git+https://github.com/awalsh128/ts-apt.git#342c7d0f453dcf1a69da83d7d195f9df2611a351",
|
||||
"winston": "^3.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@devcontainers/cli": "^0.87.0",
|
||||
"@robingenz/zli": "^0.2.0",
|
||||
"@rollup/plugin-commonjs": "^29.0.3",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
|
|
@ -104,19 +99,27 @@
|
|||
"@semantic-release/npm": "^13.1.5",
|
||||
"@semantic-release/release-notes-generator": "^14.1.1",
|
||||
"@types/node": "^24.1.0",
|
||||
"@types/tar": "^6.1.13",
|
||||
"@typescript-eslint/eslint-plugin": "^8.62.1",
|
||||
"@typescript-eslint/parser": "^8.62.1",
|
||||
"@vitest/coverage-v8": "^4.1.9",
|
||||
"eslint": "^10.6.0",
|
||||
"internet-message": "^1.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"rollup": "^4.68.0",
|
||||
"semantic-release": "^25.0.5",
|
||||
"type-coverage": "^2.29.7",
|
||||
"typedoc": "^0.28.19",
|
||||
"typedoc-plugin-markdown": "^4.12.0",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.10",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"overrides": {
|
||||
"glob@>=9.0.0 <14.0.2": "^14.0.4",
|
||||
"eslint": {
|
||||
"minimatch": "^11.0.5"
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"minimatch": "^11.0.5"
|
||||
},
|
||||
"type-coverage": {
|
||||
"minimatch": "^11.0.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
142
scripts/ops/check_deps_pin.mts
Normal file
142
scripts/ops/check_deps_pin.mts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
#!/usr/bin/env -S node --experimental-strip-types
|
||||
/**
|
||||
* Fails when a dependency is not resolved from a pushed commit SHA pin.
|
||||
*
|
||||
* Guards against shipping code built or tested against a locally linked
|
||||
* dependency (e.g. npm run dev:tsapt:link), which CI cannot see. For each
|
||||
* dependency name given as an argument, checks that:
|
||||
* 1. node_modules/<name> is not a symlink (skipped if it is absent).
|
||||
* 2. package.json pins <name> to a full commit SHA (`...#<40 hex chars>`).
|
||||
* 3. package-lock.json resolves <name> to that same SHA and not a link.
|
||||
*
|
||||
* Used by .githooks/pre-push and the CI action-deps-pinned job.
|
||||
*
|
||||
* Usage: check_deps_pin.mts <dependency> [<dependency>...]
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
fail,
|
||||
logError,
|
||||
logSuccess,
|
||||
readJsonFile,
|
||||
ROOT_DIR,
|
||||
runMain,
|
||||
usage,
|
||||
} from "../devopslib.mts";
|
||||
|
||||
const SHA_PIN_PATTERN = /#([0-9a-f]{40})$/;
|
||||
|
||||
type PackageJson = {
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
};
|
||||
|
||||
type LockEntry = {
|
||||
resolved?: string;
|
||||
link?: boolean;
|
||||
};
|
||||
|
||||
type PackageLock = {
|
||||
packages?: Record<
|
||||
string,
|
||||
LockEntry & {
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
function isSymlink(filePath: string): boolean {
|
||||
try {
|
||||
return fs.lstatSync(filePath).isSymbolicLink();
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns one problem message per failed check for the dependency. */
|
||||
function checkDependencyPin(
|
||||
name: string,
|
||||
packageJson: PackageJson,
|
||||
packageLock: PackageLock,
|
||||
): string[] {
|
||||
const problems: string[] = [];
|
||||
|
||||
const modulePath = path.join(ROOT_DIR, "node_modules", name);
|
||||
if (isSymlink(modulePath)) {
|
||||
problems.push(
|
||||
`node_modules/${name} is a symlink to ${fs.readlinkSync(modulePath)}; unlink it and run 'npm ci' first.`,
|
||||
);
|
||||
}
|
||||
|
||||
const spec =
|
||||
packageJson.dependencies?.[name] ?? packageJson.devDependencies?.[name];
|
||||
if (spec === undefined) {
|
||||
problems.push(`package.json has no ${name} dependency.`);
|
||||
return problems;
|
||||
}
|
||||
const sha = SHA_PIN_PATTERN.exec(spec)?.[1];
|
||||
if (sha === undefined) {
|
||||
problems.push(
|
||||
`package.json ${name} '${spec}' is not pinned to a full commit SHA.`,
|
||||
);
|
||||
return problems;
|
||||
}
|
||||
|
||||
const root = packageLock.packages?.[""];
|
||||
const rootSpec =
|
||||
root?.dependencies?.[name] ?? root?.devDependencies?.[name];
|
||||
if (rootSpec !== spec) {
|
||||
problems.push(
|
||||
`package-lock.json root ${name} '${rootSpec}' does not match package.json '${spec}'.`,
|
||||
);
|
||||
}
|
||||
|
||||
const entry = packageLock.packages?.[`node_modules/${name}`];
|
||||
if (entry === undefined) {
|
||||
problems.push(`package-lock.json has no node_modules/${name} entry.`);
|
||||
} else if (entry.link) {
|
||||
problems.push(
|
||||
`package-lock.json ${name} is a link to '${entry.resolved}'.`,
|
||||
);
|
||||
} else if (!entry.resolved?.endsWith(`#${sha}`)) {
|
||||
problems.push(
|
||||
`package-lock.json ${name} resolved '${entry.resolved}' does not match pin ${sha}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return problems;
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const names = process.argv.slice(2);
|
||||
if (names.length === 0 || names.some((name) => name.length === 0)) {
|
||||
fail(usage(import.meta.url, "<dependency> [<dependency>...]"));
|
||||
}
|
||||
|
||||
const packageJson = readJsonFile(
|
||||
path.join(ROOT_DIR, "package.json"),
|
||||
) as PackageJson;
|
||||
const packageLock = readJsonFile(
|
||||
path.join(ROOT_DIR, "package-lock.json"),
|
||||
) as PackageLock;
|
||||
|
||||
const problems = names.flatMap((name) =>
|
||||
checkDependencyPin(name, packageJson, packageLock),
|
||||
);
|
||||
for (const problem of problems) {
|
||||
logError(problem);
|
||||
}
|
||||
if (problems.length > 0) {
|
||||
fail(`Dependency pin check failed with ${problems.length} error(s).`);
|
||||
}
|
||||
|
||||
logSuccess(`Dependency pin check passed for ${names.join(", ")}.`);
|
||||
}
|
||||
|
||||
await runMain(main, 1);
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Fails when ts-apt is not resolved from a pushed commit SHA pin.
|
||||
#
|
||||
# Guards against shipping code built or tested against a local ts-apt link
|
||||
# (npm run dev:tsapt:link), which CI cannot see. Checks that:
|
||||
# 1. node_modules/ts-apt is not a symlink (skipped if node_modules is absent).
|
||||
# 2. package.json pins ts-apt to a full commit SHA.
|
||||
# 3. package-lock.json resolves ts-apt to that same SHA and not a link.
|
||||
#
|
||||
# Used by .githooks/pre-push and the CI action-deps-pinned job.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(git rev-parse --show-toplevel)"
|
||||
cd "${ROOT_DIR}"
|
||||
|
||||
errors=0
|
||||
error() {
|
||||
echo "ERROR: $*" >&2
|
||||
errors=$((errors + 1))
|
||||
}
|
||||
|
||||
if [[ -L node_modules/ts-apt ]]; then
|
||||
error "node_modules/ts-apt is a symlink to $(readlink node_modules/ts-apt); run 'npm run dev:tsapt:unlink' first."
|
||||
fi
|
||||
|
||||
# Prints one problem per line; package.json and package-lock.json are parsed
|
||||
# with node to avoid a jq dependency.
|
||||
lock_problems="$(node --input-type=module - <<'EOF'
|
||||
import { readFileSync } from "node:fs";
|
||||
const pin = /^git\+https:\/\/github\.com\/awalsh128\/ts-apt\.git#([0-9a-f]{40})$/;
|
||||
const spec = JSON.parse(readFileSync("package.json", "utf8")).dependencies?.["ts-apt"];
|
||||
const match = pin.exec(spec ?? "");
|
||||
if (!match) {
|
||||
console.log(`package.json ts-apt '${spec}' is not pinned to a full commit SHA.`);
|
||||
process.exit(0);
|
||||
}
|
||||
const sha = match[1];
|
||||
const lock = JSON.parse(readFileSync("package-lock.json", "utf8"));
|
||||
const rootSpec = lock.packages?.[""]?.dependencies?.["ts-apt"];
|
||||
if (rootSpec !== spec) {
|
||||
console.log(`package-lock.json root ts-apt '${rootSpec}' does not match package.json '${spec}'.`);
|
||||
}
|
||||
const entry = lock.packages?.["node_modules/ts-apt"];
|
||||
if (!entry) {
|
||||
console.log("package-lock.json has no node_modules/ts-apt entry.");
|
||||
} else if (entry.link) {
|
||||
console.log(`package-lock.json ts-apt is a link to '${entry.resolved}'.`);
|
||||
} else if (!entry.resolved?.endsWith(`#${sha}`)) {
|
||||
console.log(`package-lock.json ts-apt resolved '${entry.resolved}' does not match pin ${sha}.`);
|
||||
}
|
||||
EOF
|
||||
)"
|
||||
|
||||
if [[ -n "${lock_problems}" ]]; then
|
||||
while IFS= read -r problem; do
|
||||
error "${problem}"
|
||||
done <<<"${lock_problems}"
|
||||
fi
|
||||
|
||||
if ((errors > 0)); then
|
||||
echo "ts-apt pin check failed with ${errors} error(s)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "ts-apt pin check passed."
|
||||
Loading…
Reference in a new issue