@@ -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
11491284type ExecuteEnvironment = {
0 commit comments