diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4108ef8c2e..fd5515dda0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,11 +22,14 @@ jobs: BASE_SHA: ${{ github.event.pull_request.base.sha }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # linkcheck needs the base commit. fetch-depth: 0 + - name: Test CI scripts + run: ci/tests/report-scheduled-linkcheck-failure.sh + - name: Cache binaries id: mdbook-cache uses: actions/cache@v4 @@ -88,3 +91,33 @@ jobs: run: | # using split_inclusive that uses regex feature that uses an unstable feature RUSTC_BOOTSTRAP=1 cargo run --release --manifest-path ci/sembr/Cargo.toml src + + notify-scheduled-failure: + name: Open an issue if links are broken + needs: ci + if: failure() && github.event_name == 'schedule' + runs-on: ubuntu-latest + permissions: + actions: read + issues: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Open an issue or comment on an existing one + run: ci/report-scheduled-linkcheck-failure.sh + env: + GH_TOKEN: ${{ github.token }} + + close-scheduled-failure: + name: Close linkcheck issue if no links are broken + needs: ci + if: success() && github.event_name == 'schedule' + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + steps: + - uses: actions/checkout@v7 + - name: Close issue + run: ci/close-scheduled-linkcheck-issues.sh + env: + GH_TOKEN: ${{ github.token }} diff --git a/ci/close-scheduled-linkcheck-issues.sh b/ci/close-scheduled-linkcheck-issues.sh new file mode 100755 index 0000000000..7fb4c74cc1 --- /dev/null +++ b/ci/close-scheduled-linkcheck-issues.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY must be set}" +: "${GITHUB_RUN_ID:?GITHUB_RUN_ID must be set}" +: "${GITHUB_SERVER_URL:?GITHUB_SERVER_URL must be set}" +: "${GH_TOKEN:?GH_TOKEN must be set}" + +successful_run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" + +issue_numbers=$(gh issue list \ + --repo "$GITHUB_REPOSITORY" \ + --state open \ + --label C-broken-links \ + --label C-CI \ + --label A-linkcheck \ + --limit 100 \ + --json number,title \ + --jq '.[] | select(.title == "[automation] Dead links found") | .number') + +if [[ -z "$issue_numbers" ]]; then + echo "No scheduled linkcheck failure issue is open." + exit 0 +fi + +while read -r issue_number; do + gh issue close "$issue_number" \ + --repo "$GITHUB_REPOSITORY" \ + --comment "The scheduled link check is succeeding again: $successful_run_url" +done <<< "$issue_numbers" diff --git a/ci/report-scheduled-linkcheck-failure.sh b/ci/report-scheduled-linkcheck-failure.sh new file mode 100755 index 0000000000..178b3b40a7 --- /dev/null +++ b/ci/report-scheduled-linkcheck-failure.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +set -euo pipefail + +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY must be set}" +: "${GITHUB_RUN_ID:?GITHUB_RUN_ID must be set}" +: "${GITHUB_RUN_NUMBER:?GITHUB_RUN_NUMBER must be set}" +: "${GITHUB_SERVER_URL:?GITHUB_SERVER_URL must be set}" +: "${GITHUB_SHA:?GITHUB_SHA must be set}" +: "${GH_TOKEN:?GH_TOKEN must be set}" + +title="[automation] Dead links found" +job_url=$(gh api \ + "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/jobs" \ + --jq '.jobs[] | select(.name == "ci") | .html_url') + +issue_number=$(gh issue list \ + --repo "$GITHUB_REPOSITORY" \ + --state open \ + --label C-broken-links \ + --label C-CI \ + --label A-linkcheck \ + --limit 100 \ + --json number,title \ + --jq '[.[] | select(.title == "[automation] Dead links found")][0].number // empty') + +if [[ -n "$issue_number" ]]; then + gh issue comment "$issue_number" \ + --repo "$GITHUB_REPOSITORY" \ + --body "The scheduled link check failed again in [CI run #$GITHUB_RUN_NUMBER]($job_url)." + exit 0 +fi + +body=$(cat <' "$@" + printf '\n' +} >> "$GH_MOCK_LOG" + +case "$command" in + api) + if [[ ${GH_MOCK_API_FAIL-} == 1 ]]; then + exit 1 + fi + echo "https://github.example/jobs/123" + ;; + issue) + if [[ ${1-} == list && -n ${GH_MOCK_ISSUE_NUMBER-} ]]; then + echo "$GH_MOCK_ISSUE_NUMBER" + fi + ;; +esac diff --git a/ci/tests/report-scheduled-linkcheck-failure.sh b/ci/tests/report-scheduled-linkcheck-failure.sh new file mode 100755 index 0000000000..96526ef0ea --- /dev/null +++ b/ci/tests/report-scheduled-linkcheck-failure.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +script="$repo_root/ci/report-scheduled-linkcheck-failure.sh" +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +export PATH="$repo_root/ci/tests/fakes:$PATH" +export GH_MOCK_LOG="$tmp/gh.log" +export GITHUB_REPOSITORY="rust-lang/rustc-dev-guide" +export GITHUB_RUN_ID=123 +export GITHUB_RUN_NUMBER=456 +export GITHUB_SERVER_URL="https://github.com" +export GITHUB_SHA=0123456789abcdef +export GH_TOKEN=test-token + +fail() { + echo "error: $*" >&2 + exit 1 +} + +assert_log_contains() { + grep -F -- "$1" "$GH_MOCK_LOG" >/dev/null || fail "gh log does not contain: $1" +} + +assert_log_excludes() { + if grep -F -- "$1" "$GH_MOCK_LOG" >/dev/null; then + fail "gh log unexpectedly contains: $1" + fi +} + +# A first failure creates the issue with all labels and links to the failed job. +: > "$GH_MOCK_LOG" +unset GH_MOCK_ISSUE_NUMBER GH_MOCK_API_FAIL +"$script" >/dev/null +assert_log_contains 'issue <--repo> <--state> ' +assert_log_contains '<--label> <--label> <--label> ' +assert_log_contains '<--json> <--jq> <[.[] | select(.title == "[automation] Dead links found")][0].number // empty>' +assert_log_contains 'issue ' +assert_log_contains '<--title> <[automation] Dead links found>' +assert_log_contains '<--label> <--label> <--label> ' +assert_log_contains '[CI run #456](https://github.example/jobs/123)' +assert_log_excludes 'issue ' + +# A later failure comments on the existing issue instead of creating another. +: > "$GH_MOCK_LOG" +export GH_MOCK_ISSUE_NUMBER=42 +"$script" >/dev/null +assert_log_contains 'issue <42>' +assert_log_contains 'failed again in [CI run #456](https://github.example/jobs/123)' +assert_log_excludes 'issue ' + +# GitHub API errors and missing required environment variables remain fatal. +: > "$GH_MOCK_LOG" +export GH_MOCK_API_FAIL=1 +if "$script" >/dev/null 2>&1; then + fail "script succeeded after gh api failed" +fi +unset GH_MOCK_API_FAIL + +for variable in GITHUB_REPOSITORY GITHUB_RUN_ID GITHUB_RUN_NUMBER GITHUB_SERVER_URL GITHUB_SHA GH_TOKEN; do + if env -u "$variable" "$script" >/dev/null 2>&1; then + fail "script succeeded without $variable" + fi +done + +echo "report-scheduled-linkcheck-failure tests passed"