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
12 changes: 9 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,17 @@ jobs:
- name: Download embedded PHP files
shell: bash
run: |
php_version=$(grep -m1 '^PHP_VERSION' Makefile | cut -d' ' -f3)
# Line endings may be converted on checkout.
var() { tr -d '\r' < Makefile | sed -n "s/^$1 = //p"; }
php_version=$(var PHP_VERSION)
asset="php-$php_version-windows-amd64.exe"
php_sha256=$(tr -d '\r' < internal/legacy/php_sha256sums.txt | awk -v a="$asset" '$2 == a { print $1 }')
mkdir -p internal/legacy/archives
curl -fSL "https://github.com/upsun/cli-php-builds/releases/download/php-$php_version/php-$php_version-windows-amd64.exe" \
curl -fSL "https://github.com/upsun/cli-php-builds/releases/download/php-$php_version-$(var PHP_BUILD)/$asset" \
-o internal/legacy/archives/php_windows.exe
curl -fSL https://curl.se/ca/cacert.pem -o internal/legacy/archives/cacert.pem
curl -fSL "https://curl.se/ca/cacert-$(var CACERT_DATE).pem" -o internal/legacy/archives/cacert.pem
echo "$php_sha256 internal/legacy/archives/php_windows.exe" | sha256sum -c -
echo "$(var CACERT_SHA256) internal/legacy/archives/cacert.pem" | sha256sum -c -
# Only needed so that the package builds.
touch internal/legacy/archives/platform.phar
touch internal/legacy/archives/commands-upsun.json internal/legacy/archives/commands-platformsh.json
Expand Down
6 changes: 1 addition & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,6 @@ jobs:
- name: Install legacy dependencies
run: cd legacy && composer install --no-interaction

- name: Get PHP version from Makefile
id: php-version
run: echo "version=$(grep '^PHP_VERSION' Makefile | cut -d= -f2 | tr -d ' ')" >> $GITHUB_OUTPUT

- name: Get tool versions from Makefile
id: tool-versions
run: |
Expand All @@ -87,7 +83,7 @@ jobs:
path: |
internal/legacy/archives/php_*
internal/legacy/archives/php-version
key: php-binaries-v2-${{ steps.php-version.outputs.version }}
key: php-binaries-v3-${{ hashFiles('internal/legacy/php_sha256sums.txt') }}

- name: Download all PHP binaries
if: steps.php-cache.outputs.cache-hit != 'true'
Expand Down
94 changes: 94 additions & 0 deletions .github/workflows/update-php.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Opens or updates a pull request when there is a new build of the embedded PHP
# (from upsun/cli-php-builds) or a new CA bundle. See scripts/update-php.sh.
name: Update PHP

on:
schedule:
- cron: "30 6 * * *"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: update-php

env:
BRANCH: automated/update-php

jobs:
update:
runs-on: ubuntu-latest
# Holds the credentials of the GitHub App that opens the pull request.
environment: php-updates

steps:
- name: Check out repository code
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Check for updates
id: check
env:
GH_TOKEN: ${{ github.token }}
run: |
scripts/update-php.sh "$RUNNER_TEMP/title" "$RUNNER_TEMP/body"
if [[ -f "$RUNNER_TEMP/title" ]]; then
echo "title=$(cat "$RUNNER_TEMP/title")" >> "$GITHUB_OUTPUT"
fi

# Skips pushing again (and re-running CI) if the branch already has this update.
- name: Check the existing branch
id: existing
if: steps.check.outputs.title
env:
TITLE: ${{ steps.check.outputs.title }}
run: |
if git fetch --depth=1 origin "$BRANCH" 2>/dev/null \
&& [[ "$(git log -1 --format=%s FETCH_HEAD)" == "$TITLE" ]]; then
echo "up_to_date=true" >> "$GITHUB_OUTPUT"
fi

- name: Verify the downloads
if: steps.check.outputs.title && !steps.existing.outputs.up_to_date
run: |
make download-php
expected=$(sed -n 's/^PHP_VERSION = //p' Makefile)
actual=$(internal/legacy/archives/php_linux_amd64 -r 'echo PHP_VERSION;')
if [[ "$actual" != "$expected" ]]; then
echo "::error::Expected PHP $expected, got $actual"
exit 1
fi

- name: Generate GitHub App token
id: app-token
if: steps.check.outputs.title && !steps.existing.outputs.up_to_date
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write

# Pushes with the App token, so that CI runs on the pull request.
- name: Push and open or update the pull request
if: steps.check.outputs.title && !steps.existing.outputs.up_to_date
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
TITLE: ${{ steps.check.outputs.title }}
run: |
user_id=$(gh api "/users/${APP_SLUG}%5Bbot%5D" --jq .id)
git config user.name "${APP_SLUG}[bot]"
git config user.email "${user_id}+${APP_SLUG}[bot]@users.noreply.github.com"
git switch -c "$BRANCH"
printf '%s\n\n' "$TITLE" | cat - "$RUNNER_TEMP/body" | git commit -a -F -
git push --force "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:refs/heads/$BRANCH"

pr=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number')
if [[ -n "$pr" ]]; then
gh pr edit "$pr" --title "$TITLE" --body-file "$RUNNER_TEMP/body"
else
gh pr create --base "$GITHUB_REF_NAME" --head "$BRANCH" --title "$TITLE" --body-file "$RUNNER_TEMP/body"
fi
11 changes: 5 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,12 @@ The CLI operates as a wrapper around a legacy PHP CLI:
- GoReleaser builds multiple variants (upsun, platform, vendor-specific)

**PHP Binary Handling**:
- PHP binaries are downloaded from [upsun/cli-php-builds](https://github.com/upsun/cli-php-builds) releases
- PHP binaries are downloaded from [upsun/cli-php-builds](https://github.com/upsun/cli-php-builds) releases, tagged `php-<PHP_VERSION>-<PHP_BUILD>`. They are rebuilt (with a new build number) when PHP or a library has a new release
- Their SHA-256 checksums are pinned in `internal/legacy/php_sha256sums.txt` (the release's `SHA256SUMS.txt`), and verified after download
- All platforms use static binaries built with [static-php-cli](https://github.com/crazywhalecc/static-php-cli)
- Supported platforms: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64
- Extensions included: curl, filter, openssl, pcntl (Unix), phar, posix (Unix), zlib
- Windows requires `cacert.pem` for OpenSSL (embedded separately)
- Windows requires `cacert.pem` for OpenSSL (embedded separately), pinned by date and checksum (`CACERT_DATE`, `CACERT_SHA256`)

**Downloading PHP Binaries**:
```bash
Expand All @@ -144,10 +145,8 @@ make download-php
```

**Upgrading PHP Version**:
1. Trigger the build workflow at [upsun/cli-php-builds](https://github.com/upsun/cli-php-builds/actions) with the new PHP version
2. Update `PHP_VERSION` in the Makefile
3. Run `make php` (or `make download-php`) to download the new binaries
4. Test and release
- The `update-php` workflow runs daily: it finds the latest cli-php-builds release for the current PHP minor version, and the latest CA bundle, and opens or updates a pull request that updates `PHP_VERSION`, `PHP_BUILD`, the checksums and the CA bundle (with `scripts/update-php.sh`). The pull request lists the changed source archives (from the releases' `sources.json`)
- For a new PHP minor version: trigger the build workflow at [upsun/cli-php-builds](https://github.com/upsun/cli-php-builds/actions) with the new version, then set `PHP_VERSION` to it and `PHP_BUILD` to `0` (builds are numbered from 1), run `scripts/update-php.sh <title-file> <body-file>`, and `make download-php` to check the downloads

## Development Notes

Expand Down
30 changes: 26 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
# The PHP version, and the cli-php-builds build number (rebuilt when a library is updated).
# Checksums of the binaries are pinned in PHP_SHA256SUMS. Updated by the update-php workflow.
PHP_VERSION = 8.5.11
PHP_BUILD = 1

# The CA bundle embedded for PHP on Windows, from https://curl.se/docs/caextract.html.
CACERT_DATE = 2026-09-25
CACERT_SHA256 = a41b5d356aea97a529fe27e0f7316d2f9d946d75927476cf9cf1b90637d00505

.DEFAULT_GOAL := help

Expand Down Expand Up @@ -29,7 +36,19 @@ REPOGEN_VERSION=v1.0.4
# PHP binaries are downloaded from cli-php-builds releases.
# See: https://github.com/upsun/cli-php-builds
PHP_BUILDS_REPO = upsun/cli-php-builds
PHP_RELEASE_URL = https://github.com/$(PHP_BUILDS_REPO)/releases/download/php-$(PHP_VERSION)
PHP_RELEASE_TAG = php-$(PHP_VERSION)-$(PHP_BUILD)
PHP_RELEASE_URL = https://github.com/$(PHP_BUILDS_REPO)/releases/download/$(PHP_RELEASE_TAG)
PHP_SHA256SUMS = internal/legacy/php_sha256sums.txt

# Verifies a downloaded file against a checksum. Usage: $(call verify-sha256,file,checksum)
SHA256SUM := $(shell command -v sha256sum 2>/dev/null || echo shasum -a 256)
define verify-sha256
@test -n "$(2)" || { echo "No checksum for $(1)" >&2; exit 1; }
echo "$(2) $(1)" | $(SHA256SUM) -c -
endef

# The pinned checksum of a PHP release asset. Usage: $(call php-sha256,asset-name)
php-sha256 = $(shell awk '$$2 == "$(1)" { print $$1 }' $(PHP_SHA256SUMS))

# Files embedded in the Go binary.
ARCHIVES = internal/legacy/archives
Expand Down Expand Up @@ -113,25 +132,28 @@ $(ARCHIVES)/commands-vendor.json: $(ARCHIVES)/platform.phar internal/config/embe
legacy/vendor/autoload.php:
cd legacy && composer install --no-interaction

# Records PHP_VERSION, so that the PHP binaries are downloaded again when it changes.
# Records the PHP release tag, so that the PHP binaries are downloaded again when it changes.
$(ARCHIVES)/php-version: FORCE
@mkdir -p $(ARCHIVES)
@echo $(PHP_VERSION) | cmp -s - $@ || echo $(PHP_VERSION) > $@
@echo $(PHP_RELEASE_TAG) | cmp -s - $@ || echo $(PHP_RELEASE_TAG) > $@

$(PHP_UNIX_BINARIES): $(ARCHIVES)/php_%: $(ARCHIVES)/php-version
curl -fSL "$(PHP_RELEASE_URL)/php-$(PHP_VERSION)-$(subst _,-,$*)" -o $@
$(call verify-sha256,$@,$(call php-sha256,php-$(PHP_VERSION)-$(subst _,-,$*)))
chmod +x $@

$(ARCHIVES)/php_windows.exe: $(ARCHIVES)/php-version
curl -fSL "$(PHP_RELEASE_URL)/php-$(PHP_VERSION)-windows-amd64.exe" -o $@
$(call verify-sha256,$@,$(call php-sha256,php-$(PHP_VERSION)-windows-amd64.exe))

.PHONY: $(ARCHIVES)/php_windows_amd64
$(ARCHIVES)/php_windows_amd64: $(ARCHIVES)/php_windows.exe $(ARCHIVES)/cacert.pem

.PHONY: $(ARCHIVES)/cacert.pem
$(ARCHIVES)/cacert.pem:
mkdir -p $(ARCHIVES)
curl -fSL https://curl.se/ca/cacert.pem -o $@
curl -fSL https://curl.se/ca/cacert-$(CACERT_DATE).pem -o $@
$(call verify-sha256,$@,$(CACERT_SHA256))

.PHONY: FORCE
FORCE:
Expand Down
6 changes: 6 additions & 0 deletions internal/legacy/php_sha256sums.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
26af30fdde5cd3ea5032415ce5aea40e5a0c69d52c4cc0cc8d5c669592fd9c27 php-8.5.11-darwin-amd64
8899461b8796b03f807bc71661650790aaf2a98a4ac5d59fe1520cda5f4212d2 php-8.5.11-darwin-arm64
a3806545776bbac282211346700f3467a7ccde8b44771095d61a11f265978a01 php-8.5.11-linux-amd64
e0968165a094ed374e3d134f6a048e667dba92b5df40b1e5962a48b47ee9b63b php-8.5.11-linux-arm64
d034a9a3a1fc6505723d5c879f82c414406615ea64c172afbb2b9de4ce098441 php-8.5.11-windows-amd64.exe
e6659656c6df2be75caef66f403ecf10b7933b65d87a81edd904db2b880fa55f sources.json
114 changes: 114 additions & 0 deletions scripts/update-php.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env bash
# Updates the pinned PHP build (in the Makefile and the checksums file) to the latest cli-php-builds release for the
# current PHP minor version, and the pinned CA bundle to the latest from curl.se.
# Writes a pull request title and body to the files given as arguments, if anything changed.
# Requires: gh (authenticated), jq, curl, sha256sum.
set -euo pipefail

title_file=${1:?Usage: $0 TITLE_FILE BODY_FILE}
body_file=${2:?Usage: $0 TITLE_FILE BODY_FILE}

repo=upsun/cli-php-builds
sums_file=internal/legacy/php_sha256sums.txt

makefile_var() {
sed -n "s/^$1 = //p" Makefile
}

set_makefile_var() {
sed -i.bak "s/^$1 = .*/$1 = $2/" Makefile && rm Makefile.bak
}

tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

php_version=$(makefile_var PHP_VERSION)
php_build=$(makefile_var PHP_BUILD)
cacert_date=$(makefile_var CACERT_DATE)
# Builds are numbered from 1, so PHP_BUILD = 0 (for a new PHP version) always updates.
old_tag="php-$php_version-$php_build"
minor=$(cut -d. -f1,2 <<< "$php_version")

new_tag=$(gh release list --repo "$repo" --exclude-drafts --exclude-pre-releases --limit 100 \
--json tagName --jq '.[].tagName' \
| grep -E "^php-${minor//./\\.}\.[0-9]+-[0-9]+$" | sort -V | tail -n1 || true)
if [[ -z "$new_tag" ]]; then
echo "No release found for PHP $minor" >&2
exit 1
fi

new_cacert_date=$(curl -fsSL https://curl.se/docs/caextract.html \
| grep -oE 'cacert-[0-9]{4}-[0-9]{2}-[0-9]{2}\.pem' | sed 's/^cacert-//; s/\.pem$//' | sort | tail -n1)
if [[ -z "$new_cacert_date" ]]; then
echo "No CA bundle found" >&2
exit 1
fi

titles=()
body=""

if [[ "$new_tag" != "$old_tag" ]]; then
Comment thread
upsun-dispatch[bot] marked this conversation as resolved.
if [[ "$(sort -V <<< "$old_tag"$'\n'"$new_tag" | tail -n1)" != "$new_tag" ]]; then
echo "The latest release $new_tag is older than $old_tag" >&2
exit 1
fi
new_version=$(sed -E 's/^php-(.*)-[0-9]+$/\1/' <<< "$new_tag")
new_build=${new_tag##*-}

gh release download "$new_tag" --repo "$repo" --dir "$tmp/new" --pattern SHA256SUMS.txt --pattern sources.json
if ! gh release download "$old_tag" --repo "$repo" --dir "$tmp/old" --pattern sources.json; then
mkdir -p "$tmp/old" && echo '{}' > "$tmp/old/sources.json"
fi

cp "$tmp/new/SHA256SUMS.txt" "$sums_file"
set_makefile_var PHP_VERSION "$new_version"
set_makefile_var PHP_BUILD "$new_build"

if [[ "$new_version" == "$php_version" ]]; then
titles+=("rebuild PHP $new_version (build $new_build)")
else
titles+=("upgrade PHP to $new_version (build $new_build)")
fi

# Lists changed source archives, one row per change, with the platforms it applies to.
changes=$(jq -rn --slurpfile old "$tmp/old/sources.json" --slurpfile new "$tmp/new/sources.json" '
$old[0] as $o | $new[0] as $n
| [($o + $n | keys[]) as $p | (($o[$p] // {}) + ($n[$p] // {}) | keys[]) as $lib
| {p: $p, lib: $lib, old: ($o[$p][$lib] // "-"), new: ($n[$p][$lib] // "-")}
| select(.old != .new)]
| group_by([.lib, .old, .new])
| map("| \(.[0].lib) | \(.[0].old) | \(.[0].new) | \(map(.p) | join(", ")) |")
| .[]')
body+="Updates the embedded PHP binaries from [$old_tag](https://github.com/$repo/releases/tag/$old_tag)"
body+=" to [$new_tag](https://github.com/$repo/releases/tag/$new_tag)."$'\n\n'
if [[ -n "$changes" ]]; then
body+="| Source | Old | New | Platforms |"$'\n'"|---|---|---|---|"$'\n'"$changes"$'\n\n'
else
body+="No source archives changed."$'\n\n'
fi
fi

if [[ "$new_cacert_date" > "$cacert_date" ]]; then
file="cacert-$new_cacert_date.pem"
curl -fsSL "https://curl.se/ca/$file" -o "$tmp/$file"
curl -fsSL "https://curl.se/ca/$file.sha256" -o "$tmp/$file.sha256"
(cd "$tmp" && sha256sum -c "$file.sha256")
set_makefile_var CACERT_DATE "$new_cacert_date"
set_makefile_var CACERT_SHA256 "$(cut -d' ' -f1 "$tmp/$file.sha256")"
titles+=("update the CA bundle to $new_cacert_date")
body+="Updates the CA bundle (embedded for PHP on Windows) from $cacert_date to $new_cacert_date."
body+=" See: https://curl.se/docs/caextract.html"$'\n\n'
fi

if [[ ${#titles[@]} -eq 0 ]]; then
echo "Up to date: $old_tag, CA bundle $cacert_date"
exit 0
fi

title="build: ${titles[0]}"
if [[ ${#titles[@]} -gt 1 ]]; then
title+=" and ${titles[1]}"
fi
echo "$title" > "$title_file"
printf '%s' "$body" > "$body_file"
echo "$title"
Loading