Files
mattermost/.github/scripts/conflicts-babysit.sh
T
Maria A NunezandCursor b0ddc5a13a Add Conflicts Babysit workflow to dispatch conflicting PRs to a Cursor agent (#39272)
* Add Conflicts Babysit workflow to dispatch conflicting labeled PRs to Cursor

On every push to master, find open PRs labeled AI/ConflictsBabysit whose
mergeable state is CONFLICTING (retrying briefly while GitHub reports
UNKNOWN) and POST each one to the Cursor automation webhook.

Co-authored-by: Maria A Nunez <maria.nunez@mattermost.com>

* Remove workflow_dispatch trigger from Conflicts Babysit

Manual dispatch can target any branch, which would run that branch's
version of the script with the webhook secrets.

Co-authored-by: Maria A Nunez <maria.nunez@mattermost.com>

* Make Conflicts Babysit best-effort so it never fails master CI

Webhook and PR search failures are reported as warnings and in the job
summary instead of failing the run; checkout and dispatch steps are
continue-on-error as a backstop.

Co-authored-by: Maria A Nunez <maria.nunez@mattermost.com>

* Report Conflicts Babysit failures hidden by continue-on-error

Skip dispatch when checkout fails, add a step that warns and writes the
job summary when checkout or dispatch fails, and write the job summary
when the PR search fails.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-20 09:32:50 -04:00

151 lines
4.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Sends open PRs labeled $BABYSIT_LABEL that conflict with $BASE_REF to the
# Cursor automation webhook. Used by .github/workflows/conflicts-babysit.yml.
set -euo pipefail
: "${GITHUB_REPOSITORY:?}" "${WEBHOOK_URL:?}" "${WEBHOOK_TOKEN:?}"
LABEL="${BABYSIT_LABEL:-AI/ConflictsBabysit}"
BASE_REF="${BASE_REF:-master}"
BASE_SHA="${BASE_SHA:-}"
RUN_URL="${RUN_URL:-}"
INITIAL_DELAY_SECONDS="${INITIAL_DELAY_SECONDS:-15}"
UNKNOWN_MAX_ATTEMPTS="${UNKNOWN_MAX_ATTEMPTS:-5}"
UNKNOWN_BACKOFF_SECONDS="${UNKNOWN_BACKOFF_SECONDS:-5}"
SUMMARY_FILE="${GITHUB_STEP_SUMMARY:-/dev/null}"
OWNER="${GITHUB_REPOSITORY%%/*}"
REPO="${GITHUB_REPOSITORY#*/}"
search_prs() {
# shellcheck disable=SC2016
gh api graphql --paginate \
-f searchQuery="repo:${GITHUB_REPOSITORY} is:pr is:open base:${BASE_REF} label:\"${LABEL}\"" \
-f query='
query($searchQuery: String!, $endCursor: String) {
search(query: $searchQuery, type: ISSUE, first: 50, after: $endCursor) {
pageInfo { hasNextPage endCursor }
nodes {
... on PullRequest {
number url mergeable headRefName headRefOid isCrossRepository
}
}
}
}' \
--jq '.data.search.nodes[] | [.number, .url, .headRefName, .headRefOid, .mergeable, .isCrossRepository] | @tsv'
}
fetch_mergeable() {
# shellcheck disable=SC2016
gh api graphql \
-f owner="$OWNER" -f name="$REPO" -F number="$1" \
-f query='
query($owner: String!, $name: String!, $number: Int!) {
repository(owner: $owner, name: $name) {
pullRequest(number: $number) { mergeable }
}
}' \
--jq '.data.repository.pullRequest.mergeable' || echo UNKNOWN
}
dispatch() {
local number="$1" payload
payload=$(jq -n \
--arg repository "$GITHUB_REPOSITORY" \
--argjson pr_number "$number" \
--arg pr_url "${url[$number]}" \
--arg head_ref "${head_ref[$number]}" \
--arg head_sha "${head_sha[$number]}" \
--arg base_ref "$BASE_REF" \
--arg base_sha "$BASE_SHA" \
--arg label "$LABEL" \
--arg run_url "$RUN_URL" \
'$ARGS.named')
curl -fsS --retry 3 --retry-all-errors --max-time 30 -o /dev/null \
-X POST \
-H "Authorization: Bearer ${WEBHOOK_TOKEN}" \
-H "Content-Type: application/json" \
--data "$payload" \
"$WEBHOOK_URL"
}
declare -A url head_ref head_sha state action
numbers=()
unknown=()
sleep "$INITIAL_DELAY_SECONDS"
if ! candidates=$(search_prs); then
echo "::warning::Searching for PRs labeled ${LABEL} failed; will retry on the next ${BASE_REF} merge."
{
echo "## Conflicts babysit"
echo ""
echo "Searching for PRs labeled \`${LABEL}\` failed, so no PRs were processed. Will retry on the next \`${BASE_REF}\` merge."
} >> "$SUMMARY_FILE"
exit 0
fi
while IFS=$'\t' read -r number pr_url ref sha mergeable cross_repo; do
[ -n "$number" ] || continue
numbers+=("$number")
url[$number]="$pr_url"
head_ref[$number]="$ref"
head_sha[$number]="$sha"
state[$number]="$mergeable"
if [ "$cross_repo" = "true" ]; then
action[$number]="skipped (fork)"
elif [ "$mergeable" = "UNKNOWN" ]; then
unknown+=("$number")
fi
done <<< "$candidates"
for ((attempt = 1; attempt < UNKNOWN_MAX_ATTEMPTS && ${#unknown[@]} > 0; attempt++)); do
sleep $((attempt * UNKNOWN_BACKOFF_SECONDS))
still_unknown=()
for number in "${unknown[@]}"; do
state[$number]=$(fetch_mergeable "$number")
if [ "${state[$number]}" = "UNKNOWN" ]; then
still_unknown+=("$number")
fi
done
unknown=("${still_unknown[@]}")
done
failures=0
for number in "${numbers[@]}"; do
[ -z "${action[$number]:-}" ] || continue
case "${state[$number]}" in
CONFLICTING)
if dispatch "$number"; then
action[$number]="dispatched"
else
action[$number]="failed"
failures=$((failures + 1))
echo "::warning::Webhook call failed for #${number}; will retry on the next ${BASE_REF} merge."
fi
;;
UNKNOWN)
action[$number]="unknown"
echo "::warning::Mergeable state for #${number} still UNKNOWN after ${UNKNOWN_MAX_ATTEMPTS} attempts; will retry on the next ${BASE_REF} merge."
;;
*)
action[$number]="skipped"
;;
esac
done
{
echo "## Conflicts babysit"
echo ""
if [ "${#numbers[@]}" -eq 0 ]; then
echo "No open PRs labeled \`${LABEL}\` targeting \`${BASE_REF}\`."
else
echo "| PR | Mergeable | Action |"
echo "| --- | --- | --- |"
for number in "${numbers[@]}"; do
echo "| [#${number}](${url[$number]}) | ${state[$number]} | ${action[$number]} |"
done
fi
} | tee -a "$SUMMARY_FILE"
if [ "$failures" -gt 0 ]; then
echo "::warning::${failures} webhook call(s) failed."
fi