Skip to content

ci: add PR hygiene automation (linked issue check + stale PR cleanup)#521

Merged
andreatgretel merged 11 commits intomainfrom
andreatgretel/feat/pr-hygiene
Apr 13, 2026
Merged

ci: add PR hygiene automation (linked issue check + stale PR cleanup)#521
andreatgretel merged 11 commits intomainfrom
andreatgretel/feat/pr-hygiene

Conversation

@andreatgretel
Copy link
Copy Markdown
Contributor

@andreatgretel andreatgretel commented Apr 9, 2026

Summary

Add GitHub Actions workflows to enforce contribution quality, clean up
abandoned PRs, and run weekly agentic triage of the full issue+PR landscape.

Closes #518

Changes

Added

  • pr-linked-issue.yml - linked issue check with two jobs:
    • check (PR events) - validates non-collaborator PRs reference a triaged issue via Fixes #N. Collaborators bypass. Uses the collaborator API pattern from agentic-ci-pr-review.yml and peter-evans actions for idempotent comment management.
    • retrigger (issue labeled) - when a maintainer adds triaged to an issue, finds open PRs referencing it and edits a hidden HTML comment in their body to re-trigger the check.
  • pr-stale.yml - daily cron (09:00 UTC) that scans open PRs with failing checks. Posts a reminder after 7 days (non-collaborator) or 14 days (collaborator) of inactivity, auto-closes after 14/28 days. keep-open label as escape hatch. On auto-close, adds needs-attention label to the linked issue so the triage workflow picks it up.
  • agentic-ci-issue-triage.yml - weekly scheduled workflow (Monday 10:00 UTC) that uses Claude on the agentic-ci runner to triage all open issues and PRs. Produces a combined dashboard report posted to a pinned tracking issue.
  • .agents/recipes/issue-triage/recipe.md - recipe prompt for the triage agent. Classifies issues (bug/feature/chore/discussion), checks staleness (14/30 day thresholds), cross-references merged PRs to detect resolved issues, flags duplicates, and checks PR health (missing linked issues, failing checks, orphaned PRs, duplicate fixes).
  • plans/518/pr-hygiene-plan.md - standalone plan
  • New labels: triaged, task, keep-open, needs-attention

Changed

  • CONTRIBUTING.md - documented linked issue requirement, stale PR policy, and auto-retrigger behavior

What changes for external contributors

New contributors opening a PR against this repo will see the following flow:

  1. Open an issue first using the repo's issue templates.
  2. Wait for triage - a maintainer reviews the issue and adds the triaged label.
  3. Link the issue in the PR body using a closing keyword: Fixes #123, Closes #123, or Resolves #123.
  4. The linked issue check validates that the reference points to a real issue (not a PR) and that it carries the triaged label. If not, the check blocks and a bot comment explains what to fix.
  5. PRs can be opened before triage - the check re-runs automatically when a maintainer adds the triaged label to the linked issue. No action needed from the contributor.
  6. Stale PR cleanup - if checks are failing and the contributor is inactive for 7 days, a reminder is posted. After 14 days, the PR is auto-closed. The keep-open label (maintainer-only) prevents auto-close.

Collaborators (write/admin) bypass the linked issue check entirely and get longer stale thresholds (14-day reminder, 28-day auto-close).

How the workflows connect

pr-stale.yml (daily)                 agentic-ci-issue-triage.yml (weekly)
    |                                         |
    | auto-closes stale PR                    | reads all open issues + PRs
    | adds `needs-attention` to               | reads `needs-attention` label
    |   linked issue                          | cross-references merged PRs
    |                                         | detects duplicates
    +---- labels -----> pinned issue <--------+
                        (triage dashboard)

Attention areas

  • pr-linked-issue.yml uses pull_request_target (not pull_request) so the workflow token can post comments on fork PRs. Safe because no checkout or code execution from the PR branch occurs.
  • agentic-ci-issue-triage.yml requires two repo-level settings:
    • AGENTIC_CI_MODEL variable (already set for PR review)
    • ISSUE_TRIAGE_TRACKING_ISSUE variable (number of the pinned tracking issue)

Post-merge

  • Add Linked Issue Check as a required status check in branch protection for main
  • Create a pinned issue ("Repository Triage Dashboard") and set the ISSUE_TRIAGE_TRACKING_ISSUE repo variable
Draft: pinned tracking issue body

Repository Triage Dashboard

Automated weekly triage of open issues and pull requests, posted by the
Repository Triage workflow.

How it works

Every Monday at 10:00 UTC, an agent scans all open issues and PRs and posts
a report as a comment below. The comment is edited in place each run - there
is always exactly one report reflecting the latest state.

The report covers:

  • Issue classification - bug, feature, chore, discussion
  • Staleness detection - active / aging / stale (14/30 day thresholds)
  • Verification - cross-references merged PRs to flag potentially resolved issues
  • Duplicate detection - flags overlapping issues or multiple PRs fixing the same issue
  • PR health - missing linked issues, failing checks, stale PRs, orphaned PRs

Labels

Label Set by Meaning
needs-attention Stale PR workflow A linked PR was auto-closed; issue needs a new owner or should be closed
keep-open Maintainers Prevents the stale PR workflow from auto-closing a PR
triaged Maintainers Marks an issue as reviewed; required for external PRs

Manual trigger

Run the workflow on-demand from the Actions tab
using "Run workflow".

Testing

Manual validation:

  • Open test PR from non-collaborator account, verify check blocks
  • Add triaged to linked issue, verify re-check passes automatically
  • Verify collaborator PRs pass without a linked issue
  • Run stale workflow via workflow_dispatch, verify correct PR targeting
  • Confirm keep-open label prevents auto-close
  • Run triage workflow via workflow_dispatch, verify report posted to tracking issue
  • Verify needs-attention label added to linked issue on stale PR auto-close

Description updated with AI

Add two workflows to enforce contribution quality and clean up abandoned PRs:

- pr-linked-issue.yml: required status check that validates external PRs
  reference a triaged issue. Collaborators bypass. Re-triggers automatically
  when a maintainer adds the `triaged` label to the linked issue.

- pr-stale.yml: daily cron that reminds authors of failing checks after 7/14
  days of inactivity and auto-closes after 14/28 days (external/collaborator).
  Respects `keep-open` label.

New labels created: `triaged`, `task`, `keep-open`.

Closes #518
Signed-off-by: Andrea Manoel <[email protected]>
Add a weekly scheduled workflow that uses Claude to triage all open issues
and PRs, producing a combined dashboard report on a pinned tracking issue.

- New recipe (.agents/recipes/issue-triage/) classifies issues, checks
  staleness, cross-references merged PRs, detects duplicates, and flags
  PR health problems (missing linked issues, failing checks, orphaned PRs)
- New workflow (.github/workflows/agentic-ci-issue-triage.yml) runs every
  Monday 10:00 UTC on the agentic-ci runner, with manual dispatch support
- pr-stale.yml now adds needs-attention label to linked issues when a PR
  is auto-closed, bridging the two workflows via labels
@andreatgretel andreatgretel force-pushed the andreatgretel/feat/pr-hygiene branch from c9fb5ed to 3fea4ff Compare April 13, 2026 17:33
@andreatgretel andreatgretel marked this pull request as ready for review April 13, 2026 17:33
@andreatgretel andreatgretel requested a review from a team as a code owner April 13, 2026 17:33
@github-actions
Copy link
Copy Markdown
Contributor

Summary

PR #521 adds three GitHub Actions workflows and a triage recipe to enforce contribution quality for the DataDesigner repository:

  1. pr-linked-issue.yml — Requires external contributors to link PRs to a triaged issue. Collaborators bypass. Uses pull_request_target for fork-PR comment access and a retrigger job that edits PR bodies when issues are labeled triaged.
  2. pr-stale.yml — Daily cron that identifies PRs with failing checks and no author activity. Reminds after 7/14 days (external/collaborator) and auto-closes after 14/28 days. keep-open label as escape hatch.
  3. agentic-ci-issue-triage.yml — Weekly scheduled workflow running Claude Code on a self-hosted runner to triage all open issues and PRs, posting a dashboard report to a pinned tracking issue.
  4. recipe.md — Triage recipe prompt for the agent, with classification, staleness, verification, and duplicate detection instructions.
  5. CONTRIBUTING.md — Updated to document the linked issue requirement and stale PR policy.
  6. plans/518/pr-hygiene-plan.md — Implementation plan.

Total: 873 additions, 1 deletion across 6 files. All new files except the CONTRIBUTING.md edit.


Findings

Critical

No critical issues found.

High

  1. pr-linked-issue.ymlPR_BODY injection risk in shell context (line ~414)

    The step "Parse issue reference from PR body" sets PR_BODY: ${{ github.event.pull_request.body }} as an environment variable. While using pull_request_target is correctly chosen (no code checkout), the PR body is attacker-controlled text from external contributors. The grep -ioP on $PR_BODY is relatively safe since it reads from an env var (not a direct expression injection), but the quoting around $PR_BODY in the -z check and echo should be verified. The current pattern (echo "$PR_BODY") is acceptable since it's piped to grep, but a PR body containing shell metacharacters in the env var expansion is inherently risky.

    Recommendation: Consider writing $PR_BODY to a temp file first, then grepping the file, to avoid any shell expansion edge cases. This matches the pattern already used for comment bodies later in the same workflow.

  2. pr-linked-issue.yml — Retrigger job edits PR body with gh pr edit --body (line ~610)

    The retrigger job reads the current PR body and appends/updates a hidden HTML comment, then writes it back via gh pr edit. This body content comes from gh pr view and is passed as a positional argument to gh pr edit --body "$NEW_BODY". If the PR body contains characters that break shell quoting (e.g., unmatched quotes, backticks), this could fail or behave unexpectedly.

    Recommendation: Write $NEW_BODY to a temp file and use --body-file instead of --body "$NEW_BODY".

Medium

  1. pr-stale.yml — API rate limit risk in the per-PR loop (lines ~680-800)

    The stale check iterates over all open PRs and for each one makes 3-4 API calls (collaborator permission check, PR checks, commits endpoint, comments endpoint). With 200 open PRs (the --limit), this could hit the GITHUB_TOKEN rate limit (1000 requests/hour for Actions). The workflow has no rate limiting or batching.

    Recommendation: Add a brief sleep between iterations or batch API calls. Alternatively, consider using GraphQL to fetch all needed data in fewer requests.

  2. pr-stale.ymlissues: read permission may be insufficient (line ~636)

    The workflow needs issues: write to add the needs-attention label to linked issues when auto-closing a stale PR (line ~768: gh issue edit ... --add-label). The current permissions block only grants issues: read.

    Recommendation: Change issues: read to issues: write in the permissions block.

  3. pr-linked-issue.yml — Comment indentation in heredocs (lines ~480-530)

    The heredoc bodies for comments are indented with spaces to match the surrounding YAML/shell indentation. GitHub will render this indented markdown with unexpected code-block formatting. The MSG heredoc is not using <<- (which strips leading tabs, not spaces).

    Recommendation: Either use <<- with tab indentation, or left-align the heredoc content (no leading whitespace), or use a sed / dedent step to strip the indentation before writing to /tmp/comment-body.md.

  4. agentic-ci-issue-triage.ymlsed frontmatter stripping is fragile (line ~283)

    RECIPE_BODY=$(cat .agents/recipes/issue-triage/recipe.md \
      | sed '1,/^---$/{ /^---$/,/^---$/d }')

    This sed expression attempts to strip YAML frontmatter between --- markers. The nested address range is brittle — if the recipe content itself contains --- (e.g., as a horizontal rule in markdown), it may strip too much. The existing agentic-ci-pr-review.yml likely uses the same pattern, but it's worth noting as a fragility.

    Recommendation: Use awk for more predictable frontmatter stripping: awk 'BEGIN{n=0} /^---$/{n++; next} n>=2'.

  5. pr-stale.ymldate -u -j -f fallback is macOS-specific (lines ~719-720)

    The date parsing includes a date -u -j -f "%Y-%m-%dT%H:%M:%SZ" fallback for macOS date. Since this runs on ubuntu-latest, the macOS fallback will never execute. It's harmless dead code, but adds noise.

    Recommendation: Remove the macOS fallback since the workflow is pinned to ubuntu-latest, or add a comment explaining it's a portability measure for local testing.

Low

  1. plans/518/pr-hygiene-plan.md — Plan status and checklist mismatch (lines ~826, 903-906)

    The plan status is in-progress and the deliverables checklist shows .github/workflows/pr-linked-issue.yml and .github/workflows/pr-stale.yml as unchecked (- [ ]), even though they are included in this PR. The plan should be updated to reflect the current state.

  2. plans/518/pr-hygiene-plan.md — Non-goals mismatch with actual PR (line ~849-850)

    The plan states "No agent or self-hosted runner involvement" as a non-goal, but the PR includes agentic-ci-issue-triage.yml which runs on [self-hosted, agentic-ci]. This scope expansion should be noted in the plan.

  3. pr-linked-issue.ymlsynchronize event type may cause unnecessary re-runs

    The workflow triggers on synchronize (new commits pushed), which means every push to a PR re-runs the linked issue check. Since the check only reads the PR body (not the code), synchronize events add unnecessary workflow runs. This is a minor efficiency concern.

    Recommendation: Consider removing synchronize from the trigger types if re-checking on push is not needed.

  4. agentic-ci-issue-triage.yml — Missing needs-attention label in permissions context

    The recipe instructs the agent to read the needs-attention label from issues, and the stale PR workflow adds it. But neither workflow ensures these labels exist. If they don't exist, gh issue edit --add-label will create them automatically (GitHub behavior), but with default color/description.

    Recommendation: Document (or script) label creation as a post-merge step, which is partially covered in the PR description but could be more explicit.

  5. Recipe recipe.mdgh pr checks loop could be slow (lines ~44-47)

    The recipe instructs the agent to run gh pr checks in a loop for every open PR. With many open PRs, this sequential API call pattern could consume significant agent time and turns.

    Recommendation: Consider using gh api with GraphQL to batch-fetch check statuses for all PRs in one call.


Verdict

Approve with suggestions. This is a well-structured PR that adds meaningful contribution-quality guardrails. The workflow design follows established patterns in the repo (agentic-ci-pr-review.yml), the pull_request_target usage is safe (no code checkout), and the tiered thresholds for collaborators vs. external contributors are reasonable.

Must-fix before merge:

Strongly recommended:

The remaining findings are minor improvements that can be addressed in follow-up work.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 13, 2026

Greptile Summary

Adds three GitHub Actions workflows to enforce PR hygiene: a linked-issue check (pull_request_target, no checkout — safe), a daily stale-PR closer, and a weekly agentic triage that posts a dashboard to a pinned tracking issue. Two pagination gaps were found that affect correctness in the long run; all three previously-flagged bugs (wrong status output, missing issues:write, multi-line PR-number output) are confirmed resolved.

Confidence Score: 5/5

Safe to merge; the two remaining findings are latent P2 pagination gaps that won't manifest for months and don't affect the primary agent path.

All three P0/P1 issues from prior rounds are resolved. The two new findings are P2: the fallback comment-lookup pagination gap only bites after 30+ weekly runs (~7 months), and the commits pagination issue only affects PRs with >30 commits. Neither blocks the intended behavior today.

.github/workflows/agentic-ci-issue-triage.yml (fallback pagination) and .github/workflows/pr-stale.yml (commits pagination) have minor latent correctness issues worth a quick fix.

Important Files Changed

Filename Overview
.agents/recipes/issue-triage/recipe.md New agent recipe defining data-gathering, classification, and report-generation instructions for the weekly triage workflow; logic and format are consistent with the workflow that invokes it.
.github/workflows/agentic-ci-issue-triage.yml New weekly agentic triage workflow with a correct primary posting path; the fallback comment-lookup query lacks --paginate and will silently target the wrong comment after ~30 runs.
.github/workflows/pr-linked-issue.yml New linked-issue check using pull_request_target safely (no checkout); previously flagged bugs (wrong status output, PR-number multiline output) confirmed fixed in prior commits.
.github/workflows/pr-stale.yml New stale-PR cleanup workflow; reminder/close logic is sound, but the unpaginated commits API call can return a stale "last push" timestamp for PRs with >30 commits, potentially triggering false reminders.
CONTRIBUTING.md Documents linked-issue requirement and stale-PR policy accurately matching the new workflow behavior.
plans/518/pr-hygiene-plan.md Design document for the PR hygiene work; no executable content, consistent with the implemented workflows.

Sequence Diagram

sequenceDiagram
  participant C as Contributor
  participant GH as GitHub
  participant LIC as pr-linked-issue.yml
  participant Stale as pr-stale.yml
  participant Triage as agentic-ci-issue-triage.yml
  participant TI as Tracking Issue

  C->>GH: Opens PR (fork)
  GH->>LIC: pull_request_target [opened]
  LIC->>GH: Check author permissions (collaborator API)
  LIC->>GH: Parse body for Fixes/Closes/Resolves #N
  LIC->>GH: Validate issue exists + has triaged label
  LIC->>C: Post failure comment (or pass silently)

  Note over GH,LIC: Maintainer labels issue as triaged
  GH->>LIC: issues [labeled: triaged]
  LIC->>GH: Find open PRs referencing issue
  LIC->>GH: Edit PR body (hidden timestamp triggers edited event)
  GH->>LIC: pull_request_target [edited]
  LIC->>C: Check passes, delete failure comment

  Note over Stale: Daily cron 09:00 UTC
  GH->>Stale: schedule / workflow_dispatch
  Stale->>GH: List open PRs with failing checks
  Stale->>C: Post reminder (7d external / 14d collab)
  Stale->>GH: Close PR + add needs-attention to linked issue (14d / 28d)

  Note over Triage: Weekly Monday 10:00 UTC
  GH->>Triage: schedule / workflow_dispatch
  Triage->>GH: Fetch all issues + open/merged PRs
  Triage->>TI: Edit existing triage comment (or post new)
  Note over TI: Tracks needs-attention labels from Stale workflow
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: .github/workflows/agentic-ci-issue-triage.yml
Line: 111-113

Comment:
**Fallback comment lookup breaks after ~30 weekly runs**

`gh api` without `--paginate` returns only the first page (30 items). GitHub's issue comments API returns comments in ascending (oldest-first) order, so once 30+ weekly triage comments have accumulated, `last` selects the 30th-oldest comment rather than the most recent one. The fallback step would then either silently overwrite a ~7-month-old comment or skip the update entirely (if that old comment happened to contain today's date string), leaving the tracking issue without a current report.

```suggestion
          EXISTING=$(gh api "repos/${{ github.repository }}/issues/${TRACKING_ISSUE}/comments" \
            --paginate \
            --jq "[.[] | select(.user.login == \"github-actions[bot]\") | select(.body | contains(\"${MARKER}\"))] | last | .id" \
            2>/dev/null || echo "")
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: .github/workflows/pr-stale.yml
Line: 89-90

Comment:
**Unpaginated commits API gives stale "last push" for PRs with 30+ commits**

`gh api .../commits` without `--paginate` returns the first page (30 commits), ordered oldest-first. For a PR with more than 30 commits, `last` gets the 30th-oldest commit's timestamp rather than the actual most recent push. A contributor who has been actively pushing to a long-lived PR with many commits could have their inactivity clock based on a commit from weeks ago, triggering a false stale reminder or even premature auto-close.

```suggestion
            LAST_PUSH=$(gh api "repos/${REPO}/pulls/${PR_NUM}/commits?per_page=100" \
              --jq 'last | .commit.committer.date' 2>/dev/null || echo "")
```

For PRs exceeding 100 commits `--paginate` would be needed; `per_page=100` covers the vast majority of cases with a single request.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (4): Last reviewed commit: "Merge branch 'main' into andreatgretel/f..." | Re-trigger Greptile

- pr-linked-issue: fix comment gate so failure comments are posted
- pr-stale: upgrade issues permission to write for labeling
- pr-stale: compare reminder timestamp against last activity so
  push/comment actually resets the stale timer
PR bodies with backticks or unmatched quotes would break the
gh pr edit --body "$NEW_BODY" call. Write to a temp file and
use --body-file instead.
andreatgretel and others added 5 commits April 13, 2026 18:12
jq outputs newline-separated numbers but GITHUB_OUTPUT only
preserves the first line. Convert to space-separated so the
for loop processes all matching PRs.
- Move attacker-influenced values (${{ user.login }}, step outputs)
  from expression interpolation in run: blocks to env vars
- Replace echo "$PR_BODY" | grep with write-to-file + grep-file
  to avoid shell expansion of untrusted PR body content
- Same treatment for PR body handling in retrigger and stale jobs
Remove peter-evans/find-comment and peter-evans/create-or-update-comment
third-party action dependencies. Replace with gh api calls for finding,
creating, updating, and deleting bot comments. Eliminates supply chain
risk from unpinned third-party actions.
@andreatgretel andreatgretel added the agent-review Trigger agentic CI review label Apr 13, 2026
@github-actions
Copy link
Copy Markdown
Contributor

PR Review: #521 — ci: add PR hygiene automation (linked issue check + stale PR cleanup)

Summary

This PR adds three interconnected GitHub Actions workflows to enforce contribution quality and clean up abandoned PRs:

  1. pr-linked-issue.yml — Required status check: external PRs must reference a triaged issue. Collaborators bypass. Auto-retriggers when the triaged label is added to the linked issue.
  2. pr-stale.yml — Daily cron: reminds then auto-closes PRs with failing checks after configurable inactivity thresholds (7/14 days external, 14/28 collaborator).
  3. agentic-ci-issue-triage.yml — Weekly Claude-powered triage of all open issues and PRs, posting a dashboard report to a pinned tracking issue.

Also includes a triage recipe (.agents/recipes/issue-triage/recipe.md), CONTRIBUTING.md updates, and a plan document.

Files changed: 6 | +888 / -1 | Commits: 10

The security posture is strong. The commit history shows iterative hardening: pull_request_target is documented and safe (no checkout), attacker-controlled values were moved from expression interpolation to env vars, file-based grep replaced echo | grep for untrusted content, --body-file replaced --body "$VAR" for PR body editing, and third-party actions (peter-evans/*) were replaced with gh api calls to eliminate supply chain risk.


Findings

Critical

No critical issues found.

High

No high-severity issues found.

Medium

1. agentic-ci-issue-triage.ymlactions/checkout@v4 not pinned to SHA

The existing agentic-ci-pr-review.yml pins the checkout action to a specific commit SHA:

uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

The new triage workflow uses the unpinned mutable tag:

uses: actions/checkout@v4

For consistency and supply chain safety, pin to the same SHA.

2. pr-stale.yml — Missing --paginate on comment-fetching API calls

The gh api calls that fetch PR comments to find the reminder marker or determine the last author comment default to 30 results per page:

REMINDER_JSON=$(gh api "repos/${REPO}/issues/${PR_NUM}/comments" --jq "..." ...)
LAST_COMMENT=$(gh api "repos/${REPO}/issues/${PR_NUM}/comments" --jq "..." ...)

On PRs with 30+ comments, this could miss the reminder comment (leading to duplicate reminders or premature closure) or the latest author comment (causing false stale classification). Adding --paginate to these calls would make the logic robust.

The same concern applies to the comment-lookup in pr-linked-issue.yml, though it's lower risk since those PRs are typically newer.

3. pr-stale.yml — API rate limit risk in the per-PR loop

The stale check iterates over all open PRs and makes 3-4 API calls per PR (collaborator permission, PR checks, commits endpoint, comments endpoint). With the --limit 200 ceiling, this could reach 600-800 API calls in a single run, approaching the GITHUB_TOKEN rate limit of 1,000 requests/hour for Actions.

Recommendation: Consider adding --paginate carefully (which also adds calls) or using GraphQL to batch-fetch data. At minimum, document the rate-limit assumption.

Low

4. agentic-ci-issue-triage.yml — Fallback step uses shell variable for large PATCH body

The fallback step updates an existing comment with:

REPORT=$(cat /tmp/issue-triage-report.md)
gh api -X PATCH "repos/.../issues/comments/${EXISTING}" -f body="$REPORT"

For very large reports or reports with shell-special characters, this could be fragile. The new-comment path already uses --body-file. For consistency:

jq -n --rawfile body /tmp/issue-triage-report.md '{"body": $body}' \
  | gh api -X PATCH "repos/.../issues/comments/${EXISTING}" --input -

5. plans/518/pr-hygiene-plan.md — Status and checklist out of date

The plan frontmatter has status: in-progress and several deliverables are still unchecked, even though the corresponding files are included in this PR. The non-goals section states "No agent or self-hosted runner involvement," but the PR includes agentic-ci-issue-triage.yml which runs on [self-hosted, agentic-ci]. The plan should be updated to reflect the actual scope and completion state.

6. recipe.mdissues: write permission vs. "read-only triage" constraint

The recipe frontmatter declares issues: write, while the constraints section says "Read-only triage. Do not close, label, or modify any issues or PRs." The write permission is needed for posting to the tracking issue (not for modifying triaged items), but a clarifying comment in the frontmatter would prevent confusion:

permissions:
  issues: write    # for posting to the tracking issue, not modifying triaged items

Info

7. ${{ github.repository }} used in run: blocks via expression interpolation

Several run: blocks in the retrigger job and triage workflow use ${{ github.repository }} directly rather than through env vars. Since this is an org-controlled value (not user input), it is safe. However, for consistency with the injection-hardening approach applied elsewhere in this PR, these could also be moved to env vars. Purely a style point.

8. macOS date fallback in pr-stale.yml

Date parsing includes date -u -j -f as a fallback for macOS. Since the workflow runs on ubuntu-latest, this branch never executes. Harmless but adds noise — could be removed or commented as a local-testing aid.


What looks good

  • Security documentation: The pull_request_target comment block explaining why it's used and what must never be added (checkout) is excellent.
  • Shell injection hardening: The iterative hardening across commits 5-8 is thorough — env vars for user-controlled values, file-based grep, --body-file for PR body editing.
  • Third-party action removal: Replacing peter-evans/find-comment and peter-evans/create-or-update-comment with gh api calls eliminates unpinned third-party dependencies entirely.
  • Retrigger mechanism: The hidden HTML comment approach to trigger the edited event is clever and well-implemented. The fix for the multi-PR output issue (newline to space conversion) shows attention to edge cases.
  • Stale timer reset: Comparing reminder timestamp against last activity and deleting the reminder when the author responds prevents the "reminder posted → immediately close" race condition.
  • Escape hatches: keep-open label and collaborator bypass provide appropriate override mechanisms without special-casing.
  • Workflow interconnection: The needs-attention label bridge between stale cleanup and triage is a clean way to connect the workflows without coupling them.
  • Consistent patterns: The triage workflow mirrors the structure of agentic-ci-pr-review.yml (pre-flight checks, recipe frontmatter stripping, fallback posting, job summary).
  • CONTRIBUTING.md update: Concisely documents the new policies and the auto-retrigger behavior.

Verdict

Approve with suggestions. The core logic is correct, security posture is strong, and the architecture is well-designed with good escape hatches. The iterative hardening across the commit history shows careful attention to security.

Recommended before merge:

The remaining findings are minor improvements that can be addressed in follow-up work.

@github-actions github-actions bot removed the agent-review Trigger agentic CI review label Apr 13, 2026
@andreatgretel andreatgretel merged commit 82c1a69 into main Apr 13, 2026
47 checks passed
@andreatgretel andreatgretel deleted the andreatgretel/feat/pr-hygiene branch April 14, 2026 11:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: add PR hygiene automation (linked issue check + stale PR cleanup)

2 participants