From 5b34d704042066bbaa3226ca92b4fff4eea8cf88 Mon Sep 17 00:00:00 2001 From: noctuum <25441068+noctuum@users.noreply.github.com> Date: Wed, 21 Oct 2026 07:21:42 +0500 Subject: [PATCH] feat(workflow): open an issue for a failed run notify-failure.yml runs on workflow_run of Check AUR Package Updates. --- .github/workflows/notify-failure.yml | 58 ++++++++++++++++++ docs/WORKFLOW.md | 6 ++ tests/workflows.bats | 89 ++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+) create mode 100644 .github/workflows/notify-failure.yml diff --git a/.github/workflows/notify-failure.yml b/.github/workflows/notify-failure.yml new file mode 100644 index 0000000..125f265 --- /dev/null +++ b/.github/workflows/notify-failure.yml @@ -0,0 +1,58 @@ +name: Notify Failed Runs + +# The issues of the packages come from the publish job. A run that fails +# before it, or in it, opens this one issue instead. +on: + workflow_run: + workflows: [Check AUR Package Updates] + types: [completed] + +permissions: + actions: read + issues: write + +# Two failed runs that end together open one issue +concurrency: + group: notify-failure + +jobs: + notify: + if: github.event.workflow_run.conclusion == 'failure' || github.event.workflow_run.conclusion == 'timed_out' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Open an issue for the failed run + shell: bash + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RUN_ID: ${{ github.event.workflow_run.id }} + RUN_URL: ${{ github.event.workflow_run.html_url }} + RUN_BRANCH: ${{ github.event.workflow_run.head_branch }} + run: | + title="❌ Check AUR Package Updates failed" + + # gh issue list fails in a repository with Issues turned off + has_issues=$(gh api "repos/$GH_REPO" --jq .has_issues) + if [[ "$has_issues" == "false" ]]; then + echo "::notice::Issues are disabled in $GH_REPO, so the failed run gets no issue" + exit 0 + fi + + # Exact titles, as in the issue step of publish + titles=$(gh issue list --label auto-update --state open --limit 1000 --json title --jq '.[].title') + if grep -qxF -e "$title" <<<"$titles"; then + echo "Issue already open, skipping" + exit 0 + fi + + # The names of the failed jobs only fill the text, so the issue + # opens without them as well + failed=$(gh api "repos/$GH_REPO/actions/runs/$RUN_ID/jobs" \ + --jq '[.jobs[] | select(.conclusion == "failure" or .conclusion == "timed_out") | .name] | join(", ")') || failed="" + + gh label create auto-update --color d73a4a --description "Automated update failure" 2>/dev/null || true + gh issue create --title "$title" --label auto-update --body "$(printf '%s\n' \ + "Run $RUN_URL on \`$RUN_BRANCH\` failed in ${failed:-jobs that could not be listed}." \ + "" \ + "Packages may be left unchecked or unpublished until a run succeeds.")" diff --git a/docs/WORKFLOW.md b/docs/WORKFLOW.md index 853b95a..e512758 100644 --- a/docs/WORKFLOW.md +++ b/docs/WORKFLOW.md @@ -786,6 +786,12 @@ 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. +### 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. + +GitHub starts a `workflow_run` workflow only from its file on the default branch. + ## 6. CI Quality Gate: ci.yml `ci.yml` runs on every push and pull request to `main`. diff --git a/tests/workflows.bats b/tests/workflows.bats index 61fc051..0539186 100644 --- a/tests/workflows.bats +++ b/tests/workflows.bats @@ -508,3 +508,92 @@ EOF run yq '.jobs.publish.steps[] | select(.with.name == "verified") | .if' "$CHECK_UPDATES" [ "$output" = "needs.verify.result == 'success'" ] } + +NOTIFY="$GITHUB_DIR/workflows/notify-failure.yml" + +@test "notify-failure: runs on a failed run of the update workflow" { + run yq '.name' "$CHECK_UPDATES" + local name="$output" + run yq -o=json -I=0 '.on' "$NOTIFY" + [ "$status" -eq 0 ] + [ "$output" = "{\"workflow_run\":{\"workflows\":[\"$name\"],\"types\":[\"completed\"]}}" ] + run yq '.jobs.notify.if' "$NOTIFY" + [ "$output" = "github.event.workflow_run.conclusion == 'failure' || github.event.workflow_run.conclusion == 'timed_out'" ] +} + +@test "notify-failure: reads the jobs of the run and writes issues, nothing else" { + run yq -o=json -I=0 '.permissions' "$NOTIFY" + [ "$output" = '{"actions":"read","issues":"write"}' ] + run yq '[.jobs[].steps[] | select(has("uses"))] | length' "$NOTIFY" + [ "$output" = "0" ] +} + +# Runs the step of notify-failure for run 7 of owner/fork. gh lists the +# titles in $GH_TITLES as the open issues, answers has_issues with +# $GH_HAS_ISSUES (true by default), lists the jobs in $GH_JOBS as the failed +# ones or fails that when $GH_JOBS_FAIL is set, fails the list of issues +# when $GH_FAIL is set and logs every call to $GH_LOG. +run_notify_step() { + local bin="$BATS_TEST_TMPDIR/bin" + mkdir -p "$bin" + 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}" ;; + "api repos/owner/fork/actions/runs/7/jobs") + [[ -z "${GH_JOBS_FAIL:-}" ]] || exit 1 + echo "$GH_JOBS" + ;; + "issue list") + [[ -z "${GH_FAIL:-}" ]] || exit 1 + [[ -z "${GH_TITLES:-}" ]] || printf '%s\n' "$GH_TITLES" + ;; +esac +EOF + chmod +x "$bin/gh" + GH_LOG="$BATS_TEST_TMPDIR/gh.log" + : >"$GH_LOG" + yq '.jobs.notify.steps[0].run' "$NOTIFY" >"$BATS_TEST_TMPDIR/step.sh" + run env PATH="$bin:$PATH" GH_LOG="$GH_LOG" GH_TITLES="${GH_TITLES:-}" GH_FAIL="${GH_FAIL:-}" \ + GH_HAS_ISSUES="${GH_HAS_ISSUES:-}" GH_JOBS="${GH_JOBS:-}" GH_JOBS_FAIL="${GH_JOBS_FAIL:-}" \ + GH_REPO=owner/fork RUN_ID=7 \ + RUN_URL=https://example.invalid/runs/7 RUN_BRANCH=main bash -e "$BATS_TEST_TMPDIR/step.sh" +} + +@test "notify-failure: opens an issue that names the run and its failed jobs" { + GH_JOBS="check, publish" run_notify_step + [ "$status" -eq 0 ] + grep -q '^issue create --title ❌ Check AUR Package Updates failed --label auto-update --body ' "$GH_LOG" + grep -q 'https://example.invalid/runs/7' "$GH_LOG" + grep -q 'failed in check, publish' "$GH_LOG" +} + +@test "notify-failure: opens the issue when the jobs cannot be listed" { + GH_JOBS_FAIL=1 run_notify_step + [ "$status" -eq 0 ] + grep -q '^issue create --title ❌ Check AUR Package Updates failed ' "$GH_LOG" + grep -q 'failed in jobs that could not be listed' "$GH_LOG" +} + +@test "notify-failure: opens no second issue while one is open" { + GH_TITLES="❌ Check AUR Package Updates failed" run_notify_step + [ "$status" -eq 0 ] + run grep -c '^issue create' "$GH_LOG" + [ "$output" = "0" ] +} + +@test "notify-failure: gives a notice when Issues are disabled" { + GH_HAS_ISSUES=false GH_FAIL=1 run_notify_step + [ "$status" -eq 0 ] + [[ "$output" == *"::notice::Issues are disabled in owner/fork"* ]] + run grep -c '^issue ' "$GH_LOG" + [ "$output" = "0" ] +} + +@test "notify-failure: fails when the open issues cannot be listed" { + GH_FAIL=1 run_notify_step + [ "$status" -ne 0 ] + run grep -c '^issue create' "$GH_LOG" + [ "$output" = "0" ] +}