Deployer v8
After a long road through alpha, beta, and release candidates, Deployer v8 is finally out. This release modernizes the foundations of Deployer, introduces a new recipe format, and tightens up many of the rough edges that accumulated over the v7 cycle.
Deployer turned ten in 2024, and v8 is the first release that really takes advantage of modern PHP. The minimum supported version is now PHP 8.3, and the codebase has been refactored to use typed properties, named arguments, and the rest of the toolkit that was missing when the project started.
If you are upgrading an existing project, the full upgrade guide covers every breaking change in detail. The rest of this post focuses on the parts I think are most worth knowing about.
Named arguments for run()
The biggest day-to-day change is in how options are passed to run() and runLocally(). The old $options array is gone, replaced by named arguments.
// Before (v7):
run('deploy.sh', ['timeout' => 5, 'no_throw' => true]);
// After (v8):
run('deploy.sh', timeout: 5, nothrow: true);
A few parameters were renamed along the way to match the rest of the codebase:
no_throwis nownothrowreal_time_outputis nowforceOutputidle_timeoutis nowidleTimeout
There is also a new cwd parameter, so you no longer need to wrap a cd() around every command:
run('ls', cwd: '/var/www');
Multiple secrets per command
The single secret parameter has been replaced with secrets, an associative array. This makes it natural to interpolate several secret values into a single command without leaking them into logs.
// Before (v7):
run('echo %secret%', secret: getenv('MY_SECRET'));
// After (v8):
run('echo %my_secret%', secrets: [
'my_secret' => getenv('MY_SECRET'),
]);
Each placeholder is replaced before execution, but the substituted value is masked in any output Deployer prints or logs.
A safer quote() function
Every internal use of PHP's escapeshellarg() has been replaced with a new quote() helper. It uses ANSI-C $'...' quoting, which handles a much wider range of characters and locales correctly than escapeshellarg() does.
// Before (v7):
run('echo ' . escapeshellarg($arg));
// After (v8):
run('echo ' . quote($arg));
There is also a quote filter you can use directly inside template strings, which keeps recipes readable:
run('echo {{ message | quote }}');
Template escaping
While we were touching template handling, we added a way to output literal {{ without triggering config replacement. Just escape with a backslash:
run('echo \{{not_replaced}}');
// outputs: {{not_replaced}}
A small thing, but it makes life easier when generating shell scripts or config files that themselves use Mustache or Jinja style braces.
Httpie returns a response object
Httpie::send() used to return a string. In v8 it returns an HttpResponse object, so you can inspect status codes and headers, then call ->body() when you need the body as a string.
$response = Httpie::get('https://example.com/health')->send();
$response->status(); // 200
$response->header('Content-Type');
$response->body(); // string
Other notes in the same area:
Httpie::getJson()is deprecated. UsesendJson().Httpiemethods no longer clone the object. They mutate and return$this, which matches how most fluent builders behave in PHP.- The high-level
fetch()function now supportsput,patch, anddelete, alongsidegetandpost.
MAML recipes
This is the one I am most excited about. Deployer has supported YAML recipes for years, but the implementation always felt a bit fragile. Error reporting was approximate, line numbers were guessed via string matching, and the YAML parser threw away the metadata we needed to do better.
In v8, alongside PHP and YAML recipes, you can now write a deploy.maml file:
{
import: "recipe/common.php"
hosts: {
"deployer.org": {
remote_user: "deployer"
deploy_path: "~/deployer.org"
}
}
tasks: {
deploy: [
"deploy:prepare"
"deploy:publish"
]
build: [
{ runLocally: "npm run clear" }
{ runLocally: "npm run build" }
]
}
}
MAML is a small language built on top of JSON. It preserves source positions through the AST, ships with a real schema validator, and produces accurate error messages with the right line numbers and code snippets.
YAML recipes still work, and PHP recipes are completely unchanged. You can mix all three in the same project. If you want the longer story behind why MAML exists and how it integrates with the rest of Deployer, I wrote about it in MAML Recipes.
local_archive update strategy
Deployer has had a few update_code strategies (clone, archive, rsync). v8 adds one more: local_archive. Instead of fetching code on the remote host, it builds an archive from your local working copy and uploads it. This is useful in CI, when the artifact you want to deploy is already on the runner, or when you do not want to give the deploy host any access to your git server.
set('update_code_strategy', 'local_archive');
Other useful additions
A few smaller features that are worth mentioning:
composer_versionconfig: pin a specific Composer version to install on the remote host, for exampleset('composer_version', '2.7').Host::setShellPath(): customize the shell path per host, which makes life easier on systems where bash is not at the default location.- ACL improvements:
writable_acl_groupslets you grant write access to multiple groups, andwritable_acl_forceresets ACLs even when they look correct.
What was removed
A few things did not survive the cleanup:
- The self-update command is gone. As most installs come from Composer.
- Symfony 6 is no longer supported. Deployer v8 requires Symfony 7.4 or 8.0 components.
Try it
composer require --dev deployer/deployer:^8.0
Or, if you prefer the standalone binary, grab the latest phar from the download page.
Thanks to everyone who tested the alphas, filed issues, and sent pull requests over the past year. v8 would not be here without you.
