whycode-cli 0.2.4__tar.gz → 0.2.5__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.
- {whycode_cli-0.2.4/src/whycode_cli.egg-info → whycode_cli-0.2.5}/PKG-INFO +1 -1
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/pyproject.toml +1 -1
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/src/whycode/__init__.py +1 -1
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/src/whycode/cli.py +39 -0
- whycode_cli-0.2.5/src/whycode/templates/github-workflow.yml +64 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5/src/whycode_cli.egg-info}/PKG-INFO +1 -1
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/tests/test_cli.py +33 -0
- whycode_cli-0.2.4/src/whycode/templates/github-workflow.yml +0 -40
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/LICENSE +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/README.md +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/setup.cfg +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/src/whycode/__main__.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/src/whycode/git_facts.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/src/whycode/ignore.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/src/whycode/mcp_server.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/src/whycode/risk_card.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/src/whycode/scorer.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/src/whycode/signals.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/src/whycode/suppressions.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/src/whycode/templates/__init__.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/src/whycode/templates/pre-commit +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/src/whycode_cli.egg-info/SOURCES.txt +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/src/whycode_cli.egg-info/dependency_links.txt +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/src/whycode_cli.egg-info/entry_points.txt +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/src/whycode_cli.egg-info/requires.txt +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/src/whycode_cli.egg-info/top_level.txt +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/tests/test_git_facts.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/tests/test_ignore.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/tests/test_scorer.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/tests/test_signals.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.5}/tests/test_suppressions.py +0 -0
|
@@ -238,6 +238,14 @@ def diff(
|
|
|
238
238
|
json_out: bool = typer.Option(
|
|
239
239
|
False, "--json", help="Emit machine-readable JSON instead of a table."
|
|
240
240
|
),
|
|
241
|
+
markdown: bool = typer.Option(
|
|
242
|
+
False,
|
|
243
|
+
"--markdown",
|
|
244
|
+
help=(
|
|
245
|
+
"Emit GitHub-flavoured markdown suitable for posting as a PR comment. "
|
|
246
|
+
"Pipe into a workflow step that calls `gh pr comment`."
|
|
247
|
+
),
|
|
248
|
+
),
|
|
241
249
|
fail_on: str | None = typer.Option(
|
|
242
250
|
None,
|
|
243
251
|
"--fail-on",
|
|
@@ -305,6 +313,37 @@ def diff(
|
|
|
305
313
|
|
|
306
314
|
flagged = [c for c in cards if _is_actionable(c)]
|
|
307
315
|
quiet_n = len(cards) - len(flagged)
|
|
316
|
+
scope_md = "files staged for commit" if staged else f"files changed vs `{actual_base}`"
|
|
317
|
+
if markdown:
|
|
318
|
+
# Stable marker so a follow-up workflow step can find-and-update the
|
|
319
|
+
# same comment on subsequent pushes instead of stacking new ones.
|
|
320
|
+
print("<!-- whycode-comment -->")
|
|
321
|
+
print("## WhyCode risk briefing")
|
|
322
|
+
print()
|
|
323
|
+
print(f"**{len(files)} {scope_md}**")
|
|
324
|
+
print()
|
|
325
|
+
if not flagged:
|
|
326
|
+
print("Nothing flagged. Read the diff anyway.")
|
|
327
|
+
else:
|
|
328
|
+
print("| Score | Band | File | Top signal |")
|
|
329
|
+
print("| ----: | ---- | ---- | ---------- |")
|
|
330
|
+
for c in flagged:
|
|
331
|
+
top_signal = c.signals[0].headline.replace("|", "\\|")
|
|
332
|
+
print(
|
|
333
|
+
f"| {c.score.value} | {c.score.band.value} | "
|
|
334
|
+
f"`{c.path}` | {top_signal} |"
|
|
335
|
+
)
|
|
336
|
+
if quiet_n:
|
|
337
|
+
print()
|
|
338
|
+
print(f"_+ {quiet_n} file(s) changed with no flags._")
|
|
339
|
+
print()
|
|
340
|
+
print(
|
|
341
|
+
"_Run `whycode why <path>` for the full Risk Card on any of the above._"
|
|
342
|
+
)
|
|
343
|
+
if threshold is not None and any(c.score.value >= threshold for c in cards):
|
|
344
|
+
raise typer.Exit(1)
|
|
345
|
+
return
|
|
346
|
+
|
|
308
347
|
scope = "staged for commit" if staged else f"changed vs {actual_base}"
|
|
309
348
|
console.print(f"[bold]{len(files)} file(s) {scope}[/bold]")
|
|
310
349
|
if not flagged:
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Risk-rank pull requests with WhyCode.
|
|
2
|
+
#
|
|
3
|
+
# On every PR this workflow:
|
|
4
|
+
# 1. Computes the Risk Card for each changed file (vs the PR base)
|
|
5
|
+
# 2. Prints a risk-ranked table to the job log
|
|
6
|
+
# 3. Posts (or updates) a single PR comment with the same table
|
|
7
|
+
#
|
|
8
|
+
# Advisory by default — humans decide. To turn it into a hard gate that
|
|
9
|
+
# blocks merging, append `--fail-on <band>` to the diff line:
|
|
10
|
+
# handle block at HANDLE WITH CARE (score >= 75)
|
|
11
|
+
# history block at READ HISTORY FIRST (score >= 50)
|
|
12
|
+
# look stricter — block at >= 25
|
|
13
|
+
name: WhyCode
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
pull_request:
|
|
17
|
+
types: [opened, synchronize, reopened]
|
|
18
|
+
|
|
19
|
+
permissions:
|
|
20
|
+
contents: read
|
|
21
|
+
pull-requests: write # required to post / update the PR comment
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
whycode:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- name: Check out PR
|
|
28
|
+
uses: actions/checkout@v4
|
|
29
|
+
with:
|
|
30
|
+
fetch-depth: 0 # WhyCode needs full history
|
|
31
|
+
|
|
32
|
+
- name: Set up Python
|
|
33
|
+
uses: actions/setup-python@v5
|
|
34
|
+
with:
|
|
35
|
+
python-version: "3.11"
|
|
36
|
+
|
|
37
|
+
- name: Install WhyCode
|
|
38
|
+
run: pip install whycode-cli
|
|
39
|
+
|
|
40
|
+
- name: Risk-rank files in this PR (job log)
|
|
41
|
+
run: whycode diff --base origin/${{ github.base_ref }}
|
|
42
|
+
|
|
43
|
+
- name: Build PR comment body
|
|
44
|
+
run: whycode diff --base origin/${{ github.base_ref }} --markdown > whycode-comment.md
|
|
45
|
+
|
|
46
|
+
- name: Post or update the PR comment
|
|
47
|
+
env:
|
|
48
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
49
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
50
|
+
REPO: ${{ github.repository }}
|
|
51
|
+
run: |
|
|
52
|
+
set -euo pipefail
|
|
53
|
+
BODY=$(cat whycode-comment.md)
|
|
54
|
+
# Find any existing comment we previously posted (identified by the
|
|
55
|
+
# hidden HTML marker WhyCode emits at the top of the message).
|
|
56
|
+
EXISTING=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
|
|
57
|
+
--jq 'map(select(.body | contains("<!-- whycode-comment -->"))) | .[0].id // empty')
|
|
58
|
+
if [ -n "$EXISTING" ]; then
|
|
59
|
+
gh api -X PATCH "repos/${REPO}/issues/comments/${EXISTING}" \
|
|
60
|
+
-f body="$BODY" > /dev/null
|
|
61
|
+
else
|
|
62
|
+
gh api -X POST "repos/${REPO}/issues/${PR_NUMBER}/comments" \
|
|
63
|
+
-f body="$BODY" > /dev/null
|
|
64
|
+
fi
|
|
@@ -508,6 +508,39 @@ def test_scan_no_ignore_brings_them_back(repo, days_ago) -> None: # type: ignor
|
|
|
508
508
|
assert "CHANGELOG" in permissive_run.output or "src/app.py" in permissive_run.output
|
|
509
509
|
|
|
510
510
|
|
|
511
|
+
def test_diff_markdown_output(repo, days_ago) -> None: # type: ignore[no-untyped-def]
|
|
512
|
+
repo.commit("init", {"refund.py": "0"}, when=days_ago(120))
|
|
513
|
+
sha = repo.commit("feat: refund flow", {"refund.py": "1"}, when=days_ago(60))
|
|
514
|
+
repo.revert(sha, when=days_ago(50))
|
|
515
|
+
repo.commit(
|
|
516
|
+
"hotfix: regression",
|
|
517
|
+
{"refund.py": "2"},
|
|
518
|
+
body="incident #INC-42",
|
|
519
|
+
when=days_ago(10),
|
|
520
|
+
)
|
|
521
|
+
result = _invoke(repo.root, "diff", "--base", "HEAD~3", "--markdown")
|
|
522
|
+
assert result.exit_code == 0
|
|
523
|
+
out = result.output
|
|
524
|
+
# Hidden marker so the workflow can find-and-update its prior comment.
|
|
525
|
+
assert "<!-- whycode-comment -->" in out
|
|
526
|
+
# Markdown table syntax.
|
|
527
|
+
assert "| Score | Band |" in out
|
|
528
|
+
assert "| ----: |" in out
|
|
529
|
+
# File path appears as inline code.
|
|
530
|
+
assert "`refund.py`" in out
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def test_diff_markdown_quiet_repo(repo, days_ago) -> None: # type: ignore[no-untyped-def]
|
|
534
|
+
repo.commit("init", {"a.py": "1"}, when=days_ago(40))
|
|
535
|
+
repo.commit("docs: tweak", {"a.py": "2"}, when=days_ago(20))
|
|
536
|
+
result = _invoke(repo.root, "diff", "--base", "HEAD~1", "--markdown")
|
|
537
|
+
assert result.exit_code == 0
|
|
538
|
+
out = result.output
|
|
539
|
+
assert "<!-- whycode-comment -->" in out
|
|
540
|
+
# No flagged files → friendly note instead of an empty table.
|
|
541
|
+
assert "Nothing flagged" in out
|
|
542
|
+
|
|
543
|
+
|
|
511
544
|
def test_scan_respects_user_whycodeignore(repo, days_ago) -> None: # type: ignore[no-untyped-def]
|
|
512
545
|
(repo.root / ".whycodeignore").write_text("internal/legacy.py\n")
|
|
513
546
|
sha = repo.commit(
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# Risk-rank pull requests with WhyCode.
|
|
2
|
-
#
|
|
3
|
-
# On every PR this workflow computes the Risk Card for each changed file
|
|
4
|
-
# (vs the PR base) and prints a risk-ranked table to the job log.
|
|
5
|
-
# Advisory by default — humans decide.
|
|
6
|
-
#
|
|
7
|
-
# To turn it into a hard gate that blocks merging, append `--fail-on <band>`
|
|
8
|
-
# to the `whycode diff` line below:
|
|
9
|
-
# handle block at HANDLE WITH CARE (score >= 75)
|
|
10
|
-
# history block at READ HISTORY FIRST (score >= 50)
|
|
11
|
-
# look stricter — block at >= 25
|
|
12
|
-
name: WhyCode
|
|
13
|
-
|
|
14
|
-
on:
|
|
15
|
-
pull_request:
|
|
16
|
-
types: [opened, synchronize, reopened]
|
|
17
|
-
|
|
18
|
-
permissions:
|
|
19
|
-
contents: read
|
|
20
|
-
pull-requests: read
|
|
21
|
-
|
|
22
|
-
jobs:
|
|
23
|
-
whycode:
|
|
24
|
-
runs-on: ubuntu-latest
|
|
25
|
-
steps:
|
|
26
|
-
- name: Check out PR
|
|
27
|
-
uses: actions/checkout@v4
|
|
28
|
-
with:
|
|
29
|
-
fetch-depth: 0 # WhyCode needs full history
|
|
30
|
-
|
|
31
|
-
- name: Set up Python
|
|
32
|
-
uses: actions/setup-python@v5
|
|
33
|
-
with:
|
|
34
|
-
python-version: "3.11"
|
|
35
|
-
|
|
36
|
-
- name: Install WhyCode
|
|
37
|
-
run: pip install whycode-cli
|
|
38
|
-
|
|
39
|
-
- name: Risk-rank files in this PR
|
|
40
|
-
run: whycode diff --base origin/${{ github.base_ref }}
|
|
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
|