MAML Recipes
Deployer v8 introduces MAML-based recipes as a replacement for YAML, while PHP recipes remain unchanged. This shift raises an important question: why move from YAML to MAML? In this blog post, I will explain the rationale behind this decision.
{
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" }
]
"deploy:update_code": [
{
upload: {
src: "build/"
dest: "{{release_path}}"
}
}
]
}
}
YAML Recipes
To begin with, let’s examine how the current implementation for YAML recipes works. We rely on the symfony/yaml
package, which parses the YAML into a PHP object that is then used as the recipe.
Unfortunately, this process loses line-level information. As a result, when an error occurs and we need to indicate the exact line where it happened, the PHP code ends up relying on a rather unwieldy workaround function.
function find_line_number(string $source, string $string): int
{
$string = explode(PHP_EOL, $string)[0];
$before = strstr($source, $string, true);
if (false !== $before) {
return count(explode(PHP_EOL, $before));
}
return 1;
}
As you might expect, this approach is not particularly reliable and can lead to inconsistent or unexpected results. The overall YAML recipes implementation also leaves much to be desired and is far from ideal.
self::$recipeSource = file_get_contents($path, true);
$root = array_filter(Yaml::parse(self::$recipeSource), static function (string $key) {
return !str_starts_with($key, '.');
}, ARRAY_FILTER_USE_KEY);
foreach (array_keys($root) as $key) {
static::$key($root[$key]);
}
YAML itself also has its drawbacks. I won’t dwell on them, as plenty has already been written on the subject; most people have a good sense of both its strengths and its shortcomings.
To improve YAML recipes, we needed a parser that would not only return objects, but also preserve metadata and parse the input into an AST. However, after looking at the YAML specification, I realized how difficult that would be. The people who attempt to implement YAML parsers are true heroes.
MAML Recipes
I decided to start by designing a simple language specification based on JSON, while addressing many of the aspects I found problematic in it. I was fortunate to collaborate with a group of highly talented individuals who provided invaluable support.
At this point, the MAML specification, along with its implementations across various programming languages and editors, has gained broad adoption, allowing us to confidently start using MAML recipes in Deployer.
Special thanks to David Septimus for the excellent implementation of the MAML plugin for PhpStorm.
Deployer relies on the maml/maml package, which provides:
- 100% test coverage
- An AST-based parser and stringifier
- Built-in schema validation
- Support for JSON Schema generation
use Maml\Schema\S;
// Part of Deployer schema code:
$cd = S::object([
'cd' => S::string(),
]);
$run = S::object([
'run' => S::string(),
]);
$step = S::union(
$cd,
$run,
);
$schema = S::object([
'tasks' => S::optional(
S::map(
S::union(
S::arrayOf($step),
S::arrayOf(S::string()),
),
),
),
]);
Error messages are now reported accurately, including the correct line number. Syntax errors in MAML recipes are also highlighted, often with a small code snippet indicating exactly where the issue occurred.
As a result of switching to MAML, I was also able to fix several long-standing issues that existed in YAML recipes. Overall, the implementation has become significantly cleaner and more robust, and I’m much more confident in it now.
I’m intentionally not going into detail about the MAML language specification here, but if you’re familiar with JSON, MAML should feel quite intuitive. You can also visit the MAML website to learn more about the language.
YAML recipes remain fully supported in Deployer v8, so existing projects can continue to operate without any changes. At
the same time, for those who want to adopt MAML, an automatic migration tool is available: the
to-maml npm package. It allows you to convert existing YAML recipes into MAML
with minimal effort, making the transition straightforward and incremental.
MAML is designed to integrate seamlessly with existing Deployer workflows. Recipes are fully interoperable, meaning you can mix MAML and PHP without friction.
For example, you can import MAML recipes from a PHP recipe:
import('recipe.maml');
And likewise, you can import PHP recipes inside a MAML recipe using the same import key:
import: ["recipe/common.php"]
All of this makes the transition to MAML pretty painless. YAML recipes are not going anywhere, so you don’t have to rush and rewrite everything overnight.
But if you’re starting something new, I’d definitely recommend giving MAML a try. It’s simpler, more predictable, and a lot nicer to work with (at least from my perspective).
Freundliche Grüsse
