## Summary This PR simplifies the CICD workflow by merging related lanes, reducing duplicated script logic, and keeping the same overall pipeline behavior and gates. It also updates CI documentation to match the new job topology. ## What Changed ### Workflow consolidation - Merged base and complete CICD image publication into one producer job: - Build and Push CICD Images - Merged dependency audits into one informational lane: - Dependency Audits (Informational) - Merged runtime image build lanes into one release producer: - Build Release Images - Merged tester image build lanes into one tester producer: - Build Tester Images ### Dependency/gate rewiring - Updated downstream needs to consume merged producers. - Kept output contracts for deployable and tester image references. - Updated production and postmortem gates to the new job IDs. ### Cleanup/simplification - Removed duplicate helper-function definition(s) in CICD scripts. - Replaced a manual docker login retry loop with existing retry helper usage. - Removed redundant shell option declarations where behavior was unchanged. - Removed one unused E2E environment variable. ### Documentation alignment - Updated CI architecture documentation to reflect merged workflow lanes. - Updated troubleshooting guidance to reference current job sequencing. ## Why - Reduce job startup overhead on self-hosted runners. - Keep behavior consistent while lowering workflow complexity. - Improve maintainability by removing duplicated/unused script fragments. - Keep docs in sync with operational workflow reality. ## Validation - Workflow file checks passed with pre-commit. - Documentation checks passed with pre-commit (including markdownlint/prettier). - No diagnostics/errors reported for updated workflow/docs files. ## Risk and Impact - Low-to-medium operational risk due to job-ID/needs rewiring. - Mitigated by preserving output keys consumed by integration and e2e lanes. - Audit lane remains informational-only (non-blocking), same intent as before. Co-authored-by: copilotcoder <copilotcoder@darkhelm.org> Reviewed-on: #86
17 KiB
CI/CD Build Optimization & Troubleshooting Guide
Overview
This document captures the specific optimizations, fixes, and troubleshooting approaches developed during November 2025 for the plex-playlist CI/CD pipeline. Each entry includes the problem, root cause analysis, solution implementation, and performance impact.
Current Workflow Reference (2026-07)
The authoritative workflow is .gitea/workflows/cicd.yaml.
When this guide conflicts with older examples, prefer:
- current job names and dependencies in
cicd.yaml - current registry endpoint
kankali.darkhelm.lan:3001 - current retry and auth-realm host pinning logic embedded in image build lanes
High-Value Failure Signatures (Current)
0. Dependency audit step fails but workflow stays green
Symptom:
frontend audit reported vulnerabilities
backend audit reported vulnerabilities
and overall workflow still succeeds.
Cause: expected behavior. Dependency Audits (Informational) is intentionally non-blocking and runs both frontend and backend audit steps.
Fast check:
- Confirm
Dependency Audits (Informational)ran. - Confirm both audit step logs are present.
Fix:
No CI fix required unless policy changes. Treat findings as remediation backlog items.
1. docker_login_with_retry: command not found
Symptom:
line <n>: docker_login_with_retry: command not found
Cause: shell helper function referenced in a job step but missing in that same step's run block.
Fast check:
- Open failing job step in
.gitea/workflows/cicd.yaml. - Confirm
docker_login_with_retry()is defined before first call in that block.
Fix:
Add the helper definition locally in that step block (functions do not cross step boundaries).
2. Registry token timeout while pushing/pulling
Symptom:
Client.Timeout exceeded while awaiting headers
... /v2/token?...service=container_registry
Cause: runner resolves/pins registry host, but token realm host from WWW-Authenticate challenge is unresolved/unreachable.
Fast check:
- Verify
Configure registry host resolutionstep ran. - Confirm auth realm host pinning logic is present in failing lane.
- Check lane-specific login/push retry helpers are active.
Fix:
Use ensure_registry_auth_realm_host + docker_login_with_retry + bounded retry_registry_op in the failing lane.
3. Empty downstream digest/tag outputs
Symptom:
evaluated to '%!t(string=)'
Cause: upstream image lane failed before writing expected outputs (*_tag_ref, *_digest_ref).
Fast check:
- Inspect upstream image job conclusion (
Build Frontend Main Image,Build Integration Tester Image, etc.). - Confirm output writes (
echo key=value >> $GITHUB_OUTPUT) execute after push and digest resolution.
Fix:
Repair failing upstream lane first; downstream expressions become valid once outputs are emitted.
4. Base image publication mismatch
Symptom:
Required immutable base image is not available
Cause: expected hash tag not yet published or failed publication lane.
Fix order:
Build and Push CICD Images- remaining source, image, and runtime lanes
5. Release tags missing after tests passed
Symptom:
staging images exist but deployable-backend/deployable-frontend tags not updated
Cause: promotion lane did not run or failed (Promote Release Images).
Fast check:
- Confirm
Runtime Black-Box Integration TestsandEnd-to-End Testssucceeded. - Confirm the workflow run was an automated
pushonmain; promotion is skipped for PR validation and other non-main events. - Check
Promote Release Imageslogs for registry login, tag creation, pull/tag/push failures. - Verify staging refs (
deployable-backend-staging,deployable-frontend-staging) were emitted byBuild Release Images.
Fix:
Re-run Promote Release Images after correcting registry/auth issues.
6. Unexpected release version chosen
Symptom:
release_version differs from expected manual guess
Cause: promotion versioning follows workflow rules:
- Promotion only runs for automated
pushevents onmain. - If the
maincommit already has semver tagvX.Y.Z, use that exact patch tag. - Otherwise, find the latest semver tag in the repository and auto-create the next patch tag.
- If no semver tag exists yet, bootstrap from
v0.0.0and createv0.0.1. - Also publish
vX.Y.0andvX.Y.Z-<7-char-short-sha>.
Fix:
If you need an exact patch version, tag the main commit with semver before promotion runs; otherwise let CI assign the next patch automatically.
Performance Optimizations
1. Dependency-First Build Pattern
Performance Impact: 85% faster builds (3-5min vs 15-20min typical)
Problem: Every code commit invalidated Docker dependency cache layers, causing full dependency reinstallation.
Root Cause: Dependencies were installed after source code clone in Dockerfile, making them part of frequently-changing layers.
Solution: Restructured build to install dependencies before full source clone:
# BEFORE: Source code changes bust dependency cache
RUN git clone full_repo /workspace
RUN cd /workspace && uv sync --dev # ❌ Rebuilds on every commit
# AFTER: Dependencies cached independently
RUN git clone --depth 1 && extract pyproject.toml, package.json # ✅ Lightweight
RUN uv sync --dev # ✅ Cached unless pyproject.toml changes
RUN git clone full_repo && merge_preserving_deps # ✅ Source changes don't bust deps
Technical Challenges & Solutions:
-
Local Package Build Error:
OSError: Readme file does not exist: ../README.md# Fix: Create minimal structure for package build RUN mkdir -p src/backend && \ echo "# Temporary README for dependency caching phase" > ../README.md && \ echo "# Minimal __init__.py for build" > src/backend/__init__.py && \ uv sync --dev -
Dependency Preservation: Need to preserve installed packages when copying source
# Fix: Backup/restore strategy RUN if [ -d "/workspace/backend/.venv" ]; then mv /workspace/backend/.venv /tmp/venv_backup; fi && \ cp -rf /tmp/fullrepo/* /workspace/ && \ if [ -d "/tmp/venv_backup" ]; then mv /tmp/venv_backup /workspace/backend/.venv; fi -
No rsync Available: Base image doesn't include rsync for selective copying
# Fix: Use standard cp with backup strategy instead of rsync
# rsync -av --exclude='node_modules' /tmp/fullrepo/ /workspace/ # ❌ Not available
# Standard cp with manual exclusions # ✅ Works everywhere
Metrics:
- Dependency cache hit rate: ~95% (only miss when pyproject.toml/package.json change)
- Average build time reduction: 12-17 minutes saved per build
- Resource efficiency: Better CPU/memory utilization on Raspberry Pi workers
2. Chromium-Only CI Testing
Performance Impact: 100% CI reliability vs 60% with multi-browser
Problem: Firefox and WebKit browsers failing consistently in Docker CI environment.
Root Cause Analysis:
- Firefox: Sandbox restrictions in Docker containers, requires
--no-sandboxand security compromises - WebKit: Content loading timeout issues, navigation reliability problems in headless mode
- Docker Environment: Limited resources (RPi 4GB) exacerbate browser compatibility issues
Solution: Conditional browser testing based on environment:
// playwright.config.ts
const projects = process.env.CI
? [
// CI: Only Chromium (most reliable in Docker)
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
]
: [
// Local: Full browser coverage
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
];
Rationale:
- Chromium engine powers 95%+ of web browsers (Chrome, Edge, Opera, Brave)
- Excellent Docker compatibility and resource efficiency
- Core functionality testing coverage maintained
- Full browser testing available for local development
Error Examples Resolved:
Firefox: error: unknown option '--headed=false'
WebKit: Test timeout 30000ms exceeded... waiting for navigation
Firefox: browserType.launch: Executable doesn't exist
Network Resilience Enhancements
Comprehensive Retry Strategy
Problem: Self-hosted CI environment has intermittent network failures causing build failures.
Impact: ~40% CI failure rate due to network timeouts during Docker operations.
Solution: Multi-level retry logic with exponential backoff:
Docker Registry Operations
# .gitea/workflows/cicd-checks.yaml
- name: Login to Container Registry (with retry)
run: |
for attempt in {1..5}; do
echo "Attempt $attempt: Logging into Docker registry..."
if timeout 60 echo "${{ secrets.PACKAGE_ACCESS_TOKEN }}" | \
docker login dogar.darkhelm.org --username ${{ gitea.actor }} --password-stdin; then
echo "✓ Docker login successful"
break
else
if [ $attempt -eq 5 ]; then
echo "❌ Docker login failed after 5 attempts"
exit 1
fi
echo "⚠ Attempt $attempt failed, retrying in 15 seconds..."
sleep 15
fi
done
Playwright Browser Installation
- name: Install Playwright Browsers (with retry)
run: |
cd frontend
for attempt in {1..3}; do
if timeout 600 yarn playwright install --with-deps chromium; then
echo "✓ Playwright browsers installed successfully"
break
else
echo "⚠ Browser install attempt $attempt failed, retrying..."
[ $attempt -lt 3 ] && sleep 30
fi
done
E2E Test Navigation Resilience
// frontend/tests/e2e/app.spec.ts
async function navigateWithRetry(
page: Page,
url: string,
maxRetries: number = 3,
): Promise<void> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await page.goto(url, {
waitUntil: "networkidle",
timeout: 90000, // Extended timeout
});
return;
} catch (error) {
if (attempt === maxRetries) throw error;
console.log(`Navigation attempt ${attempt} failed, retrying...`);
await page.waitForTimeout(2000);
}
}
}
Configuration Enhancements:
// playwright.config.ts - CI optimizations
use: {
headless: true,
timeout: 90000, // Extended for unstable networks
ignoreHTTPSErrors: true, // Self-signed certs
// Network error tolerance
}
Results:
- CI success rate: 40% → 95%
- Average retry overhead: +30 seconds per build
- Network timeout elimination: 100% of Docker operations now succeed
Docker Base Image Compatibility
Missing Optimization Graceful Degradation
Problem: Production base image missing pre-installed Python dev tools optimization.
Symptom:
⚠ Pre-installed Python dev tools not found - fresh installation
Base image may need rebuild for optimal caching
Impact: +15-20 seconds build time (acceptable degradation vs failure)
Solution: Graceful fallback detection:
# Dockerfile.cicd - Resilient optimization detection
RUN 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; \
else \
echo "⚠ Pre-installed Python dev tools not found - fresh installation" && \
echo "Base image may need rebuild for optimal caching"; \
fi
Strategy: Build continues successfully without optimization rather than failing entirely.
Troubleshooting Playbook
Missing Immutable Base Image
Symptom:
❌ Required immutable base image is not available: kankali.darkhelm.lan:3001/darkhelm.org/plex-playlist-cicd-base:<hash>
Publish the base image via the CICD Base Image workflow before rerunning main CI.
Meaning: The main CI workflow computed the expected base hash correctly, but the dedicated base-image workflow has not published that immutable tag yet.
Checks:
- Confirm whether
Dockerfile.cicd-base,.dockerignore, orscripts/compute-cicd-base-hash.shchanged in the branch. - Check the
CICD Base Imageworkflow for the same commit or PR. - Verify the registry contains
plex-playlist-cicd-base:<hash>.
Resolution:
- If the base workflow is still running, rerun main CI after it completes.
- If the base workflow did not trigger, run it manually with
force_rebuild=false. - If the tag should be republished despite already existing, run it manually with
force_rebuild=true.
Why CI does not self-heal here: rebuilding the base inside the main workflow would make different runners produce and consume different local artifacts, which defeats the publish-once/consume-many design.
Docker Build Failures
1. rsync Command Not Found
/bin/bash: line 1: rsync: command not found
Fix: Replace with standard cp commands and backup strategy (implemented)
2. README.md Not Found During uv sync
OSError: Readme file does not exist: ../README.md
Fix: Create dummy README.md during dependency installation phase (implemented)
3. Dependency Cache Invalidation
Symptom: Dependencies rebuilding on every commit Fix: Verify dependency-first build pattern is correctly implemented
E2E Test Failures
1. Browser Not Found
Executable doesn't exist at /root/.cache/ms-playwright/chromium-*/
Fix: Ensure yarn playwright install --with-deps runs before tests
2. Navigation Timeouts
Test timeout 30000ms exceeded
Fix: Use navigateWithRetry helper with extended timeouts
3. Multi-browser Failures in CI
Fix: Use Chromium-only configuration for CI environments
Network-Related Issues
1. Docker Registry Timeouts
Fix: Retry logic with exponential backoff (5 attempts, 15s intervals)
2. Package Download Failures
Fix: Increase timeouts and add retry mechanisms
3. SSL Certificate Issues
Fix: Set ignoreHTTPSErrors: true and NODE_TLS_REJECT_UNAUTHORIZED=0
Performance Monitoring
Key Metrics to Track
-
Build Duration by Phase:
- Dependency extraction: ~10-15s (should be fast)
- Backend dependency install: ~20-30s (cached) vs 5-8min (fresh)
- Frontend dependency install: ~1-2min (cached) vs 10-15min (fresh)
- Source code merge: ~5-10s
-
Cache Hit Rates:
- Backend dependencies: Target >90%
- Frontend dependencies: Target >90%
- Docker base image: Target >95%
-
Network Reliability:
- Docker operations success rate: Target >95%
- E2E test completion rate: Target >95%
Performance Regression Indicators
- Build time >10 minutes consistently (investigate cache invalidation)
- E2E failure rate >10% (investigate network/browser issues)
- Docker operation retries >2 attempts average (investigate network stability)
✅ COMPREHENSIVE SUCCESS - November 2025
Complete Resolution Summary
🎉 MILESTONE ACHIEVED: First fully successful CI/CD workflow completion with all optimizations working together.
Final Performance Metrics:
- Total Pipeline Time: ~3-5 minutes (down from 15-25 minutes)
- Success Rate: 100% (all test phases passing)
- Build Optimization: 85% time reduction achieved
- E2E Test Reliability: 100% (simplified Docker approach)
Key Issues Resolved in Final Sprint
- ✅ README.md Dependency Fix: Dummy file creation for dependency-only builds
- ✅ Rsync Replacement: Standard cp commands with backup/restore strategy
- ✅ Yarn PnP State Regeneration: Fixed state corruption after source copy
- ✅ E2E Test Simplification: Removed unnecessary complex retry logic
- ✅ Memory Management: Proper swap configuration and Node.js memory limits
Validated Working Components
- Multi-stage Docker builds with optimal layer caching
- Dependency-first build pattern preventing cache invalidation
- Network-resilient Playwright setup with Chromium-only CI testing
- Pre-installed development tools in base image for speed
- SSH-based secure repository access with proper key management
- Comprehensive test coverage (linting, unit tests, integration, E2E)
Architecture Stability
All components now work cohesively:
- Base image caching (cicd-base) ↔️ Complete image building (cicd)
- Python dependency management (uv) ↔️ Backend source integration
- Frontend dependency management (Yarn PnP) ↔️ Source code preservation
- E2E testing ↔️ Simple Docker registry operations
Future Optimization Opportunities
- Multi-architecture Builds: Native ARM64 for Raspberry Pi workers
- Parallel Dependency Installation: Backend and frontend deps simultaneously
- Smarter Cache Invalidation: Hash-based detection of dependency changes
- Registry Caching: Pre-warm package registries during low-traffic periods
- Resource Allocation: Dedicated high-memory workers for frontend builds
Document Status: ✅ CURRENT & VALIDATED - All optimizations documented and verified working as of November 2025. Update when implementing new optimizations or encountering new issues.