Skip to content

Bugfix Version Index - pull_request_target 78022 #3077

Bugfix Version Index - pull_request_target 78022

Bugfix Version Index - pull_request_target 78022 #3077

name: Bugfix Version Index
run-name: Bugfix Version Index - ${{ github.event_name }} ${{ github.event.number || github.ref_name || github.event.schedule || 'run' }}
# Maintains the deterministic code_kb.versions index (fix side) + the heuristic
# code_kb.task_summaries cause cache, via the github-bugfix-versions plugin run directly with
# `claude -p` on the [ubuntu, ai] runner. The runner checks out this repo, so the plugin's
# version_backfill.py has a real clone for `git tag --contains` (no REST-only path, no container
# mount). Events:
#
# ① origin bugfix PR merges to main → :index-fix (seed versions) + :affected-versions (cache cause)
# ② a backport PR merges to branch-X.Y* → :index-fix (rebuild the origin's versions record)
# ③ a release tag X.Y.Z is pushed → :index-fix <tag> (resolve pending-<line> → concrete tag)
# ④ daily schedule → :index-fix reconcile (pending-only, versions/fix side)
# + :affected-versions reconcile (cause cache self-heal)
# ⑤ weekly schedule → :index-fix reconcile-all (re-check concrete too — corrects
# a wrong concrete tag / reverse error; heavier)
# ⑥ manual dispatch (migrate-enrichment) → :affected-versions migrate-enrichment (one-off: backfill
# legacy cause records up to the rich schema — severity /
# cause author+title / ## Summary + ## User Impact body;
# dispatch-only, separate from the daily correctness sweep)
#
# NOTE: plugin commands MUST be namespaced (/github-bugfix-versions:<cmd>). A bare /index-fix in
# headless `-p` does NOT resolve — it is silently treated as prompt text (no write).
#
# The index steps FAIL LOUD: no `|| true`, and `defaults.run.shell: bash` forces `-o pipefail`
# so a failing `claude ... | tee` propagates claude's non-zero exit (tee's success does not mask it).
# The cause step (affected-versions) stays best-effort; the daily `affected-versions reconcile` job
# (④) re-selects any origin still cause-missing/partial/stale, so a transient origin-merge cause
# failure self-heals on a later run (bounded + per-record backoff — see the skill's Reconcile mode).
#
# PREREQUISITES on the [ubuntu, ai] runner (one-time, infra):
# - `gh` (authenticated via GITHUB_TOKEN), `git`, `python3` on PATH — version_backfill.py uses gh
# for PR/backport data and `git tag --contains` against the checkout to resolve concrete tags;
# - `claude` with the `github-bugfix-versions` plugin installed and the `context-base` MCP
# configured with `code_kb` USAGE (read+write) for the runner's identity;
# - `code_kb.versions` (collection_type=knowledge) and `code_kb.task_summaries`
# (collection_type=task_summary) collections pre-created by an admin.
on:
pull_request_target:
branches:
- main
# release lines. Use [0-9]+ (one-or-more), NOT bare [0-9] (single digit) — else multi-digit
# versions like branch-3.12, branch-3.5.20, branch-3.12.20 silently never match the filter.
- 'branch-[0-9]+.[0-9]+'
- 'branch-[0-9]+.[0-9]+.[0-9]+'
types:
- closed
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
schedule:
- cron: '0 19 * * *' # daily 19:00 UTC (03:00 Asia/Shanghai) → reconcile (pending-only)
- cron: '0 20 * * 0' # weekly Sun 20:00 UTC (Mon 04:00 Asia/Shanghai) → reconcile-all (concrete too)
workflow_dispatch: # manual trigger — run a self-heal sweep on demand, don't only wait for cron
inputs:
mode:
description: 'Sweep to run'
type: choice
required: true
default: reconcile
options:
- reconcile # = daily: index-fix pending sweep + affected-versions cause self-heal
- reconcile-all # = weekly: re-check concrete records too (heavier)
- migrate-enrichment # = one-off: backfill legacy cause records up to the rich schema
budget:
description: 'migrate-enrichment only: per-run analysis cap (integer, e.g. 20; blank = low default). Clamped to 50 in the skill.'
type: string
required: false
default: ''
# Concurrency is per-ORIGIN for the single-origin EVENTS (origin-merge / backport-merge share
# `BVI-…-origin-N`, so different origins run in parallel but the same origin serializes — a
# whole-entity upsert can't be lost). The BULK jobs (tag-cut / reconcile / reconcile-all) use their
# OWN group, so they are NOT serialized against a same-origin backport-merge. That race (a bulk run
# overwriting a backport just written by an event) is handled in the plugin, NOT here: the monotonic
# guard keeps a concrete earliest_fix_tag from reverting to pending, and the anti-clobber guard drops
# any origin whose stored record was updated after the sweep started (so a mid-sweep backport-merge
# write is never clobbered). A shared group is deliberately NOT used — it would make different-origin
# backport-writes cancel each other (only one pending kept), dropping writes.
permissions:
contents: read
pull-requests: read
defaults:
run:
shell: bash # GitHub runs this as `bash --noprofile --norc -eo pipefail {0}` → pipefail makes
# `claude ... | tee` fail the step when claude fails (tee's exit no longer masks it).
jobs:
# ① origin bugfix PR merged to main → seed versions (fix side) + cache cause (task_summaries).
origin-merge:
if: >
github.event_name == 'pull_request_target' &&
github.event.action == 'closed' &&
github.event.pull_request.merged == true &&
github.base_ref == 'main' &&
contains(github.event.pull_request.title, '[BugFix]') &&
!startsWith(github.head_ref, 'mergify/') &&
!contains(github.head_ref, '-sync-pr-')
concurrency:
group: BVI-${{ github.repository }}-origin-${{ github.event.number }}
cancel-in-progress: false
runs-on: [ubuntu, ai]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
PR_URL: ${{ github.event.pull_request.html_url }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: fetch release branches
run: git fetch origin '+refs/heads/branch-*:refs/remotes/origin/branch-*' || true
- name: index-fix (deterministic fix side → code_kb.versions) [FAIL-LOUD]
run: |
echo "origin-merge PR #${{ github.event.number }} → ${PR_URL}"
claude --dangerously-skip-permissions \
-p "/github-bugfix-versions:index-fix ${PR_URL}" 2>&1 | tee "${RUNNER_TEMP}/index_fix.txt"
- name: affected-versions (heuristic cause → code_kb.task_summaries) [best-effort]
if: always()
continue-on-error: true
run: |
claude --dangerously-skip-permissions \
-p "/github-bugfix-versions:affected-versions ${PR_URL}" 2>&1 | tee "${RUNNER_TEMP}/affected_versions.txt" || true
# ② a backport PR merged to a release branch → rebuild EACH origin it backports.
backport-resolve:
if: >
github.event_name == 'pull_request_target' &&
github.event.action == 'closed' &&
github.event.pull_request.merged == true &&
startsWith(github.base_ref, 'branch-') &&
contains(github.event.pull_request.title, 'backport') &&
!contains(github.head_ref, '-sync-pr-')
runs-on: [ubuntu, ai]
outputs:
origins: ${{ steps.parse.outputs.origins }}
steps:
- name: parse all (backport #N) markers → origin list
id: parse
env:
TITLE: ${{ github.event.pull_request.title }}
run: |
json="$(printf '%s' "$TITLE" | grep -oiE 'backport #[0-9]+' | grep -oE '[0-9]+' | sort -un \
| awk 'BEGIN{printf "["} {printf "%s\"%s\"",(NR>1?",":""),$0} END{printf "]"}')"
echo "origins=$json" >> "$GITHUB_OUTPUT"
echo "backport PR #${{ github.event.number }} (${{ github.base_ref }}) → origins: $json"
backport-write:
needs: backport-resolve
if: ${{ needs.backport-resolve.outputs.origins != '[]' && needs.backport-resolve.outputs.origins != '' }}
strategy:
fail-fast: false
matrix:
origin: ${{ fromJSON(needs.backport-resolve.outputs.origins) }}
concurrency:
group: BVI-${{ github.repository }}-origin-${{ matrix.origin }}
cancel-in-progress: false
runs-on: [ubuntu, ai]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: fetch release branches
run: git fetch origin '+refs/heads/branch-*:refs/remotes/origin/branch-*' || true
- name: index-fix (rebuild origin ${{ matrix.origin }} from its merged backports) [FAIL-LOUD]
run: |
echo "rebuild origin ${{ github.repository }}#${{ matrix.origin }} (triggered by backport PR #${{ github.event.number }})"
claude --dangerously-skip-permissions \
-p "/github-bugfix-versions:index-fix ${{ github.repository }}#${{ matrix.origin }}" 2>&1 | tee "${RUNNER_TEMP}/index_fix_${{ matrix.origin }}.txt"
# ③ release tag cut → resolve pending-<line> records on that line to the concrete tag.
tag-cut:
if: github.event_name == 'push'
concurrency:
group: BVI-${{ github.repository }}-tagcut-${{ github.ref_name }}
cancel-in-progress: false
runs-on: [ubuntu, ai]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
TAG: ${{ github.ref_name }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: fetch release branches
run: git fetch origin '+refs/heads/branch-*:refs/remotes/origin/branch-*' || true
- name: index-fix (tag-cut → concrete tag for this line) [FAIL-LOUD]
run: |
echo "tag-cut ${TAG}"
claude --dangerously-skip-permissions \
-p "/github-bugfix-versions:index-fix ${TAG}" 2>&1 | tee "${RUNNER_TEMP}/index_fix.txt"
# ④ daily self-heal → pending-only sweep across all maintained lines.
reconcile:
if: >
(github.event_name == 'schedule' && github.event.schedule == '0 19 * * *') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.mode == 'reconcile')
concurrency:
group: BVI-${{ github.repository }}-reconcile
cancel-in-progress: false
runs-on: [ubuntu, ai]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: fetch release branches
run: git fetch origin '+refs/heads/branch-*:refs/remotes/origin/branch-*' || true
- name: index-fix reconcile (re-sweep pending on all maintained lines) [FAIL-LOUD]
run: |
echo "daily reconcile (versions / fix side)"
claude --dangerously-skip-permissions \
-p "/github-bugfix-versions:index-fix reconcile" 2>&1 | tee "${RUNNER_TEMP}/reconcile.txt"
- name: affected-versions reconcile (cause cache self-heal) [best-effort]
if: always()
continue-on-error: true
run: |
# Cause-side self-heal: fill/refresh task_summaries for in-scope bugfixes whose cause is
# missing/partial/stale (e.g. an origin-merge cause step that failed best-effort, or a
# historical gap). Bounded per run (N=30d priority / MAX_FULL_ANALYSES=12 default hard cap /
# R=3 reserved backlog / MAX_CONCURRENCY=4), with per-record backoff so a poison record can't
# starve the quota — see the skill. Rich-schema backfill of legacy records is a SEPARATE mode
# (migrate-enrichment, job ⑥), not this daily correctness sweep.
# Best-effort + `|| true`: cause is heuristic; a transient blip must not fail the FAIL-LOUD
# versions reconcile above, and tomorrow's run re-selects anything left cause-incomplete.
echo "daily reconcile (cause side)"
claude --dangerously-skip-permissions \
-p "/github-bugfix-versions:affected-versions reconcile" 2>&1 | tee "${RUNNER_TEMP}/affected_reconcile.txt" || true
# ⑤ weekly deep self-heal → re-check concrete records too, correcting a wrong concrete tag
# (reverse error like #74855: stored 4.1.3 but really 4.1.2). Heavier — every maintained-line
# origin is rebuilt from GitHub — so weekly, off-peak, not on the daily cron.
reconcile-all:
if: >
(github.event_name == 'schedule' && github.event.schedule == '0 20 * * 0') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.mode == 'reconcile-all')
concurrency:
group: BVI-${{ github.repository }}-reconcile-all
cancel-in-progress: false
runs-on: [ubuntu, ai]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: fetch release branches
run: git fetch origin '+refs/heads/branch-*:refs/remotes/origin/branch-*' || true
- name: index-fix reconcile-all (re-check concrete + pending on all maintained lines) [FAIL-LOUD]
run: |
echo "weekly reconcile-all"
claude --dangerously-skip-permissions \
-p "/github-bugfix-versions:index-fix reconcile-all" 2>&1 | tee "${RUNNER_TEMP}/reconcile_all.txt"
# ⑥ manual one-off → backfill legacy cause records up to the rich schema (severity / cause author+title /
# ## Summary + ## User Impact body). SEPARATE from the daily correctness reconcile (④) so it never
# competes for that budget; dispatch-only (no cron). Bounded in the skill: MAX_FULL_ANALYSES default 3,
# raised via the `budget` input and clamped to 50. Best-effort (cause is heuristic).
migrate-enrichment:
if: >
github.event_name == 'workflow_dispatch' &&
github.event.inputs.mode == 'migrate-enrichment'
concurrency:
group: BVI-${{ github.repository }}-migrate-enrichment
cancel-in-progress: false
runs-on: [ubuntu, ai]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
RUN_BUDGET: ${{ github.event.inputs.budget }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: fetch release branches
run: git fetch origin '+refs/heads/branch-*:refs/remotes/origin/branch-*' || true
- name: validate budget input [FAIL-LOUD]
# A free-form dispatch input flows into a `--dangerously-skip-permissions` `claude -p` prompt as
# $ARGUMENTS. It is not a shell-injection vector (double-quoted env expansion), but an arbitrary
# string would be PROMPT injection, and a plain typo would be silently masked to a green no-op by
# the best-effort `|| true` below. So validate FIRST, in its own step WITHOUT continue-on-error:
# empty (= low default) is fine; otherwise it MUST be digits only. A bad value fails the job (red),
# and the best-effort step below is then skipped.
run: |
if [ -n "${RUN_BUDGET}" ] && ! [[ "${RUN_BUDGET}" =~ ^[0-9]+$ ]]; then
echo "::error::budget must be empty or a non-negative integer (got '${RUN_BUDGET}'); the skill clamps it to 50"
exit 1
fi
- name: affected-versions migrate-enrichment (rich-schema backfill) [best-effort]
continue-on-error: true
run: |
echo "migrate-enrichment (budget='${RUN_BUDGET}')"
claude --dangerously-skip-permissions \
-p "/github-bugfix-versions:affected-versions migrate-enrichment ${RUN_BUDGET}" 2>&1 \
| tee "${RUNNER_TEMP}/migrate_enrichment.txt" || true