chore: replace compat workflows

This commit is contained in:
le0m
2026-10-24 21:12:02 +02:00
parent 8a21d40f24
commit ca961e7807
6 changed files with 226 additions and 126 deletions
+16
View File
@@ -0,0 +1,16 @@
name: Track deb.sury.org patches
description: >
For each EOL version in eol/, compares the Salsa commit pinned in its Dockerfiles' DEB_PATCHES
with the one deb.sury.org last built from, and opens or updates an issue when they differ.
inputs:
token:
description: Token used to read and write the repository's issues.
default: ${{ github.token }}
distribution:
description: Debian release whose deb.sury.org source index is read.
default: trixie
runs:
using: node24
main: index.mjs
+171
View File
@@ -0,0 +1,171 @@
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { gunzipSync } from 'node:zlib';
const SALSA = 'https://salsa.debian.org/php-team/php';
const SALSA_API = 'https://salsa.debian.org/api/v4/projects/php-team%2Fphp';
const token = process.env['INPUT_TOKEN'];
const distribution = process.env['INPUT_DISTRIBUTION'] || 'trixie';
const repository = process.env.GITHUB_REPOSITORY;
const githubApi = process.env.GITHUB_API_URL || 'https://api.github.com';
// Salsa sometimes answers with a 502, so server and network errors are retried.
async function download(url, attempts = 5) {
for (let attempt = 1; ; attempt++) {
const response = await fetch(url).catch((error) => ({ ok: false, status: 0, error }));
if (response.ok) {
return response;
}
const clientError = response.status >= 400 && response.status < 500;
if (clientError || attempt === attempts) {
throw new Error(`${url}: ${response.error?.message ?? `HTTP ${response.status}`}`);
}
await new Promise((resolve) => setTimeout(resolve, attempt * 3000));
}
}
async function github(method, path, body) {
const response = await fetch(`${githubApi}${path}`, {
method,
headers: {
accept: 'application/vnd.github+json',
authorization: `Bearer ${token}`,
'content-type': 'application/json',
'x-github-api-version': '2022-11-28',
},
body: body && JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`GitHub API ${method} ${path}: HTTP ${response.status} ${await response.text()}`);
}
return response.json();
}
/** The EOL versions: every eol/<X.Y> with a cli Dockerfile. */
function eolVersions() {
return readdirSync('eol', { withFileTypes: true })
.filter((entry) => entry.isDirectory() && existsSync(join('eol', entry.name, 'cli', 'Dockerfile')))
.map((entry) => entry.name)
.sort();
}
/** The Salsa commit pinned in DEB_PATCHES, which every flavour of the version must agree on. */
function pinnedCommit(version) {
const commits = new Set();
for (const flavor of readdirSync(join('eol', version))) {
const dockerfile = join('eol', version, flavor, 'Dockerfile');
const match = readFileSync(dockerfile, 'utf8').match(/DEB_PATCHES=\S*\/archive\/([0-9a-f]{40})\//);
if (!match) {
throw new Error(`${dockerfile} has no DEB_PATCHES commit`);
}
commits.add(match[1]);
}
if (commits.size !== 1) {
throw new Error(`the Dockerfiles pin different commits: ${[...commits].join(', ')}`);
}
return [...commits][0];
}
/** Package name → version, from deb.sury.org's index of source packages. */
async function latestBuilds() {
const response = await download(`https://packages.sury.org/php/dists/${distribution}/main/source/Sources.gz`);
const index = gunzipSync(Buffer.from(await response.arrayBuffer())).toString('utf8');
const builds = new Map();
for (const stanza of index.split(/\n\s*\n/)) {
const name = stanza.match(/^Package: (\S+)$/m)?.[1];
const version = stanza.match(/^Version: (\S+)$/m)?.[1];
if (name && version && !builds.has(name)) {
builds.set(name, version);
}
}
return builds;
}
/**
* Splits a build version such as "1:7.4.33-31+0~20261021.121+debian13~1.gbpe9c428" into its
* revision without epoch ("7.4.33-31") and the start of the Salsa commit it was built from
* ("e9c428").
*/
function parseBuild(version) {
const match = version.match(/^(?:\d+:)?([^+]+)\+.*\.gbp([0-9a-f]+)$/);
return match && { revision: match[1], commitPrefix: match[2] };
}
async function fullCommit(prefix) {
const response = await download(`${SALSA_API}/repository/commits/${prefix}`);
return (await response.json()).id;
}
async function seriesPatches(commit) {
const response = await download(`${SALSA}/-/raw/${commit}/debian/patches/series`);
return (await response.text())
.split('\n')
.map((line) => line.trim().split(/\s+/)[0])
.filter((name) => name && !name.startsWith('#'));
}
async function openIssues() {
const issues = [];
for (let page = 1; ; page++) {
const batch = await github('GET', `/repos/${repository}/issues?state=open&per_page=100&page=${page}`);
issues.push(...batch.filter((issue) => !issue.pull_request));
if (batch.length < 100) {
return issues;
}
}
}
async function check(version, builds, issues) {
const pinned = pinnedCommit(version);
const build = builds.get(`php${version}`);
const parsed = build && parseBuild(build);
if (!parsed) {
throw new Error(`no php${version} build with a commit in the deb.sury.org index (got ${build ?? 'nothing'})`);
}
if (pinned.startsWith(parsed.commitPrefix)) {
console.log(`PHP ${version}: up to date (${build})`);
return;
}
const latest = await fullCommit(parsed.commitPrefix);
const [before, after] = await Promise.all([seriesPatches(pinned), seriesPatches(latest)]);
const added = after.filter((name) => !before.includes(name));
const title = `PHP ${version} patches: new deb.sury.org build ${parsed.revision}`;
const body = [
`deb.sury.org published \`${build}\`, built from Salsa commit [\`${latest.slice(0, 12)}\`](${SALSA}/-/commit/${latest}). Our Dockerfiles pin [\`${pinned.slice(0, 12)}\`](${SALSA}/-/commit/${pinned}).`,
'',
`Changes: ${SALSA}/-/compare/${pinned}...${latest}`,
'',
'Patches added to its series:',
...(added.length ? added.map((name) => `- \`${name}\``) : ['None.']),
'',
`To update, set the new commit in \`DEB_PATCHES\` and the revision in \`PHP_PATCHES\` in every \`eol/${version}/*/Dockerfile\`, and review each new patch into \`eol/patches/series.${version}\`, applied or commented out.`,
].join('\n');
const existing = issues.find((issue) => issue.title.startsWith(`PHP ${version} patches:`));
if (existing) {
await github('PATCH', `/repos/${repository}/issues/${existing.number}`, { title, body });
console.log(`PHP ${version}: updated issue #${existing.number} (${build})`);
} else {
const issue = await github('POST', `/repos/${repository}/issues`, { title, body });
console.log(`PHP ${version}: opened issue #${issue.number} (${build})`);
}
}
try {
const builds = await latestBuilds();
const issues = await openIssues();
for (const version of eolVersions()) {
try {
await check(version, builds, issues);
} catch (error) {
console.log(`::error::PHP ${version}: ${error.message}`);
process.exitCode = 1;
}
}
} catch (error) {
console.log(`::error::${error.message}`);
process.exitCode = 1;
}
+16
View File
@@ -70,6 +70,7 @@ jobs:
[[ "$VERSION" == '5.6' || "$VERSION" == '7.0' ]] && pcov=false || pcov=true
[[ "$ARCH" == 'amd64' ]] && xhprof=true || xhprof=false
[[ "$VERSION" == 8.* || "$VERSION" == 'latest' || "$VERSION" == 'alpine' ]] && otel=true || otel=false
[[ "$VERSION" == '5.6' ]] && compat=true || compat=false
{
echo "tag=$tag"
echo "eol=$eol"
@@ -79,6 +80,7 @@ jobs:
echo "pcov=$pcov"
echo "xhprof=$xhprof"
echo "otel=$otel"
echo "compat=$compat"
} >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
@@ -184,6 +186,20 @@ jobs:
no-cache: ${{ env.NO_CACHE }}
test: otel
- name: Build compat image
if: ${{ steps.plan.outputs.compat == 'true' }}
uses: ./.github/actions/build-image
with:
image: php-compat
tag: ${{ steps.plan.outputs.tag }}
arch: ${{ matrix.arch }}
context: ./compat
build-args: BASE_IMAGE=${{ steps.php.outputs.ref }}
push: ${{ env.PUSH }}
cache-write: ${{ env.CACHE_WRITE }}
no-cache: ${{ env.NO_CACHE }}
test: compat
- name: Upload digests
if: ${{ env.PUSH == 'ghcr' }}
uses: actions/upload-artifact@v7
-71
View File
@@ -1,71 +0,0 @@
name: Publish compatibility images
on:
workflow_run:
workflows: ["Test compatibility images"]
branches: [main]
types: [completed]
# Every completed test run starts this workflow; runs that won't publish get their own group, so
# they can't cancel a publish in progress.
concurrency:
group: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'pull_request' && github.event.workflow_run.head_sha == github.sha && github.workflow || github.run_id }}
cancel-in-progress: true
jobs:
publish-compat:
name: Build and publish compatibility images
runs-on: ubuntu-22.04
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'pull_request' && github.event.workflow_run.head_sha == github.sha }}
strategy:
fail-fast: false
matrix:
version: [ '5.6' ]
flavor: [ '', '-apache', '-fpm' ]
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
with:
driver-opts: network=host
- name: Login to DockerHub (pull)
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_PULL_USER_4 }}
password: ${{ secrets.DOCKER_PULL_PASS_4 }}
- name: Pull base image
run: docker pull debian:stretch-slim
- name: Login to DockerHub (push)
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image and push to registry
uses: docker/build-push-action@v7
with:
platforms: linux/amd64,linux/arm64
# cache-from: type=registry,ref=ghcr.io/chialab/php-compat:${{ matrix.version }}${{ matrix.flavor }}
cache-to: type=inline
context: ./compat
build-contexts: patches=eol/patches
file: ${{ format('compat/{0}.{1}{2}', 'Dockerfile', matrix.version, matrix.flavor) }}
tags: |
chialab/php-compat:${{ matrix.version }}${{ matrix.flavor }}
ghcr.io/chialab/php-compat:${{ matrix.version }}${{ matrix.flavor }}
push: true
-55
View File
@@ -1,55 +0,0 @@
name: Test compatibility images
on:
schedule:
# chosen by fair dice roll
- cron: '42 18 * * 2'
push:
branches: [main]
pull_request:
types: [opened, synchronize]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test-compat:
name: Build and test compatibility images
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
version: [ '5.6' ]
flavor: [ '', '-apache', '-fpm' ]
services:
registry:
image: registry:2
ports:
- 5000:5000
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Login to DockerHub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_PULL_USER_4 }}
password: ${{ secrets.DOCKER_PULL_PASS_4 }}
- name: Build image for testing
uses: docker/build-push-action@v7
with:
# cache-from: type=registry,ref=ghcr.io/chialab/php-compat:${{ matrix.version }}${{ matrix.flavor }}
cache-to: type=inline
context: ./compat
build-contexts: patches=eol/patches
file: ${{ format('compat/{0}.{1}{2}', 'Dockerfile', matrix.version, matrix.flavor) }}
tags: localhost:5000/chialab/php-compat:${{ matrix.version }}${{ matrix.flavor }}
push: true
- name: Test image
env:
REGISTRY: localhost:5000/
VERSION: ${{ matrix.version }}${{ matrix.flavor }}
run: 'docker pull localhost:5000/chialab/php-compat:${{ matrix.version }}${{ matrix.flavor }} && make -C compat test'
+23
View File
@@ -0,0 +1,23 @@
name: Track deb.sury.org patches
# Opens an issue when deb.sury.org publishes a build of an EOL version from a newer Salsa commit
# than the one pinned in its Dockerfiles' DEB_PATCHES.
on:
schedule:
- cron: '17 6 * * 1'
workflow_dispatch:
jobs:
track:
name: Compare pinned commits with deb.sury.org builds
runs-on: ubuntu-24.04
permissions:
contents: read
issues: write
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Check each EOL version
uses: ./.github/actions/track-patches