Files
Cleanuparr/.github/workflows/e2e.yml

253 lines
7.5 KiB
YAML

name: E2E Tests
on:
workflow_call:
inputs:
ref:
description: 'Git ref to checkout (branch, tag, or SHA). Defaults to the triggering ref.'
type: string
required: false
default: ''
workflow_dispatch:
concurrency:
group: E2E Tests-${{ inputs.ref || github.ref }}
cancel-in-progress: true
permissions:
contents: read
actions: read
jobs:
suites:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
list: ${{ steps.suites.outputs.list }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
timeout-minutes: 1
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- name: Build the suite matrix
id: suites
working-directory: e2e
run: echo "list=$(bash ./scripts/ci-matrix.sh)" >> "$GITHUB_OUTPUT"
build-app:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
variant: [plain, patched]
name: build-app (${{ matrix.variant }})
steps:
- name: Checkout repository
uses: actions/checkout@v7
timeout-minutes: 1
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- name: Get vault secrets
uses: hashicorp/vault-action@v4
with:
url: ${{ secrets.VAULT_HOST }}
method: approle
roleId: ${{ secrets.VAULT_ROLE_ID }}
secretId: ${{ secrets.VAULT_SECRET_ID }}
secrets:
secrets/data/github packages_pat | PACKAGES_PAT
# Nothing reverts these. The runner throws the checkout away.
- name: Apply the e2e patches
if: matrix.variant == 'patched'
run: bash e2e/scripts/with-patches.sh
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
timeout-minutes: 5
- name: Build the app image
uses: docker/build-push-action@v7
timeout-minutes: 25
with:
context: ${{ github.workspace }}/code
file: ${{ github.workspace }}/code/Dockerfile
provenance: false
build-args: |
PACKAGES_USERNAME=${{ github.repository_owner }}
secrets: |
packages_pat=${{ env.PACKAGES_PAT }}
tags: cleanuparr-e2e-app:${{ matrix.variant }}
outputs: type=docker,dest=/tmp/app-image.tar
cache-from: type=gha,scope=e2e-${{ matrix.variant }}
cache-to: type=gha,scope=e2e-${{ matrix.variant }},mode=max
# gzip -1 beats letting upload-artifact deflate a layer tar this size.
- name: Compress the image
run: gzip -1 /tmp/app-image.tar
- name: Upload the image
uses: actions/upload-artifact@v7
with:
name: e2e-app-image-${{ matrix.variant }}
path: /tmp/app-image.tar.gz
retention-days: 1
compression-level: 0
e2e:
needs: suites
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
suite: ${{ fromJSON(needs.suites.outputs.list) }}
name: e2e (${{ matrix.suite.name }})
steps:
- name: Checkout repository
uses: actions/checkout@v7
timeout-minutes: 1
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- name: Start the stack
working-directory: e2e
run: make ${{ matrix.suite.stack-target }}
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 26
cache: 'npm'
cache-dependency-path: e2e/package-lock.json
- name: Install E2E dependencies
working-directory: e2e
run: npm ci
- name: Resolve Playwright version
id: playwright
working-directory: e2e
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }}
- name: Report runner disk space
run: df -h /
- name: Install Playwright browsers
working-directory: e2e
timeout-minutes: 10
env:
DEBUG: pw:install
run: npx playwright install --with-deps chromium
- name: Wait for the app image
timeout-minutes: 30
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VARIANT: ${{ matrix.suite.image-variant }}
RUN_JOBS: /repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/jobs
RUN_ARTIFACTS: /repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts
run: |
set -euo pipefail
artifact="e2e-app-image-$VARIANT"
job="build-app ($VARIANT)"
grace=12
# The artifact appears only once build-app uploads it. Watch that job too,
# so a build failure ends this leg instead of stalling it until the timeout.
while :; do
# Assigned, not piped into grep: under pipefail a grep that exits on the
# first match can SIGPIPE gh, making a found artifact read as not found.
id=$(gh api "$RUN_ARTIFACTS" --paginate \
--jq ".artifacts[] | select(.name == \"$artifact\") | .id" || true)
if [ -n "$id" ]; then
break
fi
# A transient API error must not end the leg, so keep polling.
conclusion=$(gh api "$RUN_JOBS" --paginate \
--jq ".jobs[] | select(.name == \"$job\") | .conclusion" || true)
case "$conclusion" in
''|null) ;;
success)
grace=$((grace - 1))
if [ "$grace" -le 0 ]; then
echo "::error::$job succeeded but $artifact never appeared"
exit 1
fi
;;
*)
echo "::error::$job concluded $conclusion, no image to run"
exit 1
;;
esac
sleep 10
done
- name: Download the app image
uses: actions/download-artifact@v8
with:
name: e2e-app-image-${{ matrix.suite.image-variant }}
path: /tmp
- name: Load the app image
run: gunzip -c /tmp/app-image.tar.gz | docker load
- name: Start the app
working-directory: e2e
env:
APP_IMAGE: cleanuparr-e2e-app:${{ matrix.suite.image-variant }}
run: make up-app
- name: Run E2E tests
working-directory: e2e
run: npx playwright test ${{ matrix.suite.projects }}
- name: Dump service logs
if: failure()
working-directory: e2e
run: |
docker compose -f docker-compose.e2e.yml ps
# A container can serve fine and still fail its check, so record what the check saw.
docker compose -f docker-compose.e2e.yml ps -q | xargs -r docker inspect \
--format '{{.Name}} {{with .State.Health}}{{.Status}}{{range .Log}} [exit {{.ExitCode}}] {{.Output}}{{end}}{{end}}'
docker compose -f docker-compose.e2e.yml logs --no-color --tail 500
- name: Upload test results
uses: actions/upload-artifact@v7
if: always()
with:
name: e2e-test-results-${{ matrix.suite.name }}
path: |
e2e/playwright-report/
e2e/test-results/
retention-days: 7
- name: Stop services
if: always()
working-directory: e2e
run: make down