diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml new file mode 100644 index 000000000..2516db132 --- /dev/null +++ b/.github/workflows/ci-tests.yml @@ -0,0 +1,55 @@ +# Regression guard for the flaky "Lint GitHub Actions" download in ci.yml +# (issue #894): every curl download in ci.yml must carry bounded retries so a +# transient GitHub Releases blip retries instead of failing the job. +# See test/workflows/ci/install-resilience.bats. +name: CI Workflow Tests + +on: + pull_request: + paths: + - '.github/workflows/ci.yml' + - 'test/workflows/ci/**' + - '.github/workflows/ci-tests.yml' + push: + branches: [main] + paths: + - '.github/workflows/ci.yml' + - 'test/workflows/ci/**' + - '.github/workflows/ci-tests.yml' + +permissions: {} + +concurrency: + group: ci-tests-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: bats + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Install bats + run: | + set -euo pipefail + for attempt in {1..3}; do + if sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends bats; then + exit 0 + fi + if [ "$attempt" -lt 3 ]; then + echo "Apt install failed, retrying in 5 seconds..." >&2 + sleep 5 + fi + done + echo "Apt install failed after 3 attempts" >&2 + exit 1 + + - name: bats + run: bats --print-output-on-failure test/workflows/ci/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9486980d..ea157ffd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,14 @@ jobs: # Install actionlint via verified download (pinned version + checksum) ACTIONLINT_VERSION="1.7.7" ACTIONLINT_SHA="023070a287cd8cccd71515fedc843f1985bf96c436b7effaecce67290e7e0757" - curl -sLo actionlint.tar.gz "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" + # Bounded retries so a transient GitHub Releases 5xx / connection blip + # retries instead of failing the job (guards the fleet failure-rate + # metric — see test/workflows/ci/install-resilience.bats, issue #894). + curl -sSfL --proto '=https' --proto-redir '=https' \ + --connect-timeout 10 --max-time 60 \ + --retry 3 --retry-delay 2 --retry-connrefused --retry-all-errors \ + -o actionlint.tar.gz \ + "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" echo "${ACTIONLINT_SHA} actionlint.tar.gz" | sha256sum -c - tar xzf actionlint.tar.gz actionlint chmod +x actionlint diff --git a/test/workflows/ci/helpers/setup.bash b/test/workflows/ci/helpers/setup.bash new file mode 100644 index 000000000..727a4401e --- /dev/null +++ b/test/workflows/ci/helpers/setup.bash @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Common test helpers for the ci.yml bats suite. +# Mirrors the pattern in test/workflows/pr-review-mention/helpers/setup.bash. + +# Repo root, regardless of where bats is invoked from. +TT_REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../../../.." && pwd)" +export TT_REPO_ROOT + +TT_WORKFLOW="${TT_REPO_ROOT}/.github/workflows/ci.yml" +export TT_WORKFLOW diff --git a/test/workflows/ci/install-resilience.bats b/test/workflows/ci/install-resilience.bats new file mode 100644 index 000000000..7ba36330b --- /dev/null +++ b/test/workflows/ci/install-resilience.bats @@ -0,0 +1,52 @@ +#!/usr/bin/env bats +# Regression guard for the flaky "Lint GitHub Actions" step in +# .github/workflows/ci.yml. +# +# Pins issue #894: ci.yml intermittently fails (~14% of runs) because the pinned +# rhysd/actionlint binary is fetched from GitHub Releases — which redirects to +# objects.githubusercontent.com — with a bare `curl` that aborts on the first +# transient network/5xx blip. This is the same failure mode fixed for +# pr-review-mention-tests.yml under #739. AGENTS.md requires CI to be reliable; +# the one unavoidable network fetch must therefore retry on transient errors. +# These tests assert every download in ci.yml carries bounded curl retries, so a +# future edit can't silently reintroduce a bare download. + +load 'helpers/setup' + +@test "install: the ci workflow exists" { + [ -f "$TT_WORKFLOW" ] +} + +@test "install: every curl download in ci.yml uses bounded retries" { + [ -f "$TT_WORKFLOW" ] + + # Parse the workflow joining backslash-continued lines so that a curl command + # split across multiple lines is treated as a single logical invocation. + # This avoids false failures when flags appear on continuation lines, and + # avoids false passes when a bare `curl` is split from its flags. + local curls=() + local current="" + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line%$'\r'}" + if [[ "$line" == *\\ ]]; then + current+="${line%\\} " + else + current+="$line" + if [[ "$current" == *curl* ]] && ! [[ "$current" =~ ^[[:space:]]*# ]] && ! [[ "$current" =~ ^[[:space:]]*-?[[:space:]]*(name|uses|with): ]]; then + curls+=("$current") + fi + current="" + fi + done < "$TT_WORKFLOW" + + # At least one curl invocation must be present so the test remains meaningful. + [ "${#curls[@]}" -ge 1 ] + for cmd in "${curls[@]}"; do + # A finite retry budget (not unbounded) so a truly-down mirror still fails fast. + [[ "$cmd" =~ --retry[[:space:]=][1-9] ]] + # Retry on connection refused, which curl otherwise treats as non-transient. + [[ "$cmd" == *"--retry-connrefused"* ]] + # Retry on transient HTTP 5xx too, not just connection-level errors. + [[ "$cmd" == *"--retry-all-errors"* ]] + done +}