-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall-bisector.js
More file actions
356 lines (299 loc) · 11.6 KB
/
install-bisector.js
File metadata and controls
356 lines (299 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
#!/usr/bin/env node
/* eslint-disable no-console */
// A script to bisect TypeScript versions to find when a breaking change occurred
// This script only checks if yarn install is successful in dd repo with the selected TypeScript version
const { execSync } = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');
// Configuration
const TS_REPO_PATH = process.env.TS_REPO_PATH;
const DD_REPO_PATH = process.env.DD_REPO_PATH;
const LOGS_DIR = path.join(DD_REPO_PATH, 'ts-bisector/install-logs');
const LOG_FILE = path.join(LOGS_DIR, 'bisect-results.txt');
const BISECT_REPLAY_PATH = path.join(LOGS_DIR, 'bisect-replay.log');
/**
* Returns a formatted timestamp for logging
* @returns {string} Formatted timestamp [YYYY-MM-DD, HH:MM:SS]
*/
function getTimestamp() {
const now = new Date();
return `[${now.toLocaleString()}]`;
}
/**
* Logs a message with timestamp
* @param {string} message - Message to log
*/
function logWithTime(message) {
console.log(`${getTimestamp()} ${message}`);
}
/**
* Executes a command and returns its output
* @param {string} command - Command to execute
* @param {string} cwd - Working directory
* @returns {string} Command output
*/
function runCommand(command, cwd) {
try {
logWithTime(`Running: ${command} (in ${cwd})`);
const startTime = Date.now();
const result = execSync(command, {
cwd,
stdio: ['pipe', 'pipe', 'pipe'],
encoding: 'utf-8',
})
.toString()
.trim();
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
logWithTime(`Command completed in ${duration}s`);
return result;
} catch (error) {
logWithTime(`Error executing command: ${command}`);
logWithTime(error.message);
// We don't exit here because we want to handle errors during yarn install
throw error;
}
}
/**
* Build TypeScript and create a tarball
* @returns {string} Path to the created TypeScript tarball
*/
function buildTypeScriptTarball() {
logWithTime('Building TypeScript tarball...');
// Step 1: Build TypeScript
logWithTime('0. Running npm ci...');
runCommand('npm ci', TS_REPO_PATH);
logWithTime('1. Running npx hereby LKG...');
runCommand('npx hereby LKG', TS_REPO_PATH);
logWithTime('2. Running npx hereby clean...');
runCommand('npx hereby clean', TS_REPO_PATH);
logWithTime('3. Adding Git head to package.json...');
runCommand(
'node ./scripts/addPackageJsonGitHead.mjs package.json',
TS_REPO_PATH,
);
logWithTime('4. Creating npm package...');
runCommand('npm pack', TS_REPO_PATH);
// Step 5: Find the created tarball using fs.readdirSync instead of glob
const files = fs
.readdirSync(TS_REPO_PATH)
.filter(
(file) => file.startsWith('typescript-') && file.endsWith('.tgz'),
)
// Sort by creation time, newest first
.map((file) => {
const filePath = path.join(TS_REPO_PATH, file);
return {
name: file,
path: filePath,
ctime: fs.statSync(filePath).ctime,
};
})
.sort((a, b) => b.ctime - a.ctime)
.map((file) => file.name);
if (files.length === 0) {
logWithTime('Could not find TypeScript tarball after npm pack');
process.exit(1);
}
// Get the path to the most recently created tarball
const originalTarballPath = path.join(TS_REPO_PATH, files[0]);
// Rename it to a consistent name
const renamedTarballPath = path.join(TS_REPO_PATH, 'typescript.tgz');
// Remove any existing typescript.tgz first
try {
if (fs.existsSync(renamedTarballPath)) {
fs.unlinkSync(renamedTarballPath);
}
// Rename the file
fs.renameSync(originalTarballPath, renamedTarballPath);
logWithTime(`Renamed tarball to: ${renamedTarballPath}`);
} catch (error) {
logWithTime(`Error renaming tarball: ${error.message}`);
process.exit(1);
}
return renamedTarballPath;
}
/**
* Tests if the TypeScript installation is successful
* @returns {boolean} True if installation succeeds, false otherwise
*/
function testTypeScriptInstall() {
logWithTime('Testing TypeScript installation...');
// Step 1: Build TypeScript and rename tarball to typescript.tgz
try {
buildTypeScriptTarball();
} catch (error) {
logWithTime('Failed to build TypeScript tarball');
return false;
}
// Step 2: Install in dd repo
logWithTime('Installing TypeScript in dd repo...');
let tsVersion = 'unknown';
let installSuccessful = false;
let errorMessage = '';
try {
// Run yarn install
runCommand('yarn install', DD_REPO_PATH);
// Get the TypeScript version to log
const tsVersionOutput = runCommand('yarn tsc -v', DD_REPO_PATH);
tsVersion =
tsVersionOutput.split('\n').pop().replace('Version ', '') ||
'unknown';
// If we get here, installation was successful
installSuccessful = true;
logWithTime(`TypeScript version ${tsVersion} installed successfully`);
} catch (error) {
errorMessage = error.message;
logWithTime(
`TypeScript installation failed with error: ${errorMessage}`,
);
}
// Get the current TS commit hash
const commitHash = runCommand('git rev-parse HEAD', TS_REPO_PATH);
const shortHash = commitHash.substring(0, 8);
// Ensure logs directory exists
if (!fs.existsSync(LOGS_DIR)) {
fs.mkdirSync(LOGS_DIR, { recursive: true });
}
// Create a log file for this run
const fileName = `${tsVersion}-${shortHash}-${
installSuccessful ? 'success' : 'fail'
}.txt`;
const filePath = path.join(LOGS_DIR, fileName);
// Write result details to a file
const details = `Commit: ${commitHash}
TypeScript Version: ${tsVersion}
Installation: ${installSuccessful ? 'Success' : 'Failure'}
${!installSuccessful ? `Error: ${errorMessage}` : ''}`;
fs.writeFileSync(filePath, details);
// Also append a summary entry to the log file
const summaryEntry = `${getTimestamp()},${commitHash},${tsVersion},${
installSuccessful ? 'success' : 'failure'
}`;
fs.appendFileSync(LOG_FILE, `${summaryEntry}\n`);
logWithTime(`Test result: ${installSuccessful ? 'SUCCESS' : 'FAILURE'}`);
logWithTime(`Details saved to: ${filePath}`);
return installSuccessful;
}
/**
* Runs the TypeScript bisection process checking installation success
*/
async function bisectTypeScript() {
// Prepare logs directory
if (!fs.existsSync(LOGS_DIR)) {
fs.mkdirSync(LOGS_DIR, { recursive: true });
}
// Ensure we're in the TypeScript repo for bisection
process.chdir(TS_REPO_PATH);
// Check if a bisect is already in progress
logWithTime('Checking if a bisection is in progress...');
let bisectInProgress = false;
try {
const logOutput = runCommand('git bisect log', TS_REPO_PATH);
// If we get output and it has a bisect start, a bisection has been initiated
if (logOutput) {
bisectInProgress = true;
logWithTime('Current bisection state:');
logWithTime(logOutput);
} else {
logWithTime('No bisection in progress.');
}
} catch (error) {
logWithTime('No bisection in progress.');
}
// Create or continue with log file
if (!fs.existsSync(LOG_FILE)) {
fs.writeFileSync(LOG_FILE, 'timestamp,commit,ts_version,result\n');
logWithTime(`Created new log file at ${LOG_FILE}`);
} else if (bisectInProgress) {
logWithTime(`Continuing with existing log file at ${LOG_FILE}`);
} else {
// Reset log file when starting a new bisection
fs.writeFileSync(LOG_FILE, 'timestamp,commit,ts_version,result\n');
logWithTime(`Reset log file at ${LOG_FILE}`);
}
// Start bisection if not already in progress
if (!bisectInProgress) {
logWithTime(
'\n=== Starting new TypeScript bisection for install test ===',
);
runCommand('git bisect start', TS_REPO_PATH);
// You should adjust these versions to match your specific good/bad points
runCommand('git bisect bad v5.6.2', TS_REPO_PATH);
runCommand(
'git bisect good 15f67e0b482faf9f6a3ab9965f3c11196bf3e99b',
TS_REPO_PATH,
);
}
let bisectComplete = false;
// Continue bisection until complete
while (!bisectComplete) {
// Test installation success for current commit
const installSuccessful = testTypeScriptInstall();
// Mark the commit as good if install succeeds, bad if it fails
const verdict = installSuccessful ? 'bad' : 'good';
// Reset changes in ts repo before marking the commit
runCommand('git restore .', TS_REPO_PATH);
// Mark the commit
logWithTime(`\nMarking current commit as ${verdict}...`);
const result = runCommand(`git bisect ${verdict}`, TS_REPO_PATH);
logWithTime(result);
// Check if bisection is complete
if (result.includes('is the first bad commit')) {
logWithTime('\n=== Bisection Complete ===');
bisectComplete = true;
// Extract culprit commit hash
const commitHashMatch = result.match(
/^([a-f0-9]{40}) is the first bad commit/,
);
const culpritCommit = commitHashMatch
? commitHashMatch[1]
: 'unknown';
// Capture the full bisect log before resetting
logWithTime('Saving bisect log for future replay...');
try {
const bisectLog = runCommand('git bisect log', TS_REPO_PATH);
// Ensure the directory exists
const replayDir = path.dirname(BISECT_REPLAY_PATH);
if (!fs.existsSync(replayDir)) {
fs.mkdirSync(replayDir, { recursive: true });
}
// Write the bisect log
fs.writeFileSync(BISECT_REPLAY_PATH, bisectLog);
logWithTime(`Bisect log saved to: ${BISECT_REPLAY_PATH}`);
} catch (error) {
logWithTime(
`Warning: Could not save bisect log: ${error.message}`,
);
}
// Add final entry to log file
fs.appendFileSync(
LOG_FILE,
`\nBreaking change introduced in commit: ${culpritCommit}\n`,
);
logWithTime(
'\nThe bisection process has identified the commit that introduced the breaking change.',
);
logWithTime(`See ${LOG_FILE} for all results.`);
// Reset bisect when done
runCommand('git bisect reset', TS_REPO_PATH);
}
// Check if we've hit a merge base or any other special case
else if (result.includes('a merge base must be tested')) {
logWithTime('\n=== Bisection Needs More Information ===');
logWithTime('Skipping problematic commit...');
const skipResult = runCommand('git bisect skip', TS_REPO_PATH);
logWithTime(skipResult);
}
}
}
// Run the bisection process
bisectTypeScript().catch((err) => {
logWithTime(`Error during bisection: ${err.message}`);
// Make sure to reset bisect even if there's an error
try {
runCommand('git bisect reset', TS_REPO_PATH);
} catch (resetErr) {
logWithTime(`Error resetting bisect: ${resetErr.message}`);
}
process.exit(1);
});