Skip to content

Deployer 7 + PHP 8.4 (v4) - #73

Open
mrrobot47 wants to merge 6 commits into
masterfrom
feat/php84-deployer-v7
Open

Deployer 7 + PHP 8.4 (v4)#73
mrrobot47 wants to merge 6 commits into
masterfrom
feat/php84-deployer-v7

Conversation

@mrrobot47

@mrrobot47 mrrobot47 commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

Upgrades the action's runtime from Deployer 6.9 to Deployer 7.5.12 with PHP 8.4 as updated base, and makes the migration invisible to consumer repos that use the stock pipeline: existing hosts.yml files and workflows keep working unchanged.

This is a major release (v4). The one hard breaking change is for repos that override .github/deploy/deploy.php (or ship an addon.php) written against the Deployer 6 API — those recipes fail to load on the new image and must be ported (checklist below).

What changed

composer.json / composer.lock

  • deployer/deployer ^6.8^7.0 (locks 7.5.12, the final 7.x).
  • deployer/recipes removed — Deployer 7 absorbed it; the rsync recipe now lives in deployer/deployer/contrib/rsync.php.
  • Platform pin 7.4.338.4.0.
  • Deployer 8 (Apr 2026) was evaluated and deliberately not targeted: it introduces a new recipe format (MAML) and is three months old; 7.5.x is the mature target for this migration.

deploy.php (built-in recipe)

  • v6 → v7 API: inventory()import(), per-host addSshOption() loop → global ssh_arguments, cleanup/successdeploy:cleanup/deploy:success, ssh_type dropped (v7 is native-ssh only), rsync recipe path updated.
  • Task list: v7 turned deploy:prepare into a group task that runs deploy:update_code (requires a git repository — this action deploys via rsync, so it throws) and duplicates lock/release/shared. Both task lists now run deploy:info + deploy:setup explicitly. Without this, every deploy fails on its first task.
  • Release-counter migration: v7 numbers releases from .dep/latest_release, a file v6 never wrote. On any existing site the first v7 deploy would abort with Release name "1" already exists. The recipe now seeds the counter from the highest numbered directory in releases/ when the file is absent. (Confirmed against a live server — see Testing.)
  • keep_releases pinned to 5 — v7's new default of 10 would silently double per-site disk usage.
  • $output initialized in opcache:reset / core_db:update (PHP 8 undefined-variable warnings on non-EasyEngine hosts).

main.sh

  • setup_hosts_file now rewrites the consumer's v6 flat hosts.yml (branch keys at top level, user:) into the v7 schema (hosts: wrapper, remote_user:) before Deployer imports it — v7's import() schema-rejects the flat format. Files already in v7 format pass through untouched. Every other hosts.yml read in main.sh (check_branch_in_hosts_file, setup_ssh_access, setup_wordpress_files, block_emails) uses the original flat file and is unaffected, so consumer repos change nothing.

Dockerfile

  • Base toolchain php7.4-*php8.4-* (ondrej PPA, resolved at image build time — nothing is installed at deploy time). php-json dropped (core since 8.0). DEFAULT_PHP_VERSION=8.4.
  • 8.4 rather than 8.5: wp-cli 2.12 and the WP toolchain are proven on 8.4, and it matches what consumer EasyEngine sites run today.

Breaking changes

Who Impact Action needed
Stock-pipeline consumers (no .github/deploy/) None Nothing — verified live, see Testing
Consumers with custom deploy.php / addon.php (Deployer 6 API) Recipe fails to load (inventory() undefined, deployer/recipes path gone) Port using the checklist below
Anything depending on the container's PHP 7.4 Container PHP is now 8.4 Validate scripts against 8.4

Release mechanics: publish the image as v4.0.0 and tag the action v4. Do not move any v3* tag — action.yml keeps pinning v3.4.1 until the follow-up release commit.

Custom deploy.php port checklist (v6 → v7)

  1. inventory( '/hosts.yml' )import( '/hosts.yml' ).
  2. Delete the addSshOption() host loop; use set( 'ssh_arguments', [ '-o UserKnownHostsFile=/dev/null', '-o StrictHostKeyChecking=no' ] );
  3. deployer/recipes/recipe/rsync.phpdeployer/deployer/contrib/rsync.php (both the local-vendor and COMPOSER_HOME fallback paths).
  4. Remove set( 'ssh_type', 'native' ).
  5. In task lists: replace deploy:prepare with deploy:info, deploy:setup; rename cleanupdeploy:cleanup; after( 'deploy', 'success' )after( 'deploy', 'deploy:success' ).
  6. Add the release_name fallback (copy from this repo's deploy.php) so the first v7 deploy on an existing site doesn't collide with v6-era releases.
  7. Consider set( 'keep_releases', 5 ) — the v7 default is 10.
  8. In host configs: {{hostname}} is the SSH host; use a custom key (e.g. site:) for the EasyEngine site name if they differ.

A complete worked example (Bedrock + custom build steps) is in rtCamp/test-action-deploy-wordpress@dc4c9a8 under .github/deploy/.

One-time note for existing sites

Releases created by v6 are absent from v7's .dep/releases_log, so deploy:cleanup will never prune them. After switching, delete old numbered directories under releases/ once by hand (keep current's target).

Testing

All flows were exercised against a live EasyEngine test site, plus local end-to-end rigs (fresh sshd container) for failure-path cases:

Scenario Evidence Result
Classic WP-files repo, stock pipeline, no override live run ✅ full chain incl. wp core update-db succeeding via ee shell
Classic repo + minor custom deploy.php override live run ✅ override picked up (marker task ran, keep_releases 3 applied)
Bedrock repo + full custom pipeline (ported recipe) live run ✅ composer + parallel npm builds + deploy in 92s
v6→v7 release-counter migration on a live site with pre-existing releases/1 live run ✅ deployed as release 2 instead of colliding
Same migration path, local rig with releases/1..4 and no .dep/latest_release local sshd e2e ✅ deployed as release 5
v6 flat hosts.yml (incl. generated file with stray keys) all runs ✅ transformed, schema-validates, aliases select via dep deploy <branch>
v6-API custom recipe on the new image (expected failure) local ❌ fails as documented → drives the v4 major
Second deploy idempotency (release N+1, current retarget, cleanup) local sshd e2e

Also verified in isolation: dep still discovers /deploy.php by walking up from the workspace; positional dep deploy <alias> host selection; global ssh_arguments reaching hosts (fresh host key accepted non-interactively); contrib rsync config keys unchanged; jq/shyaml/wp-cli/node toolchain intact on the new base.

Follow-ups (separate PRs)

  • action.yml: bump runs.image to the published v4 image at release time.
  • Refresh repo-root sample hosts.yml (currently empty — import() of an empty file crashes with a cryptic TypeError before setup_hosts_file replaces it), example/ workflow, and README for v7-era docs.
  • Evaluate Deployer 8 (MAML recipes) once it matures.

Deployer 7 absorbed the deployer/recipes package (the rsync recipe now lives in deployer/deployer's contrib/), so the separate dependency goes away. 7.5.12 is the final 7.x release; Deployer 8 (Apr 2026) exists but introduces a new recipe format and is intentionally out of scope.

composer platform is pinned to php 8.4.0 to match the new container runtime (see the Dockerfile commit in this series).

BREAKING CHANGE: recipes written against the Deployer 6 API no longer load. Consumers with a custom .github/deploy/deploy.php must port it — see UPGRADE notes in the v4 release.
Mechanical v6 → v7 changes:
- inventory('/hosts.yml') → import('/hosts.yml')
- per-host addSshOption() loop → global ssh_arguments
- deployer/recipes rsync path → deployer/deployer contrib/rsync.php
- 'cleanup' / 'success' → 'deploy:cleanup' / 'deploy:success'
- ssh_type 'native' removed (v7 only has native ssh)

Behavioral changes that needed more than a rename:
- deploy:prepare is a group task in v7 that runs deploy:update_code (fails without a git 'repository' — this action deploys via rsync) and duplicates lock/release/shared. Both task lists now run deploy:info + deploy:setup explicitly instead.
- release_name is seeded from the highest numbered directory in releases/ when .dep/latest_release is absent. Deployer 6 never wrote that file, so without the fallback the first v7 deploy to any existing site aborts with "Release name 1 already exists".
- keep_releases pinned to 5: v7 raised the default to 10, which would silently double disk usage per site.
- cachetool selection unchanged, but $output is initialized in opcache:reset / core_db:update so non-EasyEngine hosts don't emit undefined-variable warnings on PHP 8.

BREAKING CHANGE: task pipeline is Deployer 7-only; custom recipes and addon.php files using the v6 API will fail to load.
Deployer 7's import() validates hosts.yml against a strict schema: hosts must live under a top-level 'hosts:' key and the SSH user key is 'remote_user'. Every existing consumer repo uses the v6 flat format (branch names as top-level keys, 'user:'), which import() rejects outright.

setup_hosts_file now rewrites the consumer file into the v7 schema (via PyYAML, already shipped as a shyaml dependency) when writing /hosts.yml. Files that already have a top-level 'hosts:' key pass through untouched, so repos can migrate at their own pace — or never.

All other main.sh reads (check_branch_in_hosts_file, setup_ssh_access, setup_wordpress_files, block_emails) keep using the original flat file via shyaml and are unaffected.
php8.4-* packages from the ondrej PPA replace php7.4-*; php-json is dropped (core since PHP 8.0). DEFAULT_PHP_VERSION follows so maybe_run_php_build() restores the right CLI after a consumer's PHP_VERSION override.

PHP 8.4 rather than 8.5: wp-cli 2.12 and the WordPress toolchain are proven on 8.4, and 8.4 is what consumer sites (EasyEngine) run today.

BREAKING CHANGE: scripts relying on the container's PHP 7.4 behavior must be validated against 8.4.
@mrrobot47
mrrobot47 requested a review from Copilot July 24, 2026 11:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Upgrades the action runtime to Deployer 7 / PHP 8.4 while preserving compatibility with consumer repos’ existing hosts.yml and stock workflows.

Changes:

  • Transform legacy (v6-style) flat hosts.yml into Deployer 7’s strict hosts: schema before import.
  • Update the built-in deploy.php recipe for Deployer 7 (import API, rsync contrib path, task list adjustments, release counter seeding, keep_releases).
  • Update the container toolchain to PHP 8.4 packages and defaults.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 3 comments.

File Description
main.sh Adds an on-the-fly YAML transformation step to produce a Deployer 7–compatible /hosts.yml.
deploy.php Ports the built-in recipe to Deployer 7 APIs and behaviors, including release counter migration logic.
composer.json Bumps Deployer to ^7.0 and pins PHP platform to 8.4.0.
Dockerfile Switches installed PHP packages and default PHP version to 8.4.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread main.sh
Comment thread main.sh Outdated
Comment thread main.sh
…mment

The hosts transform imports PyYAML at runtime; it was only present transitively via shyaml. Install pyyaml explicitly so the deploy-critical hosts rewrite doesn't depend on shyaml's dependency chain.

Also correct the setup_hosts_file comment: a file already in the v7 schema is re-serialized (comments and key order are not preserved), not copied as-is.
The release_name closure trusted .dep/latest_release whenever it existed and only scanned releases/ when the file was absent. A deploy that dies after mkdir releases/N but before persisting the counter leaves the file stale at N-1, so the next deploy returns N, collides with the existing directory, and aborts with "Release name already exists".

Always compute both the counter and the highest numbered directory and take max() + 1, so a stale counter can never number below an existing release. This also subsumes the v6-migration case (absent file → counter 0 → falls back to the directory scan).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants