name: Notify # Opens an issue when a package fails to update on main, so it shows up in the # notifications on git.felo.gg. One issue per package: a broken package must # not hide behind another one, and a run of only some packages must not close # the issues of the others. A later failure refreshes the issue instead of # commenting, as the schedule would otherwise add one every 6 hours. The next # successful update of that package closes it again. # # Talks to the Gitea API with curl: gh only knows GitHub. on: workflow_run: workflows: [Update AUR packages] types: [completed] permissions: actions: read issues: write concurrency: group: notify cancel-in-progress: false jobs: notify: name: Notify runs-on: ubuntu-latest if: >- github.event.workflow_run.event != 'pull_request' && github.event.workflow_run.head_branch == github.event.repository.default_branch steps: - name: Open, update or close the failure issues env: TOKEN: ${{ github.token }} API: ${{ github.api_url }}/repos/${{ github.repository }} OWNER: ${{ github.repository_owner }} WORKFLOW: ${{ github.event.workflow_run.name || 'Update AUR packages' }} CONCLUSION: ${{ github.event.workflow_run.conclusion }} EVENT: ${{ github.event.workflow_run.event }} RUN_ID: ${{ github.event.workflow_run.id }} RUN_URL: ${{ github.event.workflow_run.html_url }} SHA: ${{ github.event.workflow_run.head_sha }} run: | set -euo pipefail label=ci-failure api() { local method=$1 path=$2 shift 2 curl -fsSL -X "$method" -H "Authorization: token $TOKEN" \ -H 'Content-Type: application/json' "$API$path" "$@" } issues="$(api GET "/issues?state=open&type=issues&labels=$label&limit=50")" label_id() { local id id="$(api GET '/labels?limit=50' | jq -r --arg l "$label" '.[] | select(.name == $l) | .id')" if [ -z "$id" ]; then id="$(api POST /labels -d "$(jq -cn --arg l "$label" \ '{name: $l, color: "#d73a4a", description: "A package fails to update"}')" | jq -r .id)" fi echo "$id" } # The issue title for a job: the package for "update ()", # the workflow for anything else. title_for() { if [[ $1 =~ ^update\ \((.+)\)$ ]]; then echo "${BASH_REMATCH[1]} fails to update" else echo "$WORKFLOW is failing" fi } issue_for() { jq -r --arg title "$1" '.[] | select(.title == $title) | .number' <<<"$issues" | head -1 } # The lines of the failed step leading up to its first error, enough # to see what broke without opening the log. Falls back to the end # of the log. excerpt_for() { local log log="$(api GET "/actions/jobs/$1/logs" | sed -E 's/^[0-9TZ:.-]+ //; s/\x1b\[[0-9;]*m//g')" || return 0 awk '/^(##\[group\]|::group::)Run / { out = "" } { out = out $0 "\n" } /^(##\[error\]|::error::)/ { printf "%s", out; found = 1; exit } END { if (!found) exit 1 }' <<<"$log" | tail -40 || tail -40 <<<"$log" } # report <job json, or empty if the run never started> report() { local title=$1 job=$2 line excerpt="" issue body if [ -n "$job" ]; then line="$(jq -r '"- [\(.name)](\(.html_url))" + ([.steps[]? | select(.conclusion == "failure") | .name] | if length > 0 then ": " + join(", ") else "" end)' <<<"$job")" excerpt="$(excerpt_for "$(jq -r .id <<<"$job")")" else line="The run did not start ($CONCLUSION)." fi body="$(cat <<EOF [$WORKFLOW]($RUN_URL) failed on \`${SHA:0:7}\`, started by \`$EVENT\`. $line EOF )" if [ -n "$excerpt" ]; then body="$body <details> <summary>Log</summary> \`\`\` $excerpt \`\`\` </details>" fi issue="$(issue_for "$title")" if [ -n "$issue" ]; then api PATCH "/issues/$issue" -d "$(jq -cn --arg b "$body" '{body: $b}')" >/dev/null else api POST /issues -d "$(jq -cn --arg t "$title" --arg b "@$OWNER $body" \ --argjson l "$(label_id)" '{title: $t, body: $b, labels: [$l]}')" >/dev/null fi } close() { local issue issue="$(issue_for "$1")" if [ -n "$issue" ]; then api POST "/issues/$issue/comments" \ -d "$(jq -cn --arg b "Passing again: $RUN_URL" '{body: $b}')" >/dev/null api PATCH "/issues/$issue" -d '{"state": "closed"}' >/dev/null fi } if [ "$CONCLUSION" = startup_failure ]; then report "$WORKFLOW is failing" "" exit 0 fi # Cancelled and skipped jobs say nothing about a package. page=1 while :; do jobs="$(api GET "/actions/runs/$RUN_ID/jobs?limit=50&page=$page" | jq -c '.jobs[]?')" [ -n "$jobs" ] || break while IFS= read -r -u 3 job; do title="$(title_for "$(jq -r .name <<<"$job")")" case "$(jq -r .conclusion <<<"$job")" in success) close "$title" ;; failure | timed_out) report "$title" "$job" ;; esac done 3<<<"$jobs" [ "$(wc -l <<<"$jobs")" -ge 50 ] || break page=$((page + 1)) done