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
180 changes: 180 additions & 0 deletions .github/workflows/build-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
name: Build and push images

on:
push:
branches:
- main
paths:
- '*/*/Dockerfile'
- '*/*/variants.yaml'
- '*/*/rootfs/**'
- '*/*/.scripts/**'
- '*/*/otel/**'
- '*/latest'
workflow_dispatch:
inputs:
target:
description: 'Image/version to build (e.g. php-hyperf/8.3 or php-hyperf/latest).'
required: true

permissions:
contents: read

jobs:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
detect:
runs-on: ubuntu-latest
outputs:
builds: ${{ steps.list.outputs.builds }}
steps:
- name: Checkout
uses: actions/checkout@v4
Comment thread
wilcorrea marked this conversation as resolved.

- name: Get changed files
id: changed
if: github.event_name == 'push'
uses: tj-actions/changed-files@v44

- name: Build matrix
id: list
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_TARGET: ${{ inputs.target }}
CHANGED_FILES: ${{ steps.changed.outputs.all_changed_files }}
run: |
set -euo pipefail

declare -a versions=()

collect_version() {
local v="$1"
[ -z "$v" ] && return
case "$v" in
[A-Za-z0-9_./-]*) ;;
*) echo "::warning::ignoring suspicious path '$v'"; return ;;
esac
local depth
depth=$(awk -F/ '{print NF}' <<< "$v")
if [ "$depth" != "2" ]; then return; fi
local resolved="$v"
if [ -L "$v" ]; then
local link_target
link_target=$(readlink "$v")
resolved="${v%/*}/${link_target}"
fi
if [ ! -f "${resolved}/Dockerfile" ]; then return; fi
for existing in "${versions[@]+"${versions[@]}"}"; do
if [ "$existing" = "$v" ]; then return; fi
done
versions+=("$v")
}

if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
collect_version "$INPUT_TARGET"
else
declare -a changed_dirs=()
for f in $CHANGED_FILES; do
dir=$(awk -F/ 'NF>=2 {print $1"/"$2}' <<< "$f")
[ -z "$dir" ] && continue
for existing in "${changed_dirs[@]+"${changed_dirs[@]}"}"; do
if [ "$existing" = "$dir" ]; then dir=""; break; fi
done
[ -n "$dir" ] && changed_dirs+=("$dir")
done

for v in "${changed_dirs[@]+"${changed_dirs[@]}"}"; do
collect_version "$v"
done

for symlink in */latest; do
[ -L "$symlink" ] || continue
target=$(readlink "$symlink")
parent="${symlink%/*}"
resolved="${parent}/${target}"
for changed in "${changed_dirs[@]+"${changed_dirs[@]}"}"; do
if [ "$changed" = "$resolved" ]; then
collect_version "$symlink"
break
fi
done
done
fi

declare -a builds=()
for v in "${versions[@]+"${versions[@]}"}"; do
image="${v%%/*}"
version="${v##*/}"
ctx="$v"
if [ -L "$ctx" ]; then
link_target=$(readlink "$ctx")
ctx="${ctx%/*}/${link_target}"
fi

manifest="${ctx}/variants.yaml"
if [ -f "$manifest" ]; then
if ! yq eval 'all_c(
(.target | type) == "!!str" and .target != "" and
has("suffix") and
((.suffix == null) or ((.suffix | type) == "!!str"))
)' "$manifest" | grep -qx true; then
echo "::error file=${manifest}::each entry must declare 'target' (non-empty string) and 'suffix' (key required; value must be string or null)"
exit 1
fi
entries=$(yq eval -o=json "$manifest")
else
entries='[{"target":"'"$image"'","suffix":""}]'
fi

while IFS= read -r line; do
[ -z "$line" ] && continue
builds+=("$line")
done < <(jq -c --arg img "$image" --arg ver "$version" --arg ctx "$ctx" '
.[] | {
image: $img,
tag: ($ver + (if (.suffix // "") == "" then "" else "-" + .suffix end)),
context: $ctx,
target: .target,
build_args: ((.args // {}) | to_entries | map(.key + "=" + (.value | tostring)) | join("\n"))
}
' <<< "$entries")
done

if [ ${#builds[@]} -eq 0 ]; then
echo "builds=[]" >> "$GITHUB_OUTPUT"
else
joined=$(IFS=,; echo "${builds[*]}")
echo "builds=[${joined}]" >> "$GITHUB_OUTPUT"
fi

build:
needs: detect
if: needs.detect.outputs.builds != '[]'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
build: ${{ fromJson(needs.detect.outputs.builds) }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: ./${{ matrix.build.context }}
target: ${{ matrix.build.target }}
build-args: ${{ matrix.build.build_args }}
platforms: linux/amd64
push: true
tags: devitools/${{ matrix.build.image }}:${{ matrix.build.tag }}
cache-from: type=gha,scope=${{ matrix.build.image }}-${{ matrix.build.tag }}
cache-to: type=gha,mode=max,scope=${{ matrix.build.image }}-${{ matrix.build.tag }}
106 changes: 106 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: Validate variants.yaml

on:
pull_request:
branches:
- main

permissions:
contents: read

jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Get changed variants.yaml files
id: changed
uses: tj-actions/changed-files@v44
with:
files: '*/*/variants.yaml'

- name: Validate each manifest
if: steps.changed.outputs.any_changed == 'true'
shell: bash
env:
CHANGED_MANIFESTS: ${{ steps.changed.outputs.all_changed_files }}
run: |
set -uo pipefail
errors=0

for manifest in $CHANGED_MANIFESTS; do
case "$manifest" in
[A-Za-z0-9_./-]*) ;;
*) echo "::warning::skipping suspicious path '$manifest'"; continue ;;
esac
echo "::group::${manifest}"
dir="$(dirname "$manifest")"
dockerfile="${dir}/Dockerfile"

if ! yq eval '.' "$manifest" > /dev/null 2>&1; then
echo "::error file=${manifest}::invalid YAML syntax"
errors=$((errors+1))
echo "::endgroup::"
continue
fi

kind=$(yq eval 'type' "$manifest")
if [ "$kind" != "!!seq" ]; then
echo "::error file=${manifest}::root must be a YAML list (got ${kind})"
errors=$((errors+1))
echo "::endgroup::"
continue
fi

entries=$(yq eval -o=json "$manifest")

bad=$(jq -c '[.[] | select(
(.target | type) != "string" or .target == "" or
(has("suffix") | not) or
(.suffix != null and (.suffix | type) != "string")
)]' <<< "$entries")
if [ "$(jq 'length' <<< "$bad")" != "0" ]; then
echo "::error file=${manifest}::entries with invalid 'target' (non-empty string required) or 'suffix' (string or null required):"
jq -r '.[] | " - " + (. | tostring)' <<< "$bad"
errors=$((errors+1))
fi

bad_args=$(jq -c '[.[] | select(has("args") and (.args | type) != "object")]' <<< "$entries")
if [ "$(jq 'length' <<< "$bad_args")" != "0" ]; then
echo "::error file=${manifest}::'args' must be a mapping when present:"
jq -r '.[] | " - " + (. | tostring)' <<< "$bad_args"
errors=$((errors+1))
fi

if [ ! -f "$dockerfile" ]; then
echo "::error file=${manifest}::sibling Dockerfile not found at ${dockerfile}"
errors=$((errors+1))
else
for target in $(jq -r '.[].target' <<< "$entries" | sort -u); do
if ! grep -qE "^FROM[[:space:]]+.*[[:space:]]+AS[[:space:]]+${target}([[:space:]]|$)" "$dockerfile"; then
echo "::error file=${manifest}::target '${target}' is not declared as a stage in ${dockerfile} (expected: FROM <base> AS ${target})"
errors=$((errors+1))
fi
done
fi

duplicates=$(jq -r '
[.[] | (if (.suffix // "") == "" then "<empty>" else .suffix end)] |
group_by(.) | map(select(length > 1)) | map(.[0]) | .[]
' <<< "$entries" | sort -u)
if [ -n "$duplicates" ]; then
echo "::error file=${manifest}::duplicate suffix(es) detected (would produce colliding tags):"
echo "$duplicates" | sed 's/^/ - /'
errors=$((errors+1))
fi

echo "::endgroup::"
done

if [ $errors -gt 0 ]; then
echo "Found ${errors} error(s) across the changed manifests."
exit 1
fi
echo "All changed variants.yaml are valid."
47 changes: 47 additions & 0 deletions php-hyperf/8.3/.scripts/setup-dev.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
set -e

SONAR_SCANNER_VERSION=6.2.1.4610

if [ "$1" = "dev" ]; then
if ! command -v apk &> /dev/null; then
echo "Error: 'apk' not found. Make sure you are running this script in an Alpine Linux environment." >&2
exit 1
fi

if ! command -v composer &> /dev/null; then
echo "Error: 'composer' not found. Make sure it is installed and accessible in the system PATH." >&2
exit 1
fi

echo "[$1] Installing PHP extensions and dependencies"

apk add --no-cache \
libstdc++ \
ca-certificates \
libc6-compat \
openjdk17-jre \
php83-pecl-xdebug \
php83-pecl-pcov

{
echo "opcache.enable=0"
echo "opcache.interned_strings_buffer=72"
echo "xdebug.mode=develop,debug,coverage"
echo "xdebug.idekey=PHPSTORM"
} >> /etc/php83/conf.d/zzz_2_php.ini

mkdir -p /opt
SONAR_BASE_URL=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli
SONAR_ASSET=sonar-scanner-cli-${SONAR_SCANNER_VERSION}-linux-x64.zip
curl -fSL "${SONAR_BASE_URL}/${SONAR_ASSET}" -o "/opt/${SONAR_ASSET}"
curl -fSL "${SONAR_BASE_URL}/${SONAR_ASSET}.sha256" -o "/opt/${SONAR_ASSET}.sha256"
(cd /opt && echo "$(cat "${SONAR_ASSET}.sha256") ${SONAR_ASSET}" | sha256sum -c -)

unzip -qq "/opt/${SONAR_ASSET}" -d /opt
mv /opt/sonar-scanner-${SONAR_SCANNER_VERSION}-linux-x64 /sonar-scanner
rm "/opt/${SONAR_ASSET}" "/opt/${SONAR_ASSET}.sha256"

ln -s /sonar-scanner/bin/sonar-scanner /bin/sonar-scanner

sed -i 's/use_embedded_jre=true/use_embedded_jre=false/g' /sonar-scanner/bin/sonar-scanner
fi
23 changes: 23 additions & 0 deletions php-hyperf/8.3/.scripts/setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
set -e

TIMEZONE=${1:-UTC}

if [ ! -f "/usr/share/zoneinfo/${TIMEZONE}" ]; then
echo "setup.sh: invalid TIMEZONE '${TIMEZONE}' (no zoneinfo file at /usr/share/zoneinfo/${TIMEZONE})" >&2
exit 1
fi

git config --global --add safe.directory /opt/www
git config --global init.defaultBranch main

# - config PHP
{
echo "upload_max_filesize=128M"
echo "post_max_size=128M"
echo "memory_limit=1G"
echo "date.timezone=${TIMEZONE}"
} | tee /etc/php83/conf.d/zzz_0_php.ini

# - config timezone
ln -sf "/usr/share/zoneinfo/${TIMEZONE}" /etc/localtime
echo "${TIMEZONE}" > /etc/timezone
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading
Loading