mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 20:29:00 +00:00
ci: name the commit that actually broke golden
Упавший тяжёлый гейт не пишет маркер переиспользования — и правильно, иначе починка осталась бы незамеченной. Но следствие в том, что следующий коммит гонит ту же job на тех же входах, падает так же, и письмо «Run failed» называет его. 29 августа так был назван0f7b6f5, документ ревью, который не может изменить ни одного пикселя: сцену без эталона добавилdbbe94ae. Теперь падение оставляет второй маркер — с тем же ключом, что у маркера успеха. Совпадение ключа доказывает равенство входов, поэтому повторное падение может честно сказать «красная с такого-то SHA, этот коммит её не ронял», а первое — «причина здесь». Само падение по-прежнему не кэшируется: job прогоняется всегда, меняется только формулировка в notice и summary. Первопричина записывается только на первом падении: иначе SHA съехал бы на свидетеля и сообщение перевернулось бы смыслом. Issue: #386 User-Visible: no
This commit is contained in:
+81
-2
@@ -21,7 +21,7 @@
|
||||
// Переиспользование физически не может ослабить релизный гейт.
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import { appendFileSync, existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { appendFileSync, existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { relative, resolve } from 'node:path';
|
||||
|
||||
import { sourceFingerprint } from './source-fingerprint.mjs';
|
||||
@@ -101,13 +101,92 @@ export function reuseKey(root, job) {
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Маркер падения (issue #386). Маркер успеха отвечает на вопрос «прогонять ли
|
||||
* снова», этот — на другой: «с какого коммита эта job красная».
|
||||
*
|
||||
* Разница видна в почте. Упавший прогон маркера успеха не пишет, поэтому
|
||||
* следующий коммит гонит job заново и падает так же — и письмо «Run failed»
|
||||
* называет того, кто пушнул следующим. 29 августа так был назван `0f7b6f5`,
|
||||
* документ ревью, который не мог изменить ни одного пикселя: сцену без эталона
|
||||
* добавил `dbbe94ae` четырьмя коммитами раньше.
|
||||
*
|
||||
* Ключ здесь тот же, что у маркера успеха, а значит совпадение ключа доказывает
|
||||
* равенство входов. Само падение при этом не кэшируется: job прогоняется всегда,
|
||||
* иначе починка осталась бы незамеченной. Меняется только формулировка.
|
||||
*/
|
||||
export function parseFailureMarker(text) {
|
||||
if (typeof text !== 'string' || !text.trim()) return null;
|
||||
const field = (name) => text.match(new RegExp(`^${name}:\\s*(.+)$`, 'm'))?.[1]?.trim() || '';
|
||||
const sha = field('SHA');
|
||||
return { sha, runUrl: field('прогон') };
|
||||
}
|
||||
|
||||
const short = (sha) => (typeof sha === 'string' && /^[0-9a-f]{7,}$/i.test(sha) ? sha.slice(0, 8) : '');
|
||||
|
||||
/**
|
||||
* Формулировка для упавшей job. `prior` — разобранный маркер предыдущего
|
||||
* падения с тем же ключом либо null.
|
||||
*/
|
||||
export function inheritedFailureNote({ job, sha, runUrl, prior }) {
|
||||
const here = short(sha);
|
||||
if (!prior) {
|
||||
return {
|
||||
first: true,
|
||||
notice: `${job} упала впервые на этих входах — причина в этом коммите${here ? ` (${here})` : ''}`,
|
||||
summary: [
|
||||
`### ${job}: падение на новых входах`,
|
||||
`Предыдущего падения с тем же ключом переиспользования не было — изменения этого коммита${here ? ` (\`${here}\`)` : ''} и есть причина.`,
|
||||
],
|
||||
};
|
||||
}
|
||||
const first = short(prior.sha);
|
||||
const since = first ? `с \`${first}\`` : 'с более раннего прогона (SHA в маркере не записан)';
|
||||
const link = prior.runUrl ? ` Первый такой прогон: ${prior.runUrl}` : '';
|
||||
return {
|
||||
first: false,
|
||||
notice: `${job} красная на тех же входах ${first ? `с ${first}` : '(SHA первого падения не записан)'}`
|
||||
+ ` — этот коммит её не ронял`,
|
||||
summary: [
|
||||
`### ${job}: унаследованное падение`,
|
||||
`Входы этой job побайтово те же, что в первом падении ${since}: этот коммит её не ронял.`,
|
||||
'Красноту снимает либо приёмка эталонов, либо правка того, что job проверяет —'
|
||||
+ ' и то и другое меняет ключ переиспользования.' + link,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function main(argv) {
|
||||
const job = (argv.find((a) => a.startsWith('--job=')) || '').slice('--job='.length);
|
||||
const root = (argv.find((a) => a.startsWith('--repo=')) || '').slice('--repo='.length) || process.cwd();
|
||||
if (!job) {
|
||||
process.stderr.write(`usage: gate-reuse.mjs --job=<${JOBS.join('|')}> [--repo=<path>]\n`);
|
||||
process.stderr.write(`usage: gate-reuse.mjs --job=<${JOBS.join('|')}> [--note --marker=<path>] [--repo=<path>]\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
// Режим объяснения падения (#386): ключ уже посчитан job `reuse`, считать его
|
||||
// здесь заново нельзя — дерево то же, но лишний проход по эталонам стоит
|
||||
// секунд, а расхождение с ключом кэша было бы незаметным и вредным.
|
||||
if (argv.includes('--note')) {
|
||||
const marker = (argv.find((a) => a.startsWith('--marker=')) || '').slice('--marker='.length)
|
||||
|| '.fail-marker';
|
||||
const prior = existsSync(marker) ? parseFailureMarker(readFileSync(marker, 'utf8')) : null;
|
||||
const sha = process.env.GITHUB_SHA || '';
|
||||
const runUrl = process.env.RUN_URL || '';
|
||||
const note = inheritedFailureNote({ job, sha, runUrl, prior });
|
||||
process.stdout.write(`::notice::${note.notice} (#386)\n`);
|
||||
if (process.env.GITHUB_STEP_SUMMARY) {
|
||||
appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${note.summary.join('\n\n')}\n`);
|
||||
}
|
||||
// Первое падение оставляет маркер следующим прогонам; унаследованное не
|
||||
// трогает файл, чтобы SHA первопричины не съехал на свидетеля.
|
||||
if (note.first) {
|
||||
writeFileSync(marker, `${job} упала\nSHA: ${sha}\nпрогон: ${runUrl}\n`);
|
||||
}
|
||||
if (process.env.GITHUB_OUTPUT) {
|
||||
appendFileSync(process.env.GITHUB_OUTPUT, `first=${note.first}\n`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const key = reuseKey(root, job);
|
||||
process.stdout.write(`${key}\n`);
|
||||
if (process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, `key=${key}\n`);
|
||||
|
||||
Reference in New Issue
Block a user