honcho/.github/workflows/unified-tests.yml

194 lines
7.0 KiB
YAML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

name: Unified Tests (Fly Runner)
on:
push:
branches: [main]
paths:
- 'src/**'
- 'tests/**'
# TEMPORARY: run on PRs to test tag-based secret resolution. REVERT before merging.
# No `paths` filter on purpose so a workflow-only PR still triggers this.
pull_request:
branches: [main]
permissions:
contents: read
actions: read
jobs:
start-runner:
name: Start Fly Runner
uses: ./.github/workflows/start-fly-runner.yml
secrets: inherit
unified-tests:
name: Run Unified Tests
runs-on: ${{ fromJSON(format('[{0}]', needs.start-runner.outputs.runner-labels)) }}
needs: start-runner
if: needs.start-runner.outputs.runner-ready == 'true'
timeout-minutes: 90
environment: unified-tests
permissions:
id-token: write # Required for OIDC authentication with AWS
contents: read
env:
PYTHONUNBUFFERED: "1"
TEST_DISCORD_WEBHOOK_URL: ${{ secrets.TEST_DISCORD_WEBHOOK_URL }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_OIDC_ROLE_ARN }}
aws-region: us-east-1
role-duration-seconds: 43200 # 12 hours
- name: Resolve secret id from latest git tag
id: resolve-secret
env:
SECRET_PREFIX: ${{ secrets.STAGING_SECRET_PREFIX }}
run: |
set -euo pipefail
: "${SECRET_PREFIX:?STAGING_SECRET_PREFIX secret is not set for this environment}"
# Keep the secret-name prefix out of public CI logs.
echo "::add-mask::${SECRET_PREFIX}"
# Two newest v<major.minor.patch> tags, highest first.
versions="$(git ls-remote --tags origin 'v*' \
| sed -n 's#.*refs/tags/v\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)$#\1#p' \
| sort -t. -k1,1nr -k2,2nr -k3,3nr -u)"
latest="$(printf '%s\n' "$versions" | sed -n '1p')"
second="$(printf '%s\n' "$versions" | sed -n '2p')"
if [ -z "${latest:-}" ]; then
echo "::error::No v<semver> git tags found to resolve a secret version"
exit 1
fi
# Try the latest tag's secret; on a genuine NotFound, fall back to the second-latest.
secret_id=""
for ver in "$latest" "$second"; do
[ -z "$ver" ] && continue
candidate="${SECRET_PREFIX}${ver}"
if err="$(aws secretsmanager get-secret-value \
--secret-id "$candidate" --query SecretString --output text 2>&1 >/dev/null)"; then
secret_id="$candidate"
echo "Using secret for version ${ver}"
break
elif printf '%s' "$err" | grep -q 'ResourceNotFoundException'; then
echo "::warning::No secret published for version ${ver}; trying next"
continue
else
# Log only the AWS error code (e.g. AccessDeniedException) — never the
# raw message, which embeds the account id, role ARN, and secret ARN.
code="$(printf '%s' "$err" | sed -n 's/.*An error occurred (\([^)]*\)).*/\1/p' | head -n1)"
echo "::error::Failed to read secret for version ${ver}: ${code:-unknown error}"
exit 1
fi
done
if [ -z "$secret_id" ]; then
echo "::error::No secret found for the latest or second-latest tag"
exit 1
fi
echo "::add-mask::${secret_id}"
echo "secret-id=${secret_id}" >> "$GITHUB_OUTPUT"
- name: Fetch secrets from AWS Secrets Manager
uses: aws-actions/aws-secretsmanager-get-secrets@v2
with:
secret-ids: |
,${{ steps.resolve-secret.outputs.secret-id }}
parse-json-secrets: true
- name: Verify Docker is available
run: docker info
- name: Verify uv and Python
run: |
uv --version
python3.12 --version
which python3.12
- name: Install the project
run: uv sync --all-extras
- name: Run unified tests
run: uv run python -m tests.unified.run
cleanup-machine:
name: Cleanup Fly Machine and Runner
runs-on: ubuntu-latest
needs: [start-runner, unified-tests]
if: always() && needs.start-runner.outputs.machine-id != ''
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_TESTING }}
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_ACTIONS }}
FLY_RUNNER_APP: ivysaur
steps:
- name: Setup Fly CLI
uses: superfly/flyctl-actions/setup-flyctl@1.5
- name: Cleanup fly machine
run: |
set -euo pipefail
MACHINE_ID="${{ needs.start-runner.outputs.machine-id }}"
if [ -z "$MACHINE_ID" ]; then
echo "No machine ID provided, skipping Fly cleanup."
exit 0
fi
echo "🧹 Cleaning up machine: $MACHINE_ID"
flyctl machines stop "$MACHINE_ID" -a "$FLY_RUNNER_APP" || echo "Machine may already be stopped"
flyctl machines destroy "$MACHINE_ID" -a "$FLY_RUNNER_APP" --force || echo "Failed to destroy machine"
- name: Cleanup GitHub runner
run: |
set -euo pipefail
RUNNER_NAME="${{ needs.start-runner.outputs.runner-name }}"
FALLBACK_LABEL="${{ github.run_id }}"
echo "🗑️ Cleaning up GitHub runner (name: ${RUNNER_NAME:-unknown}, label: ${FALLBACK_LABEL})"
RUNNERS_RESPONSE=$(curl -s \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/actions/runners")
if echo "$RUNNERS_RESPONSE" | grep -q '"message"'; then
echo "⚠️ Failed to fetch runners: $(echo "$RUNNERS_RESPONSE" | jq -r '.message')"
exit 0
fi
RUNNER_ID="" 
if [ -n "$RUNNER_NAME" ]; then
RUNNER_ID=$(echo "$RUNNERS_RESPONSE" | jq -r --arg name "$RUNNER_NAME" '.runners[]? | select(.name == $name) | .id')
fi
if [ -z "$RUNNER_ID" ]; then
RUNNER_ID=$(echo "$RUNNERS_RESPONSE" | jq -r --arg label "$FALLBACK_LABEL" '.runners[]? | select([.labels[].name] | index($label)) | .id' | head -n 1)
fi
if [ -z "$RUNNER_ID" ] || [ "$RUNNER_ID" = "null" ]; then
echo "⚠️ Runner not found, nothing to delete."
exit 0
fi
DELETE_RESPONSE=$(curl -s -w "%{http_code}" \
-X DELETE \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/${{ github.repository }}/actions/runners/$RUNNER_ID")
HTTP_CODE="${DELETE_RESPONSE: -3}"
if [ "$HTTP_CODE" = "204" ]; then
echo "✅ Successfully deleted runner."
else
echo "⚠️ Failed to delete runner. HTTP code: $HTTP_CODE"
echo "Response: ${DELETE_RESPONSE%???}"
fi