From 1ca35d87700cf7c64a467b73042dd6014d87e239 Mon Sep 17 00:00:00 2001 From: Ash Shaw Date: Sun, 7 Jun 2026 12:16:14 +0200 Subject: [PATCH 1/3] docs: codify issue template frontmatter decision (#880) --- .github/custom-instructions.md | 6 +- docs/AUTOMATION.md | 4 +- docs/ISSUE_CREATION_GUIDE.md | 6 +- .../__tests__/check-template-labels.test.js | 19 ++ .../agents/includes/check-template-labels.js | 237 +++++++++++++++++- 5 files changed, 259 insertions(+), 13 deletions(-) create mode 100644 scripts/agents/includes/__tests__/check-template-labels.test.js diff --git a/.github/custom-instructions.md b/.github/custom-instructions.md index 2a44c3ce6..d43e54f05 100644 --- a/.github/custom-instructions.md +++ b/.github/custom-instructions.md @@ -59,11 +59,11 @@ Pick from `.github/ISSUE_TEMPLATE/01-*.md` to `.github/ISSUE_TEMPLATE/26-*.md`. Current parity note: -- Canonical issue types = 29 (from `.github/issue-types.yml`). +- Canonical issue types = 35 (from `.github/issue-types.yml`). - Numbered templates = 26. -- Types currently without dedicated templates: `type:chore`, `type:question`, `type:support`. +- Several canonical types intentionally share broader templates. -For these three missing-template types, use the nearest template and explicitly state the target type in the opening section. +When the selected template is broader than the request, use the nearest numbered template and explicitly state the target canonical type in the opening section. ### 3. Fill structured sections completely diff --git a/docs/AUTOMATION.md b/docs/AUTOMATION.md index 46debad93..dab828252 100644 --- a/docs/AUTOMATION.md +++ b/docs/AUTOMATION.md @@ -74,7 +74,7 @@ If your project allows hotfixes directly to `main`, ensure validation workflows | **changelog-validate.yml** | develop | Enforce changelog requirements and PR labelling standards | changelog validation | | **planner.yml** | develop | Post merge-readiness checklists and exit criteria to PRs | planner.agent.js | | **reviewer.yml** | develop | Automated PR review and quality feedback | reviewer.agent.js | -| **project-meta-sync.yml** | develop | Sync project board with PR/issue status | project-meta-sync.agent.js | +| **project-meta-sync.yml** | develop | Sync project board with PR/issue labels, status, priority, type, and supported project fields | project-meta-sync.agent.js | | **checklist-finalisation.yml** | issues.closed / pull_request_target.closed | Final checklist sync for completed issues and merged PRs | workflow backstop | | **release.yml** | main | Versioning, changelog generation, tagging, and release notes | release.agent.js | | **reporting.yml** | develop | Generate metrics and activity reports | reporting.agent.js | @@ -147,7 +147,7 @@ The labelling agent enforces canonical label usage: Issue types are defined once in `.github/issue-types.yml` and used by both: -- **Issue templates:** Pre-populate the `type` field (maps to `type:*` labels) +- **Issue templates:** Provide the canonical template selection and body guidance for the intended `type:*` - **Labelling agent:** Auto-applies `type:*` labels based on issue type field and content heuristics **Enforcement:** One type per issue (one-hot principle); issue type field mirrors `type:*` label for consistency. diff --git a/docs/ISSUE_CREATION_GUIDE.md b/docs/ISSUE_CREATION_GUIDE.md index a93f651d0..7d2ed0a05 100644 --- a/docs/ISSUE_CREATION_GUIDE.md +++ b/docs/ISSUE_CREATION_GUIDE.md @@ -56,10 +56,10 @@ This guide helps contributors, team members, and AI agents create high-quality G ### Current Template Parity Note - Numbered issue templates available: 26 (`01`-`26`) -- Canonical issue types available: 29 -- Types currently without dedicated templates: `type:chore`, `type:question`, `type:support` +- Canonical issue types available: 35 +- Numbered templates intentionally cover multiple canonical types in a few places. -For these three, use the nearest template and state the intended canonical type in the issue body. +Where a template is broader than the request, use the nearest numbered template and state the intended canonical type in the issue body. --- diff --git a/scripts/agents/includes/__tests__/check-template-labels.test.js b/scripts/agents/includes/__tests__/check-template-labels.test.js new file mode 100644 index 000000000..7cfae8859 --- /dev/null +++ b/scripts/agents/includes/__tests__/check-template-labels.test.js @@ -0,0 +1,19 @@ +/** + * Smoke test for the issue template label validator. + */ +const path = require("path"); +const { execFileSync } = require("child_process"); + +describe("check-template-labels.js", () => { + it("validates the current issue template frontmatter and mappings", () => { + const scriptPath = path.join(__dirname, "../check-template-labels.js"); + const output = execFileSync(process.execPath, [scriptPath], { + cwd: path.join(__dirname, "../../../.."), + encoding: "utf8", + }); + + expect(output).toContain( + "All template frontmatter, labels, and type mappings are valid.", + ); + }); +}); diff --git a/scripts/agents/includes/check-template-labels.js b/scripts/agents/includes/check-template-labels.js index a29b81d96..87d7dfa20 100644 --- a/scripts/agents/includes/check-template-labels.js +++ b/scripts/agents/includes/check-template-labels.js @@ -1,9 +1,8 @@ #!/usr/bin/env node /** * check-template-labels.js - * Validates that all labels referenced in issue/PR templates exist in labels.yml + * Validates issue-template frontmatter, canonical labels, and template-to-type mappings. */ -// TODO: Align this helper with the latest automation spec updates. import fs from "fs"; import yaml from "js-yaml"; @@ -27,10 +26,114 @@ const ISSUE_TEMPLATE_DIR = resolveFromRoot( ".github/ISSUE_TEMPLATE", ); +const TEMPLATE_TYPE_MAP = { + "01-task.md": { + primaryType: "type:task", + }, + "02-bug.md": { + primaryType: "type:bug", + }, + "03-feature.md": { + primaryType: "type:feature", + secondaryTypes: ["type:enhancement"], + }, + "04-design.md": { + primaryType: "type:design", + secondaryTypes: ["type:ui", "type:a11y"], + }, + "05-epic.md": { + primaryType: "type:epic", + }, + "06-story.md": { + primaryType: "type:story", + }, + "07-improvement.md": { + primaryType: "type:improve", + secondaryTypes: ["type:enhancement"], + }, + "08-user-experience-feedback.md": { + primaryType: "type:ux-feedback", + }, + "09-code-refactor.md": { + primaryType: "type:refactor", + secondaryTypes: ["type:maintenance", "type:chore"], + }, + "10-build-ci.md": { + primaryType: "type:build", + secondaryTypes: ["type:ci"], + }, + "11-automation.md": { + primaryType: "type:automation", + }, + "12-testing-coverage.md": { + primaryType: "type:test", + secondaryTypes: ["type:qa"], + }, + "13-performance.md": { + primaryType: "type:performance", + }, + "14-a11y.md": { + primaryType: "type:a11y", + }, + "15-security.md": { + primaryType: "type:security", + }, + "16-compatibility.md": { + primaryType: "type:compatibility", + }, + "17-integration-issue.md": { + primaryType: "type:integration", + secondaryTypes: ["type:dependency"], + }, + "18-release.md": { + primaryType: "type:release", + }, + "19-maintenance.md": { + primaryType: "type:maintenance", + secondaryTypes: ["type:chore"], + }, + "20-documentation.md": { + primaryType: "type:documentation", + }, + "21-research.md": { + primaryType: "type:research", + secondaryTypes: ["type:investigation"], + }, + "22-audit.md": { + primaryType: "type:audit", + }, + "23-code-review.md": { + primaryType: "type:review", + }, + "24-ai-ops.md": { + primaryType: "type:ai-ops", + }, + "25-content-modelling.md": { + primaryType: "type:content-modelling", + }, + "26-help.md": { + primaryType: "type:help", + secondaryTypes: ["type:question", "type:support"], + }, +}; + function loadYaml(file) { return yaml.load(fs.readFileSync(file, "utf8")); } +function extractFrontmatter(content) { + const match = content.match(/^---\n([\s\S]*?)\n---\n?/); + if (!match) { + return null; + } + + try { + return yaml.load(match[1]) || {}; + } catch (error) { + return { __error: error.message }; + } +} + function getCanonicalLabels() { const labels = loadYaml(LABELS_FILE); return new Set( @@ -62,7 +165,7 @@ function getIssueTypeLabels() { function getTemplateLabels() { const files = fs .readdirSync(ISSUE_TEMPLATE_DIR) - .filter((f) => f.endsWith(".md")); + .filter((f) => /^\d{2}-.+\.md$/u.test(f)); const labelRegex = /labels?:\s*\[([^\]]+)\]|labels?:\s*([^\n]+)/gi; const labels = new Set(); for (const file of files) { @@ -86,19 +189,143 @@ function getTemplateLabels() { return labels; } +function normaliseTemplateLabels(labelsValue) { + if (!labelsValue) { + return []; + } + + if (Array.isArray(labelsValue)) { + return labelsValue.flatMap((label) => + typeof label === "string" ? [label.trim()] : [], + ); + } + + if (typeof labelsValue === "string") { + return labelsValue + .split(",") + .map((label) => label.replace(/['"\[\]]/gu, "").trim()) + .filter(Boolean); + } + + return []; +} + +function validateTemplateFrontmatter(file, frontmatter, canonicalLabels, issueTypeLabels) { + const requiredKeys = [ + "file_type", + "name", + "description", + "version", + "last_updated", + "category", + ]; + + if (!frontmatter) { + throw new Error(`Missing YAML frontmatter in ${file}`); + } + + if (frontmatter.__error) { + throw new Error(`Invalid YAML frontmatter in ${file}: ${frontmatter.__error}`); + } + + for (const key of requiredKeys) { + if (!frontmatter[key]) { + throw new Error(`Missing required frontmatter key "${key}" in ${file}`); + } + } + + if (frontmatter.file_type !== "issue-template") { + throw new Error( + `Unexpected file_type in ${file}: expected "issue-template", found "${frontmatter.file_type}"`, + ); + } + + const declaredLabels = normaliseTemplateLabels(frontmatter.labels); + const declaredTypes = normaliseTemplateLabels(frontmatter.type).filter((label) => + label.startsWith("type:"), + ); + + for (const label of declaredLabels) { + if (label && !canonicalLabels.has(label)) { + throw new Error(`Unknown label "${label}" referenced in ${file}`); + } + } + + for (const typeLabel of declaredTypes) { + if (!issueTypeLabels.has(typeLabel)) { + throw new Error(`Unknown issue type "${typeLabel}" referenced in ${file}`); + } + } +} + function main() { const canonical = getCanonicalLabels(); const issueTypeLabels = getIssueTypeLabels(); const templateLabels = getTemplateLabels(); + const templateFiles = fs + .readdirSync(ISSUE_TEMPLATE_DIR) + .filter((f) => /^\d{2}-.+\.md$/u.test(f)); const all = new Set([...issueTypeLabels, ...templateLabels]); const unknown = [...all].filter((l) => l && !canonical.has(l)); if (unknown.length) { console.error("Unknown labels found in templates or issue-types.yml:"); for (const l of unknown) console.error(` - ${l}`); process.exit(1); - } else { - console.log("All template and type labels are valid."); } + + const templateMapEntries = Object.entries(TEMPLATE_TYPE_MAP); + const missingMappedFiles = templateFiles.filter( + (file) => !TEMPLATE_TYPE_MAP[file], + ); + if (missingMappedFiles.length > 0) { + console.error("Missing template-to-type mapping entries:"); + for (const file of missingMappedFiles) console.error(` - ${file}`); + process.exit(1); + } + + const mappedTypes = new Set(); + for (const [file, mapping] of templateMapEntries) { + mappedTypes.add(mapping.primaryType); + for (const secondaryType of mapping.secondaryTypes || []) { + mappedTypes.add(secondaryType); + } + + const content = fs.readFileSync(path.join(ISSUE_TEMPLATE_DIR, file), "utf8"); + const frontmatter = extractFrontmatter(content); + validateTemplateFrontmatter(file, frontmatter, canonical, issueTypeLabels); + + const declaredTypes = [ + ...normaliseTemplateLabels(frontmatter?.type).filter((label) => + label.startsWith("type:"), + ), + ...normaliseTemplateLabels(frontmatter?.labels).filter((label) => + label.startsWith("type:"), + ), + ]; + + if (declaredTypes.length > 0) { + const allowedTypes = new Set([ + mapping.primaryType, + ...(mapping.secondaryTypes || []), + ]); + + const invalidTypes = declaredTypes.filter((typeLabel) => !allowedTypes.has(typeLabel)); + if (invalidTypes.length > 0) { + console.error(`Unexpected template-to-type mapping in ${file}:`); + for (const typeLabel of invalidTypes) console.error(` - ${typeLabel}`); + process.exit(1); + } + } + } + + const unknownMappedTypes = [...mappedTypes].filter((typeLabel) => !issueTypeLabels.has(typeLabel)); + if (unknownMappedTypes.length > 0) { + console.error("Template map references unknown issue types:"); + for (const typeLabel of unknownMappedTypes) console.error(` - ${typeLabel}`); + process.exit(1); + } + + console.log("All template frontmatter, labels, and type mappings are valid."); } main(); From e912cc68da382a7b45b1e229695a871e74a62010 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 12:27:05 +0000 Subject: [PATCH 2/3] chore: bump frontmatter last_updated and version for changed files https://claude.ai/code/session_01HntQfZXJeEGp6EGZGYzFCw --- docs/AUTOMATION.md | 2 +- docs/ISSUE_CREATION_GUIDE.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/AUTOMATION.md b/docs/AUTOMATION.md index dab828252..586fbbe3f 100644 --- a/docs/AUTOMATION.md +++ b/docs/AUTOMATION.md @@ -3,7 +3,7 @@ file_type: "documentation" title: "Automation & Workflows" description: "Strategy, governance, and workflow documentation for GitHub automation in LightSpeed repositories." version: "v1.0.2" -last_updated: "2026-06-08" +last_updated: "2026-06-09" owners: ["LightSpeedWP Team"] tags: ["automation", "workflows", "governance", "agents"] status: "active" diff --git a/docs/ISSUE_CREATION_GUIDE.md b/docs/ISSUE_CREATION_GUIDE.md index 7d2ed0a05..4a8fa93cc 100644 --- a/docs/ISSUE_CREATION_GUIDE.md +++ b/docs/ISSUE_CREATION_GUIDE.md @@ -2,9 +2,9 @@ title: GitHub Issue Creation Guide description: How to create well-formed issues, select templates, and trigger automation file_type: documentation -version: "1.0.1" +version: "1.0.2" created_date: "2026-05-31" -last_updated: '2026-06-03' +last_updated: '2026-06-09' author: Claude Code maintainer: Ash Shaw owners: From 04b4d5f3a7c7abe397553e0d6db84c4eb4293fae Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 12:28:38 +0000 Subject: [PATCH 3/3] fix: remove unnecessary escape in check-template-labels regex https://claude.ai/code/session_01HntQfZXJeEGp6EGZGYzFCw --- scripts/agents/includes/check-template-labels.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/agents/includes/check-template-labels.js b/scripts/agents/includes/check-template-labels.js index 87d7dfa20..fd937fd71 100644 --- a/scripts/agents/includes/check-template-labels.js +++ b/scripts/agents/includes/check-template-labels.js @@ -203,7 +203,7 @@ function normaliseTemplateLabels(labelsValue) { if (typeof labelsValue === "string") { return labelsValue .split(",") - .map((label) => label.replace(/['"\[\]]/gu, "").trim()) + .map((label) => label.replace(/['"\][]]/gu, "").trim()) .filter(Boolean); }