diff --git a/.github/workflows/auto-rebase-health.yml b/.github/workflows/auto-rebase-health.yml new file mode 100644 index 000000000..b65395845 --- /dev/null +++ b/.github/workflows/auto-rebase-health.yml @@ -0,0 +1,114 @@ +# ───────────────────────────────────────────────────────────────────────────── +# SOURCE OF TRUTH: petry-projects/.github-private/.github/workflows/auto-rebase-health.yml +# +# AGENTS — READ BEFORE EDITING: +# • Daily Auto-rebase Health report (#737, epic #736). Counts conflict-sentinel +# fires vs. dev-lead `rebase` responses (the agentic-conflict-resolution rate) +# and estimates auto-rebase fan-out CI volume. All logic lives in +# scripts/auto_rebase_health.sh (unit-tested in tests/auto_rebase_health.bats). +# • No LLM calls — pure gh-API + jq aggregation. Keep it that way; the cost cap +# for this report is a single scheduled run/day with no per-PR work. +# • Single-repo scan: github.token (with the permissions below) suffices — +# actions:read for run telemetry, issues/pull-requests:read for the comment +# and PR scans. No cross-org PAT is required (unlike token-report.yml). +# • This is a repo-specific workflow with no org template — see AGENTS.md +# (Workflow Files exception). Do not let template syncs remove it. +# ───────────────────────────────────────────────────────────────────────────── +name: Auto-rebase Health Report + +on: + schedule: + # Daily at 07:00 UTC — between the PR-review health check (06:00) and the + # weekly token report (Mon 08:00). + - cron: '0 7 * * *' + workflow_dispatch: + inputs: + lookback_days: + description: "Days of history to scan for sentinels and run telemetry" + required: false + default: "7" + type: string + +permissions: + contents: read + issues: write + pull-requests: read + actions: read + +concurrency: + group: auto-rebase-health + cancel-in-progress: true + +jobs: + report: + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + LOOKBACK_DAYS: ${{ inputs.lookback_days || '7' }} + + steps: + - name: Checkout agent repo + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Generate auto-rebase health report + env: + GH_TOKEN: ${{ github.token }} + AUTO_REBASE_HEALTH_OUT: auto_rebase_health.md + run: bash scripts/auto_rebase_health.sh + + - name: Post report to tracking issue + if: hashFiles('auto_rebase_health.md') != '' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ github.token }} + script: | + const fs = require('fs'); + const MAX = 65000; + let report = fs.readFileSync('auto_rebase_health.md', 'utf8'); + if (!report.trim()) { core.setFailed('Report file is empty'); return; } + + const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; + if (report.length > MAX) { + report = report.slice(0, MAX) + + `\n\n---\n_Report truncated — full output in the [workflow run summary](${runUrl})._`; + } + + const LABEL = 'auto-rebase-health'; + const TITLE = '🔁 Auto-rebase Health — Daily Report'; + + // Ensure the tracking label exists. + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: LABEL }); + } catch (e) { + await github.rest.issues.createLabel({ + owner: context.repo.owner, repo: context.repo.repo, + name: LABEL, color: '1d76db', + description: 'Daily auto-rebase instrumentation report', + }); + } + + // Find-or-create the single pinned tracking issue. + const existing = await github.paginate(github.rest.issues.listForRepo, { + owner: context.repo.owner, repo: context.repo.repo, + labels: LABEL, state: 'open', per_page: 100, + }); + + let issue = existing.find(i => i.title === TITLE); + if (!issue) { + const created = await github.rest.issues.create({ + owner: context.repo.owner, repo: context.repo.repo, + title: TITLE, labels: [LABEL], + body: 'Tracking issue for the daily Auto-rebase Health report. ' + + 'Each run posts a new comment below with the agentic-conflict-resolution ' + + 'rate and the auto-rebase fan-out CI-volume estimate.\n\n' + + 'Source: `scripts/auto_rebase_health.sh` · `.github/workflows/auto-rebase-health.yml`', + }); + issue = created.data; + core.notice(`Created tracking issue #${issue.number}: ${issue.html_url}`); + } + + const comment = await github.rest.issues.createComment({ + owner: context.repo.owner, repo: context.repo.repo, + issue_number: issue.number, body: report, + }); + core.notice(`Posted auto-rebase health report: ${comment.data.html_url}`); diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 423aa9a61..9f4720b41 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -44,6 +44,7 @@ jobs: bats \ tests/fleet_report.bats \ tests/token_report.bats \ + tests/auto_rebase_health.bats \ tests/model_pricing.bats \ tests/test_review_cycle.bats \ tests/test_ci_status.bats \ diff --git a/AGENTS.md b/AGENTS.md index 8cb53ca61..1127499f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,6 +56,14 @@ This is the `.github-private` org infrastructure repo for `petry-projects`. It c path, which the thin-caller `auto-rebase.yml` and central reusable cannot cover. It must not be removed by template syncs. If the org template gains a retry equivalent, remove this exception and defer to the template instead. +- **Exception:** `auto-rebase-health.yml` (#737, epic #736) is a documented repo-specific workflow with no + corresponding org template in `standards/workflows/`. It is a daily (≤1/day) + `workflow_dispatch` report + that quantifies the agentic-conflict-resolution rate (conflict-sentinel fires vs. dev-lead `rebase` + responses, both HTML-comment markers GitHub search cannot index) and estimates auto-rebase fan-out CI + volume from `auto-rebase.yml` run telemetry. It is a no-LLM gh-API + jq report (logic in + `scripts/auto_rebase_health.sh`, tests in `tests/auto_rebase_health.bats`) and runs on `github.token` + only — no cross-org PAT. It must not be removed by template syncs. If the org template gains an + auto-rebase instrumentation equivalent, remove this exception and defer to the template instead. ### Agent Profiles (`agents/*.md`) diff --git a/scripts/auto_rebase_health.sh b/scripts/auto_rebase_health.sh new file mode 100644 index 000000000..7c60e90f7 --- /dev/null +++ b/scripts/auto_rebase_health.sh @@ -0,0 +1,221 @@ +#!/usr/bin/env bash +# auto_rebase_health.sh — Auto-rebase instrumentation report (#737, epic #736). +# +# Measures two things the maintainer needs to make the Merge Queue vs. auto-rebase +# decision with concrete numbers instead of estimates: +# +# 1. Agentic-conflict-resolution rate — how often the auto-rebase conflict +# sentinel (`` +# posted by scripts/dev-lead-fix-reviews.sh (any status) +# * applied — the `status=applied` subset (resolved & force-pushed) +# +# 2. Fan-out CI-run volume — the `Auto-rebase non-Dependabot PRs` workflow +# (.github/workflows/auto-rebase.yml) runs per push to main and fans +# branch-update CI re-runs onto behind PRs. Per-run behind-PR counts are not +# 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. +# +# 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. +# * main() does the network I/O: token selection, comment + run telemetry pulls. +# +# Env vars consumed: +# GH_TOKEN — primary token (needs actions:read + repo read on this repo) +# 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) +# 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}" +AUTO_REBASE_WORKFLOW="auto-rebase.yml" + +# Markers (kept in one place so a rename in the dev-lead scripts is a one-line fix). +SENTINEL_MARKER='\nNeeds rebase.","created_at":"2026-06-10T01:00:00Z"}, + {"body":"\nNeeds rebase.","created_at":"2026-06-10T02:00:00Z"}, + {"body":"\nNeeds rebase.","created_at":"2026-06-11T03:00:00Z"}, + {"body":"\nNeeds rebase.","created_at":"2026-06-11T04:00:00Z"}, + {"body":"\nRebase completed and pushed.","created_at":"2026-06-10T01:05:00Z"}, + {"body":"\nRebase completed and pushed.","created_at":"2026-06-10T02:05:00Z"}, + {"body":"\nApplication logic conflicts require human resolution.","created_at":"2026-06-11T03:05:00Z"}, + {"body":"LGTM, thanks!","created_at":"2026-06-11T05:00:00Z"} + ]' + + # Auto-rebase run telemetry: 5 runs (4 success, 1 failure). + RUNS_JSON='[ + {"conclusion":"success","created_at":"2026-06-10T00:00:00Z"}, + {"conclusion":"success","created_at":"2026-06-10T06:00:00Z"}, + {"conclusion":"failure","created_at":"2026-06-10T12:00:00Z"}, + {"conclusion":"success","created_at":"2026-06-11T00:00:00Z"}, + {"conclusion":"success","created_at":"2026-06-11T06:00:00Z"} + ]' +} + +# --------------------------------------------------------------------------- +# count_marker +# --------------------------------------------------------------------------- + +@test "count_marker: counts comments whose body contains the substring" { + run count_marker "$COMMENTS_JSON" "