Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/node-support-window.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ jobs:
fi

gh issue create --assignee oddharsh \
--label "type: chore" --label "area: infra" \
--label "type: chore" --label "area: infra" --type Watch \
--title "$title" --body "$(cat <<BODY
\`\`\`
$(cat window.txt)
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-order.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ jobs:
gh issue comment "$number" --body "$body"
else
gh issue create --assignee oddharsh \
--label "type: test" --label "area: tools" \
--label "type: test" --label "area: tools" --type Watch \
--title "$title" --body "$body"
fi

Expand Down
54 changes: 54 additions & 0 deletions config/infra.json
Original file line number Diff line number Diff line change
Expand Up @@ -1215,6 +1215,60 @@
"mean to, and a deletion strips the label from every issue and pull",
"request that carried it, historically and with no undo."
],
"issue_types": [
{ "name": "Task", "color": "yellow", "description": "A specific piece of work" },
{ "name": "Bug", "color": "red", "description": "An unexpected problem or behavior" },
{ "name": "Feature", "color": "blue", "description": "A request, idea, or new functionality" },
{ "name": "Watch", "color": "purple", "description": "A tracker a robot keeps open until something outside the repo moves" }
],
"$issue_types": [
"ACCOUNT-WIDE, unlike the labels above. GitHub defines issue types on the",
"oddharsh account (GA for personal accounts 2026-11-11), so this list is",
"what every oddharsh repository offers, and editing it here edits theirs.",
"",
"The first three are GitHub's defaults, spelled as it ships them. `Watch`",
"is the one this repo adds: on 2026-11-11, 17 of the 19 issues ever",
"opened here were robot-filed trackers (timbrado canary legs, the",
"node-support-window and test-order reports, the dependency digest).",
"They close when an upstream release or a pin moves, which no other",
"type says.",
"",
"Colours are GitHub's named palette (gray, blue, green, yellow, orange,",
"red, pink, purple), not hex. A workflow's `gh issue create --type`",
"must name a type declared here, since gh cannot apply one the account",
"lacks, so check-infra scans for it the same way it scans `--label`.",
"",
"Writes need the classic `user` scope, which the default gh login lacks:",
"`gh auth refresh -h github.com -s user`, then",
"`bun run labels:sync -- --confirm`."
],
"issue_fields": [
{
"name": "Priority",
"data_type": "single_select",
"description": "How soon this needs attention",
"options": [
{ "name": "Urgent", "color": "red" },
{ "name": "High", "color": "orange" },
{ "name": "Medium", "color": "yellow" },
{ "name": "Low", "color": "gray" }
]
},
{ "name": "Upstream", "data_type": "text", "description": "URL of the upstream issue, PR or release this waits on" }
],
"$issue_fields": [
"Account-wide too. Turning fields on creates Priority, Effort, Start date",
"and Target date; this keeps Priority as shipped and drops the other",
"three, since one owner with no sprints has nothing to put in them.",
"`Upstream` holds the link a Watch issue waits on, the same fact",
"tools/lib/upstream-watches.ts records for the pins.",
"",
"Every field is `all` visibility because the repo is public. Pinning a",
"field to a type has no REST endpoint, so that half lives in account",
"settings (Planning > Issue fields) and nothing here checks it.",
"`labels:sync -- --confirm --prune` deletes an undeclared field, and a",
"deletion takes its value off every issue that carried it."
],
"assignee": "oddharsh",
"self_labelling_workflows": 6,
"$self_labelling_workflows": [
Expand Down
1 change: 1 addition & 0 deletions docs/routines/workers-issue-triage.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Open one issue in `oddharsh/site`:

- **Title:** `workers-issue: ` followed by the error in under 70 characters.
- **Label:** `workers-issue`.
- **Type:** `Bug`.
- **Body**, in this order: the marker line from step 1 as the first line; a
two-or-three sentence summary of what failed and where; the redacted error
and the relevant stack frames in a code block; your findings from step 3
Expand Down
20 changes: 20 additions & 0 deletions tools/check-infra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,18 @@ async function checkTriageDeclaration(repo) {
// that touch no label at all. Backslash continuations are folded first
// because every one of these invocations is wrapped across lines, and each
// passes `--label` before the multi-line `--body` that ends the logical line.
// Issue types are account state with GitHub's named palette rather than hex,
// and a `gh issue create --type` naming one that is missing cannot apply it.
const COLORS = ["gray", "blue", "green", "yellow", "orange", "red", "pink", "purple"];
const types = new Set<string>();
for (const type of triage.issue_types ?? []) {
const at = `infra.json issue type ${JSON.stringify(type.name)}`;
if (types.has(type.name)) fail(`${at} is declared twice`);
types.add(type.name);
if (!COLORS.includes(type.color)) fail(`${at} has colour ${JSON.stringify(type.color)}, which is not one of GitHub's ${COLORS.join(", ")}`);
if (!type.description) fail(`${at} has no description`);
}

const dir = join(ROOT, ".github/workflows");
let inline = 0;
for (const file of (await readdir(dir)).filter((f) => f.endsWith(".yml"))) {
Expand All @@ -410,6 +422,14 @@ async function checkTriageDeclaration(repo) {
);
}
}
for (const m of text.matchAll(/--type\s+(?:"([^"]+)"|'([^']+)'|(\S+))/g)) {
const name = m[1] ?? m[2] ?? m[3];
if (!types.has(name)) {
fail(
`.github/workflows/${file} passes \`--type ${JSON.stringify(name)}\`, which infra.json does not declare. \`gh issue create --type\` cannot apply a type the account lacks, so the report lands untyped or not at all`,
);
}
}
for (const m of text.matchAll(/--assignee\s+(?:"([^"]+)"|'([^']+)'|(\S+))/g)) {
const who = m[1] ?? m[2] ?? m[3];
if (who && who !== triage.assignee) {
Expand Down
42 changes: 42 additions & 0 deletions tools/lib/infra-compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,7 @@ export type WorkflowBlocks = { total: number; without: string[] };
export type RepositoryObservation = {
core: Read<RulesetsObservation>;
labels?: Read<any[]>;
issueTypes?: Read<any[]>;
codeScanning?: Read<any>;
actions?: Read<{ live: any; workflow: any }>;
workflowBlocks: WorkflowBlocks;
Expand Down Expand Up @@ -1284,6 +1285,46 @@ export function compareLabels(repo, labels: Read<any[]> | undefined, opts: Repos
return out.list;
}

// Issue types are ACCOUNT state that the repository mirrors, so this reads
// /repos/:slug/issue-types, which a repo-scoped token can see, rather than
// /user/issue-types, which needs `read:user`. That endpoint 404s without a
// credential (checked 2026-11-11), so an unread list is an advisory.
//
// A missing or disabled type FAILS, because a workflow passing it to
// `gh issue create --type` cannot apply it. A stray type warns: it is
// another repository's business as much as this one's.
export function compareIssueTypes(repo, types: Read<any[]> | undefined): Finding[] {
const declared = repo.triage?.issue_types;
if (!declared) return [];
const out = collector();
const slug = `${repo.owner}/${repo.name}`;
if (!types || !types.ok) {
out.warn(
`issue types could not be read: ${types ? types.error : UNOBSERVED}. The endpoint needs a token: GITHUB_TOKEN=$(gh auth token) bun run infra:check`,
);
return out.list;
}
const byName = new Map<string, any>(types.value.map((t) => [t.name, t]));
for (const want of declared) {
const got = byName.get(want.name);
byName.delete(want.name);
if (!got || !got.is_enabled) {
out.fail(`${slug} has no enabled issue type ${JSON.stringify(want.name)}. Run \`bun run labels:sync -- --confirm\` with the \`user\` scope`);
continue;
}
if (got.color !== want.color || (got.description ?? "") !== want.description) {
out.fail(
`issue type ${JSON.stringify(want.name)} is ${got.color} ${JSON.stringify(got.description ?? "")}, declared ${want.color} ${JSON.stringify(want.description)} (\`bun run labels:sync -- --confirm\`)`,
);
}
}
const strays = [...byName.keys()];
if (strays.length) out.warn(`the account carries ${strays.length} undeclared issue type(s): ${strays.join(", ")}`);
if (out.failed()) return out.list;
out.pass(`${slug}'s ${declared.length} declared issue type(s) match${strays.length ? `, with ${strays.length} stray` : ""}`);
return out.list;
}

// CodeQL default setup, declared for the same reason the rulesets are: it is a
// curated decision living in a dashboard, and #241 recorded the language list
// plus the cost argument for it in MAINTENANCE.md while noting infra:check
Expand Down Expand Up @@ -1538,6 +1579,7 @@ export function compareRepository(repo, observed: RepositoryObservation, opts: R
return [
...findings,
...compareLabels(repo, observed.labels, opts),
...compareIssueTypes(repo, observed.issueTypes),
...compareCodeScanning(repo, observed.codeScanning),
...compareActionsPermissions(repo, observed.actions, observed.workflowBlocks),
...compareSecretScanning(repo, observed.core.value.meta),
Expand Down
3 changes: 3 additions & 0 deletions tools/lib/infra-ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,9 @@ export async function readRepository(gh: GithubGet, repo, workflowBlocks: Reposi
return live;
});
}
if (repo.triage?.issue_types) {
observed.issueTypes = await attempt(() => gh(`/repos/${slug}/issue-types`));
}
if (repo.code_scanning) {
observed.codeScanning = await attempt(() => gh(`/repos/${slug}/code-scanning/default-setup`));
}
Expand Down
Loading
Loading