Skip to content

Commit 902d820

Browse files
Annotate hash mismatches when Determinate features are enabled (#158)
* Render hash mismatches as feedback * Remove superfluous punctuation * Use determinate-nixd's hash fix feature * Feature gate annotations * Add annotation telemetry * Remove .drv when rendering pretty derivation names
1 parent 17a3ce7 commit 902d820

File tree

4 files changed

+241
-5
lines changed

4 files changed

+241
-5
lines changed

dist/index.js

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

src/annotate.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import * as core from "@actions/core";
2+
3+
import type { Fix, FixHashesOutputV1, Mismatch } from "./fixHashes.js";
4+
5+
function prettyDerivation(derivation: string): string {
6+
return derivation.replace(/\/nix\/store\/\w+-/, "").replace(/.drv$/, "");
7+
}
8+
9+
function annotateSingle(
10+
file: string,
11+
line: number,
12+
{ derivation, replacement }: Mismatch,
13+
): void {
14+
const pretty = prettyDerivation(derivation);
15+
core.error(`To correct the hash mismatch for ${pretty}, use ${replacement}`, {
16+
file,
17+
startLine: line,
18+
});
19+
}
20+
21+
function annotateMultiple(
22+
file: string,
23+
{ line, found, mismatches }: Fix,
24+
): void {
25+
const matches = mismatches
26+
.map(({ derivation, replacement }) => {
27+
const pretty = prettyDerivation(derivation);
28+
return `* For the derivation ${pretty}, use ${replacement}`;
29+
})
30+
.join("\n");
31+
32+
core.error(
33+
`There are multiple replacements for the expression ${found}:\n${matches}`,
34+
{
35+
file,
36+
startLine: line,
37+
},
38+
);
39+
}
40+
41+
function annotate(file: string, fix: Fix): void {
42+
if (fix.mismatches.length === 1) {
43+
annotateSingle(file, fix.line, fix.mismatches[0]);
44+
} else {
45+
annotateMultiple(file, fix);
46+
}
47+
}
48+
49+
/**
50+
* Annotates fixed-output derivation hash mismatches using GitHub Actions'
51+
*
52+
* @param output The output of `determinate-nixd fix hashes --json`
53+
* @returns The number of annotations reported to the user
54+
*/
55+
export function annotateMismatches(output: FixHashesOutputV1): number {
56+
let count = 0;
57+
58+
for (const { file, fixes } of output.files) {
59+
for (const fix of fixes) {
60+
annotate(file, fix);
61+
count++;
62+
}
63+
}
64+
65+
return count;
66+
}

src/fixHashes.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { getExecOutput } from "@actions/exec";
2+
3+
export interface Mismatch {
4+
readonly derivation: string;
5+
readonly replacement: string;
6+
}
7+
8+
export interface Fix {
9+
readonly line: number;
10+
readonly found: string;
11+
readonly mismatches: readonly Mismatch[];
12+
}
13+
14+
export interface FileFix {
15+
readonly file: string;
16+
readonly fixes: readonly Fix[];
17+
}
18+
19+
export interface FixHashesOutputV1 {
20+
readonly version: "v1";
21+
readonly files: readonly FileFix[];
22+
}
23+
24+
export async function getFixHashes(): Promise<FixHashesOutputV1> {
25+
const output = await getExecOutput(
26+
"determinate-nixd",
27+
["fix", "hashes", "--json"],
28+
{ silent: true },
29+
);
30+
31+
if (output.exitCode !== 0) {
32+
throw new Error(
33+
`determinate-nixd fix hashes returned non-zero exit code ${output.exitCode} with the following error output:\n${output.stderr}`,
34+
);
35+
}
36+
37+
return JSON.parse(output.stdout);
38+
}

src/index.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { DetSysAction, inputs, platform, stringifyError } from "detsys-ts";
1010
import { randomUUID } from "node:crypto";
1111
import got from "got";
1212
import { setTimeout } from "node:timers/promises";
13+
import { getFixHashes } from "./fixHashes.js";
14+
import { annotateMismatches } from "./annotate.js";
1315

1416
// Nix installation events
1517
const EVENT_INSTALL_NIX_FAILURE = "install_nix_failure";
@@ -27,6 +29,10 @@ const EVENT_LOGIN_TO_FLAKEHUB = "login_to_flakehub";
2729

2830
// Other events
2931
const EVENT_CONCLUDE_JOB = "conclude_job";
32+
const EVENT_FOD_ANNOTATE = "fod_annotate";
33+
34+
// Feature flag names
35+
const FEAT_ANNOTATIONS = "hash-mismatch-annotations";
3036

3137
// Facts
3238
const FACT_DETERMINATE_NIX = "determinate_nix";
@@ -124,6 +130,7 @@ class NixInstallerAction extends DetSysAction {
124130
}
125131

126132
async post(): Promise<void> {
133+
await this.annotateMismatches();
127134
await this.cleanupDockerShim();
128135
await this.reportOverall();
129136
}
@@ -1104,6 +1111,38 @@ class NixInstallerAction extends DetSysAction {
11041111
);
11051112
}
11061113
}
1114+
1115+
private async annotateMismatches(): Promise<void> {
1116+
if (!this.determinate) {
1117+
return;
1118+
}
1119+
1120+
const active = this.getFeature(FEAT_ANNOTATIONS)?.variant;
1121+
if (!active) {
1122+
actionsCore.debug("The annotations feature is disabled for this run");
1123+
return;
1124+
}
1125+
1126+
try {
1127+
actionsCore.debug("Getting hash fixes from determinate-nixd");
1128+
const mismatches = await getFixHashes();
1129+
if (mismatches.version !== "v1") {
1130+
throw new Error(
1131+
`Unsupported \`determinate-nixd fix hashes\` output (got ${mismatches.version}, expected v1)`,
1132+
);
1133+
}
1134+
1135+
actionsCore.debug("Annotating mismatches");
1136+
const count = annotateMismatches(mismatches);
1137+
this.recordEvent(EVENT_FOD_ANNOTATE, { count });
1138+
} catch (error) {
1139+
// Don't hard fail the action if something exploded; this feature is only a nice-to-have
1140+
actionsCore.warning(`Could not consume hash mismatch events: ${error}`);
1141+
this.recordEvent("annotation-mismatch-execution:error", {
1142+
exception: stringifyError(error),
1143+
});
1144+
}
1145+
}
11071146
}
11081147

11091148
type ExecuteEnvironment = {

0 commit comments

Comments
 (0)