From 2e299324ea83e753be60a2b44c0b3542f00e9a76 Mon Sep 17 00:00:00 2001 From: noctuum <25441068+noctuum@users.noreply.github.com> Date: Fri, 16 Oct 2026 05:51:52 +0500 Subject: [PATCH] fix(workflow): match open issues by exact title --- .github/workflows/check-updates.yml | 6 ++- docs/WORKFLOW.md | 2 +- tests/workflows.bats | 67 +++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check-updates.yml b/.github/workflows/check-updates.yml index 33d04fa..41a5fef 100644 --- a/.github/workflows/check-updates.yml +++ b/.github/workflows/check-updates.yml @@ -420,8 +420,10 @@ jobs: gh label create "auto-update" --color "d73a4a" --description "Automated update failure" 2>/dev/null || true - existing=$(gh issue list --label "auto-update" --search "$pkg_name failed" --state open --json number --jq 'length') - if [[ "$existing" -gt 0 ]]; then + # Exact titles: search splits foo-bar into words, so a + # search for foo also finds the issue of foo-bar + titles=$(gh issue list --label "auto-update" --state open --limit 1000 --json title --jq '.[].title') + if grep -qxF -e "❌ $pkg_name: check failed" -e "❌ $pkg_name: update failed" <<<"$titles"; then echo "Issue already open for $pkg_name, skipping" continue fi diff --git a/docs/WORKFLOW.md b/docs/WORKFLOW.md index 742abc7..26437cc 100644 --- a/docs/WORKFLOW.md +++ b/docs/WORKFLOW.md @@ -766,7 +766,7 @@ The message is `chore: update package versions [skip ci]`. `[skip ci]` keeps the The step runs with `if: always()`. For every `.status/` entry with `check_failed:` or `update_failed:`, it: 1. Creates the `auto-update` label when it is missing. -2. Skips the package when an open `auto-update` issue already matches ` failed`. +2. Skips the package when an open `auto-update` issue already has the title `❌ : check failed` or `❌ : update failed`, so a package never gets a second issue while one is open. The step compares the titles of up to 1000 open issues and uses no search, because a search splits `foo-bar` into words and finds the issue of `foo-bar` for `foo`. 3. Opens an issue titled `❌ : check failed` or `❌ : update failed`, after the status, with the package, the status, a link to the run and the error. The error holds text from upstream, a rejected tag name for example, so it stands in a code block, where links, images and mentions do not render, and is cut to its last 4000 characters. `builder` writes the body with `failure_issue_body` from `lib.sh`, so the step runs no code from the workspace as root. The step belongs to `publish`, which runs when the status table changes. A package that keeps failing gets its issue in the run where it starts failing, and a closed issue comes back only after its status changes again. diff --git a/tests/workflows.bats b/tests/workflows.bats index efbc0e5..dd095db 100644 --- a/tests/workflows.bats +++ b/tests/workflows.bats @@ -335,6 +335,73 @@ EOF [[ "$output" == *"::error::"* ]] } +# Runs the issue step of publish in a workspace with the statuses given as +# name=status pairs. gh lists the titles in $GH_TITLES, one per line, as +# every open issue whatever --search asks for, the first 30 unless --limit +# says otherwise, and logs every call to $GH_LOG. +run_issue_step() { + local ws="$BATS_TEST_TMPDIR/ws" bin="$BATS_TEST_TMPDIR/bin" pair + mkdir -p "$ws/.status" "$bin" + for pair in "$@"; do + printf '%s\n' "${pair#*=}" >"$ws/.status/${pair%%=*}" + done + cat >"$bin/gh" <<'EOF' +#!/usr/bin/env bash +printf '%s\n' "$*" >>"$GH_LOG" +[[ "$1 $2" == "issue list" ]] || exit 0 +[[ -z "${GH_FAIL:-}" ]] || exit 1 +limit=30 jq_expr="" +while (($#)); do + case "$1" in + --limit) limit=$2 && shift ;; + --jq) jq_expr=$2 && shift ;; + esac + shift +done +titles=$(grep -v '^$' <<<"${GH_TITLES:-}" | head -n "$limit") +case "$jq_expr" in + length) grep -c . <<<"$titles" ;; + ".[].title") [[ -z "$titles" ]] || printf '%s\n' "$titles" ;; + *) exit 2 ;; +esac +EOF + printf '#!/bin/sh\necho body\n' >"$bin/sudo" + chmod +x "$bin/gh" "$bin/sudo" + GH_LOG="$BATS_TEST_TMPDIR/gh.log" + : >"$GH_LOG" + step_run publish "Create issues for failed packages" >"$BATS_TEST_TMPDIR/step.sh" + run env -C "$ws" PATH="$bin:$PATH" GH_LOG="$GH_LOG" GH_TITLES="${GH_TITLES:-}" \ + GH_FAIL="${GH_FAIL:-}" RUN_URL=https://example.invalid/run sh -e "$BATS_TEST_TMPDIR/step.sh" +} + +@test "check-updates: an open issue of another package holds back no issue" { + GH_TITLES="❌ foo-bar: update failed" run_issue_step foo="update_failed: boom" + [ "$status" -eq 0 ] + grep -q '^issue create --title ❌ foo: update failed ' "$GH_LOG" +} + +@test "check-updates: an open issue of the package holds back another one" { + GH_TITLES="❌ foo: check failed" run_issue_step foo="update_failed: boom" + [ "$status" -eq 0 ] + run grep -c '^issue create' "$GH_LOG" + [ "$output" = "0" ] +} + +@test "check-updates: the issue step finds an open issue behind 30 others" { + GH_TITLES="$(printf '❌ pkg%s: check failed\n' {1..30})"$'\n❌ foo: update failed' \ + run_issue_step foo="update_failed: boom" + [ "$status" -eq 0 ] + run grep -c '^issue create' "$GH_LOG" + [ "$output" = "0" ] +} + +@test "check-updates: the issue step fails when the open issues cannot be listed" { + GH_FAIL=1 run_issue_step foo="update_failed: boom" + [ "$status" -ne 0 ] + run grep -c '^issue create' "$GH_LOG" + [ "$output" = "0" ] +} + @test "check-updates: publish takes the statuses from the check job" { run yq '.jobs.publish.steps[] | select(.with.name == "checked") | .with.path' "$CHECK_UPDATES" [ "$output" = ".status" ]