Skip to content

Commit b09ec83

Browse files
Render hash mismatches as feedback
1 parent 0e85837 commit b09ec83

File tree

2 files changed

+230
-9
lines changed

2 files changed

+230
-9
lines changed

dist/index.js

Lines changed: 93 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.ts

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,13 @@ class NixInstallerAction extends DetSysAction {
126126
await this.scienceDebugFly();
127127
await this.detectAndForceDockerShim();
128128
await this.install();
129-
await this.slurpEventLog();
129+
await this.spewEventLog();
130130
}
131131

132132
async post(): Promise<void> {
133133
await this.cleanupDockerShim();
134134
await this.reportOverall();
135+
await this.slurpEventLog();
135136
}
136137

137138
private get isMacOS(): boolean {
@@ -1111,7 +1112,7 @@ class NixInstallerAction extends DetSysAction {
11111112
}
11121113
}
11131114

1114-
private async slurpEventLog(): Promise<void> {
1115+
private async spewEventLog(): Promise<void> {
11151116
if (!this.determinate) {
11161117
return;
11171118
}
@@ -1144,6 +1145,140 @@ class NixInstallerAction extends DetSysAction {
11441145

11451146
daemon.unref();
11461147
}
1148+
1149+
private async slurpEventLog(): Promise<void> {
1150+
if (!this.determinate) {
1151+
return;
1152+
}
1153+
1154+
try {
1155+
const logPath = actionsCore.getState(STATE_EVENT_LOG);
1156+
const events = await readMismatchEvents(logPath);
1157+
1158+
// No point doing any more work if there are no mismatch events
1159+
if (events.length === 0) {
1160+
actionsCore.debug("No hash mismatches found.");
1161+
return;
1162+
}
1163+
1164+
const listing = await getFileListing();
1165+
1166+
// For each file, search for potentially bad hashes
1167+
for (const file of listing) {
1168+
const text = await readFile(file, "utf-8");
1169+
const lines = text.split("\n");
1170+
1171+
for (const [index, line] of lines.entries()) {
1172+
const lineNumber = index + 1;
1173+
1174+
for (const event of events) {
1175+
const match = line.match(event.search);
1176+
if (!match) {
1177+
continue;
1178+
}
1179+
1180+
// Allegedly, match.index is optional, so default to 0
1181+
const column = (match.index ?? 0) + 1;
1182+
1183+
actionsCore.error(`This derivation's hash is ${event.good}`, {
1184+
title: "Outdated hash",
1185+
file,
1186+
startLine: lineNumber,
1187+
startColumn: column,
1188+
});
1189+
}
1190+
}
1191+
}
1192+
} catch (error) {
1193+
// Don't hard fail the action if something exploded; this feature is only a nice-to-have
1194+
actionsCore.warning(`Could not consume hash mismatch logs: ${error}`);
1195+
}
1196+
}
1197+
}
1198+
1199+
// Fields we're interested in from the source event
1200+
interface MismatchSourceEvent {
1201+
readonly drv: string;
1202+
readonly good: string;
1203+
readonly bad: readonly string[];
1204+
}
1205+
1206+
// Our augmented event with the RegExp to match against the bad hashes
1207+
interface MismatchEvent extends MismatchSourceEvent {
1208+
readonly search: RegExp;
1209+
}
1210+
1211+
async function readMismatchEvents(logPath: string): Promise<MismatchEvent[]> {
1212+
const prefix = "data: ";
1213+
1214+
// Used to deduplicate events (see below)
1215+
const memo = new Set<string>();
1216+
1217+
const events = (await readFile(logPath, "utf-8"))
1218+
.split(/\n/)
1219+
.filter((line) => line.startsWith(prefix))
1220+
.map((line) => {
1221+
// Note: this currently assumes that all events being ingested are mismatches
1222+
const json = line.slice(prefix.length);
1223+
const source = JSON.parse(json) as MismatchSourceEvent;
1224+
1225+
// Construct a regular expression to search for any of the hash patterns
1226+
// (do it here to avoid creating RegExp objects in a loop below)
1227+
const search = new RegExp(
1228+
source.bad.map((s) => s.replace(/[+]/, (ch) => `\\${ch}`)).join("|"),
1229+
);
1230+
1231+
return {
1232+
...source,
1233+
search,
1234+
} satisfies MismatchEvent;
1235+
})
1236+
.filter((event) => {
1237+
// Deduplicate based on the derivation's store path and list of bad hashes.
1238+
const key = [event.drv, ...event.bad].join("\0");
1239+
if (memo.has(key)) {
1240+
false;
1241+
}
1242+
1243+
memo.add(key);
1244+
return true;
1245+
});
1246+
1247+
return events;
1248+
}
1249+
1250+
// Get the list of files with potential hash mismatches (limited currently to *.{nix,json,toml})
1251+
async function getFileListing(): Promise<readonly string[]> {
1252+
return new Promise((resolve, reject) => {
1253+
const chunks: Buffer[] = [];
1254+
let length = 0;
1255+
1256+
const child = spawn("git", ["ls-files", "*.nix", "*.json", "*.toml"], {
1257+
stdio: ["ignore", "pipe", "inherit"],
1258+
});
1259+
1260+
child.stdout.on("data", (chunk: Buffer) => {
1261+
chunks.push(chunk);
1262+
length += chunk.length;
1263+
});
1264+
1265+
child.stdout.on("end", () => {
1266+
const lines = Buffer.concat(chunks, length).toString("utf-8").split(/\n/);
1267+
resolve(lines);
1268+
});
1269+
1270+
child.stdout.on("error", reject);
1271+
1272+
child.on("error", reject);
1273+
child.on("exit", (code, signal) => {
1274+
// We should consider rejecting the promise here
1275+
if (code !== 0) {
1276+
actionsCore.warning(
1277+
`git ls-files exited suspiciously code=${code}; signal=${signal}`,
1278+
);
1279+
}
1280+
});
1281+
});
11471282
}
11481283

11491284
type ExecuteEnvironment = {

0 commit comments

Comments
 (0)