Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
5 changes: 5 additions & 0 deletions .github/workflows/dev-lead-retry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ name: Dev-Lead Retry (rate-limited)
# whose registered reviewer-bot issue comment has no covering disposition (and no
# info_status_pattern match) gets one deduplicated fix-bot-comment retry per scan,
# addressed by the comment's node id (scan_pr_for_undispositioned_bot_comments).
#
# Each target repo runs its harness at its OWN dev-lead.yml pin, so the sweep
# skips (with a ::warning::) any repo pinned to an older channel than the sweep's.
Comment thread
don-petry marked this conversation as resolved.
Resolved
# Such a repo could be sent a client_payload field it cannot read yet (#2086,
# scripts/lib/dispatch-channel-gate.sh).
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

on:
schedule:
Expand Down
31 changes: 30 additions & 1 deletion scripts/dev-lead-retry.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ set -euo pipefail
# DISPATCH_DELAY_SEC — seconds between repo dispatches (default: 30) to
# prevent cascading org-wide rate-limit hits
# DRY_RUN — if "true", log what would be dispatched but don't send
# SWEEP_AGENT_REF — override the sweep's dev-lead channel for the #2086
# per-repo gate (default: this repo's dev-lead.yml pin)
# NOW_ISO — override current time for testing (ISO-8601 UTC)
# BOT_COMMENT_RETRY_CLAIM_SETTLE_SEC — seconds to wait after posting a
# bot-comment retry marker before re-listing markers
Expand Down Expand Up @@ -78,6 +80,13 @@ set -euo pipefail
# threads with no reply from our automation, and dispatches one deduplicated
# fix-reviews pass per PR (lib/bot-thread-retry.sh). Threads that exhaust their
# attempts get a single visible notice, so the stall is never silent.
#
# Each target repo is gated on its own dev-lead.yml pin (#2086). The sweep runs one
# release of scripts/ (this repo's pinned channel), but every target runs its
# harness at its own pin. A repo pinned to an older channel than the sweep could
# be sent a client_payload field its harness cannot read, so main() skips it with
# a ::warning:: (lib/dispatch-channel-gate.sh). If the sweep's own pin cannot be
# read, the gate is off and every repo is scanned, as before.

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Escalation gate (#946): pr_has_escalation_label / NEEDS_HUMAN_REVIEW_LABEL.
Expand All @@ -96,6 +105,9 @@ source "$SCRIPT_DIR/lib/bot-comment-retry.sh"
# Unreplied bot review-thread retry decision + thread fetch (#2046).
# shellcheck source=lib/bot-thread-retry.sh
source "$SCRIPT_DIR/lib/bot-thread-retry.sh"
# Per-target-repo channel gate (#2086): dcg_resolve_sweep_ref / dcg_repo_dispatch_allowed.
# shellcheck source=lib/dispatch-channel-gate.sh
source "$SCRIPT_DIR/lib/dispatch-channel-gate.sh"

TARGET_ORG="${TARGET_ORG:-petry-projects}"
DELEGATION_ORGS="${DELEGATION_ORGS:-}"
Expand Down Expand Up @@ -1257,8 +1269,24 @@ main() {
local repo_count="${#all_repos[@]}"
echo "[retry] scanning ${repo_count} repo(s) across org(s)"

local repo_index=0
# Gate each target on its own dev-lead.yml pin (#2086). If the sweep's own pin
# is unreadable the gate is off: failing closed here would silence the whole
# safety net on one transient read.
local sweep_ref="" sweep_status=0
sweep_ref="$(dcg_resolve_sweep_ref)" || sweep_status=$?
if [ "$sweep_status" -eq 0 ]; then
echo "[retry] sweep channel: ${sweep_ref} (repos pinned older are skipped)"
else
sweep_ref=""
echo "::warning::[retry] cannot resolve the sweep's dev-lead channel from ${DCG_HOST_REPO}'s dev-lead.yml; per-repo channel gate disabled (#2086)" >&2
fi

local repo_index=0 skipped=0
for repo in "${all_repos[@]}"; do
if [ -n "$sweep_ref" ] && ! dcg_repo_dispatch_allowed "$repo" "$sweep_ref"; then
skipped=$(( skipped + 1 ))
continue
fi
if [ "$repo_index" -gt 0 ] && [ "$DISPATCH_DELAY_SEC" -gt 0 ]; then
# Stagger dispatches to avoid hammering the rate-limited API simultaneously
echo "[retry] waiting ${DISPATCH_DELAY_SEC}s before next repo (stagger)..."
Expand All @@ -1267,6 +1295,7 @@ main() {
scan_repo "$repo"
repo_index=$(( repo_index + 1 ))
done
[ "$skipped" -eq 0 ] || echo "[retry] skipped ${skipped} repo(s) on the channel gate"

echo "[retry] done at $(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
Expand Down
121 changes: 121 additions & 0 deletions scripts/lib/dispatch-channel-gate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# dispatch-channel-gate.sh — skip a dispatch target whose dev-lead channel pin is
# older than the sweep's (#2086).
#
# WHY THIS EXISTS
# dev-lead-retry.yml runs scripts/ at ONE release: the channel this repo's
# dev-lead.yml pins via `agent_ref` (#2050). The sweep then sends
# repository_dispatch to every repo in the org, and each target runs its
# harness at ITS OWN dev-lead.yml pin (the stub contract allows a per-repo
# ring/channel pin). A target pinned to an older channel than the sweep can be
# sent a client_payload field its harness cannot read yet — the #2017 / #2050
# skew again, across repos instead of across time.
#
# WHAT THIS LIBRARY DECIDES
# For each target repo, read its dev-lead.yml `agent_ref` and compare it with
# the sweep's channel by git ancestry (the compare API on the dev-lead host
# repo), not by tag name — rings of one major sit on different commits, and a
# rollback can move a channel backwards:
# identical | behind the target has every commit the sweep has → scan
# ahead | diverged the sweep has commits the target lacks → SKIP, warn
# no dev-lead.yml nothing in that repo receives a dispatch → skip
# unreadable / unknown cannot prove compatibility → SKIP, warn (fail closed)
# A skipped repo keeps its markers; its retries resume once it is repinned to
# the sweep's channel or newer (or the sweep's channel moves back to it).
#
# Env:
# DCG_HOST_REPO repo that hosts the dev-lead reusable, its channel tags and
# the sweep's own dev-lead.yml (default: $GITHUB_REPOSITORY,
# else petry-projects/.github-private)
# SWEEP_AGENT_REF override the sweep's channel (default: DCG_HOST_REPO's pin —
# the same ref dev-lead-retry.yml checks scripts/ out at)

DCG_HOST_REPO="${DCG_HOST_REPO:-${GITHUB_REPOSITORY:-petry-projects/.github-private}}"
# Same channel-tag shape dev-lead-retry.yml accepts for the sweep's own pin.
DCG_CHANNEL_RE='^dev-lead/v[0-9]+-(next|ring0|ring1|stable)$'
# target agent_ref -> 0 (compatible) | 1 (older) | 2 (unknown), per sweep run.
declare -gA _DCG_COMPAT_CACHE=()

# dcg_parse_agent_ref: stdin = dev-lead.yml text; prints its `agent_ref` channel
# tag. Returns 1 (prints nothing) when it is missing or not a channel tag.
dcg_parse_agent_ref() {
local ref
ref="$(sed -nE "s/^[[:space:]]*agent_ref:[[:space:]]*[\"']?([^\"'[:space:]#]*).*/\1/p" | head -n 1)"
[[ "$ref" =~ $DCG_CHANNEL_RE ]] || return 1
printf '%s\n' "$ref"
}

# dcg_read_repo_pin <repo>: prints the repo's dev-lead.yml agent_ref (default
# branch). Returns 2 when the repo has no dev-lead.yml, 1 on any other read
# failure or a malformed pin.
dcg_read_repo_pin() {
local repo="$1" out gh_status=0
out="$(gh api "repos/${repo}/contents/.github/workflows/dev-lead.yml" \
-H "Accept: application/vnd.github.raw" 2>&1)" || gh_status=$?
if [ "$gh_status" -ne 0 ]; then
if [[ "$out" == *"HTTP 404"* ]]; then
Comment thread
don-petry marked this conversation as resolved.
Resolved
return 2
fi
return 1
fi
dcg_parse_agent_ref <<<"$out"
}

# dcg_resolve_sweep_ref: prints the channel the sweep's scripts/ run at.
dcg_resolve_sweep_ref() {
if [ -n "${SWEEP_AGENT_REF:-}" ]; then
[[ "$SWEEP_AGENT_REF" =~ $DCG_CHANNEL_RE ]] || return 1
printf '%s\n' "$SWEEP_AGENT_REF"
return 0
fi
dcg_read_repo_pin "$DCG_HOST_REPO"
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
Comment thread
don-petry marked this conversation as resolved.
Resolved
}

# dcg_pin_compat <target_ref> <sweep_ref>: 0 = the target's channel has every
# commit the sweep's has; 1 = it is older (or diverged); 2 = unknown. Cached.
dcg_pin_compat() {
local target="$1" sweep="$2" status rc cmp_status=0
[ "$target" = "$sweep" ] && return 0
if [ -n "${_DCG_COMPAT_CACHE[$target]+set}" ]; then
return "${_DCG_COMPAT_CACHE[$target]}"
fi
status="$(gh api "repos/${DCG_HOST_REPO}/compare/${target}...${sweep}" --jq '.status' 2>/dev/null)" || cmp_status=$?
if [ "$cmp_status" -ne 0 ]; then
status=""
fi
case "$status" in
identical|behind) rc=0 ;;
ahead|diverged) rc=1 ;;
*) rc=2 ;;
esac
_DCG_COMPAT_CACHE[$target]="$rc"
return "$rc"
}

# dcg_repo_dispatch_allowed <repo> <sweep_ref>: 0 = scan and dispatch to the
# repo; 1 = skip it. Logs the reason to stderr.
dcg_repo_dispatch_allowed() {
local repo="$1" sweep="$2" target rc=0
target="$(dcg_read_repo_pin "$repo")" || rc=$?
case "$rc" in
0) ;;
2)
echo "[retry] skipping ${repo}: no dev-lead.yml, so nothing receives a dispatch" >&2
return 1 ;;
*)
echo "::warning::[retry] skipping ${repo}: cannot read a dev-lead channel from its dev-lead.yml agent_ref, so payload compatibility with the sweep's ${sweep} is unproven (#2086)" >&2
return 1 ;;
esac

rc=0
dcg_pin_compat "$target" "$sweep" || rc=$?
case "$rc" in
0) return 0 ;;
1)
echo "::warning::[retry] skipping ${repo}: its dev-lead.yml pins ${target}, older than the sweep's ${sweep}; its harness may not read the sweep's dispatch payloads (#2086). Repin it to ${sweep} or newer." >&2
return 1 ;;
*)
echo "::warning::[retry] skipping ${repo}: could not compare its pin ${target} with the sweep's ${sweep} (#2086)" >&2
return 1 ;;
esac
}
Loading
Loading