diff --git a/.gitea/workflows/cicd.yaml b/.gitea/workflows/cicd.yaml index a478d6c..c5266c7 100644 --- a/.gitea/workflows/cicd.yaml +++ b/.gitea/workflows/cicd.yaml @@ -273,6 +273,34 @@ jobs: - *failure_diagnostics_step + build-cicd-postmortem: + name: Build CICD Image Failure Postmortem + runs-on: ubuntu-act + needs: build_cicd + if: always() && needs.build_cicd.result == 'failure' + timeout-minutes: 10 + steps: + - name: Identify runner + run: | + echo "=== Runner Identity ===" + echo "runner_name=${RUNNER_NAME:-unknown}" + echo "runner_hostname=${HOSTNAME:-unknown}" + echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)" + + - name: Build CICD postmortem diagnostics + run: | + echo "=== Build CICD Postmortem ===" + echo "build_cicd_result=${{ needs.build_cicd.result }}" + uname -a || true + cat /etc/os-release 2>/dev/null || true + df -h || true + free -h || true + if command -v docker >/dev/null 2>&1; then + docker version || true + docker info || true + fi + timeout 10 curl -fsSIL "https://${GITEA_REGISTRY}/v2/" || true + backend-tests: name: Backend Tests runs-on: ubuntu-act @@ -488,11 +516,170 @@ jobs: name: CICD Tests Complete runs-on: ubuntu-act needs: [backend-tests, precommit-tests, frontend-tests, xdoctest] + if: always() steps: - name: Confirm all source lanes passed run: | + set -euo pipefail + backend_status="${{ needs.backend-tests.result }}" + precommit_status="${{ needs.precommit-tests.result }}" + frontend_status="${{ needs.frontend-tests.result }}" + xdoctest_status="${{ needs.xdoctest.result }}" + + echo "backend-tests=${backend_status}" + echo "precommit-tests=${precommit_status}" + echo "frontend-tests=${frontend_status}" + echo "xdoctest=${xdoctest_status}" + + if [ "${backend_status}" != "success" ] || [ "${precommit_status}" != "success" ] || [ "${frontend_status}" != "success" ] || [ "${xdoctest_status}" != "success" ]; then + echo "❌ One or more CICD source lanes failed" + exit 1 + fi + echo "✅ Source checks complete" + - *failure_diagnostics_step + + build-backend-base-image: + name: Build Backend Base Image + runs-on: ubuntu-act-8gb + needs: [build_cicd, cicd-tests-complete] + outputs: + backend_base_tag_ref: ${{ steps.backend_base_ref.outputs.backend_base_tag_ref }} + backend_base_digest_ref: ${{ steps.backend_base_ref.outputs.backend_base_digest_ref }} + steps: + - name: Resolve head SHA + id: meta + run: | + echo "head_sha=${{ needs.build_cicd.outputs.head_sha }}" >> "$GITHUB_OUTPUT" + + - name: Minimal checkout for backend base image inputs + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + HEAD_SHA: ${{ steps.meta.outputs.head_sha }} + run: | + set -euo pipefail + umask 077 + trap 'rm -f ~/.ssh/id_rsa' EXIT + + if ! grep -q "${GITEA_REGISTRY_HOST}" /etc/hosts; then + echo "${GITEA_REGISTRY_IP} ${GITEA_REGISTRY_HOST}" >> /etc/hosts + fi + + mkdir -p ~/.ssh + echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -p "${GITEA_SSH_PORT}" "${GITEA_SSH_HOST}" >> ~/.ssh/known_hosts 2>/dev/null + + GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \ + git clone --depth 1 --no-checkout "${GITEA_REPO_SSH_URL}" . + + if GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \ + git fetch --depth 1 origin "${HEAD_SHA}" >/dev/null 2>&1; then + git checkout FETCH_HEAD -- Dockerfile.backend backend/pyproject.toml backend/uv.lock + else + git checkout HEAD -- Dockerfile.backend backend/pyproject.toml backend/uv.lock + fi + + - name: Build and push backend base image + id: backend_base_ref + env: + PACKAGE_ACCESS_TOKEN: ${{ secrets.PACKAGE_ACCESS_TOKEN }} + REGISTRY_USER: ${{ github.actor }} + HEAD_SHA: ${{ steps.meta.outputs.head_sha }} + run: | + set -euo pipefail + + echo "${PACKAGE_ACCESS_TOKEN}" | docker login "http://${GITEA_REGISTRY}" -u "${REGISTRY_USER}" --password-stdin + BACKEND_BASE_REPO="${GITEA_REGISTRY}/darkhelm.org/plex-playlist-backend-base" + BACKEND_BASE_TAG_REF="${BACKEND_BASE_REPO}:${HEAD_SHA}" + + docker build -f Dockerfile.backend --target dependencies -t plex-playlist-backend-base:"${HEAD_SHA}" . + docker tag plex-playlist-backend-base:"${HEAD_SHA}" "${BACKEND_BASE_TAG_REF}" + docker push "${BACKEND_BASE_TAG_REF}" + docker pull "${BACKEND_BASE_TAG_REF}" >/dev/null + + BACKEND_BASE_DIGEST_REF="$({ docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "${BACKEND_BASE_TAG_REF}" | grep "^${BACKEND_BASE_REPO}@sha256:" | head -n 1; } || true)" + if [ -z "${BACKEND_BASE_DIGEST_REF}" ]; then + echo "❌ Unable to resolve backend base image digest" + exit 1 + fi + + echo "backend_base_tag_ref=${BACKEND_BASE_TAG_REF}" >> "$GITHUB_OUTPUT" + echo "backend_base_digest_ref=${BACKEND_BASE_DIGEST_REF}" >> "$GITHUB_OUTPUT" + + - *failure_diagnostics_step + + build-frontend-base-image: + name: Build Frontend Base Image + runs-on: ubuntu-act-8gb + needs: [build_cicd, cicd-tests-complete] + outputs: + frontend_base_tag_ref: ${{ steps.frontend_base_ref.outputs.frontend_base_tag_ref }} + frontend_base_digest_ref: ${{ steps.frontend_base_ref.outputs.frontend_base_digest_ref }} + steps: + - name: Resolve head SHA + id: meta + run: | + echo "head_sha=${{ needs.build_cicd.outputs.head_sha }}" >> "$GITHUB_OUTPUT" + + - name: Minimal checkout for frontend base image inputs + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + HEAD_SHA: ${{ steps.meta.outputs.head_sha }} + run: | + set -euo pipefail + umask 077 + trap 'rm -f ~/.ssh/id_rsa' EXIT + + if ! grep -q "${GITEA_REGISTRY_HOST}" /etc/hosts; then + echo "${GITEA_REGISTRY_IP} ${GITEA_REGISTRY_HOST}" >> /etc/hosts + fi + + mkdir -p ~/.ssh + echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -p "${GITEA_SSH_PORT}" "${GITEA_SSH_HOST}" >> ~/.ssh/known_hosts 2>/dev/null + + GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \ + git clone --depth 1 --no-checkout "${GITEA_REPO_SSH_URL}" . + + if GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \ + git fetch --depth 1 origin "${HEAD_SHA}" >/dev/null 2>&1; then + git checkout FETCH_HEAD -- Dockerfile.frontend frontend/package.json frontend/yarn.lock frontend/.yarnrc.yml + else + git checkout HEAD -- Dockerfile.frontend frontend/package.json frontend/yarn.lock frontend/.yarnrc.yml + fi + + - name: Build and push frontend base image + id: frontend_base_ref + env: + PACKAGE_ACCESS_TOKEN: ${{ secrets.PACKAGE_ACCESS_TOKEN }} + REGISTRY_USER: ${{ github.actor }} + HEAD_SHA: ${{ steps.meta.outputs.head_sha }} + run: | + set -euo pipefail + + echo "${PACKAGE_ACCESS_TOKEN}" | docker login "http://${GITEA_REGISTRY}" -u "${REGISTRY_USER}" --password-stdin + FRONTEND_BASE_REPO="${GITEA_REGISTRY}/darkhelm.org/plex-playlist-frontend-base" + FRONTEND_BASE_TAG_REF="${FRONTEND_BASE_REPO}:${HEAD_SHA}" + + docker build -f Dockerfile.frontend --target deps -t plex-playlist-frontend-base:"${HEAD_SHA}" . + docker tag plex-playlist-frontend-base:"${HEAD_SHA}" "${FRONTEND_BASE_TAG_REF}" + docker push "${FRONTEND_BASE_TAG_REF}" + docker pull "${FRONTEND_BASE_TAG_REF}" >/dev/null + + FRONTEND_BASE_DIGEST_REF="$({ docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "${FRONTEND_BASE_TAG_REF}" | grep "^${FRONTEND_BASE_REPO}@sha256:" | head -n 1; } || true)" + if [ -z "${FRONTEND_BASE_DIGEST_REF}" ]; then + echo "❌ Unable to resolve frontend base image digest" + exit 1 + fi + + echo "frontend_base_tag_ref=${FRONTEND_BASE_TAG_REF}" >> "$GITHUB_OUTPUT" + echo "frontend_base_digest_ref=${FRONTEND_BASE_DIGEST_REF}" >> "$GITHUB_OUTPUT" + + - *failure_diagnostics_step + build-integration-tester-image: name: Build Integration Tester Image runs-on: ubuntu-act-8gb @@ -696,9 +883,10 @@ jobs: - *failure_diagnostics_step runtime_images: - name: Build and Publish Runtime Images + name: Build and Publish Runtime Images (Legacy Disabled) runs-on: ubuntu-act-8gb - needs: [build_cicd, cicd-tests-complete] + if: false + needs: [build_cicd, cicd-tests-complete, build-backend-base-image, build-frontend-base-image] timeout-minutes: 45 outputs: head_sha: ${{ steps.meta.outputs.head_sha }} @@ -903,22 +1091,264 @@ jobs: - *failure_diagnostics_step + build-backend-main-image: + name: Build Backend Main Image + runs-on: ubuntu-act-8gb + needs: [build_cicd, cicd-tests-complete, build-backend-base-image] + outputs: + head_sha: ${{ steps.meta.outputs.head_sha }} + deployable_backend_tag_ref: ${{ steps.deployable_backend_ref.outputs.deployable_backend_tag_ref }} + deployable_backend_digest_ref: ${{ steps.deployable_backend_ref.outputs.deployable_backend_digest_ref }} + steps: + - name: Resolve head SHA + id: meta + run: | + echo "head_sha=${{ needs.build_cicd.outputs.head_sha }}" >> "$GITHUB_OUTPUT" + + - name: Minimal checkout for backend main image inputs + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + HEAD_SHA: ${{ steps.meta.outputs.head_sha }} + run: | + set -euo pipefail + umask 077 + trap 'rm -f ~/.ssh/id_rsa' EXIT + + if ! grep -q "${GITEA_REGISTRY_HOST}" /etc/hosts; then + echo "${GITEA_REGISTRY_IP} ${GITEA_REGISTRY_HOST}" >> /etc/hosts + fi + + mkdir -p ~/.ssh + echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -p "${GITEA_SSH_PORT}" "${GITEA_SSH_HOST}" >> ~/.ssh/known_hosts 2>/dev/null + + GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \ + git clone --depth 1 --no-checkout "${GITEA_REPO_SSH_URL}" . + + if GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \ + git fetch --depth 1 origin "${HEAD_SHA}" >/dev/null 2>&1; then + git checkout FETCH_HEAD -- \ + .dockerignore \ + Dockerfile.backend \ + backend \ + scripts/verify-deployable-image-purity.sh + else + git checkout HEAD -- \ + .dockerignore \ + Dockerfile.backend \ + backend \ + scripts/verify-deployable-image-purity.sh + fi + + - name: Build and push backend main image + id: deployable_backend_ref + env: + PACKAGE_ACCESS_TOKEN: ${{ secrets.PACKAGE_ACCESS_TOKEN }} + REGISTRY_USER: ${{ github.actor }} + HEAD_SHA: ${{ steps.meta.outputs.head_sha }} + run: | + set -euo pipefail + + retry_registry_op() { + op_name="$1" + image_ref="$2" + attempts="${3:-5}" + backoff="${4:-3}" + attempt=1 + + while [ "${attempt}" -le "${attempts}" ]; do + echo "${op_name} attempt ${attempt}/${attempts} for ${image_ref}" + if [ "${op_name}" = "push" ]; then + if docker push "${image_ref}"; then + return 0 + fi + else + if docker pull "${image_ref}" >/dev/null; then + return 0 + fi + fi + + if [ "${attempt}" -lt "${attempts}" ]; then + sleep_seconds=$((backoff * attempt)) + echo "Retrying in ${sleep_seconds}s" + sleep "${sleep_seconds}" + fi + attempt=$((attempt + 1)) + done + + return 1 + } + + docker build -f Dockerfile.backend -t deployable-backend:"${HEAD_SHA}" . + bash ./scripts/verify-deployable-image-purity.sh --image deployable-backend:"${HEAD_SHA}" --profile backend + + echo "${PACKAGE_ACCESS_TOKEN}" | docker login "http://${GITEA_REGISTRY}" -u "${REGISTRY_USER}" --password-stdin + DEPLOYABLE_BACKEND_REPO="${GITEA_REGISTRY}/darkhelm.org/deployable-backend" + DEPLOYABLE_BACKEND_TAG_REF="${DEPLOYABLE_BACKEND_REPO}:${HEAD_SHA}" + docker tag "deployable-backend:${HEAD_SHA}" "${DEPLOYABLE_BACKEND_TAG_REF}" + + retry_registry_op push "${DEPLOYABLE_BACKEND_TAG_REF}" 5 4 + retry_registry_op pull "${DEPLOYABLE_BACKEND_TAG_REF}" 5 3 + + DEPLOYABLE_BACKEND_DIGEST_REF="$({ docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "${DEPLOYABLE_BACKEND_TAG_REF}" | grep "^${DEPLOYABLE_BACKEND_REPO}@sha256:" | head -n 1; } || true)" + if [ -z "${DEPLOYABLE_BACKEND_DIGEST_REF}" ]; then + echo "❌ Unable to resolve backend main digest" + exit 1 + fi + + echo "deployable_backend_tag_ref=${DEPLOYABLE_BACKEND_TAG_REF}" >> "$GITHUB_OUTPUT" + echo "deployable_backend_digest_ref=${DEPLOYABLE_BACKEND_DIGEST_REF}" >> "$GITHUB_OUTPUT" + + - *failure_diagnostics_step + + build-frontend-main-image: + name: Build Frontend Main Image + runs-on: ubuntu-act-8gb + needs: [build_cicd, cicd-tests-complete, build-frontend-base-image] + outputs: + head_sha: ${{ steps.meta.outputs.head_sha }} + deployable_frontend_tag_ref: ${{ steps.deployable_frontend_ref.outputs.deployable_frontend_tag_ref }} + deployable_frontend_digest_ref: ${{ steps.deployable_frontend_ref.outputs.deployable_frontend_digest_ref }} + steps: + - name: Resolve head SHA + id: meta + run: | + echo "head_sha=${{ needs.build_cicd.outputs.head_sha }}" >> "$GITHUB_OUTPUT" + + - name: Minimal checkout for frontend main image inputs + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + HEAD_SHA: ${{ steps.meta.outputs.head_sha }} + run: | + set -euo pipefail + umask 077 + trap 'rm -f ~/.ssh/id_rsa' EXIT + + if ! grep -q "${GITEA_REGISTRY_HOST}" /etc/hosts; then + echo "${GITEA_REGISTRY_IP} ${GITEA_REGISTRY_HOST}" >> /etc/hosts + fi + + mkdir -p ~/.ssh + echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -p "${GITEA_SSH_PORT}" "${GITEA_SSH_HOST}" >> ~/.ssh/known_hosts 2>/dev/null + + GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \ + git clone --depth 1 --no-checkout "${GITEA_REPO_SSH_URL}" . + + if GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \ + git fetch --depth 1 origin "${HEAD_SHA}" >/dev/null 2>&1; then + git checkout FETCH_HEAD -- \ + .dockerignore \ + Dockerfile.frontend \ + frontend \ + scripts/verify-deployable-image-purity.sh + else + git checkout HEAD -- \ + .dockerignore \ + Dockerfile.frontend \ + frontend \ + scripts/verify-deployable-image-purity.sh + fi + + - name: Build and push frontend main image + id: deployable_frontend_ref + env: + PACKAGE_ACCESS_TOKEN: ${{ secrets.PACKAGE_ACCESS_TOKEN }} + REGISTRY_USER: ${{ github.actor }} + HEAD_SHA: ${{ steps.meta.outputs.head_sha }} + run: | + set -euo pipefail + + retry_registry_op() { + op_name="$1" + image_ref="$2" + attempts="${3:-5}" + backoff="${4:-3}" + attempt=1 + + while [ "${attempt}" -le "${attempts}" ]; do + echo "${op_name} attempt ${attempt}/${attempts} for ${image_ref}" + if [ "${op_name}" = "push" ]; then + if docker push "${image_ref}"; then + return 0 + fi + else + if docker pull "${image_ref}" >/dev/null; then + return 0 + fi + fi + + if [ "${attempt}" -lt "${attempts}" ]; then + sleep_seconds=$((backoff * attempt)) + echo "Retrying in ${sleep_seconds}s" + sleep "${sleep_seconds}" + fi + attempt=$((attempt + 1)) + done + + return 1 + } + + docker build -f Dockerfile.frontend --target production -t deployable-frontend:"${HEAD_SHA}" . + bash ./scripts/verify-deployable-image-purity.sh --image deployable-frontend:"${HEAD_SHA}" --profile frontend + + echo "${PACKAGE_ACCESS_TOKEN}" | docker login "http://${GITEA_REGISTRY}" -u "${REGISTRY_USER}" --password-stdin + DEPLOYABLE_FRONTEND_REPO="${GITEA_REGISTRY}/darkhelm.org/deployable-frontend" + DEPLOYABLE_FRONTEND_TAG_REF="${DEPLOYABLE_FRONTEND_REPO}:${HEAD_SHA}" + docker tag "deployable-frontend:${HEAD_SHA}" "${DEPLOYABLE_FRONTEND_TAG_REF}" + + retry_registry_op push "${DEPLOYABLE_FRONTEND_TAG_REF}" 5 4 + retry_registry_op pull "${DEPLOYABLE_FRONTEND_TAG_REF}" 5 3 + + DEPLOYABLE_FRONTEND_DIGEST_REF="$({ docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "${DEPLOYABLE_FRONTEND_TAG_REF}" | grep "^${DEPLOYABLE_FRONTEND_REPO}@sha256:" | head -n 1; } || true)" + if [ -z "${DEPLOYABLE_FRONTEND_DIGEST_REF}" ]; then + echo "❌ Unable to resolve frontend main digest" + exit 1 + fi + + echo "deployable_frontend_tag_ref=${DEPLOYABLE_FRONTEND_TAG_REF}" >> "$GITHUB_OUTPUT" + echo "deployable_frontend_digest_ref=${DEPLOYABLE_FRONTEND_DIGEST_REF}" >> "$GITHUB_OUTPUT" + + - *failure_diagnostics_step + production-images-complete: name: Production Images Complete - runs-on: ubuntu-act - needs: runtime_images + runs-on: ubuntu-act-8gb + needs: [build-backend-main-image, build-frontend-main-image, build-integration-tester-image, build-e2e-tester-image] + if: always() + timeout-minutes: 10 steps: - - name: Confirm deployable runtime images are published + - name: Confirm required production/tester images are complete run: | - echo "✅ Deployable backend/frontend runtime images are available" + set -euo pipefail + backend_main_status="${{ needs.build-backend-main-image.result }}" + frontend_main_status="${{ needs.build-frontend-main-image.result }}" + integration_tester_status="${{ needs.build-integration-tester-image.result }}" + e2e_tester_status="${{ needs.build-e2e-tester-image.result }}" + + echo "build-backend-main-image=${backend_main_status}" + echo "build-frontend-main-image=${frontend_main_status}" + echo "build-integration-tester-image=${integration_tester_status}" + echo "build-e2e-tester-image=${e2e_tester_status}" + + if [ "${backend_main_status}" != "success" ] || [ "${frontend_main_status}" != "success" ] || [ "${integration_tester_status}" != "success" ] || [ "${e2e_tester_status}" != "success" ]; then + echo "❌ Production Images Complete gate failed" + exit 1 + fi + + echo "✅ Production, integration tester, and E2E tester images are complete" + + - *failure_diagnostics_step runtime-images-postmortem: name: Runtime Images Failure Postmortem # Run on the broader runner pool so we can still capture diagnostics when # runtime_images fails during platform "Set up job" before any step executes. - runs-on: ubuntu-act - needs: runtime_images - if: always() && needs.runtime_images.result == 'failure' + runs-on: ubuntu-act-8gb + needs: [runtime_images, build-backend-main-image, build-frontend-main-image, build-integration-tester-image, build-e2e-tester-image] + if: always() && (needs.runtime_images.result == 'failure' || needs['build-backend-main-image'].result == 'failure' || needs['build-frontend-main-image'].result == 'failure' || needs['build-integration-tester-image'].result == 'failure' || needs['build-e2e-tester-image'].result == 'failure') timeout-minutes: 10 steps: - name: Identify runner @@ -1135,7 +1565,7 @@ jobs: # Pin integration tests to high-memory worker to reduce setup-stage runner churn. runs-on: ubuntu-act-8gb timeout-minutes: 20 - needs: [runtime_images, production-images-complete, build-integration-tester-image] + needs: [build_cicd, production-images-complete, build-backend-main-image, build-frontend-main-image, build-integration-tester-image] steps: - name: Identify runner run: | @@ -1152,9 +1582,9 @@ jobs: - *configure_registry_host_step - name: Run runtime black-box integration checks via compose env: - HEAD_SHA: ${{ needs.runtime_images.outputs.head_sha }} - DEPLOYABLE_BACKEND_TAG_REF: ${{ needs.runtime_images.outputs.deployable_backend_tag_ref }} - DEPLOYABLE_BACKEND_DIGEST_REF: ${{ needs.runtime_images.outputs.deployable_backend_digest_ref }} + HEAD_SHA: ${{ needs.build_cicd.outputs.head_sha }} + DEPLOYABLE_BACKEND_TAG_REF: ${{ needs.build-backend-main-image.outputs.deployable_backend_tag_ref }} + DEPLOYABLE_BACKEND_DIGEST_REF: ${{ needs.build-backend-main-image.outputs.deployable_backend_digest_ref }} INTEGRATION_TESTER_DIGEST_REF: ${{ needs.build-integration-tester-image.outputs.integration_tester_digest_ref }} run: | set -euo pipefail @@ -1365,12 +1795,33 @@ jobs: - *failure_diagnostics_step + integration-tests-postmortem: + name: Integration Tests Failure Postmortem + runs-on: ubuntu-act + needs: integration-tests + if: always() && needs.integration-tests.result == 'failure' + timeout-minutes: 10 + steps: + - name: Integration postmortem diagnostics + run: | + echo "=== Integration Tests Postmortem ===" + echo "integration_tests_result=${{ needs.integration-tests.result }}" + uname -a || true + cat /etc/os-release 2>/dev/null || true + df -h || true + free -h || true + if command -v docker >/dev/null 2>&1; then + docker version || true + docker info || true + fi + timeout 10 curl -fsSIL "https://${GITEA_REGISTRY}/v2/" || true + e2e-tests: name: End-to-End Tests # Use ubuntu-act runner pool for consistent availability. runs-on: ubuntu-act-8gb timeout-minutes: 45 - needs: [runtime_images, production-images-complete, build-integration-tester-image, build-e2e-tester-image] + needs: [build_cicd, production-images-complete, build-backend-main-image, build-frontend-main-image, build-integration-tester-image, build-e2e-tester-image] steps: - name: Identify runner run: | @@ -1387,12 +1838,12 @@ jobs: - *configure_registry_host_step - name: Run E2E tests against runtime services via compose env: - HEAD_SHA: ${{ needs.runtime_images.outputs.head_sha }} + HEAD_SHA: ${{ needs.build_cicd.outputs.head_sha }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} - DEPLOYABLE_BACKEND_TAG_REF: ${{ needs.runtime_images.outputs.deployable_backend_tag_ref }} - DEPLOYABLE_BACKEND_DIGEST_REF: ${{ needs.runtime_images.outputs.deployable_backend_digest_ref }} - DEPLOYABLE_FRONTEND_TAG_REF: ${{ needs.runtime_images.outputs.deployable_frontend_tag_ref }} - DEPLOYABLE_FRONTEND_DIGEST_REF: ${{ needs.runtime_images.outputs.deployable_frontend_digest_ref }} + DEPLOYABLE_BACKEND_TAG_REF: ${{ needs.build-backend-main-image.outputs.deployable_backend_tag_ref }} + DEPLOYABLE_BACKEND_DIGEST_REF: ${{ needs.build-backend-main-image.outputs.deployable_backend_digest_ref }} + DEPLOYABLE_FRONTEND_TAG_REF: ${{ needs.build-frontend-main-image.outputs.deployable_frontend_tag_ref }} + DEPLOYABLE_FRONTEND_DIGEST_REF: ${{ needs.build-frontend-main-image.outputs.deployable_frontend_digest_ref }} INTEGRATION_TESTER_DIGEST_REF: ${{ needs.build-integration-tester-image.outputs.integration_tester_digest_ref }} E2E_TESTER_DIGEST_REF: ${{ needs.build-e2e-tester-image.outputs.e2e_tester_digest_ref }} run: | @@ -1720,3 +2171,24 @@ jobs: find "${ARTIFACT_DIR}" -maxdepth 2 -type f | sort || true - *failure_diagnostics_step + + e2e-tests-postmortem: + name: E2E Tests Failure Postmortem + runs-on: ubuntu-act + needs: e2e-tests + if: always() && needs.e2e-tests.result == 'failure' + timeout-minutes: 10 + steps: + - name: E2E postmortem diagnostics + run: | + echo "=== E2E Tests Postmortem ===" + echo "e2e_tests_result=${{ needs.e2e-tests.result }}" + uname -a || true + cat /etc/os-release 2>/dev/null || true + df -h || true + free -h || true + if command -v docker >/dev/null 2>&1; then + docker version || true + docker info || true + fi + timeout 10 curl -fsSIL "https://${GITEA_REGISTRY}/v2/" || true diff --git a/Dockerfile.frontend b/Dockerfile.frontend index b66cb41..662b7ae 100644 --- a/Dockerfile.frontend +++ b/Dockerfile.frontend @@ -1,11 +1,13 @@ # Frontend Dockerfile for Vue/Vite TypeScript project -FROM node:24-alpine AS build +FROM node:24-alpine AS deps WORKDIR /app COPY frontend/package.json frontend/yarn.lock frontend/.yarnrc.yml ./ RUN corepack enable && yarn install --immutable +FROM deps AS build + COPY frontend/ . RUN yarn build