ci-audit: the weekly issue is the workflow bot's own (creator filter, newest first)

findIssue read the first 100 open issues oldest first and matched any issue
with the title and marker, so an outsider's copy won the match and a repo with
more than 100 open issues got a new audit issue every week. It now lists only
github-actions[bot] issues, newest first, and requires that author.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-31 18:55:12 +00:00
1 parent 530fbf50e2
commit 080268fb43
3 files changed
+26 -6

No files matched your search

+3 -2
View File
@@ -82,7 +82,8 @@ Turn it off with `gstack-config set ci_audit_ship off`.
`lib/ci-audit/templates/ci-audit-weekly.yml` runs the full report every Saturday at 15:00 UTC
and keeps one open "Weekly CI audit" issue current. A complete audit creates or replaces
the issue, or closes it when nothing is left to fix. A partial or failed audit comments
on the issue and keeps the last complete findings. The issue is matched by its title and
a marker line, so an issue a person opened with the same title is left alone. It reads
on the issue and keeps the last complete findings. The issue is matched by its author
(`github-actions[bot]`, the workflow's own token), its title and a marker line, so an
issue anyone else opened, even with the same title and marker, is left alone. It reads
with the workflow's `GITHUB_TOKEN` (1,000 calls an hour), so it runs with
`--max-calls 400`.
+8 -3
View File
@@ -45,11 +45,16 @@ export function issueBody(r: AuditReport, runUrl?: string): string {
return head + (md.length > MAX_BODY ? md.slice(0, MAX_BODY) + '\n\n(truncated; the full report is the run artifact)' : md);
}
/** The open issue this audit owns: its title and marker, so a person's issue with the same title is left alone. */
export const ISSUE_AUTHOR = 'github-actions[bot]';
/**
* The open issue this audit owns: opened by the workflow's own token, with its title and marker.
* Anyone can copy the title and marker, so an issue another account opened is never touched.
*/
export async function findIssue(client: ApiClient, repo: string): Promise<Existing | null> {
const res = await client.get(`/repos/${repo}/issues?state=open&per_page=100&sort=created&direction=asc`);
const res = await client.get(`/repos/${repo}/issues?state=open&creator=${encodeURIComponent(ISSUE_AUTHOR)}&per_page=100&sort=created&direction=desc`);
if (res.status !== 200) throw new AuditError('ci_audit_api_failed', `issues list returned HTTP ${res.status}`, 'now');
const hit = ((res.json as any[]) ?? []).find(i => !i.pull_request && i.title === ISSUE_TITLE && String(i.body ?? '').includes(ISSUE_MARKER));
const hit = ((res.json as any[]) ?? []).find(i => !i.pull_request && i.user?.login === ISSUE_AUTHOR && i.title === ISSUE_TITLE && String(i.body ?? '').includes(ISSUE_MARKER));
return hit ? { number: Number(hit.number), body: String(hit.body) } : null;
}
+15 -1
View File
@@ -7,7 +7,7 @@ import type { Run } from '../lib/ci-audit/collect';
import { concurrencyFlag, lessonRunners } from '../lib/ci-audit/lessons';
import { cohort, measure, phases } from '../lib/ci-audit/measure';
import { priceRunner } from '../lib/ci-audit/prices';
import { ISSUE_MARKER, planIssue } from '../lib/ci-audit/publish';
import { ISSUE_MARKER, ISSUE_TITLE, findIssue, planIssue } from '../lib/ci-audit/publish';
import type { AuditReport } from '../lib/ci-audit/report';
import { CI_SHAPING, MIN_COHORT, comparable, judgeHead, staticChecks } from '../lib/ci-audit/ship-check';
import { parseWorkflow, readStatic } from '../lib/ci-audit/static';
@@ -263,6 +263,20 @@ describe('weekly issue', () => {
expect(planIssue({ exit: 3, report: null, error: 'CI_AUDIT: unavailable x (ci_audit_deadline)' }, null).action).toBe('none');
expect(planIssue({ exit: 4, report: { ...withFixes, partial: ['x'] } as AuditReport }, null).action).toBe('create');
});
test('finds only the workflow bot\'s own issue, past 100 open issues and an outsider\'s copy', async () => {
const issue = (number: number, login: string, title = 'other', body = '') => ({ number, title, body, user: { login } });
const open = [issue(1, 'outsider', ISSUE_TITLE, `${ISSUE_MARKER}\nfake findings`),
...Array.from({ length: 150 }, (_, i) => issue(i + 2, 'person')),
issue(152, 'github-actions[bot]', ISSUE_TITLE, `${ISSUE_MARKER}\nreal findings`)];
const client = { text: async () => null, get: async (apiPath: string) => {
const q = new URL(apiPath, 'https://api.github.com').searchParams;
const rows = open.filter(i => !q.get('creator') || i.user.login === q.get('creator'));
if (q.get('direction') !== 'asc') rows.reverse();
return { status: 200, json: rows.slice(0, Number(q.get('per_page') ?? 30)), headers: {} };
} };
expect(await findIssue(client as never, 'o/n')).toEqual({ number: 152, body: `${ISSUE_MARKER}\nreal findings` });
});
});
describe('Saturday workflow template', () => {