whycode-cli 0.2.3__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.
Files changed (31) hide show
  1. {whycode_cli-0.2.3/src/whycode_cli.egg-info → whycode_cli-0.2.5}/PKG-INFO +1 -1
  2. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/pyproject.toml +1 -1
  3. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/src/whycode/__init__.py +1 -1
  4. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/src/whycode/cli.py +77 -45
  5. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/src/whycode/git_facts.py +50 -2
  6. whycode_cli-0.2.5/src/whycode/templates/github-workflow.yml +64 -0
  7. {whycode_cli-0.2.3 → whycode_cli-0.2.5/src/whycode_cli.egg-info}/PKG-INFO +1 -1
  8. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/tests/test_cli.py +33 -0
  9. whycode_cli-0.2.3/src/whycode/templates/github-workflow.yml +0 -40
  10. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/LICENSE +0 -0
  11. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/README.md +0 -0
  12. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/setup.cfg +0 -0
  13. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/src/whycode/__main__.py +0 -0
  14. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/src/whycode/ignore.py +0 -0
  15. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/src/whycode/mcp_server.py +0 -0
  16. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/src/whycode/risk_card.py +0 -0
  17. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/src/whycode/scorer.py +0 -0
  18. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/src/whycode/signals.py +0 -0
  19. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/src/whycode/suppressions.py +0 -0
  20. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/src/whycode/templates/__init__.py +0 -0
  21. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/src/whycode/templates/pre-commit +0 -0
  22. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/src/whycode_cli.egg-info/SOURCES.txt +0 -0
  23. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/src/whycode_cli.egg-info/dependency_links.txt +0 -0
  24. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/src/whycode_cli.egg-info/entry_points.txt +0 -0
  25. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/src/whycode_cli.egg-info/requires.txt +0 -0
  26. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/src/whycode_cli.egg-info/top_level.txt +0 -0
  27. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/tests/test_git_facts.py +0 -0
  28. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/tests/test_ignore.py +0 -0
  29. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/tests/test_scorer.py +0 -0
  30. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/tests/test_signals.py +0 -0
  31. {whycode_cli-0.2.3 → whycode_cli-0.2.5}/tests/test_suppressions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: whycode-cli
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: Tells you what to be afraid of before you touch a file.
5
5
  Author: Kevin
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "whycode-cli"
7
- version = "0.2.3"
7
+ version = "0.2.5"
8
8
  description = "Tells you what to be afraid of before you touch a file."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,3 +1,3 @@
1
1
  """WhyCode — tells you what to be afraid of before touching a file."""
2
2
 
3
- __version__ = "0.2.3"
3
+ __version__ = "0.2.5"
@@ -71,12 +71,29 @@ def _path_is_known_to_git(repo_root: Path, rel: str) -> bool:
71
71
  if gf.is_tracked(repo_root, rel):
72
72
  return True
73
73
  try:
74
- out = gf._run_git(repo_root, "log", "--oneline", "-1", "--all", "--", rel)
74
+ out = gf.run_git(repo_root, "log", "--oneline", "-1", "--all", "--", rel)
75
75
  except gf.GitError:
76
76
  return False
77
77
  return bool(out.strip())
78
78
 
79
79
 
80
+ def _require_tracked(path_arg: str) -> tuple[Path, str]:
81
+ """Resolve ``path_arg`` to ``(repo_root, rel)`` or exit with a friendly warning.
82
+
83
+ Used by every command that takes a path argument and needs git history
84
+ to be useful (``why``, ``timeline``, ``honest``). Combines the two earlier
85
+ helpers so callers don't repeat the warn-and-exit boilerplate.
86
+ """
87
+ repo_root, rel = _resolve_repo_and_path(path_arg)
88
+ if not _path_is_known_to_git(repo_root, rel):
89
+ err.print(
90
+ f"[yellow]warning:[/yellow] [bold]{rel}[/bold] is not tracked by git "
91
+ f"and has no history in this repo. Nothing to learn from."
92
+ )
93
+ raise typer.Exit(1)
94
+ return repo_root, rel
95
+
96
+
80
97
  # --- shared: band threshold parsing ----------------------------------------
81
98
 
82
99
  _BAND_THRESHOLDS_BY_KEY: dict[str, int] = {
@@ -142,17 +159,11 @@ def why(
142
159
  ),
143
160
  ) -> None:
144
161
  """Print the Risk Card for ``path``."""
145
- repo_root, rel = _resolve_repo_and_path(path)
146
- if not _path_is_known_to_git(repo_root, rel):
147
- err.print(
148
- f"[yellow]warning:[/yellow] [bold]{rel}[/bold] is not tracked by git "
149
- f"and has no history in this repo. Nothing to learn from."
150
- )
151
- raise typer.Exit(1)
162
+ repo_root, rel = _require_tracked(path)
152
163
  resolved_ref: str | None = None
153
164
  if at is not None:
154
165
  try:
155
- resolved_ref = gf._run_git(
166
+ resolved_ref = gf.run_git(
156
167
  repo_root, "rev-parse", "--verify", f"{at}^{{commit}}"
157
168
  ).strip()
158
169
  except gf.GitError:
@@ -202,7 +213,7 @@ def _resolve_base_ref(repo_root: Path, requested: str | None) -> str:
202
213
  candidates = ("origin/main", "origin/master", "main", "master", "HEAD~1")
203
214
  for ref in candidates:
204
215
  try:
205
- gf._run_git(repo_root, "rev-parse", "--verify", "--quiet", f"{ref}^{{commit}}")
216
+ gf.run_git(repo_root, "rev-parse", "--verify", "--quiet", f"{ref}^{{commit}}")
206
217
  return ref
207
218
  except gf.GitError:
208
219
  continue
@@ -227,6 +238,14 @@ def diff(
227
238
  json_out: bool = typer.Option(
228
239
  False, "--json", help="Emit machine-readable JSON instead of a table."
229
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
+ ),
230
249
  fail_on: str | None = typer.Option(
231
250
  None,
232
251
  "--fail-on",
@@ -241,13 +260,13 @@ def diff(
241
260
  try:
242
261
  repo_root = gf.discover_repo_root(repo.resolve())
243
262
  if staged:
244
- raw = gf._run_git(
263
+ raw = gf.run_git(
245
264
  repo_root, "diff", "--cached", "--name-only", "--diff-filter=ACMR"
246
265
  )
247
266
  actual_base = "(staged changes)"
248
267
  else:
249
268
  actual_base = _resolve_base_ref(repo_root, base)
250
- raw = gf._run_git(repo_root, "diff", "--name-only", f"{actual_base}...HEAD")
269
+ raw = gf.run_git(repo_root, "diff", "--name-only", f"{actual_base}...HEAD")
251
270
  except gf.GitError as exc:
252
271
  err.print(f"[red]error:[/red] {exc}")
253
272
  raise typer.Exit(2) from exc
@@ -294,6 +313,37 @@ def diff(
294
313
 
295
314
  flagged = [c for c in cards if _is_actionable(c)]
296
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
+
297
347
  scope = "staged for commit" if staged else f"changed vs {actual_base}"
298
348
  console.print(f"[bold]{len(files)} file(s) {scope}[/bold]")
299
349
  if not flagged:
@@ -483,13 +533,7 @@ def timeline(
483
533
  ),
484
534
  ) -> None:
485
535
  """Show how this file's risk score evolved over its history."""
486
- repo_root, rel = _resolve_repo_and_path(path)
487
- if not _path_is_known_to_git(repo_root, rel):
488
- err.print(
489
- f"[yellow]warning:[/yellow] [bold]{rel}[/bold] is not tracked by git "
490
- f"and has no history in this repo."
491
- )
492
- raise typer.Exit(1)
536
+ repo_root, rel = _require_tracked(path)
493
537
 
494
538
  commits = gf.commits_for_path(repo_root, rel)
495
539
  if not commits:
@@ -588,7 +632,7 @@ def scan(
588
632
  err.print(f"[red]error:[/red] {exc}")
589
633
  raise typer.Exit(2) from exc
590
634
 
591
- raw = gf._run_git(repo_root, "ls-files")
635
+ raw = gf.run_git(repo_root, "ls-files")
592
636
  all_paths = [line for line in raw.splitlines() if line.strip()]
593
637
  patterns = () if no_ignore else ign.effective_patterns(repo_root)
594
638
  paths = [p for p in all_paths if not ign.is_ignored(p, patterns)][:sample]
@@ -648,13 +692,7 @@ def honest(
648
692
  Use when the Risk Card's first-sentence truncation is hiding important
649
693
  context — e.g., a commit whose constraint is stated across two lines.
650
694
  """
651
- repo_root, rel = _resolve_repo_and_path(path)
652
- if not _path_is_known_to_git(repo_root, rel):
653
- err.print(
654
- f"[yellow]warning:[/yellow] [bold]{rel}[/bold] is not tracked by git "
655
- f"and has no history in this repo."
656
- )
657
- raise typer.Exit(1)
695
+ repo_root, rel = _require_tracked(path)
658
696
  facts = gf.gather(repo_root, rel)
659
697
  if not facts.invariant_quotes:
660
698
  if json_out:
@@ -716,24 +754,18 @@ def show(
716
754
  """Risk-flavored summary for a single commit: classification + per-file risk."""
717
755
  try:
718
756
  repo_root = gf.discover_repo_root(repo.resolve())
719
- full_sha = gf._run_git(repo_root, "rev-parse", "--verify", f"{sha}^{{commit}}").strip()
720
757
  except gf.GitError as exc:
721
758
  err.print(f"[red]error:[/red] {exc}")
722
759
  raise typer.Exit(2) from exc
723
760
 
724
- raw = gf._run_git(
725
- repo_root, "log", "-1", "--no-merges", f"--pretty=format:{gf._log_format()}", full_sha
726
- )
727
- commits = gf._parse_log_records(raw)
728
- if not commits:
729
- err.print(f"[red]error:[/red] could not read commit {full_sha}")
761
+ commit = gf.read_commit(repo_root, sha)
762
+ if commit is None:
763
+ err.print(f"[red]error:[/red] could not read commit {sha!r}")
730
764
  raise typer.Exit(2)
731
- commit = commits[0]
765
+ full_sha = commit.sha
732
766
 
733
- is_incident = bool(
734
- gf._INCIDENT_RE.search(commit.subject + "\n" + commit.body)
735
- or gf._BREAKING_CC_RE.search(commit.subject)
736
- )
767
+ classification = gf.classify_commit(commit)
768
+ is_incident = classification.incident_flavoured
737
769
  invariants = gf.extract_invariant_quotes([commit])
738
770
  file_changes = gf.files_changed_in(repo_root, full_sha)
739
771
 
@@ -768,14 +800,14 @@ def show(
768
800
  )
769
801
  console.print(f" {commit.subject}")
770
802
  console.print()
771
- classification = []
803
+ badges: list[str] = []
772
804
  if is_incident:
773
- classification.append("[bold red]incident-flavored[/bold red]")
805
+ badges.append("[bold red]incident-flavored[/bold red]")
774
806
  if invariants:
775
- classification.append(f"[yellow]states {len(invariants)} invariant(s)[/yellow]")
776
- if not classification:
777
- classification.append("[dim]no special classification[/dim]")
778
- console.print(" " + " ".join(classification))
807
+ badges.append(f"[yellow]states {len(invariants)} invariant(s)[/yellow]")
808
+ if not badges:
809
+ badges.append("[dim]no special classification[/dim]")
810
+ console.print(" " + " ".join(badges))
779
811
  console.print(f" [dim]{len(file_changes)} files changed[/dim]")
780
812
 
781
813
  if not cards:
@@ -129,8 +129,13 @@ class GitError(RuntimeError):
129
129
  """Raised when a git invocation fails or produces unexpected output."""
130
130
 
131
131
 
132
- def _run_git(repo_root: Path, *args: str) -> str:
133
- """Invoke git, return stdout. Raises GitError on non-zero exit."""
132
+ def run_git(repo_root: Path, *args: str) -> str:
133
+ """Invoke ``git -C <repo_root> <args>`` and return stdout.
134
+
135
+ Public API: callers (CLI, MCP server) use this to run git commands
136
+ that aren't already wrapped in a higher-level helper here. Raises
137
+ :class:`GitError` on non-zero exit or when ``git`` itself is missing.
138
+ """
134
139
  cmd = ["git", "-C", str(repo_root), *args]
135
140
  try:
136
141
  proc = subprocess.run(
@@ -150,6 +155,10 @@ def _run_git(repo_root: Path, *args: str) -> str:
150
155
  return proc.stdout
151
156
 
152
157
 
158
+ # Back-compat alias. Prefer ``run_git`` in new code.
159
+ _run_git = run_git
160
+
161
+
153
162
  def discover_repo_root(start: Path) -> Path:
154
163
  """Find the enclosing git repo root for ``start``."""
155
164
  out = _run_git(start, "rev-parse", "--show-toplevel").strip()
@@ -240,6 +249,25 @@ def all_commits(repo_root: Path, *, max_count: int | None = None) -> list[Commit
240
249
  return _parse_log_records(raw)
241
250
 
242
251
 
252
+ def read_commit(repo_root: Path, ref: str) -> Commit | None:
253
+ """Resolve ``ref`` (SHA, tag, branch, ``HEAD~3`` …) to a single ``Commit``.
254
+
255
+ Returns ``None`` when the ref doesn't exist or doesn't resolve to a
256
+ commit. Used by ``whycode show <sha>`` and similar single-commit views.
257
+ """
258
+ try:
259
+ full_sha = run_git(
260
+ repo_root, "rev-parse", "--verify", f"{ref}^{{commit}}"
261
+ ).strip()
262
+ except GitError:
263
+ return None
264
+ raw = run_git(
265
+ repo_root, "log", "-1", "--no-merges", f"--pretty=format:{_log_format()}", full_sha
266
+ )
267
+ parsed = _parse_log_records(raw)
268
+ return parsed[0] if parsed else None
269
+
270
+
243
271
  def files_changed_in(repo_root: Path, sha: str) -> list[FileChange]:
244
272
  """Return the list of files (with diffstat) changed in ``sha``."""
245
273
  raw = _run_git(
@@ -359,6 +387,26 @@ def find_incidents(commits: Sequence[Commit]) -> list[Commit]:
359
387
  return out
360
388
 
361
389
 
390
+ @dataclass(frozen=True)
391
+ class CommitClassification:
392
+ """Light-weight summary of what kind of work a single commit represents."""
393
+
394
+ incident_flavoured: bool
395
+ invariant_count: int
396
+
397
+
398
+ def classify_commit(commit: Commit) -> CommitClassification:
399
+ """Classify a single commit by reusing the same rules ``find_incidents`` and
400
+ ``extract_invariant_quotes`` apply to a list. Public API for ``whycode show``
401
+ and any other surface that wants a single-commit verdict without
402
+ re-implementing the regex ladder.
403
+ """
404
+ return CommitClassification(
405
+ incident_flavoured=bool(find_incidents([commit])),
406
+ invariant_count=len(extract_invariant_quotes([commit])),
407
+ )
408
+
409
+
362
410
  # Straight, backtick, and the four common Unicode "smart" quote code points.
363
411
  # We build the string from chr() calls because ruff's RUF001 ambiguous-char
364
412
  # check rejects the literal Unicode quotes inline.
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: whycode-cli
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: Tells you what to be afraid of before you touch a file.
5
5
  Author: Kevin
6
6
  License-Expression: MIT
@@ -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