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
51 changes: 39 additions & 12 deletions engineering_method/issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,44 @@
r"<!-- engineering-method:issue (?P<payload>\{[^\n]*\}) -->"
)
ISSUE_PREFIX = "<!-- engineering-method:issue "
CONTROLLED_LABELS: tuple[tuple[str, str, str], ...] = (
("engineering-method", "0B7285", "Managed by Peter's Engineering Method."),
("priority:p0", "B60205", "Blocking or urgent work."),
("priority:p1", "D93F0B", "Next planned work."),
("priority:p2", "FBCA04", "Planned work."),
("priority:p3", "C5DEF5", "Later improvement."),
("status:open", "1D76DB", "Open Engineering Method work."),
("status:in-progress", "5319E7", "Engineering Method work in progress."),
("status:blocked", "B60205", "Engineering Method work currently blocked."),
)
CONTROLLED_LABEL_NAMES = frozenset(label[0] for label in CONTROLLED_LABELS)
LEGACY_METHOD_LABEL = "engineering-method"
_METHOD_NAME = re.compile(r"[a-z][a-z0-9-]{0,48}")


def method_label(root: Path | None = None) -> str:
"""Name of the method that bundles this package, read from its method.json.

Core is copied into each method; the label on managed Issues must name the
method that owns the backlog, not the one this package is named after.
Without a readable identity (Core's own checkout) the historical name is kept.
"""
root = Path(__file__).resolve().parents[1] if root is None else root
try:
name = json.loads((root / "method.json").read_text(encoding="utf-8"))["name"]
except (OSError, ValueError, KeyError, TypeError):
return LEGACY_METHOD_LABEL
return name if isinstance(name, str) and _METHOD_NAME.fullmatch(name) else LEGACY_METHOD_LABEL


def controlled_labels(label: str) -> tuple[tuple[str, str, str], ...]:
display = label.replace("-", " ").title()
return (
(label, "0B7285", f"Managed by Peter's {display}."),
("priority:p0", "B60205", "Blocking or urgent work."),
("priority:p1", "D93F0B", "Next planned work."),
("priority:p2", "FBCA04", "Planned work."),
("priority:p3", "C5DEF5", "Later improvement."),
("status:open", "1D76DB", f"Open {display} work."),
("status:in-progress", "5319E7", f"{display} work in progress."),
("status:blocked", "B60205", f"{display} work currently blocked."),
)


METHOD_LABEL = method_label()
CONTROLLED_LABELS: tuple[tuple[str, str, str], ...] = controlled_labels(METHOD_LABEL)
# The historical label stays controlled so that an Issue created before the
# method had its own label loses it on the next synchronization.
CONTROLLED_LABEL_NAMES = frozenset(label[0] for label in CONTROLLED_LABELS) | {LEGACY_METHOD_LABEL}
QUEUE_SCHEMA_VERSION = 1
QUEUE_KINDS = frozenset({"add", "status", "priority", "dependencies"})

Expand Down Expand Up @@ -147,7 +174,7 @@ def _remote_map(


def _labels(item: BacklogItem) -> tuple[str, ...]:
labels = ["engineering-method", f"priority:{item.priority.value.lower()}"]
labels = [METHOD_LABEL, f"priority:{item.priority.value.lower()}"]
if item.status is not TaskStatus.COMPLETE:
labels.append(f"status:{item.status.value.replace('_', '-')}")
return tuple(labels)
Expand Down
14 changes: 7 additions & 7 deletions tests/test_issues_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def test_partial_failure_retry_upserts_every_field_and_reconciles_relations(self
identifier="EM-002",
title="EM-002: Old blocker",
state="open",
labels=("engineering-method", "priority:p3", "status:open"),
labels=(issues.METHOD_LABEL, "priority:p3", "status:open"),
)
runner = MutableGitHubRunner(issues=tuple(noise + [parent, blocker]), page_size=100)
runner.sub_issues[150] = {90_002}
Expand Down Expand Up @@ -199,7 +199,7 @@ def test_partial_failure_retry_upserts_every_field_and_reconciles_relations(self
self.assertEqual(by_id["EM-001"]["state"], "open")
self.assertEqual(
{label["name"] for label in by_id["EM-001"]["labels"]},
{"custom", "engineering-method", "priority:p1", "status:open"},
{"custom", issues.METHOD_LABEL, "priority:p1", "status:open"},
)
self.assertIn('Notes JSON: "Keep context"', str(by_id["EM-001"]["body"]))
self.assertEqual(by_id["EM-002"]["state"], "closed")
Expand All @@ -209,7 +209,7 @@ def test_partial_failure_retry_upserts_every_field_and_reconciles_relations(self
self.assertEqual(
set(runner.labels),
{
"engineering-method",
issues.METHOD_LABEL,
"priority:p0",
"priority:p1",
"priority:p2",
Expand Down Expand Up @@ -240,21 +240,21 @@ def test_reparents_only_after_removing_the_stale_parent_relation(self) -> None:
101,
identifier="EM-001",
title="EM-001: Desired parent",
labels=("engineering-method", "priority:p1", "status:open"),
labels=(issues.METHOD_LABEL, "priority:p1", "status:open"),
),
remote_issue(
2,
102,
identifier="EM-001.1",
title="EM-001.1: Child",
labels=("engineering-method", "priority:p1", "status:open"),
labels=(issues.METHOD_LABEL, "priority:p1", "status:open"),
),
remote_issue(
3,
103,
identifier="EM-002",
title="EM-002: Stale parent",
labels=("engineering-method", "priority:p1", "status:open"),
labels=(issues.METHOD_LABEL, "priority:p1", "status:open"),
),
)
runner = MutableGitHubRunner(issues=remote)
Expand Down Expand Up @@ -328,7 +328,7 @@ def test_refresh_reads_remote_state_instead_of_remigrating_the_cache(self) -> No
),
"state": "open",
"labels": [
{"name": "engineering-method"},
{"name": issues.METHOD_LABEL},
{"name": "priority:p0"},
{"name": "status:in-progress"},
],
Expand Down
34 changes: 34 additions & 0 deletions tests/test_method_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,43 @@
import subprocess
import tempfile
import unittest
import unittest.mock
import zipfile
from scripts.package_plugin import build_package
ROOT=Path(__file__).resolve().parents[1]
import engineering_method.issues as issues
from engineering_method.gh import RemoteIssue
from engineering_method.models import BacklogItem, Priority, TaskStatus

class MethodLabelTests(unittest.TestCase):
"""Managed Issues carry the label of the method that owns the backlog."""
def identity(self, content):
root=Path(tempfile.mkdtemp());self.addCleanup(shutil.rmtree,root)
if content is not None:(root/'method.json').write_text(content)
return root
def test_label_comes_from_the_bundling_method_identity(self):
# A hard-coded name labels a Writing Method backlog as engineering-method.
self.assertEqual('writing-method',issues.method_label(self.identity(json.dumps({'name':'writing-method'}))))
self.assertEqual('engineering-method',issues.method_label(self.identity(json.dumps({'name':'engineering-method'}))))
def test_missing_or_unusable_identity_keeps_the_historical_label(self):
for content in [None,'not json',json.dumps({}),json.dumps({'name':'Bad Name; rm'}),json.dumps({'name':7})]:
self.assertEqual('engineering-method',issues.method_label(self.identity(content)))
def test_installed_identity_matches_this_checkout(self):
manifest=ROOT/'method.json'
expected=json.loads(manifest.read_text())['name'] if manifest.is_file() else 'engineering-method'
self.assertEqual(expected,issues.METHOD_LABEL)
self.assertEqual(issues.METHOD_LABEL,issues.CONTROLLED_LABELS[0][0])
def test_descriptions_name_the_method(self):
labels={name:description for name,_,description in issues.controlled_labels('writing-method')}
self.assertEqual("Managed by Peter's Writing Method.",labels['writing-method'])
self.assertEqual('Open Writing Method work.',labels['status:open'])
self.assertNotIn('engineering-method',labels)
def test_issue_labelled_under_the_historical_name_is_relabelled(self):
item=BacklogItem(id='WM-001',title='Video',status=TaskStatus.IN_PROGRESS,priority=Priority.P1,parent_id=None,depends_on=(),notes='',updated_at='2026-11-13T00:00:00Z')
remote=RemoteIssue(id=1,number=1,title='WM-001: Video',body='',state='open',labels=('engineering-method','priority:p1','status:open','custom'),updated_at='2026-11-13T00:00:00Z')
with unittest.mock.patch.object(issues,'METHOD_LABEL','writing-method'):
desired=issues._desired_remote_labels(item,remote)
self.assertEqual(('custom','writing-method','priority:p1','status:in-progress'),desired)

class MethodIdentityTests(unittest.TestCase):
def test_writing_archive_uses_committed_manifest_identity(self):
Expand Down
Loading