From 162d8d2bf54338216ea211352d49c6801eed6076 Mon Sep 17 00:00:00 2001 From: noctuum <25441068+noctuum@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:00:11 +0500 Subject: [PATCH] feat(workflow): close issues of recovered packages The step runs in publish, before the step that opens issues. --- .github/workflows/check-updates.yml | 36 ++++++++++ docs/WORKFLOW.md | 7 +- scripts/lib.sh | 2 +- tests/publish.bats | 2 +- tests/workflows.bats | 105 ++++++++++++++++++++++++++++ 5 files changed, 148 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check-updates.yml b/.github/workflows/check-updates.yml index 380c121..99fd13e 100644 --- a/.github/workflows/check-updates.yml +++ b/.github/workflows/check-updates.yml @@ -493,6 +493,42 @@ jobs: echo "Committed: $COMMIT_SHA (verified)" + # Before the issue step: an open issue of a package holds back a new + # one, so the check issue of a package whose update now fails has to + # be closed first. The step does not run after a failed step, so a + # status that publish did not write closes nothing. + - name: Close issues of recovered packages + env: + GH_TOKEN: ${{ github.token }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + STATUS_DIR=".status" + [[ -d "$STATUS_DIR" ]] || exit 0 + + has_issues=$(gh api "repos/$GITHUB_REPOSITORY" --jq .has_issues) + [[ "$has_issues" != "false" ]] || exit 0 + open=$(gh issue list --label "auto-update" --state open --limit 1000 --json number,title --jq '.[] | "\(.number) \(.title)"') + + while read -r number title; do + [[ -n "$number" ]] || continue + for status_file in "$STATUS_DIR"/*; do + [[ -f "$status_file" ]] || continue + pkg_name=$(basename "$status_file") + [[ "$pkg_name" == _* ]] && continue + status=$(cat "$status_file") + # A check issue closes once the check passes, an update issue + # once the version is in the AUR + case "$title:$status" in + "❌ $pkg_name: check failed:check_failed:"*) continue ;; + "❌ $pkg_name: check failed:"*) ;; + "❌ $pkg_name: update failed:up_to_date" | "❌ $pkg_name: update failed:updated: "* | \ + "❌ $pkg_name: update failed:downgrade: "*) ;; + *) continue ;; + esac + gh issue close "$number" --comment "The status of $pkg_name is \`$status\` in $RUN_URL." + done + done <<<"$open" + - name: Create issues for failed packages if: always() env: diff --git a/docs/WORKFLOW.md b/docs/WORKFLOW.md index ee42a31..48d5974 100644 --- a/docs/WORKFLOW.md +++ b/docs/WORKFLOW.md @@ -721,9 +721,10 @@ The four steps that run `check-all.sh` as `builder` stop workflow commands while 7. **Publish updates:** runs `check-all.sh --publish handoff` as `builder`, with `GPG_KEY_ID`, `GIT_AUTHOR_NAME` and `GIT_AUTHOR_EMAIL`, and without `GITHUB_TOKEN`, as it runs no strategy. It keeps the statuses of `check` and checks the prepared files before it pushes, see [4.4](#44-prepare-and-publish). Workflow commands are stopped while it runs. 8. **Update README status table:** runs `update-readme.sh` as `builder`. 9. **Commit changes via API:** see [5.5](#55-verified-commit). -10. **Create issues for failed packages:** see [5.6](#56-failure-issues). +10. **Close issues of recovered packages:** see [5.6](#56-failure-issues). +11. **Create issues for failed packages:** see [5.6](#56-failure-issues). -Steps 9 and 10 run as root with `GH_TOKEN` set to the workflow token, `github.token`. +Steps 9 to 11 run as root with `GH_TOKEN` set to the workflow token, `github.token`. ### 5.4 README update @@ -788,6 +789,8 @@ For every `.status/` entry with `check_failed:` or `update_failed:`, the step: 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. +The step before it, `Close issues of recovered packages`, closes the issue of a package that recovered, with a comment that names the new status and the run. An open `❌ : check failed` closes when the status is anything but `check_failed`. An open `❌ : update failed` closes when the status is `up_to_date`, `updated` or `downgrade`, so the version is in the AUR. The close step comes first because an open issue of a package holds back a new one: a package whose check passes and whose update fails gets its check issue closed and an update issue opened in the same run. It does not run after a failed step, so a status that `publish` did not write closes nothing, and with Issues turned off it does nothing. + ### 5.7 Failed runs The issue step of `publish` reports failed packages, and a run that fails in `image` or `check` never reaches it. `notify-failure.yml` runs on `workflow_run` when a run of `Check AUR Package Updates` completes, and its job runs only when the conclusion of that run is `failure` or `timed_out`. It opens one issue titled `❌ Check AUR Package Updates failed`, with a link to the run, its branch and the names of the failed jobs, and opens no second one while that issue is open. A repository with Issues turned off gets a notice instead, as in [5.6](#56-failure-issues). The workflow has `actions: read` to list the jobs and `issues: write`, and checks out nothing. diff --git a/scripts/lib.sh b/scripts/lib.sh index bd59796..2f704ec 100644 --- a/scripts/lib.sh +++ b/scripts/lib.sh @@ -931,7 +931,7 @@ failure_issue_body() { done # The backticks are Markdown code around the package name # shellcheck disable=SC2016 - printf 'Package: `%s`\nStatus: %s\nRun: %s\n\n%s\n%s\n%s\n\nAuto-created by update workflow. Close after fixing.\n' \ + printf 'Package: `%s`\nStatus: %s\nRun: %s\n\n%s\n%s\n%s\n\nAuto-created by update workflow. A run closes it when the package recovers.\n' \ "${1##*/}" "${kind//_/ }" "$2" "$fence" "$error" "$fence" } diff --git a/tests/publish.bats b/tests/publish.bats index 16d04c0..6f12f25 100644 --- a/tests/publish.bats +++ b/tests/publish.bats @@ -1300,7 +1300,7 @@ Run: https://github.com/owner/repo/actions/runs/1 rejected '1.0.1 @octocat https://evil.example' ``` -Auto-created by update workflow. Close after fixing. +Auto-created by update workflow. A run closes it when the package recovers. EOF ) [ "$output" = "$expected" ] diff --git a/tests/workflows.bats b/tests/workflows.bats index 0539186..aef89ec 100644 --- a/tests/workflows.bats +++ b/tests/workflows.bats @@ -484,6 +484,111 @@ EOF [ ! -s "$GH_LOG" ] } +# Runs the close step of publish in a workspace with the statuses given as +# name=status pairs. gh lists the open issues in $GH_OPEN, one +# " " per line, the first 30 unless --limit says otherwise, +# answers has_issues with $GH_HAS_ISSUES (true by default), fails the list +# when $GH_FAIL is set and logs every call to $GH_LOG. +run_close_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" +case "$1 $2" in + "api repos/owner/fork") echo "${GH_HAS_ISSUES:-true}" ;; + "issue list") + [[ -z "${GH_FAIL:-}" ]] || exit 1 + limit=30 + while (($#)); do + [[ "$1" != --limit ]] || limit=$2 + shift + done + [[ -z "${GH_OPEN:-}" ]] || head -n "$limit" <<<"$GH_OPEN" + ;; +esac +EOF + chmod +x "$bin/gh" + GH_LOG="$BATS_TEST_TMPDIR/gh.log" + : >"$GH_LOG" + step_run publish "Close issues of recovered packages" >"$BATS_TEST_TMPDIR/step.sh" + run env -C "$ws" PATH="$bin:$PATH" GH_LOG="$GH_LOG" GH_OPEN="${GH_OPEN:-}" GH_FAIL="${GH_FAIL:-}" \ + GH_HAS_ISSUES="${GH_HAS_ISSUES:-}" GITHUB_REPOSITORY=owner/fork \ + RUN_URL=https://example.invalid/run sh -e "$BATS_TEST_TMPDIR/step.sh" +} + +@test "check-updates: the close step runs before the issue step, and not after a failed step" { + run yq '[.jobs.publish.steps[].name] | join(",")' "$CHECK_UPDATES" + [[ "$output" == *"Close issues of recovered packages,Create issues for failed packages" ]] + run yq '.jobs.publish.steps[] | select(.name == "Close issues of recovered packages") | has("if")' "$CHECK_UPDATES" + [ "$output" = "false" ] +} + +@test "check-updates: the close step finds an open issue behind 30 others" { + local i + GH_OPEN="$(for i in {1..30}; do echo "$i ❌ pkg$i: check failed"; done)"$'\n31 ❌ foo: check failed' \ + run_close_step foo=up_to_date + [ "$status" -eq 0 ] + grep -q '^issue close 31 ' "$GH_LOG" +} + +@test "check-updates: closes the check issue of a package whose check passed" { + GH_OPEN=$'12 ❌ foo: check failed\n13 ❌ bar: check failed' \ + run_close_step foo=up_to_date bar="new_version: 2.0.0" _error_count=0 + [ "$status" -eq 0 ] + grep -qx 'issue close 12 --comment The status of foo is `up_to_date` in https://example.invalid/run.' "$GH_LOG" + grep -qx 'issue close 13 --comment The status of bar is `new_version: 2.0.0` in https://example.invalid/run.' "$GH_LOG" +} + +@test "check-updates: closes the update issue of a package that reached the AUR" { + GH_OPEN=$'12 ❌ foo: update failed\n13 ❌ bar: update failed\n14 ❌ baz: update failed' \ + run_close_step foo=up_to_date bar="updated: 2.0.0" baz="downgrade: 1.0.0" + [ "$status" -eq 0 ] + run grep -c '^issue close' "$GH_LOG" + [ "$output" = "3" ] +} + +@test "check-updates: keeps the update issue of a package that has not reached the AUR" { + GH_OPEN=$'12 ❌ foo: update failed\n13 ❌ bar: update failed\n14 ❌ baz: update failed' \ + run_close_step foo="new_version: 2.0.0" bar="downgrade_skipped: 1.0.0" baz="update_failed: boom" + [ "$status" -eq 0 ] + run grep -c '^issue close' "$GH_LOG" + [ "$output" = "0" ] +} + +@test "check-updates: keeps the issues of a package whose check still fails" { + GH_OPEN=$'12 ❌ foo: check failed\n13 ❌ foo: update failed' \ + run_close_step foo="check_failed: API down" + [ "$status" -eq 0 ] + run grep -c '^issue close' "$GH_LOG" + [ "$output" = "0" ] +} + +@test "check-updates: closes no issue of another package" { + GH_OPEN=$'12 ❌ foo-bar: check failed\n13 ❌ Check AUR Package Updates failed' \ + run_close_step foo=up_to_date + [ "$status" -eq 0 ] + run grep -c '^issue close' "$GH_LOG" + [ "$output" = "0" ] +} + +@test "check-updates: the close step lists nothing when Issues are disabled" { + GH_HAS_ISSUES=false GH_FAIL=1 run_close_step foo=up_to_date + [ "$status" -eq 0 ] + run grep -c '^issue ' "$GH_LOG" + [ "$output" = "0" ] +} + +@test "check-updates: the close step fails when the open issues cannot be listed" { + GH_FAIL=1 run_close_step foo=up_to_date + [ "$status" -ne 0 ] + run grep -c '^issue close' "$GH_LOG" + [ "$output" = "0" ] +} + @test "check-updates: verify runs as builder and hands over as prepare does" { run step_run verify "Verify checksums" [[ "$output" == *"sudo -u builder ./scripts/check-all.sh --verify handoff"* ]]