This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git


The following commit(s) were added to refs/heads/main by this push:
     new a171e60  Flag unbounded gh list calls (#216)
a171e60 is described below

commit a171e60909d54b68856eda6532a8cf9a2548f2af
Author: Justin Mclean <[email protected]>
AuthorDate: Mon May 25 15:02:41 2026 +1000

    Flag unbounded gh list calls (#216)
    
    Generated-by: Codex (GPT-5)
---
 .../skills/pr-management-code-review/selectors.md  |  6 +--
 .claude/skills/pr-management-triage/actions.md     |  3 ++
 .claude/skills/security-issue-invalidate/SKILL.md  |  3 ++
 .../security-tracker-stats-dashboard/SKILL.md      | 12 +----
 .../src/skill_validator/__init__.py                | 43 ++++++++++++++++
 tools/skill-validator/tests/test_validator.py      | 58 ++++++++++++++++++++++
 6 files changed, 111 insertions(+), 14 deletions(-)

diff --git a/.claude/skills/pr-management-code-review/selectors.md 
b/.claude/skills/pr-management-code-review/selectors.md
index de19154..bbd810b 100644
--- a/.claude/skills/pr-management-code-review/selectors.md
+++ b/.claude/skills/pr-management-code-review/selectors.md
@@ -117,7 +117,7 @@ since="${SINCE:-30 days ago}"   # default; overridable via 
since:<window>
 
 # 1) Files in open PRs authored by viewer:
 viewer_open_prs=$(gh pr list --repo <repo> --author "$viewer" \
-  --state open --json number --jq '.[].number')
+  --state open --limit 100 --json number --jq '.[].number')
 
 mine_via_open_prs=$(for n in $viewer_open_prs; do
   gh pr view "$n" --repo <repo> --json files --jq '.files[].path'
@@ -490,8 +490,8 @@ gh pr list \
 ```
 
 Often combined with `area:<LBL>` to scope. Without `area:` it's
-typically too broad for a single sitting; warn the maintainer if
-the result count exceeds 30.
+typically too broad for a single sitting. If the result count equals
+the `--limit` value, note that there may be additional results not shown.
 
 ---
 
diff --git a/.claude/skills/pr-management-triage/actions.md 
b/.claude/skills/pr-management-triage/actions.md
index 9a1fc76..0fb9671 100644
--- a/.claude/skills/pr-management-triage/actions.md
+++ b/.claude/skills/pr-management-triage/actions.md
@@ -620,6 +620,7 @@ path.
 ```bash
 # 1. List open PRs by the author
 gh pr list --repo <repo> --author <author_login> --state open \
+  --limit 100 \
   --json number --jq '.[].number'
 
 # 2. For each PR, in parallel — close + label + comment
@@ -630,6 +631,8 @@ for pr in $PR_NUMBERS; do
 done
 ```
 
+If the result count equals the limit, note that there may be additional 
results not shown.
+
 Body template: 
[`comment-templates.md#suspicious-changes`](comment-templates.md).
 
 The comment is deliberately short and non-accusatory — the
diff --git a/.claude/skills/security-issue-invalidate/SKILL.md 
b/.claude/skills/security-issue-invalidate/SKILL.md
index 085ed61..420f071 100644
--- a/.claude/skills/security-issue-invalidate/SKILL.md
+++ b/.claude/skills/security-issue-invalidate/SKILL.md
@@ -244,12 +244,15 @@ in Gmail.
 ```bash
 # Find open trackers with a INVALID triage proposal
 gh issue list --repo <tracker> --state open --label "needs triage" \
+  --limit 100 \
   --json number,title,comments \
   --jq '.[] | select(.comments | map(.body) | any(
     startswith("**Triage proposal**") and contains("INVALID")
   )) | .number'
 ```
 
+If the result count equals the limit, note that there may be additional 
results not shown.
+
 Then, per resolved tracker, check the triage-proposal comment's
 reactions and follow-up comments for the team-consensus marker
 via `gh api repos/<tracker>/issues/comments/<id>/reactions`.
diff --git a/.claude/skills/security-tracker-stats-dashboard/SKILL.md 
b/.claude/skills/security-tracker-stats-dashboard/SKILL.md
index d940c0b..f7b4eee 100644
--- a/.claude/skills/security-tracker-stats-dashboard/SKILL.md
+++ b/.claude/skills/security-tracker-stats-dashboard/SKILL.md
@@ -1,16 +1,6 @@
 ---
 name: security-tracker-stats-dashboard
-description: |
-  Generate a self-contained HTML dashboard of `<tracker>` repository
-  statistics: issue-lifecycle bands (untriaged / triaged / PR-merged /
-  fixed-released / closed-other), opened-vs-untriaged backlog,
-  cumulative opened/closed, mean time to triage, mean time to first
-  response, and — when `<upstream>` is configured — mean time
-  createdAt -> PR-opened, PR-open -> PR-merged, and PR-merged ->
-  advisory announced. All charts are line / area (no bars) with
-  `connectgaps: true`. Vertical annotations on every chart mark the
-  milestones declared in the project's overlay (e.g. "skill
-  adoption", "team handover", "process change").
+description: Generate a self-contained HTML dashboard of `<tracker>` 
repository statistics for security-team review.
 when_to_use: |
   Invoke when the user says "regenerate the tracker dashboard", "show
   monthly/quarterly stats", "tracker stats", "dashboard", or
diff --git a/tools/skill-validator/src/skill_validator/__init__.py 
b/tools/skill-validator/src/skill_validator/__init__.py
index 3b16ffe..b26876f 100644
--- a/tools/skill-validator/src/skill_validator/__init__.py
+++ b/tools/skill-validator/src/skill_validator/__init__.py
@@ -141,12 +141,14 @@ INJECTION_GUARD_CATEGORY = "injection_guard"
 INJECTION_GUARD_TODO_CATEGORY = "injection_guard_todo"
 
 BODY_INLINE_CATEGORY = "body_inline"
+GH_LIST_CATEGORY = "gh_list_no_limit"
 SOFT_CATEGORIES: frozenset[str] = frozenset(
     {
         PRINCIPLE_CATEGORY,
         TRIGGER_PRESERVATION_CATEGORY,
         INJECTION_GUARD_TODO_CATEGORY,
         BODY_INLINE_CATEGORY,
+        GH_LIST_CATEGORY,
     }
 )
 
@@ -912,6 +914,45 @@ def validate_body_inline(path: Path, text: str) -> 
Iterable[Violation]:
         )
 
 
+# ---------------------------------------------------------------------------
+# gh list --limit check
+# ---------------------------------------------------------------------------
+
+_GH_LIST_RE = re.compile(r"\bgh\s+(issue|pr)\s+list\b")
+
+
+def _join_continuations(block_body: str) -> str:
+    r"""Join shell line-continuations (trailing ``\``) within a fenced 
block."""
+    return re.sub(r"\\\n\s*", " ", block_body)
+
+
+def validate_gh_list_limit(path: Path, text: str) -> Iterable[Violation]:
+    """Flag ``gh issue list`` / ``gh pr list`` in fenced blocks without 
``--limit``.
+
+    Unbounded list calls silently return GitHub CLI's default page size, so
+    downstream counts or filters can operate on an incomplete result set.
+    """
+    for block_match in _FENCED_CODE_RE.finditer(text):
+        joined = _join_continuations(block_match.group())
+        for cmd_match in _GH_LIST_RE.finditer(joined):
+            line_start = joined.rfind("\n", 0, cmd_match.start()) + 1
+            line_end = joined.find("\n", cmd_match.end())
+            if line_end == -1:
+                line_end = len(joined)
+            logical_line = joined[line_start:line_end]
+            if "--limit" in logical_line:
+                continue
+            line_no = text[: block_match.start()].count("\n") + joined[: 
cmd_match.start()].count("\n") + 1
+            yield Violation(
+                path,
+                line_no,
+                f"gh-list-no-limit: `{cmd_match.group()}` has no `--limit` — "
+                f"unbounded list calls silently cap at 30 results on large 
repos; "
+                f"add `--limit <N>` (or `--limit 100` as a safe default)",
+                category=GH_LIST_CATEGORY,
+            )
+
+
 def collect_doc_files(root: Path | None = None) -> set[Path]:
     """Return every .md file under docs/ and projects/_template/."""
     repo_root = root or find_repo_root()
@@ -949,6 +990,7 @@ def run_validation(root: Path | None = None) -> 
list[Violation]:
         violations.extend(validate_links(path, text, skill_dirs, doc_files))
         violations.extend(validate_placeholders(path, text))
         violations.extend(validate_body_inline(path, text))
+        violations.extend(validate_gh_list_limit(path, text))
 
     return violations
 
@@ -1011,6 +1053,7 @@ _SOFT_RULE_PREFIXES: tuple[str, ...] = (
     "parenthetical rationale",
     "trigger phrase",
     "injection-guard TODO",
+    "gh-list-no-limit",
 )
 
 
diff --git a/tools/skill-validator/tests/test_validator.py 
b/tools/skill-validator/tests/test_validator.py
index 1a50693..f33259d 100644
--- a/tools/skill-validator/tests/test_validator.py
+++ b/tools/skill-validator/tests/test_validator.py
@@ -26,6 +26,7 @@ import pytest
 from skill_validator import (
     BODY_INLINE_CATEGORY,
     FORBIDDEN_PATTERNS,
+    GH_LIST_CATEGORY,
     INJECTION_GUARD_CALLOUT_SENTINEL,
     INJECTION_GUARD_CATEGORY,
     INJECTION_GUARD_TODO_CATEGORY,
@@ -49,6 +50,7 @@ from skill_validator import (
     slugify,
     validate_body_inline,
     validate_frontmatter,
+    validate_gh_list_limit,
     validate_injection_guard,
     validate_links,
     validate_placeholders,
@@ -932,6 +934,62 @@ class TestSoftCategories:
         assert TRIGGER_PRESERVATION_CATEGORY in SOFT_CATEGORIES
         assert INJECTION_GUARD_TODO_CATEGORY in SOFT_CATEGORIES
         assert BODY_INLINE_CATEGORY in SOFT_CATEGORIES
+        assert GH_LIST_CATEGORY in SOFT_CATEGORIES
+
+
+# ---------------------------------------------------------------------------
+# gh list --limit check
+# ---------------------------------------------------------------------------
+
+
+def _fenced(cmd: str) -> str:
+    """Wrap a command in a fenced bash block."""
+    return f"```bash\n{cmd}\n```\n"
+
+
+class TestGhListLimit:
+    def test_fires_for_gh_issue_list_no_limit(self, tmp_path: Path) -> None:
+        path = tmp_path / "SKILL.md"
+        violations = list(validate_gh_list_limit(path, _fenced("gh issue list 
--repo <repo>")))
+        assert any("gh-list-no-limit" in v.message for v in violations)
+
+    def test_fires_for_gh_pr_list_no_limit(self, tmp_path: Path) -> None:
+        path = tmp_path / "SKILL.md"
+        violations = list(validate_gh_list_limit(path, _fenced("gh pr list 
--repo <repo>")))
+        assert any("gh-list-no-limit" in v.message for v in violations)
+
+    def test_fires_on_sub_doc(self, tmp_path: Path) -> None:
+        path = tmp_path / "actions.md"
+        violations = list(validate_gh_list_limit(path, _fenced("gh pr list 
--repo <repo> --state open")))
+        assert any("gh-list-no-limit" in v.message for v in violations)
+
+    def test_violation_is_soft_category(self, tmp_path: Path) -> None:
+        path = tmp_path / "SKILL.md"
+        violations = list(validate_gh_list_limit(path, _fenced("gh issue list 
--repo <repo>")))
+        assert all(v.category == GH_LIST_CATEGORY for v in violations)
+
+    def test_silent_when_limit_on_same_line(self, tmp_path: Path) -> None:
+        path = tmp_path / "SKILL.md"
+        violations = list(validate_gh_list_limit(path, _fenced("gh issue list 
--repo <repo> --limit 100")))
+        assert not any("gh-list-no-limit" in v.message for v in violations)
+
+    def test_silent_when_limit_on_continuation_line(self, tmp_path: Path) -> 
None:
+        path = tmp_path / "selectors.md"
+        text = _fenced("gh pr list \\\n  --repo <repo> \\\n  --state open \\\n 
 --limit 100")
+        violations = list(validate_gh_list_limit(path, text))
+        assert not any("gh-list-no-limit" in v.message for v in violations)
+
+    def test_silent_for_inline_backtick_mention(self, tmp_path: Path) -> None:
+        path = tmp_path / "SKILL.md"
+        text = "Use `gh issue list` with `--limit` to avoid truncation.\n"
+        violations = list(validate_gh_list_limit(path, text))
+        assert not any("gh-list-no-limit" in v.message for v in violations)
+
+    def test_silent_outside_fenced_block(self, tmp_path: Path) -> None:
+        path = tmp_path / "SKILL.md"
+        text = "Run gh issue list --repo <repo> to see open issues.\n"
+        violations = list(validate_gh_list_limit(path, text))
+        assert not any("gh-list-no-limit" in v.message for v in violations)
 
 
 # ---------------------------------------------------------------------------

Reply via email to