Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/notify-failure.yml
Original file line number Diff line number Diff line change
@@ -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.")"
6 changes: 6 additions & 0 deletions docs/WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
89 changes: 89 additions & 0 deletions tests/workflows.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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" ]
}
Loading