From 535c0fe9813bf38d671a0562a2f3566a28438043 Mon Sep 17 00:00:00 2001 From: Jacob Heider Date: Sun, 25 Oct 2026 15:34:12 -0400 Subject: [PATCH] complain: reopen matching issues instead of filing duplicates The complain action found existing issues through the search API only, filtered to open issues, with fuzzy in:title matching. A rerun that failed after its issue was labeled (gha-issue, 404) and closed filed a new issue, and two platforms failing together could both miss each other's fresh issue because the search index lags. Now it matches titles exactly, from the REST issue list (consistent immediately) plus search (for older issues). It logs to the oldest open match; otherwise it reopens the latest match closed as completed and drops its triage label so relabeling closes it again; one closed as not_planned (yanked, old-version) gets the log without reopening. After filing a new issue it re-lists, and if an older open issue with the same title exists, closes its own as a duplicate and logs there. Log comments now name the failing platform and link the run attempt. Exercised: shellcheck, actionlint, and DRY_RUN=1 against live pantry issues (open-duplicate, reopen with 404 and gha-issue labels, fuzzy near-miss title, unknown package). Not exercised: the real writes, which only run when a build fails in CI. Co-Authored-By: Claude Opus 5.5 --- .github/actions/complain/action.yml | 38 +++---------- .github/actions/complain/complain.sh | 83 ++++++++++++++++++++++++++++ .github/workflows/pkg-platform.yml | 1 + 3 files changed, 91 insertions(+), 31 deletions(-) create mode 100755 .github/actions/complain/complain.sh diff --git a/.github/actions/complain/action.yml b/.github/actions/complain/action.yml index dae283ae53..6bcba72153 100644 --- a/.github/actions/complain/action.yml +++ b/.github/actions/complain/action.yml @@ -5,6 +5,9 @@ inputs: pkg: description: project to complain about required: true + platform: + description: platform that failed, noted in the log comment + required: false token: description: github token required: true @@ -19,37 +22,10 @@ runs: shell: bash run: sleep $(( RANDOM % 120 )) - - name: Find or Create Issue - id: issue + - name: Find, Reopen or Create Issue shell: bash env: GH_TOKEN: ${{ inputs.token }} - TITLE: '❌ build issues: ${{ inputs.pkg }}' - run: | - issue_number=$(gh api --method GET search/issues \ - -f q="repo:${GITHUB_REPOSITORY} is:issue is:open label:build-failure in:title ${TITLE}" \ - -f per_page=1 \ - --jq '.items[0].number // empty') - - if test -z "$issue_number"; then - issue_number=$(gh api "repos/${GITHUB_REPOSITORY}/issues" \ - -f title="$TITLE" \ - -f body='Running log of build failure for ${{ inputs.pkg }}' \ - -f labels[]=build-failure \ - -f assignees[]=jhheider \ - --jq '.number') - fi - - echo "number=$issue_number" >> "$GITHUB_OUTPUT" - - - name: Log Comment - shell: bash - env: - GH_TOKEN: ${{ inputs.token }} - ISSUE_NUMBER: ${{ steps.issue.outputs.number }} - run: | - gh api "repos/${GITHUB_REPOSITORY}/issues/${ISSUE_NUMBER}/comments" \ - -f body="# Build failure - ## ${{ inputs.pkg }} - - logs: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + PKG: ${{ inputs.pkg }} + PLATFORM: ${{ inputs.platform }} + run: ${{ github.action_path }}/complain.sh diff --git a/.github/actions/complain/complain.sh b/.github/actions/complain/complain.sh new file mode 100755 index 0000000000..24bbdb66f6 --- /dev/null +++ b/.github/actions/complain/complain.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# finds (or reopens, or creates) the build-failure issue for $PKG and logs this failure on it +# DRY_RUN=1 prints the writes instead of making them +set -euo pipefail + +TITLE="❌ build issues: $PKG" +LOGS="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/attempts/${GITHUB_RUN_ATTEMPT:-1}" +TRIAGE_LABELS='["gha-issue","404","version-lockstep-required"]' + +write() { + if [ -n "${DRY_RUN:-}" ]; then + echo "DRY_RUN: gh $*" >&2 + echo 0 + else + gh "$@" + fi +} + +# exact-title matches as "number state state_reason", oldest first. +# the REST list is consistent immediately (sees issues created seconds ago); +# search reaches older issues, but its index lags, so it can't be the only source. +matches() { + { + gh api "repos/$GITHUB_REPOSITORY/issues?labels=build-failure&state=all&sort=created&direction=desc&per_page=100" --jq '.[]' + gh api -X GET search/issues \ + -f q="repo:$GITHUB_REPOSITORY is:issue label:build-failure in:title \"$TITLE\"" \ + -f per_page=100 --jq '.items[]' || + echo "::warning::issue search failed; only recent issues were checked" >&2 + } | jq -rs --arg t "$TITLE" ' + map(select(.title == $t and .pull_request == null)) + | unique_by(.number) | .[] + | "\(.number) \(.state) \(.state_reason // "")"' +} + +oldest_open() { awk '$2 == "open" { print $1; exit }'; } + +found=$(matches) +issue=$(oldest_open <<<"$found") +note="" + +if [ -z "$issue" ]; then + read -r closed reason < <(awk '$2 == "closed" { n = $1; r = $3 } END { print n, r }' <<<"$found") + if [ -n "$closed" ] && [ "$reason" != "not_planned" ]; then + # closed as transient (gha-issue, 404…) or fixed; it failed again, so reopen it. + # drop the triage label so applying it again closes the issue again + issue=$closed + write api -X PATCH "repos/$GITHUB_REPOSITORY/issues/$issue" -f state=open >/dev/null + for label in $(gh api "repos/$GITHUB_REPOSITORY/issues/$issue/labels" \ + --jq ".[].name | select(IN(${TRIAGE_LABELS}[]))"); do + write api -X DELETE "repos/$GITHUB_REPOSITORY/issues/$issue/labels/$label" >/dev/null + done + note="Reopened: this failed again after the issue was closed." + elif [ -n "$closed" ]; then + # closed as not planned (yanked, old-version): log it there without reopening + issue=$closed + else + issue=$(write api "repos/$GITHUB_REPOSITORY/issues" \ + -f title="$TITLE" \ + -f body="Running log of build failure for $PKG" \ + -f labels[]=build-failure \ + -f assignees[]=jhheider \ + --jq '.number') + + # another platform's complain job may have created one at the same moment; + # the oldest open issue wins and the rest close as duplicates + sleep $(( 5 + RANDOM % 10 )) + winner=$(matches | oldest_open) + if [ -n "$winner" ] && [ "$winner" != "$issue" ]; then + write api "repos/$GITHUB_REPOSITORY/issues/$issue/comments" -f body="Duplicate of #$winner" >/dev/null + write api -X PATCH "repos/$GITHUB_REPOSITORY/issues/$issue" \ + -f state=closed -f state_reason=not_planned >/dev/null + issue=$winner + fi + fi +fi + +echo "logging to #$issue" >&2 +write api "repos/$GITHUB_REPOSITORY/issues/$issue/comments" -f body="# Build failure +## $PKG${PLATFORM:+ ($PLATFORM)} +${note:+ +$note +} +logs: $LOGS" >/dev/null diff --git a/.github/workflows/pkg-platform.yml b/.github/workflows/pkg-platform.yml index 0c7d250b3f..8c28027401 100644 --- a/.github/workflows/pkg-platform.yml +++ b/.github/workflows/pkg-platform.yml @@ -306,3 +306,4 @@ jobs: - uses: ./.github/actions/complain with: pkg: ${{ fromJSON(inputs.pkg).project }}=${{ fromJSON(inputs.pkg).version.value }} + platform: ${{ inputs.name }}