Files
plex-playlist/.gitea/workflows/docker-build-base.yaml
Cliff Hill d02039a22e
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
Backend runtime upgraded to Python 3.14 with exact dependency pinning (#57)
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>
2026-06-18 11:19:24 -04:00

538 lines
21 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Docker Build Base
on:
workflow_dispatch:
inputs:
force_rebuild:
description: Force a rebuild even when the immutable base tag already exists
required: false
default: "false"
head_sha:
description: Commit SHA to process
required: false
source_workflow:
description: Upstream workflow name
required: false
trace_id:
description: Correlation id propagated across CICD dispatch chain
required: false
concurrency:
group: base-${{ github.sha }}
cancel-in-progress: true
env:
GITEA_SSH_HOST: kankali.darkhelm.lan
GITEA_SSH_PORT: "2222"
GITEA_REPO_SSH_URL: ssh://git@kankali.darkhelm.lan:2222/DarkHelm.org/plex-playlist.git
GITEA_REGISTRY: kankali.darkhelm.lan:3001
GITEA_REGISTRY_IP: 10.18.75.2
GITEA_REGISTRY_HOST: kankali.darkhelm.lan
defaults:
run:
shell: bash
jobs:
publish-base:
name: Build and Publish CICD Base Image
# Use ubuntu-act runner pool which is known to be available.
runs-on: ubuntu-act
timeout-minutes: 35
outputs:
base_hash: ${{ steps.base-state.outputs.base_hash }}
head_sha: ${{ steps.meta.outputs.head_sha }}
steps:
- name: Identify runner
run: |
echo "=== Runner Identity ==="
echo "runner_name=${RUNNER_NAME:-}"
echo "runner_name_hint=${GITEA_RUNNER_NAME:-${ACT_RUNNER_NAME:-${RUNNER_NAME:-unknown}}}"
echo "runner_hostname_env=${HOSTNAME:-unknown}"
echo "runner_uname_n=$(uname -n 2>/dev/null || echo unknown)"
echo "runner_etc_hostname=$(cat /etc/hostname 2>/dev/null || echo unknown)"
echo "runner_os=${RUNNER_OS:-unknown}"
echo "runner_arch=${RUNNER_ARCH:-unknown}"
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
- name: Emit startup diagnostics
shell: sh
env:
EVENT_NAME: ${{ github.event_name }}
SOURCE_WORKFLOW: ${{ github.event.inputs.source_workflow }}
FORCE_REBUILD: ${{ github.event.inputs.force_rebuild }}
TRACE_ID_INPUT: ${{ github.event.inputs.trace_id }}
HEAD_SHA_INPUT: ${{ github.event.inputs.head_sha }}
HEAD_SHA_FALLBACK: ${{ github.sha }}
REF: ${{ github.ref }}
REF_NAME: ${{ github.ref_name }}
HEAD_REF: ${{ github.head_ref }}
TARGET_LABEL: ubuntu-act
run: |
RESOLVED_HEAD_SHA="${HEAD_SHA_INPUT:-${HEAD_SHA_FALLBACK}}"
TRACE_SUFFIX="$(printf '%s' "${RESOLVED_HEAD_SHA}" | cut -c1-8)"
TRACE_ID="${TRACE_ID_INPUT:-cicd-base-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${TRACE_SUFFIX}}"
echo "=== Base Workflow Startup Audit ==="
echo "event_name=${EVENT_NAME}"
echo "source_workflow=${SOURCE_WORKFLOW}"
echo "force_rebuild=${FORCE_REBUILD}"
echo "head_sha_input=${HEAD_SHA_INPUT}"
echo "head_sha=${RESOLVED_HEAD_SHA}"
echo "ref=${REF}"
echo "ref_name=${REF_NAME}"
echo "head_ref=${HEAD_REF}"
echo "trace_id=${TRACE_ID}"
echo "target_runner_label=${TARGET_LABEL}"
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "startup_audit=ok"
- name: Preflight Docker and registry connectivity
env:
PACKAGE_ACCESS_TOKEN: ${{ secrets.PACKAGE_ACCESS_TOKEN }}
REGISTRY_USER: ${{ github.actor }}
run: |
set -e
if ! grep -q "${GITEA_REGISTRY_HOST}" /etc/hosts; then
echo "${GITEA_REGISTRY_IP} ${GITEA_REGISTRY_HOST}" >> /etc/hosts
fi
echo "=== Docker Preflight ==="
command -v docker
docker --version
docker info >/tmp/docker-info.log 2>&1 || (echo "❌ docker info failed" && tail -n 40 /tmp/docker-info.log && exit 1)
grep -qi "${GITEA_REGISTRY}" /tmp/docker-info.log || echo "⚠ Registry not listed in docker info insecure registries: ${GITEA_REGISTRY}"
echo "=== Registry Login Preflight ==="
if [ -z "${PACKAGE_ACCESS_TOKEN}" ]; then
echo "❌ PACKAGE_ACCESS_TOKEN is empty"
exit 1
fi
echo "${PACKAGE_ACCESS_TOKEN}" | docker login "http://${GITEA_REGISTRY}" -u "${REGISTRY_USER}" --password-stdin >/tmp/docker-login.log 2>&1 || (echo "❌ Registry login preflight failed" && tail -n 40 /tmp/docker-login.log && exit 1)
echo "registry_login_preflight=ok"
- name: Audit trigger context
env:
EVENT_NAME: ${{ github.event_name }}
SOURCE_WORKFLOW: ${{ github.event.inputs.source_workflow }}
FORCE_REBUILD: ${{ github.event.inputs.force_rebuild }}
TRACE_ID_INPUT: ${{ github.event.inputs.trace_id }}
HEAD_SHA_INPUT: ${{ github.event.inputs.head_sha }}
HEAD_SHA_FALLBACK: ${{ github.sha }}
REF: ${{ github.ref }}
REF_NAME: ${{ github.ref_name }}
HEAD_REF: ${{ github.head_ref }}
run: |
RESOLVED_HEAD_SHA="${HEAD_SHA_INPUT:-${HEAD_SHA_FALLBACK}}"
TRACE_ID="${TRACE_ID_INPUT:-cicd-base-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${RESOLVED_HEAD_SHA:0:8}}"
echo "=== Dispatch Audit: CICD Base Image ==="
echo "event_name=${EVENT_NAME}"
echo "source_workflow=${SOURCE_WORKFLOW}"
echo "force_rebuild=${FORCE_REBUILD}"
echo "head_sha_input=${HEAD_SHA_INPUT}"
echo "head_sha=${RESOLVED_HEAD_SHA}"
echo "ref=${REF}"
echo "ref_name=${REF_NAME}"
echo "head_ref=${HEAD_REF}"
echo "trace_id=${TRACE_ID}"
- name: Resolve head SHA
id: meta
env:
HEAD_SHA_INPUT: ${{ github.event.inputs.head_sha }}
HEAD_SHA_FALLBACK: ${{ github.sha }}
run: |
RESOLVED_HEAD_SHA="${HEAD_SHA_INPUT:-${HEAD_SHA_FALLBACK}}"
echo "head_sha=${RESOLVED_HEAD_SHA}" >> "$GITHUB_OUTPUT"
- name: Minimal checkout for base inputs
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
GITHUB_SHA: ${{ steps.meta.outputs.head_sha }}
run: |
umask 077
trap 'rm -f ~/.ssh/id_rsa' EXIT
echo "=== Minimal Repository Checkout for Base Inputs ==="
if ! grep -q "${GITEA_REGISTRY_HOST}" /etc/hosts; then
echo "${GITEA_REGISTRY_IP} ${GITEA_REGISTRY_HOST}" >> /etc/hosts
fi
if [ -n "${SSH_PRIVATE_KEY}" ]; then
mkdir -p ~/.ssh
echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
echo "Loaded SSH key fingerprint: $(ssh-keygen -lf ~/.ssh/id_rsa | awk '{print $2}')"
ssh-keyscan -p "${GITEA_SSH_PORT}" "${GITEA_SSH_HOST}" >> ~/.ssh/known_hosts 2>/dev/null
else
echo "❌ SSH_PRIVATE_KEY is empty"
exit 1
fi
GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \
git clone --depth 1 --no-checkout \
"${GITEA_REPO_SSH_URL}" .
if [ -n "${GITHUB_SHA}" ] && \
GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \
git fetch --depth 1 origin "${GITHUB_SHA}" >/dev/null 2>&1; then
git checkout FETCH_HEAD -- Dockerfile.cicd-base .dockerignore scripts/compute-cicd-base-hash.sh
echo "✓ Checked out base inputs from commit ${GITHUB_SHA}"
else
git checkout HEAD -- Dockerfile.cicd-base .dockerignore scripts/compute-cicd-base-hash.sh
echo "⚠ Falling back to default branch HEAD for base inputs checkout"
fi
chmod +x scripts/compute-cicd-base-hash.sh
rm -f ~/.ssh/id_rsa
- name: Compute base hash and inspect registry
id: base-state
env:
PACKAGE_ACCESS_TOKEN: ${{ secrets.PACKAGE_ACCESS_TOKEN }}
REGISTRY_USER: ${{ github.actor }}
FORCE_REBUILD: ${{ github.event.inputs.force_rebuild }}
run: |
echo "=== Computing CICD Base Image Hash ==="
BASE_HASH=$(./scripts/compute-cicd-base-hash.sh)
BASE_REF_HASH="${GITEA_REGISTRY}/darkhelm.org/plex-playlist-cicd-base:${BASE_HASH}"
BASE_REF_LATEST="${GITEA_REGISTRY}/darkhelm.org/plex-playlist-cicd-base:latest"
echo "Base hash: ${BASE_HASH}"
echo "base_hash=${BASE_HASH}" >> $GITHUB_OUTPUT
echo "base_ref_hash=${BASE_REF_HASH}" >> $GITHUB_OUTPUT
echo "base_ref_latest=${BASE_REF_LATEST}" >> $GITHUB_OUTPUT
if ! grep -q "${GITEA_REGISTRY_HOST}" /etc/hosts; then
echo "${GITEA_REGISTRY_IP} ${GITEA_REGISTRY_HOST}" >> /etc/hosts
fi
if ! echo "${PACKAGE_ACCESS_TOKEN}" | docker login "http://${GITEA_REGISTRY}" -u "${REGISTRY_USER}" --password-stdin >/dev/null 2>&1; then
echo "❌ Registry login failed during base inspection"
exit 1
fi
if [ "${FORCE_REBUILD}" = "true" ]; then
echo "Manual force rebuild requested"
echo "needs_build=true" >> $GITHUB_OUTPUT
exit 0
fi
for i in 1 2 3; do
echo "Existing base check ${i}/3 for ${BASE_REF_HASH}..."
# Manifest inspect is unreliable on some runners; use pull as truth.
if timeout 900 docker pull "${BASE_REF_HASH}" >/tmp/base-state-pull.log 2>&1; then
echo "✓ Immutable base image already exists and is pullable: ${BASE_REF_HASH}"
echo "needs_build=false" >> $GITHUB_OUTPUT
exit 0
else
pull_exit=$?
echo "Existing base pull check failed with exit code ${pull_exit}"
if [ "${pull_exit}" -eq 124 ]; then
echo "Existing base pull timed out after 900s"
fi
if [ -s /tmp/base-state-pull.log ]; then
tail -n 20 /tmp/base-state-pull.log || true
fi
fi
if [ "${i}" -lt 3 ]; then
sleep 10
fi
done
echo " Immutable base image missing or not yet pullable: ${BASE_REF_HASH}"
echo "needs_build=true" >> $GITHUB_OUTPUT
- name: Write registry push helpers
run: |
cat > /tmp/registry-push-helpers.sh <<'EOF'
#!/bin/bash
ensure_skopeo() {
if command -v skopeo >/dev/null 2>&1; then
return 0
fi
if command -v apt-get >/dev/null 2>&1; then
apt-get update && apt-get install -y skopeo
elif command -v apk >/dev/null 2>&1; then
apk add --no-cache skopeo
fi
if ! command -v skopeo >/dev/null 2>&1; then
echo "❌ skopeo not available for fallback push"
return 1
fi
}
push_ref_with_fallback() {
ref="$1"
local_image="$2"
archive_path="$3"
registry_user="$4"
package_access_token="$5"
for i in 1 2 3; do
echo "Docker push attempt ${i}/3 for ${ref}..."
if docker push "${ref}"; then
echo "✓ Docker push succeeded for ${ref}"
return 0
fi
if [ "${i}" -lt 3 ]; then
sleep 10
fi
done
echo "⚠ Docker push failed for ${ref}; trying skopeo fallback"
if ! ensure_skopeo; then
return 1
fi
if [ ! -f "${archive_path}" ]; then
echo "Creating local image archive for fallback push..."
if ! docker save "${local_image}" -o "${archive_path}"; then
echo "❌ Failed to create docker archive for fallback push"
return 1
fi
fi
if skopeo copy \
--dest-creds "${registry_user}:${package_access_token}" \
--dest-tls-verify=false \
"docker-archive:${archive_path}" \
"docker://${ref}"; then
echo "✓ Skopeo fallback push succeeded for ${ref}"
return 0
fi
echo "❌ Skopeo fallback push failed for ${ref}"
return 1
}
EOF
chmod 700 /tmp/registry-push-helpers.sh
- name: Build and push base image
if: steps.base-state.outputs.needs_build == 'true'
env:
PACKAGE_ACCESS_TOKEN: ${{ secrets.PACKAGE_ACCESS_TOKEN }}
REGISTRY_USER: ${{ github.actor }}
BASE_HASH: ${{ steps.base-state.outputs.base_hash }}
BASE_REF_HASH: ${{ steps.base-state.outputs.base_ref_hash }}
BASE_REF_LATEST: ${{ steps.base-state.outputs.base_ref_latest }}
run: |
echo "=== Building CICD Base Image ==="
if ! grep -q "${GITEA_REGISTRY_HOST}" /etc/hosts; then
echo "${GITEA_REGISTRY_IP} ${GITEA_REGISTRY_HOST}" >> /etc/hosts
fi
if ! echo "${PACKAGE_ACCESS_TOKEN}" | docker login "http://${GITEA_REGISTRY}" -u "${REGISTRY_USER}" --password-stdin; then
echo "❌ Registry login failed before build"
exit 1
fi
PLAYWRIGHT_BROWSERS_MIRROR_TAG="${GITEA_REGISTRY}/darkhelm.org/playwright-browsers:v1.56.1-jammy"
echo "playwright_browsers_mirror_tag=${PLAYWRIGHT_BROWSERS_MIRROR_TAG}"
if ! docker pull "${PLAYWRIGHT_BROWSERS_MIRROR_TAG}" >/tmp/playwright-mirror-pull.log 2>&1; then
echo "❌ Required internal Playwright browsers mirror image is missing: ${PLAYWRIGHT_BROWSERS_MIRROR_TAG}"
echo "Mirror this image into your internal registry before running base builds."
tail -n 80 /tmp/playwright-mirror-pull.log || true
exit 1
fi
PLAYWRIGHT_BROWSERS_IMAGE=$(docker image inspect --format='{{index .RepoDigests 0}}' "${PLAYWRIGHT_BROWSERS_MIRROR_TAG}" 2>/dev/null || true)
if [ -z "${PLAYWRIGHT_BROWSERS_IMAGE}" ]; then
echo "❌ Failed to resolve immutable digest for ${PLAYWRIGHT_BROWSERS_MIRROR_TAG}"
exit 1
fi
echo "playwright_browsers_image=${PLAYWRIGHT_BROWSERS_IMAGE}"
export DOCKER_BUILDKIT=1
BUILD_TIMEOUT_SECONDS=5400
BUILD_START_EPOCH=$(date +%s)
echo "docker_build_timeout_seconds=${BUILD_TIMEOUT_SECONDS}"
echo "docker_build_started_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
timeout "${BUILD_TIMEOUT_SECONDS}" docker build --progress=plain -f Dockerfile.cicd-base \
--build-arg BASE_IMAGE_VERSION="v1.0.0-${BASE_HASH}" \
--build-arg BASE_IMAGE_HASH="${BASE_HASH}" \
--build-arg PLAYWRIGHT_BROWSERS_IMAGE="${PLAYWRIGHT_BROWSERS_IMAGE}" \
-t cicd-base:latest .
BUILD_EXIT_CODE=$?
if [ "${BUILD_EXIT_CODE}" -ne 0 ]; then
if [ "${BUILD_EXIT_CODE}" -eq 124 ]; then
echo "❌ docker build timed out after ${BUILD_TIMEOUT_SECONDS}s"
else
echo "❌ docker build failed with exit code ${BUILD_EXIT_CODE}"
fi
exit "${BUILD_EXIT_CODE}"
fi
BUILD_END_EPOCH=$(date +%s)
BUILD_ELAPSED=$((BUILD_END_EPOCH - BUILD_START_EPOCH))
echo "docker_build_elapsed_seconds=${BUILD_ELAPSED}"
ARCHIVE_PATH="/tmp/cicd-base.tar"
source /tmp/registry-push-helpers.sh
docker tag cicd-base:latest "${BASE_REF_HASH}"
docker tag cicd-base:latest "${BASE_REF_LATEST}"
push_ref_with_fallback "${BASE_REF_HASH}" "cicd-base:latest" "${ARCHIVE_PATH}" "${REGISTRY_USER}" "${PACKAGE_ACCESS_TOKEN}"
push_ref_with_fallback "${BASE_REF_LATEST}" "cicd-base:latest" "${ARCHIVE_PATH}" "${REGISTRY_USER}" "${PACKAGE_ACCESS_TOKEN}"
- name: Verify published base image
env:
PACKAGE_ACCESS_TOKEN: ${{ secrets.PACKAGE_ACCESS_TOKEN }}
REGISTRY_USER: ${{ github.actor }}
BASE_REF_HASH: ${{ steps.base-state.outputs.base_ref_hash }}
run: |
echo "=== Verifying Published CICD Base Image ==="
if ! grep -q "${GITEA_REGISTRY_HOST}" /etc/hosts; then
echo "${GITEA_REGISTRY_IP} ${GITEA_REGISTRY_HOST}" >> /etc/hosts
fi
if ! echo "${PACKAGE_ACCESS_TOKEN}" | docker login "http://${GITEA_REGISTRY}" -u "${REGISTRY_USER}" --password-stdin; then
echo "❌ Registry login failed during verification"
exit 1
fi
for i in 1 2 3 4 5 6; do
echo "Verification attempt ${i}/6 for ${BASE_REF_HASH}..."
if docker pull "${BASE_REF_HASH}"; then
echo "✓ Published base image is pullable: ${BASE_REF_HASH}"
exit 0
fi
if [ "${i}" -lt 6 ]; then
echo "⚠ Pull failed; waiting 20s before retry"
sleep 20
fi
done
echo "❌ Published base image could not be pulled after 6 attempts: ${BASE_REF_HASH}"
exit 1
- name: Dispatch main build workflow
env:
ACTIONS_TRIGGER_TOKEN: ${{ secrets.ACTIONS_TRIGGER_TOKEN }}
PACKAGE_ACCESS_TOKEN: ${{ secrets.PACKAGE_ACCESS_TOKEN }}
HEAD_SHA: ${{ steps.meta.outputs.head_sha }}
BASE_HASH: ${{ steps.base-state.outputs.base_hash }}
REPO_FULL: ${{ github.repository }}
HEAD_REF: ${{ github.head_ref }}
REF_NAME: ${{ github.ref_name }}
TRACE_ID_INPUT: ${{ github.event.inputs.trace_id }}
run: |
set -e
DISPATCH_TOKEN="${ACTIONS_TRIGGER_TOKEN:-${PACKAGE_ACCESS_TOKEN:-}}"
if [ -z "${DISPATCH_TOKEN}" ]; then
echo "❌ Missing dispatch token. Set ACTIONS_TRIGGER_TOKEN (repo write scope) or ensure PACKAGE_ACCESS_TOKEN has Actions workflow-dispatch permissions."
exit 1
fi
if ! grep -q "${GITEA_REGISTRY_HOST}" /etc/hosts; then
echo "${GITEA_REGISTRY_IP} ${GITEA_REGISTRY_HOST}" >> /etc/hosts
fi
REPO_OWNER="${REPO_FULL%/*}"
REPO_NAME="${REPO_FULL#*/}"
TARGET_REF="${HEAD_REF:-${REF_NAME}}"
TRACE_ID="${TRACE_ID_INPUT:-cicd-base-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${HEAD_SHA:0:8}}"
echo "trace_id=${TRACE_ID}"
echo "target_ref=${TARGET_REF}"
CANDIDATE_API_BASES=()
if [ -n "${GITHUB_SERVER_URL:-}" ]; then
CANDIDATE_API_BASES+=("${GITHUB_SERVER_URL%/}/api/v1")
fi
CANDIDATE_API_BASES+=("http://${GITEA_REGISTRY_IP}:3001/api/v1")
CANDIDATE_API_BASES+=("http://${GITEA_REGISTRY_HOST}:3001/api/v1")
if ! command -v curl >/dev/null 2>&1; then
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq curl ca-certificates
fi
HELPER_PATH="/tmp/dispatch-workflow.sh"
fetch_dispatch_helper() {
local helper_ref="$1"
local api_base
for api_base in "${CANDIDATE_API_BASES[@]}"; do
helper_url="${api_base}/repos/${REPO_OWNER}/${REPO_NAME}/raw/scripts/dispatch-workflow.sh?ref=${helper_ref}"
if curl -fsS --connect-timeout 5 --max-time 20 \
-H "Authorization: token ${DISPATCH_TOKEN}" \
-H "User-Agent: plex-playlist-cicd-base" \
-o "${HELPER_PATH}" \
"${helper_url}"; then
chmod +x "${HELPER_PATH}"
return 0
fi
done
return 1
}
if ! fetch_dispatch_helper "${TARGET_REF}" && ! fetch_dispatch_helper "${HEAD_SHA}"; then
echo "❌ Failed to fetch scripts/dispatch-workflow.sh from repository"
exit 1
fi
DISPATCH_ARGS=(
--token "${DISPATCH_TOKEN}"
--repo "${REPO_FULL}"
--workflow "docker-build-main.yaml"
--ref "${TARGET_REF}"
--head-sha "${HEAD_SHA}"
--source-workflow "CICD Base Image"
--trace-id "${TRACE_ID}"
--base-needed "true"
--base-hash "${BASE_HASH}"
)
for API_BASE in "${CANDIDATE_API_BASES[@]}"; do
DISPATCH_ARGS+=(--api-base "${API_BASE}")
done
"${HELPER_PATH}" "${DISPATCH_ARGS[@]}"
- name: Failure diagnostics
if: failure()
run: |
echo "=== Failure Diagnostics ==="
date -u '+timestamp_utc=%Y-%m-%dT%H:%M:%SZ'
echo "runner_name=${RUNNER_NAME:-unknown}"
echo "runner_hostname=${HOSTNAME:-unknown}"
uname -a || true
cat /etc/os-release 2>/dev/null || true
df -h || true
free -h || true
ps aux --sort=-%mem | head -n 30 || true
if command -v docker >/dev/null 2>&1; then
echo "=== Docker Diagnostics ==="
docker version || true
docker info || true
docker ps -a || true
docker images --digests | head -n 50 || true
else
echo "docker not available on this runner"
fi
echo "=== Kernel Tail ==="
dmesg | tail -n 120 || true