## Summary
Replace the existing source-context integration lane with backend runtime black-box integration checks that run against started deployable containers.
This change wires deployable backend image references (both commit tag and immutable digest) from the build workflow into the tests workflow, then validates runtime behavior over network endpoints.
## Why
Integration confidence should come from testing running service artifacts, not only source-mounted or in-process execution.
## What Changed
- Build workflow now:
- Publishes deployable backend image tag reference and digest reference
- Exposes both as job outputs
- Passes both references into CICD Tests dispatch inputs
- CICD Tests workflow now:
- Accepts deployable backend tag and digest inputs
- Propagates these through setup outputs
- Replaces previous integration lane behavior with runtime black-box execution:
- Starts isolated Docker network
- Starts Postgres container
- Starts backend container from digest-pinned deployable image
- Enforces tag-to-digest consistency before running checks
- Runs endpoint checks against live container:
- GET /
- GET /compatibility
- GET /health
- Captures backend/db logs and container state on failure
- Cleans up containers and network via trap
- Documentation updated:
- Runtime contract enforcement section now includes runtime black-box integration checks
- CI success summary now reflects runtime integration lane behavior
## Scope
Included:
- Backend runtime black-box integration replacement for the existing integration lane
- Digest + tag identity enforcement
- Failure diagnostics for triage
Out of scope:
- Frontend runtime smoke checks
- E2E lane redesign
## Acceptance Criteria Mapping
- Integration tests execute against runtime container endpoints: ✅
- Integration lane consumes built image references (not source-mounted execution): ✅
- Failures surface service logs and test logs for triage: ✅
## Verification
- Workflow files pass local validation checks
- Pre-commit hooks pass on committed changes
- Branch pushed and ready for PR review
## Related
- Issue: #61
- Dependency context: #66
Co-authored-by: copilotcoder <copilotcoder@darkhelm.org>
Reviewed-on: #72
309 lines
14 KiB
Docker
309 lines
14 KiB
Docker
# CICD Complete Setup - Optimized Build Order for Maximum Caching
|
||
# OPTIMIZATION STRATEGY:
|
||
# Phase 1: Extract dependency files (package.json, pyproject.toml)
|
||
# Phase 2: Install dependencies (cached layer, only invalidates when deps change)
|
||
# Phase 3: Clone full source code (doesn't bust dependency cache)
|
||
# Phase 4-6: Install packages and verify (requires full source)
|
||
#
|
||
# BENEFITS: Dependency installation ~20-30 minutes is cached across source code changes
|
||
ARG CICD_BASE_IMAGE=kankali.darkhelm.lan:3001/darkhelm.org/plex-playlist-cicd-base:latest
|
||
FROM ${CICD_BASE_IMAGE}
|
||
|
||
# Build args for cache busting
|
||
ARG GITHUB_SHA
|
||
ENV GITHUB_SHA=${GITHUB_SHA}
|
||
LABEL org.opencontainers.image.revision=${GITHUB_SHA}
|
||
|
||
# Accept build arguments for Git checkout (no secrets here!)
|
||
ARG GITHUB_SHA
|
||
|
||
# Set working directory
|
||
WORKDIR /workspace
|
||
|
||
# OPTIMIZATION: Extract dependency files first for better layer caching
|
||
# Step 1: Clone repository minimally to get dependency files only
|
||
RUN --mount=type=secret,id=ssh_private_key \
|
||
mkdir -p ~/.ssh && \
|
||
cp /run/secrets/ssh_private_key ~/.ssh/id_rsa && \
|
||
chmod 600 ~/.ssh/id_rsa && \
|
||
echo "Host kankali.darkhelm.lan" > ~/.ssh/config && \
|
||
echo " Port 2222" >> ~/.ssh/config && \
|
||
echo " StrictHostKeyChecking no" >> ~/.ssh/config && \
|
||
echo " UserKnownHostsFile /dev/null" >> ~/.ssh/config && \
|
||
chmod 600 ~/.ssh/config && \
|
||
(ssh-keyscan -p 2222 kankali.darkhelm.lan >> ~/.ssh/known_hosts 2>/dev/null || echo "Warning: ssh-keyscan failed, continuing with StrictHostKeyChecking=no") && \
|
||
echo "=== Extracting dependency files for optimized caching ===" && \
|
||
GIT_SSH_COMMAND="ssh -F ~/.ssh/config" \
|
||
git -c core.autocrlf=false -c core.eol=lf clone --depth 1 --branch main \
|
||
ssh://git@kankali.darkhelm.lan:2222/DarkHelm.org/plex-playlist.git /tmp/repo && \
|
||
git -C /tmp/repo config core.autocrlf false && \
|
||
git -C /tmp/repo config core.eol lf && \
|
||
if [ -n "$GITHUB_SHA" ]; then \
|
||
cd /tmp/repo && \
|
||
if GIT_SSH_COMMAND="ssh -F ~/.ssh/config" git fetch --depth 1 origin "$GITHUB_SHA" >/tmp/git-fetch-sha.log 2>&1 && \
|
||
git checkout "$GITHUB_SHA" >/tmp/git-checkout-sha.log 2>&1; then \
|
||
echo "✓ Checked out requested GITHUB_SHA: $GITHUB_SHA"; \
|
||
else \
|
||
echo "❌ Failed to checkout requested GITHUB_SHA: $GITHUB_SHA"; \
|
||
echo "--- git fetch output ---"; \
|
||
cat /tmp/git-fetch-sha.log 2>/dev/null || true; \
|
||
echo "--- git checkout output ---"; \
|
||
cat /tmp/git-checkout-sha.log 2>/dev/null || true; \
|
||
exit 1; \
|
||
fi; \
|
||
fi && \
|
||
# Extract only dependency files for caching optimization
|
||
mkdir -p /workspace/backend /workspace/frontend && \
|
||
cp /tmp/repo/backend/pyproject.toml /workspace/backend/ 2>/dev/null || echo "No backend pyproject.toml" && \
|
||
cp /tmp/repo/backend/uv.lock /workspace/backend/ 2>/dev/null || echo "No backend uv.lock" && \
|
||
cp /tmp/repo/frontend/package.json /workspace/frontend/ 2>/dev/null || echo "No frontend package.json" && \
|
||
cp /tmp/repo/frontend/yarn.lock /workspace/frontend/ 2>/dev/null || echo "No frontend yarn.lock" && \
|
||
cp /tmp/repo/frontend/.yarnrc.yml /workspace/frontend/ 2>/dev/null || echo "No frontend .yarnrc.yml" && \
|
||
cp /tmp/repo/.pre-commit-config.yaml /workspace/ 2>/dev/null || echo "No pre-commit config" && \
|
||
cp /tmp/repo/.markdownlint.yaml /workspace/ 2>/dev/null || echo "No markdownlint config" && \
|
||
echo "✓ Dependency files extracted for optimized layer caching" && \
|
||
rm -rf ~/.ssh
|
||
|
||
# OPTIMIZATION PHASE 1: Install backend dependencies from extracted pyproject.toml
|
||
WORKDIR /workspace/backend
|
||
ENV VIRTUAL_ENV=/workspace/backend/.venv
|
||
|
||
# Install backend dependencies first (before source code) for better caching
|
||
RUN echo "=== Installing Backend Dependencies (Phase 1: Optimized Caching) ===" && \
|
||
# Create project virtual environment
|
||
uv venv $VIRTUAL_ENV && \
|
||
# Check if base image optimization is available
|
||
echo "=== Base Image Optimization Status ===" && \
|
||
if [ -f "/opt/python-dev-tools/bin/python" ]; then \
|
||
echo "✓ Found pre-installed Python dev tools - leveraging cache" && \
|
||
uv pip list --python /opt/python-dev-tools/bin/python --format=freeze > /tmp/base-tools.txt && \
|
||
echo "Available pre-installed tools:" && \
|
||
head -10 /tmp/base-tools.txt; \
|
||
else \
|
||
echo "⚠ Pre-installed Python dev tools not found - fresh installation" && \
|
||
echo "Base image may need rebuild for optimal caching"; \
|
||
fi && \
|
||
# Install dependencies from extracted pyproject.toml (this layer will cache!)
|
||
if [ -f "pyproject.toml" ]; then \
|
||
echo "Installing project dependencies from pyproject.toml..." && \
|
||
# Create minimal source structure to satisfy package build requirements
|
||
mkdir -p src/backend && \
|
||
echo "# Temporary README for dependency caching phase" > ../README.md && \
|
||
echo "# Minimal __init__.py for build" > src/backend/__init__.py && \
|
||
# Install all dependencies including dev dependencies
|
||
if [ -f "uv.lock" ]; then \
|
||
echo "Using locked backend dependencies (uv.lock)" && \
|
||
uv sync --dev --frozen; \
|
||
else \
|
||
echo "uv.lock not found, resolving dependencies from pyproject.toml" && \
|
||
uv sync --dev; \
|
||
fi && \
|
||
echo "✓ Backend dependencies installed and cached"; \
|
||
else \
|
||
echo "No pyproject.toml found, skipping dependency installation"; \
|
||
fi
|
||
|
||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||
|
||
# OPTIMIZATION PHASE 2: Install frontend dependencies from extracted package.json
|
||
WORKDIR /workspace/frontend
|
||
|
||
# Setup frontend environment and install dependencies (before source code) for better caching
|
||
RUN echo "=== Installing Frontend Dependencies (Phase 2: Optimized Caching) ===" && \
|
||
echo "Available global tools (installed via npm):" && \
|
||
npm list -g --depth=0 2>/dev/null | head -10 || echo "Global npm tools available" && \
|
||
which tsc && which eslint && which prettier || echo "Global tools verified"
|
||
|
||
# Install frontend dependencies from extracted package.json (this layer will cache!)
|
||
RUN if [ -f "package.json" ]; then \
|
||
echo "Installing frontend dependencies from extracted package.json..." && \
|
||
export NODE_OPTIONS="--max-old-space-size=1024 --max-semi-space-size=64" && \
|
||
export UV_WORKERS=1 && \
|
||
echo "Memory info before install:" && \
|
||
free -h || true && \
|
||
INSTALL_SUCCESS=false && \
|
||
echo "Attempt 1/1: Installing project-specific frontend dependencies..." && \
|
||
echo "(Common dev tools pre-installed globally for performance)" && \
|
||
timeout 1200 yarn install --immutable --mode=skip-build \
|
||
&& INSTALL_SUCCESS=true || \
|
||
(echo "Attempt failed, cleaning up..." && \
|
||
rm -rf node_modules .yarn/cache .yarn/install-state.gz && \
|
||
yarn cache clean --all 2>/dev/null || true) && \
|
||
rm -rf .yarn/cache && \
|
||
if [ "$INSTALL_SUCCESS" = "false" ]; then \
|
||
echo "WARNING: Frontend dependencies installation failed"; \
|
||
echo "Continuing without frontend dependencies for CI/CD environment"; \
|
||
touch .frontend-deps-failed; \
|
||
else \
|
||
echo "✓ Frontend dependencies installed and cached"; \
|
||
fi; \
|
||
else \
|
||
echo "No package.json found, skipping frontend dependencies"; \
|
||
fi
|
||
|
||
# OPTIMIZATION PHASE 3: Reuse the repo cloned in phase 1 to avoid re-clone/session timeout
|
||
WORKDIR /workspace
|
||
RUN if [ ! -d "/tmp/repo/.git" ]; then \
|
||
echo "❌ Missing /tmp/repo clone from phase 1" && \
|
||
exit 1; \
|
||
fi
|
||
|
||
RUN echo "Copying source code while preserving installed dependencies..." && \
|
||
echo "Source files in repo:" && \
|
||
ls -la /tmp/repo/ && \
|
||
echo "Current workspace state:" && \
|
||
find /workspace -name "node_modules" -o -name ".venv" -o -name ".yarn" && \
|
||
echo "Copying source files (excluding dependencies)..." && \
|
||
for item in /tmp/repo/*; do \
|
||
basename_item=$(basename "$item"); \
|
||
if [ "$basename_item" = "backend" ] && [ -d "/workspace/backend/.venv" ]; then \
|
||
echo "Copying backend files while preserving .venv..."; \
|
||
find "$item" -mindepth 1 -maxdepth 1 ! -name ".venv" -exec cp -rf {} /workspace/backend/ \;; \
|
||
elif [ "$basename_item" = "frontend" ] && [ -d "/workspace/frontend/node_modules" ]; then \
|
||
echo "Copying frontend files (including dotfiles) while preserving node_modules..."; \
|
||
find "$item" -mindepth 1 -maxdepth 1 ! -name "node_modules" -exec cp -rf {} /workspace/frontend/ \;; \
|
||
else \
|
||
echo "Copying $basename_item..."; \
|
||
cp -rf "$item" /workspace/; \
|
||
fi; \
|
||
done && \
|
||
# Copy common hidden root files without touching . or ..
|
||
for dotfile in .dockerignore .gitignore .pre-commit-config.yaml .markdownlint.yaml .editorconfig; do \
|
||
if [ -f "/tmp/repo/${dotfile}" ]; then \
|
||
cp -f "/tmp/repo/${dotfile}" /workspace/; \
|
||
fi; \
|
||
done && \
|
||
echo "Final dependency check:" && \
|
||
find /workspace -name "node_modules" -o -name ".venv" -o -name ".yarn" && \
|
||
if [ -n "${GITHUB_SHA}" ]; then \
|
||
echo "${GITHUB_SHA}" > /workspace/.cicd-source-sha; \
|
||
fi && \
|
||
echo "✓ Full source code copied, dependencies preserved" && \
|
||
rm -rf /tmp/repo
|
||
|
||
# PHASE 3.5: Re-link Yarn after source code update (node-modules linker only)
|
||
WORKDIR /workspace/frontend
|
||
RUN if [ -f "package.json" ] && [ -d "node_modules" ]; then \
|
||
echo "=== Re-linking Yarn node_modules After Source Code Update ===" && \
|
||
echo "Ensuring package.json and installed node_modules are in sync..." && \
|
||
yarn install --immutable && \
|
||
echo "✓ Yarn node_modules re-linked successfully"; \
|
||
else \
|
||
echo "ℹ No node_modules detected, skipping Yarn re-link"; \
|
||
fi
|
||
|
||
# PHASE 4: Install backend package in development mode (requires full source)
|
||
WORKDIR /workspace/backend
|
||
RUN echo "=== Installing Backend Package in Development Mode ===" && \
|
||
uv pip install -e . && \
|
||
echo "✓ Backend package installed in development mode"
|
||
|
||
# PHASE 5: Install pre-commit environments (requires full source with .pre-commit-config.yaml)
|
||
WORKDIR /workspace
|
||
RUN echo "=== Installing Pre-commit Hook Environments ===" && \
|
||
if [ -f ".pre-commit-config.yaml" ]; then \
|
||
# pre-commit requires a git repo at runtime; keep it in the final image.
|
||
git config --global user.email "ci@cicd" && \
|
||
git config --global user.name "CICD" && \
|
||
git init /workspace && \
|
||
git -C /workspace add -A && \
|
||
cd /workspace/backend && \
|
||
if uv run pre-commit install-hooks; then \
|
||
echo "✓ Pre-commit hook environments installed successfully"; \
|
||
else \
|
||
echo "⚠ pre-commit hook prewarming failed; continuing"; \
|
||
fi; \
|
||
else \
|
||
echo "No .pre-commit-config.yaml found, skipping hook installation"; \
|
||
fi
|
||
|
||
# PHASE 6: Playwright browsers optimization check
|
||
WORKDIR /workspace/frontend
|
||
RUN if [ -f ".frontend-deps-failed" ]; then \
|
||
echo "Frontend dependencies failed - Playwright E2E tests will be skipped"; \
|
||
elif [ -f "package.json" ] && grep -q '@playwright/test' package.json && [ -d "node_modules" ]; then \
|
||
echo "Checking Playwright browser optimization status..." && \
|
||
# Check if Playwright CLI is available via yarn (from project dependencies)
|
||
if yarn playwright --version >/dev/null 2>&1; then \
|
||
echo "✓ Playwright CLI available via yarn" && \
|
||
PW_BROWSER_PATH="${PLAYWRIGHT_BROWSERS_PATH:-/root/.cache/ms-playwright}" && \
|
||
if find "${PW_BROWSER_PATH}" -maxdepth 1 -type d -name 'chromium-*' | grep -q .; then \
|
||
echo "✓ Playwright Chromium browser preinstalled at ${PW_BROWSER_PATH}"; \
|
||
else \
|
||
echo "⚠ Playwright Chromium not preinstalled at ${PW_BROWSER_PATH} - CI will install on demand"; \
|
||
fi; \
|
||
else \
|
||
echo "⚠ Playwright CLI not available - E2E setup will be handled in CI"; \
|
||
fi && \
|
||
echo "✓ Playwright environment checked"; \
|
||
else \
|
||
echo "ℹ No Playwright tests configured in this project"; \
|
||
fi
|
||
|
||
# Verify all tools are working with the project
|
||
RUN cd /workspace/backend && \
|
||
echo "=== Backend Tools Verification ===" && \
|
||
uv run ruff --version && \
|
||
uv run pyright --version && \
|
||
uv run pydoclint --version && \
|
||
uv run pytest --version && \
|
||
uv run yamllint --version && \
|
||
uv run toml-sort --version && \
|
||
uv run xdoctest --version && \
|
||
uv run pre-commit --version
|
||
|
||
RUN cd /workspace/frontend && \
|
||
echo "=== Frontend Tools Verification ===" && \
|
||
if [ -f ".frontend-deps-failed" ]; then \
|
||
echo "WARNING: Skipping frontend tool verification due to failed dependencies installation"; \
|
||
echo "Frontend CI/CD jobs may be limited in this environment"; \
|
||
elif [ -d "node_modules" ]; then \
|
||
verify_required_tool() { \
|
||
tool_name="$1"; \
|
||
yarn_target="$2"; \
|
||
fallback_bin="$3"; \
|
||
if yarn "${yarn_target}" --version; then \
|
||
return 0; \
|
||
fi; \
|
||
if command -v "${fallback_bin}" >/dev/null 2>&1; then \
|
||
"${fallback_bin}" --version; \
|
||
return 0; \
|
||
fi; \
|
||
echo "ERROR: ${tool_name} is not available via yarn or global fallback"; \
|
||
return 1; \
|
||
}; \
|
||
verify_optional_yarn_tool() { \
|
||
tool_name="$1"; \
|
||
yarn_target="$2"; \
|
||
if yarn "${yarn_target}" --version; then \
|
||
return 0; \
|
||
fi; \
|
||
echo "⚠ ${tool_name} version check failed under Yarn wrapper; continuing"; \
|
||
return 0; \
|
||
}; \
|
||
echo "✓ node_modules dependencies found, verifying tools..." && \
|
||
verify_required_tool "eslint" "eslint" "eslint" && \
|
||
verify_required_tool "prettier" "prettier" "prettier" && \
|
||
verify_required_tool "typescript" "tsc" "tsc" && \
|
||
verify_optional_yarn_tool "vitest" "vitest" && \
|
||
echo "✓ All frontend tools verified successfully"; \
|
||
else \
|
||
echo "ERROR: No frontend node_modules found"; \
|
||
echo "Available files in frontend:"; \
|
||
ls -la .; \
|
||
exit 1; \
|
||
fi
|
||
|
||
# Set Python path for backend
|
||
ENV PYTHONPATH=/workspace/backend/src:/workspace/backend
|
||
|
||
# Make global development tools available in PATH for fallback
|
||
ENV PATH="/opt/python-dev-tools/bin:$PATH"
|
||
|
||
# Set working directory back to root
|
||
WORKDIR /workspace
|
||
|
||
# Default to bash
|
||
SHELL ["/bin/bash", "-c"]
|
||
CMD ["/bin/bash"]
|