Files
hermes-agent/tests/install/e2e-assets/red-tracker.mjs
teknium1 5afba1e8d9 ci(install-e2e): keep one install-e2e-red issue in step with the scheduled matrix
A red scheduled run blocked nothing and told nobody. install-e2e-red.yml
runs after every scheduled "Install & Update E2E" run: red opens one issue
labelled install-e2e-red (or rewrites the open one's body in place) with the
red legs grouped by failure class and linked; the first green run closes
it. No per-run comment, never a second issue.

It is its own workflow_run workflow because install-e2e.yml is also called
by stable-release.yml with read-only permissions, and a nested job asking
for issues: write would fail that call at startup. workflow_dispatch with a
dry-run default previews the change for any run id.
2026-09-27 04:15:09 -07:00

119 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* Keep ONE tracking issue in step with the scheduled install/update matrix.
*
* Run by .github/workflows/install-e2e-red.yml after every scheduled
* "Install & Update E2E" run. A red run (any leg failed, was cancelled, or timed
* out) opens the issue labelled `install-e2e-red`, or rewrites the body of the
* one already open, so the issue always names the CURRENT red legs and the run
* that produced them. A green run closes it with a pointer to that run. Never a
* second issue and never a comment per red run: subscribers hear about the open
* and the close, and the body is the live state in between.
*
* node tests/install/e2e-assets/red-tracker.mjs --run-id <id> [--dry-run]
*
* Needs GH_TOKEN (issues: write, actions: read) and GITHUB_REPOSITORY.
*/
import { execFileSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseArgs } from 'node:util';
export const LABEL = 'install-e2e-red';
const RED = new Set(['failure', 'cancelled', 'timed_out', 'startup_failure']);
const MAX_ROWS = 60;
/**
* Decide what the tracker does for one completed run. Pure, so the policy is
* testable without GitHub.
*
* @param {{conclusion: string | null, html_url: string, head_sha: string, created_at: string}} run
* @param {{name: string, conclusion: string | null, html_url: string, steps?: {name: string, conclusion: string | null}[]}[]} jobs
* @param {{number: number} | null} openIssue
* @returns {{action: 'open' | 'update' | 'close' | 'none', title?: string, body?: string}}
*/
export function planTracker(run, jobs, openIssue) {
const red = jobs.filter((j) => RED.has(String(j.conclusion)));
const runRed = RED.has(String(run.conclusion)) || red.length > 0;
if (!runRed) {
if (run.conclusion === 'success' && openIssue) {
return { action: 'close', body: `The scheduled matrix is green again: ${run.html_url} (\`${run.head_sha.slice(0, 10)}\`). Closing; the next red run reopens a fresh tracker.` };
}
return { action: 'none' };
}
// Group by (conclusion, step) so 34 legs cancelled in the same step read as
// one failure class, not 34 unrelated reds.
/** @type {Map<string, typeof red>} */
const classes = new Map();
for (const job of red) {
const step = (job.steps || []).find((s) => RED.has(String(s.conclusion)))?.name || '(no step reported)';
// "Install v2026.9.24 (installer-script)" and "Install HEAD (installer-script+desktop)"
// are the same step of the driver; the leg name already carries ref and method.
const phase = step.replace(/\bv\d{4}\.\d+\.\d+(?:\.\d+)?\b|\bHEAD\b|\bNEXT\b/g, '<ref>').replace(/\s*\([^)]*\)\s*$/, '');
const key = `${job.conclusion} in step \`${phase}\``;
if (!classes.has(key)) classes.set(key, []);
classes.get(key)?.push(job);
}
const lines = [
`The scheduled **Install & Update E2E** run ${run.html_url} (\`${run.head_sha.slice(0, 10)}\`, ${run.created_at}) finished **${run.conclusion}** with ${red.length} red leg(s).`,
'',
'This issue is rewritten in place by `.github/workflows/install-e2e-red.yml` after every scheduled run and closed by the first green one. Root-cause each class below; a historical-release limitation that cannot be fixed goes into `tests/install/e2e-assets/known-failures.json`.',
'',
'### Red legs by failure class',
];
let rows = 0;
for (const [key, members] of [...classes].sort((a, b) => b[1].length - a[1].length)) {
lines.push('', `**${members.length} × ${key}**`, '');
for (const job of members) {
if (rows >= MAX_ROWS) break;
lines.push(`- [${job.name}](${job.html_url})`);
rows += 1;
}
}
if (red.length > rows) lines.push('', `…and ${red.length - rows} more; see the run's jobs list.`);
const title = `Install & Update E2E matrix is red (${red.length} leg${red.length === 1 ? '' : 's'})`;
return { action: openIssue ? 'update' : 'open', title, body: lines.join('\n') };
}
/** @param {string[]} args @returns {string} */
function gh(args) {
return execFileSync('gh', args, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
}
async function main() {
const { values } = parseArgs({ options: { 'run-id': { type: 'string' }, 'dry-run': { type: 'boolean', default: false } } });
const repo = process.env.GITHUB_REPOSITORY;
const runId = values['run-id'];
if (!repo || !runId || !/^\d+$/.test(runId)) throw new Error('need GITHUB_REPOSITORY and a numeric --run-id');
const run = JSON.parse(gh(['api', `repos/${repo}/actions/runs/${runId}`]));
const jobs = gh(['api', '--paginate', `repos/${repo}/actions/runs/${runId}/jobs?per_page=100`,
'--jq', '.jobs[] | {name, conclusion, html_url, steps: [.steps[]? | {name, conclusion}]}'])
.split('\n').filter((l) => l.trim()).map((l) => JSON.parse(l));
const open = JSON.parse(gh(['api', `repos/${repo}/issues?labels=${LABEL}&state=open&per_page=5`]))
.filter((/** @type {any} */ i) => !i.pull_request);
const openIssue = open.length ? { number: open[0].number } : null;
const plan = planTracker(run, jobs, openIssue);
console.log(`run ${runId}: conclusion=${run.conclusion} red-legs=${jobs.filter((j) => RED.has(String(j.conclusion))).length} open-tracker=${openIssue ? `#${openIssue.number}` : 'none'} -> ${plan.action}`);
if (values['dry-run'] || plan.action === 'none') {
if (plan.body) console.log(`\n--- ${plan.title || 'comment'} ---\n${plan.body}`);
return;
}
if (plan.action === 'open') {
gh(['label', 'create', LABEL, '--repo', repo, '--force', '--color', 'B60205',
'--description', 'The scheduled install/update E2E matrix is red (managed by install-e2e-red.yml)']);
const url = gh(['issue', 'create', '--repo', repo, '--label', LABEL, '--title', String(plan.title), '--body', String(plan.body)]);
console.log(`opened ${url.trim()}`);
} else if (plan.action === 'update' && openIssue) {
gh(['api', '-X', 'PATCH', `repos/${repo}/issues/${openIssue.number}`, '-f', `title=${plan.title}`, '-f', `body=${plan.body}`]);
console.log(`updated #${openIssue.number} in place`);
} else if (plan.action === 'close' && openIssue) {
gh(['issue', 'comment', String(openIssue.number), '--repo', repo, '--body', String(plan.body)]);
gh(['issue', 'close', String(openIssue.number), '--repo', repo]);
console.log(`closed #${openIssue.number}`);
}
}
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
await main();
}