From 64a160d1cc61c4e75a2885a64fbc4f57fc1c7a2a Mon Sep 17 00:00:00 2001 From: awalsh128 Date: Mon, 9 Nov 2026 11:15:05 -0800 Subject: [PATCH] style: set Prettier print width to 110 and reformat Without a repo config, the Prettier CLI and Trunk used the default width of 80, while editors with a user printWidth reflowed files on save, producing formatting-only diffs. Pin the width in .prettierrc.json and reformat source, tests, scripts and JS/TS config files to match. Files that already failed Prettier at width 80 for unrelated reasons (tsconfig*.json, .trunk/*, V2_FAQ.MD) are left untouched. No code changes. Co-Authored-By: Claude Opus 5.5 --- .github/FUNDING.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 7 +- .prettierrc.json | 3 + eslint.config.js | 7 +- release.config.ts | 3 +- rollup.config.mjs | 7 +- scripts/dev/create_testcase_logs.mts | 89 +++------- scripts/dev/gh_sync.mts | 193 ++++------------------ scripts/dev/hotfix_pr.mts | 45 +---- scripts/dev/node_ver.mts | 67 +++----- scripts/dev/setup_devenv.mts | 82 ++------- scripts/devopslib.mts | 68 ++------ scripts/ops/check_configs.mts | 30 +--- scripts/ops/check_deps_pin.mts | 44 ++--- scripts/ops/check_latest_action_pin.mts | 89 ++-------- scripts/ops/check_release_tag.mts | 21 +-- scripts/ops/opslib.mts | 9 +- scripts/ops/pr_checks.mts | 139 ++++------------ scripts/tsconfig.json | 2 +- src/action.ts | 46 +----- src/cache.ts | 124 ++++---------- src/index.ts | 24 +-- src/instrumentation.ts | 17 +- src/manifest.ts | 17 +- src/packages.ts | 54 ++---- test/action-runner.test.ts | 8 +- test/action.test.ts | 24 +-- test/cache-restore.test.ts | 92 ++++------- test/check-deps-pin.test.ts | 25 +-- test/instrumentation.test.ts | 8 +- test/manifest.test.ts | 27 +-- test/packages.test.ts | 53 +----- 32 files changed, 309 insertions(+), 1117 deletions(-) create mode 100644 .prettierrc.json diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c43782b..5036679 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ # GitHub Sponsors configuration -github: awalsh128 \ No newline at end of file +github: awalsh128 diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bbcbbe7..2bc5d5f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,10 +1,9 @@ --- name: Feature request about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - +title: "" +labels: "" +assignees: "" --- **Is your feature request related to a problem? Please describe.** diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..970507e --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "printWidth": 110 +} diff --git a/eslint.config.js b/eslint.config.js index 39a6477..58324ca 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,12 +5,7 @@ export default [ ignores: ["dist/**", "node_modules/**", "target/**"], }, { - files: [ - "src/**/*.ts", - "test/**/*.ts", - "scripts/**/*.ts", - "scripts/**/*.mts", - ], + files: ["src/**/*.ts", "test/**/*.ts", "scripts/**/*.ts", "scripts/**/*.mts"], languageOptions: { parser: tsParser, ecmaVersion: "latest", diff --git a/release.config.ts b/release.config.ts index 20ba3e1..2a1f681 100644 --- a/release.config.ts +++ b/release.config.ts @@ -42,8 +42,7 @@ export default { "@semantic-release/git", { assets: ["CHANGELOG.md", "package.json", "package-lock.json"], - message: - "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}", + message: "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}", }, ], diff --git a/rollup.config.mjs b/rollup.config.mjs index e4e4b6f..e76b8eb 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -50,13 +50,10 @@ export default { // Dependencies trigger these and they are harmless and very noisy: // top level `this` in ES modules, misplaced /*@__PURE__*/ comments and // circular imports inside third-party packages. - const fromDependency = (warning.ids ?? [warning.id ?? ""]).every((id) => - id.includes("node_modules"), - ); + const fromDependency = (warning.ids ?? [warning.id ?? ""]).every((id) => id.includes("node_modules")); if ( warning.code === "THIS_IS_UNDEFINED" || - (fromDependency && - ["INVALID_ANNOTATION", "CIRCULAR_DEPENDENCY"].includes(warning.code)) + (fromDependency && ["INVALID_ANNOTATION", "CIRCULAR_DEPENDENCY"].includes(warning.code)) ) { return; } diff --git a/scripts/dev/create_testcase_logs.mts b/scripts/dev/create_testcase_logs.mts index 24240c9..9f9a8ef 100755 --- a/scripts/dev/create_testcase_logs.mts +++ b/scripts/dev/create_testcase_logs.mts @@ -26,10 +26,7 @@ function toLog(result: LogResult): string { } /** Determine exit code: use code if available, else map signal to 137 (SIGKILL) or 143 (SIGTERM) */ -function getFinalExitCode( - code: number | null, - signal: NodeJS.Signals | null, -): number { +function getFinalExitCode(code: number | null, signal: NodeJS.Signals | null): number { if (code !== null) { return code; } else if (signal === "SIGTERM") { @@ -127,11 +124,7 @@ async function execute(cmdLine: string): Promise { }); } -async function writeLogFile( - baseDir: string, - relativePath: string, - result: LogResult, -): Promise { +async function writeLogFile(baseDir: string, relativePath: string, result: LogResult): Promise { const outputPath = resolve(baseDir, `${relativePath}.log`); await mkdir(dirname(outputPath), { recursive: true }); await writeFile(outputPath, toLog(result), "utf8"); @@ -172,30 +165,25 @@ async function main(): Promise { const testLogFiles: TestLogEntry[] = [ { filepath: "autoclean", - execCmdLine: - "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 autoclean", + execCmdLine: "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 autoclean", }, { filepath: "autoremove_found", preExecCmdLine: ["apt-get install -y xdot", "apt-get remove -y xdot"], - execCmdLine: - "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 autoremove", + execCmdLine: "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 autoremove", }, { filepath: "autoremove_notfound", preExecCmdLine: ["apt-get autoremove -y"], - execCmdLine: - "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 autoremove", + execCmdLine: "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 autoremove", }, { filepath: "cacheshow_mixedfoundnotfound", - execCmdLine: - "/usr/bin/apt-cache --quiet=0 --no-all-versions show xdot python3 nonexistentpackage", + execCmdLine: "/usr/bin/apt-cache --quiet=0 --no-all-versions show xdot python3 nonexistentpackage", }, { filepath: "cacheshow_multiplefound", - execCmdLine: - "/usr/bin/apt-cache --quiet=0 --no-all-versions show xdot python3", + execCmdLine: "/usr/bin/apt-cache --quiet=0 --no-all-versions show xdot python3", }, { filepath: "cacheshow_singlefound", @@ -203,8 +191,7 @@ const testLogFiles: TestLogEntry[] = [ }, { filepath: "cacheshow_singlenotfound", - execCmdLine: - "/usr/bin/apt-cache --quiet=0 --no-all-versions show nonexistentpackage", + execCmdLine: "/usr/bin/apt-cache --quiet=0 --no-all-versions show nonexistentpackage", }, { filepath: "cacheshow_virtualpackage", @@ -212,8 +199,7 @@ const testLogFiles: TestLogEntry[] = [ }, { filepath: "cacheshow_virtualandnotpackages", - execCmdLine: - "/usr/bin/apt-cache --quiet=0 --no-all-versions show libvips xdot", + execCmdLine: "/usr/bin/apt-cache --quiet=0 --no-all-versions show libvips xdot", }, { filepath: "cacheshowpkg_virtualpackage", @@ -222,35 +208,26 @@ const testLogFiles: TestLogEntry[] = [ { filepath: "install_mixedfoundnotfound", preExecCmdLine: ["apt-get remove -y xdot"], - execCmdLine: - "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install -f xdot nonexistentpackage", + execCmdLine: "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install -f xdot nonexistentpackage", }, { filepath: "install_mixedinstallstatus", - preExecCmdLine: [ - "apt-get install -y xdot", - "apt-get remove -y rolldice", - "apt-get autoremove -y", - ], - execCmdLine: - "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install -f rolldice xdot", + preExecCmdLine: ["apt-get install -y xdot", "apt-get remove -y rolldice", "apt-get autoremove -y"], + execCmdLine: "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install -f rolldice xdot", }, { filepath: "install_singleinstalled", preExecCmdLine: ["apt-get install -y xdot"], - execCmdLine: - "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install -f xdot", + execCmdLine: "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install -f xdot", }, { filepath: "install_singlenotfound", - execCmdLine: - "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install -f nonexistentpackage", + execCmdLine: "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install -f nonexistentpackage", }, { filepath: "install_singlenotinstalled", preExecCmdLine: ["apt-get remove -y xdot", "apt-get autoremove -y"], - execCmdLine: - "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install -f xdot", + execCmdLine: "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install -f xdot", }, { filepath: "listinstalled", @@ -296,8 +273,7 @@ const testLogFiles: TestLogEntry[] = [ { filepath: "remove_singlenotinstalled", preExecCmdLine: ["apt-get remove -y xdot"], - execCmdLine: - "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 remove -f xdot --autoremove", + execCmdLine: "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 remove -f xdot --autoremove", }, { filepath: "remove_singlenotfound", @@ -307,8 +283,7 @@ const testLogFiles: TestLogEntry[] = [ { filepath: "remove_singleinstalled", preExecCmdLine: ["apt-get install -y xdot"], - execCmdLine: - "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 remove -f xdot --autoremove", + execCmdLine: "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 remove -f xdot --autoremove", }, { filepath: "search_multiplefound", @@ -329,8 +304,7 @@ const testLogFiles: TestLogEntry[] = [ { filepath: "upgrade_mixedfoundnotfound", preExecCmdLine: ["apt-get remove -y xdot"], - execCmdLine: - "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install xdot nonexistentpackage", + execCmdLine: "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install xdot nonexistentpackage", }, { filepath: "upgrade_mixedupgradestatus", @@ -339,42 +313,29 @@ const testLogFiles: TestLogEntry[] = [ "apt-get remove -y xxd", "apt-get install -y xxd=2:8.1.2269-1ubuntu5.30", ], - execCmdLine: - "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install xxd xdot", + execCmdLine: "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install xxd xdot", }, { filepath: "upgrade_singleupgraded", preExecCmdLine: ["apt-get upgrade -y xdot"], - execCmdLine: - "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 upgrade", + execCmdLine: "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 upgrade", }, { filepath: "upgrade_singlenotfound", - execCmdLine: - "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install nonexistentpackage", + execCmdLine: "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install nonexistentpackage", }, { filepath: "upgrade_singlenotupgraded", - preExecCmdLine: [ - "apt-get remove -y xxd", - "apt-get install -y xxd=2:8.1.2269-1ubuntu5.30", - ], - execCmdLine: - "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install xxd", + preExecCmdLine: ["apt-get remove -y xxd", "apt-get install -y xxd=2:8.1.2269-1ubuntu5.30"], + execCmdLine: "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install xxd", }, { filepath: "upgrade_singleinstalled", preExecCmdLine: ["apt-get install xdot"], - execCmdLine: - "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install xdot", + execCmdLine: "/usr/bin/apt-get --quiet=0 -y -o DPkg::Lock::Timeout=-1 install xdot", }, ]; await main().catch((error) => { - fail( - "Error occurred during log creation:\n" + - String(error) + - "\n" + - (error.stack || ""), - ); + fail("Error occurred during log creation:\n" + String(error) + "\n" + (error.stack || "")); }); diff --git a/scripts/dev/gh_sync.mts b/scripts/dev/gh_sync.mts index 9281d8b..7c68737 100644 --- a/scripts/dev/gh_sync.mts +++ b/scripts/dev/gh_sync.mts @@ -17,13 +17,7 @@ */ import { spawnSync } from "node:child_process"; -import { - mkdirSync, - mkdtempSync, - readFileSync, - rmSync, - writeFileSync, -} from "node:fs"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { defineCommand, defineOptions } from "@robingenz/zli"; @@ -79,12 +73,7 @@ type SettingsFile = { type BypassActor = { actor_id: number; - actor_type: - | "RepositoryRole" - | "Team" - | "Integration" - | "OrganizationAdmin" - | "DeployKey"; + actor_type: "RepositoryRole" | "Team" | "Integration" | "OrganizationAdmin" | "DeployKey"; bypass_mode: "always" | "pull_request"; }; @@ -194,11 +183,7 @@ function ensureDirForFile(filePath: string): string { return absPath; } -function writeJsonFile( - filePath: string, - payload: unknown, - successLabel: string, -): void { +function writeJsonFile(filePath: string, payload: unknown, successLabel: string): void { const absPath = ensureDirForFile(filePath); writeFileSync(absPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8"); logSuccess(`Wrote ${successLabel} to ${absPath}`); @@ -223,11 +208,7 @@ function writeTempPayload(payload: unknown, prefix: string): string { return payloadFile; } -function withTempPayload( - payload: unknown, - prefix: string, - fn: (payloadFile: string) => T, -): T { +function withTempPayload(payload: unknown, prefix: string, fn: (payloadFile: string) => T): T { const tempDir = mkdtempSync(path.join(tmpdir(), prefix)); const payloadFile = path.join(tempDir, "payload.json"); @@ -291,12 +272,7 @@ function downloadSettings(owner: string, repo: string, filePath: string): void { writeJsonFile(filePath, payload, "repository settings"); } -function uploadSettings( - owner: string, - repo: string, - filePath: string, - dryRun: boolean, -): void { +function uploadSettings(owner: string, repo: string, filePath: string, dryRun: boolean): void { const settingsFile = readSettingsFile(filePath); const payload = buildPatchPayload(settingsFile.settings); @@ -314,14 +290,7 @@ function uploadSettings( } withTempPayload(payload, "ts-apt-repo-settings-", (payloadFile) => { - runGh([ - "api", - "-X", - "PATCH", - `repos/${owner}/${repo}`, - "--input", - payloadFile, - ]); + runGh(["api", "-X", "PATCH", `repos/${owner}/${repo}`, "--input", payloadFile]); }); logSuccess(`Uploaded repository settings to ${owner}/${repo}`); @@ -342,24 +311,17 @@ function fetchRulesets(owner: string, repo: string): Ruleset[] { const list = JSON.parse(listRaw) as Array<{ id: number }>; return list.map((entry) => { - const detailRaw = runGh([ - "api", - `/repos/${owner}/${repo}/rulesets/${entry.id}`, - ]); + const detailRaw = runGh(["api", `/repos/${owner}/${repo}/rulesets/${entry.id}`]); return JSON.parse(detailRaw) as Ruleset; }); } -function mergeBypassActors( - ruleset: Ruleset, - appsBypassActors: AppBypassConfig[], -): BypassActor[] { +function mergeBypassActors(ruleset: Ruleset, appsBypassActors: AppBypassConfig[]): BypassActor[] { const desiredActors = [...ruleset.bypass_actors]; for (const cfg of appsBypassActors) { const exists = desiredActors.some( - (actor) => - actor.actor_id === cfg.appId && actor.actor_type === "Integration", + (actor) => actor.actor_id === cfg.appId && actor.actor_type === "Integration", ); if (exists) { @@ -416,12 +378,7 @@ function downloadRulesets(owner: string, repo: string, filePath: string): void { logInfo(`Found ${rulesets.length} ruleset(s).`); } -function uploadRulesets( - owner: string, - repo: string, - filePath: string, - dryRun: boolean, -): void { +function uploadRulesets(owner: string, repo: string, filePath: string, dryRun: boolean): void { const config = readRulesetsFile(filePath); if (config.rulesets.length === 0) { @@ -431,9 +388,7 @@ function uploadRulesets( logInfo(`Fetching current rulesets for ${owner}/${repo}...`); const remoteRulesets = fetchRulesets(owner, repo); - const remoteById = new Map( - remoteRulesets.map((ruleset) => [ruleset.id, ruleset]), - ); + const remoteById = new Map(remoteRulesets.map((ruleset) => [ruleset.id, ruleset])); if (remoteRulesets.length === 0) { logWarn("No rulesets found in the repository."); @@ -448,37 +403,22 @@ function uploadRulesets( } const payload = buildRulesetPutPayload(ruleset, config.appsBypassActors); - logInfo( - `Ruleset "${ruleset.name}" (${ruleset.id}): syncing full definition...`, - ); + logInfo(`Ruleset "${ruleset.name}" (${ruleset.id}): syncing full definition...`); if (dryRun) { - logInfo( - `Dry-run mode: would PUT with ${JSON.stringify(payload, null, 2)}`, - ); + logInfo(`Dry-run mode: would PUT with ${JSON.stringify(payload, null, 2)}`); continue; } runGh( - [ - "api", - "-X", - "PUT", - `/repos/${owner}/${repo}/rulesets/${ruleset.id}`, - "--input", - "-", - ], + ["api", "-X", "PUT", `/repos/${owner}/${repo}/rulesets/${ruleset.id}`, "--input", "-"], JSON.stringify(payload), ); logSuccess(`Ruleset "${ruleset.name}" (${ruleset.id}) synchronized.`); } - logSuccess( - dryRun - ? "Dry-run complete. No remote changes were made." - : "Rulesets applied successfully.", - ); + logSuccess(dryRun ? "Dry-run complete. No remote changes were made." : "Rulesets applied successfully."); } function readVariablesFile(filePath: string): VariablesFile { @@ -500,11 +440,7 @@ function fetchVariables(owner: string, repo: string): RepoVariable[] { .sort((left, right) => left.name.localeCompare(right.name)); } -function downloadVariables( - owner: string, - repo: string, - filePath: string, -): void { +function downloadVariables(owner: string, repo: string, filePath: string): void { logInfo(`Downloading repository variables for ${owner}/${repo}...`); const payload: VariablesFile = { owner, @@ -516,17 +452,10 @@ function downloadVariables( writeJsonFile(filePath, payload, "repository variables"); } -function uploadVariables( - owner: string, - repo: string, - filePath: string, - dryRun: boolean, -): void { +function uploadVariables(owner: string, repo: string, filePath: string, dryRun: boolean): void { const config = readVariablesFile(filePath); const remoteVariables = fetchVariables(owner, repo); - const remoteByName = new Map( - remoteVariables.map((variable) => [variable.name, variable]), - ); + const remoteByName = new Map(remoteVariables.map((variable) => [variable.name, variable])); if (config.variables.length === 0) { logWarn("No repository variables configured; nothing to apply."); @@ -540,14 +469,7 @@ function uploadVariables( logInfo(`Repository variable "${variable.name}": creating...`); if (!dryRun) { runGh( - [ - "api", - "-X", - "POST", - `/repos/${owner}/${repo}/actions/variables`, - "--input", - "-", - ], + ["api", "-X", "POST", `/repos/${owner}/${repo}/actions/variables`, "--input", "-"], JSON.stringify(variable), ); } @@ -562,23 +484,14 @@ function uploadVariables( logInfo(`Repository variable "${variable.name}": updating...`); if (!dryRun) { runGh( - [ - "api", - "-X", - "PATCH", - `/repos/${owner}/${repo}/actions/variables/${variable.name}`, - "--input", - "-", - ], + ["api", "-X", "PATCH", `/repos/${owner}/${repo}/actions/variables/${variable.name}`, "--input", "-"], JSON.stringify({ value: variable.value }), ); } } logSuccess( - dryRun - ? "Dry-run complete. No remote changes were made." - : "Repository variables applied successfully.", + dryRun ? "Dry-run complete. No remote changes were made." : "Repository variables applied successfully.", ); } @@ -615,12 +528,7 @@ function downloadTags(owner: string, repo: string, filePath: string): void { writeJsonFile(filePath, payload, "repository tags"); } -function uploadTags( - owner: string, - repo: string, - filePath: string, - dryRun: boolean, -): void { +function uploadTags(owner: string, repo: string, filePath: string, dryRun: boolean): void { const config = readTagsFile(filePath); const remoteTags = fetchTags(owner, repo); const remoteByName = new Map(remoteTags.map((tag) => [tag.name, tag])); @@ -637,14 +545,7 @@ function uploadTags( logInfo(`Repository tag "${tag.name}": creating at ${tag.sha}...`); if (!dryRun) { runGh( - [ - "api", - "-X", - "POST", - `/repos/${owner}/${repo}/git/refs`, - "--input", - "-", - ], + ["api", "-X", "POST", `/repos/${owner}/${repo}/git/refs`, "--input", "-"], JSON.stringify({ ref: `refs/tags/${tag.name}`, sha: tag.sha }), ); } @@ -663,16 +564,11 @@ function uploadTags( } logSuccess( - dryRun - ? "Dry-run complete. No remote changes were made." - : "Repository tags applied successfully.", + dryRun ? "Dry-run complete. No remote changes were made." : "Repository tags applied successfully.", ); } -function resolveFilePath( - target: Exclude, - fileOverride?: string, -): string { +function resolveFilePath(target: Exclude, fileOverride?: string): string { return fileOverride?.trim() || DEFAULT_TARGET_PATHS[target]; } @@ -730,17 +626,10 @@ function applyTargets( ): void { if (target === "all") { if (fileOverride?.trim()) { - fail( - "--file cannot be used with --target all. Use the default target files instead.", - ); + fail("--file cannot be used with --target all. Use the default target files instead."); } - for (const currentTarget of [ - "settings", - "rulesets", - "vars", - "tags", - ] as const) { + for (const currentTarget of ["settings", "rulesets", "vars", "tags"] as const) { applyTarget(action, currentTarget, owner, repo, undefined, dryRun); } return; @@ -774,14 +663,8 @@ const sharedOptions = defineOptions( .enum(["settings", "rulesets", "vars", "tags", "all"]) .default("all") .describe("Metadata target to sync"), - file: z - .string() - .optional() - .describe("Override the default JSON file path for a single target"), - dryRun: z - .boolean() - .default(false) - .describe("Print changes without applying them"), + file: z.string().optional().describe("Override the default JSON file path for a single target"), + dryRun: z.boolean().default(false).describe("Print changes without applying them"), }), { o: "owner", r: "repo", t: "target", f: "file", d: "dryRun" }, ); @@ -790,14 +673,7 @@ const downloadCommand = defineCommand({ description: "Download GitHub repository metadata to tracked JSON files", options: sharedOptions, action: async (options) => { - applyTargets( - "download", - options.target, - options.owner, - options.repo, - options.file, - options.dryRun, - ); + applyTargets("download", options.target, options.owner, options.repo, options.file, options.dryRun); }, }); @@ -805,14 +681,7 @@ const uploadCommand = defineCommand({ description: "Upload GitHub repository metadata from tracked JSON files", options: sharedOptions, action: async (options) => { - applyTargets( - "upload", - options.target, - options.owner, - options.repo, - options.file, - options.dryRun, - ); + applyTargets("upload", options.target, options.owner, options.repo, options.file, options.dryRun); }, }); diff --git a/scripts/dev/hotfix_pr.mts b/scripts/dev/hotfix_pr.mts index 5baeff3..5899aeb 100755 --- a/scripts/dev/hotfix_pr.mts +++ b/scripts/dev/hotfix_pr.mts @@ -20,22 +20,12 @@ import { type BranchName = string; function branchExistsOnOrigin(branch: BranchName): boolean { - return tryRun( - "git", - ["ls-remote", "--exit-code", "--heads", "origin", branch], - { cwd: ROOT_DIR }, - ).ok; + return tryRun("git", ["ls-remote", "--exit-code", "--heads", "origin", branch], { cwd: ROOT_DIR }).ok; } -function createOrCheckoutBranch( - issueId: string, - checkoutBranch: BranchName, - baseBranch: BranchName, -): void { +function createOrCheckoutBranch(issueId: string, checkoutBranch: BranchName, baseBranch: BranchName): void { if (branchExistsOnOrigin(checkoutBranch)) { - logInfo( - `Branch ${checkoutBranch} exists. Checking out and merging ${baseBranch}...`, - ); + logInfo(`Branch ${checkoutBranch} exists. Checking out and merging ${baseBranch}...`); run("git", ["checkout", checkoutBranch], { cwd: ROOT_DIR }); run("git", ["pull", "origin", checkoutBranch], { cwd: ROOT_DIR }); run("git", ["merge", baseBranch], { cwd: ROOT_DIR }); @@ -64,12 +54,7 @@ function commitIfNeeded(message: string): void { process.exit(commit.status ?? 1); } -function pushChanges( - issueId: string, - fixType: string, - syncBranch: BranchName, - syncBase: BranchName, -): void { +function pushChanges(issueId: string, fixType: string, syncBranch: BranchName, syncBase: BranchName): void { const message = `${fixType}: resolve critical production issue in #${issueId}`; commitIfNeeded(message); @@ -96,20 +81,9 @@ function pushChanges( logSuccess(`PR already exists: ${prUrl}`); } else { logInfo("No PR found. Creating new PR..."); - run( - "gh", - [ - "pr", - "create", - "--head", - syncBranch, - "--base", - syncBase, - "--title", - message, - ], - { cwd: ROOT_DIR }, - ); + run("gh", ["pr", "create", "--head", syncBranch, "--base", syncBase, "--title", message], { + cwd: ROOT_DIR, + }); } logInfo(`Pushing changes from ${syncBase} to ${syncBranch}...`); @@ -118,10 +92,7 @@ function pushChanges( const hotfixCommand = defineCommand({ description: "Create or update hotfix and sync branches for an issue", - args: z.tuple([ - z.string().describe("Issue ID"), - z.string().describe("Target branch"), - ]), + args: z.tuple([z.string().describe("Issue ID"), z.string().describe("Target branch")]), action: async (_options, args) => { const [issueId, base] = args; diff --git a/scripts/dev/node_ver.mts b/scripts/dev/node_ver.mts index a6a5e58..f734462 100755 --- a/scripts/dev/node_ver.mts +++ b/scripts/dev/node_ver.mts @@ -63,22 +63,18 @@ function listYamlFiles(rootPath: string): string[] { } function buildReferenceSpecs(nodeMajor: string): ReferenceSpec[] { - const yamlReferenceSpecs: ReferenceSpec[] = listYamlFiles(rootDir).map( - (filePath) => ({ - path: filePath, - replacements: [ - [/node-version:\s*\[\d+(?:\.x)?\]/g, `node-version: [${nodeMajor}]`], - [/node-version:\s*\d+(?:\.x)?/g, `node-version: ${nodeMajor}`], - ], - }), - ); + const yamlReferenceSpecs: ReferenceSpec[] = listYamlFiles(rootDir).map((filePath) => ({ + path: filePath, + replacements: [ + [/node-version:\s*\[\d+(?:\.x)?\]/g, `node-version: [${nodeMajor}]`], + [/node-version:\s*\d+(?:\.x)?/g, `node-version: ${nodeMajor}`], + ], + })); return [ { path: `${rootDir}/package.json`, - replacements: [ - [/"node":\s*">=\d+(?:\.\d+\.\d+)?"/g, `"node": ">=${nodeMajor}"`], - ], + replacements: [[/"node":\s*">=\d+(?:\.\d+\.\d+)?"/g, `"node": ">=${nodeMajor}"`]], }, { path: `${rootDir}/scripts/devopslib.mts`, @@ -97,10 +93,7 @@ function buildReferenceSpecs(nodeMajor: string): ReferenceSpec[] { ]; } -function applyReplacements( - content: string, - replacements: ReferenceSpec["replacements"], -): string { +function applyReplacements(content: string, replacements: ReferenceSpec["replacements"]): string { let updated = content; for (const [pattern, replacement] of replacements) { updated = updated.replace(pattern, replacement); @@ -160,9 +153,7 @@ async function getAvailableNodeMajors(): Promise { }); if (!response.ok) { - fail( - `Unable to read Node.js versions from npm registry: HTTP ${response.status}`, - ); + fail(`Unable to read Node.js versions from npm registry: HTTP ${response.status}`); } packageJson = (await response.json()) as NodeRegistryPackage; @@ -179,9 +170,7 @@ async function getAvailableNodeMajors(): Promise { return [ ...new Set( - versions - .map((version) => String(version).split(".")[0] ?? "") - .filter((major) => major.length > 0), + versions.map((version) => String(version).split(".")[0] ?? "").filter((major) => major.length > 0), ), ] .filter((major) => /^\d+$/.test(major)) @@ -206,15 +195,11 @@ function runScriptWithOptionalSudo(scriptText: string): void { }); if (result.error) { - throw new Error( - `Failed to execute NodeSource setup script: ${result.error.message}`, - ); + throw new Error(`Failed to execute NodeSource setup script: ${result.error.message}`); } if (result.status !== 0) { - throw new Error( - `NodeSource setup script failed with exit code ${result.status ?? "unknown"}.`, - ); + throw new Error(`NodeSource setup script failed with exit code ${result.status ?? "unknown"}.`); } } @@ -233,9 +218,7 @@ async function setupNodeSourceRepo(nodeMajor: string): Promise { }); if (!response.ok) { - fail( - `Unable to download NodeSource setup script: HTTP ${response.status}`, - ); + fail(`Unable to download NodeSource setup script: HTTP ${response.status}`); } scriptText = await response.text(); @@ -273,14 +256,10 @@ function installNodejs(nodeMajor: string): void { const fullNodeVersion = runCaptureOutput("node", ["-v"], { cwd: rootDir }); if (!fullNodeVersion.startsWith(`v${nodeMajor}.`)) { - fail( - `Installed Node.js version (${fullNodeVersion}) does not match expected major v${nodeMajor}.`, - ); + fail(`Installed Node.js version (${fullNodeVersion}) does not match expected major v${nodeMajor}.`); } - logSuccess( - `Node.js ${fullNodeVersion} installed/updated for major ${nodeMajor}.`, - ); + logSuccess(`Node.js ${fullNodeVersion} installed/updated for major ${nodeMajor}.`); } function verifyInstalledNodeMajor(nodeMajor: string): void { @@ -291,9 +270,7 @@ function verifyInstalledNodeMajor(nodeMajor: string): void { const fullNodeVersion = String(nodeResult.stdout ?? "").trim(); if (!fullNodeVersion.startsWith(`v${nodeMajor}.`)) { - fail( - `Installed Node.js version (${fullNodeVersion}) does not match expected major v${nodeMajor}.`, - ); + fail(`Installed Node.js version (${fullNodeVersion}) does not match expected major v${nodeMajor}.`); } logSuccess(`Installed Node.js version matches major ${nodeMajor}.`); @@ -302,14 +279,8 @@ function verifyInstalledNodeMajor(nodeMajor: string): void { const nodeVersionOptions = defineOptions( z .object({ - verify: z - .boolean() - .default(false) - .describe("Verify version references and installed Node.js"), - update: z - .boolean() - .default(false) - .describe("Update version references and install Node.js"), + verify: z.boolean().default(false).describe("Verify version references and installed Node.js"), + update: z.boolean().default(false).describe("Update version references and install Node.js"), }) .refine((options) => options.verify !== options.update, { message: "Specify exactly one of --verify or --update.", diff --git a/scripts/dev/setup_devenv.mts b/scripts/dev/setup_devenv.mts index cd60baa..3041cd5 100755 --- a/scripts/dev/setup_devenv.mts +++ b/scripts/dev/setup_devenv.mts @@ -35,13 +35,7 @@ type SetupDevEnvOptions = { ide: SupportedIde; }; -const SUPPORTED_IDES: SupportedIde[] = [ - "vscode", - "cursor", - "windsurf", - "zed", - "jetbrains", -]; +const SUPPORTED_IDES: SupportedIde[] = ["vscode", "cursor", "windsurf", "zed", "jetbrains"]; const IDE_TARGET_DIRS: Record = { vscode: ".vscode", @@ -59,11 +53,7 @@ const IDE_SETTINGS_RELPATHS: Partial> = { }; const AGENTS_MD_SETTING_KEY = "chat.useAgentsMdFile"; -const AGENTS_MD_SUPPORTED_IDES: ReadonlySet = new Set([ - "vscode", - "cursor", - "windsurf", -]); +const AGENTS_MD_SUPPORTED_IDES: ReadonlySet = new Set(["vscode", "cursor", "windsurf"]); type JsonPrimitive = null | boolean | number | string; interface JsonArray extends Array {} @@ -76,10 +66,7 @@ function isJsonRecord(value: unknown): value is JsonRecord { return value !== null && typeof value === "object" && !Array.isArray(value); } -function mergeJsonArrays( - existing: JsonLike[], - template: JsonLike[], -): JsonLike[] { +function mergeJsonArrays(existing: JsonLike[], template: JsonLike[]): JsonLike[] { const merged = [...existing]; const seen = new Set(existing.map((item) => JSON.stringify(item))); @@ -94,10 +81,7 @@ function mergeJsonArrays( return merged; } -function mergeJsonWithTemplate( - existing: JsonLike, - template: JsonLike, -): JsonLike { +function mergeJsonWithTemplate(existing: JsonLike, template: JsonLike): JsonLike { if (Array.isArray(existing) && Array.isArray(template)) { return mergeJsonArrays(existing, template); } @@ -123,13 +107,7 @@ function mergeJsonWithTemplate( } function applyIdeTemplate(ide: SupportedIde): void { - const templateDir = path.join( - ROOT_DIR, - "scripts", - "dev", - "ide_templates", - ide, - ); + const templateDir = path.join(ROOT_DIR, "scripts", "dev", "ide_templates", ide); if (!existsSync(templateDir)) { fail(`IDE template directory does not exist: ${templateDir}`); } @@ -187,14 +165,7 @@ function ensureIdeSettingsFile(ide: SupportedIde): string | null { const ideSettingsDir = path.dirname(ideSettingsPath); ensureDirExists(ideSettingsDir); - const templateSettingsPath = path.join( - ROOT_DIR, - "scripts", - "dev", - "ide_templates", - ide, - "settings.json", - ); + const templateSettingsPath = path.join(ROOT_DIR, "scripts", "dev", "ide_templates", ide, "settings.json"); if (!existsSync(templateSettingsPath)) { if (!existsSync(ideSettingsPath)) { @@ -213,10 +184,7 @@ function ensureIdeSettingsFile(ide: SupportedIde): string | null { } const existingSettingsJson = readJsonFile(ideSettingsPath) as JsonLike; - const mergedSettingsJson = mergeJsonWithTemplate( - existingSettingsJson, - templateSettingsJson, - ); + const mergedSettingsJson = mergeJsonWithTemplate(existingSettingsJson, templateSettingsJson); writeJsonFile(ideSettingsPath, mergedSettingsJson); return ideSettingsPath; @@ -237,9 +205,7 @@ function ensureAgentsFilesUsed(ide: SupportedIde): void { if (!ideSettingsJson[AGENTS_MD_SETTING_KEY]) { ideSettingsJson[AGENTS_MD_SETTING_KEY] = true; writeJsonFile(ideSettingsPath, ideSettingsJson); - logWarn( - `Updated ${ideSettingsPath} to enable chat.useAgentsMdFile for AGENTS.md support.`, - ); + logWarn(`Updated ${ideSettingsPath} to enable chat.useAgentsMdFile for AGENTS.md support.`); } } @@ -260,9 +226,7 @@ function ensureAptDependencies(): void { const missingPackages = [ ...new Set( - aptPackages - .filter(([command]) => !commandExists(command)) - .map(([, packageName]) => packageName), + aptPackages.filter(([command]) => !commandExists(command)).map(([, packageName]) => packageName), ), ]; @@ -282,23 +246,17 @@ function ensureAptDependencies(): void { function ensureNodeDependencies(nodeVersionMajor: string): void { if (!commandExists("npm")) { - fail( - `npm is required but not installed. Install Node.js >=${nodeVersionMajor} first.`, - ); + fail(`npm is required but not installed. Install Node.js >=${nodeVersionMajor} first.`); } // Fast deterministic install path aligned with lockfile-based CI behavior. - logInfo( - "Installing npm dependencies (including devDependencies) via npm ci...", - ); + logInfo("Installing npm dependencies (including devDependencies) via npm ci..."); run("npm", ["ci", "--include=dev"]); } function warnIfDockerMissing(): void { if (commandExists("docker")) { - logSuccess( - "Docker CLI detected for devcontainer-backed integration tests.", - ); + logSuccess("Docker CLI detected for devcontainer-backed integration tests."); return; } @@ -370,14 +328,10 @@ function ensureAuditRemediation(): void { ); runAuditFix(false); counts = getAuditCounts(); - logInfo( - `Audit after npm audit fix: high=${counts.high}, critical=${counts.critical}.`, - ); + logInfo(`Audit after npm audit fix: high=${counts.high}, critical=${counts.critical}.`); if (counts.high === 0 && counts.critical === 0) { - logSuccess( - "High/critical npm vulnerabilities remediated with npm audit fix.", - ); + logSuccess("High/critical npm vulnerabilities remediated with npm audit fix."); return; } @@ -386,9 +340,7 @@ function ensureAuditRemediation(): void { ); runAuditFix(true); counts = getAuditCounts(); - logInfo( - `Audit after npm audit fix --force: high=${counts.high}, critical=${counts.critical}.`, - ); + logInfo(`Audit after npm audit fix --force: high=${counts.high}, critical=${counts.critical}.`); if (counts.high > 0 || counts.critical > 0) { fail( @@ -414,9 +366,7 @@ async function main(options: SetupDevEnvOptions): Promise { logInfo("Ensuring required APT dependencies are installed..."); ensureAptDependencies(); logInfo("Checking optional Docker support..."); - logInfo( - `Setting up development environment for Node.js v${nodeVersionMajor}.x...`, - ); + logInfo(`Setting up development environment for Node.js v${nodeVersionMajor}.x...`); ensureNodeDependencies(nodeVersionMajor); ensureAuditRemediation(); warnIfDockerMissing(); diff --git a/scripts/devopslib.mts b/scripts/devopslib.mts index 170212b..e1ddcf4 100755 --- a/scripts/devopslib.mts +++ b/scripts/devopslib.mts @@ -1,18 +1,7 @@ #!/usr/bin/env -S node --experimental-strip-types -import { - accessSync, - constants, - mkdirSync, - readFileSync, - writeFileSync, -} from "node:fs"; -import { - execFileSync, - spawnSync, - type ExecFileSyncOptions, - type SpawnSyncOptions, -} from "node:child_process"; +import { accessSync, constants, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { execFileSync, spawnSync, type ExecFileSyncOptions, type SpawnSyncOptions } from "node:child_process"; import { defineConfig, processConfig, @@ -90,9 +79,7 @@ export function fail(message: string, exitCode = 1, params: LogParams = {}) { } export function log(message: string, params: LogParams = {}) { - console.log( - `${" ".repeat(params.indent ?? 0)}${params.color ?? ""}${message}${COLORS.reset}`, - ); + console.log(`${" ".repeat(params.indent ?? 0)}${params.color ?? ""}${message}${COLORS.reset}`); } export function logInfo(message: string, params: LogParams = {}) { @@ -111,11 +98,7 @@ export function logError(message: string, params: LogParams = {}) { log(`❌ ${message}`, params); } -export function assertNonEmpty( - value: string, - message: string, - usageMessage?: string, -) { +export function assertNonEmpty(value: string, message: string, usageMessage?: string) { if (!value || value.trim().length === 0) { if (usageMessage) { console.error(usageMessage); @@ -140,9 +123,7 @@ export function commandExists(command: string) { const pathValue = process.env.PATH ?? ""; const dirs = pathValue.split(path.delimiter).filter(Boolean); - return dirs.some((dirPath: string) => - isExecutable(path.join(dirPath, command)), - ); + return dirs.some((dirPath: string) => isExecutable(path.join(dirPath, command))); } export function ensureDirExists(dirPath: string) { @@ -183,9 +164,7 @@ export function readNodeMajorVersion(rootDir = ROOT_DIR): string { fail(".node_ver must contain exactly one non-empty line."); } if (!/^\d+$/.test(rawVersion)) { - fail( - ".node_ver must contain only the Node.js major version (e.g. 20, 24).", - ); + fail(".node_ver must contain only the Node.js major version (e.g. 20, 24)."); } return rawVersion; @@ -249,9 +228,7 @@ export function run( return execFileSync(command, args, options); } catch (error) { const message = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to execute '${command} ${args.join(" ")}'. ${message}`, - ); + throw new Error(`Failed to execute '${command} ${args.join(" ")}'. ${message}`); } } @@ -264,9 +241,7 @@ export function tryRun( const result = spawnSync(command, args, options); if (result.error) { - throw new Error( - `Failed to spawn '${command} ${args.join(" ")}'. ${result.error.message}`, - ); + throw new Error(`Failed to spawn '${command} ${args.join(" ")}'. ${result.error.message}`); } return { @@ -286,9 +261,7 @@ export function runCaptureOutput( const result = spawnSync(command, args, normalizedOptions); if (result.error) { - throw new Error( - `Failed to spawn '${command} ${args.join(" ")}'. ${result.error.message}`, - ); + throw new Error(`Failed to spawn '${command} ${args.join(" ")}'. ${result.error.message}`); } if (result.status !== 0) { throw new Error( @@ -299,10 +272,7 @@ export function runCaptureOutput( return String(result.stdout ?? "").trim(); } -type CliAction = ( - options: unknown, - args: string[], -) => unknown | Promise; +type CliAction = (options: unknown, args: string[]) => unknown | Promise; type CliResult = ProcessResult>; @@ -340,9 +310,7 @@ export function createCliConfig(params: CliConfigParams) { } } - fail( - "createCliConfig requires either a non-empty 'name' or a valid 'importMetaUrl'.", - ); + fail("createCliConfig requires either a non-empty 'name' or a valid 'importMetaUrl'."); })(); return defineConfig({ @@ -356,19 +324,13 @@ export function createCliConfig(params: CliConfigParams) { }); } -export async function runCli( - cliConfig: CliConfig, - argv: string[] = process.argv.slice(2), -): Promise { +export async function runCli(cliConfig: CliConfig, argv: string[] = process.argv.slice(2)): Promise { const result = processConfig(cliConfig, argv) as CliResult; const commandResult = result as unknown as CliResultCommand; await commandResult.command.action(commandResult.options, commandResult.args); } -export async function runMain( - action: () => void | Promise, - exitCode = 1, -): Promise { +export async function runMain(action: () => void | Promise, exitCode = 1): Promise { try { await action(); } catch (error) { @@ -395,9 +357,7 @@ export async function confirmPrompt( .replace(/\\([^\\])/g, "$1"); while (true) { const answer = await rl - .question( - `${message} [${getText(yesPattern)} | ${getText(noPattern)}] `, - ) + .question(`${message} [${getText(yesPattern)} | ${getText(noPattern)}] `) .then((ans: string) => ans.trim().toLowerCase()); if (yesPattern.test(answer) || answer.trim() === "") { return; diff --git a/scripts/ops/check_configs.mts b/scripts/ops/check_configs.mts index 8deabf3..c92d0b6 100755 --- a/scripts/ops/check_configs.mts +++ b/scripts/ops/check_configs.mts @@ -1,14 +1,6 @@ #!/usr/bin/env -S node --experimental-strip-types -import { - fail, - logInfo, - logSuccess, - logError, - readJsonFile, - ROOT_DIR, - REPO_SLUG, -} from "../devopslib.mts"; +import { fail, logInfo, logSuccess, logError, readJsonFile, ROOT_DIR, REPO_SLUG } from "../devopslib.mts"; type ErrorCollector = { errors: string[]; @@ -18,9 +10,7 @@ type ErrorCollector = { type JsonRecord = Record; const packageJson = readJsonFile(`${ROOT_DIR}/package.json`) as JsonRecord; -const vscodeSettingsJson = readJsonFile( - `${ROOT_DIR}/.vscode/settings.json`, -) as JsonRecord; +const vscodeSettingsJson = readJsonFile(`${ROOT_DIR}/.vscode/settings.json`) as JsonRecord; const tsconfigJson = readJsonFile(`${ROOT_DIR}/tsconfig.json`) as JsonRecord; const typedocJson = readJsonFile(`${ROOT_DIR}/typedoc.json`) as JsonRecord; @@ -68,10 +58,7 @@ function tokenizePath(pathExpression: string): Array { return tokens; } -function getByPath( - root: unknown, - pathExpression: string, -): { found: boolean; value: unknown } { +function getByPath(root: unknown, pathExpression: string): { found: boolean; value: unknown } { const tokens = tokenizePath(pathExpression); let cursor = root; @@ -134,9 +121,7 @@ function expectFieldType( const actualType = valueType(result.value); if (actualType !== expectedType) { - addError( - `field '${fieldPath}' type '${actualType}' does not match expected '${expectedType}'`, - ); + addError(`field '${fieldPath}' type '${actualType}' does not match expected '${expectedType}'`); } } @@ -167,12 +152,7 @@ function validatePackageJson(json: JsonRecord): string[] { expectFieldValue(json, "name", "ts-apt", add); expectFieldValue(json, "license", "Apache-2.0", add); expectFieldValue(json, "repository.type", "git", add); - expectFieldValue( - json, - "repository.url", - `https://github.com/${REPO_SLUG}.git`, - add, - ); + expectFieldValue(json, "repository.url", `https://github.com/${REPO_SLUG}.git`, add); return errors; } diff --git a/scripts/ops/check_deps_pin.mts b/scripts/ops/check_deps_pin.mts index 813caac..d46ad6d 100644 --- a/scripts/ops/check_deps_pin.mts +++ b/scripts/ops/check_deps_pin.mts @@ -19,20 +19,11 @@ import fs from "node:fs"; import path from "node:path"; -import { - fail, - logError, - logSuccess, - readJsonFile, - ROOT_DIR, - runMain, - usage, -} from "../devopslib.mts"; +import { fail, logError, logSuccess, readJsonFile, ROOT_DIR, runMain, usage } from "../devopslib.mts"; const SHA_PIN_PATTERN = /#([0-9a-f]{40})$/; /** Exact semver version, optionally prerelease; rejects ranges and tags. */ -const EXACT_VERSION_PATTERN = - /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z.-]+)?$/; +const EXACT_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z.-]+)?$/; const NPM_REGISTRY_URL = "https://registry.npmjs.org/"; /** Parsed package.json, limited to the fields the check reads. */ @@ -94,8 +85,7 @@ export function checkDependencyPin( ); } - const spec = - packageJson.dependencies?.[name] ?? packageJson.devDependencies?.[name]; + const spec = packageJson.dependencies?.[name] ?? packageJson.devDependencies?.[name]; if (spec === undefined) { problems.push(`package.json has no ${name} dependency.`); return problems; @@ -112,29 +102,21 @@ export function checkDependencyPin( 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}'.`, - ); + 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}'.`, - ); + problems.push(`package-lock.json ${name} is a link to '${entry.resolved}'.`); } else if (sha !== undefined) { if (!entry.resolved?.endsWith(`#${sha}`)) { - problems.push( - `package-lock.json ${name} resolved '${entry.resolved}' does not match pin ${sha}.`, - ); + problems.push(`package-lock.json ${name} resolved '${entry.resolved}' does not match pin ${sha}.`); } } else { if (entry.version !== spec) { - problems.push( - `package-lock.json ${name} version '${entry.version}' does not match pin ${spec}.`, - ); + problems.push(`package-lock.json ${name} version '${entry.version}' does not match pin ${spec}.`); } if (!entry.resolved?.startsWith(NPM_REGISTRY_URL)) { problems.push( @@ -155,16 +137,10 @@ function main(): void { fail(usage(import.meta.url, " [...]")); } - const packageJson = readJsonFile( - path.join(ROOT_DIR, "package.json"), - ) as PackageJson; - const packageLock = readJsonFile( - path.join(ROOT_DIR, "package-lock.json"), - ) as PackageLock; + 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), - ); + const problems = names.flatMap((name) => checkDependencyPin(name, packageJson, packageLock)); for (const problem of problems) { logError(problem); } diff --git a/scripts/ops/check_latest_action_pin.mts b/scripts/ops/check_latest_action_pin.mts index 3fea2ad..5715134 100755 --- a/scripts/ops/check_latest_action_pin.mts +++ b/scripts/ops/check_latest_action_pin.mts @@ -5,11 +5,7 @@ import path from "node:path"; import process from "node:process"; import { defineCommand, defineOptions } from "@robingenz/zli"; import { z } from "zod"; -import { - loadActionRepoData, - parseActionRepoSlug, - resolveActionRefSha, -} from "./opslib.mts"; +import { loadActionRepoData, parseActionRepoSlug, resolveActionRefSha } from "./opslib.mts"; import { COLORS, createCliConfig, @@ -78,21 +74,13 @@ async function fetchJson(url: string): Promise { return response.json(); } -async function getLatestRelease( - repoSlug: string, -): Promise { - const release = await fetchJson( - `${GITHUB_API_BASE_URL}/repos/${repoSlug}/releases/latest`, - ); - return release && typeof release === "object" - ? (release as GitHubRelease) - : null; +async function getLatestRelease(repoSlug: string): Promise { + const release = await fetchJson(`${GITHUB_API_BASE_URL}/repos/${repoSlug}/releases/latest`); + return release && typeof release === "object" ? (release as GitHubRelease) : null; } async function getReleases(repoSlug: string): Promise { - const releases = await fetchJson( - `${GITHUB_API_BASE_URL}/repos/${repoSlug}/releases?per_page=100`, - ); + const releases = await fetchJson(`${GITHUB_API_BASE_URL}/repos/${repoSlug}/releases?per_page=100`); return Array.isArray(releases) ? releases : []; } @@ -101,11 +89,7 @@ function findReleaseBySha( tagMap: Map, sha: string, ): GitHubRelease | null { - return ( - releases.find( - (release) => tagMap.get(release.tag_name)?.toLowerCase() === sha, - ) ?? null - ); + return releases.find((release) => tagMap.get(release.tag_name)?.toLowerCase() === sha) ?? null; } function walkFiles(rootDir: string): string[] { @@ -142,11 +126,7 @@ function findActionPins(rootDir: string): ActionPin[] { } const spec = match[1] ?? ""; - if ( - !spec.includes("@") || - spec.startsWith("./") || - spec.startsWith("docker://") - ) { + if (!spec.includes("@") || spec.startsWith("./") || spec.startsWith("docker://")) { continue; } @@ -179,10 +159,7 @@ async function getRepoData(repoSlug: string): Promise { const promise = (async (): Promise => { const { tagToSha: tagMap } = loadActionRepoData(repoSlug, process.cwd()); - const [latestRelease, releases] = await Promise.all([ - getLatestRelease(repoSlug), - getReleases(repoSlug), - ]); + const [latestRelease, releases] = await Promise.all([getLatestRelease(repoSlug), getReleases(repoSlug)]); return { tagMap, latestRelease, releases }; })(); @@ -190,25 +167,15 @@ async function getRepoData(repoSlug: string): Promise { return promise; } -async function analyzeAction( - actionPath: string, - currentRef: string, -): Promise { +async function analyzeAction(actionPath: string, currentRef: string): Promise { const repoSlug = parseActionRepoSlug(actionPath); const { tagMap, latestRelease, releases } = await getRepoData(repoSlug); - const currentSha = resolveActionRefSha( - repoSlug, - currentRef, - tagMap, - process.cwd(), - ); + const currentSha = resolveActionRefSha(repoSlug, currentRef, tagMap, process.cwd()); if (!currentSha) { throw new Error(`Unable to resolve ref '${currentRef}' for ${repoSlug}.`); } const currentRelease = findReleaseBySha(releases, tagMap, currentSha); - const latestSha = latestRelease - ? (tagMap.get(latestRelease.tag_name) ?? null) - : null; + const latestSha = latestRelease ? (tagMap.get(latestRelease.tag_name) ?? null) : null; const isCurrent = latestSha !== null && latestSha === currentSha; return { @@ -235,21 +202,10 @@ function printAnalysis(analysis: ActionAnalysis): number { logFieldValue("Current ref", analysis.currentRef); logFieldValue("Current sha", analysis.currentSha); logFieldValue("Current release", analysis.currentRelease?.tag_name ?? "none"); - logFieldValue( - "Current release URL", - analysis.currentRelease?.html_url ?? "none", - ); + logFieldValue("Current release URL", analysis.currentRelease?.html_url ?? "none"); logFieldValue("Latest release", analysis.latestRelease?.tag_name ?? "none"); - logFieldValue( - "Latest pin", - analysis.latestSha - ? `${analysis.actionPath}@${analysis.latestSha}` - : "none", - ); - logFieldValue( - "Latest release URL", - analysis.latestRelease?.html_url ?? "none", - ); + logFieldValue("Latest pin", analysis.latestSha ? `${analysis.actionPath}@${analysis.latestSha}` : "none"); + logFieldValue("Latest release URL", analysis.latestRelease?.html_url ?? "none"); if (analysis.latestSha === null) { logError(`Status: no release found`); return 1; @@ -266,10 +222,7 @@ const checkLatestActionPinCommand = defineCommand({ description: "Check whether a GitHub Actions reference is up to date", options: defineOptions( z.object({ - scan: z - .boolean() - .default(false) - .describe("Scan a directory for pinned GitHub Actions references"), + scan: z.boolean().default(false).describe("Scan a directory for pinned GitHub Actions references"), }), { s: "scan" }, ), @@ -312,9 +265,7 @@ const checkLatestActionPinCommand = defineCommand({ const singleArg = args[0] ?? ""; const atIndex = singleArg.lastIndexOf("@"); if (atIndex <= 0 || atIndex === singleArg.length - 1) { - fail( - "Action reference must be provided as or .", - ); + fail("Action reference must be provided as or ."); } actionPath = singleArg.slice(0, atIndex); @@ -323,14 +274,10 @@ const checkLatestActionPinCommand = defineCommand({ actionPath = args[0] ?? ""; currentRef = args[1] ?? ""; if (actionPath.length === 0 || currentRef.length === 0) { - fail( - "Action reference must be provided as or .", - ); + fail("Action reference must be provided as or ."); } } else { - fail( - "Action reference must be provided as or .", - ); + fail("Action reference must be provided as or ."); } const analysis = await analyzeAction(actionPath, currentRef); diff --git a/scripts/ops/check_release_tag.mts b/scripts/ops/check_release_tag.mts index 5f7c5ef..1745878 100755 --- a/scripts/ops/check_release_tag.mts +++ b/scripts/ops/check_release_tag.mts @@ -1,11 +1,5 @@ #!/usr/bin/env -S node --experimental-strip-types -import { - fail, - logInfo, - logSuccess, - readJsonFile, - ROOT_DIR, -} from "../devopslib.mts"; +import { fail, logInfo, logSuccess, readJsonFile, ROOT_DIR } from "../devopslib.mts"; const tag: string | undefined = process.env.RELEASE_TAG; if (!tag) { @@ -14,17 +8,12 @@ if (!tag) { } if (!/^v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(tag)) { - fail( - `Release tag '${tag}' is invalid. Expected semantic version format like v1.2.3 or v1.2.3-rc.1`, - ); + fail(`Release tag '${tag}' is invalid. Expected semantic version format like v1.2.3 or v1.2.3-rc.1`); } const packageJsonPath = `${ROOT_DIR}/package.json`; const packageJson = readJsonFile(packageJsonPath) as { version?: string }; -if ( - typeof packageJson.version !== "string" || - packageJson.version.length === 0 -) { +if (typeof packageJson.version !== "string" || packageJson.version.length === 0) { fail("package.json version is missing or invalid."); } @@ -35,6 +24,4 @@ if (tag !== expectedTag) { ); } -logSuccess( - `Release preflight passed for tag ${tag} and package version ${packageJson.version}.`, -); +logSuccess(`Release preflight passed for tag ${tag} and package version ${packageJson.version}.`); diff --git a/scripts/ops/opslib.mts b/scripts/ops/opslib.mts index 461435b..27b2f6b 100644 --- a/scripts/ops/opslib.mts +++ b/scripts/ops/opslib.mts @@ -48,9 +48,7 @@ function tryGit( export function parseActionRepoSlug(actionPath: string): string { const parts = actionPath.split("/").filter(Boolean); if (parts.length < 2) { - throw new Error( - `Action path '${actionPath}' must include at least owner/repo.`, - ); + throw new Error(`Action path '${actionPath}' must include at least owner/repo.`); } return `${parts[0]}/${parts[1]}`; @@ -86,10 +84,7 @@ export function formatSemVer(version: SemVer): string { return `v${version.major}.${version.minor}.${version.patch}`; } -export function loadActionRepoData( - repoSlug: string, - cwd = process.cwd(), -): ActionRepoData { +export function loadActionRepoData(repoSlug: string, cwd = process.cwd()): ActionRepoData { const cached = actionRepoCache.get(repoSlug); if (cached) { return cached; diff --git a/scripts/ops/pr_checks.mts b/scripts/ops/pr_checks.mts index 1e666b1..12f4e00 100755 --- a/scripts/ops/pr_checks.mts +++ b/scripts/ops/pr_checks.mts @@ -92,11 +92,7 @@ function extractActionUses(content: string): ActionUse[] { } const spec = match[1] ?? ""; - if ( - !spec.includes("@") || - spec.startsWith("./") || - spec.startsWith("docker://") - ) { + if (!spec.includes("@") || spec.startsWith("./") || spec.startsWith("docker://")) { continue; } @@ -133,16 +129,9 @@ function findActionRefChanges( previousContent: string, currentContent: string, ): ActionRefChange[] { - const previousByPath = groupUsesByActionPath( - extractActionUses(previousContent), - ); - const currentByPath = groupUsesByActionPath( - extractActionUses(currentContent), - ); - const actionPaths = new Set([ - ...previousByPath.keys(), - ...currentByPath.keys(), - ]); + const previousByPath = groupUsesByActionPath(extractActionUses(previousContent)); + const currentByPath = groupUsesByActionPath(extractActionUses(currentContent)); + const actionPaths = new Set([...previousByPath.keys(), ...currentByPath.keys()]); const changes: ActionRefChange[] = []; for (const actionPath of actionPaths) { @@ -170,11 +159,7 @@ function findActionRefChanges( } if (currentUses.length > previousUses.length) { - for ( - let index = previousUses.length; - index < currentUses.length; - index += 1 - ) { + for (let index = previousUses.length; index < currentUses.length; index += 1) { const currentUse = currentUses[index]; if (!currentUse) { continue; @@ -196,15 +181,7 @@ function findActionRefChanges( function getChangedWorkflowYamlFiles(diffRange: string): string[] { const files = runGitNameOnly( - [ - "diff", - "--name-only", - "--diff-filter=ACMR", - diffRange, - "--", - ".github/workflows", - ".github/actions", - ], + ["diff", "--name-only", "--diff-filter=ACMR", diffRange, "--", ".github/workflows", ".github/actions"], ROOT_DIR, ); @@ -223,21 +200,11 @@ function getFileContentAtRef(ref: string, filePath: string): string | null { return result.stdout; } -function validateModifiedActionRefs( - baseRef: string, - headRef: string, -): string[] { +function validateModifiedActionRefs(baseRef: string, headRef: string): string[] { const errors: string[] = []; const warnings: string[] = []; - const fetched = tryGit([ - "fetch", - "--no-tags", - "--depth=200", - "origin", - baseRef, - headRef, - ]); + const fetched = tryGit(["fetch", "--no-tags", "--depth=200", "origin", baseRef, headRef]); if (!fetched.ok) { errors.push( `Unable to fetch refs for action pin checks (${baseRef}, ${headRef}): ${fetched.stderr || "unknown error"}`, @@ -261,18 +228,14 @@ function validateModifiedActionRefs( continue; } - actionRefChanges.push( - ...findActionRefChanges(filePath, previousContent, currentContent), - ); + actionRefChanges.push(...findActionRefChanges(filePath, previousContent, currentContent)); } if (actionRefChanges.length === 0) { return errors; } - logInfo( - `Validating ${actionRefChanges.length} modified GitHub Action reference(s) in PR changes.`, - ); + logInfo(`Validating ${actionRefChanges.length} modified GitHub Action reference(s) in PR changes.`); for (const change of actionRefChanges) { if (!SHA_PIN_PATTERN.test(change.currentRef)) { @@ -288,22 +251,9 @@ function validateModifiedActionRefs( try { const repoSlug = parseActionRepoSlug(change.actionPath); - const { tagToSha, shaToBestVersion } = loadActionRepoData( - repoSlug, - ROOT_DIR, - ); - const previousSha = resolveActionRefSha( - repoSlug, - change.previousRef, - tagToSha, - ROOT_DIR, - ); - const currentSha = resolveActionRefSha( - repoSlug, - change.currentRef, - tagToSha, - ROOT_DIR, - ); + const { tagToSha, shaToBestVersion } = loadActionRepoData(repoSlug, ROOT_DIR); + const previousSha = resolveActionRefSha(repoSlug, change.previousRef, tagToSha, ROOT_DIR); + const currentSha = resolveActionRefSha(repoSlug, change.currentRef, tagToSha, ROOT_DIR); if (!previousSha || !currentSha) { warnings.push( @@ -341,28 +291,19 @@ function validateModifiedActionRefs( return errors; } -const PROHIBITED_PATHS = [ - "docs/api", - "docs/api-md", -]; +const PROHIBITED_PATHS = ["docs/api", "docs/api-md"]; function getProhibitedPathsChanges(): string[] { const prohibitedPaths = PROHIBITED_PATHS; - const staged = runGitNameOnly( - ["diff", "--name-only", "--cached", "--", ...prohibitedPaths], - ROOT_DIR, - ); - const unstaged = runGitNameOnly( - ["diff", "--name-only", "--", ...prohibitedPaths], - ROOT_DIR, - ); + const staged = runGitNameOnly(["diff", "--name-only", "--cached", "--", ...prohibitedPaths], ROOT_DIR); + const unstaged = runGitNameOnly(["diff", "--name-only", "--", ...prohibitedPaths], ROOT_DIR); const untracked = runGitNameOnly( ["ls-files", "--others", "--exclude-standard", "--", ...prohibitedPaths], ROOT_DIR, ); - return [...new Set([...staged, ...unstaged, ...untracked])].sort( - (left, right) => left.localeCompare(right), + return [...new Set([...staged, ...unstaged, ...untracked])].sort((left, right) => + left.localeCompare(right), ); } @@ -374,21 +315,11 @@ function resolveRefs(options: PrCheckOptions): { baseRef: string; headRef: string; } { - const baseRef = getNormalizedRef( - options.baseRef || process.env.GITHUB_BASE_REF || "", - ); - const headRef = getNormalizedRef( - options.headRef || process.env.GITHUB_HEAD_REF || "", - ); + const baseRef = getNormalizedRef(options.baseRef || process.env.GITHUB_BASE_REF || ""); + const headRef = getNormalizedRef(options.headRef || process.env.GITHUB_HEAD_REF || ""); - if ( - (baseRef.length > 0 && headRef.length === 0) || - (headRef.length > 0 && baseRef.length === 0) - ) { - fail( - "Both base and head refs must be provided together (flags or environment).", - 1, - ); + if ((baseRef.length > 0 && headRef.length === 0) || (headRef.length > 0 && baseRef.length === 0)) { + fail("Both base and head refs must be provided together (flags or environment).", 1); } return { baseRef, headRef }; @@ -402,9 +333,7 @@ async function runChecks(options: PrCheckOptions): Promise { if (baseRef.length > 0 && headRef.length > 0) { if (baseRef === "main" && headRef !== "staging") { - errors.push( - `Merge to 'main' is only allowed from 'staging'. Current source: '${headRef}'.`, - ); + errors.push(`Merge to 'main' is only allowed from 'staging'. Current source: '${headRef}'.`); errors.push("To fix this, rebase your feature branch onto 'staging'."); } @@ -425,33 +354,23 @@ async function runChecks(options: PrCheckOptions): Promise { return errors; } -async function shouldAllowAdminBypass( - options: PrCheckOptions, -): Promise { +async function shouldAllowAdminBypass(options: PrCheckOptions): Promise { if (!options.allowAdminBypass) { return false; } - return isAdminActor( - process.env.GITHUB_ACTOR ?? "", - process.env.GITHUB_REPOSITORY ?? "", - ); + return isAdminActor(process.env.GITHUB_ACTOR ?? "", process.env.GITHUB_REPOSITORY ?? ""); } const prChecksCommand = defineCommand({ description: "Run pull request checks", options: defineOptions( z.object({ - baseRef: z - .string() - .default("") - .describe("Pull request base branch ref (for example: staging)"), + baseRef: z.string().default("").describe("Pull request base branch ref (for example: staging)"), headRef: z .string() .default("") - .describe( - "Pull request head branch ref (for example: feature/my-feature)", - ), + .describe("Pull request head branch ref (for example: feature/my-feature)"), allowAdminBypass: z .boolean() .default(false) @@ -476,9 +395,7 @@ const prChecksCommand = defineCommand({ logError(error); } if (allowAdminBypass) { - logWarn( - "Admin bypass is allowed for this pull request actor. Treating as warning only.", - ); + logWarn("Admin bypass is allowed for this pull request actor. Treating as warning only."); } else { fail("Pull request checks failed."); } diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json index eebf68d..887874a 100644 --- a/scripts/tsconfig.json +++ b/scripts/tsconfig.json @@ -1,3 +1,3 @@ { "extends": "../tsconfig.scripts.json" -} \ No newline at end of file +} diff --git a/src/action.ts b/src/action.ts index 13636a0..7e49856 100644 --- a/src/action.ts +++ b/src/action.ts @@ -1,8 +1,4 @@ -import { - createPackageManager, - type CommandRunner, - type PackageName, -} from "ts-apt"; +import { createPackageManager, type CommandRunner, type PackageName } from "ts-apt"; import { Cache, CacheKey } from "./cache.js"; import { Manifest, ManifestEntry } from "./manifest.js"; import { ActionPackageNames, buildFileList } from "./packages.js"; @@ -34,11 +30,7 @@ export class ActionRunner { private readonly commandRunner: CommandRunner; private readonly logger: winston.Logger; - constructor( - cache: Cache, - commandRunner: CommandRunner, - logger: winston.Logger, - ) { + constructor(cache: Cache, commandRunner: CommandRunner, logger: winston.Logger) { this.cache = cache; this.commandRunner = commandRunner; this.logger = logger; @@ -56,17 +48,11 @@ export class ActionRunner { * @param nativeArch Runner architecture as reported by dpkg (e.g. amd64). * @returns Comma-delimited name[:arch]=version list. */ - private toCsv( - packageNames: readonly PackageName[], - nativeArch: string, - ): string { + private toCsv(packageNames: readonly PackageName[], nativeArch: string): string { return [...packageNames] .sort((a, b) => a.compareTo(b)) .map((name) => { - const foreignArch = - name.arch !== undefined && name.arch !== nativeArch - ? `:${name.arch}` - : ""; + const foreignArch = name.arch !== undefined && name.arch !== nativeArch ? `:${name.arch}` : ""; const version = name.version === undefined ? "" : `=${name.version}`; return `${name.name}${foreignArch}${version}`; }) @@ -81,37 +67,21 @@ export class ActionRunner { */ async runAction(inputs: ActionInputs): Promise { const packageNames = ActionPackageNames.fromInput(inputs.packages); - const cacheKey = new CacheKey( - inputs.version, - FORCE_UPDATE_INCREMENT, - process.arch, - packageNames, - ); + const cacheKey = new CacheKey(inputs.version, FORCE_UPDATE_INCREMENT, process.arch, packageNames); - let manifest = await this.cache.loadAndRestore( - cacheKey, - inputs.executeInstallScripts, - ); + let manifest = await this.cache.loadAndRestore(cacheKey, inputs.executeInstallScripts); const cacheHit = manifest !== undefined; // Needed on a cache hit too, to resolve the native arch for outputs. // apt-fast only speeds up installs, so skip probing for it on a hit. - const installManager = await createPackageManager( - !cacheHit, - this.logger, - this.logger, - ); + const installManager = await createPackageManager(!cacheHit, this.logger, this.logger); if (!cacheHit) { const packageInfos = await installManager.install(packageNames.toArray()); // Collect each package's installed files so the archives have content. const entries = await Promise.all( packageInfos.map( - async (info) => - new ManifestEntry( - info.name, - await buildFileList(info.name, installManager), - ), + async (info) => new ManifestEntry(info.name, await buildFileList(info.name, installManager)), ), ); manifest = new Manifest(new Date(), entries, cacheKey); diff --git a/src/cache.ts b/src/cache.ts index 91859c4..0210958 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -17,9 +17,7 @@ export const CACHE_KEY_FILENAME = "cache_key.md5"; * @param cacheDir Directory name under the user's home directory. * @returns Absolute path to the cache directory. */ -export function resolveCacheDir( - cacheDir: string = CACHE_DEFAULT_DIRNAME, -): string { +export function resolveCacheDir(cacheDir: string = CACHE_DEFAULT_DIRNAME): string { return path.join(os.homedir(), cacheDir); } @@ -44,10 +42,7 @@ const EXTRACT_ARCHIVES_SCRIPT = type InstallScriptType = "preinst" | "postinst"; /** Order dpkg runs maintainer scripts in during install. */ -const INSTALL_SCRIPT_ORDER: readonly InstallScriptType[] = [ - "preinst", - "postinst", -]; +const INSTALL_SCRIPT_ORDER: readonly InstallScriptType[] = ["preinst", "postinst"]; /** Action argument dpkg passes to each maintainer script on a fresh install. */ const INSTALL_SCRIPT_ACTIONS: Readonly> = { @@ -73,20 +68,12 @@ export class CacheKey { /** MD5 hash of the serialized cache key. Not considered sensitive in the GitHub Cache action. */ readonly hash: string; - constructor( - version: string, - forceUpdateIncrement: string, - arch: string, - packageNames: ActionPackageNames, - ) { + constructor(version: string, forceUpdateIncrement: string, arch: string, packageNames: ActionPackageNames) { this.version = version; this.forceUpdateIncrement = forceUpdateIncrement; this.arch = arch; this.packageNames = packageNames; - this.hash = crypto - .createHash("md5") - .update(this.toJSON(false)) - .digest("hex"); + this.hash = crypto.createHash("md5").update(this.toJSON(false)).digest("hex"); } /** @@ -171,38 +158,26 @@ export class Cache { * @param packageName Package whose script to run. * @param scriptType Maintainer script to run. */ - private async runInstallScript( - packageName: PackageName, - scriptType: InstallScriptType, - ): Promise { + private async runInstallScript(packageName: PackageName, scriptType: InstallScriptType): Promise { const scriptPath = await findInstallScript(packageName, scriptType, "/"); if (!scriptPath) { - this.logger.info( - `No ${scriptType} script found for package ${packageName.serialize()}, skipping`, - ); + this.logger.info(`No ${scriptType} script found for package ${packageName.serialize()}, skipping`); return; } this.logger.info( `Running ${scriptType} script '${path.basename(scriptPath)}' for package ${packageName.serialize()}...`, ); - await this.commandRunner.run( - "sudo", - ["sh", "-x", scriptPath, INSTALL_SCRIPT_ACTIONS[scriptType]], - { timeoutMs: NO_TIMEOUT_MS }, - ); + await this.commandRunner.run("sudo", ["sh", "-x", scriptPath, INSTALL_SCRIPT_ACTIONS[scriptType]], { + timeoutMs: NO_TIMEOUT_MS, + }); this.logger.info(`Ran ${scriptType} script for ${packageName.name}`); } - async loadAndRestore( - key: CacheKey, - executeInstallScripts: boolean, - ): Promise { + async loadAndRestore(key: CacheKey, executeInstallScripts: boolean): Promise { const cacheHit = await ghcache.restoreCache([this.path], key.hash); if (!cacheHit) { - this.logger.info( - `Cache miss for key ${key.hash}, skipping cache restore.`, - ); + this.logger.info(`Cache miss for key ${key.hash}, skipping cache restore.`); return undefined; } @@ -210,9 +185,7 @@ export class Cache { const contents = await fs .readdir(this.path) .then((entries) => entries.join("\n")) - .catch( - (reason: any) => `Unable to read cache directory contents: ${reason}`, - ); + .catch((reason: any) => `Unable to read cache directory contents: ${reason}`); this.logger.error( `${prefixMessage}, skipping cache restore.\n` + `This may indicate a cache corruption or an unexpected cache hit for a different package set.\n` + @@ -232,13 +205,9 @@ export class Cache { return undefined; } const manifestAllPath = path.join(this.path, MANIFEST_ALL_FILENAME); - const manifestAll = await Manifest.readFromFile(manifestAllPath).catch( - () => undefined, - ); + const manifestAll = await Manifest.readFromFile(manifestAllPath).catch(() => undefined); if (!manifestAll || manifestAll.entries.length === 0) { - await logFileError( - `Manifest of all packages not found or empty at ${manifestAllPath}`, - ); + await logFileError(`Manifest of all packages not found or empty at ${manifestAllPath}`); return undefined; } @@ -250,10 +219,7 @@ export class Cache { .filter((entry) => entry.filepaths.length > 0) .map((entry) => ({ packageName: entry.packageName, - archivePath: path.join( - this.path, - entry.packageName.serialize() + ".tar", - ), + archivePath: path.join(this.path, entry.packageName.serialize() + ".tar"), })); for (const { archivePath } of archivePaths) { if ( @@ -291,24 +257,17 @@ export class Cache { * * @param archivePaths Absolute paths to the package archives. */ - private async extractArchives( - archivePaths: readonly string[], - ): Promise { + private async extractArchives(archivePaths: readonly string[]): Promise { if (archivePaths.length === 0) { return; } - await this.commandRunner.run( - "sudo", - ["sh", "-c", EXTRACT_ARCHIVES_SCRIPT, "sh", ...archivePaths], - { timeoutMs: NO_TIMEOUT_MS }, - ); + await this.commandRunner.run("sudo", ["sh", "-c", EXTRACT_ARCHIVES_SCRIPT, "sh", ...archivePaths], { + timeoutMs: NO_TIMEOUT_MS, + }); } private async createArchive(manifestEntry: ManifestEntry): Promise { - const archivePath = path.join( - this.path, - manifestEntry.packageName.serialize() + ".tar", - ); + const archivePath = path.join(this.path, manifestEntry.packageName.serialize() + ".tar"); if ( await fs .access(archivePath) @@ -348,11 +307,7 @@ export class Cache { { input: manifestEntry.filepaths.join("\0"), timeoutMs: NO_TIMEOUT_MS }, ); // Hand the archive back to the runner user so the cache save can manage it. - await this.commandRunner.run("sudo", [ - "chown", - `${os.userInfo().uid}:${os.userInfo().gid}`, - archivePath, - ]); + await this.commandRunner.run("sudo", ["chown", `${os.userInfo().uid}:${os.userInfo().gid}`, archivePath]); this.logger.info( `Archive created for package ${manifestEntry.packageName.serialize()} at ${archivePath}`, ); @@ -362,9 +317,7 @@ export class Cache { const allEntries = manifest.entries; const packages = manifest.cacheKey.packageNames; // Keep the installed name, which carries the resolved version and arch. - const mainEntries = allEntries.filter((entry) => - packages.includes(entry.packageName), - ); + const mainEntries = allEntries.filter((entry) => packages.includes(entry.packageName)); const write = async (manifest: Manifest, filename: string) => { this.logger.info( @@ -378,14 +331,8 @@ export class Cache { const now = new Date(); - await write( - new Manifest(now, mainEntries, manifest.cacheKey), - MANIFEST_MAIN_FILENAME, - ); - await write( - new Manifest(now, allEntries, manifest.cacheKey), - MANIFEST_ALL_FILENAME, - ); + await write(new Manifest(now, mainEntries, manifest.cacheKey), MANIFEST_MAIN_FILENAME); + await write(new Manifest(now, allEntries, manifest.cacheKey), MANIFEST_ALL_FILENAME); } /** @@ -397,20 +344,12 @@ export class Cache { async archiveAndSave(manifest: Manifest): Promise { const key = manifest.cacheKey; if (!ghcache.isFeatureAvailable()) { - throw new Error( - "GitHub Actions cache service is not available in this environment", - ); + throw new Error("GitHub Actions cache service is not available in this environment"); } await fs.mkdir(this.path, { recursive: true }); - this.logger.info( - `Writing cache key to ${path.join(this.path, CACHE_KEY_FILENAME)} with ${key}...`, - ); - await fs.writeFile( - path.join(this.path, CACHE_KEY_FILENAME), - key.hash, - "utf8", - ); + this.logger.info(`Writing cache key to ${path.join(this.path, CACHE_KEY_FILENAME)} with ${key}...`); + await fs.writeFile(path.join(this.path, CACHE_KEY_FILENAME), key.hash, "utf8"); this.logger.info(`Wrote cache key`); await this.writeManifests(manifest); @@ -418,15 +357,10 @@ export class Cache { await this.createArchive(entry); } - this.logger.info( - `Saving cache with key ${key} for ${manifest.entries.length} entries...`, - ); + this.logger.info(`Saving cache with key ${key} for ${manifest.entries.length} entries...`); try { - const cacheId = await ghcache.saveCache( - [this.path], - manifest.cacheKey.hash, - ); + const cacheId = await ghcache.saveCache([this.path], manifest.cacheKey.hash); if (cacheId === undefined) { throw new Error("Cache save failed: no cache action ID returned."); } diff --git a/src/index.ts b/src/index.ts index fd4f6fe..8054b36 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,13 +18,7 @@ import { Instruments } from "./instrumentation.js"; function ensureBundledScriptsExecutable( bundleDir: string = path.dirname(fileURLToPath(import.meta.url)), ): void { - const aptFastPath = path.join( - bundleDir, - "..", - "scripts", - "dist", - "apt-fast.sh", - ); + const aptFastPath = path.join(bundleDir, "..", "scripts", "dist", "apt-fast.sh"); if (fs.existsSync(aptFastPath)) { fs.chmodSync(aptFastPath, 0o755); } @@ -37,16 +31,12 @@ function ensureBundledScriptsExecutable( * @returns Parsed behavior enum value. * @throws Error when value is outside the supported set. */ -function parseEmptyPackagesBehavior( - value: string, -): "error" | "warn" | "ignore" { +function parseEmptyPackagesBehavior(value: string): "error" | "warn" | "ignore" { if (value === "error" || value === "warn" || value === "ignore") { return value; } - throw new Error( - `empty_packages_behavior value '${value}' must be one of: error, warn, ignore.`, - ); + throw new Error(`empty_packages_behavior value '${value}' must be one of: error, warn, ignore.`); } /** @@ -55,8 +45,7 @@ function parseEmptyPackagesBehavior( * @returns Parsed and validated action inputs. */ function getInputs(): ActionInputs { - const emptyPackagesBehaviorRaw = - core.getInput("empty_packages_behavior") || "error"; + const emptyPackagesBehaviorRaw = core.getInput("empty_packages_behavior") || "error"; return { packages: core.getInput("packages", { required: true }), @@ -80,10 +69,7 @@ async function main(): Promise { let failed = false; try { ensureBundledScriptsExecutable(); - const commandRunner = new DefaultCommandRunner( - instruments.execLogger, - instruments.execLogger, - ); + const commandRunner = new DefaultCommandRunner(instruments.execLogger, instruments.execLogger); const outputs = await runAction( inputs, diff --git a/src/instrumentation.ts b/src/instrumentation.ts index 5e64434..aff60de 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -14,9 +14,7 @@ import { const APP_LOG_FILENAME = "capa_app.log"; const EXEC_LOG_FILENAME = "capa_exec.log"; -function createStream( - filepath: string, -): winston.transports.StreamTransportInstance { +function createStream(filepath: string): winston.transports.StreamTransportInstance { return new winston.transports.Stream({ stream: fs.createWriteStream(filepath, { flags: "a", @@ -36,8 +34,7 @@ export class Artifacts { /** Whether debug is enabled which affects artifacts otherwise not. */ readonly debug: boolean = false, /** Unique runner ID of the GitHub Action workflow run. */ - readonly runId: string = process.env.GITHUB_RUN_ID ?? - `ghrunid-notfound-${crypto.randomUUID()}`, + readonly runId: string = process.env.GITHUB_RUN_ID ?? `ghrunid-notfound-${crypto.randomUUID()}`, ) {} /** Upload artifacts to GitHub Actions via [@actions/artifact]. */ @@ -145,13 +142,7 @@ export class Instruments { MANIFEST_MAIN_FILENAME, MANIFEST_ALL_FILENAME, ]); - this.appLogger = createGitHubLogger( - debug, - path.join(this.cacheDirPath, APP_LOG_FILENAME), - ); - this.execLogger = createExecLogger( - debug, - path.join(this.cacheDirPath, EXEC_LOG_FILENAME), - ); + this.appLogger = createGitHubLogger(debug, path.join(this.cacheDirPath, APP_LOG_FILENAME)); + this.execLogger = createExecLogger(debug, path.join(this.cacheDirPath, EXEC_LOG_FILENAME)); } } diff --git a/src/manifest.ts b/src/manifest.ts index fe4a509..5fc1599 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -29,10 +29,7 @@ export class ManifestEntry { * @returns The deserialized entry. */ static fromJSON(json: any): ManifestEntry { - return new ManifestEntry( - packageNameFromJSON(json.packageName), - json.filepaths as string[], - ); + return new ManifestEntry(packageNameFromJSON(json.packageName), json.filepaths as string[]); } } @@ -69,11 +66,7 @@ export class Manifest { return new Manifest( new Date(json.created), json.entries.map( - (e: any) => - new ManifestEntry( - packageNameFromJSON(e.packageName), - e.filepaths as string[], - ), + (e: any) => new ManifestEntry(packageNameFromJSON(e.packageName), e.filepaths as string[]), ), CacheKey.fromJSON(json.cacheKey), ); @@ -97,10 +90,6 @@ export class Manifest { * @param filePath Destination path. */ async writeToFile(filePath: string): Promise { - await fs.promises.writeFile( - filePath, - JSON.stringify(this, null, 2), - "utf8", - ); + await fs.promises.writeFile(filePath, JSON.stringify(this, null, 2), "utf8"); } } diff --git a/src/packages.ts b/src/packages.ts index ff52e5e..d39bc7b 100644 --- a/src/packages.ts +++ b/src/packages.ts @@ -1,11 +1,6 @@ import * as fs from "fs"; import path from "path"; -import { - createPackageName, - deserializePackageName, - type PackageName, - type PackageManager, -} from "ts-apt"; +import { createPackageName, deserializePackageName, type PackageName, type PackageManager } from "ts-apt"; import winston from "winston"; /** @@ -21,8 +16,7 @@ export async function buildFileList( root: string = "/", ): Promise { // Converts absolute paths to tar-relative paths. - const tarRelativePath = (filePath: string) => - filePath.startsWith("/") ? filePath.slice(1) : filePath; + const tarRelativePath = (filePath: string) => (filePath.startsWith("/") ? filePath.slice(1) : filePath); const files = (await packageManager.listInstalledFiles(packageName)) .filter((filePath) => { @@ -102,7 +96,7 @@ export class ActionPackageNames { const names: PackageName[] = []; - for (let index = 0; index < tokens.length; ) { + for (let index = 0; index < tokens.length;) { const current = tokens[index]!; const next = tokens[index + 1]; @@ -123,23 +117,12 @@ export class ActionPackageNames { const split = splitNameArch(namePart.trim()); let arch = split.arch; - if ( - arch === undefined && - next !== undefined && - !next.includes("=") && - next.trim() !== "" - ) { + if (arch === undefined && next !== undefined && !next.includes("=") && next.trim() !== "") { arch = next.trim(); index += 1; } - names.push( - createPackageName( - split.name, - version === "" ? undefined : version, - arch, - ), - ); + names.push(createPackageName(split.name, version === "" ? undefined : version, arch)); index += 1; continue; } @@ -157,15 +140,9 @@ export class ActionPackageNames { } static fromJSON(json: any): ActionPackageNames { - const sourceItems = Array.isArray(json) - ? json - : Array.isArray(json?.items) - ? json.items - : []; + const sourceItems = Array.isArray(json) ? json : Array.isArray(json?.items) ? json.items : []; - const items = sourceItems.map((item: any) => - createPackageName(item.name, item.version, item.arch), - ); + const items = sourceItems.map((item: any) => createPackageName(item.name, item.version, item.arch)); return new ActionPackageNames(items); } @@ -190,9 +167,7 @@ export class ActionPackageNames { return this.items.some( (requested) => requested.name === installed.name && - (requested.arch === undefined || - installed.arch === undefined || - requested.arch === installed.arch), + (requested.arch === undefined || installed.arch === undefined || requested.arch === installed.arch), ); } } @@ -210,10 +185,7 @@ export async function updateAptLists( ): Promise { const maxDepth = 5; - const search = async ( - currentPath: string, - currentDepth: number, - ): Promise => { + const search = async (currentPath: string, currentDepth: number): Promise => { if (currentDepth > maxDepth) { return false; } @@ -239,14 +211,10 @@ export async function updateAptLists( }; if (await search(aptListsPath, 0)) { - logger.info( - `Apt lists directory '${aptListsPath}' appears to contain files, skipping 'apt update'`, - ); + logger.info(`Apt lists directory '${aptListsPath}' appears to contain files, skipping 'apt update'`); return; } - logger.info( - `Apt lists directory '${aptListsPath}' appears stale or empty, running 'apt update'...`, - ); + logger.info(`Apt lists directory '${aptListsPath}' appears stale or empty, running 'apt update'...`); await packageManager.update(); } diff --git a/test/action-runner.test.ts b/test/action-runner.test.ts index 9df6a17..a1e7bff 100644 --- a/test/action-runner.test.ts +++ b/test/action-runner.test.ts @@ -67,9 +67,7 @@ describe("ActionRunner", () => { const { runner, cache } = createRunner(); const packageNames = ActionPackageNames.fromInput("curl"); const cacheKey = new CacheKey("v1", "0", process.arch, packageNames); - cache.loadAndRestore.mockResolvedValue( - new Manifest(new Date(), [], cacheKey), - ); + cache.loadAndRestore.mockResolvedValue(new Manifest(new Date(), [], cacheKey)); vi.mocked(createPackageManager).mockResolvedValue({ getNativeArchitecture: vi.fn().mockResolvedValue(NATIVE_ARCH), } as never); @@ -108,8 +106,6 @@ describe("ActionRunner", () => { expect(cache.archiveAndSave).toHaveBeenCalledTimes(1); const [manifest] = cache.archiveAndSave.mock.calls[0]!; - expect(manifest.entries).toEqual([ - new ManifestEntry(installedPackage, [installedFile.slice(1)]), - ]); + expect(manifest.entries).toEqual([new ManifestEntry(installedPackage, [installedFile.slice(1)])]); }); }); diff --git a/test/action.test.ts b/test/action.test.ts index 0f55ed5..a18a0ad 100644 --- a/test/action.test.ts +++ b/test/action.test.ts @@ -1,9 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { - runAction, - type ActionInputs, - type ActionOutputs, -} from "../src/action.js"; +import { runAction, type ActionInputs, type ActionOutputs } from "../src/action.js"; import { Manifest, ManifestEntry } from "../src/manifest.js"; import { CacheKey } from "../src/cache.js"; import { createPackageName } from "ts-apt"; @@ -38,12 +34,7 @@ describe("runAction", () => { }; const packageNames = ActionPackageNames.fromInput(inputs.packages); - const cacheKey = new CacheKey( - inputs.version, - "0", - process.arch, - packageNames, - ); + const cacheKey = new CacheKey(inputs.version, "0", process.arch, packageNames); const manifest = new Manifest( new Date(), [ @@ -111,16 +102,9 @@ describe("runAction", () => { ); const mockCache = { loadAndRestore: async () => manifest }; - const outputs = await runAction( - inputs, - {} as never, - mockCache as never, - {} as never, - ); + const outputs = await runAction(inputs, {} as never, mockCache as never, {} as never); - expect(outputs.packageVersionList).toBe( - `${PKG_NAME}=${PKG_VER},${PKG2_NAME}:${FOREIGN_ARCH}=${PKG_VER}`, - ); + expect(outputs.packageVersionList).toBe(`${PKG_NAME}=${PKG_VER},${PKG2_NAME}:${FOREIGN_ARCH}=${PKG_VER}`); expect(outputs.allPackageVersionList).toBe( `${PKG_NAME}=${PKG_VER},${PKG2_NAME}:${FOREIGN_ARCH}=${PKG_VER},${PKG3_NAME}=${PKG_VER}`, ); diff --git a/test/cache-restore.test.ts b/test/cache-restore.test.ts index 1d988ef..35c3aa2 100644 --- a/test/cache-restore.test.ts +++ b/test/cache-restore.test.ts @@ -1,11 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import * as ghcache from "@actions/cache"; -import { - createPackageName, - type CommandOptions, - type PackageName, -} from "ts-apt"; +import { createPackageName, type CommandOptions, type PackageName } from "ts-apt"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import winston from "winston"; @@ -25,9 +21,7 @@ const { HOME_DIR } = await vi.hoisted(async () => { const nodeOs = await import("node:os"); const nodePath = await import("node:path"); return { - HOME_DIR: nodeFs.mkdtempSync( - nodePath.join(nodeOs.tmpdir(), "cache-restore-test-"), - ), + HOME_DIR: nodeFs.mkdtempSync(nodePath.join(nodeOs.tmpdir(), "cache-restore-test-")), }; }); @@ -52,20 +46,11 @@ const INSTALL_ORDER = [ ]; // Packages without files (e.g. metapackages) get no archive. const METAPACKAGE = createPackageName("meta", "1.0"); -const KEY = new CacheKey( - "v1", - "1", - "x86_64", - ActionPackageNames.fromInput(REQUESTED_PACKAGE.serialize()), -); +const KEY = new CacheKey("v1", "1", "x86_64", ActionPackageNames.fromInput(REQUESTED_PACKAGE.serialize())); -const archivePath = (packageName: PackageName) => - path.join(CACHE_DIR, packageName.serialize() + ".tar"); -const filesOf = (packageName: PackageName) => [ - `usr/lib/${packageName.name}.so`, -]; -const entryOf = (packageName: PackageName) => - new ManifestEntry(packageName, filesOf(packageName)); +const archivePath = (packageName: PackageName) => path.join(CACHE_DIR, packageName.serialize() + ".tar"); +const filesOf = (packageName: PackageName) => [`usr/lib/${packageName.name}.so`]; +const entryOf = (packageName: PackageName) => new ManifestEntry(packageName, filesOf(packageName)); const MANIFEST = new Manifest( new Date(0), [...INSTALL_ORDER.map(entryOf), new ManifestEntry(METAPACKAGE)], @@ -74,11 +59,9 @@ const MANIFEST = new Manifest( async function writeCache(archivedPackages = INSTALL_ORDER): Promise { fs.mkdirSync(CACHE_DIR, { recursive: true }); - await new Manifest( - MANIFEST.created, - [entryOf(REQUESTED_PACKAGE)], - KEY, - ).writeToFile(path.join(CACHE_DIR, MANIFEST_MAIN_FILENAME)); + await new Manifest(MANIFEST.created, [entryOf(REQUESTED_PACKAGE)], KEY).writeToFile( + path.join(CACHE_DIR, MANIFEST_MAIN_FILENAME), + ); await MANIFEST.writeToFile(path.join(CACHE_DIR, MANIFEST_ALL_FILENAME)); archivedPackages.forEach((pkg) => fs.writeFileSync(archivePath(pkg), "")); } @@ -96,20 +79,18 @@ const argValue = (args: string[], flag: string) => /** Records sudo tar calls, including the file list piped to tar create. */ function recordTar(calls: TarCalls) { - return vi.fn( - async (_command: string, args: string[] = [], options: CommandOptions = {}) => { - const archive = argValue(args, "--file") ?? ""; - if (args.includes("--create")) { - calls.created.push([archive, String(options.input ?? "").split("\0")]); - } - // Extraction runs as `sh -c