From b5ddd860d781b9cb6ba9e1ac77db5294e98d6a8f Mon Sep 17 00:00:00 2001 From: rrs <276464689+robotrocketscience@users.noreply.github.com> Date: Sun, 10 May 2026 10:13:49 -0700 Subject: [PATCH 1/2] ci(merge-thrash): add pr-size-soft-cap advisory workflow (#602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Posts a sticky marker comment on PRs whose additions+deletions > 200 or changed_files > 3, suggesting a split. Quiet under both thresholds and self-heals (removes the comment if a flagged PR shrinks back below the line). `size:override` label opts out for legitimate large diffs (refactors, module removals, generated code). Addresses the conflict-probability axis of #602's merge-thrash diagnosis — bigger PRs collide with more open branches and produce the rebase loops observed on PR #591 (3 force-pushes / 9 label-flips in 30 min) and #540 (4 force-pushes / 8 label-flips in 9 min). The serialization axis (label-driven merge-train) ships separately. --- .github/workflows/pr-size-soft-cap.yml | 105 +++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 .github/workflows/pr-size-soft-cap.yml diff --git a/.github/workflows/pr-size-soft-cap.yml b/.github/workflows/pr-size-soft-cap.yml new file mode 100644 index 000000000..31410ab0d --- /dev/null +++ b/.github/workflows/pr-size-soft-cap.yml @@ -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: '' + 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")" + + 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 < /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 From 8179bbb9af214b0203c9ac516fc0d1de485f500b Mon Sep 17 00:00:00 2001 From: rrs <276464689+robotrocketscience@users.noreply.github.com> Date: Sun, 10 May 2026 10:13:54 -0700 Subject: [PATCH 2/2] docs(changelog): unreleased entry for #602 pr-size-soft-cap workflow --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ea0167f0..ddb167023 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `` 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.