Setting the file. One moment.
Execution Log · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page
Size
6 KB lib/ execution-log.js
JavaScript · 170 lines · 6 KB
14 //
15 // This is NOT a heartbeat. `lib/progress-log.js` writes liveness to a caller-supplied path, which
16 // in practice may be a temp file outside the project; that contract is unchanged.
17 // `appendExecutionLog` always writes inside the project directory.
18
19 const fs = require ( 'node:fs/promises' );
20 const path = require ( 'node:path' );
21
22 const EXECUTION_LOG_FILE = 'execution-log.md' ;
23
24 const HEADER = [
25 '# Execution Log' ,
26 '' ,
27 'Chronology of this migration run: what ran, in what order, and where the run stopped or' ,
28 'resumed. Append-only -- later passes add lines, never rewrite them.' ,
29 '' ,
30 '**Not authoritative for counts.** The final outcome is' ,
31 '`execution/completion-report.json`; the human-readable account is' ,
32 '`execution/review/completion-summary.md`, rendered from that report. Never read a final' ,
33 'number out of this file.' ,
34 '' ,
35 ]. join ( ' \n ' );
36
37 const ENTRY_FIELDS = new Set ([ 'at' , 'runId' , 'phase' , 'event' , 'detail' , 'artifacts' ]);
38
39 // A handoff, a summary and this log all get attached to pull requests and pasted into chat. The
40 // same confidentiality rule applies to all three: counts, ids, reason codes and artifact paths --
41 // never record values. An email address is the one record value that reliably survives a careless
42 // template (`imported customer ${record.email}`), so it is rejected outright rather than trusted
43 // to reviewer discipline.
44 const EMAIL_SHAPED = / [\w.+-] + @ [\w-] + \. [\w.-] + / ;
45
46 function requireString ( value , field ) {
47 if ( ! value || typeof value !== 'string' ) {
48 throw new Error ( `execution log entry ${ field } must be a non-empty string` );
49 }
50 return value;
51 }
52
53 function assertNoRecordValues ( value , field ) {
54 if ( EMAIL_SHAPED . test (value)) {
55 throw new Error (
56 `execution log entry ${ field } looks like it carries a record value (email address); `
57 + 'the execution log records counts, ids, reason codes and artifact paths only' ,
58 );
59 }
60 return value;
61 }
62
63 function executionLogPath ( projectDir ) {
64 return path. join (projectDir, EXECUTION_LOG_FILE );
65 }
66
67 function normalizeExecutionLogEntry ( input = {}) {
68 if ( ! input || typeof input !== 'object' || Array. isArray (input)) {
69 throw new Error ( 'execution log entry must be an object' );
70 }
71 for ( const key of Object. keys (input)) {
72 if ( ! ENTRY_FIELDS . has (key)) {
73 throw new Error ( `unknown execution log entry field: ${ key }` );
74 }
75 }
76 const runId = requireString (input.runId, 'runId' );
77 const phase = requireString (input.phase, 'phase' );
78 const event = requireString (input.event, 'event' );
79 const detail = assertNoRecordValues ( requireString (input.detail, 'detail' ), 'detail' );
80 const at = input.at === undefined || input.at === null ? new Date (). toISOString () : requireString (input.at, 'at' );
81 const artifacts = input.artifacts === undefined || input.artifacts === null ? [] : input.artifacts;
82 if ( ! Array. isArray (artifacts)) {
83 throw new Error ( 'execution log entry artifacts must be an array' );
84 }
85 return {
86 at,
87 runId,
88 phase,
89 event,
90 detail,
91 artifacts: artifacts. map (( artifact , index ) => assertNoRecordValues (
92 requireString (artifact, `artifacts[${ index }]` ),
93 `artifacts[${ index }]` ,
94 )),
95 };
96 }
97
98 function renderExecutionLogEntry ( entry ) {
99 const artifacts = entry.artifacts. length ? ` | artifacts: ${ entry . artifacts . join ( ', ' ) }` : '' ;
100 return `- ${ entry . at } | run ${ entry . runId } | ${ entry . phase } | ${ entry . event } | ${ entry . detail }${ artifacts }` ;
101 }
102
103 async function pathExists ( filePath ) {
104 try {
105 await fs. stat (filePath);
106 return true ;
107 } catch (error) {
108 if (error && error.code === 'ENOENT' ) return false ;
109 throw error;
110 }
111 }
112
113 // `fs.appendFile` rather than read-modify-write: there is no code path here that can truncate the
114 // file, not even a crash between the read and the write.
115 async function appendExecutionLog ( projectDir , entry ) {
116 const normalized = normalizeExecutionLogEntry (entry);
117 const filePath = executionLogPath (projectDir);
118 await fs. mkdir (path. dirname (filePath), { recursive: true });
119 const existed = await pathExists (filePath);
120 const line = renderExecutionLogEntry (normalized);
121 await fs. appendFile (filePath, existed ? `${ line } \n ` : `${ HEADER }${ line } \n ` , 'utf8' );
122 return { path: EXECUTION_LOG_FILE , entry: normalized, line, createdFile: ! existed };
123 }
124
125 async function executionLogExists ( projectDir ) {
126 return pathExists ( executionLogPath (projectDir));
127 }
128
129 // Reads entries back for orchestration and tests -- for "did this event happen, and in what
130 // order", never for a count. Deliberately returns rows, not totals.
131 async function readExecutionLogEntries ( projectDir ) {
132 const filePath = executionLogPath (projectDir);
133 let text;
134 try {
135 text = await fs. readFile (filePath, 'utf8' );
136 } catch (error) {
137 if (error && error.code === 'ENOENT' ) return { exists: false , entries: [] };
138 throw error;
139 }
140 const entries = [];
141 for ( const rawLine of text. split ( ' \n ' )) {
142 if ( ! rawLine. startsWith ( '- ' )) continue ;
143 const parts = rawLine. slice ( 2 ). split ( ' | ' );
144 if (parts. length < 5 ) continue ;
145 const [ at , runField , phase , event , ... rest ] = parts;
146 if ( ! runField. startsWith ( 'run ' )) continue ;
147 const artifactsPart = rest. length > 1 && rest[rest. length - 1 ]. startsWith ( 'artifacts: ' )
148 ? rest. pop (). slice ( 'artifacts: ' . length )
149 : null ;
150 entries. push ({
151 at,
152 runId: runField. slice ( 'run ' . length ),
153 phase,
154 event,
155 detail: rest. join ( ' | ' ),
156 artifacts: artifactsPart ? artifactsPart. split ( ', ' ) : [],
157 });
158 }
159 return { exists: true , entries };
160 }
161
162 module . exports = {
163 EXECUTION_LOG_FILE,
164 executionLogPath,
165 normalizeExecutionLogEntry,
166 renderExecutionLogEntry,
167 appendExecutionLog,
168 executionLogExists,
169 readExecutionLogEntries,
170 };