feat(kvb-git): add pr-create, pr-label and issue-solve; rename close-issues to issue-close
kvb-git-pr-create opens a pull request for the current branch: it assesses the branch, hands the push to the operator (gipsn) and waits until git ls-remote shows the head, writes a body with one Closes line per verified issue, confirms once, and opens it ready at the kvb-org-split bar or as a draft below it, then runs kvb-git-pr-label. kvb-git-pr-label sets hotfix on a pull request that targets the default branch and fixes a failure there, and 90/5 once it meets the split bar, each with its evidence, creating the labels once and marking a draft ready when it gains 90/5. kvb-git-issue-solve solves open issues one sidetree each through kvb-org-split, skipping fixed ones and those a peer already works on, and opens one pull request per issue. kvb-git-close-issues becomes kvb-git-issue-close, following the kvb-git-<object>-<verb> scheme: with a pull request it works as before; without one it covers every open issue, and the new done.py lists the default-branch commits that reference each (one git log, foreign repositories excluded). tests/test_close_issues.py moves to tests/test_issue_close.py and gains TestDone on a temporary repository. Six skill descriptions are shortened, keeping their triggers, so the listing stays within its 1500-word budget (1517 to 1493). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
32f58712e5
commit
f815060584
16 files changed
+399
-134
No files matched your search
@@ -49,7 +49,10 @@ flowchart LR
|
||||
git --> git_commit_next[kvb-git-commit-next]
|
||||
git --> git_commit_no_verify[kvb-git-commit-no-verify]
|
||||
git --> git_merge_fix[kvb-git-merge-fix]
|
||||
git --> git_close_issues[kvb-git-close-issues]
|
||||
git --> git_issue_close[kvb-git-issue-close]
|
||||
git --> git_issue_solve[kvb-git-issue-solve]
|
||||
git --> git_pr_create[kvb-git-pr-create]
|
||||
git --> git_pr_label[kvb-git-pr-label]
|
||||
git --> git_update_branches[kvb-git-update-branches]
|
||||
git --> git_branch_gc[kvb-git-branch-gc]
|
||||
git --> git_changelog[kvb-git-changelog]
|
||||
@@ -128,7 +131,10 @@ flowchart LR
|
||||
commit_next[kvb-git-commit-next]
|
||||
commit_no_verify[kvb-git-commit-no-verify]
|
||||
merge_fix[kvb-git-merge-fix]
|
||||
close_issues[kvb-git-close-issues]
|
||||
issue_close[kvb-git-issue-close]
|
||||
issue_solve[kvb-git-issue-solve]
|
||||
pr_create[kvb-git-pr-create]
|
||||
pr_label[kvb-git-pr-label]
|
||||
update_branches[kvb-git-update-branches]
|
||||
branch_gc[kvb-git-branch-gc]
|
||||
changelog[kvb-git-changelog]
|
||||
@@ -186,7 +192,13 @@ flowchart LR
|
||||
commit_next --> commit_no_verify
|
||||
commit_next --> explain
|
||||
commit_next --> dialectic
|
||||
close_issues --> confidence
|
||||
issue_close --> confidence
|
||||
issue_solve --> issue_close
|
||||
issue_solve --> funnel
|
||||
issue_solve --> pr_create
|
||||
pr_create --> issue_close
|
||||
pr_create --> pr_label
|
||||
pr_create -. push .-> operator
|
||||
update_branches --> active_listening
|
||||
update_branches -. conflicts .-> merge_fix
|
||||
update_branches -. pushes .-> operator
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: kvb-check-security
|
||||
description: >
|
||||
Run the MEDUSA scanner (medusa-security) on a repository, working tree or
|
||||
secret stores. Use for security scans, vetting a repo, leaked secrets in
|
||||
chat or shell history, or /kvb-check-security. Defensive only.
|
||||
Run the MEDUSA scanner on a repository, working tree or secret stores:
|
||||
security scans, vetting a repo, leaked secrets in chat or shell history, or
|
||||
/kvb-check-security. Defensive only.
|
||||
---
|
||||
|
||||
MEDUSA is a defensive AI-first security scanner (40,000+ patterns: CVEs, prompt injection, MCP tool poisoning, leaked secrets).
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: kvb-cleanup
|
||||
description: >
|
||||
Free disk space agents filled: ask peers, inventory /tmp, Docker and agent
|
||||
dirs, and delete only what the operator picks. Trigger on /kvb-cleanup, "aufräumen", "bereinigen", "platte voll", "no
|
||||
space left on device".
|
||||
Free disk space agents filled in /tmp, Docker and agent dirs; delete only
|
||||
what the operator picks. Trigger on /kvb-cleanup, "aufräumen", "bereinigen",
|
||||
"platte voll", "no space left on device".
|
||||
---
|
||||
|
||||
Nothing is deleted before the operator has picked it. Every candidate carries
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: kvb-code-scope-guard
|
||||
description: >
|
||||
Deliver exactly what was asked and nothing more. Trigger on every task that
|
||||
creates files, commands or commits, on autonomous runs and hand-overs, and
|
||||
when asked why something unrequested exists.
|
||||
Deliver exactly what was asked. Trigger on every task that creates files,
|
||||
commands or commits, on autonomous runs and hand-overs, and when asked why
|
||||
something unrequested exists.
|
||||
---
|
||||
|
||||
The requested scope is the deliverable. A problem you noticed behind the
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: kvb-create-plan
|
||||
description: >
|
||||
Turn a request into an objective-oriented plan and hand it to the kvb-org-robot
|
||||
skill. Trigger on /kvb-create-plan or when the operator asks for a plan, breakdown or
|
||||
strategy before work starts.
|
||||
Turn a request into an objective-oriented plan for the kvb-org-robot skill.
|
||||
Trigger on /kvb-create-plan or a request for a plan, breakdown or strategy
|
||||
before work starts.
|
||||
---
|
||||
|
||||
Produce a plan whose every item is an outcome with a check, then execute it
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
---
|
||||
name: kvb-git-changelog
|
||||
description: >
|
||||
Write the release message for `pkgmgr release`;
|
||||
prints only, never edits, tags or releases. Trigger on
|
||||
/kvb-git-changelog, "release message", "changelog entry", before cutting a release,
|
||||
or on pkgmgr's markdown-lint rejection.
|
||||
Write the release message for `pkgmgr release`; prints only. Trigger on
|
||||
/kvb-git-changelog, "release message", "changelog entry", before cutting a
|
||||
release, or on pkgmgr's markdown-lint rejection.
|
||||
---
|
||||
|
||||
The message is one text that pkgmgr writes to four places: the CHANGELOG.md
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
name: kvb-git-close-issues
|
||||
description: >
|
||||
Close the issues a pull request fixes, each verified and confirmed.
|
||||
Trigger on /kvb-git-close-issues [pr].
|
||||
---
|
||||
|
||||
Closing an issue is outward-facing: nothing is closed or edited before the
|
||||
operator confirms the list, and every closed issue names the pull request
|
||||
and commit that fixed it.
|
||||
|
||||
## Step 1: candidates
|
||||
|
||||
The pull request named after the command, else the one of the current
|
||||
branch (`gh pr view --json number`). `${CLAUDE_SKILL_DIR}/scripts/issues.py`
|
||||
lists every issue it references, from closing keywords and mentions in the
|
||||
body and the commits, a trailing number in the branch name, and the issues
|
||||
GitHub already links:
|
||||
|
||||
```bash
|
||||
gh pr view <pr> --json number,body,headRefName,commits,closingIssuesReferences | issues.py --repo <owner/name>
|
||||
```
|
||||
|
||||
## Step 2: verify
|
||||
|
||||
Per candidate, `gh issue view <n> --json state,title,body,comments` (with
|
||||
`--repo` for a foreign one); a closed issue is skipped. Judge it `fixed`,
|
||||
`partly` or `unrelated` against the pull request's diff, with evidence: the
|
||||
change that addresses each point of the issue and the test or CI run that
|
||||
proves it. Only `fixed` goes on; `partly` gets a progress comment instead.
|
||||
|
||||
## Step 3: confirm
|
||||
|
||||
`AskUserQuestion` with `multiSelect: true`, one option per `fixed` issue,
|
||||
the evidence in its description, nothing preselected.
|
||||
|
||||
## Step 4: close
|
||||
|
||||
Where the fix lives decides how:
|
||||
|
||||
- **Merged into the default branch**: `gh issue close <n> --reason completed
|
||||
--comment "Fixed by #<pr> (<sha>)."`.
|
||||
- **Open, or merged into another branch**: add one `Closes #<n>` line per
|
||||
issue to the pull request body (`gh pr edit <pr> --body-file <file>`,
|
||||
keeping the existing body), so the merge into the default branch closes
|
||||
them; never close them early.
|
||||
|
||||
## Output
|
||||
|
||||
| Issue | Verdict | Evidence | Action |
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: kvb-git-issue-close
|
||||
description: >
|
||||
Close finished issues, of one pull request or the whole repository, each
|
||||
verified and confirmed. Trigger on /kvb-git-issue-close [pr].
|
||||
---
|
||||
|
||||
Closing an issue is outward-facing: nothing is closed or edited before the
|
||||
operator confirms the list, and every closed issue names the pull request or
|
||||
commit that fixed it.
|
||||
|
||||
## Step 1: candidates
|
||||
|
||||
- **A pull request named** (or the current branch's, `gh pr view --json
|
||||
number`): `${CLAUDE_SKILL_DIR}/scripts/issues.py` lists every issue it
|
||||
references, from closing keywords and mentions in the body and the commits,
|
||||
a trailing number in the branch name, and the issues GitHub already links:
|
||||
|
||||
```bash
|
||||
gh pr view <pr> --json number,body,headRefName,commits,closingIssuesReferences | issues.py --repo <owner/name>
|
||||
```
|
||||
|
||||
- **No pull request**: every open issue of the repository. After `git fetch`,
|
||||
`${CLAUDE_SKILL_DIR}/scripts/done.py` lists the default-branch commits that
|
||||
reference each one:
|
||||
|
||||
```bash
|
||||
gh issue list --state open --limit 1000 --json number | done.py --repo <owner/name> --git-dir . --ref origin/<default>
|
||||
```
|
||||
|
||||
An open issue no commit references can still be fixed: search the default
|
||||
branch by its description with the kvb-git-merge-fix skill's `find_fix.py`.
|
||||
|
||||
## Step 2: verify
|
||||
|
||||
Per candidate, `gh issue view <n> --json state,title,body,comments` (with
|
||||
`--repo` for a foreign one); a closed issue is skipped. Judge it `fixed`,
|
||||
`partly` or `unrelated` against the diff, with evidence: the change that
|
||||
addresses each point of the issue and the test or CI run that proves it.
|
||||
Only `fixed` goes on; `partly` gets a progress comment instead.
|
||||
|
||||
## Step 3: confirm
|
||||
|
||||
`AskUserQuestion` with `multiSelect: true`, one option per `fixed` issue,
|
||||
the evidence in its description, nothing preselected.
|
||||
|
||||
## Step 4: close
|
||||
|
||||
Where the fix lives decides how:
|
||||
|
||||
- **On the default branch**: `gh issue close <n> --reason completed
|
||||
--comment "Fixed by #<pr> (<sha>)."`, or the commit alone when no pull
|
||||
request carried it.
|
||||
- **In an open pull request, or merged into another branch**: add one
|
||||
`Closes #<n>` line per issue to the pull request body (`gh pr edit <pr>
|
||||
--body-file <file>`, keeping the existing body), so the merge into the
|
||||
default branch closes them; never close them early.
|
||||
|
||||
## Output
|
||||
|
||||
| Issue | Verdict | Evidence | Action |
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""List the default-branch commits that reference each open issue.
|
||||
|
||||
Args:
|
||||
stdin: JSON list of open issues, as `gh issue list --state open --json
|
||||
number` prints it.
|
||||
--repo: owner/name of the repository; its own references count, foreign
|
||||
ones do not.
|
||||
--git-dir: path of a local clone, fetched.
|
||||
--ref: the default branch to scan, e.g. origin/main.
|
||||
--json: print the result as JSON instead of tab-separated lines.
|
||||
|
||||
Output:
|
||||
one line per open issue that a commit references: issue, closing or
|
||||
mention, the short shas.
|
||||
|
||||
Exit codes:
|
||||
0 done, 2 usage error.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from issues import PATTERN, fail
|
||||
|
||||
|
||||
def references(log: str, own: str, open_issues: set[int]) -> dict[int, dict]:
|
||||
found: dict[int, dict] = {}
|
||||
for record in filter(None, log.split("\x1e")):
|
||||
sha, _, message = record.strip("\n").partition("\x1f")
|
||||
for match in PATTERN.finditer(message):
|
||||
repo = match.group("repo") or match.group("url") or ""
|
||||
number = int(match.group("num"))
|
||||
if number not in open_issues or (repo and repo.lower() != own.lower()):
|
||||
continue
|
||||
entry = found.setdefault(number, {"closing": False, "commits": []})
|
||||
entry["closing"] = entry["closing"] or bool(match.group("kw"))
|
||||
if sha[:10] not in entry["commits"]:
|
||||
entry["commits"].append(sha[:10])
|
||||
return found
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument("--repo", required=True)
|
||||
parser.add_argument("--git-dir", required=True)
|
||||
parser.add_argument("--ref", required=True)
|
||||
parser.add_argument("--json", action="store_true")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
issues = json.load(sys.stdin)
|
||||
open_issues = {int(issue["number"]) for issue in issues}
|
||||
except (ValueError, TypeError, KeyError):
|
||||
fail("stdin is not the JSON of gh issue list --json number")
|
||||
log = subprocess.run(
|
||||
["git", "log", "--format=%H%x1f%B%x1e", args.ref],
|
||||
cwd=args.git_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if log.returncode != 0:
|
||||
fail(log.stderr.strip() or f"git log {args.ref} failed")
|
||||
found = references(log.stdout, args.repo, open_issues)
|
||||
rows = [{"issue": number, **found[number]} for number in sorted(found)]
|
||||
if args.json:
|
||||
print(json.dumps(rows, indent=1))
|
||||
return 0
|
||||
for row in rows:
|
||||
kind = "closing" if row["closing"] else "mention"
|
||||
print(f"{row['issue']}\t{kind}\t{', '.join(row['commits'])}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
File renamed without changes.
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: kvb-git-issue-solve
|
||||
description: >
|
||||
Solve open issues, each in its own worktree, and open one pull request per
|
||||
issue. Trigger on /kvb-git-issue-solve [issue...].
|
||||
---
|
||||
|
||||
Every issue gets its own sidetree, its own evidence and its own pull
|
||||
request, so a red one blocks nothing else.
|
||||
|
||||
## Procedure
|
||||
|
||||
1. **Candidates**: the issues named, else `gh issue list --state open`. Drop
|
||||
what the `kvb-git-issue-close` skill finds already fixed, and what a peer
|
||||
already works on: `council.py list` and the `kvb-org-organize` Peer
|
||||
protocol; when a peer solves it, only comment on the issue who solves it
|
||||
where.
|
||||
2. **Rank**: issues about a failing default branch first, then the rank of
|
||||
the `kvb-org-wsjf` skill; the operator's order wins.
|
||||
3. **Solve** each with the `kvb-org-split` skill: claim `work/issue-<n>`,
|
||||
enter it in the `kvb-org-rasci` register (objective: the issue fixed; key
|
||||
results: a test that failed before and passes after, the pull request
|
||||
open), branch `fix/<scope>/<n>` from the default branch in its own
|
||||
worktree, evidence, self-assessment, independent review. Commits go to
|
||||
that branch only, never to the default branch; parallel work follows the
|
||||
resource chair's grants.
|
||||
4. **Open the pull request** with the `kvb-git-pr-create` skill, closing
|
||||
exactly its issue. Below the split bar the proposal becomes a draft pull
|
||||
request with the residuals and the evidence that would close them.
|
||||
5. **Report** per issue, then release its claim and tear its sidetree down
|
||||
as `kvb-org-split` describes; the branch stays until the merge.
|
||||
|
||||
## Output
|
||||
|
||||
| Issue | Branch | Confidence | Break risk | PR | State |
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: kvb-git-pr-create
|
||||
description: >
|
||||
Open a pull request for the current branch: push handed over, Closes lines,
|
||||
labels. Trigger on /kvb-git-pr-create.
|
||||
---
|
||||
|
||||
Opening a pull request is outward-facing and needs the branch on the remote,
|
||||
which only the operator pushes. Nothing is created before the operator
|
||||
confirms it.
|
||||
|
||||
## Procedure
|
||||
|
||||
1. **Check**: the branch is committed and clean, branched from the default
|
||||
branch (`gh repo view --json defaultBranchRef`), and has no open pull
|
||||
request yet (`gh pr list --head <branch>`).
|
||||
2. **Assess** the branch with the `kvb-check-confidence` skill: fix
|
||||
confidence, break risk, residuals.
|
||||
3. **Push**: hand the operator `gipsn` in the worktree through the
|
||||
`kvb-org-operator` skill, and continue once `git ls-remote --heads origin
|
||||
<branch>` shows the branch's head commit.
|
||||
4. **Body**, written to a scratch file: what changed and why, the test
|
||||
evidence, the confidence block, and one `Closes #<n>` line per issue the
|
||||
branch fixes, each verified as the `kvb-git-issue-close` skill does.
|
||||
5. **Confirm** with one `AskUserQuestion`: title (Conventional Commits),
|
||||
base, state and labels. The state follows the bar of the `kvb-org-split`
|
||||
skill: ready at or above it, a draft with the residuals in the body below
|
||||
it.
|
||||
6. **Create**: `gh pr create --base <default> --head <branch> --title
|
||||
"<title>" --body-file <file>`, plus `--draft` below the bar.
|
||||
7. **Label** with the `kvb-git-pr-label` skill.
|
||||
|
||||
## Output
|
||||
|
||||
| PR | Base | State | Labels | Closes |
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: kvb-git-pr-label
|
||||
description: >
|
||||
Label pull requests `hotfix` and `90/5` from evidence. Trigger on
|
||||
/kvb-git-pr-label [pr]; kvb-git-pr-create runs it.
|
||||
---
|
||||
|
||||
Two labels, each set only with its evidence and removed when it no longer
|
||||
holds:
|
||||
|
||||
- **`hotfix`**: the pull request targets the default branch and fixes a
|
||||
failure on it, a red default-branch run or an issue that reproduces there;
|
||||
name the run or issue.
|
||||
- **`90/5`**: the pull request meets the bar of the `kvb-org-split` skill (fix
|
||||
confidence of at least 90 %, break risk under 5 %), from the
|
||||
`kvb-check-confidence` numbers in its body or freshly assessed.
|
||||
|
||||
## Procedure
|
||||
|
||||
1. Target: the pull request named, else the current branch's (`gh pr view
|
||||
--json number,baseRefName,headRefName,isDraft,labels,body`); the default
|
||||
branch from `gh repo view --json defaultBranchRef`.
|
||||
2. Judge both labels with their evidence.
|
||||
3. A missing label is created once: `gh label create hotfix --color B60205
|
||||
--description "Fixes a failure on the default branch"`, `gh label create
|
||||
90/5 --color 0E8A16 --description "Fix confidence >= 90 %, break risk < 5 %"`.
|
||||
4. Standalone, confirm the changes with one `AskUserQuestion`; from
|
||||
`kvb-git-pr-create`, its confirmation covers them.
|
||||
5. `gh pr edit <pr> --add-label <l>` or `--remove-label <l>`. A draft that
|
||||
gains `90/5` also gets `gh pr ready <pr>`.
|
||||
|
||||
## Output
|
||||
|
||||
| PR | Label | Change | Evidence |
|
||||
@@ -2,8 +2,8 @@
|
||||
name: kvb-org-split
|
||||
description: >
|
||||
Keep agent work in its own worktree until 90% fix confidence and under 5%
|
||||
break risk; below that it becomes a proposal. Trigger when delegating to or
|
||||
parallelizing agents, or on /kvb-org-split.
|
||||
break risk, else a proposal. Trigger when delegating or parallelizing, or on
|
||||
/kvb-org-split.
|
||||
---
|
||||
|
||||
# Funnel
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
"""Exercise issues.py: closing keywords, mentions, commits, branch number and linked issues."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = REPO_ROOT / "skills" / "kvb-git-close-issues" / "scripts" / "issues.py"
|
||||
PR = {
|
||||
"number": 40,
|
||||
"body": (
|
||||
"Fixes #12 and touches #13.\nCloses: other/repo#7\nSee https://github.com/a/b/issues/9\n"
|
||||
"Rows WordPress#0 and docs#1 are not issues; this is #40."
|
||||
),
|
||||
"headRefName": "fix/sys-ctl-hlth-runtime/vulnerability-engines/1324",
|
||||
"commits": [
|
||||
{"oid": "abcdef1234567", "messageHeadline": "fix: guard", "messageBody": "Resolved #13"},
|
||||
{"oid": "1111111111111", "messageHeadline": "chore: tidy", "messageBody": ""},
|
||||
],
|
||||
"closingIssuesReferences": [
|
||||
{"number": 21, "url": "https://github.com/owner/repo/issues/21"}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _run(payload: str) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "--repo", "Owner/repo", "--json"],
|
||||
input=payload,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
class TestIssues(unittest.TestCase):
|
||||
def test_every_reference_is_found_with_its_kind_and_source(self):
|
||||
result = _run(json.dumps(PR))
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
rows = {(r["repo"], r["issue"]): r for r in json.loads(result.stdout)}
|
||||
self.assertEqual(
|
||||
{key: (row["closing"], row["sources"]) for key, row in rows.items()},
|
||||
{
|
||||
("", 12): (True, ["body"]),
|
||||
("", 13): (True, ["body", "commit abcdef1234"]),
|
||||
("other/repo", 7): (True, ["body"]),
|
||||
("a/b", 9): (False, ["body"]),
|
||||
("", 1324): (False, ["branch"]),
|
||||
("", 21): (True, ["linked"]),
|
||||
},
|
||||
)
|
||||
|
||||
def test_invalid_input_exits_2(self):
|
||||
self.assertEqual(_run("not json").returncode, 2)
|
||||
self.assertEqual(_run("[]").returncode, 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Exercise issues.py (one pull request's issues) and done.py (open issues the default branch references)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = REPO_ROOT / "skills" / "kvb-git-issue-close" / "scripts" / "issues.py"
|
||||
DONE = SCRIPT.with_name("done.py")
|
||||
PR = {
|
||||
"number": 40,
|
||||
"body": (
|
||||
"Fixes #12 and touches #13.\nCloses: other/repo#7\nSee https://github.com/a/b/issues/9\n"
|
||||
"Rows WordPress#0 and docs#1 are not issues; this is #40."
|
||||
),
|
||||
"headRefName": "fix/sys-ctl-hlth-runtime/vulnerability-engines/1324",
|
||||
"commits": [
|
||||
{"oid": "abcdef1234567", "messageHeadline": "fix: guard", "messageBody": "Resolved #13"},
|
||||
{"oid": "1111111111111", "messageHeadline": "chore: tidy", "messageBody": ""},
|
||||
],
|
||||
"closingIssuesReferences": [
|
||||
{"number": 21, "url": "https://github.com/owner/repo/issues/21"}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _run(payload: str) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "--repo", "Owner/repo", "--json"],
|
||||
input=payload,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
class TestIssues(unittest.TestCase):
|
||||
def test_every_reference_is_found_with_its_kind_and_source(self):
|
||||
result = _run(json.dumps(PR))
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
rows = {(r["repo"], r["issue"]): r for r in json.loads(result.stdout)}
|
||||
self.assertEqual(
|
||||
{key: (row["closing"], row["sources"]) for key, row in rows.items()},
|
||||
{
|
||||
("", 12): (True, ["body"]),
|
||||
("", 13): (True, ["body", "commit abcdef1234"]),
|
||||
("other/repo", 7): (True, ["body"]),
|
||||
("a/b", 9): (False, ["body"]),
|
||||
("", 1324): (False, ["branch"]),
|
||||
("", 21): (True, ["linked"]),
|
||||
},
|
||||
)
|
||||
|
||||
def test_invalid_input_exits_2(self):
|
||||
self.assertEqual(_run("not json").returncode, 2)
|
||||
self.assertEqual(_run("[]").returncode, 2)
|
||||
|
||||
|
||||
class TestDone(unittest.TestCase):
|
||||
def setUp(self):
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmp.cleanup)
|
||||
self.repo = Path(tmp.name)
|
||||
self.env = {
|
||||
"PATH": "/usr/bin:/bin",
|
||||
"HOME": tmp.name,
|
||||
"GIT_CONFIG_NOSYSTEM": "1",
|
||||
"GIT_AUTHOR_NAME": "t",
|
||||
"GIT_AUTHOR_EMAIL": "t@example.org",
|
||||
"GIT_COMMITTER_NAME": "t",
|
||||
"GIT_COMMITTER_EMAIL": "t@example.org",
|
||||
}
|
||||
self._git("init", "-q", "-b", "main")
|
||||
for message in (
|
||||
"fix: guard\n\nFixes #3",
|
||||
"docs: mention #4 and other/repo#6",
|
||||
"fix: port\n\nCloses Owner/repo#5",
|
||||
"chore: tidy #3",
|
||||
):
|
||||
self._git("commit", "-q", "--allow-empty", "-m", message)
|
||||
self.shas = self._git("log", "--format=%H", "main").split()
|
||||
|
||||
def _git(self, *args: str) -> str:
|
||||
return subprocess.run(
|
||||
["git", *args], cwd=self.repo, env=self.env, capture_output=True, text=True, check=True
|
||||
).stdout
|
||||
|
||||
def _run(self, payload: str, ref: str = "main") -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
[sys.executable, str(DONE), "--repo", "owner/Repo", "--git-dir", str(self.repo), "--ref", ref, "--json"],
|
||||
input=payload,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_open_issues_with_their_commits(self):
|
||||
result = self._run(json.dumps([{"number": n} for n in (3, 4, 5, 6, 7)]))
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
newest, closes5, mentions4, fixes3 = (sha[:10] for sha in self.shas)
|
||||
self.assertEqual(
|
||||
json.loads(result.stdout),
|
||||
[
|
||||
{"issue": 3, "closing": True, "commits": [newest, fixes3]},
|
||||
{"issue": 4, "closing": False, "commits": [mentions4]},
|
||||
{"issue": 5, "closing": True, "commits": [closes5]},
|
||||
],
|
||||
)
|
||||
|
||||
def test_bad_input_or_ref_exits_2(self):
|
||||
self.assertEqual(self._run("not json").returncode, 2)
|
||||
self.assertEqual(self._run('[{"id": 1}]').returncode, 2)
|
||||
self.assertEqual(self._run("[]", ref="nope").returncode, 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user