# CICD Base Setup - System dependencies and language runtimes only ARG PLAYWRIGHT_VERSION=1.61.1 ARG PLAYWRIGHT_DISTRO=jammy ARG PLAYWRIGHT_BROWSERS_IMAGE=mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-${PLAYWRIGHT_DISTRO} ARG UBUNTU_VERSION=26.04 FROM ${PLAYWRIGHT_BROWSERS_IMAGE} AS playwright-browsers FROM ubuntu:${UBUNTU_VERSION} ARG PLAYWRIGHT_VERSION ARG PYTHON_VERSION=3.14 ARG NODE_MAJOR=24 ARG TYPESCRIPT_VERSION=5.3.3 ARG ESLINT_VERSION=9.33.0 ARG PRETTIER_VERSION=3.6.2 ARG VITE_VERSION=7.1.10 ARG NODE_TYPES_VERSION=20.16.0 # Build args for cache busting (base dependencies change rarely) ARG BASE_IMAGE_VERSION=v1.0.0 ARG BASE_IMAGE_HASH=unknown ENV BASE_IMAGE_VERSION=${BASE_IMAGE_VERSION} LABEL darkhelm.cicd-base.hash=${BASE_IMAGE_HASH} LABEL org.opencontainers.image.version=${BASE_IMAGE_VERSION} # Set timezone and make installs non-interactive ENV DEBIAN_FRONTEND=noninteractive ENV TZ=America/New_York # Configure timezone RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # Configure apt timeouts and retries RUN echo 'Acquire::Retries "3";' > /etc/apt/apt.conf.d/80retries && \ echo 'Acquire::http::Timeout "60";' >> /etc/apt/apt.conf.d/80retries && \ echo 'Acquire::https::Timeout "60";' >> /etc/apt/apt.conf.d/80retries && \ echo 'Acquire::ftp::Timeout "60";' >> /etc/apt/apt.conf.d/80retries # Bootstrap certificates over HTTP, then enforce HTTPS for all remaining package operations. RUN set -eux; \ apt-get clean; \ rm -rf /var/lib/apt/lists/*; \ find /etc/apt -type f \( -name 'sources.list' -o -name '*.sources' -o -name '*.list' \) -print0 \ | xargs -0 sed -i 's|https://ports.ubuntu.com/ubuntu-ports|http://ports.ubuntu.com/ubuntu-ports|g; s|https://archive.ubuntu.com/ubuntu|http://archive.ubuntu.com/ubuntu|g; s|https://security.ubuntu.com/ubuntu|http://security.ubuntu.com/ubuntu|g'; \ for i in 1 2 3; do \ echo "Attempt $i: Bootstrapping CA certificates..."; \ if apt-get update && apt-get install -y --no-install-recommends ca-certificates; then \ break; \ fi; \ if [ "$i" -lt 3 ]; then \ echo "Bootstrap attempt $i failed, retrying..."; \ sleep 10; \ else \ exit 1; \ fi; \ done; \ update-ca-certificates; \ find /etc/apt -type f \( -name 'sources.list' -o -name '*.sources' -o -name '*.list' \) -print0 \ | xargs -0 sed -i 's|http://ports.ubuntu.com/ubuntu-ports|https://ports.ubuntu.com/ubuntu-ports|g; s|http://archive.ubuntu.com/ubuntu|https://archive.ubuntu.com/ubuntu|g; s|http://security.ubuntu.com/ubuntu|https://security.ubuntu.com/ubuntu|g'; \ rm -rf /var/lib/apt/lists/*; \ for i in 1 2 3; do \ echo "Attempt $i: Updating package lists over HTTPS..."; \ if apt-get update; then \ break; \ fi; \ if [ "$i" -lt 3 ]; then \ echo "HTTPS update attempt $i failed, retrying..."; \ sleep 10; \ else \ exit 1; \ fi; \ done; \ if apt-cache show libasound2t64 >/dev/null 2>&1; then \ asound_package=libasound2t64; \ else \ asound_package=libasound2; \ fi; \ apt-get install -y --no-install-recommends \ git \ curl \ ca-certificates \ software-properties-common \ build-essential \ openssh-client \ # Playwright Chromium runtime dependencies (Linux) libnspr4 \ libnss3 \ libatk1.0-0 \ libatspi2.0-0 \ libxcomposite1 \ libxdamage1 \ libxfixes3 \ libxrandr2 \ libgbm1 \ libxkbcommon0 \ "$asound_package" \ tzdata \ gnupg \ wget \ && rm -rf /var/lib/apt/lists/* # Install Python with retry and fallback mechanisms RUN for i in 1 2 3; do \ echo "Attempt $i: Adding deadsnakes PPA..." && \ add-apt-repository -y ppa:deadsnakes/ppa && \ find /etc/apt -type f \( -name 'sources.list' -o -name '*.sources' -o -name '*.list' \) -print0 | xargs -0 sed -i 's|http://|https://|g' && \ apt-get update && \ break || \ (echo "Attempt $i failed, retrying in 10s..." && sleep 10); \ done RUN for i in 1 2 3; do \ echo "Attempt $i: Installing Python ${PYTHON_VERSION}..." && \ timeout 300 apt-get install -y --no-install-recommends \ python${PYTHON_VERSION} \ python${PYTHON_VERSION}-venv \ python${PYTHON_VERSION}-dev && \ break || \ (echo "Attempt $i failed, retrying in 15s..." && sleep 15); \ done && \ rm -rf /var/lib/apt/lists/* # Install Node.js with retry mechanism RUN node_arch="$(dpkg --print-architecture)"; \ case "$node_arch" in \ amd64) node_tarball_arch=linux-x64 ;; \ arm64) node_tarball_arch=linux-arm64 ;; \ *) echo "unsupported architecture for Node.js tarball: $node_arch" >&2; exit 1 ;; \ esac; \ for i in 1 2 3; do \ echo "Attempt $i: Installing Node.js ${NODE_MAJOR}..."; \ node_version="$(python3 -c "import json, urllib.request; versions = json.load(urllib.request.urlopen('https://nodejs.org/dist/index.json')); print(next(item['version'].lstrip('v') for item in versions if item['version'].startswith('v${NODE_MAJOR}.')))" )"; \ if curl -fsSLO "https://nodejs.org/dist/v${node_version}/node-v${node_version}-${node_tarball_arch}.tar.xz" && \ tar -xJf "node-v${node_version}-${node_tarball_arch}.tar.xz" -C /usr/local --strip-components=1 && \ rm -f "node-v${node_version}-${node_tarball_arch}.tar.xz"; then \ break; \ fi; \ if [ "$i" -lt 3 ]; then \ echo "Attempt $i failed, retrying in 15s..."; \ sleep 15; \ else \ exit 1; \ fi; \ done # Enable corepack for yarn and set up Yarn Berry RUN corepack enable \ && corepack prepare yarn@stable --activate \ && yarn set version berry # Configure Yarn globally for CI performance RUN yarn config set httpTimeout 60000 && \ yarn config set enableGlobalCache false && \ yarn config set compressionLevel 0 && \ yarn config set nmMode hardlinks-local # Install uv package manager globally without requiring GHCR pulls RUN set -eu && \ curl -LsSf https://astral.sh/uv/install.sh -o /tmp/uv-install.sh && \ sh /tmp/uv-install.sh && \ rm -f /tmp/uv-install.sh && \ cp /root/.local/bin/uv /usr/local/bin/uv && \ chmod +x /usr/local/bin/uv && \ uv --version # Install common development tools globally using npm (more reliable for global installs) RUN echo "=== Installing Global Development Tools ===" && \ # Use npm for global installations (works better than yarn global in Berry) npm install -g \ @playwright/test@${PLAYWRIGHT_VERSION} \ typescript@${TYPESCRIPT_VERSION} \ eslint@${ESLINT_VERSION} \ prettier@${PRETTIER_VERSION} \ vite@${VITE_VERSION} \ @types/node@${NODE_TYPES_VERSION} && \ # Verify global tools are available which playwright && \ which tsc && \ which eslint && \ which prettier && \ echo "✓ Global Node.js development tools installed via npm" # Keep browser binaries in a deterministic location so E2E runs do not re-download them. ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright # Copy preinstalled browsers from the official Playwright image to avoid flaky CDN downloads. COPY --from=playwright-browsers /ms-playwright /ms-playwright RUN echo "=== Verifying preinstalled Playwright Chromium Browser ===" && \ chromium_count=$(find "${PLAYWRIGHT_BROWSERS_PATH}" -maxdepth 1 -type d -name 'chromium-*' | wc -l) && \ echo "playwright_chromium_dir_count=${chromium_count} path=${PLAYWRIGHT_BROWSERS_PATH}" && \ if [ "${chromium_count}" -eq 0 ]; then \ echo "❌ No Chromium browser directories found in preinstalled Playwright cache"; \ exit 1; \ fi && \ ls -la "${PLAYWRIGHT_BROWSERS_PATH}" && \ echo "✓ Playwright Chromium preinstalled at ${PLAYWRIGHT_BROWSERS_PATH}" # Pre-install common Python development dependencies globally # These are stable tools that rarely change and take time to compile RUN echo "=== Pre-installing Common Python Dev Tools ===" && \ # Create a global uv environment with common dev tools uv venv /opt/python-dev-tools && \ # Use system uv to install packages into the virtual environment uv pip install --python /opt/python-dev-tools/bin/python \ ruff>=0.6.0 \ pyright>=1.1.380 \ pytest>=7.4.0 \ pytest-asyncio>=0.21.0 \ pytest-cov>=4.1.0 \ pytest-mock>=3.12.0 \ pre-commit>=3.0.0 \ pyyaml>=6.0 \ yamllint>=1.35.0 \ toml-sort>=0.23.0 \ xdoctest>=1.1.0 \ language-formatters-pre-commit-hooks>=2.14.0 \ poethepoet>=0.24.0 && \ echo "✓ Common Python dev tools pre-installed in /opt/python-dev-tools" # Create a script to set up SSH for git operations (using secrets mount) RUN echo '#!/bin/bash' > /usr/local/bin/setup-ssh && \ echo 'if [ -f /run/secrets/ssh_private_key ]; then' >> /usr/local/bin/setup-ssh && \ echo ' mkdir -p ~/.ssh' >> /usr/local/bin/setup-ssh && \ echo ' cp /run/secrets/ssh_private_key ~/.ssh/id_rsa' >> /usr/local/bin/setup-ssh && \ echo ' chmod 600 ~/.ssh/id_rsa' >> /usr/local/bin/setup-ssh && \ echo ' ssh-keyscan -H github.com >> ~/.ssh/known_hosts 2>/dev/null' >> /usr/local/bin/setup-ssh && \ echo ' ssh-keyscan -p 2222 -H kankali.darkhelm.lan >> ~/.ssh/known_hosts 2>/dev/null' >> /usr/local/bin/setup-ssh && \ echo 'else' >> /usr/local/bin/setup-ssh && \ echo ' echo "No SSH key provided via secrets mount"' >> /usr/local/bin/setup-ssh && \ echo 'fi' >> /usr/local/bin/setup-ssh && \ chmod +x /usr/local/bin/setup-ssh # Verify that all base tools are working RUN echo "=== Base System Verification ===" && \ python${PYTHON_VERSION} --version && \ node --version && \ yarn --version && \ uv --version && \ git --version && \ echo "=== Global Development Tools Verification ===" && \ playwright --version && \ tsc --version && \ eslint --version && \ prettier --version && \ /opt/python-dev-tools/bin/ruff --version && \ /opt/python-dev-tools/bin/pyright --version && \ /opt/python-dev-tools/bin/pytest --version && \ /opt/python-dev-tools/bin/pre-commit --version && \ /opt/python-dev-tools/bin/poe --version && \ echo "✓ All base system and development tools verified successfully" # Set working directory WORKDIR /workspace # Default to bash SHELL ["/bin/bash", "-c"] CMD ["/bin/bash"]