ci: Docker images for various PHP versions

Also adds semver support.
This commit is contained in:
Kévin Dunglas 2023-07-07 18:17:06 +02:00
parent e66ac34ac7
commit 70110dcc77
10 changed files with 326 additions and 131 deletions

View File

@ -1,10 +1,10 @@
/.git/
/.github/
/.gitmodules/
/.idea/
/.vscode/
/docs/
/*Dockerfile*
/.*ignore
/*.hcl
/*.md
# ignored
**/*
# authorized
!**/Caddyfile
!**/*.go
!**/go.*
!**/*.c
!**/*.h
!testdata/*.php

View File

@ -11,35 +11,179 @@ on:
workflow_dispatch:
inputs: {}
jobs:
build:
prepare:
runs-on: ubuntu-latest
outputs:
# Push only if we're committing in the main branch
push: ${{toJson(github.ref == 'refs/heads/main' && github.event_name != 'pull_request')}}
variants: ${{ steps.matrix.outputs.variants }}
platforms: ${{ steps.matrix.outputs.platforms }}
metadata: ${{ steps.matrix.outputs.metadata }}
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
version: latest
- name: Create variants matrix
id: matrix
run: |
METADATA=$(docker buildx bake --print)
echo "variants=$(jq -c '.group.default.targets|map(sub("runner-|builder-"; ""))|unique' <<< $METADATA)" >> "$GITHUB_OUTPUT"
echo "platforms=$(jq -c 'first(.target[]) | .platforms' <<< $METADATA)" >> "$GITHUB_OUTPUT"
echo "metadata=$(jq -c <<< $METADATA)" >> "$GITHUB_OUTPUT"
env:
LATEST: '1' # TODO: unset this variable when releasing the first tagged version
SHA: ${{github.sha}}
VERSION: ${{github.ref_name}}
build:
runs-on: ubuntu-latest
needs:
- prepare
strategy:
fail-fast: false
matrix:
variant: ${{ fromJson(needs.prepare.outputs.variants) }}
platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
include:
- race: ""
qemu: true
- platform: linux/amd64
qemu: false
race: "-race" # The Go race detector is only supported on amd64
- platform: linux/386
qemu: false
steps:
- uses: actions/checkout@v3
- name: Set up QEMU
if: matrix.qemu
uses: docker/setup-qemu-action@v2
with:
platforms: ${{matrix.platform}}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
platforms: ${{matrix.platform}}
version: latest
- name: Login to DockerHub
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
if: fromJson(needs.prepare.outputs.push)
uses: docker/login-action@v2
with:
#registry: ${{secrets.REGISTRY_LOGIN_SERVER}}
username: ${{secrets.REGISTRY_USERNAME}}
password: ${{secrets.REGISTRY_PASSWORD}}
- name: Build for amd64
- name: Build
id: build
uses: docker/bake-action@v3
with:
pull: true
load: true
set: "*.platform=linux/amd64"
- name: Build for all platforms
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
uses: docker/bake-action@v3
targets: |
builder-${{matrix.variant}}
runner-${{matrix.variant}}
# Remove tags to prevent "can't push tagged ref [...] by digest" error
set: |
*.tags=
*.platform=${{matrix.platform}}
*.cache-from=type=gha,scope=${{github.ref}}-${{matrix.platform}}
*.cache-from=type=gha,scope=refs/heads/main-${{matrix.platform}}
*.cache-to=type=gha,scope=${{github.ref}}-${{matrix.platform}}
*.output=type=image,name=dunglas/frankenphp,push-by-digest=true,name-canonical=true,push=${{ needs.prepare.outputs.push }}
env:
LATEST: '1' # TODO: unset this variable when releasing the first tagged version
SHA: ${{github.sha}}
VERSION: ${{github.ref_name}}
# Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
- name: Export metadata
if: fromJson(needs.prepare.outputs.push)
run: |
mkdir -p /tmp/metadata/builder /tmp/metadata/runner
builderDigest=$(jq -r '."builder-${{matrix.variant}}"."containerimage.digest"' <<< $METADATA)
touch "/tmp/metadata/builder/${builderDigest#sha256:}"
runnerDigest=$(jq -r '."runner-${{matrix.variant}}"."containerimage.digest"' <<< $METADATA)
touch "/tmp/metadata/runner/${runnerDigest#sha256:}"
env:
METADATA: ${{steps.build.outputs.metadata}}
- name: Upload runner metadata
if: fromJson(needs.prepare.outputs.push)
uses: actions/upload-artifact@v3
with:
pull: true
push: true
- run: docker image ls
- name: Run tests on Debian Bookworm
name: metadata-builder-${{matrix.variant}}
path: /tmp/metadata/builder/*
if-no-files-found: error
retention-days: 1
- name: Upload runner metadata
if: fromJson(needs.prepare.outputs.push)
uses: actions/upload-artifact@v3
with:
name: metadata-runner-${{matrix.variant}}
path: /tmp/metadata/runner/*
if-no-files-found: error
retention-days: 1
- name: Run tests
if: '!matrix.qemu'
continue-on-error: true
run: |
docker run --rm dunglas/frankenphp:builder-bookworm "sh -c 'go test -race -v ./... && cd caddy && go test -race -v ./...'"
- name: Run tests on Alpine
docker run --platform=${{matrix.platform}} --rm \
dunglas/frankenphp@$(jq -r '."builder-${{matrix.variant}}"."containerimage.digest"' <<< $METADATA) \
"sh -c 'go test ${{matrix.race}} -v ./... && cd caddy && go test ${{matrix.race}} -v ./...'"
env:
METADATA: ${{steps.build.outputs.metadata}}
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
push:
runs-on: ubuntu-latest
needs:
- prepare
- build
if: fromJson(needs.prepare.outputs.push)
strategy:
fail-fast: false
matrix:
variant: ${{ fromJson(needs.prepare.outputs.variants) }}
target: ['builder', 'runner']
steps:
- name: Download metadata
uses: actions/download-artifact@v3
with:
name: metadata-${{matrix.target}}-${{matrix.variant}}
path: /tmp/metadata
- run: ls -R /tmp/metadata
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
version: latest
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{secrets.REGISTRY_USERNAME}}
password: ${{secrets.REGISTRY_PASSWORD}}
- name: Create manifest list and push
working-directory: /tmp/metadata
run: |
docker run --rm dunglas/frankenphp:builder-alpine "sh -c 'go test -race -v ./... && cd caddy && go test -race -v ./...'"
docker buildx imagetools create $(jq -cr '.target."${{matrix.target}}-${{matrix.variant}}".tags | map("-t " + .) | join(" ")' <<< $METADATA) \
$(printf 'dunglas/frankenphp@sha256:%s ' *)
env:
METADATA: ${{needs.prepare.outputs.metadata}}
- name: Inspect image
run: |
docker buildx imagetools inspect $(jq -cr '.target."${{matrix.target}}-${{matrix.variant}}".tags | first' <<< $METADATA)
env:
METADATA: ${{needs.prepare.outputs.metadata}}

View File

@ -8,9 +8,11 @@ jobs:
php-versions: ['8.2', '8.3']
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.20'
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
@ -19,10 +21,13 @@ jobs:
coverage: none
env:
phpts: ts
- name: Set include flags
run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
- name: Run library tests
run: go test -race -v ./...
- name: Run Caddy module tests
working-directory: caddy/
run: go test -race -v ./...

View File

@ -5,7 +5,7 @@
Build the dev Docker image:
docker build -t frankenphp-dev -f Dockerfile.dev .
docker build -t frankenphp-dev -f dev.Dockerfile .
docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -v $PWD:/go/src/app -it frankenphp-dev
The image contains the usual development tools (Go, GDB, Valgrind, Neovim...).

View File

@ -1,5 +1,52 @@
# syntax=docker/dockerfile:1
FROM php-base
FROM php-base AS builder
COPY --from=golang-base /usr/local/go/bin/go /usr/local/bin/go
COPY --from=golang-base /usr/local/go /usr/local/go
# This is required to link the FrankenPHP binary to the PHP binary
RUN apt-get update && \
apt-get -y --no-install-recommends install \
libargon2-dev \
libcurl4-openssl-dev \
libonig-dev \
libreadline-dev \
libsodium-dev \
libsqlite3-dev \
libssl-dev \
libxml2-dev \
zlib1g-dev \
&& \
apt-get clean
WORKDIR /go/src/app
COPY go.mod go.sum ./
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
RUN mkdir caddy && cd caddy
COPY caddy/go.mod caddy/go.sum ./caddy/
RUN cd caddy && go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
COPY *.* ./
COPY caddy caddy
COPY C-Thread-Pool C-Thread-Pool
COPY internal internal
COPY testdata testdata
# todo: automate this?
# see https://github.com/docker-library/php/blob/master/8.2/bookworm/zts/Dockerfile#L57-L59 for PHP values
ENV CGO_LDFLAGS="-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS" CGO_CFLAGS=$PHP_CFLAGS CGO_CPPFLAGS=$PHP_CPPFLAGS
RUN cd caddy/frankenphp && \
go build && \
cp frankenphp /usr/local/bin && \
cp /go/src/app/caddy/frankenphp/Caddyfile /etc/Caddyfile
ENTRYPOINT ["/bin/bash","-c"]
FROM php-base AS runner
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/

View File

@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
FROM php-base
FROM php-base AS builder
COPY --from=golang-base /usr/local/go/bin/go /usr/local/bin/go
COPY --from=golang-base /usr/local/go /usr/local/go
@ -44,3 +44,26 @@ RUN cd caddy/frankenphp && \
cp /go/src/app/caddy/frankenphp/Caddyfile /etc/Caddyfile
ENTRYPOINT ["/bin/sh","-c"]
FROM php-base AS runner
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
WORKDIR /app
RUN mkdir -p /app/public
RUN echo '<?php phpinfo();' > /app/public/index.php
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
COPY --from=builder /etc/Caddyfile /etc/Caddyfile
COPY --from=php-base /usr/local/include/php/ /usr/local/include/php
COPY --from=php-base /usr/local/lib/libphp.* /usr/local/lib
COPY --from=php-base /usr/local/lib/php/ /usr/local/lib/php
COPY --from=php-base /usr/local/php/ /usr/local/php
COPY --from=php-base /usr/local/bin/ /usr/local/bin
COPY --from=php-base /usr/src /usr/src
RUN sed -i 's/php/frankenphp run/g' /usr/local/bin/docker-php-entrypoint
CMD [ "--config", "/etc/Caddyfile" ]

View File

@ -1,47 +0,0 @@
# syntax=docker/dockerfile:1
FROM php-base
COPY --from=golang-base /usr/local/go/bin/go /usr/local/bin/go
COPY --from=golang-base /usr/local/go /usr/local/go
# This is required to link the FrankenPHP binary to the PHP binary
RUN apt-get update && \
apt-get -y --no-install-recommends install \
libargon2-dev \
libcurl4-openssl-dev \
libonig-dev \
libreadline-dev \
libsodium-dev \
libsqlite3-dev \
libssl-dev \
libxml2-dev \
zlib1g-dev \
&& \
apt-get clean
WORKDIR /go/src/app
COPY go.mod go.sum ./
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
RUN mkdir caddy && cd caddy
COPY caddy/go.mod caddy/go.sum ./caddy/
RUN cd caddy && go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
COPY *.* ./
COPY caddy caddy
COPY C-Thread-Pool C-Thread-Pool
COPY internal internal
COPY testdata testdata
# todo: automate this?
# see https://github.com/docker-library/php/blob/master/8.2-rc/bullseye/zts/Dockerfile#L57-L59 for php values
ENV CGO_LDFLAGS="-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS" CGO_CFLAGS=$PHP_CFLAGS CGO_CPPFLAGS=$PHP_CPPFLAGS
RUN cd caddy/frankenphp && \
go build && \
cp frankenphp /usr/local/bin && \
cp /go/src/app/caddy/frankenphp/Caddyfile /etc/Caddyfile
ENTRYPOINT ["/bin/bash","-c"]

View File

@ -1,68 +1,90 @@
variable "REPO_NAME" {
variable "IMAGE_NAME" {
default = "dunglas/frankenphp"
}
group "default" {
targets = ["bookworm-variants", "alpine-variants"]
variable "VERSION" {
default = "dev"
}
group "bookworm-variants" {
targets = ["bookworm-php-82", "builder-bookworm-php-82"]
variable "SHA" {}
variable "LATEST" {
default = false
}
group "alpine-variants" {
targets = ["alpine-php-82", "builder-alpine-php-82"]
variable "CACHE" {
default = ""
}
target "common" {
context = "."
platforms = ["linux/amd64", "linux/arm64"]
function "tag" {
params = [version, os, php-version, tgt]
result = [
version != "" ? format("%s:%s%s-php%s-%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", php-version, os) : "",
os == "bookworm" && php-version == "8.2" && version != "" ? format("%s:%s%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "") : "",
php-version == "8.2" && version != "" ? format("%s:%s%s-%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", os) : "",
os == "bookworm" && version != "" ? format("%s:%s%s-php%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", php-version) : ""
]
}
target "common-bookworm" {
# cleanTag ensures that the tag is a valid Docker tag
# see https://github.com/distribution/distribution/blob/v2.8.2/reference/regexp.go#L37
function "clean_tag" {
params = [tag]
result = substr(regex_replace(regex_replace(tag, "[^\\w.-]", "-"), "^([^\\w])", "r$0"), 0, 127)
}
# semver adds semver-compliant tag if a semver version number is passed, or returns the revision itself
# see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
function "semver" {
params = [rev]
result = __semver(_semver(regexall("^v?(?P<major>0|[1-9]\\d*)\\.(?P<minor>0|[1-9]\\d*)\\.(?P<patch>0|[1-9]\\d*)(?:-(?P<prerelease>(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", rev)))
}
function "_semver" {
params = [matches]
result = length(matches) == 0 ? {} : matches[0]
}
function "__semver" {
params = [v]
result = v == {} ? [clean_tag(VERSION)] : v.prerelease == null ? ["latest", v.major, "${v.major}.${v.minor}", "${v.major}.${v.minor}.${v.patch}"] : ["${v.major}.${v.minor}.${v.patch}-${v.prerelease}"]
}
target "default" {
name = "${tgt}-php-${replace(php-version, ".", "-")}-${os}"
matrix = {
os = ["bookworm", "alpine"]
php-version = ["8.2", "8.3.0alpha3"]
tgt = ["builder", "runner"]
}
contexts = {
php-base = "docker-image://php:8.2-zts-bookworm"
golang-base = "docker-image://golang:1.20-bookworm"
php-base = "docker-image://php:${php-version}-zts-${os}"
golang-base = "docker-image://golang:1.20-${os}"
}
dockerfile = os == "alpine" ? "alpine.Dockerfile" : "Dockerfile"
context = "./"
target = tgt
platforms = [
"linux/amd64",
"linux/386",
"linux/arm/v6",
"linux/arm/v7",
"linux/arm64",
]
tags = distinct(flatten([
LATEST ? tag("latest", os, php-version, tgt) : [],
tag(SHA == "" ? "" : "sha-${substr(SHA, 0, 7)}", os, php-version, tgt),
[for v in semver(VERSION) : tag(v, os, php-version, tgt)]
]))
labels = {
"org.opencontainers.image.title" = "FrankenPHP"
"org.opencontainers.image.description" = "The modern PHP app server"
"org.opencontainers.image.url" = "https://frankenphp.dev"
"org.opencontainers.image.source" = "https://github.com/dunglas/frankenphp"
"org.opencontainers.image.licenses" = "MIT"
"org.opencontainers.image.vendor" = "Kévin Dunglas"
"org.opencontainers.image.created" = "${timestamp()}"
"org.opencontainers.image.version" = VERSION
"org.opencontainers.image.revision" = SHA
}
}
target "common-alpine" {
contexts = {
php-base = "docker-image://php:8.2-zts-alpine3.18"
golang-base = "docker-image://golang:1.20-alpine3.18"
}
}
# Builders
target "builder-bookworm-php-82" {
inherits = ["common-bookworm"]
dockerfile = "builder-debian.Dockerfile"
tags = ["${REPO_NAME}:builder", "${REPO_NAME}:builder-bookworm"]
}
target "builder-alpine-php-82" {
inherits = ["common-alpine"]
dockerfile = "builder-alpine.Dockerfile"
tags = ["${REPO_NAME}:builder-alpine"]
}
#
# FrankenPHP
#
target "bookworm-php-82" {
inherits = ["common", "common-bookworm"]
contexts = {
builder = "target:builder-bookworm-php-82"
}
tags = ["${REPO_NAME}:bookworm", "${REPO_NAME}:latest"]
}
target "alpine-php-82" {
inherits = ["common", "common-alpine"]
contexts = {
builder = "target:builder-alpine-php-82"
}
tags = ["${REPO_NAME}:alpine"]
}

View File

@ -583,8 +583,9 @@ static void *manager_thread(void *arg) {
free(arg);
uintptr_t rh;
while ((rh = go_fetch_request()))
while ((rh = go_fetch_request())) {
thpool_add_work(thpool, go_execute_script, (void *) rh);
}
/* channel closed, shutdown gracefully */
thpool_wait(thpool);