Files
CaddyProxyManager/.github/workflows/pr-e2e.yml
Pacerino 36b0672d75 [CI] Add per-PR E2E deployment and teardown workflow
Deploy the PR image to the self-hosted runner in API mode and run a full
end-to-end test (login, host creation, live reverse-proxy to whoami, basic
auth plugin, module discovery). The environment is published on a fixed port
3001 and left running until the PR is closed or a /teardown comment is posted.

- docker-compose.e2e.yml: cpm (API mode) + whoami upstream, per-PR isolation
- test/e2e/run.sh: full api-mode E2E assertions against the live container
- .github/workflows/pr-e2e.yml: deploy/E2E, teardown on close, /teardown comment
2026-06-15 22:59:20 +02:00

146 lines
5.6 KiB
YAML

name: PR E2E
# Deploys the PR's image to the self-hosted runner and runs a full end-to-end
# test against it in API mode. The environment is left running until the PR is
# closed (or torn down early via a "/teardown" comment) so it can be inspected
# through a Cloudflare Tunnel.
on:
pull_request:
types: [opened, synchronize, reopened, closed]
issue_comment:
types: [created]
# Only ever run one PR environment at a time (fixed port 3001).
concurrency:
group: pr-e2e
cancel-in-progress: false
env:
COMPOSE_FILE: docker-compose.e2e.yml
CPM_PORT: "3001"
jobs:
deploy-e2e:
name: Deploy & E2E
runs-on: self-hosted
# Own PRs only (not forks), and not on the closed event.
if: >
github.event_name == 'pull_request' &&
github.event.action != 'closed' &&
github.event.pull_request.head.repo.full_name == github.repository
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
CPM_ADMIN_EMAIL: ${{ secrets.E2E_ADMIN_EMAIL || 'admin@example.com' }}
CPM_ADMIN_PASSWORD: ${{ secrets.E2E_ADMIN_PASSWORD || 'changeme' }}
steps:
- uses: actions/checkout@v4
# Fail fast if a *different* PR's stack already holds the fixed port.
- name: Ensure port 3001 is free for this PR
run: |
set -euo pipefail
project="cpm-pr-${PR_NUMBER}"
# Any container publishing :3001 that is NOT this PR's stack blocks us.
holder=$(docker ps --filter "publish=${CPM_PORT}" --format '{{.Names}}' | grep -v "^cpm-pr-${PR_NUMBER}" || true)
if [ -n "$holder" ]; then
echo "::error::Port ${CPM_PORT} is in use by another PR environment: $holder"
echo "Tear it down first (close that PR or comment /teardown there)."
exit 1
fi
echo "Port ${CPM_PORT} is available for ${project}."
- name: Build PR image
run: |
set -euo pipefail
IMAGE="cpm-pr-${PR_NUMBER}:${GITHUB_SHA::8}"
docker build \
--build-arg VERSION="pr-${PR_NUMBER}" \
--build-arg COMMIT="${GITHUB_SHA::8}" \
-t "$IMAGE" .
echo "CPM_IMAGE=$IMAGE" >> "$GITHUB_ENV"
- name: Bring up the environment
run: |
set -euo pipefail
# Recreate cleanly (handles the synchronize re-run case).
docker compose -p "cpm-pr-${PR_NUMBER}" -f "$COMPOSE_FILE" up -d --force-recreate
- name: Run E2E tests
env:
CPM_URL: http://localhost:3001
CADDY_URL: http://localhost:8080
ADMIN_EMAIL: ${{ env.CPM_ADMIN_EMAIL }}
ADMIN_PASSWORD: ${{ env.CPM_ADMIN_PASSWORD }}
TEST_DOMAIN: app.e2e.local
run: bash test/e2e/run.sh
- name: Dump logs on failure
if: failure()
run: docker compose -p "cpm-pr-${PR_NUMBER}" -f "$COMPOSE_FILE" logs --no-color || true
- name: Comment environment URL
if: success()
uses: actions/github-script@v7
with:
script: |
const n = context.payload.pull_request.number;
const marker = "<!-- cpm-e2e-env -->";
const body = `${marker}\n✅ **PR E2E environment is up** (port \`3001\`).\n\n` +
`Point your Cloudflare Tunnel at \`localhost:3001\` to inspect it.\n\n` +
`Comment \`/teardown\` to stop it early; it is otherwise removed when the PR closes.`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner, repo: context.repo.repo, issue_number: n,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body });
} else {
await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: n, body });
}
teardown-on-close:
name: Teardown (PR closed)
runs-on: self-hosted
if: >
github.event_name == 'pull_request' &&
github.event.action == 'closed'
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
steps:
- uses: actions/checkout@v4
- name: Tear down the environment
run: |
set -euo pipefail
docker compose -p "cpm-pr-${PR_NUMBER}" -f "$COMPOSE_FILE" down -v --remove-orphans || true
docker image rm "cpm-pr-${PR_NUMBER}:${GITHUB_SHA::8}" 2>/dev/null || true
echo "Environment for PR #${PR_NUMBER} torn down."
teardown-on-comment:
name: Teardown (/teardown comment)
runs-on: self-hosted
# Only on PR comments containing "/teardown", authored by the repo owner.
if: >
github.event_name == 'issue_comment' &&
github.event.issue.pull_request != null &&
contains(github.event.comment.body, '/teardown') &&
github.event.comment.user.login == github.repository_owner
env:
PR_NUMBER: ${{ github.event.issue.number }}
steps:
- uses: actions/checkout@v4
- name: Tear down the environment
run: |
set -euo pipefail
docker compose -p "cpm-pr-${PR_NUMBER}" -f "$COMPOSE_FILE" down -v --remove-orphans || true
echo "Environment for PR #${PR_NUMBER} torn down."
- name: Acknowledge
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: ${{ env.PR_NUMBER }},
body: "🧹 PR E2E environment torn down.",
});