-
Notifications
You must be signed in to change notification settings - Fork 1
feat(auto-rebase-health): measure post-restriction fan-out reduction (#739) #890
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f88eda4
92ed98d
98fc98e
ba5571f
bac6a27
56f9b19
2c40288
6ae0435
7fd9a4c
b3fd981
ff20d9e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -21,6 +21,15 @@ | |||||||||||||||||||||||||||||||||||||
| # logged in this repo (the update-branch calls live in the central reusable), | ||||||||||||||||||||||||||||||||||||||
| # so the re-run volume is an ESTIMATE: runs × current open non-Dependabot PRs. | ||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||
| # 3. Post-restriction fan-out — since petry-projects/.github#468 (issue #465) the | ||||||||||||||||||||||||||||||||||||||
| # reusable only update-branches *review-ready* PRs (non-draft AND (current | ||||||||||||||||||||||||||||||||||||||
| # APPROVED review OR the `auto-rebase:ready` label)). To show the reduction the | ||||||||||||||||||||||||||||||||||||||
| # report mirrors the same predicate against the current open PRs and reports the | ||||||||||||||||||||||||||||||||||||||
| # eligible-PR multiplier, the restricted re-run estimate, and the reduction vs. | ||||||||||||||||||||||||||||||||||||||
| # the unrestricted (all-behind-PRs) multiplier. This is the cleaner before/after | ||||||||||||||||||||||||||||||||||||||
| # signal feeding the Merge Queue go/no-go record (#739). Like (2) it is a | ||||||||||||||||||||||||||||||||||||||
| # snapshot ESTIMATE — eligibility is read now, not per historical run. | ||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||
| # Layout (mirrors scripts/token_report.sh): | ||||||||||||||||||||||||||||||||||||||
| # * The count_*/summarize_*/fmt_*/render_report functions are PURE — they take | ||||||||||||||||||||||||||||||||||||||
| # JSON / scalars and write to stdout. Unit-tested in tests/auto_rebase_health.bats. | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -31,13 +40,17 @@ | |||||||||||||||||||||||||||||||||||||
| # GH_PAT_FALLBACK — optional fallback PAT if GH_TOKEN lacks run-telemetry access | ||||||||||||||||||||||||||||||||||||||
| # AGENT_REPO — repo to scan (default: petry-projects/.github-private) | ||||||||||||||||||||||||||||||||||||||
| # LOOKBACK_DAYS — days of history to consider (default: 7) | ||||||||||||||||||||||||||||||||||||||
| # READY_LABEL — label that opts a non-draft PR into auto-rebase without an | ||||||||||||||||||||||||||||||||||||||
| # approval; must match the reusable's `ready_label` input | ||||||||||||||||||||||||||||||||||||||
| # (default: auto-rebase:ready) | ||||||||||||||||||||||||||||||||||||||
| # AUTO_REBASE_HEALTH_OUT — optional path; report is written there in addition to stdout | ||||||||||||||||||||||||||||||||||||||
| # GITHUB_STEP_SUMMARY — written by the Actions runner when present | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| set -euo pipefail | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| WORKFLOW_REPO="${AGENT_REPO:-petry-projects/.github-private}" | ||||||||||||||||||||||||||||||||||||||
| LOOKBACK_DAYS="${LOOKBACK_DAYS:-7}" | ||||||||||||||||||||||||||||||||||||||
| READY_LABEL="${READY_LABEL:-auto-rebase:ready}" | ||||||||||||||||||||||||||||||||||||||
| AUTO_REBASE_WORKFLOW="auto-rebase.yml" | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| # Markers (kept in one place so a rename in the dev-lead scripts is a one-line fix). | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -110,11 +123,58 @@ fmt_rate() { | |||||||||||||||||||||||||||||||||||||
| echo "$(( num * 100 / denom ))%" | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| # render_report <comments_json> <runs_json> <lookback_days> <behind_prs> [today] | ||||||||||||||||||||||||||||||||||||||
| # fmt_reduction <from> <to> | ||||||||||||||||||||||||||||||||||||||
| # Integer percentage DECREASE going from <from> to <to> (e.g. 7→3 = "57%"). | ||||||||||||||||||||||||||||||||||||||
| # Zero/negative <from> guard renders "n/a" (no PRs to reduce). Mirrors the | ||||||||||||||||||||||||||||||||||||||
| # behind→eligible multiplier reduction that the eligibility gate buys. | ||||||||||||||||||||||||||||||||||||||
| fmt_reduction() { | ||||||||||||||||||||||||||||||||||||||
| local from="${1:-0}" to="${2:-0}" | ||||||||||||||||||||||||||||||||||||||
| if [ "$from" -le 0 ]; then | ||||||||||||||||||||||||||||||||||||||
| echo "n/a" | ||||||||||||||||||||||||||||||||||||||
| return 0 | ||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||
| echo "$(( (from - to) * 100 / from ))%" | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| # pr_has_current_approval <reviews_json> | ||||||||||||||||||||||||||||||||||||||
| # Returns 0 if a PR currently has at least one APPROVED review, else 1. Mirrors | ||||||||||||||||||||||||||||||||||||||
| # petry-projects/.github .github/scripts/auto-rebase/lib/eligibility.sh exactly: | ||||||||||||||||||||||||||||||||||||||
| # the reviewer's most recent decision review wins (a later CHANGES_REQUESTED or | ||||||||||||||||||||||||||||||||||||||
| # DISMISSED cancels an earlier APPROVED; COMMENTED/PENDING do not change a stance). | ||||||||||||||||||||||||||||||||||||||
| # We read real review states rather than reviewDecision, which is null on repos | ||||||||||||||||||||||||||||||||||||||
| # without required reviews (issue #465 implementer note). | ||||||||||||||||||||||||||||||||||||||
| pr_has_current_approval() { | ||||||||||||||||||||||||||||||||||||||
| local json="${1:-}" result | ||||||||||||||||||||||||||||||||||||||
| [ -n "$json" ] || json='[]' | ||||||||||||||||||||||||||||||||||||||
| result=$(printf '%s' "$json" | jq -r ' | ||||||||||||||||||||||||||||||||||||||
| reduce (.[] | select((.state == "APPROVED" or .state == "CHANGES_REQUESTED" or .state == "DISMISSED") and .user?.login != null)) as $r ({}; .[$r.user.login] = $r.state) | ||||||||||||||||||||||||||||||||||||||
| | any(. == "APPROVED")') | ||||||||||||||||||||||||||||||||||||||
| [ "$result" = "true" ] | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| # count_eligible <prs_meta_json> <ready_label> | ||||||||||||||||||||||||||||||||||||||
| # Counts review-ready PRs in a metadata array. Each element is | ||||||||||||||||||||||||||||||||||||||
| # {"draft":bool,"approved":bool,"labels":[{"name":...}]}; a PR is eligible when it | ||||||||||||||||||||||||||||||||||||||
| # is non-draft AND (approved OR carries <ready_label>). Same predicate as the | ||||||||||||||||||||||||||||||||||||||
| # reusable's `review-ready` mode. Absent/empty JSON → 0. | ||||||||||||||||||||||||||||||||||||||
| count_eligible() { | ||||||||||||||||||||||||||||||||||||||
| local json="${1:-}" label="${2:-}" | ||||||||||||||||||||||||||||||||||||||
| [ -n "$json" ] || json='[]' | ||||||||||||||||||||||||||||||||||||||
| printf '%s' "$json" | jq --arg L "$label" ' | ||||||||||||||||||||||||||||||||||||||
| [ .[] | ||||||||||||||||||||||||||||||||||||||
| | select(((.draft // false) | not) | ||||||||||||||||||||||||||||||||||||||
| and ((.approved // false) or any(.labels[]?; .name == $L))) ] | ||||||||||||||||||||||||||||||||||||||
| | length' | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+160
to
+168
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can make the
Suggested change
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in scripts/auto_rebase_health.sh |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| # render_report <comments_json> <runs_json> <lookback_days> <behind_prs> [today] [eligible_prs] [ready_label] | ||||||||||||||||||||||||||||||||||||||
| # Writes the full Markdown report to stdout. Pure: no network. | ||||||||||||||||||||||||||||||||||||||
| # When <eligible_prs> is supplied (non-empty), the post-restriction section is | ||||||||||||||||||||||||||||||||||||||
| # rendered; omit it to render the legacy two-section report. | ||||||||||||||||||||||||||||||||||||||
| render_report() { | ||||||||||||||||||||||||||||||||||||||
| local comments_json="${1:-[]}" runs_json="${2:-[]}" | ||||||||||||||||||||||||||||||||||||||
| local lookback="${3:-7}" behind="${4:-0}" today="${5:-}" | ||||||||||||||||||||||||||||||||||||||
| local eligible="${6:-}" ready_label="${7:-auto-rebase:ready}" | ||||||||||||||||||||||||||||||||||||||
| [ -n "$today" ] || today="$(date -u +%Y-%m-%d)" | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| local sentinels responses applied | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -149,6 +209,30 @@ render_report() { | |||||||||||||||||||||||||||||||||||||
| "$runs_per_day" "$rerun_per_day" | ||||||||||||||||||||||||||||||||||||||
| printf '> Fan-out is an **estimate** — per-run behind-PR counts are not logged in this repo, ' | ||||||||||||||||||||||||||||||||||||||
| printf 'so re-runs = runs × current open non-Dependabot PR count.\n' | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| # Post-restriction section — only when an eligible-PR count is supplied. | ||||||||||||||||||||||||||||||||||||||
| [ -n "$eligible" ] || return 0 | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| local restricted reduction restricted_per_day verdict | ||||||||||||||||||||||||||||||||||||||
| restricted="$(estimate_fanout "$total" "$eligible")" | ||||||||||||||||||||||||||||||||||||||
| reduction="$(fmt_reduction "$behind" "$eligible")" | ||||||||||||||||||||||||||||||||||||||
| restricted_per_day="$(awk -v t="$restricted" -v d="$lookback" 'BEGIN { printf "%.1f", (d > 0 ? t / d : 0) }')" | ||||||||||||||||||||||||||||||||||||||
| # ≥50% multiplier reduction is the epic #736 success metric. | ||||||||||||||||||||||||||||||||||||||
| if [ "$behind" -gt 0 ] && [ "$(( (behind - eligible) * 100 / behind ))" -ge 50 ]; then | ||||||||||||||||||||||||||||||||||||||
| verdict="✅ met" | ||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||
| verdict="❌ not met" | ||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| printf '\n## Post-restriction fan-out (review-ready eligibility)\n\n' | ||||||||||||||||||||||||||||||||||||||
| printf -- '- **Eligible PRs** (non-draft AND (current `APPROVED` review OR `%s` label)): %s of %s open non-Dependabot PRs\n' \ | ||||||||||||||||||||||||||||||||||||||
| "$ready_label" "$eligible" "$behind" | ||||||||||||||||||||||||||||||||||||||
| printf -- '- **Estimated branch-update CI re-runs (restricted)**: ~%s (~%s/day)\n' \ | ||||||||||||||||||||||||||||||||||||||
| "$restricted" "$restricted_per_day" | ||||||||||||||||||||||||||||||||||||||
| printf -- '- **Fan-out reduction** (behind→eligible multiplier): %s\n' "$reduction" | ||||||||||||||||||||||||||||||||||||||
| printf -- '- **≥50%% reduction success metric** (epic #736): %s\n\n' "$verdict" | ||||||||||||||||||||||||||||||||||||||
| printf '> Reduction is a point-in-time snapshot: eligibility is read now, not per ' | ||||||||||||||||||||||||||||||||||||||
| printf 'historical run, so the multiplier (not the absolute run count) is the like-for-like signal.\n' | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -196,15 +280,40 @@ main() { | |||||||||||||||||||||||||||||||||||||
| --paginate --jq '.workflow_runs | map({conclusion, created_at})' 2>/dev/null \ | ||||||||||||||||||||||||||||||||||||||
| | jq -s 'add // []' 2>/dev/null || echo '[]')" | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| # 3. Behind-PR multiplier — open non-Dependabot PRs (proxy for branches the | ||||||||||||||||||||||||||||||||||||||
| # fan-out updates). Best-effort; defaults to 0 so the report still renders. | ||||||||||||||||||||||||||||||||||||||
| local behind | ||||||||||||||||||||||||||||||||||||||
| behind="$(gh pr list --repo "$WORKFLOW_REPO" --state open --limit 200 --json author \ | ||||||||||||||||||||||||||||||||||||||
| --jq '[.[] | select((.author?.login // "") | test("dependabot"; "i") | not)] | length' \ | ||||||||||||||||||||||||||||||||||||||
| 2>/dev/null || echo 0)" | ||||||||||||||||||||||||||||||||||||||
| # 3. Behind-PR multiplier — fetch isDraft and labels in the same list call so | ||||||||||||||||||||||||||||||||||||||
| # we need only one reviews call per PR (not two). Best-effort; empty list → | ||||||||||||||||||||||||||||||||||||||
| # behind=0 so it still renders. | ||||||||||||||||||||||||||||||||||||||
| local open_prs behind | ||||||||||||||||||||||||||||||||||||||
| open_prs="$(gh pr list --repo "$WORKFLOW_REPO" --state open --limit 200 \ | ||||||||||||||||||||||||||||||||||||||
| --json number,author,isDraft,labels \ | ||||||||||||||||||||||||||||||||||||||
| --jq '[.[] | select((.author?.login // "") | test("dependabot"; "i") | not)]' \ | ||||||||||||||||||||||||||||||||||||||
| 2>/dev/null || echo '[]')" | ||||||||||||||||||||||||||||||||||||||
| behind="$(printf '%s' "$open_prs" | jq 'length')" | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| # 4. Eligible-PR multiplier — the review-ready subset the gate now updates. For | ||||||||||||||||||||||||||||||||||||||
| # each open non-Dependabot PR, read the current approval state (reviews call), | ||||||||||||||||||||||||||||||||||||||
| # then apply the same predicate as the reusable. No CI re-runs and no LLM cost. | ||||||||||||||||||||||||||||||||||||||
| # Best-effort: any failure leaves eligible empty so the post-restriction | ||||||||||||||||||||||||||||||||||||||
| # section is simply omitted rather than reported wrongly. | ||||||||||||||||||||||||||||||||||||||
| local eligible="" prs_meta | ||||||||||||||||||||||||||||||||||||||
| if [ "$behind" -gt 0 ]; then | ||||||||||||||||||||||||||||||||||||||
| local recs="[]" n draft labels reviews_json approved rec | ||||||||||||||||||||||||||||||||||||||
| while IFS=$'\t' read -r n draft labels; do | ||||||||||||||||||||||||||||||||||||||
| [ -n "$n" ] || continue | ||||||||||||||||||||||||||||||||||||||
| reviews_json="$(gh api --paginate "repos/${WORKFLOW_REPO}/pulls/${n}/reviews" 2>/dev/null \ | ||||||||||||||||||||||||||||||||||||||
| | jq -s 'add // []' 2>/dev/null || echo '[]')" | ||||||||||||||||||||||||||||||||||||||
| if pr_has_current_approval "$reviews_json"; then approved=true; else approved=false; fi | ||||||||||||||||||||||||||||||||||||||
| rec="$(jq -nc --argjson d "$draft" --argjson a "$approved" --argjson l "$labels" \ | ||||||||||||||||||||||||||||||||||||||
| '{draft:$d, approved:$a, labels:$l}')" | ||||||||||||||||||||||||||||||||||||||
| recs="$(printf '%s' "$recs" | jq -c --argjson r "$rec" '. + [$r]')" | ||||||||||||||||||||||||||||||||||||||
| done < <(printf '%s' "$open_prs" | jq -r '.[] | "\(.number)\t\(.isDraft)\t\(.labels | tostring)"') | ||||||||||||||||||||||||||||||||||||||
| prs_meta="$recs" | ||||||||||||||||||||||||||||||||||||||
| eligible="$(count_eligible "$prs_meta" "$READY_LABEL" 2>/dev/null || echo "")" | ||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| local report | ||||||||||||||||||||||||||||||||||||||
| report="$(render_report "$comments_json" "$runs_json" "$LOOKBACK_DAYS" "$behind" "$today")" | ||||||||||||||||||||||||||||||||||||||
| report="$(render_report "$comments_json" "$runs_json" "$LOOKBACK_DAYS" "$behind" "$today" \ | ||||||||||||||||||||||||||||||||||||||
| "$eligible" "$READY_LABEL")" | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| printf '%s\n' "$report" | ||||||||||||||||||||||||||||||||||||||
| if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a review is submitted by a deleted user, the
.userobject in the GitHub API response can benull. Attempting to access.user.logindirectly will causejqto throw aCannot index null with string "login"error. We should safely navigate.userand ensure.user?.loginis not null before indexing.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in scripts/auto_rebase_health.sh
pr_has_current_approval: added.user?.login != nullto theselectpredicate so reviews from deleted users (where.useris null) are filtered out before thereduceaccesses.user.login, preventing theCannot index null with string "login"jq error. Added two new bats tests covering the null-user case in tests/auto_rebase_health.bats.