Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions .github/workflows/pr-size-soft-cap.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: pr-size-soft-cap

# #602 — advisory soft cap on PR size to reduce merge-thrash. Comments
# (and updates a sticky marker comment) when a PR exceeds 200 LOC or
# 3 changed files. Authors split or apply `size:override` to opt out;
# no hard block. Quiet when the PR is under both thresholds, including
# when a previously-flagged PR is shrunk back below the line — the
# sticky comment is removed in that case so the PR view stays clean.

on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]

permissions:
contents: read
pull-requests: write

# One pass per PR. New pushes cancel an in-flight check so we don't
# race on the comment edit.
concurrency:
group: pr-size-soft-cap-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
size-check:
runs-on: ubuntu-latest
timeout-minutes: 3
# Skip drafts — they're work-in-progress and the author already knows.
if: github.event.pull_request.draft == false
steps:
- uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit

- name: Check size and post / update / remove sticky comment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_ADDITIONS: ${{ github.event.pull_request.additions }}
PR_DELETIONS: ${{ github.event.pull_request.deletions }}
PR_CHANGED_FILES: ${{ github.event.pull_request.changed_files }}
# Comma-separated label names. `size:override` opts out.
PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
LOC_LIMIT: '200'
FILES_LIMIT: '3'
MARKER: '<!-- pr-size-soft-cap-v1 -->'
run: |
set -euo pipefail

# Opt-out via label.
if printf '%s' ",${PR_LABELS}," | grep -q ',size:override,'; then
echo "size:override label present; nothing to do."
exit 0
fi

loc=$((PR_ADDITIONS + PR_DELETIONS))
files="${PR_CHANGED_FILES}"
over=0
if [ "$loc" -gt "$LOC_LIMIT" ]; then over=1; fi
if [ "$files" -gt "$FILES_LIMIT" ]; then over=1; fi

# Find any prior sticky comment authored by github-actions[bot].
existing_id="$(gh api \
"repos/${REPO}/issues/${PR_NUMBER}/comments" --paginate \
--jq "[.[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"${MARKER}\")))] | .[0].id // empty")"

Comment on lines +51 to +67

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

size:override exits before stale sticky cleanup.

At Line 52, the script exits before resolving/deleting any existing sticky comment, so previously posted advisories can remain visible after opt-out (and contradict the message at Line 92).

💡 Suggested fix
-          # Opt-out via label.
-          if printf '%s' ",${PR_LABELS}," | grep -q ',size:override,'; then
-            echo "size:override label present; nothing to do."
-            exit 0
-          fi
-
           loc=$((PR_ADDITIONS + PR_DELETIONS))
           files="${PR_CHANGED_FILES}"
           over=0
           if [ "$loc" -gt "$LOC_LIMIT" ]; then over=1; fi
           if [ "$files" -gt "$FILES_LIMIT" ]; then over=1; fi

           # Find any prior sticky comment authored by github-actions[bot].
           existing_id="$(gh api \
               "repos/${REPO}/issues/${PR_NUMBER}/comments" --paginate \
               --jq "[.[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"${MARKER}\")))] | .[0].id // empty")"
+
+          # Opt-out via label.
+          if printf '%s' ",${PR_LABELS}," | grep -q ',size:override,'; then
+            if [ -n "$existing_id" ]; then
+              echo "size:override label present; deleting sticky comment ${existing_id}."
+              gh api -X DELETE "repos/${REPO}/issues/comments/${existing_id}" >/dev/null
+            else
+              echo "size:override label present; nothing to do."
+            fi
+            exit 0
+          fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-size-soft-cap.yml around lines 51 - 67, The early exit
when the "size:override" label is present skips removing any prior sticky
advisory comment; change the opt-out branch so it still looks up and removes an
existing sticky comment before exiting: keep the label check for PR_LABELS, but
if present run the same lookup that assigns existing_id (using MARKER and the gh
api query that filters github-actions[bot] comments), and if existing_id is
non-empty call gh api to delete
"repos/${REPO}/issues/${PR_NUMBER}/comments/${existing_id}" (or the equivalent
gh api delete command) and only then echo the opt-out message and exit;
reference the PR_LABELS check, existing_id variable, MARKER and the gh api
comments lookup/delete logic to implement this.

if [ "$over" -eq 0 ]; then
# Under threshold. Remove a prior sticky if present so a
# shrunk PR doesn't carry a stale warning.
if [ -n "$existing_id" ]; then
echo "PR back under threshold; deleting sticky comment ${existing_id}."
gh api -X DELETE "repos/${REPO}/issues/comments/${existing_id}" >/dev/null
else
echo "Under threshold (loc=${loc}, files=${files}); nothing to do."
fi
exit 0
fi

# Over threshold — compose the sticky.
cat > /tmp/comment.md <<EOF
${MARKER}
### PR-size soft cap

This PR is over the advisory size threshold:

- **${loc}** changed lines (limit: ${LOC_LIMIT})
- **${files}** changed files (limit: ${FILES_LIMIT})

Bigger PRs collide with more open work, which under the parallel-session workflow tends to produce repeated \`attn:merge-conflict\` cycles (see #602). When practical, split into smaller PRs that each touch a focused surface.

This is **advisory only** — nothing is blocked. If the size is intentional (large refactor, module removal, generated code), apply the \`size:override\` label and this comment will be removed on the next push.
EOF

jq -Rs '{body: .}' < /tmp/comment.md > /tmp/payload.json

if [ -n "$existing_id" ]; then
echo "Updating sticky comment ${existing_id}."
gh api -X PATCH "repos/${REPO}/issues/comments/${existing_id}" \
--input /tmp/payload.json >/dev/null
else
echo "Posting sticky comment."
gh api -X POST "repos/${REPO}/issues/${PR_NUMBER}/comments" \
--input /tmp/payload.json >/dev/null
fi
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ installable release; see the roadmap in [README.md](README.md).

## [Unreleased]

### Added

- **PR-size soft-cap advisory workflow** ([#602](https://github.com/robotrocketscience/aelfrice/issues/602)). New `.github/workflows/pr-size-soft-cap.yml` posts (and updates) a sticky `<!-- pr-size-soft-cap-v1 -->` comment on PRs whose `additions + deletions > 200` or `changed_files > 3`, suggesting a split. Quiet on PRs under both thresholds; the comment is removed automatically if a previously-flagged PR is shrunk back below the line. Authors apply `size:override` to opt out (large refactors / module removals / generated code). Concurrency-1 per PR number; cancels in-progress runs on new pushes so comment edits don't race. First half of the merge-thrash mitigation in #602 — addresses the conflict-probability axis (smaller PRs collide with fewer open branches). The serialization axis (label-driven merge-train) ships in a follow-up PR.

### Performance

- **Update-check cache TTL: 6h → 15min** (`src/aelfrice/lifecycle.py:CACHE_TTL_SECONDS`). The PyPI version-check cache used to expire after six hours, so a freshly-published release could lag the user-visible "update available" banner by up to that long on a busy machine (longer on a quiet one — the check is gated behind the next CLI call or `UserPromptSubmit` hook fire). PyPI's JSON endpoint is CDN-cached and unauthenticated, so a 15-minute cadence is well within the polling-etiquette band and shrinks the worst-case banner-lag window from 6h to ~15min. Detached-subprocess + on-disk-cache architecture is unchanged.
Expand Down
Loading