Backend runtime upgraded to Python 3.14 with exact dependency pinning (#57)
Some checks failed
CICD Start / Sanity and Base Decision (push) Successful in 18s
Runner Canary / Canary Heavy (ubuntu-act-8gb) (push) Has been skipped
Runner Canary / Canary Heavy (ubuntu-act-4gb) (push) Has been skipped
Runner Canary / Canary Burst (ubuntu-act (push) Failing after 11m10s
Runner Canary / Canary (ubuntu-latest) (push) Failing after 12m39s
Runner Canary / Canary (ubuntu-act) (push) Failing after 12m42s
Some checks failed
CICD Start / Sanity and Base Decision (push) Successful in 18s
Runner Canary / Canary Heavy (ubuntu-act-8gb) (push) Has been skipped
Runner Canary / Canary Heavy (ubuntu-act-4gb) (push) Has been skipped
Runner Canary / Canary Burst (ubuntu-act (push) Failing after 11m10s
Runner Canary / Canary (ubuntu-latest) (push) Failing after 12m39s
Runner Canary / Canary (ubuntu-act) (push) Failing after 12m42s
Signed-off-by: Cliff Hill <xlorep@darkhelm.org> ## Summary Upgrades backend runtime baseline and dependency management for issue #10. ### Changes 1. **Python Baseline**: Updated from 3.13 to 3.14 - Updated `backend/pyproject.toml` requires-python constraint - Updated `backend/pyrightconfig.json` pythonVersion - Updated all Dockerfile and CI references 2. **Dependency Pinning**: Switched to exact version pins in `backend/pyproject.toml` - All dev and runtime dependencies now use `==` instead of `>=` - `fastapi==0.120.2`, `uvicorn==0.38.0` - ruff, pyright, pytest suite pinned to current resolved versions - Regenerated `backend/uv.lock` under Python 3.14 3. **Startup Compatibility Guard** (TDD via RED→GREEN) - New `compatibility_status()` function evaluates runtime and pinned deps - Startup raises `RuntimeError` if policy fails - Implemented via FastAPI lifespan (non-deprecated) handler 4. **Compatibility Status Endpoint** - New `GET /compatibility` returns policy status, runtime version, and package checks - Shares single source of truth with startup validation 5. **Integration Tests** - Added failing-then-passing tests for startup guard and endpoint behavior - 100% coverage maintained 6. **Direnv Configuration** - Added `UV_PYTHON="3.14"` pin to repo `.envrc` - Ensures direnv creates/recreates venv with correct Python version ### Validation - ✅ ruff format/check - ✅ pyright strict (0 errors) - ✅ pytest: 8 passed, 100% coverage (>=95 gate) - ✅ pydoclint: pass - ✅ xdoctest: pass ### Notes - SQLAlchemy/SQLModel introduction deferred to next pass per scope - Compatibility logic currently validates fastapi/uvicorn pins (runtime deps) - Ready for container build validation and Renovate bot testing Co-authored-by: copilotcoder <copilotcoder@darkhelm.org> Reviewed-on: #57 Co-authored-by: Cliff Hill <xlorep@darkhelm.org> Co-committed-by: Cliff Hill <xlorep@darkhelm.org>
This commit was merged in pull request #57.
This commit is contained in:
193
Dockerfile.cicd
193
Dockerfile.cicd
@@ -6,12 +6,13 @@
|
||||
# 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=dogar.darkhelm.org/darkhelm.org/plex-playlist/cicd-base:latest
|
||||
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
|
||||
@@ -25,27 +26,41 @@ 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 dogar.darkhelm.org" > ~/.ssh/config && \
|
||||
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 dogar.darkhelm.org >> ~/.ssh/known_hosts 2>/dev/null && \
|
||||
(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 clone --depth 1 --branch main \
|
||||
ssh://git@dogar.darkhelm.org:2222/DarkHelm.org/plex-playlist.git /tmp/repo && \
|
||||
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 && git checkout "$GITHUB_SHA" 2>/dev/null || echo "Using main branch HEAD"; \
|
||||
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/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" && \
|
||||
echo "✓ Dependency files extracted for optimized layer caching" && \
|
||||
rm -rf /tmp/repo ~/.ssh
|
||||
rm -rf ~/.ssh
|
||||
|
||||
# OPTIMIZATION PHASE 1: Install backend dependencies from extracted pyproject.toml
|
||||
WORKDIR /workspace/backend
|
||||
@@ -89,11 +104,7 @@ WORKDIR /workspace/frontend
|
||||
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" && \
|
||||
# Create temporary swap file for memory-intensive yarn install
|
||||
dd if=/dev/zero of=/tmp/swapfile bs=1M count=1024 2>/dev/null && \
|
||||
mkswap /tmp/swapfile && \
|
||||
swapon /tmp/swapfile || echo "Swap setup failed, continuing without swap"
|
||||
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 \
|
||||
@@ -103,21 +114,16 @@ RUN if [ -f "package.json" ]; then \
|
||||
echo "Memory info before install:" && \
|
||||
free -h || true && \
|
||||
INSTALL_SUCCESS=false && \
|
||||
for i in 1 2 3; do \
|
||||
echo "Attempt $i: Installing project-specific frontend dependencies..." && \
|
||||
echo "(Common dev tools pre-installed globally for performance)" && \
|
||||
timeout 2400 yarn install --immutable --mode=skip-build \
|
||||
&& { INSTALL_SUCCESS=true; break; } || \
|
||||
(echo "Attempt $i failed, cleaning up and retrying..." && \
|
||||
rm -rf node_modules .yarn/cache .yarn/install-state.gz && \
|
||||
yarn cache clean --all 2>/dev/null || true && \
|
||||
sleep 60); \
|
||||
done && \
|
||||
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 && \
|
||||
swapoff /tmp/swapfile 2>/dev/null || true && \
|
||||
rm -f /tmp/swapfile && \
|
||||
if [ "$INSTALL_SUCCESS" = "false" ]; then \
|
||||
echo "WARNING: Frontend dependencies installation failed after 3 attempts"; \
|
||||
echo "WARNING: Frontend dependencies installation failed"; \
|
||||
echo "Continuing without frontend dependencies for CI/CD environment"; \
|
||||
touch .frontend-deps-failed; \
|
||||
else \
|
||||
@@ -127,69 +133,55 @@ RUN if [ -f "package.json" ]; then \
|
||||
echo "No package.json found, skipping frontend dependencies"; \
|
||||
fi
|
||||
|
||||
# OPTIMIZATION PHASE 3: Now clone full source code (dependencies already cached above)
|
||||
# OPTIMIZATION PHASE 3: Reuse the repo cloned in phase 1 to avoid re-clone/session timeout
|
||||
WORKDIR /workspace
|
||||
RUN --mount=type=secret,id=ssh_private_key \
|
||||
echo "=== Cloning Full Source Code (Phase 3: After Dependencies) ===" && \
|
||||
mkdir -p ~/.ssh && \
|
||||
cp /run/secrets/ssh_private_key ~/.ssh/id_rsa && \
|
||||
chmod 600 ~/.ssh/id_rsa && \
|
||||
echo "Host dogar.darkhelm.org" > ~/.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 dogar.darkhelm.org >> ~/.ssh/known_hosts 2>/dev/null && \
|
||||
# Clone full repository (dependencies already installed, this won't bust cache layers)
|
||||
GIT_SSH_COMMAND="ssh -F ~/.ssh/config" \
|
||||
git clone --depth 1 --branch main \
|
||||
ssh://git@dogar.darkhelm.org:2222/DarkHelm.org/plex-playlist.git /tmp/fullrepo && \
|
||||
if [ -n "$GITHUB_SHA" ]; then \
|
||||
cd /tmp/fullrepo && git checkout "$GITHUB_SHA" 2>/dev/null || echo "Using main branch HEAD"; \
|
||||
fi && \
|
||||
# Copy source code while preserving installed dependencies
|
||||
echo "Copying source code while preserving installed dependencies..." && \
|
||||
# Instead of backup/restore, copy selectively to avoid overwriting dependencies
|
||||
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/fullrepo/ && \
|
||||
ls -la /tmp/repo/ && \
|
||||
echo "Current workspace state:" && \
|
||||
find /workspace -name "node_modules" -o -name ".venv" -o -name ".yarn" && \
|
||||
# Copy source files excluding dependency directories
|
||||
echo "Copying source files (excluding dependencies)..." && \
|
||||
# Copy all files and directories except the ones we want to preserve
|
||||
for item in /tmp/fullrepo/*; do \
|
||||
for item in /tmp/repo/*; do \
|
||||
basename_item=$(basename "$item"); \
|
||||
target_path="/workspace/$basename_item"; \
|
||||
if [ "$basename_item" = "backend" ] && [ -d "/workspace/backend/.venv" ]; then \
|
||||
echo "Copying backend files while preserving .venv..."; \
|
||||
# Copy backend files but skip .venv if it exists
|
||||
find "$item" -mindepth 1 -maxdepth 1 ! -name ".venv" -exec cp -rf {} /workspace/backend/ \;; \
|
||||
elif [ "$basename_item" = "frontend" ] && ([ -d "/workspace/frontend/node_modules" ] || [ -f "/workspace/frontend/.pnp.cjs" ]); then \
|
||||
echo "Copying frontend files (will regenerate Yarn state after)..."; \
|
||||
# Copy all frontend files normally - we'll regenerate Yarn state afterward
|
||||
cp -rf "$item"/* /workspace/frontend/; \
|
||||
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 hidden files from root (like .gitignore, .dockerignore, etc.)
|
||||
cp -rf /tmp/fullrepo/.* /workspace/ 2>/dev/null || true && \
|
||||
# Verify dependencies are still there
|
||||
# Copy common hidden root files without touching . or ..
|
||||
for dotfile in .dockerignore .gitignore .pre-commit-config.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/fullrepo ~/.ssh
|
||||
rm -rf /tmp/repo
|
||||
|
||||
# PHASE 3.5: Regenerate Yarn PnP state after source code update
|
||||
# PHASE 3.5: Re-link Yarn after source code update (node-modules linker only)
|
||||
WORKDIR /workspace/frontend
|
||||
RUN if [ -f "package.json" ] && [ -f ".pnp.cjs" ]; then \
|
||||
echo "=== Regenerating Yarn PnP State After Source Code Update ===" && \
|
||||
echo "Source package.json and installed dependencies may have differences..." && \
|
||||
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 PnP state regenerated successfully - tools should now work"; \
|
||||
echo "✓ Yarn node_modules re-linked successfully"; \
|
||||
else \
|
||||
echo "ℹ No Yarn PnP setup detected, skipping state regeneration"; \
|
||||
echo "ℹ No node_modules detected, skipping Yarn re-link"; \
|
||||
fi
|
||||
|
||||
# PHASE 4: Install backend package in development mode (requires full source)
|
||||
@@ -202,14 +194,22 @@ RUN echo "=== Installing Backend Package in Development Mode ===" && \
|
||||
WORKDIR /workspace
|
||||
RUN echo "=== Installing Pre-commit Hook Environments ===" && \
|
||||
if [ -f ".pre-commit-config.yaml" ]; then \
|
||||
# Use project's Python environment for pre-commit
|
||||
cd backend && uv run pre-commit install-hooks && \
|
||||
echo "✓ Pre-commit hook environments installed successfully"; \
|
||||
# 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 (may be pre-installed in base image)
|
||||
# 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"; \
|
||||
@@ -218,11 +218,11 @@ RUN if [ -f ".frontend-deps-failed" ]; then \
|
||||
# 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" && \
|
||||
# Check if browsers are pre-installed from base image
|
||||
if yarn playwright install --dry-run >/dev/null 2>&1; then \
|
||||
echo "✓ Playwright browsers available from base image optimization"; \
|
||||
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 browsers not pre-installed - will install on demand in CI"; \
|
||||
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"; \
|
||||
@@ -237,7 +237,7 @@ RUN cd /workspace/backend && \
|
||||
echo "=== Backend Tools Verification ===" && \
|
||||
uv run ruff --version && \
|
||||
uv run pyright --version && \
|
||||
uv run darglint --version && \
|
||||
uv run pydoclint --version && \
|
||||
uv run pytest --version && \
|
||||
uv run yamllint --version && \
|
||||
uv run toml-sort --version && \
|
||||
@@ -249,19 +249,38 @@ RUN cd /workspace/frontend && \
|
||||
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" ] || [ -f ".pnp.cjs" ]; then \
|
||||
if [ -f ".pnp.cjs" ]; then \
|
||||
echo "✓ Yarn PnP dependencies found (.pnp.cjs), verifying tools..."; \
|
||||
else \
|
||||
echo "✓ Traditional node_modules found, verifying tools..."; \
|
||||
fi && \
|
||||
yarn eslint --version && \
|
||||
yarn prettier --version && \
|
||||
yarn tsc --version && \
|
||||
yarn vitest --version && \
|
||||
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 dependencies found (neither node_modules nor .pnp.cjs)"; \
|
||||
echo "ERROR: No frontend node_modules found"; \
|
||||
echo "Available files in frontend:"; \
|
||||
ls -la .; \
|
||||
exit 1; \
|
||||
|
||||
Reference in New Issue
Block a user