Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/publish-ci-php-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Publish shared CI PHP images

on:
pull_request:
workflow_dispatch:

permissions:
contents: read
packages: write

concurrency:
group: codecorn-ci-image-publisher
cancel-in-progress: false

jobs:
publish:
name: Build, verify and publish GHCR PHP/Playwright toolchains
runs-on: [self-hosted, Linux, X64, codecorn-org]
timeout-minutes: 120
if: github.repository == 'CodeCornTech/.github' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

- name: Verify DKR-01 disk and build prerequisites
shell: bash
run: |
set -euo pipefail
test "$(hostname -s | tr '[:upper:]' '[:lower:]')" = dkr-01
docker info >/dev/null
bash ci/images/build.sh --dry-run

- name: Build container images
shell: bash
run: bash ci/images/build.sh --build

- name: Smoke-test PHP, Composer, Node and Playwright
shell: bash
run: bash ci/images/build.sh --check

- name: Authenticate only for GHCR publication
if: github.event_name == 'workflow_dispatch'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Publish the five versioned images
if: github.event_name == 'workflow_dispatch'
shell: bash
run: printf 'PUBLISH\n' | bash ci/images/build.sh --publish
43 changes: 43 additions & 0 deletions ci/images/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# syntax=docker/dockerfile:1
# Ubuntu Noble + maintained PHP packages (including legacy 8.1), Node 22 and Composer.
# Build once per PHP minor; never install PHP from inside a CI job.
ARG PHP_VERSION=8.3
FROM node:22-bookworm-slim AS node
FROM composer:2 AS composer
FROM ubuntu:24.04 AS base
ARG PHP_VERSION
LABEL org.opencontainers.image.source="https://github.com/CodeCornTech/.github"
ENV DEBIAN_FRONTEND=noninteractive COMPOSER_ALLOW_SUPERUSER=1 NODE_PATH=/usr/local/lib/node_modules
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
ca-certificates curl gnupg software-properties-common \
bash git zip unzip jq python3; \
add-apt-repository -y ppa:ondrej/php; \
apt-get update; \
apt-get install -y --no-install-recommends \
"php${PHP_VERSION}-cli" "php${PHP_VERSION}-zip" "php${PHP_VERSION}-bcmath" \
"php${PHP_VERSION}-intl" "php${PHP_VERSION}-mbstring" \
"php${PHP_VERSION}-sqlite3" "php${PHP_VERSION}-pgsql" \
"php${PHP_VERSION}-mysql" "php${PHP_VERSION}-xml" \
"php${PHP_VERSION}-curl"; \
ln -s "/usr/bin/php${PHP_VERSION}" /usr/local/bin/php; \
apt-get clean; \
rm -rf /var/lib/apt/lists/*
COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /usr/local/lib/node_modules/ /usr/local/lib/node_modules/
COPY --from=composer /usr/bin/composer /usr/local/bin/composer
RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
&& ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx \
&& php -v && node -v && npm -v && composer --version
WORKDIR /workspace

FROM base AS ci

FROM base AS playwright
ARG PLAYWRIGHT_VERSION=1.62.1
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \
PLAYWRIGHT_MODULE=/usr/local/lib/node_modules/playwright
RUN npm install --global "playwright@${PLAYWRIGHT_VERSION}" \
&& npx playwright install --with-deps chromium \
&& node -e 'require(process.env.PLAYWRIGHT_MODULE).chromium.executablePath()'
21 changes: 21 additions & 0 deletions ci/images/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Shared PHP/Node CI containers on DKR-01

Use five versioned images in GHCR:

- `ghcr.io/codecorntech/ci-php:8.1-node22-v1`
- `ghcr.io/codecorntech/ci-php:8.2-node22-v1`
- `ghcr.io/codecorntech/ci-php:8.3-node22-v1`
- `ghcr.io/codecorntech/ci-php:8.4-node22-v1`
- `ghcr.io/codecorntech/ci-php:8.1-node22-playwright1.62.1-v1`

These are proposed tags **not yet published**. Images use Ubuntu 24.04 and the same updated Ondrej PHP APT packages as DKR-01 (including the PHP 8.1 compatibility branch), Node 22 from official Node images and Composer 2. This avoids building CI on a long-unpatched official PHP 8.1 image. Playwright installs Chromium once at image build time.

On DKR-01 (the 32 GB VM) run `bash ci/images/build.sh --dry-run`, then `--build`, then `--check`. Publish only when the check passes, with an authorized `docker login ghcr.io` using a `write:packages` token and `--publish`.

Private GHCR packages need explicit GitHub Actions access for *both* `CodeCornTech/barbagia-media-manager` and `fgirolami29/wp-life-helper`, or make the image public. Workflow `jobs.container` attempts to pull images; a locally built tag is not a published GHCR package. **Do not merge consumer workflows before image access is confirmed.**

The build command requires >=6 GiB free on /var/lib/docker and does not remove volumes, images or caches. With only 32 GiB, inspect `docker system df` after builds. Jobs must be trusted: membership in the Docker group is root-equivalent and different Unix accounts sharing the same Docker daemon do **not** isolate runner credentials. Do not add untrusted fork PRs. Do not mount Docker socket into PHP job containers.

## Manual publisher (no PAT stored on VM)

After a successful local `--check`, the source PR has been approved and merged to the default branch, and the organization runner is online, manually dispatch `.github/workflows/publish-ci-php-images.yml`. It authenticates using the short-lived workflow `GITHUB_TOKEN` with `packages: write` and publishes the five versioned tags. Do not dispatch until there is sufficient disk free and no other heavy build running.
59 changes: 59 additions & 0 deletions ci/images/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/bin/bash
# DKR-01: prepare four PHP/Node toolchains and one Playwright image.
# --dry-run (default), --build, --check, --publish (explicit).
set -Eeuo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")"
mode="${1:---dry-run}"
[[ "$#" -le 1 ]] || exit 2
case "$mode" in --dry-run|--build|--check|--publish) ;; *) echo 'Usage: build.sh --dry-run|--build|--check|--publish' >&2; exit 2 ;; esac
base='ghcr.io/codecorntech/ci-php'
versions=(8.1 8.2 8.3 8.4)
browser_tag="${base}:8.1-node22-playwright1.62.1-v1"
if [[ "$mode" == --dry-run ]]; then
echo 'NO CHANGES: would build/verify four PHP images + one Playwright image'
for v in "${versions[@]}"; do echo "${base}:${v}-node22-v1"; done
echo "$browser_tag"
echo 'No Docker prune. Publish separate and explicit, requires GHCR login.'
df -h /var/lib/docker 2>/dev/null || df -h /
exit 0
fi
docker info >/dev/null || { echo 'ERROR: cannot access Docker daemon' >&2; exit 1; }
if [[ "$mode" == --build ]]; then
free_kb="$(df -Pk /var/lib/docker | awk 'NR==2 {print $4}')"
[[ "$free_kb" =~ ^[0-9]+$ && "$free_kb" -ge 6291456 ]] || { echo 'ERROR: less than 6GiB disk free' >&2; exit 1; }
for v in "${versions[@]}"; do
tag="${base}:${v}-node22-v1"
docker buildx build --load --pull --progress=plain --target ci -f Dockerfile --build-arg "PHP_VERSION=$v" -t "$tag" .
done
docker buildx build --load --progress=plain --target playwright -f Dockerfile \
--build-arg 'PHP_VERSION=8.1' \
--build-arg 'PLAYWRIGHT_VERSION=1.62.1' -t "$browser_tag" .
echo 'Build finished; run --check before --publish.'
elif [[ "$mode" == --check ]]; then
for v in "${versions[@]}"; do
tag="${base}:${v}-node22-v1"
docker image inspect "$tag" >/dev/null || { echo "MISSING: $tag"; exit 1; }
major="${v%%.*}"; minor="${v#*.}"
docker run --rm --pull=never --entrypoint sh "$tag" -ec \
"php -r 'exit(PHP_MAJOR_VERSION===$major && PHP_MINOR_VERSION===$minor ? 0 : 1);' && php -m | grep -iq zip && node --version | grep -q '^v22[.]' && composer --version"
echo "OK: $tag"
done
docker image inspect "$browser_tag" >/dev/null || { echo "MISSING: $browser_tag"; exit 1; }
docker run --rm --pull=never --entrypoint node "$browser_tag" -e \
'const p = require(process.env.PLAYWRIGHT_MODULE); const fs = require("node:fs"); if (!fs.existsSync(p.chromium.executablePath())) process.exit(1);'
echo "OK: $browser_tag"
docker system df
echo 'CHECK PASSED'
else
echo 'Requires existing docker login ghcr.io (write:packages).'
read -r -p 'Type PUBLISH to upload all five images: ' confirm
[[ "$confirm" == PUBLISH ]] || { echo 'Aborted'; exit 1; }
for v in "${versions[@]}"; do
tag="${base}:${v}-node22-v1"
docker image inspect "$tag" >/dev/null
docker push "$tag"
done
docker image inspect "$browser_tag" >/dev/null
docker push "$browser_tag"
echo 'Verify GHCR visibility/access from BOTH GitHub owners before CI migration.'
fi
25 changes: 25 additions & 0 deletions ci/runners/patch-register-v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
"""Fix runner v2's incorrect GitHub SHA-256 release note parser, safely."""
from pathlib import Path
import subprocess
import sys

if len(sys.argv) != 2:
raise SystemExit("Usage: python3 patch-register-v2.py /path/to/cc-dkr01-register-runners-v2.sh")
p = Path(sys.argv[1]).resolve()
source = p.read_text()
start_marker = 'read -r download_url expected_sha archive_name < <(python3 -c'
end_marker = '[[ "$expected_sha" =~ ^[a-f0-9]{64}$ ]]'
if start_marker not in source or end_marker not in source:
raise SystemExit("STOP: expected v2 checksum parser not found; no changes")
start = source.index(start_marker)
end = source.index(end_marker, start)
replacement = '''resolved="$(python3 -c '\nimport json, re, sys\ntry:\n meta = json.load(sys.stdin)\nexcept ValueError as exc:\n raise SystemExit(f"Invalid GitHub release metadata: {exc}")\npattern = re.compile(r"actions-runner-linux-x64-[0-9]+(?:\\.[0-9]+)+\\.tar\\.gz")\nassets = [a for a in meta.get("assets", []) if pattern.fullmatch(a.get("name", ""))]\nif len(assets) != 1:\n raise SystemExit("Expected exactly one GitHub runner Linux x64 archive")\na = assets[0]\nname = a["name"]\nurl = a.get("browser_download_url", "")\nif not url.startswith("https://github.com/actions/runner/releases/download/"):\n raise SystemExit("Unexpected GitHub runner download URL")\ndig = re.fullmatch(r"sha256:([a-fA-F0-9]{64})", a.get("digest") or "")\nbody = meta.get("body") or ""\nnotes = re.search(re.escape(name)+r"[\\x60\\s:|–-]+(?:SHA-?256[\\s:=]+)?([a-fA-F0-9]{64})(?![a-fA-F0-9])", body, re.I)\nif dig and notes and dig.group(1).lower() != notes.group(1).lower():\n raise SystemExit("Checksum disagreement between API digest and release notes")\nsha = dig or notes\nif sha is None:\n raise SystemExit("No official SHA256 for Linux runner; refusing to proceed")\nprint(f"{url}\\t{sha.group(1).lower()}\\t{name}")\n' <<< "$metadata")" || fail 'Release information/checksum incomplete'\nIFS=$'\\t' read -r download_url expected_sha archive_name <<< "$resolved"\n'''
updated = source[:start] + replacement + source[end:]
backup = p.with_name(p.name + ".before-digest-fix")
if backup.exists():
raise SystemExit("STOP: backup already exists; refusing to overwrite")
subprocess.run(["bash", "-n"], input=updated, text=True, check=True)
backup.write_text(source)
p.write_text(updated)
print(f"PATCHED: {p}\nBACKUP: {backup}\nVALIDATED: bash -n passed")
Loading