diff --git a/.github/labels.json b/.github/labels.json index 070dad173..9d563286b 100644 --- a/.github/labels.json +++ b/.github/labels.json @@ -9,11 +9,6 @@ "color": "d876e3", "description": "Waiting for an answer to a specific linked question" }, - { - "name": "blocked", - "color": "b60205", - "description": "Cannot proceed until a named linked prerequisite is resolved" - }, { "name": "dependencies", "color": "ededed", diff --git a/docs/docs/developer/contributing-process.md b/docs/docs/developer/contributing-process.md index 4957b505a..726bb7015 100644 --- a/docs/docs/developer/contributing-process.md +++ b/docs/docs/developer/contributing-process.md @@ -36,7 +36,6 @@ No area assignment or label quota is required. | ------------------ | ----------------------------------------------------------------------------------------------- | | `needs-triage` | An open issue awaits initial maintainer review. Removing it means reviewed, not scheduled. | | `needs-info` | A specific unanswered question is linked in the issue. The maintainer removes it when answered. | -| `blocked` | A named, linked prerequisite prevents progress. The maintainer tracks its resolution. | | `dependencies` | Dependency updates or dependency maintenance. | | `documentation` | Documentation content, tooling or workspace dependencies. | | `integration` | A new or changed provider, source or backend integration. | @@ -48,18 +47,23 @@ No area assignment or label quota is required. The definitions are versioned in [`.github/labels.json`](https://github.com/OpenMapX/openmapx/blob/main/.github/labels.json). Maintainers own classification and these action labels. An arbitrary comment does -not resolve an information request or blocker. Add a new label only when there is +not resolve an information request. Add a new label only when there is a clear queue/filter consumer; review the taxonomy periodically. -New and reopened issues enter `needs-triage`. Closure removes `needs-triage`, -`needs-info` and `blocked`, while retaining the questions, dependency links and -closure reason in the record. The issue-lifecycle workflow re-reads current state +Record a prerequisite as a GitHub issue dependency ("blocked by"), not a label. +The issue then shows which issue blocks it, and `is:blocked` finds blocked issues. +A prerequisite outside GitHub, such as an upstream release, gets its own tracking +issue so it can be linked. + +New and reopened issues enter `needs-triage`. Closure removes `needs-triage` and +`needs-info`, while retaining the questions, dependency links and closure reason +in the record. The issue-lifecycle workflow re-reads current state and makes retries harmless; a later human triage decision takes precedence over an old reopen event. GitHub does not offer an atomic state-and-label update, so concurrent transitions are rechecked and repeated changes fail visibly for retry. -If reopening overlaps removal of `needs-info` or `blocked`, the workflow fails with -the affected issue and labels. Check the issue history before restoring them; -automatically restoring them could undo a later human decision. +If reopening overlaps removal of `needs-info`, the workflow fails with the +affected issue and label. Check the issue history before restoring it; +automatically restoring it could undo a later human decision. A monthly and manually triggered drift report lists closed workflow labels, missing/legacy label definitions and missing issue types. It does not repair diff --git a/scripts/issue-lifecycle.mjs b/scripts/issue-lifecycle.mjs index 50146e67f..9268cd332 100644 --- a/scripts/issue-lifecycle.mjs +++ b/scripts/issue-lifecycle.mjs @@ -1,7 +1,7 @@ import { readFileSync } from "node:fs"; const labels = JSON.parse(readFileSync(new URL("../.github/labels.json", import.meta.url), "utf8")); -const workflowLabels = ["needs-triage", "needs-info", "blocked"]; +const workflowLabels = ["needs-triage", "needs-info"]; const fieldPolicy = [ { name: "Priority", visibility: "public", options: ["High", "Low", "Medium", "Urgent"] }, { name: "Effort", visibility: "public", options: ["High", "Low", "Medium"] }, diff --git a/scripts/issue-lifecycle.test.ts b/scripts/issue-lifecycle.test.ts index 0f410c2dc..f1440b5b7 100644 --- a/scripts/issue-lifecycle.test.ts +++ b/scripts/issue-lifecycle.test.ts @@ -22,10 +22,7 @@ type Issue = { type?: { name: string } | null; }; type Event = { id: number; event: string; label?: { name: string }; actor?: { type: string } }; -function fixture( - state = "closed", - names = ["needs-triage", "needs-info", "blocked", "integration"], -) { +function fixture(state = "closed", names = ["needs-triage", "needs-info", "integration"]) { const issue: Issue = { number: 42, state, @@ -103,7 +100,7 @@ describe("issue lifecycle housekeeping", () => { it("does not strip labels when an old close event arrives after reopening", async () => { const f = fixture("open"); await f.run("closed"); - expect(names(f.issue)).toEqual(["blocked", "integration", "needs-info", "needs-triage"]); + expect(names(f.issue)).toEqual(["integration", "needs-info", "needs-triage"]); }); it("cleans current closed state when an old reopen event arrives", async () => { const f = fixture(); @@ -134,13 +131,11 @@ describe("issue lifecycle housekeeping", () => { f.issue.state = "open"; }); await f.run(); - expect(names(f.issue)).toEqual(["blocked", "integration", "needs-info", "needs-triage"]); + expect(names(f.issue)).toEqual(["integration", "needs-info", "needs-triage"]); }); it.each([ - ["needs-info", ["needs-info", "blocked"]], - ["blocked", ["blocked", "integration"]], - ["needs-info", ["needs-triage", "needs-info", "blocked"]], - ["blocked", ["needs-triage", "needs-info", "blocked"]], + ["needs-info", ["needs-info", "integration"]], + ["needs-info", ["needs-triage", "needs-info"]], ])( "reports an ambiguous reopen during removal of %s instead of claiming success", async (target, labels) => { @@ -156,14 +151,14 @@ describe("issue lifecycle housekeeping", () => { }, ); it("does not overwrite later human label decisions when reporting a reopen race", async () => { - const f = fixture("closed", ["needs-info", "blocked"]); + const f = fixture("closed", ["needs-info", "integration"]); f.onRemove(() => { f.issue.state = "open"; f.issue.labels = [{ name: "needs-info" }, { name: "help wanted" }]; f.events.push({ id: 3, event: "unlabeled", - label: { name: "blocked" }, + label: { name: "integration" }, actor: { type: "User" }, }); }); @@ -195,7 +190,7 @@ describe("issue lifecycle housekeeping", () => { it.each(["commented", "labeled"])("ignores unrelated %s actions", async (action) => { const f = fixture(); await f.run(action); - expect(names(f.issue)).toHaveLength(4); + expect(names(f.issue)).toHaveLength(3); }); it("ignores PR payloads and non-issue events before any API access", async () => { const f = fixture(); @@ -209,7 +204,7 @@ describe("issue lifecycle housekeeping", () => { const f = fixture(); f.issue.pull_request = {}; await f.run(); - expect(names(f.issue)).toHaveLength(4); + expect(names(f.issue)).toHaveLength(3); }); it("tolerates a raced missing label only after confirming its absence", async () => { const f = fixture(); @@ -228,7 +223,7 @@ describe("issue lifecycle housekeeping", () => { throw Object.assign(new Error("API failure"), { status }); }; await expect(f.run()).rejects.toThrow("API failure"); - expect(names(f.issue)).toHaveLength(4); + expect(names(f.issue)).toHaveLength(3); }, ); it("fails visibly on repeated concurrent changes rather than retrying forever", async () => { @@ -401,7 +396,7 @@ describe("contribution policy consumers", () => { "--input-type=module", "-e", ` - const issue = { state: 'closed', labels: ['needs-triage', 'needs-info', 'blocked', 'integration'].map(name => ({name})) }; + const issue = { state: 'closed', labels: ['needs-triage', 'needs-info', 'integration'].map(name => ({name})) }; const github = { rest: { issues: { get: async () => ({data: structuredClone(issue)}), removeLabel: async ({name}) => { issue.labels = issue.labels.filter(label => label.name !== name); }