docassert 0.8.1__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {docassert-0.8.1/docassert.egg-info → docassert-0.9.0}/PKG-INFO +1 -1
- {docassert-0.8.1 → docassert-0.9.0}/docassert/__init__.py +1 -1
- docassert-0.9.0/docassert/bridge/board.py +126 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/bridge/gh.py +15 -3
- {docassert-0.8.1 → docassert-0.9.0}/docassert/bridge/ops.py +30 -2
- {docassert-0.8.1 → docassert-0.9.0}/docassert/cli.py +31 -2
- {docassert-0.8.1 → docassert-0.9.0/docassert.egg-info}/PKG-INFO +1 -1
- {docassert-0.8.1 → docassert-0.9.0}/docassert.egg-info/SOURCES.txt +1 -0
- {docassert-0.8.1 → docassert-0.9.0}/tests/test_bridge.py +73 -0
- {docassert-0.8.1 → docassert-0.9.0}/LICENSE +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/NOTICE +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/README.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/__main__.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/consistency.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/adr.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/brd.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/charter.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/prd.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/project.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/profiles/agile-delivery.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/profiles/lean-startup.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/profiles/regulated-industry.yaml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/adr.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/benefits-realization.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/brd.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/business-case.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/charter.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/frnfr.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/prd.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/project.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/risk-register.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/rollback-plan.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/runbook.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/status-report.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/test-cases.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/user-story.schema.json +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/skills/doc-to-pmo/SKILL.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/adr.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/benefits-realization.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/brd.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/business-case.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/charter.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/data-migration-plan.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/frnfr.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/hypercare-plan.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/post-implementation-review.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/prd.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/project.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/qa-test-plan.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/risk-register.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/rollback-plan.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/runbook.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/status-report.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/test-cases.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/user-story.template.md +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/bridge/__init__.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/bridge/plan.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/config.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/consistency.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/extract.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/graph.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/loader.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/models.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/profiles.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/projects.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/report.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/rtm.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/scaffold.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/semantic.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/status/__init__.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/status/derive.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/status/render.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert/structural.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert.egg-info/dependency_links.txt +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert.egg-info/entry_points.txt +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert.egg-info/requires.txt +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/docassert.egg-info/top_level.txt +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/pyproject.toml +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/setup.cfg +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/tests/test_badge.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/tests/test_config.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/tests/test_consistency.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/tests/test_defects.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/tests/test_draft_relaxation.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/tests/test_extract.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/tests/test_graph.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/tests/test_json_report.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/tests/test_kinds_delivery.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/tests/test_kinds_governance.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/tests/test_kinds_operate.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/tests/test_kinds_reporting.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/tests/test_profiles.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/tests/test_projects.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/tests/test_scaffold.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/tests/test_status.py +0 -0
- {docassert-0.8.1 → docassert-0.9.0}/tests/test_structural.py +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Projects v2 board operations (require a token with the `project` scope).
|
|
2
|
+
|
|
3
|
+
The board is a *view* over the bridge-managed issues: every Feature and Story
|
|
4
|
+
issue becomes a project item carrying Type, Doc, and PMO Project fields. All
|
|
5
|
+
mutations are idempotent: re-adding an existing item returns it, and field
|
|
6
|
+
writes converge on the same values.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .gh import GhRunner
|
|
11
|
+
|
|
12
|
+
FIELD_SPECS: dict[str, dict] = {
|
|
13
|
+
"Type": {"dataType": "SINGLE_SELECT", "options": ["Feature", "Story"]},
|
|
14
|
+
"Doc": {"dataType": "TEXT"},
|
|
15
|
+
"PMO Project": {"dataType": "TEXT"},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_FIELDS_QUERY = """
|
|
19
|
+
query($owner: String!, $number: Int!) {
|
|
20
|
+
user(login: $owner) {
|
|
21
|
+
projectV2(number: $number) {
|
|
22
|
+
id
|
|
23
|
+
title
|
|
24
|
+
fields(first: 30) {
|
|
25
|
+
nodes {
|
|
26
|
+
... on ProjectV2FieldCommon { id name dataType }
|
|
27
|
+
... on ProjectV2SingleSelectField { id name dataType options { id name } }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_project(gh: GhRunner, title: str) -> dict:
|
|
37
|
+
viewer = gh.graphql("query { viewer { id login } }")["viewer"]
|
|
38
|
+
data = gh.graphql(
|
|
39
|
+
"""
|
|
40
|
+
mutation($owner: ID!, $title: String!) {
|
|
41
|
+
createProjectV2(input: {ownerId: $owner, title: $title}) {
|
|
42
|
+
projectV2 { id number title }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
""",
|
|
46
|
+
owner=viewer["id"], title=title)
|
|
47
|
+
return data["createProjectV2"]["projectV2"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_project(gh: GhRunner, owner: str, number: int) -> dict:
|
|
51
|
+
proj = gh.graphql(_FIELDS_QUERY, owner=owner, number=number)["user"]["projectV2"]
|
|
52
|
+
fields = {}
|
|
53
|
+
for node in proj["fields"]["nodes"]:
|
|
54
|
+
if not node:
|
|
55
|
+
continue
|
|
56
|
+
fields[node["name"]] = {
|
|
57
|
+
"id": node["id"],
|
|
58
|
+
"dataType": node.get("dataType"),
|
|
59
|
+
"options": {o["name"]: o["id"] for o in node.get("options", [])},
|
|
60
|
+
}
|
|
61
|
+
return {"id": proj["id"], "title": proj["title"], "fields": fields}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def ensure_fields(gh: GhRunner, owner: str, number: int) -> dict:
|
|
65
|
+
"""Create any missing bridge fields, then return the refreshed project."""
|
|
66
|
+
project = get_project(gh, owner, number)
|
|
67
|
+
for name, spec in FIELD_SPECS.items():
|
|
68
|
+
if name in project["fields"]:
|
|
69
|
+
continue
|
|
70
|
+
if spec["dataType"] == "SINGLE_SELECT":
|
|
71
|
+
opts = ", ".join(
|
|
72
|
+
f'{{name: "{o}", color: GRAY, description: ""}}'
|
|
73
|
+
for o in spec["options"])
|
|
74
|
+
gh.graphql(
|
|
75
|
+
"mutation($p: ID!, $n: String!) { createProjectV2Field(input: {"
|
|
76
|
+
"projectId: $p, dataType: SINGLE_SELECT, name: $n, "
|
|
77
|
+
f"singleSelectOptions: [{opts}]"
|
|
78
|
+
"}) { projectV2Field { ... on ProjectV2FieldCommon { id } } } }",
|
|
79
|
+
p=project["id"], n=name)
|
|
80
|
+
else:
|
|
81
|
+
gh.graphql(
|
|
82
|
+
"mutation($p: ID!, $n: String!) { createProjectV2Field(input: {"
|
|
83
|
+
"projectId: $p, dataType: TEXT, name: $n"
|
|
84
|
+
"}) { projectV2Field { ... on ProjectV2FieldCommon { id } } } }",
|
|
85
|
+
p=project["id"], n=name)
|
|
86
|
+
return get_project(gh, owner, number)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def add_item(gh: GhRunner, project_id: str, content_id: str) -> str:
|
|
90
|
+
data = gh.graphql(
|
|
91
|
+
"mutation($p: ID!, $c: ID!) { addProjectV2ItemById(input: {"
|
|
92
|
+
"projectId: $p, contentId: $c}) { item { id } } }",
|
|
93
|
+
p=project_id, c=content_id)
|
|
94
|
+
return data["addProjectV2ItemById"]["item"]["id"]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def set_text(gh: GhRunner, project_id: str, item_id: str, field_id: str,
|
|
98
|
+
value: str) -> None:
|
|
99
|
+
gh.graphql(
|
|
100
|
+
"mutation($p: ID!, $i: ID!, $f: ID!, $v: String!) {"
|
|
101
|
+
"updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, "
|
|
102
|
+
"fieldId: $f, value: {text: $v}}) { projectV2Item { id } } }",
|
|
103
|
+
p=project_id, i=item_id, f=field_id, v=value)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def set_option(gh: GhRunner, project_id: str, item_id: str, field_id: str,
|
|
107
|
+
option_id: str) -> None:
|
|
108
|
+
gh.graphql(
|
|
109
|
+
"mutation($p: ID!, $i: ID!, $f: ID!, $o: String!) {"
|
|
110
|
+
"updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, "
|
|
111
|
+
"fieldId: $f, value: {singleSelectOptionId: $o}}) "
|
|
112
|
+
"{ projectV2Item { id } } }",
|
|
113
|
+
p=project_id, i=item_id, f=field_id, o=option_id)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def sync_items(gh: GhRunner, project: dict, entries: list[dict]) -> int:
|
|
117
|
+
"""entries: [{node_id, doc_id, type ('Feature'|'Story'), pmo_project}]."""
|
|
118
|
+
f = project["fields"]
|
|
119
|
+
type_field, doc_field, pmo_field = f["Type"], f["Doc"], f["PMO Project"]
|
|
120
|
+
for e in entries:
|
|
121
|
+
item = add_item(gh, project["id"], e["node_id"])
|
|
122
|
+
set_option(gh, project["id"], item, type_field["id"],
|
|
123
|
+
type_field["options"][e["type"]])
|
|
124
|
+
set_text(gh, project["id"], item, doc_field["id"], e["doc_id"])
|
|
125
|
+
set_text(gh, project["id"], item, pmo_field["id"], e["pmo_project"])
|
|
126
|
+
return len(entries)
|
|
@@ -15,11 +15,22 @@ class GhError(RuntimeError):
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class GhRunner:
|
|
18
|
-
"""Executes gh commands. Tests substitute a fake with the same interface.
|
|
18
|
+
"""Executes gh commands. Tests substitute a fake with the same interface.
|
|
19
|
+
|
|
20
|
+
`token` overrides GH_TOKEN for this runner only, so board operations can
|
|
21
|
+
use the project-scoped credential while issue operations keep the
|
|
22
|
+
default one.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, token: str | None = None) -> None:
|
|
26
|
+
self._env: dict[str, str] | None = None
|
|
27
|
+
if token:
|
|
28
|
+
import os
|
|
29
|
+
self._env = dict(os.environ, GH_TOKEN=token)
|
|
19
30
|
|
|
20
31
|
def run(self, args: list[str], input_: str | None = None) -> str:
|
|
21
32
|
proc = subprocess.run(["gh", *args], capture_output=True, text=True,
|
|
22
|
-
input=input_)
|
|
33
|
+
input=input_, env=self._env)
|
|
23
34
|
if proc.returncode != 0:
|
|
24
35
|
raise GhError(proc.stderr.strip() or f"gh {' '.join(args)} failed")
|
|
25
36
|
return proc.stdout
|
|
@@ -44,7 +55,8 @@ class DryRunner(GhRunner):
|
|
|
44
55
|
|
|
45
56
|
MUTATING = ("-X POST", "-X PATCH", "-X PUT", "-X DELETE", "mutation")
|
|
46
57
|
|
|
47
|
-
def __init__(self) -> None:
|
|
58
|
+
def __init__(self, token: str | None = None) -> None:
|
|
59
|
+
super().__init__(token)
|
|
48
60
|
self.planned: list[str] = []
|
|
49
61
|
|
|
50
62
|
def run(self, args: list[str], input_: str | None = None) -> str:
|
|
@@ -46,8 +46,14 @@ def _index(issues: list[dict]) -> dict[str, dict]:
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def scaffold(plan: BridgePlan, gh: G.GhRunner, repo: str,
|
|
49
|
-
docs_url: str | None = None
|
|
50
|
-
|
|
49
|
+
docs_url: str | None = None,
|
|
50
|
+
board_cfg: dict | None = None) -> list[str]:
|
|
51
|
+
"""Make the repo's issues match the plan. Returns human-readable actions.
|
|
52
|
+
|
|
53
|
+
board_cfg (optional): {"gh": GhRunner with project scope, "owner": login,
|
|
54
|
+
"number": int, "init": bool} — mirrors every managed issue onto the
|
|
55
|
+
Projects v2 board with Type/Doc/PMO Project fields.
|
|
56
|
+
"""
|
|
51
57
|
actions: list[str] = []
|
|
52
58
|
for name, (color, desc) in LABELS.items():
|
|
53
59
|
G.ensure_label(gh, repo, name, color, desc)
|
|
@@ -80,6 +86,28 @@ def scaffold(plan: BridgePlan, gh: G.GhRunner, repo: str,
|
|
|
80
86
|
if pnode and cnode:
|
|
81
87
|
G.add_sub_issue(gh, pnode, cnode)
|
|
82
88
|
|
|
89
|
+
if board_cfg:
|
|
90
|
+
from . import board as B
|
|
91
|
+
bgh = board_cfg["gh"]
|
|
92
|
+
if board_cfg.get("init"):
|
|
93
|
+
project = B.ensure_fields(bgh, board_cfg["owner"], board_cfg["number"])
|
|
94
|
+
actions.append(f"board: fields ensured on '{project['title']}'")
|
|
95
|
+
else:
|
|
96
|
+
project = B.get_project(bgh, board_cfg["owner"], board_cfg["number"])
|
|
97
|
+
entries = []
|
|
98
|
+
for f in plan.features:
|
|
99
|
+
issue = by_key.get(f.id)
|
|
100
|
+
if issue and issue.get("node_id"):
|
|
101
|
+
entries.append({"node_id": issue["node_id"], "doc_id": f.id,
|
|
102
|
+
"type": "Feature", "pmo_project": f.project})
|
|
103
|
+
for s in f.stories:
|
|
104
|
+
sissue = by_key.get(s.id)
|
|
105
|
+
if sissue and sissue.get("node_id"):
|
|
106
|
+
entries.append({"node_id": sissue["node_id"], "doc_id": s.id,
|
|
107
|
+
"type": "Story", "pmo_project": f.project})
|
|
108
|
+
n = B.sync_items(bgh, project, entries)
|
|
109
|
+
actions.append(f"board: {n} item(s) synced to '{project['title']}'")
|
|
110
|
+
|
|
83
111
|
for sk in plan.skipped:
|
|
84
112
|
actions.append(f"skipped {sk['id']}: {sk['reason']}")
|
|
85
113
|
return actions
|
|
@@ -300,8 +300,31 @@ def cmd_bridge(args: argparse.Namespace) -> int:
|
|
|
300
300
|
plan = build_bridge_plan(args.documents_dir)
|
|
301
301
|
gh = ghmod.DryRunner() if getattr(args, "dry_run", False) else ghmod.GhRunner()
|
|
302
302
|
|
|
303
|
+
if args.action == "create-board":
|
|
304
|
+
from .bridge import board as board_mod
|
|
305
|
+
token = os.environ.get(args.board_token_env, "")
|
|
306
|
+
if not token and not getattr(args, "dry_run", False):
|
|
307
|
+
print(f"docassert bridge: no token in ${args.board_token_env}", file=sys.stderr)
|
|
308
|
+
return 2
|
|
309
|
+
bgh = gh if isinstance(gh, ghmod.DryRunner) else ghmod.GhRunner(token=token)
|
|
310
|
+
proj = board_mod.create_project(bgh, args.title or "PMO board")
|
|
311
|
+
print(f"docassert bridge: created project #{proj.get('number')} '{proj.get('title')}'")
|
|
312
|
+
return 0
|
|
313
|
+
|
|
314
|
+
board_cfg = None
|
|
315
|
+
if args.project_number:
|
|
316
|
+
token = os.environ.get(args.board_token_env, "")
|
|
317
|
+
if not token and not getattr(args, "dry_run", False):
|
|
318
|
+
print(f"docassert bridge: no token in ${args.board_token_env} "
|
|
319
|
+
"(needed for --project-number)", file=sys.stderr)
|
|
320
|
+
return 2
|
|
321
|
+
bgh = gh if isinstance(gh, ghmod.DryRunner) else ghmod.GhRunner(token=token)
|
|
322
|
+
board_cfg = {"gh": bgh, "owner": args.project_owner or args.repo.split("/")[0],
|
|
323
|
+
"number": args.project_number, "init": args.init_board}
|
|
324
|
+
|
|
303
325
|
if args.action == "scaffold":
|
|
304
|
-
actions = ops.scaffold(plan, gh, args.repo, docs_url=args.docs_url
|
|
326
|
+
actions = ops.scaffold(plan, gh, args.repo, docs_url=args.docs_url,
|
|
327
|
+
board_cfg=board_cfg)
|
|
305
328
|
for a in actions:
|
|
306
329
|
print(f"docassert bridge: {a}")
|
|
307
330
|
if isinstance(gh, ghmod.DryRunner):
|
|
@@ -405,13 +428,19 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
405
428
|
|
|
406
429
|
|
|
407
430
|
b = sub.add_parser("bridge", help="Execution bridge: scaffold and police the GitHub board from approved stories.")
|
|
408
|
-
b.add_argument("action", choices=["scaffold", "reconcile", "status"],
|
|
431
|
+
b.add_argument("action", choices=["scaffold", "reconcile", "status", "create-board"],
|
|
409
432
|
help="scaffold: docs -> issues/board · reconcile: police the board · status: delivery figures.")
|
|
410
433
|
b.add_argument("--repo", required=True, help="Target GitHub repo (OWNER/NAME) holding the issues.")
|
|
411
434
|
b.add_argument("--docs-url", help="Base URL of the documents repo, for source links in issue bodies.")
|
|
412
435
|
b.add_argument("--dry-run", action="store_true", help="Print planned mutations without executing them.")
|
|
413
436
|
b.add_argument("--json", action="store_true", help="status: emit JSON.")
|
|
414
437
|
b.add_argument("--out", help="status --json: also write to this path.")
|
|
438
|
+
b.add_argument("--title", help="create-board: the new project's title.")
|
|
439
|
+
b.add_argument("--project-owner", help="Board owner login (default: the repo owner).")
|
|
440
|
+
b.add_argument("--project-number", type=int, help="Projects v2 board number to sync items onto.")
|
|
441
|
+
b.add_argument("--init-board", action="store_true", help="Ensure the Type/Doc/PMO Project fields exist.")
|
|
442
|
+
b.add_argument("--board-token-env", default="PROJECTS_TOKEN",
|
|
443
|
+
help="Env var holding the project-scoped token (default: PROJECTS_TOKEN).")
|
|
415
444
|
docs_dir_opt(b)
|
|
416
445
|
b.set_defaults(func=cmd_bridge)
|
|
417
446
|
|
|
@@ -90,6 +90,7 @@ docassert/_data/templates/status-report.template.md
|
|
|
90
90
|
docassert/_data/templates/test-cases.template.md
|
|
91
91
|
docassert/_data/templates/user-story.template.md
|
|
92
92
|
docassert/bridge/__init__.py
|
|
93
|
+
docassert/bridge/board.py
|
|
93
94
|
docassert/bridge/gh.py
|
|
94
95
|
docassert/bridge/ops.py
|
|
95
96
|
docassert/bridge/plan.py
|
|
@@ -247,3 +247,76 @@ def test_add_sub_issue_still_raises_on_real_errors(tmp_path):
|
|
|
247
247
|
import pytest
|
|
248
248
|
with pytest.raises(GhError):
|
|
249
249
|
add_sub_issue(BoomGh(), "N1", "N2")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ── board ops ────────────────────────────────────────────────────────────────
|
|
253
|
+
class BoardGh(FakeGh):
|
|
254
|
+
"""GraphQL router for Projects v2 shapes."""
|
|
255
|
+
|
|
256
|
+
def __init__(self, existing_fields=()):
|
|
257
|
+
super().__init__()
|
|
258
|
+
self.gql: list[str] = []
|
|
259
|
+
self._fields = list(existing_fields)
|
|
260
|
+
|
|
261
|
+
def graphql(self, query, **vars_):
|
|
262
|
+
self.gql.append(query)
|
|
263
|
+
if "viewer" in query:
|
|
264
|
+
return {"viewer": {"id": "U1", "login": "tester"}}
|
|
265
|
+
if "createProjectV2(" in query:
|
|
266
|
+
return {"createProjectV2": {"projectV2": {"id": "P1", "number": 7,
|
|
267
|
+
"title": vars_.get("title")}}}
|
|
268
|
+
if "projectV2(number" in query:
|
|
269
|
+
nodes = [{"id": f"F-{n}", "name": n, "dataType": "TEXT"}
|
|
270
|
+
for n in self._fields]
|
|
271
|
+
if "Type" in self._fields:
|
|
272
|
+
for node in nodes:
|
|
273
|
+
if node["name"] == "Type":
|
|
274
|
+
node["dataType"] = "SINGLE_SELECT"
|
|
275
|
+
node["options"] = [{"id": "O1", "name": "Feature"},
|
|
276
|
+
{"id": "O2", "name": "Story"}]
|
|
277
|
+
return {"user": {"projectV2": {"id": "P1", "title": "Board",
|
|
278
|
+
"fields": {"nodes": nodes}}}}
|
|
279
|
+
if "createProjectV2Field" in query:
|
|
280
|
+
self._fields.append(vars_["n"])
|
|
281
|
+
return {"createProjectV2Field": {"projectV2Field": {"id": "F-new"}}}
|
|
282
|
+
if "addProjectV2ItemById" in query:
|
|
283
|
+
return {"addProjectV2ItemById": {"item": {"id": "I1"}}}
|
|
284
|
+
if "updateProjectV2ItemFieldValue" in query:
|
|
285
|
+
return {"updateProjectV2ItemFieldValue": {"projectV2Item": {"id": "I1"}}}
|
|
286
|
+
return {}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_create_project():
|
|
290
|
+
from docassert.bridge import board
|
|
291
|
+
gh = BoardGh()
|
|
292
|
+
proj = board.create_project(gh, "Refuge for Humans")
|
|
293
|
+
assert proj["number"] == 7 and proj["title"] == "Refuge for Humans"
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_ensure_fields_creates_missing():
|
|
297
|
+
from docassert.bridge import board
|
|
298
|
+
gh = BoardGh(existing_fields=["Status"])
|
|
299
|
+
project = board.ensure_fields(gh, "tester", 7)
|
|
300
|
+
assert set(project["fields"]) >= {"Type", "Doc", "PMO Project"}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def test_scaffold_syncs_board(tmp_path):
|
|
304
|
+
plan = build_bridge_plan(_tree(tmp_path))
|
|
305
|
+
existing = []
|
|
306
|
+
for i, f in enumerate(plan.features):
|
|
307
|
+
e = _mk_issue(200 + i, f.id, feature_title(f))
|
|
308
|
+
e["body"] = feature_body(f, None)
|
|
309
|
+
existing.append(e)
|
|
310
|
+
for j, s in enumerate(f.stories):
|
|
311
|
+
from docassert.bridge.plan import story_title
|
|
312
|
+
e2 = _mk_issue(300 + i * 10 + j, s.id, story_title(s))
|
|
313
|
+
e2["body"] = story_body(s, None)
|
|
314
|
+
existing.append(e2)
|
|
315
|
+
gh = FakeGh(existing)
|
|
316
|
+
bgh = BoardGh(existing_fields=["Type", "Doc", "PMO Project"])
|
|
317
|
+
actions = ops.scaffold(plan, gh, "o/r",
|
|
318
|
+
board_cfg={"gh": bgh, "owner": "tester", "number": 7,
|
|
319
|
+
"init": False})
|
|
320
|
+
assert any("5 item(s) synced" in a for a in actions)
|
|
321
|
+
adds = [q for q in bgh.gql if "addProjectV2ItemById" in q]
|
|
322
|
+
assert len(adds) == 5
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/benefits-realization.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/post-implementation-review.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/benefits-realization.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/data-migration-plan.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/post-implementation-review.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/release-cutover-plan.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|