cache-apt-pkgs-action/scripts/dev/hotfix_pr.mts
Andrew Walsh 9792dbe32f
style: set Prettier print width to 110 and reformat (#246)
* 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 <noreply@anthropic.com>

* test: cover cache restore and save failure paths

Codecov flagged lines in #246 as uncovered patch lines; they were
untested before the reformat touched them. Add behavior tests for:

- Cache: cache miss, missing or empty manifests, unreadable cache dir,
  metapackage-only restore, install scripts running after their archive
  with dpkg actions, existing archives, unavailable cache service, and
  save failures.
- ManifestEntry.fromJSON round trip.
- buildFileList dropping missing files, updateAptLists skipping update
  for populated lists and searching only to max depth, and fromInput
  ignoring a version without a package name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 11:45:29 -08:00

134 lines
3.7 KiB
TypeScript
Executable file

#!/usr/bin/env -S node --experimental-strip-types
import process from "node:process";
import { defineCommand } from "@robingenz/zli";
import { z } from "zod";
import {
createCliConfig,
ROOT_DIR,
confirmPrompt,
fail,
logInfo,
logSuccess,
runCli,
runMain,
run,
runCaptureOutput,
tryRun,
} from "../devopslib.mts";
type BranchName = string;
function branchExistsOnOrigin(branch: BranchName): boolean {
return tryRun("git", ["ls-remote", "--exit-code", "--heads", "origin", branch], { cwd: ROOT_DIR }).ok;
}
function createOrCheckoutBranch(issueId: string, checkoutBranch: BranchName, baseBranch: BranchName): void {
if (branchExistsOnOrigin(checkoutBranch)) {
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 });
} else {
logInfo(`Creating hotfix branch for issue ${issueId}...`);
run("git", ["checkout", baseBranch], { cwd: ROOT_DIR });
run("git", ["checkout", "-b", checkoutBranch], { cwd: ROOT_DIR });
run("git", ["merge", baseBranch], { cwd: ROOT_DIR });
}
}
function commitIfNeeded(message: string): void {
run("git", ["add", "."], { cwd: ROOT_DIR });
const commit = tryRun("git", ["commit", "-m", message], { cwd: ROOT_DIR });
if (commit.ok) {
return;
}
const combined = `${commit.stdout}\n${commit.stderr}`;
if (/nothing to commit|no changes added to commit/i.test(combined)) {
logSuccess("No local changes to commit.");
return;
}
process.stderr.write(commit.stderr);
process.exit(commit.status ?? 1);
}
function pushChanges(issueId: string, fixType: string, syncBranch: BranchName, syncBase: BranchName): void {
const message = `${fixType}: resolve critical production issue in #${issueId}`;
commitIfNeeded(message);
const prUrl = runCaptureOutput(
"gh",
[
"pr",
"list",
"--head",
syncBranch,
"--base",
syncBase,
"--state",
"open",
"--json",
"url",
"--jq",
".[0].url",
],
{ cwd: ROOT_DIR },
);
if (prUrl) {
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,
});
}
logInfo(`Pushing changes from ${syncBase} to ${syncBranch}...`);
run("git", ["push", "origin", syncBranch], { cwd: ROOT_DIR });
}
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")]),
action: async (_options, args) => {
const [issueId, base] = args;
if (!/^\d+$/.test(issueId)) {
fail("Issue ID must be an integer.");
}
const branchSuffix = `issue-${issueId}`;
const hotfixBranch = `hotfix/${branchSuffix}`;
createOrCheckoutBranch(issueId, hotfixBranch, base);
await confirmPrompt(
"Edit files and confirm to continue. This can always be rerun to pickup where you left off. Continue?",
);
pushChanges(issueId, "fix", hotfixBranch, base);
const syncBranch = `sync/staging-${branchSuffix}`;
createOrCheckoutBranch(issueId, syncBranch, hotfixBranch);
pushChanges(issueId, "sync", syncBranch, "staging");
},
});
const cliConfig = createCliConfig({
importMetaUrl: import.meta.url,
description: "Create or update hotfix and sync branches for an issue",
commands: {
run: hotfixCommand,
},
defaultCommand: hotfixCommand,
});
async function main(): Promise<void> {
await runCli(cliConfig);
}
await runMain(main);