|
| 1 | +#!/usr/bin/env bash |
| 2 | +# |
| 3 | +# Open, update, and close the one GitHub issue that tracks "master is red". |
| 4 | +# |
| 5 | +# Why this exists: on 2026-08-10 a commit bumped the parser version in pom.xml |
| 6 | +# and missed the three connector POMs. CI caught it *within the same minute* -- |
| 7 | +# and it stayed broken for 21 hours, through a second failing run, because a red |
| 8 | +# build on a repository nobody is actively watching is a grey checkmark in a list |
| 9 | +# that nobody loads. The check was never the weak part; noticing was. |
| 10 | +# |
| 11 | +# So a failure gets something that follows you: an issue, assigned to whoever |
| 12 | +# triggered the run, which stays open until master is actually green again and |
| 13 | +# closes itself when it is. |
| 14 | +# |
| 15 | +# Called by .github/workflows/red-master.yml. Reads its input from the |
| 16 | +# environment so the workflow_run payload and a manual dry run can both drive it: |
| 17 | +# |
| 18 | +# CONCLUSION success | failure | cancelled | ... |
| 19 | +# BRANCH branch the run was on; anything but master is ignored |
| 20 | +# RUN_NAME display name of the workflow that finished |
| 21 | +# RUN_URL link to it |
| 22 | +# RUN_ID its id, used to avoid commenting twice about one run |
| 23 | +# SHA commit it ran against |
| 24 | +# ACTOR who to assign; best effort, a non-assignable login is not fatal |
| 25 | +# DRY_RUN 1 to print every mutation instead of making it |
| 26 | +# |
| 27 | +# Requires gh, authenticated with issues:write and actions:read. |
| 28 | + |
| 29 | +set -uo pipefail |
| 30 | + |
| 31 | +LABEL="ci-red" |
| 32 | +TITLE="CI is red on master" |
| 33 | +# The workflows whose result decides whether master is green. Must match the |
| 34 | +# `name:` of each, and the list in red-master.yml's `workflows:` trigger. |
| 35 | +WATCHED=("Build and test" "Nightly") |
| 36 | + |
| 37 | +CONCLUSION="${CONCLUSION:-}" |
| 38 | +BRANCH="${BRANCH:-}" |
| 39 | +RUN_NAME="${RUN_NAME:-a workflow}" |
| 40 | +RUN_URL="${RUN_URL:-}" |
| 41 | +RUN_ID="${RUN_ID:-}" |
| 42 | +SHA="${SHA:-}" |
| 43 | +ACTOR="${ACTOR:-}" |
| 44 | +DRY_RUN="${DRY_RUN:-0}" |
| 45 | + |
| 46 | +say() { printf '%s\n' "$*"; } |
| 47 | + |
| 48 | +# Every mutation goes through here, so --dry-run cannot half-apply. |
| 49 | +run() { |
| 50 | + if [ "$DRY_RUN" = "1" ]; then |
| 51 | + say "DRY RUN would: $*" |
| 52 | + return 0 |
| 53 | + fi |
| 54 | + "$@" |
| 55 | +} |
| 56 | + |
| 57 | +if [ "$BRANCH" != "master" ]; then |
| 58 | + say "run was on '$BRANCH', not master; nothing to do" |
| 59 | + exit 0 |
| 60 | +fi |
| 61 | + |
| 62 | +short_sha="${SHA:0:7}" |
| 63 | + |
| 64 | +# The open tracker, if there is one. `gh issue list` filters to open by default; |
| 65 | +# being explicit because this decides whether we create or comment. |
| 66 | +existing=$(gh issue list --label "$LABEL" --state open --limit 1 \ |
| 67 | + --json number --jq '.[0].number // empty' 2>/dev/null) |
| 68 | + |
| 69 | +case "$CONCLUSION" in |
| 70 | +failure|timed_out) |
| 71 | + if [ -z "$existing" ]; then |
| 72 | + # The label may not exist yet on a fresh repository. --force makes this |
| 73 | + # idempotent instead of failing the second time. |
| 74 | + run gh label create "$LABEL" \ |
| 75 | + --color B60205 \ |
| 76 | + --description "master is failing CI" \ |
| 77 | + --force >/dev/null 2>&1 || true |
| 78 | + |
| 79 | + body="**\`$RUN_NAME\` failed on \`master\`.** |
| 80 | +
|
| 81 | +- commit: \`$short_sha\` |
| 82 | +- run: $RUN_URL |
| 83 | +
|
| 84 | +This issue is opened automatically by [\`red-master.yml\`](https://github.com/${GITHUB_REPOSITORY:-sqlparser/gsp_demo_java}/blob/master/.github/workflows/red-master.yml) the first time a run fails on \`master\`, and closes itself once every watched workflow is green again. Later failures are added as comments rather than as new issues. |
| 85 | +
|
| 86 | +Please do not close it by hand while master is still red -- a closed issue is how the last one went unnoticed for 21 hours." |
| 87 | + |
| 88 | + if [ "$DRY_RUN" = "1" ]; then |
| 89 | + say "DRY RUN would: create issue '$TITLE' (label $LABEL, assignee ${ACTOR:-none})" |
| 90 | + say "--- body ---" |
| 91 | + say "$body" |
| 92 | + say "------------" |
| 93 | + else |
| 94 | + # `gh issue create` prints the new issue's URL; there is no --json |
| 95 | + # on it. Assign in a second call: an actor who is not a repository |
| 96 | + # collaborator cannot be assigned, and that must not cost us the |
| 97 | + # issue itself. |
| 98 | + num=$(gh issue create --title "$TITLE" --label "$LABEL" --body "$body" \ |
| 99 | + | sed -n 's|.*/issues/\([0-9][0-9]*\).*|\1|p') |
| 100 | + if [ -z "$num" ]; then |
| 101 | + say "::error::could not create the tracking issue" |
| 102 | + exit 1 |
| 103 | + fi |
| 104 | + say "opened #$num" |
| 105 | + if [ -n "$ACTOR" ] && [[ "$ACTOR" != *"[bot]" ]]; then |
| 106 | + gh issue edit "$num" --add-assignee "$ACTOR" >/dev/null 2>&1 \ |
| 107 | + && say "assigned to @$ACTOR" \ |
| 108 | + || say "could not assign @$ACTOR (not a collaborator?); left unassigned" |
| 109 | + fi |
| 110 | + fi |
| 111 | + else |
| 112 | + # One comment per failing run, not per re-read of the same one. |
| 113 | + if [ -n "$RUN_ID" ] && gh issue view "$existing" --json comments \ |
| 114 | + --jq '.comments[].body' 2>/dev/null | grep -qF "/runs/$RUN_ID"; then |
| 115 | + say "#$existing already mentions run $RUN_ID; not commenting twice" |
| 116 | + exit 0 |
| 117 | + fi |
| 118 | + run gh issue comment "$existing" --body \ |
| 119 | +"Still red: **\`$RUN_NAME\`** failed on \`$short_sha\`. |
| 120 | +
|
| 121 | +$RUN_URL" |
| 122 | + say "commented on #$existing" |
| 123 | + fi |
| 124 | + ;; |
| 125 | + |
| 126 | +success) |
| 127 | + if [ -z "$existing" ]; then |
| 128 | + say "master is green and no tracking issue is open; nothing to do" |
| 129 | + exit 0 |
| 130 | + fi |
| 131 | + |
| 132 | + # One green run does not mean master is green: build.yml can pass while the |
| 133 | + # nightly is still failing. Close only when the newest completed run of |
| 134 | + # every watched workflow succeeded. |
| 135 | + still_red="" |
| 136 | + for wf in "${WATCHED[@]}"; do |
| 137 | + latest=$(gh run list --workflow "$wf" --branch master --status completed \ |
| 138 | + --limit 1 --json conclusion --jq '.[0].conclusion // empty' 2>/dev/null) |
| 139 | + say " latest completed '$wf' on master: ${latest:-none}" |
| 140 | + case "$latest" in |
| 141 | + ""|success|skipped|neutral) ;; |
| 142 | + *) still_red="$still_red $wf" ;; |
| 143 | + esac |
| 144 | + done |
| 145 | + |
| 146 | + if [ -n "$still_red" ]; then |
| 147 | + say "leaving #$existing open;$still_red is still failing" |
| 148 | + exit 0 |
| 149 | + fi |
| 150 | + |
| 151 | + run gh issue comment "$existing" --body \ |
| 152 | +"Green again: every watched workflow passed on \`$short_sha\`. |
| 153 | +
|
| 154 | +$RUN_URL" |
| 155 | + run gh issue close "$existing" --reason completed |
| 156 | + say "closed #$existing" |
| 157 | + ;; |
| 158 | + |
| 159 | +*) |
| 160 | + say "conclusion '$CONCLUSION' is neither success nor failure; ignoring" |
| 161 | + ;; |
| 162 | +esac |
0 commit comments