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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,21 @@
"approved_at": null,
"approval_comment": null,
"blocks": "Stage 2 configuration PR (T059)"
},
{
"kind": "LABEL-UPDATE-REQUEST",
"task": "T046",
"issue_number": 3834,
"url": "https://github.com/lightspeedwp/.github/issues/3834",
"covers": [
"the full spec 008 label mapping in evidence/linear-labels.json",
"every import that has no request of its own"
],
"status": "pending",
"approved_by": null,
"approved_at": null,
"approval_comment": null,
"blocks": "Stage 2 configuration PR (T059)"
}
]
}

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .github/specs/008-label-audit-consolidation/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ The active labelling agent removes labels outside `labels.yml` today, so this ru
- [ ] T074 [US4] Give every branch type in `.github/branch-types.yml` exactly one canonical `type:*` label in `.github/branch-labels.yml` `default_labels`, because `scripts/pr-template-router.js` applies those and never the template frontmatter `labels`: add entries for the 13 types that have none (`aiops`, `automation`, `epic`, `doc`, `content`, `api`, `schema`, `seo`, `config`, `migrate`, `qa`, `uat`, `telemetry`); set the 10 that differ from their routed template (`hotfix`, `revert`, `docs`, `i18n`, `perf`, `build`, `ops`, `audit`, `codex`, `research`) to the template's canonical type label, because constitution v1.4.0 makes the template's type authoritative (for example `audit` → `type:audit`, `hotfix` → `type:release`, `docs` → `type:docs`, `codex` → `type:aiops`); `branch-labels.yml` is locked since v1.4.0, so add this change to the T046b `[TEMPLATE-UPDATE-REQUEST]` and ship it in the T059 configuration PR only after that request is approved (depends on #3732 merging and on T046b approval) and check the T046b acceptance criterion ("an `aiops/` branch PR receives `type:aiops`") by running `node scripts/pr-template-router.js aiops/example-branch` per FR-014, SC-011 (partial)
- [ ] T075 [US4] Make `scripts/automation/label-inventory.js` read a dedicated variable (for example `LABEL_INVENTORY_TOKEN`) instead of `GITHUB_TOKEN`, refuse to run when `GITHUB_ACTIONS` is set, and exit non-zero when the organisation listing returns no private repositories although `GET /orgs/{org}` reports some, so a repository-scoped token cannot produce a silently incomplete `github-api-labels.json`; add tests and update T041's command per FR-010, FR-018 (partial) — implemented in #3734, which is still open: until it merges, the script reads `GITHUB_TOKEN` and has no guard, so T075 stays open and T041 does not run (it runs after T075)
- [ ] T076 [US4] Add to the T046b `[TEMPLATE-UPDATE-REQUEST]` draft (`.github/reports/audits/2026-09-14-label-audit/change-requests/pr-aiops-template-request.md`) a reconciliation of `.github/PULL_REQUEST_TEMPLATE/config.yml` routes with `.github/branch-types.yml`, which the router reads (18 prefixes differ, for example `audit/`, `codex/`, `test/`, `security/`), and correct the draft's routing sentence (`aiops/`, `codex/` and `automation/` route to `pr_aiops.md`; `proto/` routes to `pr_feature.md`); the direction is fixed by constitution v1.4.0: `config.yml` is corrected to mirror `branch-types.yml`, never the reverse, and the edit to the locked `config.yml` ships in the T059 configuration PR only after the request is approved (#3732 has merged, so constitution v1.4.0 is active; depends on T046b) per FR-011, research R18 and R23 (contradicts)
- [ ] T077 [US4] When T043 fills `mappings[]`, update `scripts/validation/__tests__/label-audit-stage-one.test.js` in the same commit: it currently asserts `mappings` is empty. Its OpenSpec target check (`openspec` → `spec`) already matches the 2026-10-02 Q3 answer and stays per T043, T048 (partial)
- [x] T077 [US4] When T043 fills `mappings[]`, update `scripts/validation/__tests__/label-audit-stage-one.test.js` in the same commit: it currently asserts `mappings` is empty. Its OpenSpec target check (`openspec` → `spec`) already matches the 2026-10-02 Q3 answer and stays per T043, T048 (partial)
- [x] T078 [US4] Replace the approver handle `@ashley` with `@ashleyshaw` in `.github/specs/008-label-audit-consolidation/` (`spec.md`, `plan.md`, `tasks.md`, `contracts/`, `quickstart.md`, `checklists/`) and in the T049 gate draft `.github/reports/audits/2026-09-14-label-audit/change-requests/label-deletion-gate.md`, so the approval the FR-016 tool checks by login matches the text; leave `@ashleyshaw` unchanged and keep quoted historical comments as written (constitution v1.4.0, analysis finding I2) per FR-016 (contradicts) — done in #3703 for the spec 008 artefacts (44 handles) and for the last handle in the gate draft (`label-deletion-gate.md`, scope paragraph, which #3725 left); T050 keeps `@ashley` until #3725 merges, to avoid a conflict, and T079 changes it
- [ ] T079 [US4] After #3703 and #3704 merge (#3725 and #3732 have merged), reconcile `.github/specs/008-label-audit-consolidation/tasks.md` on `develop`: tick T062a (done in #3704), confirm T042, T046b, T048 and T049 are ticked (done in #3725), update the completion totals and the counts in `tests/js/label-consolidation-post-merge-docs.test.js`, and close T009 as superseded by T041, and in T050 change `@ashley` to `@ashleyshaw` (left by T078 to avoid a conflict with #3725, which has since merged) and add the FR-021 interim rule (waiting requests carry `status:blocked` until `meta:needs-approval` is in `labels.yml`; T059's follow-up swaps them) per plan: Progress (partial)

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Label Import Requests** — Every imported label now names the request that approves it, and the request is open as an issue. ([#3834](https://github.com/lightspeedwp/.github/issues/3834))
- **Label Mapping** — Adds the label mapping and a draft change request for approval. Nothing is applied yet. ([PR #3831](https://github.com/lightspeedwp/.github/pull/3831))
- **Label Consolidation Tool** — Plans label renames and creations across repositories, with deletions gated by approval per repository. Dry run by default; private evidence stays private. ([PR #3823](https://github.com/lightspeedwp/.github/pull/3823))
- **Locked Files Guard** — Pull requests that change locked labels, issue types, templates or routing files now fail a check until a linked change request is approved by @ashleyshaw. (#3734)
Expand Down
18 changes: 18 additions & 0 deletions scripts/automation/__tests__/label-mapping.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,24 @@ describe('label mapping', () => {
).toThrow(/openspec:brand-new has no GAP_MAP decision/);
});

it('gives every import a change request, and area:labels cites #3757', () => {
const imports = build().mappings.filter((m) => m.action === 'import');
expect(imports).toHaveLength(49);
for (const entry of imports) expect(Number.isInteger(entry.change_request)).toBe(true);
expect(imports.find((m) => m.source === 'area:labels').change_request).toBe(3757);
expect(imports.find((m) => m.source === 'area:builds').change_request).toBe(3554);
});

it('reports an import that cites no change request', () => {
const { mappings, proposedNames } = build();
const broken = mappings.map((m) =>
m.source === 'area:labels' ? { ...m, change_request: null } : m
);
expect(validateMappings(broken, proposedNames, yml, openspecInFiles).join(' ')).toMatch(
/rule 10: import area:labels/
);
});

it('writes the evidence file only when validation found no problems', () => {
expect(shouldWrite([])).toBe(true);
expect(shouldWrite(['rule 5: the type family has 29 labels, not 25'])).toBe(false);
Expand Down
16 changes: 15 additions & 1 deletion scripts/automation/label-mapping.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ const REPREFIX = {
/** FR-012 project-specific labels, moved to a Linear team rather than imported. */
const TEAM_SCOPE = ['area:xero', 'area:flow', 'area:jobs', 'area:monorepo'];

/**
* Change requests that cover the imports (constitution Principle II; schema rule 10).
* #3834 is the request that carries the whole mapping, #3757 asks for `area:labels`
* and #3554 for the five imports below.
*/
const IMPORT_CHANGE_REQUEST = 3834;
const OWN_CHANGE_REQUESTS = { 'area:labels': 3757 };

/** The five imports of #3554, with the colours and descriptions set in FR-012. */
const ISSUE_3554 = {
'area:builds': {
Expand Down Expand Up @@ -602,7 +610,8 @@ function buildMappings({
description:
ownDescription ||
`Imported from Linear (${issueCount} issues); description to be written at approval.`,
notes: `Proposed: applied to ${issueCount} Linear issue${issueCount === 1 ? '' : 's'}. Colour is the ${colourSource}; description is ${ownDescription ? "Linear's own" : 'a placeholder'}. Check both at approval.`,
change_request: OWN_CHANGE_REQUESTS[name] || IMPORT_CHANGE_REQUEST,
notes: `Proposed: applied to ${issueCount} Linear issue${issueCount === 1 ? '' : 's'}. Colour is the ${colourSource}; description is ${ownDescription ? "Linear's own" : 'a placeholder'}. Check both at approval. Change request #${OWN_CHANGE_REQUESTS[name] || IMPORT_CHANGE_REQUEST}.`,
});
}

Expand Down Expand Up @@ -691,6 +700,11 @@ function validateMappings(mappings, proposedNames, yml, openspecInFiles = []) {
if (m.target && /^spec:\d+$/.test(m.target))
problems.push(`rule 9: ${m.source} targets the spec number ${m.target}`);
}
// 10. Every import cites the change request that approves it (constitution Principle II).
for (const m of mappings.filter((x) => x.action === 'import')) {
if (!Number.isInteger(m.change_request))
problems.push(`rule 10: import ${m.source} cites no change request`);
}
// Each source appears once.
const seen = new Set();
for (const m of mappings) {
Expand Down
2 changes: 1 addition & 1 deletion tests/js/label-consolidation-post-merge-docs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('Label consolidation post-merge task plan', () => {

expect(taskRows).toHaveLength(118);
expect(new Set(taskRows.map(({ id }) => id)).size).toBe(taskRows.length);
expect(taskRows.filter(({ done }) => done)).toHaveLength(68);
expect(taskRows.filter(({ done }) => done)).toHaveLength(69);
expect(phase8Count).toBe(67);
expect(tasks).toContain(`**Total Tasks**: ${taskRows.length}`);
expect(tasks).toContain(`All ${taskRows.length} tasks in phases 1-10`);
Expand Down
Loading