Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/refresh-entry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,14 @@ jobs:
ENTRY_SLUG: ${{ steps.refresh.outputs.slug }}
ENTRY_FILE: ${{ steps.refresh.outputs.file }}
ENTRY_CHANGES: ${{ steps.refresh.outputs.changes }}
ENTRY_VERIFIED_KEY: ${{ steps.refresh.outputs.verified_key }}
with:
script: |
const { owner, repo } = context.repo;
const slug = process.env.ENTRY_SLUG;
const file = process.env.ENTRY_FILE;
const changes = String(process.env.ENTRY_CHANGES || '');
const verifiedKey = process.env.ENTRY_VERIFIED_KEY || 'verified';
const branch = context.payload.repository?.default_branch || 'main';

await github.rest.issues.createComment({
Expand All @@ -164,7 +166,7 @@ jobs:
'',
'> ' + changes.split('\n').join('\n> '),
'',
`Maintainers: the entry is [\`${file}\`](https://github.com/${owner}/${repo}/edit/${branch}/${file}). Apply what applies, set \`verified:\` to the day you checked, and close this issue with the pull request.`
`Maintainers: the entry is [\`${file}\`](https://github.com/${owner}/${repo}/edit/${branch}/${file}). Apply what applies, set \`${verifiedKey}:\` to the day you checked, and close this issue with the pull request.`
].join('\n')
});

Expand Down
14 changes: 12 additions & 2 deletions .github/workflows/verification-sweep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,14 @@ jobs:
- name: Install npm dependencies
run: npm ci --ignore-scripts

# The issue bodies travel through a file, not a step output: an output is
# re-exported below as one environment entry, which Linux caps near
# 128 KiB, and two hundred issue bodies can pass that. An env entry that
# large stops the runner from launching the step that reads it.
- name: Find unconfirmed entries
id: sweep
env:
SWEEP_ISSUES_FILE: ${{ runner.temp }}/sweep-issues.json
run: node scripts/verification_sweep.mjs

# Runs whatever the count is: with nothing stale there may still be issues
Expand All @@ -82,14 +88,18 @@ jobs:
- name: Open, refresh and close the entry refresh issues
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
SWEEP_ISSUES: ${{ steps.sweep.outputs.issues }}
SWEEP_ISSUES_FILE: ${{ runner.temp }}/sweep-issues.json
SWEEP_SLUGS: ${{ steps.sweep.outputs.slugs }}
SWEEP_COUNT: ${{ steps.sweep.outputs.count }}
SWEEP_MAX_NEW: ${{ steps.sweep.outputs.max_new }}
with:
script: |
const fs = require('fs');
const { owner, repo } = context.repo;
const wanted = JSON.parse(process.env.SWEEP_ISSUES || '[]');
const issuesFile = process.env.SWEEP_ISSUES_FILE;
const wanted = issuesFile && fs.existsSync(issuesFile)
? JSON.parse(fs.readFileSync(issuesFile, 'utf8'))
: [];
const staleSlugs = new Set(String(process.env.SWEEP_SLUGS || '').split('\n').filter(Boolean));
const staleCount = Number(process.env.SWEEP_COUNT || '0');
const maxNew = Number(process.env.SWEEP_MAX_NEW || '20');
Expand Down
3 changes: 3 additions & 0 deletions scripts/refresh_entry_from_issue.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ function main() {
setOutput('slug', form.slug);
setOutput('file', relative);
setOutput('changes', form.changes);
// The guidance comment names the date field to stamp, which a catalog can
// rename via entry.verified_key — hand the resolved name to the workflow.
setOutput('verified_key', verifiedKey);
// Same branch name a "yes" answer on this issue would have used, so a
// submitter who answers "yes" and then edits the issue to "no" leaves the
// workflow a name to find and close that stale confirmation with.
Expand Down
23 changes: 15 additions & 8 deletions scripts/verification_sweep.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
* one issue per entry asking the people who can answer.
*
* Usage: node scripts/verification_sweep.mjs [--today YYYY-MM-DD] [--json]
* Outputs (Actions): `count`, `issues`, `max_new` — see
* Outputs (Actions): `count`, `slugs`, `max_new`; the issue bodies are written
* to the file named by $SWEEP_ISSUES_FILE — see
* .github/workflows/verification-sweep.yml
*
* An entry is "confirmed" on the newest of its `verified`, `updated` and
Expand Down Expand Up @@ -50,12 +51,14 @@ export const DEFAULT_MAX_NEW_ISSUES = 20;
* How many issue bodies one run hands to the workflow.
*
* A separate concern from the cap above, and a much larger number: this one is
* a size guard, not a courtesy. Every body travels through `$GITHUB_OUTPUT`,
* which is not an unbounded channel, so a catalog with a thousand overdue
* entries must not turn the monthly job permanently red. The list of stale
* slugs is emitted in full regardless (it is cheap, and the workflow needs all
* of it to decide which issues to close), so nothing beyond this is mistaken
* for fresh — it is only deferred, and the run summary says so.
* a size guard, not a courtesy. The bodies travel through the file named by
* `$SWEEP_ISSUES_FILE` — never a step output copied into one environment
* entry, which Linux caps near 128 KiB and which, exceeded, stops the runner
* from even launching the step that reads it — so the cap is no longer a
* transport ceiling, just a bound on how much JSON one run builds. The list of
* stale slugs is emitted in full regardless (it is cheap, and the workflow
* needs all of it to decide which issues to close), so nothing beyond this is
* mistaken for fresh — it is only deferred, and the run summary says so.
*/
export const MAX_ISSUE_PAYLOAD = 200;
/** The template of the per-entry dedupe marker; also parsed by the workflow. */
Expand Down Expand Up @@ -399,7 +402,11 @@ function main() {
}

setOutput('count', String(stale.length));
setOutput('issues', JSON.stringify(issues));
// The bodies go through a file: two hundred of them can pass 128 KiB, and an
// environment entry that large stops the runner from launching the step that
// would read it. See the MAX_ISSUE_PAYLOAD note above.
const issuesFile = process.env.SWEEP_ISSUES_FILE;
if (issuesFile) fs.writeFileSync(issuesFile, JSON.stringify(issues));
setOutput('slugs', stale.map((entry) => entry.slug).join('\n'));
setOutput('max_new', String(maxNew));
}
Expand Down
19 changes: 19 additions & 0 deletions test/scripts/release_workflows.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -967,3 +967,22 @@ test('protected-main automation stays reviewable and generated PRs can satisfy r
assert.match(codeql, /'Analyze ruby'/u);
assert.match(workflow('performance.yml'), /\['scale', 'Performance and scale \(dispatch\)'\]/u);
});

test('the sweep hands issue bodies to its reader through a file, not an environment entry', () => {
// steps.sweep.outputs.issues copied into `env:` becomes a single Linux
// environment entry, capped near 128 KiB — two hundred issue bodies can pass
// that, and the runner then cannot launch the step that reads it.
const sweep = workflow('verification-sweep.yml');
assert.doesNotMatch(sweep, /steps\.sweep\.outputs\.issues/u);
const named = sweep.match(/SWEEP_ISSUES_FILE: \$\{\{ runner\.temp \}\}\/sweep-issues\.json/gu) ?? [];
assert.equal(named.length, 2, 'writer and reader must name the same issues file');
});

test('the changes guidance names the configured verification key, not a literal', () => {
// A catalog that renames `verified` via entry.verified_key must not be told
// to stamp a field its sweep does not read.
const refresh = workflow('refresh-entry.yml');
assert.match(refresh, /ENTRY_VERIFIED_KEY: \$\{\{ steps\.refresh\.outputs\.verified_key \}\}/u);
assert.match(refresh, /set \\`\$\{verifiedKey\}:\\` to the day you checked/u);
assert.doesNotMatch(refresh, /set \\`verified:\\` to the day you checked/u);
});
33 changes: 33 additions & 0 deletions test/scripts/verification_sweep.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
* functions produce.
*/
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';

import {
DEFAULT_MAX_NEW_ISSUES,
Expand Down Expand Up @@ -404,3 +406,34 @@ test('a custom verified_key is honoured end to end: staleEntries stops nagging o
const withSchemaKeys = staleEntries(entries, todayDay, 365, verificationKeys);
assert.equal(withSchemaKeys.length, 0);
});

/* ------------------------------------------------------------------- main */

test('the issue bodies land in $SWEEP_ISSUES_FILE, never in $GITHUB_OUTPUT', () => {
// Two hundred bodies can pass 128 KiB, and a step output re-exported as one
// environment entry that large stops the runner from launching the step
// that reads it — so the bodies must bypass $GITHUB_OUTPUT entirely.
const { root, write } = repo();
write('_data/schema.yml', 'entry:\n path: "catalog"\nfields: []\n');
write('catalog/thing/index.md', '---\ntitle: "A thing"\npublished: 2020-01-01\n---\n\nBody.\n');
const issuesFile = path.join(root, 'sweep-issues.json');
const outputFile = path.join(root, 'github-output.txt');
fs.writeFileSync(outputFile, '');

const script = fileURLToPath(new URL('../../scripts/verification_sweep.mjs', import.meta.url));
execFileSync(process.execPath, [script, '--today', '2026-08-17'], {
cwd: root,
env: { ...process.env, SWEEP_ISSUES_FILE: issuesFile, GITHUB_OUTPUT: outputFile },
});

const raw = fs.readFileSync(issuesFile, 'utf8');
const issues = JSON.parse(raw);
assert.ok(Array.isArray(issues));
assert.equal(issues.length, 1);
assert.match(raw, /refresh-entry: thing/);

const outputs = fs.readFileSync(outputFile, 'utf8');
assert.match(outputs, /^count<</mu);
assert.match(outputs, /^slugs<</mu);
assert.doesNotMatch(outputs, /^issues<</mu, 'the bodies must not travel through $GITHUB_OUTPUT');
});