From 62c2e536c5dd3301ce31c61f2a05a2010d2bbfa6 Mon Sep 17 00:00:00 2001 From: Saul Burgess Date: Mon, 29 Sep 2025 20:29:12 +0100 Subject: [PATCH 1/8] Adds smoke-test and github action to run it --- .github/scripts/run-smoke.sh | 94 ++++++++++++ .github/workflows/pr-smoke-test.yml | 226 ++++++++++++++++++++++++++++ 2 files changed, 320 insertions(+) create mode 100644 .github/scripts/run-smoke.sh create mode 100644 .github/workflows/pr-smoke-test.yml diff --git a/.github/scripts/run-smoke.sh b/.github/scripts/run-smoke.sh new file mode 100644 index 00000000..700b9c80 --- /dev/null +++ b/.github/scripts/run-smoke.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Assumptions: +# - Frontend has already been built (vite build) if needed +# - 'go run server.go' launches the application +# - Success condition: HTTPS port responds with any content OR log line 'Starting API services...' + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +LOG_DIR="$ROOT_DIR/smoke-logs" +mkdir -p "$LOG_DIR" +LOG_FILE="$LOG_DIR/server.log" +RESULT_FILE="$LOG_DIR/result.txt" +STATUS_JSON="$LOG_DIR/status.json" +PORT="8443" +TIMEOUT_SECONDS=${SMOKE_TIMEOUT_SECONDS:-45} +GRACE_KILL_SECONDS=5 + +# Start server in background +( + cd "$ROOT_DIR" + echo "[smoke] launching server..." | tee -a "$LOG_FILE" + # Disable long-running / heavy operations if possible via env flags + export SKIP_STEAMCMD=true + export LOG_LEVEL=20 # INFO + # Run server + go run server.go 2>&1 | tee -a "$LOG_FILE" & + echo $! > "$LOG_DIR/server.pid" +) || true + +PID=$(cat "$LOG_DIR/server.pid" 2>/dev/null || echo "") +if [[ -z "$PID" ]]; then + echo "{\"status\":\"failed\",\"reason\":\"No PID captured\"}" > "$STATUS_JSON" + exit 0 +fi + +echo "[smoke] server pid: $PID" + +# Wait for readiness +START_TIME=$(date +%s) +READY=0 +while true; do + NOW=$(date +%s) + ELAPSED=$((NOW-START_TIME)) + if (( ELAPSED > TIMEOUT_SECONDS )); then + echo "[smoke] timeout after $ELAPSED seconds" | tee -a "$LOG_FILE" + break + fi + # Check log line + if grep -q "Starting API services" "$LOG_FILE"; then + READY=1 + echo "[smoke] readiness via log line" | tee -a "$LOG_FILE" + break + fi + # Try HTTPS curl (ignore cert issues) + if curl -sk --max-time 2 https://localhost:$PORT/ >/dev/null 2>&1; then + READY=1 + echo "[smoke] readiness via https response" | tee -a "$LOG_FILE" + break + fi + sleep 2 +done + +if [[ $READY -eq 1 ]]; then + STATUS="passed" +else + STATUS="failed" +fi + +echo "[smoke] stopping server (status=$STATUS)" | tee -a "$LOG_FILE" +if kill "$PID" 2>/dev/null; then + # allow graceful shutdown + for i in $(seq 1 $GRACE_KILL_SECONDS); do + if kill -0 "$PID" 2>/dev/null; then + sleep 1 + else + break + fi + done + if kill -0 "$PID" 2>/dev/null; then + echo "[smoke] force killing $PID" | tee -a "$LOG_FILE" + kill -9 "$PID" || true + fi +fi + +echo "$STATUS" > "$RESULT_FILE" +cat > "$STATUS_JSON" </dev/null || echo 'missing')" >> $GITHUB_OUTPUT + echo "status_json=$(cat smoke-logs/status.json 2>/dev/null || echo '{}')" >> $GITHUB_OUTPUT + shell: bash + + - name: Upload logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: smoke-${{ matrix.os }} + path: | + smoke-logs/server.log + smoke-logs/status.json + smoke-logs/result.txt + if-no-files-found: warn + + smoke-linux-distros: + name: Smoke (Container ${{ matrix.distro }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - distro: debian + image: debian:trixie + pkg_update: apt-get update + install: apt-get install -y --no-install-recommends ca-certificates curl git build-essential wget nodejs npm bash + needs_go: true + - distro: arch + image: archlinux:base + pkg_update: pacman -Sy --noconfirm + install: pacman -S --noconfirm ca-certificates curl git wget tar nodejs npm go base-devel bash + needs_go: false + container: + image: ${{ matrix.image }} + env: + GO_VERSION: '1.22.6' + CGO_ENABLED: 0 + steps: + - name: Prepare packages + run: | + set -e + ${{ matrix.pkg_update }} + ${{ matrix.install }} + - name: Install Go (if needed) + if: matrix.needs_go == true + run: | + set -e + ARCH=$(uname -m) + case "$ARCH" in + x86_64) GOARCH=amd64 ;; + aarch64) GOARCH=arm64 ;; + *) echo "Unsupported arch: $ARCH"; exit 1 ;; + esac + curl -fsSL https://go.dev/dl/go${GO_VERSION}.linux-${GOARCH}.tar.gz -o /tmp/go.tgz + rm -rf /usr/local/go && tar -C /usr/local -xzf /tmp/go.tgz + echo "/usr/local/go/bin" >> $GITHUB_PATH + /usr/local/go/bin/go version + - name: Add Go to PATH (arch already has go) + if: matrix.needs_go == false + run: which go || echo "go not found" || true + - name: Checkout + uses: actions/checkout@v4 + - name: Node version + run: node --version && npm --version || true + - name: Install frontend deps + working-directory: frontend + run: | + if [ -f package.json ]; then npm ci || npm install; fi + - name: Build frontend + working-directory: frontend + run: | + if [ -f package.json ]; then npm run build || echo 'frontend build failed (continuing)'; fi + - name: Run smoke test + run: bash ./.github/scripts/run-smoke.sh + - name: Collect results + if: always() + id: collect + run: | + echo "status=$(cat smoke-logs/result.txt 2>/dev/null || echo 'missing')" >> $GITHUB_OUTPUT + echo "status_json=$(cat smoke-logs/status.json 2>/dev/null || echo '{}')" >> $GITHUB_OUTPUT + - name: Upload logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: smoke-${{ matrix.distro }} + path: | + smoke-logs/server.log + smoke-logs/status.json + smoke-logs/result.txt + if-no-files-found: warn + + summarize: + name: Summarize & Comment + needs: [smoke, smoke-linux-distros] + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: read + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Build summary + id: summary + run: | + set -e + echo '### Cross-OS Smoke Test Results' > summary.md + echo '' >> summary.md + STATUS_TABLE="| OS | Status | Details |\n|----|--------|---------|" + for dir in artifacts/*; do + osname=$(basename "$dir" | sed 's/^smoke-//') + resultFile=$(find "$dir" -name result.txt -maxdepth 2 -type f 2>/dev/null | head -n1) + logFile=$(find "$dir" -name server.log -maxdepth 2 -type f 2>/dev/null | head -n1) + status="missing" + if [ -f "$resultFile" ]; then status=$(cat "$resultFile"); fi + detailsLink="(logs unavailable)" + if [ -f "$logFile" ]; then + head -n5 "$logFile" > "$dir/head.txt" || true + tail -n5 "$logFile" > "$dir/tail.txt" || true + detailsLink="See below" + fi + STATUS_TABLE+="\n| $osname | $status | $detailsLink |" + done + echo "$STATUS_TABLE" >> summary.md + echo '' >> summary.md + echo '
Log excerpts' >> summary.md + for dir in artifacts/*; do + osname=$(basename "$dir" | sed 's/^smoke-//') + logFile=$(find "$dir" -name server.log -maxdepth 2 -type f 2>/dev/null | head -n1) + if [ -f "$logFile" ]; then + echo "\n#### $osname" >> summary.md + echo '\n
First 5 lines' >> summary.md + echo '\n```' >> summary.md + cat "$dir/head.txt" >> summary.md + echo '```' >> summary.md + echo '
' >> summary.md + echo '\n
Last 5 lines' >> summary.md + echo '\n```' >> summary.md + cat "$dir/tail.txt" >> summary.md + echo '```' >> summary.md + echo '
' >> summary.md + fi + done + echo '\n
' >> summary.md + SUMMARY_BODY=$(cat summary.md) + SUMMARY_BODY_ESCAPED=$(printf '%s' "$SUMMARY_BODY" | sed -e ':a' -e 'N' -e '$!ba' -e 's/%/%25/g' -e 's/\n/%0A/g' -e 's/\r/%0D/g') + echo "body=$SUMMARY_BODY_ESCAPED" >> $GITHUB_OUTPUT + + - name: Create or update PR comment + uses: actions/github-script@v7 + with: + script: | + const markerStart = ''; + const markerEnd = ''; + const body = process.env.SUMMARY_BODY || `\n${{ steps.summary.outputs.body }}`; + const {owner, repo, number} = context.issue; + const { data: comments } = await github.rest.issues.listComments({owner, repo, issue_number: number, per_page: 100}); + const existing = comments.find(c => c.body && c.body.includes(markerStart)); + const finalBody = `${markerStart}\n${body}\n${markerEnd}`; + if (existing) { + await github.rest.issues.updateComment({owner, repo, comment_id: existing.id, body: finalBody}); + } else { + await github.rest.issues.createComment({owner, repo, issue_number: number, body: finalBody}); + } + env: + SUMMARY_BODY: ${{ steps.summary.outputs.body }} From ddbeb0d1c1abce5dfc69f43c3833843b83a2481f Mon Sep 17 00:00:00 2001 From: Saul Burgess Date: Mon, 29 Sep 2025 22:15:43 +0100 Subject: [PATCH 2/8] Shifts it to be container based --- .github/workflows/pr-smoke-test.yml | 164 +++------------------------- 1 file changed, 14 insertions(+), 150 deletions(-) diff --git a/.github/workflows/pr-smoke-test.yml b/.github/workflows/pr-smoke-test.yml index 15db86f5..e2f51571 100644 --- a/.github/workflows/pr-smoke-test.yml +++ b/.github/workflows/pr-smoke-test.yml @@ -1,4 +1,4 @@ -name: PR Cross-OS Smoke Test +name: PR Container Smoke Test on: pull_request: @@ -6,78 +6,20 @@ on: permissions: contents: read - pull-requests: write jobs: smoke: - name: Smoke (Runner ${{ matrix.os }}) - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest] - env: - CGO_ENABLED: 0 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: '1.22' - check-latest: true - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - cache-dependency-path: 'frontend/package-lock.json' - continue-on-error: true - - - name: Install frontend deps - working-directory: frontend - run: | - if [ -f package.json ]; then npm ci || npm install; fi - shell: bash - - - name: Build frontend - working-directory: frontend - run: | - if [ -f package.json ]; then npm run build || echo 'frontend build failed (continuing)'; fi - shell: bash - - - name: Smoke test - run: bash ./.github/scripts/run-smoke.sh - shell: bash - - - name: Collect results - if: always() - id: collect - run: | - echo "status=$(cat smoke-logs/result.txt 2>/dev/null || echo 'missing')" >> $GITHUB_OUTPUT - echo "status_json=$(cat smoke-logs/status.json 2>/dev/null || echo '{}')" >> $GITHUB_OUTPUT - shell: bash - - - name: Upload logs - if: always() - uses: actions/upload-artifact@v4 - with: - name: smoke-${{ matrix.os }} - path: | - smoke-logs/server.log - smoke-logs/status.json - smoke-logs/result.txt - if-no-files-found: warn - - smoke-linux-distros: name: Smoke (Container ${{ matrix.distro }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: + - distro: ubuntu + image: ubuntu:24.04 + pkg_update: apt-get update + install: apt-get install -y --no-install-recommends ca-certificates curl git build-essential wget nodejs npm bash + needs_go: true - distro: debian image: debian:trixie pkg_update: apt-get update @@ -113,9 +55,9 @@ jobs: rm -rf /usr/local/go && tar -C /usr/local -xzf /tmp/go.tgz echo "/usr/local/go/bin" >> $GITHUB_PATH /usr/local/go/bin/go version - - name: Add Go to PATH (arch already has go) + - name: Verify Go (preinstalled) if: matrix.needs_go == false - run: which go || echo "go not found" || true + run: go version || which go || true - name: Checkout uses: actions/checkout@v4 - name: Node version @@ -140,87 +82,9 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: smoke-${{ matrix.distro }} - path: | - smoke-logs/server.log - smoke-logs/status.json - smoke-logs/result.txt - if-no-files-found: warn - - summarize: - name: Summarize & Comment - needs: [smoke, smoke-linux-distros] - runs-on: ubuntu-latest - permissions: - pull-requests: write - contents: read - steps: - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - - name: Build summary - id: summary - run: | - set -e - echo '### Cross-OS Smoke Test Results' > summary.md - echo '' >> summary.md - STATUS_TABLE="| OS | Status | Details |\n|----|--------|---------|" - for dir in artifacts/*; do - osname=$(basename "$dir" | sed 's/^smoke-//') - resultFile=$(find "$dir" -name result.txt -maxdepth 2 -type f 2>/dev/null | head -n1) - logFile=$(find "$dir" -name server.log -maxdepth 2 -type f 2>/dev/null | head -n1) - status="missing" - if [ -f "$resultFile" ]; then status=$(cat "$resultFile"); fi - detailsLink="(logs unavailable)" - if [ -f "$logFile" ]; then - head -n5 "$logFile" > "$dir/head.txt" || true - tail -n5 "$logFile" > "$dir/tail.txt" || true - detailsLink="See below" - fi - STATUS_TABLE+="\n| $osname | $status | $detailsLink |" - done - echo "$STATUS_TABLE" >> summary.md - echo '' >> summary.md - echo '
Log excerpts' >> summary.md - for dir in artifacts/*; do - osname=$(basename "$dir" | sed 's/^smoke-//') - logFile=$(find "$dir" -name server.log -maxdepth 2 -type f 2>/dev/null | head -n1) - if [ -f "$logFile" ]; then - echo "\n#### $osname" >> summary.md - echo '\n
First 5 lines' >> summary.md - echo '\n```' >> summary.md - cat "$dir/head.txt" >> summary.md - echo '```' >> summary.md - echo '
' >> summary.md - echo '\n
Last 5 lines' >> summary.md - echo '\n```' >> summary.md - cat "$dir/tail.txt" >> summary.md - echo '```' >> summary.md - echo '
' >> summary.md - fi - done - echo '\n
' >> summary.md - SUMMARY_BODY=$(cat summary.md) - SUMMARY_BODY_ESCAPED=$(printf '%s' "$SUMMARY_BODY" | sed -e ':a' -e 'N' -e '$!ba' -e 's/%/%25/g' -e 's/\n/%0A/g' -e 's/\r/%0D/g') - echo "body=$SUMMARY_BODY_ESCAPED" >> $GITHUB_OUTPUT - - - name: Create or update PR comment - uses: actions/github-script@v7 - with: - script: | - const markerStart = ''; - const markerEnd = ''; - const body = process.env.SUMMARY_BODY || `\n${{ steps.summary.outputs.body }}`; - const {owner, repo, number} = context.issue; - const { data: comments } = await github.rest.issues.listComments({owner, repo, issue_number: number, per_page: 100}); - const existing = comments.find(c => c.body && c.body.includes(markerStart)); - const finalBody = `${markerStart}\n${body}\n${markerEnd}`; - if (existing) { - await github.rest.issues.updateComment({owner, repo, comment_id: existing.id, body: finalBody}); - } else { - await github.rest.issues.createComment({owner, repo, issue_number: number, body: finalBody}); - } - env: - SUMMARY_BODY: ${{ steps.summary.outputs.body }} + name: smoke-${{ matrix.distro }} + path: | + smoke-logs/server.log + smoke-logs/status.json + smoke-logs/result.txt + if-no-files-found: warn From 4c2144620cdc4cd45b3700e366d3e9e717e9b11b Mon Sep 17 00:00:00 2001 From: Saul Burgess Date: Tue, 30 Sep 2025 11:19:21 +0100 Subject: [PATCH 3/8] Looking at the docs, if I remove this- it'll pull the go version from the `go.mod` --- .github/workflows/pr-smoke-test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pr-smoke-test.yml b/.github/workflows/pr-smoke-test.yml index e2f51571..2423920b 100644 --- a/.github/workflows/pr-smoke-test.yml +++ b/.github/workflows/pr-smoke-test.yml @@ -33,7 +33,6 @@ jobs: container: image: ${{ matrix.image }} env: - GO_VERSION: '1.22.6' CGO_ENABLED: 0 steps: - name: Prepare packages From 3e6d5be4ffe0db7649dcb52d04150e5d2d6405a3 Mon Sep 17 00:00:00 2001 From: Saul Burgess Date: Tue, 30 Sep 2025 11:48:28 +0100 Subject: [PATCH 4/8] Removes non-existing env --- .github/scripts/run-smoke.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/scripts/run-smoke.sh b/.github/scripts/run-smoke.sh index 700b9c80..224c72de 100644 --- a/.github/scripts/run-smoke.sh +++ b/.github/scripts/run-smoke.sh @@ -20,8 +20,6 @@ GRACE_KILL_SECONDS=5 ( cd "$ROOT_DIR" echo "[smoke] launching server..." | tee -a "$LOG_FILE" - # Disable long-running / heavy operations if possible via env flags - export SKIP_STEAMCMD=true export LOG_LEVEL=20 # INFO # Run server go run server.go 2>&1 | tee -a "$LOG_FILE" & From 7ed6abcb0a8d305244fe9f5250d960fffe31a118 Mon Sep 17 00:00:00 2001 From: Saul Burgess Date: Tue, 30 Sep 2025 11:48:52 +0100 Subject: [PATCH 5/8] Uses `go.mod` to setup go --- .github/workflows/pr-smoke-test.yml | 31 ++++++++--------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/.github/workflows/pr-smoke-test.yml b/.github/workflows/pr-smoke-test.yml index 2423920b..69e08617 100644 --- a/.github/workflows/pr-smoke-test.yml +++ b/.github/workflows/pr-smoke-test.yml @@ -18,18 +18,15 @@ jobs: - distro: ubuntu image: ubuntu:24.04 pkg_update: apt-get update - install: apt-get install -y --no-install-recommends ca-certificates curl git build-essential wget nodejs npm bash - needs_go: true + install: apt-get install -y --no-install-recommends ca-certificates curl git build-essential wget nodejs npm bash tar - distro: debian image: debian:trixie pkg_update: apt-get update - install: apt-get install -y --no-install-recommends ca-certificates curl git build-essential wget nodejs npm bash - needs_go: true + install: apt-get install -y --no-install-recommends ca-certificates curl git build-essential wget nodejs npm bash tar - distro: arch image: archlinux:base pkg_update: pacman -Sy --noconfirm - install: pacman -S --noconfirm ca-certificates curl git wget tar nodejs npm go base-devel bash - needs_go: false + install: pacman -S --noconfirm ca-certificates curl git wget tar nodejs npm base-devel bash container: image: ${{ matrix.image }} env: @@ -40,25 +37,13 @@ jobs: set -e ${{ matrix.pkg_update }} ${{ matrix.install }} - - name: Install Go (if needed) - if: matrix.needs_go == true - run: | - set -e - ARCH=$(uname -m) - case "$ARCH" in - x86_64) GOARCH=amd64 ;; - aarch64) GOARCH=arm64 ;; - *) echo "Unsupported arch: $ARCH"; exit 1 ;; - esac - curl -fsSL https://go.dev/dl/go${GO_VERSION}.linux-${GOARCH}.tar.gz -o /tmp/go.tgz - rm -rf /usr/local/go && tar -C /usr/local -xzf /tmp/go.tgz - echo "/usr/local/go/bin" >> $GITHUB_PATH - /usr/local/go/bin/go version - - name: Verify Go (preinstalled) - if: matrix.needs_go == false - run: go version || which go || true - name: Checkout uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true - name: Node version run: node --version && npm --version || true - name: Install frontend deps From 0830c9470716bdf38f89f2233762cc993ae7c5bb Mon Sep 17 00:00:00 2001 From: Saul Burgess Date: Tue, 30 Sep 2025 11:52:52 +0100 Subject: [PATCH 6/8] Applies github copilot change --- .github/scripts/run-smoke.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/scripts/run-smoke.sh b/.github/scripts/run-smoke.sh index 224c72de..1183d06d 100644 --- a/.github/scripts/run-smoke.sh +++ b/.github/scripts/run-smoke.sh @@ -22,13 +22,13 @@ GRACE_KILL_SECONDS=5 echo "[smoke] launching server..." | tee -a "$LOG_FILE" export LOG_LEVEL=20 # INFO # Run server - go run server.go 2>&1 | tee -a "$LOG_FILE" & + nohup go run server.go > "$LOG_FILE" 2>&1 & echo $! > "$LOG_DIR/server.pid" ) || true PID=$(cat "$LOG_DIR/server.pid" 2>/dev/null || echo "") -if [[ -z "$PID" ]]; then - echo "{\"status\":\"failed\",\"reason\":\"No PID captured\"}" > "$STATUS_JSON" +if [[ -z "$PID" ]] || ! kill -0 "$PID" 2>/dev/null; then + echo "{\"status\":\"failed\",\"reason\":\"No valid server PID captured\"}" > "$STATUS_JSON" exit 0 fi From 65e21035a8c8cbb5fd15522cbe6ae37d06f1c8d5 Mon Sep 17 00:00:00 2001 From: Saul Burgess Date: Tue, 30 Sep 2025 11:54:43 +0100 Subject: [PATCH 7/8] Changes wording on the actions --- .github/workflows/pr-smoke-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-smoke-test.yml b/.github/workflows/pr-smoke-test.yml index 69e08617..3f0e7d3b 100644 --- a/.github/workflows/pr-smoke-test.yml +++ b/.github/workflows/pr-smoke-test.yml @@ -1,4 +1,4 @@ -name: PR Container Smoke Test +name: PR Smoke Test on: pull_request: @@ -9,7 +9,7 @@ permissions: jobs: smoke: - name: Smoke (Container ${{ matrix.distro }}) + name: Smoke ( ${{ matrix.distro }}) runs-on: ubuntu-latest strategy: fail-fast: false From 91009802b7aee82f8c86cbdcc562a2cf140e99e3 Mon Sep 17 00:00:00 2001 From: Saul Burgess Date: Tue, 30 Sep 2025 11:56:06 +0100 Subject: [PATCH 8/8] Fix formatting in smoke job name in PR smoke test workflow --- .github/workflows/pr-smoke-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-smoke-test.yml b/.github/workflows/pr-smoke-test.yml index 3f0e7d3b..a222d8f3 100644 --- a/.github/workflows/pr-smoke-test.yml +++ b/.github/workflows/pr-smoke-test.yml @@ -9,7 +9,7 @@ permissions: jobs: smoke: - name: Smoke ( ${{ matrix.distro }}) + name: Smoke (${{ matrix.distro }}) runs-on: ubuntu-latest strategy: fail-fast: false