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
5 changes: 0 additions & 5 deletions .github/labels.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 12 additions & 8 deletions docs/docs/developer/contributing-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion scripts/issue-lifecycle.mjs
Original file line number Diff line number Diff line change
@@ -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"] },
Expand Down
27 changes: 11 additions & 16 deletions scripts/issue-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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) => {
Expand All @@ -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" },
});
});
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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); }
Expand Down
Loading