Public Access
chore: Update @types/node to 20.19.43 #85
+1008
-725
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,8 @@ jobs:
|
||||
RENOVATE_IMAGE_FALLBACK: ghcr.io/renovatebot/renovate:41
|
||||
GITEA_REGISTRY_HOST: kankali.darkhelm.lan
|
||||
GITEA_REGISTRY_IP: 10.18.75.2
|
||||
PACKAGE_ACCESS_TOKEN: ${{ secrets.PACKAGE_ACCESS_TOKEN }}
|
||||
REGISTRY_USER: ${{ github.actor }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -60,17 +62,78 @@ jobs:
|
||||
echo "${GITEA_REGISTRY_IP} ${GITEA_REGISTRY_HOST}" >> /etc/hosts
|
||||
fi
|
||||
|
||||
if [ -n "${PACKAGE_ACCESS_TOKEN:-}" ] && [ -n "${REGISTRY_USER:-}" ]; then
|
||||
echo "${PACKAGE_ACCESS_TOKEN}" | docker login "http://${GITEA_REGISTRY_HOST}:3001" -u "${REGISTRY_USER}" --password-stdin >/dev/null || true
|
||||
fi
|
||||
|
||||
extract_remote_digest_buildx() {
|
||||
image_ref="$1"
|
||||
digest="$(docker buildx imagetools inspect "${image_ref}" --format '{{json .Manifest.Digest}}' 2>/dev/null || true)"
|
||||
digest="${digest//\"/}"
|
||||
digest="${digest//$'\n'/}"
|
||||
digest="${digest//$'\r'/}"
|
||||
digest="${digest// /}"
|
||||
if [[ "${digest}" == sha256:* ]]; then
|
||||
printf '%s\n' "${digest}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
extract_remote_digest_manifest() {
|
||||
image_ref="$1"
|
||||
digest="$(docker manifest inspect "${image_ref}" 2>/dev/null | sed -n 's/.*"digest"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1 || true)"
|
||||
if [[ "${digest}" == sha256:* ]]; then
|
||||
printf '%s\n' "${digest}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
remote_digest() {
|
||||
image_ref="$1"
|
||||
if [[ "${image_ref}" == *@sha256:* ]]; then
|
||||
printf '%s\n' "${image_ref##*@}"
|
||||
return 0
|
||||
fi
|
||||
extract_remote_digest_buildx "${image_ref}" || extract_remote_digest_manifest "${image_ref}"
|
||||
}
|
||||
|
||||
local_digest_matches() {
|
||||
image_ref="$1"
|
||||
digest="$2"
|
||||
docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "${image_ref}" 2>/dev/null | grep -q "@${digest}$"
|
||||
}
|
||||
|
||||
resolve_candidate() {
|
||||
image_ref="$1"
|
||||
|
||||
if docker image inspect "${image_ref}" >/dev/null 2>&1; then
|
||||
if digest="$(remote_digest "${image_ref}")"; then
|
||||
if local_digest_matches "${image_ref}" "${digest}"; then
|
||||
echo "Using cached current image: ${image_ref}" >&2
|
||||
printf '%s\n' "${image_ref}"
|
||||
return 0
|
||||
fi
|
||||
echo "Cached image is stale for ${image_ref}; refreshing" >&2
|
||||
else
|
||||
echo "Remote digest unavailable for ${image_ref}; attempting refresh pull" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
if retry_cmd 3 15 docker pull "${image_ref}" >/dev/null; then
|
||||
printf '%s\n' "${image_ref}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
pick_renovate_image() {
|
||||
for candidate in "${RENOVATE_IMAGE_PRIMARY}" "${RENOVATE_IMAGE_FALLBACK}"; do
|
||||
echo "Trying Renovate image candidate: ${candidate}" >&2
|
||||
|
||||
if docker image inspect "${candidate}" >/dev/null 2>&1; then
|
||||
echo "Using cached Renovate image: ${candidate}" >&2
|
||||
printf '%s\n' "${candidate}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if retry_cmd 3 15 docker pull "${candidate}" >/dev/null; then
|
||||
if resolve_candidate "${candidate}" >/dev/null; then
|
||||
printf '%s\n' "${candidate}"
|
||||
return 0
|
||||
fi
|
||||
@@ -94,11 +157,6 @@ jobs:
|
||||
|
||||
echo "RENOVATE_IMAGE=${RENOVATE_IMAGE}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Configure Renovate for Gitea
|
||||
run: |
|
||||
echo "=== Configuring Renovate for Gitea ==="
|
||||
echo "✓ Renovate runtime configuration will be passed through environment and CLI flags"
|
||||
|
||||
- name: Run Renovate
|
||||
env:
|
||||
# Prefer dedicated Renovate token, then fall back to existing CI tokens.
|
||||
@@ -490,4 +548,4 @@ jobs:
|
||||
echo "Check the Dependency Dashboard issue in your repository for detailed results:"
|
||||
echo "https://dogar.darkhelm.org/DarkHelm.org/plex-playlist/issues"
|
||||
echo ""
|
||||
echo "Next scheduled run: Next Monday at 8 AM UTC"
|
||||
echo "Next scheduled run: Next weekday at 8 AM UTC"
|
||||
|
||||
+43
-35
@@ -1,5 +1,5 @@
|
||||
# CICD Base Setup - System dependencies and language runtimes only
|
||||
ARG PLAYWRIGHT_BROWSERS_IMAGE=mcr.microsoft.com/playwright:v1.56.1-jammy
|
||||
ARG PLAYWRIGHT_BROWSERS_IMAGE=mcr.microsoft.com/playwright:v1.61.1-jammy
|
||||
FROM ${PLAYWRIGHT_BROWSERS_IMAGE} AS playwright-browsers
|
||||
|
||||
FROM ubuntu:22.04
|
||||
@@ -19,43 +19,47 @@ ENV TZ=America/New_York
|
||||
# Configure timezone
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
# Install apt-fast with proper GPG handling
|
||||
RUN apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
for i in 1 2 3; do \
|
||||
echo "Attempt $i: Updating package lists..." && \
|
||||
apt-get update && break || \
|
||||
(echo "Update attempt $i failed, retrying..." && sleep 10); \
|
||||
done && \
|
||||
apt-get install -y \
|
||||
software-properties-common \
|
||||
gnupg \
|
||||
ca-certificates \
|
||||
curl \
|
||||
wget \
|
||||
&& for i in 1 2 3; do \
|
||||
echo "Attempt $i: Adding apt-fast PPA..." && \
|
||||
add-apt-repository -y ppa:apt-fast/stable && \
|
||||
apt-get update && \
|
||||
apt-get install -y apt-fast && \
|
||||
break || \
|
||||
(echo "apt-fast installation attempt $i failed, retrying..." && sleep 10); \
|
||||
done \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Configure apt-fast to use apt (not apt-get) with optimized settings
|
||||
RUN echo 'apt-fast apt-fast/maxdownloads string 10' | debconf-set-selections && \
|
||||
echo 'apt-fast apt-fast/dlflag boolean true' | debconf-set-selections && \
|
||||
echo 'apt-fast apt-fast/aptmanager string apt' | debconf-set-selections
|
||||
|
||||
# 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
|
||||
|
||||
# Install system dependencies using apt-fast
|
||||
RUN apt-fast update && apt-fast install -y \
|
||||
# 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; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
git \
|
||||
curl \
|
||||
ca-certificates \
|
||||
@@ -75,12 +79,15 @@ RUN apt-fast update && apt-fast install -y \
|
||||
libxkbcommon0 \
|
||||
libasound2 \
|
||||
tzdata \
|
||||
gnupg \
|
||||
wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python 3.14 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); \
|
||||
@@ -88,7 +95,7 @@ RUN for i in 1 2 3; do \
|
||||
|
||||
RUN for i in 1 2 3; do \
|
||||
echo "Attempt $i: Installing Python 3.14..." && \
|
||||
timeout 300 apt-fast install -y \
|
||||
timeout 300 apt-get install -y --no-install-recommends \
|
||||
python3.14 \
|
||||
python3.14-venv \
|
||||
python3.14-dev && \
|
||||
@@ -102,8 +109,9 @@ RUN for i in 1 2 3; do \
|
||||
echo "Attempt $i: Installing Node.js 24..." && \
|
||||
curl -fsSL --connect-timeout 30 --max-time 300 \
|
||||
https://deb.nodesource.com/setup_24.x | bash - && \
|
||||
apt-fast update && \
|
||||
timeout 300 apt-fast install -y nodejs && \
|
||||
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 && \
|
||||
timeout 300 apt-get install -y --no-install-recommends nodejs && \
|
||||
break || \
|
||||
(echo "Attempt $i failed, retrying in 15s..." && sleep 15); \
|
||||
done && \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ARG PLAYWRIGHT_BASE_IMAGE=mcr.microsoft.com/playwright:v1.56.1-jammy
|
||||
ARG PLAYWRIGHT_BASE_IMAGE=mcr.microsoft.com/playwright:v1.61.1-jammy
|
||||
FROM ${PLAYWRIGHT_BASE_IMAGE}
|
||||
|
||||
WORKDIR /workspace/frontend
|
||||
|
||||
+1
-2
@@ -19,8 +19,7 @@ RUN set -eux; \
|
||||
done; \
|
||||
return 1; \
|
||||
}; \
|
||||
corepack enable; \
|
||||
retry 5 corepack prepare yarn@4.10.3 --activate; \
|
||||
retry 5 npm install -g --force @yarnpkg/cli-dist@4.10.3; \
|
||||
retry 5 yarn install --immutable
|
||||
|
||||
FROM deps AS build
|
||||
|
||||
@@ -36,7 +36,7 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"fastapi==0.139.0",
|
||||
"sqlalchemy==2.0.44",
|
||||
"sqlalchemy==2.0.51",
|
||||
"psycopg[binary]==3.2.12",
|
||||
"uvicorn==0.51.0"
|
||||
]
|
||||
|
||||
@@ -21,10 +21,10 @@ from backend.database import (
|
||||
)
|
||||
|
||||
REQUIRED_PACKAGE_PINS: dict[str, str] = {
|
||||
"fastapi": "0.120.2",
|
||||
"fastapi": "0.139.0",
|
||||
"psycopg": "3.2.12",
|
||||
"sqlalchemy": "2.0.44",
|
||||
"uvicorn": "0.38.0",
|
||||
"uvicorn": "0.51.0",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -95,8 +95,8 @@ class TestAPIIntegration:
|
||||
assert "current_python" in payload
|
||||
assert payload["python_policy_valid"] is True
|
||||
assert "required_packages" in payload
|
||||
assert payload["required_packages"]["fastapi"] == "0.120.2"
|
||||
assert payload["required_packages"]["uvicorn"] == "0.38.0"
|
||||
assert payload["required_packages"]["fastapi"] == "0.139.0"
|
||||
assert payload["required_packages"]["uvicorn"] == "0.51.0"
|
||||
assert payload["package_errors"] == {}
|
||||
|
||||
monkeypatch.delenv("BACKEND_REQUIRED_PYTHON", raising=False)
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
"""Basic tests for the backend application."""
|
||||
|
||||
from typing import cast
|
||||
from importlib import metadata
|
||||
from typing import Any, cast
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.main import app, get_api_session, read_root
|
||||
from backend.main import (
|
||||
app,
|
||||
compatibility_check,
|
||||
compatibility_status,
|
||||
get_api_session,
|
||||
read_root,
|
||||
validate_runtime_policy,
|
||||
)
|
||||
|
||||
|
||||
def test_app_creation():
|
||||
@@ -56,3 +66,84 @@ def test_typeguard_validation():
|
||||
|
||||
# This would fail with typeguard active, but we'll just test the happy path
|
||||
# to ensure coverage of our code without breaking the test
|
||||
|
||||
|
||||
def test_health_check_db_unavailable():
|
||||
"""Health endpoint should return unavailable when DB probe fails."""
|
||||
|
||||
unhealthy_session = cast("AsyncSession", AsyncMock(spec=AsyncSession))
|
||||
unhealthy_session.execute = AsyncMock(
|
||||
side_effect=SQLAlchemyError("database unavailable")
|
||||
)
|
||||
|
||||
async def override_get_session():
|
||||
"""Provide an unhealthy session dependency override for tests."""
|
||||
yield unhealthy_session
|
||||
|
||||
app.dependency_overrides[get_api_session] = override_get_session
|
||||
try:
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/health")
|
||||
|
||||
assert response.status_code == 503
|
||||
assert response.json() == {
|
||||
"status": "unhealthy",
|
||||
"database": "disconnected",
|
||||
}
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def test_compatibility_status_success(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Compatibility status should be healthy when all checks match policy."""
|
||||
|
||||
monkeypatch.setenv("BACKEND_REQUIRED_PYTHON", "3.14")
|
||||
|
||||
def installed_version(package_name: str) -> str:
|
||||
versions = {
|
||||
"fastapi": "0.139.0",
|
||||
"psycopg": "3.2.12",
|
||||
"sqlalchemy": "2.0.44",
|
||||
"uvicorn": "0.51.0",
|
||||
}
|
||||
return versions[package_name]
|
||||
|
||||
monkeypatch.setattr("backend.main._installed_version", installed_version)
|
||||
status = cast("dict[str, Any]", compatibility_status())
|
||||
|
||||
assert status["ok"] is True
|
||||
assert status["package_errors"] == {}
|
||||
|
||||
|
||||
def test_compatibility_status_missing_package(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Compatibility status should record metadata lookup failures."""
|
||||
|
||||
def missing_version(_: str) -> str:
|
||||
raise metadata.PackageNotFoundError("missing")
|
||||
|
||||
monkeypatch.setattr("backend.main._installed_version", missing_version)
|
||||
status = cast("dict[str, Any]", compatibility_status())
|
||||
package_checks = cast("dict[str, bool]", status["package_checks"])
|
||||
package_errors = cast("dict[str, str]", status["package_errors"])
|
||||
|
||||
assert status["ok"] is False
|
||||
assert package_checks["fastapi"] is False
|
||||
assert "fastapi" in package_errors
|
||||
|
||||
|
||||
def test_validate_runtime_policy_raises(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Runtime policy validation should fail when compatibility is not ok."""
|
||||
|
||||
def invalid_status() -> dict[str, object]:
|
||||
return {"ok": False}
|
||||
|
||||
monkeypatch.setattr("backend.main.compatibility_status", invalid_status)
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
validate_runtime_policy()
|
||||
|
||||
|
||||
def test_compatibility_check_returns_status() -> None:
|
||||
"""Compatibility endpoint helper should return policy payload."""
|
||||
payload = compatibility_check()
|
||||
assert "ok" in payload
|
||||
|
||||
Generated
+645
-639
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,7 +1,7 @@
|
||||
---
|
||||
services:
|
||||
database:
|
||||
image: postgres:16-alpine
|
||||
image: postgres:18-alpine
|
||||
environment:
|
||||
POSTGRES_DB: plex_playlist
|
||||
POSTGRES_USER: plex_user
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
services:
|
||||
database:
|
||||
image: postgres:16-alpine
|
||||
image: postgres:18-alpine
|
||||
environment:
|
||||
POSTGRES_DB: plex_playlist
|
||||
POSTGRES_USER: plex_user
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
---
|
||||
services:
|
||||
database:
|
||||
image: postgres:16-alpine
|
||||
image: postgres:18-alpine
|
||||
environment:
|
||||
POSTGRES_DB: plex_playlist
|
||||
POSTGRES_USER: plex_user
|
||||
|
||||
@@ -4,6 +4,22 @@
|
||||
|
||||
This project uses a two-stage Docker build approach to optimize CI/CD performance by separating stable base dependencies from project-specific code and dependencies.
|
||||
|
||||
## Current Workflow Status (2026-07)
|
||||
|
||||
The source of truth is `.gitea/workflows/cicd.yaml`.
|
||||
|
||||
Current behavior to keep in mind:
|
||||
|
||||
- CICD base publication and complete CICD publication now run in one merged producer job.
|
||||
- Dependency audits run in one informational lane; frontend and backend audits always execute and are non-blocking.
|
||||
- Release and tester images are built in two merged producer jobs.
|
||||
- Deployable backend/frontend runtime images are first published as staging artifacts.
|
||||
- A dedicated promotion lane publishes release image tags only after integration and E2E lanes succeed, and only for automated `push` runs on `main`.
|
||||
- If a qualifying `main` commit is untagged, CI auto-creates the next patch tag starting from a `v0.0.0` baseline.
|
||||
- Release promotion publishes release-line tags (`v<major>.<minor>.0`) plus patch/build tags (`v<major>.<minor>.<patch>`, `v<major>.<minor>.<patch>-<7-char-short-sha>`).
|
||||
- Registry operations include auth-realm host pinning and bounded retry logic for login/pull/push in critical lanes.
|
||||
- Runtime integration checks consume tag and digest outputs and verify they resolve to the same immutable artifact before assertions.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Stage 1: Base Image (`Dockerfile.cicd-base`)
|
||||
@@ -43,7 +59,10 @@ This project uses a two-stage Docker build approach to optimize CI/CD performanc
|
||||
- Pre-commit hook environments (leverages global pre-commit)
|
||||
- Project-specific tooling verification
|
||||
|
||||
**Registry**: `dogar.darkhelm.org/darkhelm.org/plex-playlist/cicd:latest`
|
||||
**Registry**:
|
||||
|
||||
- `kankali.darkhelm.lan:3001/darkhelm.org/plex-playlist-cicd:latest`
|
||||
- `kankali.darkhelm.lan:3001/darkhelm.org/plex-playlist-cicd:<head_sha>`
|
||||
|
||||
**Rebuild Triggers**: Every CI/CD run (contains project-specific code and dependencies)
|
||||
|
||||
@@ -109,37 +128,64 @@ This project uses a two-stage Docker build approach to optimize CI/CD performanc
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
publish-base:
|
||||
name: Build and Publish CICD Base Image
|
||||
steps:
|
||||
- name: Compute base hash
|
||||
# Uses scripts/compute-cicd-base-hash.sh
|
||||
# Hashes Dockerfile.cicd-base and .dockerignore
|
||||
build_cicd:
|
||||
name: Build and Push CICD Images
|
||||
# computes base hash, checks registry, and publishes both CICD base and complete CICD images
|
||||
|
||||
- name: Build and push base image
|
||||
if: needs_build == 'true'
|
||||
# Only runs when the immutable base tag is missing or force rebuild is requested
|
||||
# Tags with both hash and 'latest'
|
||||
source-checks:
|
||||
name: Source Checks
|
||||
needs: build_cicd
|
||||
|
||||
- name: Verify published base image
|
||||
# Confirms the immutable tag is visible and pullable before success
|
||||
dependency-audits:
|
||||
name: Dependency Audits (Informational)
|
||||
needs: build_cicd
|
||||
# frontend/backend audit steps are continue-on-error and backend uses if: always()
|
||||
|
||||
prepare-base-ref:
|
||||
name: Prepare CICD Base Reference
|
||||
steps:
|
||||
- name: Compute immutable base ref
|
||||
# Uses the same helper as the base workflow
|
||||
build-release-images:
|
||||
needs: [build_cicd, source-checks]
|
||||
# publishes deployable-backend-staging and deployable-frontend-staging refs
|
||||
|
||||
setup:
|
||||
name: Build and Push CICD Complete Image
|
||||
needs: prepare-base-ref
|
||||
steps:
|
||||
- name: Build and push complete CICD image
|
||||
# Always runs, inherits from cicd-base:<hash>
|
||||
# Waits briefly for the immutable base tag to appear, then fails clearly if missing
|
||||
# Contains project code and dependencies
|
||||
build-tester-images:
|
||||
needs: [build_cicd, source-checks]
|
||||
|
||||
production-images-complete:
|
||||
needs: [build-release-images, build-tester-images]
|
||||
|
||||
integration-tests:
|
||||
needs:
|
||||
[
|
||||
build_cicd,
|
||||
production-images-complete,
|
||||
build-release-images,
|
||||
build-tester-images,
|
||||
]
|
||||
|
||||
e2e-tests:
|
||||
needs:
|
||||
[
|
||||
build_cicd,
|
||||
production-images-complete,
|
||||
build-release-images,
|
||||
build-tester-images,
|
||||
]
|
||||
|
||||
promote-release-images:
|
||||
needs: [build_cicd, build-release-images, integration-tests, e2e-tests]
|
||||
# retags staging images to deployable-backend/deployable-frontend with:
|
||||
# - release series tag (<major>.<minor>.0)
|
||||
# - exact git semver tag from HEAD (authoritative release tag)
|
||||
# - release build tag (<git-semver>-<7-char-short-sha>)
|
||||
# - latest
|
||||
```
|
||||
|
||||
### Release Tagging Model
|
||||
|
||||
- Promotion is release-tag driven: if HEAD has no semver git tag (`v<major>.<minor>.<patch>`), promotion is skipped.
|
||||
- Release identity is derived from the git tag on HEAD.
|
||||
- Release series tag is normalized to `<major>.<minor>.0`.
|
||||
- Each promoted release also publishes a build-distinguishing tag: `<git-semver>-<short_sha>`, where `<short_sha>` is the 7-character commit shorthand.
|
||||
- Promotion emits generated release notes summarizing commit subjects since the previous `<major>.<minor>.0` release tag.
|
||||
|
||||
### Responsibility Split
|
||||
|
||||
- `.gitea/workflows/cicd.yaml` owns the full CI pipeline: base image publish,
|
||||
@@ -259,6 +305,15 @@ RUN export NODE_OPTIONS="--max-old-space-size=1024" && \
|
||||
- Fix: run or rerun the `CICD Base Image` workflow, or wait for it to finish when a PR changes base inputs
|
||||
- Design note: main CI intentionally fails instead of rebuilding the base locally
|
||||
|
||||
### Registry Auth Realm Timeout
|
||||
|
||||
- Symptom: `Client.Timeout exceeded while awaiting headers` when docker push/pull calls token endpoint
|
||||
- Cause: registry challenge realm host is not consistently reachable/resolved from runner
|
||||
- Current workflow mitigation:
|
||||
- host mapping for registry host
|
||||
- challenge parsing and auth-realm host pinning
|
||||
- bounded login and push/pull retries in image lanes
|
||||
|
||||
### Common Issues
|
||||
|
||||
- **SSH Key Problems**: Ensure SSH_PRIVATE_KEY secret is properly configured
|
||||
|
||||
@@ -69,6 +69,26 @@
|
||||
|
||||
## 🏗️ **Architecture Validation**
|
||||
|
||||
### **Current Release Lifecycle (2026-07)**
|
||||
|
||||
- Runtime deployable images are published first to staging repositories:
|
||||
- `deployable-backend-staging`
|
||||
- `deployable-frontend-staging`
|
||||
- Runtime validation (`Runtime Black-Box Integration Tests` and `End-to-End Tests`) must pass before release tagging.
|
||||
- `Promote Release Images` retags validated staging artifacts to release repositories, but only on automated `push` runs to `main`:
|
||||
- `deployable-backend`
|
||||
- `deployable-frontend`
|
||||
- Published release tags:
|
||||
- `latest`
|
||||
- `v<major>.<minor>.0`
|
||||
- `v<major>.<minor>.<patch>`
|
||||
- `v<major>.<minor>.<patch>-<7-char-short-sha>`
|
||||
- Version selection rule:
|
||||
- If the qualifying `main` commit already has a semver git tag (`vX.Y.Z`), use it.
|
||||
- Otherwise, auto-create and use the next patch tag from the latest semver tag in the repository.
|
||||
- If no prior semver tag exists, bootstrap from `v0.0.0` and create `v0.0.1`.
|
||||
- Dependency audits are informational: frontend and backend audit steps both run and do not fail the full workflow.
|
||||
|
||||
### **Working Component Integration**
|
||||
|
||||
All major components now work seamlessly together:
|
||||
|
||||
@@ -4,6 +4,155 @@
|
||||
|
||||
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**:
|
||||
|
||||
```text
|
||||
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**:
|
||||
|
||||
1. Confirm `Dependency Audits (Informational)` ran.
|
||||
2. 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**:
|
||||
|
||||
```text
|
||||
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**:
|
||||
|
||||
1. Open failing job step in `.gitea/workflows/cicd.yaml`.
|
||||
2. 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**:
|
||||
|
||||
```text
|
||||
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**:
|
||||
|
||||
1. Verify `Configure registry host resolution` step ran.
|
||||
2. Confirm auth realm host pinning logic is present in failing lane.
|
||||
3. 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**:
|
||||
|
||||
```text
|
||||
evaluated to '%!t(string=)'
|
||||
```
|
||||
|
||||
**Cause**: upstream image lane failed before writing expected outputs (`*_tag_ref`, `*_digest_ref`).
|
||||
|
||||
**Fast check**:
|
||||
|
||||
1. Inspect upstream image job conclusion (`Build Frontend Main Image`, `Build Integration Tester Image`, etc.).
|
||||
2. 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**:
|
||||
|
||||
```text
|
||||
Required immutable base image is not available
|
||||
```
|
||||
|
||||
**Cause**: expected hash tag not yet published or failed publication lane.
|
||||
|
||||
**Fix order**:
|
||||
|
||||
1. `Build and Push CICD Images`
|
||||
2. remaining source, image, and runtime lanes
|
||||
|
||||
### 5. Release tags missing after tests passed
|
||||
|
||||
**Symptom**:
|
||||
|
||||
```text
|
||||
staging images exist but deployable-backend/deployable-frontend tags not updated
|
||||
```
|
||||
|
||||
**Cause**: promotion lane did not run or failed (`Promote Release Images`).
|
||||
|
||||
**Fast check**:
|
||||
|
||||
1. Confirm `Runtime Black-Box Integration Tests` and `End-to-End Tests` succeeded.
|
||||
2. Confirm the workflow run was an automated `push` on `main`; promotion is skipped for PR validation and other non-main events.
|
||||
3. Check `Promote Release Images` logs for registry login, tag creation, pull/tag/push failures.
|
||||
4. Verify staging refs (`deployable-backend-staging`, `deployable-frontend-staging`) were emitted by `Build Release Images`.
|
||||
|
||||
**Fix**:
|
||||
|
||||
Re-run `Promote Release Images` after correcting registry/auth issues.
|
||||
|
||||
### 6. Unexpected release version chosen
|
||||
|
||||
**Symptom**:
|
||||
|
||||
```text
|
||||
release_version differs from expected manual guess
|
||||
```
|
||||
|
||||
**Cause**: promotion versioning follows workflow rules:
|
||||
|
||||
1. Promotion only runs for automated `push` events on `main`.
|
||||
2. If the `main` commit already has semver tag `vX.Y.Z`, use that exact patch tag.
|
||||
3. Otherwise, find the latest semver tag in the repository and auto-create the next patch tag.
|
||||
4. If no semver tag exists yet, bootstrap from `v0.0.0` and create `v0.0.1`.
|
||||
5. Also publish `vX.Y.0` and `vX.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
|
||||
|
||||
@@ -11,6 +11,14 @@ automation work under epic #66.
|
||||
|
||||
## Scope
|
||||
|
||||
Lifecycle note:
|
||||
|
||||
- CI first builds deployable runtime artifacts in staging repositories
|
||||
(`deployable-backend-staging`, `deployable-frontend-staging`).
|
||||
- After integration and E2E validation pass, CI promotes those immutable
|
||||
artifacts to release repositories (`deployable-backend`,
|
||||
`deployable-frontend`) via tag promotion.
|
||||
|
||||
Included:
|
||||
|
||||
- Backend deployable runtime image requirements.
|
||||
|
||||
+51
-40
@@ -17,10 +17,11 @@ This document outlines how to set up your development environment and work with
|
||||
|
||||
- **[Poe Task Reference](POE_TASK_REFERENCE.md)** - Complete guide to unified development tasks
|
||||
- **[CI/CD Multi-Stage Build Architecture](CICD_MULTI_STAGE_BUILD.md)** - Technical details of the optimized build system
|
||||
- **[CI/CD Troubleshooting](GITEA_ACTIONS_TROUBLESHOOTING.md)** - Common issues and solutions
|
||||
- **[CI/CD Troubleshooting](CICD_TROUBLESHOOTING_GUIDE.md)** - Current CI lane failures and remediation paths
|
||||
- **[Secure Docker CI/CD](SECURE_DOCKER_CICD.md)** - Security considerations and practices
|
||||
- **[Deployable Runtime Contract](DEPLOYABLE_RUNTIME_CONTRACT.md)** - Canonical backend/frontend runtime artifact contract and exclusion rules
|
||||
- **[ADR003: Deployable Runtime Image Contract Boundaries](adr/ADR003-deployable_runtime_image_contract.md)** - Decision record for deployable runtime boundaries
|
||||
- **[ADR004: Registry Image Resolution and Auth Resilience Policy](adr/ADR004-registry-image-resolution-and-auth-resilience.md)** - Cross-workflow reliability policy for registry/auth/image fallback behavior
|
||||
|
||||
## Deployable Runtime Artifacts
|
||||
|
||||
@@ -209,7 +210,7 @@ git push
|
||||
1. Push your feature branch to the remote repository
|
||||
2. Navigate to the Gitea web interface
|
||||
3. Create a Pull Request from your feature branch to `main`
|
||||
4. Ensure all CI checks pass (100% green required)
|
||||
4. Ensure required CI checks pass (informational audit warnings do not block merge)
|
||||
5. Request review from team members
|
||||
6. Merge only after approval and passing CI
|
||||
|
||||
@@ -388,58 +389,68 @@ pre-commit run end-of-file-fixer --all-files
|
||||
|
||||
### Pipeline Overview
|
||||
|
||||
The CI/CD pipeline uses a **multi-stage build architecture** for optimal performance:
|
||||
The canonical CI workflow is `.gitea/workflows/cicd.yaml`.
|
||||
|
||||
- **Stage 1**: Source-level fast checks (format, lint, type-check) - **hard promotion gate**
|
||||
- **Stage 2**: Build base image (system dependencies, Python, Node.js) - **cached across runs**
|
||||
- **Stage 3**: Build complete image (project code and dependencies) - **rebuilt every time**
|
||||
Current triggers:
|
||||
|
||||
Pipeline triggers:
|
||||
- Push on `main` and `develop`
|
||||
- Pull requests targeting `main` and `develop`
|
||||
- Manual `workflow_dispatch`
|
||||
|
||||
- Push to any branch
|
||||
- Pull requests to `main` or `develop`
|
||||
Current dispatch inputs:
|
||||
|
||||
### Multi-Stage Build Benefits ✅ **VALIDATED SUCCESSFUL**
|
||||
- `head_sha`: commit SHA to process
|
||||
- `force_rebuild_base`: force base image publication
|
||||
|
||||
**Performance Gains**:
|
||||
### Current Job Topology
|
||||
|
||||
- **85% build time improvement**: 3-5 minutes (down from 15-25 minutes)
|
||||
- Base image cached when `Dockerfile.cicd-base` unchanged (~95% of runs)
|
||||
- **100% success rate** achieved with optimized dependency management
|
||||
- Raspberry Pi 4GB workers handle builds efficiently with resource optimization
|
||||
The pipeline is intentionally staged so expensive image jobs run only after source checks and required gates pass:
|
||||
|
||||
**Architecture**:
|
||||
1. Producer lane:
|
||||
`Build and Push CICD Images` computes base hash, checks registry, and publishes both CICD base and complete CICD images.
|
||||
2. Source + audit lanes:
|
||||
`Source Checks` and `Dependency Audits (Informational)` run after producer completion. Audit failures are intentionally non-blocking so both frontend and backend audits always report.
|
||||
3. Runtime image lanes:
|
||||
`Build Release Images` publishes `deployable-backend-staging` and `deployable-frontend-staging`; `Build Tester Images` publishes integration/e2e tester images; then `Production Images Complete` gates downstream runtime tests.
|
||||
4. Runtime validation lanes:
|
||||
`Runtime Black-Box Integration Tests` and `End-to-End Tests` validate staged runtime artifacts.
|
||||
5. Promotion lane:
|
||||
`Promote Release Images` runs only for automated `push` events on `main`. It retags validated staging artifacts to release repos (`deployable-backend`, `deployable-frontend`) with `latest`, `v<major>.<minor>.0`, `v<major>.<minor>.<patch>`, and `v<major>.<minor>.<patch>-<7-char-short-sha>` tags. If the `main` commit is untagged, CI creates the next patch tag automatically; if no prior semver tag exists, the bootstrap baseline is `v0.0.0`.
|
||||
6. Postmortem lanes:
|
||||
targeted postmortem jobs run when key lanes fail to capture diagnostics even when primary jobs fail early.
|
||||
|
||||
- `cicd-base:latest` - System dependencies (Python 3.14, Node.js 24, build tools, pre-installed dev packages)
|
||||
- `cicd:latest` - Complete environment (project code + optimized dependency installation)
|
||||
### Reliability and Traceability Behavior
|
||||
|
||||
**Recent Optimizations** (November 2025):
|
||||
Current workflow behavior includes:
|
||||
|
||||
- **Dependency-first build pattern** prevents cache invalidation on code changes
|
||||
- **Yarn PnP state regeneration** ensures reliable frontend builds
|
||||
- **Network-resilient E2E testing** with simplified Docker operations
|
||||
- **Memory-optimized frontend installations** with proper swap configuration
|
||||
- registry auth realm host pinning from `WWW-Authenticate` challenge when registry tokens are issued from a different host
|
||||
- bounded retry logic for docker login/pull/push operations in image lanes
|
||||
- digest/tag contract checks for deployable image references before runtime black-box tests
|
||||
- release-note summary generation in the promotion lane (commit bullets since the previous release tag, or from the `v0.0.0` bootstrap baseline on first release)
|
||||
- context hydration for image-build lanes by copying `/workspace` from the published CICD image
|
||||
- runner split between `ubuntu-act` and `ubuntu-act-8gb` based on lane resource requirements
|
||||
|
||||
For detailed technical information, see [CI/CD Multi-Stage Build Architecture](CICD_MULTI_STAGE_BUILD.md).
|
||||
For details of base/complete image build strategy, see [CI/CD Multi-Stage Build Architecture](CICD_MULTI_STAGE_BUILD.md).
|
||||
For incident handling, see [CI/CD Troubleshooting](CICD_TROUBLESHOOTING_GUIDE.md).
|
||||
|
||||
### Pipeline Jobs
|
||||
### Operator Quick Reference
|
||||
|
||||
All jobs run in parallel after the setup phases:
|
||||
Use workflow dispatch when you need deterministic reruns on a specific commit:
|
||||
|
||||
1. **Source Fast Gate**:
|
||||
Backend source checks (Ruff format/lint, Pyright), frontend source checks (Prettier, ESLint, TypeScript), and dispatches downstream build only on success.
|
||||
```bash
|
||||
# Example: rerun CI against an explicit commit
|
||||
# input head_sha=<commit>
|
||||
|
||||
2. **Setup Base**: Builds and pushes base Docker image (conditional)
|
||||
3. **Setup Complete**: Builds and pushes complete CI/CD Docker image
|
||||
4. **Code Quality**:
|
||||
Trailing whitespace check, end-of-file formatting, YAML syntax validation, and TOML syntax validation.
|
||||
# Example: force base image republish
|
||||
# input force_rebuild_base=true
|
||||
```
|
||||
|
||||
5. **Backend Validation**:
|
||||
Ruff formatting check, Ruff linting, Pyright type checking, Darglint docstring validation, unit tests with coverage, integration tests, and doctests (xdoctest).
|
||||
Recommended rerun order during flaky infrastructure incidents:
|
||||
|
||||
6. **Frontend Validation**:
|
||||
Prettier formatting check, ESLint linting, TypeScript compilation, and unit tests with coverage.
|
||||
- E2E tests (Playwright)
|
||||
1. `Build and Push CICD Images`
|
||||
2. failing runtime image lane (`Build Release Images` or `Build Tester Images`)
|
||||
3. downstream integration/e2e lanes
|
||||
4. `Promote Release Images` (if staging validation succeeded but promotion failed)
|
||||
|
||||
### Local CI/CD Testing
|
||||
|
||||
@@ -477,7 +488,7 @@ Build and test CI/CD images locally:
|
||||
1. Navigate to your pull request in Gitea
|
||||
2. Check the "Checks" tab for detailed results
|
||||
3. Click on individual job names to see logs
|
||||
4. All jobs must pass (100% green) before merging
|
||||
4. All required jobs must pass before merging (informational dependency-audit findings are non-blocking)
|
||||
|
||||
## Branch Protection and Merge Requirements
|
||||
|
||||
@@ -486,7 +497,7 @@ Build and test CI/CD images locally:
|
||||
The `main` branch is protected with the following requirements:
|
||||
|
||||
1. **No Direct Pushes**: All changes must come through pull requests
|
||||
2. **CI Must Pass**: All CI/CD jobs must be 100% green
|
||||
2. **CI Must Pass**: All required CI/CD jobs must pass (dependency audits are informational)
|
||||
3. **Review Required**: At least one team member approval needed
|
||||
4. **Up-to-date Branch**: Feature branch must be current with main
|
||||
|
||||
@@ -497,7 +508,7 @@ The `main` branch is protected with the following requirements:
|
||||
3. Address any CI failures by pushing fixes to the feature branch
|
||||
4. Request and receive code review approval
|
||||
5. Ensure branch is up-to-date with main
|
||||
6. Merge pull request (only available when all requirements met)
|
||||
6. Merge pull request (available when required jobs pass; informational audit failures do not block merge)
|
||||
|
||||
### If CI Fails
|
||||
|
||||
|
||||
@@ -4,6 +4,25 @@
|
||||
|
||||
Renovate is an automated dependency update tool that creates pull requests to keep your project dependencies up to date. This guide covers setting up Renovate for the plex-playlist project with optimal configuration.
|
||||
|
||||
## Repository Current Mode (2026-07)
|
||||
|
||||
This repository runs Renovate through `.gitea/workflows/renovate.yml`.
|
||||
|
||||
Current operational behavior:
|
||||
|
||||
1. Uses `ubuntu-act-8gb` runner due to npm/registry memory pressure.
|
||||
2. Prepares Renovate container image with mirror-first strategy:
|
||||
|
||||
- primary: `kankali.darkhelm.lan:3001/darkhelm.org/renovate:41`
|
||||
- fallback: `ghcr.io/renovatebot/renovate:41`
|
||||
|
||||
3. Uses digest-aware freshness checks before deciding whether local cached image is current.
|
||||
4. Selects endpoint dynamically between internal and external candidates based on preflight reachability.
|
||||
5. Selects token via preflight checks (repo access required) with fallback order from configured secrets.
|
||||
6. Uses constrained memory settings (`RENOVATE_NODE_ARGS`) and disables OSV alerts in this runner profile.
|
||||
|
||||
Treat workflow behavior as source of truth; use this doc as operator guidance.
|
||||
|
||||
## Setup Options
|
||||
|
||||
### Option 1: GitHub App (Recommended for GitHub)
|
||||
@@ -89,7 +108,7 @@ on:
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-act
|
||||
runs-on: ubuntu-act-8gb
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -101,7 +120,7 @@ jobs:
|
||||
token: ${{ secrets.RENOVATE_TOKEN }}
|
||||
env:
|
||||
RENOVATE_PLATFORM: gitea
|
||||
RENOVATE_ENDPOINT: https://dogar.darkhelm.org/api/v1
|
||||
RENOVATE_ENDPOINT: selected at runtime from internal/external candidates
|
||||
```
|
||||
|
||||
## Configuration Explanation
|
||||
@@ -204,6 +223,37 @@ docker run --rm \
|
||||
4. **Large Updates**: Major version updates may need manual review
|
||||
5. **Docker Registry**: Ensure base image updates don't break builds
|
||||
|
||||
### Current Workflow-Specific Failures
|
||||
|
||||
1. **Renovate image pull failures**
|
||||
|
||||
- Symptom: both mirror and GHCR candidates fail.
|
||||
- Check: runner DNS/egress and registry auth token validity.
|
||||
|
||||
2. **Endpoint preflight failures**
|
||||
|
||||
- Symptom: cannot reach both internal and external API endpoints.
|
||||
- Check: endpoint host mapping, TLS mode, and runner network route.
|
||||
|
||||
3. **Token access failures**
|
||||
|
||||
- Symptom: API `/repos/<org>/<repo>` check returns non-200.
|
||||
- Check: token scopes and secret ordering.
|
||||
|
||||
4. **OOM or abrupt termination**
|
||||
|
||||
- Symptom: process exits under memory pressure.
|
||||
- Check: `RENOVATE_NODE_ARGS`, PR concurrency limits, and optional feature toggles.
|
||||
|
||||
## Operator Notes
|
||||
|
||||
For this repository, prefer updating `.gitea/workflows/renovate.yml` over local one-off Renovate service changes. Keep docs and workflow in sync after changing:
|
||||
|
||||
- endpoint selection logic
|
||||
- token preflight/fallback order
|
||||
- image source policy (mirror/fallback)
|
||||
- memory and concurrency guardrails
|
||||
|
||||
### Quick Validation
|
||||
|
||||
For basic JSON validation without installing Renovate:
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# ADR004: Registry Image Resolution and Auth Resilience Policy
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-07-16
|
||||
|
||||
## Context
|
||||
|
||||
The CI and Renovate workflows run on self-hosted runners with intermittent DNS and network instability. Recent failures showed that image pull/push reliability depends on more than simple retries:
|
||||
|
||||
- registry token realms may resolve to a different host than the registry endpoint
|
||||
- mirror images can become stale relative to upstream
|
||||
- downstream lanes require immutable references from upstream lanes
|
||||
- Renovate must operate across internal/external endpoint paths with token variability
|
||||
|
||||
Without an explicit policy, each job implements ad hoc behavior and drift reintroduces flakiness.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt a cross-workflow reliability policy for registry/image operations:
|
||||
|
||||
1. Prefer mirrored images first, then fallback upstream sources when mirror resolution fails.
|
||||
2. Use digest-aware freshness checks when deciding whether local image cache is current.
|
||||
3. Pin registry auth realm hosts when `WWW-Authenticate` challenge host differs from registry host.
|
||||
4. Use bounded login/pull/push retry wrappers in image publication and consumption lanes.
|
||||
5. Propagate and verify immutable digest references for downstream runtime validation lanes.
|
||||
6. Keep retry counts/timeouts configurable as operational tuning, not architectural invariants.
|
||||
|
||||
## Scope
|
||||
|
||||
This ADR applies to:
|
||||
|
||||
- `.gitea/workflows/cicd.yaml`
|
||||
- `.gitea/workflows/renovate.yml`
|
||||
- helper scripts used for mirrored image resolution and lane orchestration
|
||||
|
||||
This ADR does not prescribe exact retry constants or runner sizing thresholds.
|
||||
|
||||
## Consequences
|
||||
|
||||
Positive:
|
||||
|
||||
- reduced CI flakiness from token realm host mismatch and transient registry failures
|
||||
- stronger traceability via digest-first downstream checks
|
||||
- clearer operator expectations for endpoint/token/image fallback behavior
|
||||
- consistent reliability approach across CICD and Renovate workflows
|
||||
|
||||
Negative:
|
||||
|
||||
- increased workflow script complexity and duplicated helper logic inside isolated step shells
|
||||
- additional maintenance burden to keep helper patterns consistent across lanes
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. Keep per-job ad hoc retries only.
|
||||
|
||||
- Rejected due to repeated regressions and inconsistent behavior.
|
||||
|
||||
2. Depend solely on upstream registries.
|
||||
|
||||
- Rejected due to local network and availability constraints.
|
||||
|
||||
3. Rebuild missing artifacts in downstream lanes.
|
||||
|
||||
- Rejected because it breaks publish-once/consume-many behavior and weakens traceability.
|
||||
|
||||
## Related Decisions
|
||||
|
||||
- `ADR002-cicd_base_image_tagging.md`
|
||||
- `ADR003-deployable_runtime_image_contract.md`
|
||||
Executable
+313
@@ -0,0 +1,313 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: resolve-mirrored-image.sh --image <upstream-or-primary-image> [options]
|
||||
|
||||
Required:
|
||||
--image IMAGE Desired image reference to use locally.
|
||||
|
||||
Optional:
|
||||
--mirror-image IMAGE Mirror image reference to check/pull/push.
|
||||
--upstream-image IMAGE Upstream source image to import into mirror when mirror is missing.
|
||||
--registry-user USER Registry username for mirror auth.
|
||||
--registry-password-env VAR Environment variable name containing mirror registry password/token.
|
||||
--disable-remote-digest-check Skip remote digest freshness check for local images.
|
||||
--local-only Only verify local presence; do not pull.
|
||||
--quiet Suppress informational output except final image ref.
|
||||
|
||||
Behavior:
|
||||
1. If IMAGE exists locally and matches remote digest, use it.
|
||||
2. Otherwise try to pull IMAGE.
|
||||
3. If IMAGE pull fails and MIRROR_IMAGE is set, resolve via MIRROR_IMAGE.
|
||||
4. If MIRROR_IMAGE is missing and UPSTREAM_IMAGE is set, pull upstream, push mirror, then use mirror.
|
||||
5. Prints the resolved local image reference on stdout.
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
if [[ "${QUIET}" != "true" ]]; then
|
||||
echo "$*" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
require_arg() {
|
||||
local value="$1"
|
||||
local name="$2"
|
||||
if [[ -z "${value}" ]]; then
|
||||
echo "Missing required argument: ${name}" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
login_if_needed() {
|
||||
local image_ref="$1"
|
||||
local registry password_var password
|
||||
|
||||
registry="${image_ref%%/*}"
|
||||
if [[ -z "${registry}" || "${registry}" == "${image_ref}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -z "${REGISTRY_USER}" || -z "${REGISTRY_PASSWORD_ENV}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
password_var="${REGISTRY_PASSWORD_ENV}"
|
||||
password="${!password_var:-}"
|
||||
if [[ -z "${password}" ]]; then
|
||||
log "Skipping docker login for ${registry}: env ${password_var} is empty"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n "${LOGGED_IN_REGISTRIES[${registry}]:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Logging into ${registry}"
|
||||
printf '%s' "${password}" | docker login "http://${registry}" -u "${REGISTRY_USER}" --password-stdin >/dev/null
|
||||
LOGGED_IN_REGISTRIES["${registry}"]=1
|
||||
}
|
||||
|
||||
image_present_locally() {
|
||||
local image_ref="$1"
|
||||
docker image inspect "${image_ref}" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
is_digest_pinned_ref() {
|
||||
local image_ref="$1"
|
||||
[[ "${image_ref}" == *@sha256:* ]]
|
||||
}
|
||||
|
||||
extract_remote_digest_buildx() {
|
||||
local image_ref="$1"
|
||||
local digest
|
||||
|
||||
if ! digest="$(docker buildx imagetools inspect "${image_ref}" --format '{{json .Manifest.Digest}}' 2>/dev/null)"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
digest="${digest//\"/}"
|
||||
digest="${digest//$'\n'/}"
|
||||
digest="${digest//$'\r'/}"
|
||||
digest="${digest// /}"
|
||||
|
||||
if [[ "${digest}" == sha256:* ]]; then
|
||||
printf '%s\n' "${digest}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
extract_remote_digest_manifest() {
|
||||
local image_ref="$1"
|
||||
local digest
|
||||
|
||||
if ! digest="$(docker manifest inspect "${image_ref}" 2>/dev/null | sed -n 's/.*"digest"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "${digest}" == sha256:* ]]; then
|
||||
printf '%s\n' "${digest}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
get_remote_digest() {
|
||||
local image_ref="$1"
|
||||
|
||||
if is_digest_pinned_ref "${image_ref}"; then
|
||||
printf '%s\n' "${image_ref##*@}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if extract_remote_digest_buildx "${image_ref}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if extract_remote_digest_manifest "${image_ref}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
local_image_matches_digest() {
|
||||
local image_ref="$1"
|
||||
local digest="$2"
|
||||
|
||||
docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "${image_ref}" 2>/dev/null | grep -q "@${digest}$"
|
||||
}
|
||||
|
||||
pull_image() {
|
||||
local image_ref="$1"
|
||||
login_if_needed "${image_ref}"
|
||||
docker pull "${image_ref}" >/dev/null
|
||||
}
|
||||
|
||||
resolve_reference() {
|
||||
local image_ref="$1"
|
||||
local remote_digest
|
||||
|
||||
if image_present_locally "${image_ref}"; then
|
||||
if [[ "${DISABLE_REMOTE_DIGEST_CHECK}" == "true" ]] || [[ "${LOCAL_ONLY}" == "true" ]] || is_digest_pinned_ref "${image_ref}"; then
|
||||
log "Using locally cached image ${image_ref}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
login_if_needed "${image_ref}"
|
||||
if remote_digest="$(get_remote_digest "${image_ref}")"; then
|
||||
if local_image_matches_digest "${image_ref}" "${remote_digest}"; then
|
||||
log "Using local image ${image_ref}; remote digest matches (${remote_digest})"
|
||||
return 0
|
||||
fi
|
||||
log "Local image ${image_ref} is stale; expected remote digest ${remote_digest}"
|
||||
else
|
||||
log "Unable to determine remote digest for ${image_ref}; attempting pull refresh"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${LOCAL_ONLY}" == "true" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if pull_image "${image_ref}"; then
|
||||
log "Pulled image ${image_ref}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
mirror_available() {
|
||||
local image_ref="$1"
|
||||
if pull_image "${image_ref}"; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
promote_upstream_to_mirror() {
|
||||
require_arg "${UPSTREAM_IMAGE}" "--upstream-image"
|
||||
require_arg "${MIRROR_IMAGE}" "--mirror-image"
|
||||
|
||||
log "Pulling upstream image ${UPSTREAM_IMAGE}"
|
||||
docker pull "${UPSTREAM_IMAGE}" >/dev/null
|
||||
|
||||
login_if_needed "${MIRROR_IMAGE}"
|
||||
log "Tagging ${UPSTREAM_IMAGE} as ${MIRROR_IMAGE}"
|
||||
docker tag "${UPSTREAM_IMAGE}" "${MIRROR_IMAGE}"
|
||||
log "Pushing ${MIRROR_IMAGE}"
|
||||
docker push "${MIRROR_IMAGE}" >/dev/null
|
||||
}
|
||||
|
||||
IMAGE=""
|
||||
MIRROR_IMAGE=""
|
||||
UPSTREAM_IMAGE=""
|
||||
REGISTRY_USER=""
|
||||
REGISTRY_PASSWORD_ENV=""
|
||||
DISABLE_REMOTE_DIGEST_CHECK="false"
|
||||
LOCAL_ONLY="false"
|
||||
QUIET="false"
|
||||
declare -A LOGGED_IN_REGISTRIES=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--image)
|
||||
IMAGE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--mirror-image)
|
||||
MIRROR_IMAGE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--upstream-image)
|
||||
UPSTREAM_IMAGE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--registry-user)
|
||||
REGISTRY_USER="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--registry-password-env)
|
||||
REGISTRY_PASSWORD_ENV="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--disable-remote-digest-check)
|
||||
DISABLE_REMOTE_DIGEST_CHECK="true"
|
||||
shift
|
||||
;;
|
||||
--local-only)
|
||||
LOCAL_ONLY="true"
|
||||
shift
|
||||
;;
|
||||
--quiet)
|
||||
QUIET="true"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
require_arg "${IMAGE}" "--image"
|
||||
|
||||
if resolve_reference "${IMAGE}"; then
|
||||
printf '%s\n' "${IMAGE}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${LOCAL_ONLY}" == "true" ]]; then
|
||||
echo "Image unavailable via local-only mode: ${IMAGE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${MIRROR_IMAGE}" ]]; then
|
||||
log "Primary image unavailable; checking mirror ${MIRROR_IMAGE}"
|
||||
if resolve_reference "${MIRROR_IMAGE}"; then
|
||||
printf '%s\n' "${MIRROR_IMAGE}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -n "${UPSTREAM_IMAGE}" ]]; then
|
||||
log "Mirror image ${MIRROR_IMAGE} missing; promoting from upstream ${UPSTREAM_IMAGE}"
|
||||
promote_upstream_to_mirror
|
||||
if ! pull_image "${MIRROR_IMAGE}"; then
|
||||
echo "Failed to pull promoted mirror image: ${MIRROR_IMAGE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "${MIRROR_IMAGE}"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${UPSTREAM_IMAGE}" ]]; then
|
||||
log "Falling back to upstream image ${UPSTREAM_IMAGE}"
|
||||
if resolve_reference "${UPSTREAM_IMAGE}"; then
|
||||
printf '%s\n' "${UPSTREAM_IMAGE}"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Failed to resolve image via primary/mirror/upstream path" >&2
|
||||
echo "primary=${IMAGE}" >&2
|
||||
if [[ -n "${MIRROR_IMAGE}" ]]; then
|
||||
echo "mirror=${MIRROR_IMAGE}" >&2
|
||||
fi
|
||||
if [[ -n "${UPSTREAM_IMAGE}" ]]; then
|
||||
echo "upstream=${UPSTREAM_IMAGE}" >&2
|
||||
fi
|
||||
exit 1
|
||||
Reference in New Issue
Block a user