Skip to content

Repository files navigation

RollDev

RollDev is a pure-Bash CLI that orchestrates Docker development environments for PHP frameworks and CMS platforms (Magento 1/2, Laravel, Symfony, TYPO3, Shopware, WordPress, Akeneo, Vue.js, plain PHP). It contains no application code: every command assembles a docker compose invocation from layered YAML fragments, or runs a command inside one of the resulting containers. A single shared stack (Traefik, dnsmasq, Mailpit, an SSH tunnel) fronts any number of per-project environments, each reachable over HTTPS at https://app.<project>.test/ with a locally trusted certificate.

macOS and Linux are both first-class, fully supported hosts, and Windows is supported through WSL2. Every change has to work on macOS and Linux — the two differ in bash version, sed/ping flag semantics, file-sync strategy and SSH agent handling, and RollDev runs on developer machines and on unattended Linux hosts alike.

roll db dump is broken against the MariaDB 11.x images that are in active use. Recorded in Known issues & improvement points as H2.

Contents

Release history is in CHANGELOG.md.

Stack

ConcernChoiceNotes
Host platformsmacOS and Linux, both required; Windows via WSL2Enforced at utils/config.sh:353-369, which fails on any other $OSTYPE. See Supported platforms
LanguageBash, targeting 3.2macOS ships bash 3.2.57; no associative arrays anywhere, parallel indexed arrays instead (utils/config.sh:9-15, utils/registry.sh:9-14)
Orchestrationdocker compose≥ 2.2.3Version gate at bin/roll:26-30; v1 (docker-compose) is not supported
Container imagesghcr.io/epartment/roll/*Built in a separate repository (github.com/epartment/images); overridable via ROLL_IMAGE_REPOSITORY
Reverse proxyTraefik (traefik:latest)File + Docker providers, HTTP→HTTPS redirect (config/traefik/traefik.yml)
DNSdnsmasq via ghcr.io/epartment/roll/dnsmasqaddress=/.test/127.0.0.1, upstream Cloudflare (docker/docker-compose.yml:20-53)
Mail catcheraxllent/mailpit:latestStill named mailhog as a service and hostname (docker/docker-compose.yml:55-63)
DB tunnelpanubo/sshd:latest on 127.0.0.1:2222TCP forwarding only, key at ~/.roll/tunnel/ssh_key
macOS file syncMutagen ≥ 0.11.8Only for magento1, magento2, shopware — see Environments
DocumentationSphinx + MyST, published to GitHub Pagesdocs/, built by .github/workflows/pages.yml
DistributionHomebrew tap epartment/roll/rollRelease flow in .github/workflows/tag-release.ymlpush-release-to-brew.yml
LintShellCheck.github/workflows/shellcheck.yml; the only CI test

There is no build step, no dependency manifest, and no unit-test suite. version holds the version string (0.7.2) and is rewritten by the release workflow.

Getting started

Supported platforms

RollDev must work on both macOS and Linux. That is a hard requirement, not a best-effort goal: the tool is installed on developer machines (predominantly macOS) and on Linux hosts, including unattended use where no one is present to work around a platform-specific failure (see FEATURE-REQUESTS.md, whose entire scope is unattended Linux operation). A change that works on only one of the two is not finished.

loadRollConfig enforces this at runtime: utils/config.sh:353-369 maps $OSTYPE to ROLL_ENV_SUBT and fails outright on anything else with Unsupported OSTYPE '<value>'.

HostROLL_ENV_SUBTStatus
macOS (Intel and Apple Silicon)darwinFully supported
LinuxlinuxFully supported
Windows via WSL2wslSupported, but no *.wsl.yml fragment exists — see finding M12
Anything elseRejected by utils/config.sh:366

Windows is never a host in its own right; roll runs inside the WSL2 Linux distribution. Keep the code on the WSL filesystem (~/code/project), not under /mnt/c, or file access is very slow.

Everything below is a real behavioural difference between the two required platforms, not a cosmetic one. Anything touching these areas needs testing on both:

AreamacOS (darwin)Linux (linux)Where
Bash version3.2.57 (system bash)usually 5.xWhy the Bash 3.2 constraint exists; use bash 3.2 compatible syntax only
File syncMutagen session, named volume for the web rootdirect bind mount, no Mutagenenvironments/<type>/<type>.darwin.yml, commands/sync.cmd:33-35 (fatals off darwin)
sed -iBSD — requires a backup suffixGNU — suffix optionalAlways use sed_inplace (utils/core.sh:167)
Network probeping (unreliable)ping (unreliable)isOnline probes https://ghcr.io/v2/ with curl -m 3 instead, avoiding platform flag differences and ICMP blockage
SSH agent socketfixed host path /run/host-services/ssh-auth.sock${SSH_AUTH_SOCK} from the host, and a socat proxy unless UID is 1000environments/includes/php-fpm.darwin.yml vs .linux.yml, utils/config.sh:445-447
.test DNSautomatic via /etc/resolver/testmanual — roll install only prints a warningcommands/install.cmd:56-66
Root CA trustsecurity add-trusted-certFedora/CentOS and Debian/Ubuntu paths, with a warning naming the CA file on other distroscommands/install.cmd:29-58
tunnel key permissionsuntouchedchown root:root on ssh_key.pub, because bind mounts are nativecommands/install.cmd:82-84
Xdebug hosthost.docker.internalcontainer gateway address, looked up at runtimecommands/debug.cmd:13-21
mapfileabsent (bash 3.2)presentcommands/status.cmd:20-28 shows the required fallback
CI coveragemacos-latest: ShellCheck + Docker-free smokeubuntu-latest: ShellCheck + Docker-free smoke.github/workflows/shellcheck.yml matrix

Prerequisites: Docker (Desktop on macOS/Windows, Engine on Linux) with docker compose ≥ 2.2.3, Homebrew, and gum ≥ 0.14.0, which backs every interactive prompt. The Homebrew formula declares gum as a dependency, so a brew install pulls it in; on a source checkout install it yourself (brew install gum, Charm's apt repository, dnf/pacman, or a release binary — roll install prints the list and warns rather than failing). gum is needed only for prompts: every one of them is also reachable by flag, environment variable or positional argument, so scripted and CI use works without it. On macOS, Mutagen is installed automatically on first roll sync if missing.

Install and start the shared services:

brew install epartment/roll/roll
roll svc up

The first roll svc up triggers roll install (via assertRollDevInstall, utils/install.sh:43), which generates a local root CA under ~/.roll/ssl/rootca, adds it to the system trust store, writes /etc/resolver/test on macOS, generates the tunnel SSH keypair, and appends a tunnel.roll.test block to /etc/ssh/ssh_config. Several of those steps need sudo and will prompt.

Initialise a project (run from the project root — the directory that will contain .env.roll):

roll env-init myproject magento2
roll env up

The project is then served at https://app.myproject.test/. Run RollDev from source instead of the Homebrew copy with ./bin/roll <command>.

Running the CLI from source

bin/roll resolves its own real path through one level of symlink (bin/roll:6-14), which is what makes the Homebrew symlink install work. Note the consequence: a project brought up by the installed binary loads its YAML fragments from the Homebrew cellar, not from a checkout. Mixing the two on one environment can produce a fragment set that does not match either copy.

Architecture

Dispatch

bin/roll is the single entry point. It sources its libraries in a fixed order — utils/core.sh, utils/interact.sh, utils/config.sh, utils/images.sh, utils/registry.sh, utils/env.sh, utils/backup.sh — verifies Docker and the compose version, then resolves the first argument through the command registry and sources the matching .cmd file (bin/roll:98). Sourcing rather than executing is deliberate: every command runs in the same shell and can use all globals and helper functions without re-deriving them.

Two consequences follow from set -e plus trap ... ERR at bin/roll:2-3 applying to sourced command bodies:

  • A command that lets any statement return non-zero terminates the whole CLI. Commands that shell out to Docker therefore clear the trap with trap '' ERR before doing so (for example commands/env.cmd:16).
  • Arithmetic idioms that return the arithmetic result as an exit status — ((i++)) evaluating to 0 — kill the process. Use i=$((i + 1)); the codebase does this consistently.

Argument handling splits into two modes. Commands listed in ROLL_CMD_ANYARGS (bin/roll:43) pass every flag through untouched so it reaches docker compose or the in-container command; all others collect positional arguments in ROLL_PARAMS and reject unknown flags.

Command registry

Commands are discovered, never hardcoded. A command is a pair of files: <name>.cmd (the script) and <name>.help (usage text, itself a sourced Bash file that sets ROLL_USAGE). initializeRegistry (utils/registry.sh:159) scans a set of directories with numeric priorities, lower winning, so a user or a project can override a built-in command by shadowing its name:

PriorityDirectoryPurpose
1${ROLL_ENV_PATH}/.roll/commandsProject-local commands
1~/.roll/commands/<env-type>, ~/.roll/reclu/<env-type>User commands scoped to one environment type
2${ROLL_DIR}/commands/<env-type>Built-in commands available only inside that environment type
2~/.roll/commandsUser commands
3~/.roll/recluThird-party command packs
4${ROLL_DIR}/commandsBuilt-ins

commands/magento2/ and commands/wordpress/ use the env-type mechanism: roll magento, roll magepack, roll fixowns, roll mydumper and friends simply do not exist outside a magento2 project, and roll wp only inside wordpress.

Configuration

A project is identified by an .env.roll file. locateEnvPath (utils/env.sh:4) walks up from pwd until it finds one defining both ROLL_ENV_NAME and ROLL_ENV_TYPE, and resolves the file if it is a symlink — that is how a sub-stack points at a parent stack.

utils/config.sh holds the authoritative schema. initConfigSchema (utils/config.sh:58) declares every recognised variable as type:constraint, where the constraint is required, optional, or a literal default value. Loading proceeds in this order (loadRollConfig, utils/config.sh:311):

  1. ~/.roll/.env.roll, then legacy ~/.roll/.env — global settings.
  2. ${ROLL_ENV_PATH}/.env.roll — project settings, overriding global.
  3. OS detection sets ROLL_ENV_SUBT to darwin, linux, or wsl.
  4. assertValidEnvType checks that environments/<type>/<type>.base.yml exists.
  5. applyEnvTypeDefaults fills in what the environment type needs — the magento2 service toggles, the non-local base services, the DB distribution version — but never overwrites a key the project set itself.
  6. setConfigDefault fills in every schema key that still has no value and carries a literal default.
  7. applyVersionPinFallbacks warns about every enabled service with no version pin and falls back to the version the project was already running. "No pin" is judged on the keys step 2 read out of the project's own .env.roll, not on the resolved value — a version coming from global config resolves but pins nothing, since it is absent from the repository a colleague clones.
  8. postProcessConfig derives image variants (ROLL_SVC_PHP_VARIANT, ROLL_SVC_PHP_NODE), the Xdebug tag, and the nginx template name.

The order matters. Env-type defaults have to run before the schema literals, or every ${VAR:-default} in step 5 is unreachable — the literal has already given the key a value, and :- only substitutes when unset or empty. Derivations that depend on final input values run last.

Every value is exported, so it reaches docker compose both through --env-file .env.roll and through the process environment. Where the two disagree, the exported value wins, so the ${...:-fallback} defaults inside the YAML fragments are not the effective defaults; the service version fallbacks have been removed from the fragments for that reason.

How a compose invocation is assembled

commands/env.cmd is the core of the system. It reads the service toggles and, for each enabled one, calls appendEnvPartialIfExists (utils/env.sh:90), which searches a fixed precedence chain and appends -f <file> for every match found, so later files layer on top of earlier ones:

environments/includes/<name>.base.yml
environments/includes/<name>.<subtype>.yml # subtype = darwin | linux | wsl
environments/<envtype>/<name>.base.yml
environments/<envtype>/<name>.<subtype>.yml
~/.roll/environments/… # same four, user overrides

Two further project-level overlays are appended last: ${ROLL_ENV_PATH}/.roll/roll-env.yml and ${ROLL_ENV_PATH}/.roll/roll-env.<subtype>.yml (commands/env.cmd:173-181). The final command is:

docker compose --env-file .env.roll --project-directory <project> -p "$ROLL_ENV_NAME" -f … <params>

ROLL_ENV_NAME is therefore the Compose project name, which prefixes every container and named volume. Renaming an environment silently orphans its volumes and brings up an empty database.

env up and env down also connect and disconnect the peered global services — traefik, tunnel, mailhog (utils/core.sh:5) — into the project's network with docker network connect, because they live in the roll Compose project and would otherwise be unreachable. Project networks are labelled dev.roll.environment.name, which is how roll status and roll svc up enumerate them.

On macOS, env.cmd also drives the Mutagen session around the lifecycle: start on up, resume if paused and the php-fpm container id is unchanged, pause on stop, terminate on down (commands/env.cmd:239-282).

Container exec

Every "run X in the container" command is a thin wrapper that ends in roll env exec -u www-data <container> …, defaulting to php-fpm and overridable per project via ROLL_ENV_SHELL_CONTAINER:

CommandUserTTYNotes
roll shell, roll bashwww-datayesRuns ROLL_ENV_SHELL_COMMAND (default bash)
roll cli <cmd>www-datayesArbitrary command
roll clinotty <cmd>www-datano (-T)For scripted/piped use
roll cliq <cmd>www-datanoAs clinotty, all output discarded
roll root, roll rootnotty, roll rootshellrootper nameSame shape without -u www-data
roll debug <cmd>www-datayesTargets php-debug, injects XDEBUG_REMOTE_HOST
roll composer, roll node, roll npm, roll magerunwww-datayesNamed binary in the container
roll magento <cmd>www-datayesmagento2 environments only

Global services

roll svc orchestrates the shared stack: Compose project name is always roll, project directory ~/.roll, network roll. Traefik terminates TLS for *.test using certificates from ~/.roll/ssl/certs; svc up regenerates ~/.roll/etc/traefik/dynamic.yml from whatever certificates are present, and signs one for ROLL_SERVICE_DOMAIN (default roll.test) if absent (commands/svc.cmd:54-84). Portainer and a startpage are optional extra fragments.

Module boundaries & coupling

RollDev is one Bash package rather than a set of deployable modules, so "component" here means a directory-level unit with its own responsibility. The verdicts below are about whether that responsibility is still describable.

ComponentSizeStated responsibilityVerdict
bin/roll98 linesEntry point, dispatch, argument parsingCoherent
utils/core.sh184 linesMessaging helpers, array/version utilities, network peeringCoherent — the three box-drawing functions are one-line wrappers around a shared box helper
utils/config.sh946 linesConfig schema, loading, validation, post-processing, .env.roll writesCoherent; sole owner of configuration defaults
utils/images.sh375 linesService catalog (version key, toggle and image per service) and image-tag discovery from the registryCoherent
utils/registry.sh461 linesCommand discovery and priority resolutionOversized for what it delivers — ~200 lines serve roll registry's reporting subcommands; the metadata layer they report on is a stub (M6)
utils/env.sh108 linesEnv path location, partial precedence, env-type validationCoherent
utils/install.sh63 linesHost install assertion, SSH configCoherent
commands/env.cmd286 linesAssemble and run the compose invocationCoherent — the one place that must know everything
commands/backup.cmd, restore.cmd, restore-full.cmd, duplicate.cmd3 929 lines — 57 % of all top-level command codeArchive, restore, and clone an environmentOversized and redundantrestore.cmd and restore-full.cmd are ~84 % identical (M4)
commands/magento2-init.cmd784 linesScaffold a Magento 2 projectOversized — a project generator living in a command file
commands/*.cmd (wrappers)5–25 lines eachOne roll env exec invocationCoherent
commands/magento2/, commands/wordpress/344 lines / 11 linesEnv-type-specific commandsCoherent; commands/magento1/ and both usage.help-only directories contain no commands
environments/includes/20 fragmentsOne service eachCoherent
environments/<type>/11 typesPer-type overrides and seedsCoherent — each type has an init.env (or documents why it doesn't), and local is properly documented
docker/, config/290 linesShared-services compose and Traefik/OpenSSL configCoherent
docs/3 666 lines / 31 pagesUser documentationCoherent, drifting from the code (M5)

Real coupling map

The declared structure — bin/roll sources four libraries, then sources one command — is the easy part. These are the couplings that are not visible from it:

ABMechanismWhat breaks if separated
commands/backup.cmdcommands/restore.cmd, commands/restore-full.cmdUndocumented on-disk contract: the staging directory <cwd>/.roll/backups, the archive layout, and the metadata JSON. Each file re-derives the path independently (documented in a comment at commands/backup.cmd:22-35)Changing the layout in one file silently breaks the others; restore-full.cmd never loads the env config at all, so it resolves paths differently
commands/status.cmd:6docker/docker-compose.ymlThe shared network name is recovered by grep -A3 'networks:' … | tail -n1 | sed, i.e. by text-parsing YAMLReordering keys in docker-compose.yml breaks roll status's "is RollDev running" check
environments/includes/redis.base.ymlenvironments/includes/dragonfly.base.ymlBoth define a service literally named redis; Dragonfly only swaps the image and volumeCorrect as written — it is what lets roll redis work either way — but it forces the mutual-exclusion guard at commands/env.cmd:12-14
commands/env.cmd:116-120commands/browsersync.cmdenv up shells back out to roll browsersync freeport to pick host ports before assembling the compose argsA circular dependency between two commands, evaluated on every up with browsersync enabled
docs/index.md:7README.mdSphinx include with end-before: <!-- include_open_stop -->Keep the marker: anything below it is contributor-facing and stays off the published site

Two couplings that look wrong but are not: commands/*.cmd wrappers re-invoking ${ROLL_DIR}/bin/roll as a subprocess is a deliberate choice (a sourced command cannot cleanly re-enter dispatch), and the traefik/tunnel/mailhog network peering in utils/core.sh has to be imperative because the two Compose projects cannot declare each other's networks.

Boundary findings

None open. No component has an unclear owner or a circular library dependency. The two that did cost maintenance time are resolved: the backup family now shares utils/backup.sh, and magento2-init is split into re-runnable phases in utils/magento2-init.sh.

Environments

roll env-init <name> <type> writes .env.roll with five base lines and appends environments/<type>/init.env, expanding $ROLL_ENV_NAME and $GENERATED_APP_KEY through envsubst (commands/env-init.cmd:39-53). The available types and what each actually provides:

Typeinit.envMutagen (macOS)Notable defaults from init.env
magento2yesyesPHP 8.3, Node 22, MariaDB 10.4, ES 8.11, Varnish 7.0, RabbitMQ 3.9, Redis 6.2, ROLL_INCLUDE_GIT=1
magento1yesyesPHP 7.2, Node 12, Composer 1, MariaDB 10.3
shopwareyesyesPHP 7.4, Node 12, MariaDB 10.4
laravelyesnoPHP 8.3, Node 22, Redis + RedisInsight, seeds APP_KEY, DB_*, REDIS_*
akeneoyesnoPHP 7.4, MySQL 8.0, Elasticsearch on
symfonyyesnoPHP 7.4, Node 12, MariaDB 10.4
typo3yesnoPHP 7.4, Node 12, MariaDB 10.4
wordpressyesnoPHP 7.4, Composer 1, seeds DB_*; PHP image variant is php-fpm-wordpress
phpyesnoPHP 8.1, ROLL_DB=0
vuejsyesnoPHP 8.1, Node 18, MariaDB 10.4; routes app. to nginx and watch.app. to port 8080 on php-fpm
localyesnoNetwork-only; bring your own .roll/roll-env.yml. Pins only what its default toggles enable

Without a Mutagen configuration, macOS falls back to the bind mount from environments/includes/php-fpm.base.yml (.:/var/www/html:cached), which works but is markedly slower for large codebases; roll sync refuses to run for those types (commands/sync.cmd:40-42). On Linux and WSL all types bind-mount directly and Mutagen is never used.

For the three synced types, environments/<type>/<type>.darwin.yml replaces the bind mount with a named volume (appdata:/var/www/html) and re-mounts only pub/media from the host. Two consequences worth knowing: var/ and pub/static/ are excluded from the sync (environments/magento2/magento2.mutagen.yml), so files written there by the host are not visible in the container; and ignore.vcs: true strips .git at every depth, which is why ROLL_INCLUDE_GIT=1 exists to re-mount the repository root's .git explicitly (environments/includes/git.base.yml).

Service toggles

Registered in utils/config.sh:70-87. Each enabled toggle appends the matching fragment from environments/includes/.

ToggleSchema defaultServiceExposed at
ROLL_NGINX1nginxhttps://<sub>.<domain> (priority 2)
ROLL_DB1mariadb or mysql per DB_DISTRIBUTIONdb:3306 internally, or via the SSH tunnel
ROLL_REDIS1redisredis:6379
ROLL_DRAGONFLY0Dragonfly as the redis serviceredis:6379; mutually exclusive with ROLL_REDIS
ROLL_VARNISH0varnish in front of nginxtakes over the host route (priority 1) and sets traefik.enable=false on nginx
ROLL_ELASTICSEARCH0elasticsearchhttps://elasticsearch.<domain>
ROLL_OPENSEARCH0opensearchhttps://opensearch.<domain>
ROLL_ELASTICVUE, ROLL_REDISINSIGHT0web UIsown subdomains
ROLL_RABBITMQ0rabbitmqhttps://rabbitmq.<domain> (management UI)
ROLL_MONGODB0mongodbmongodb:27017
ROLL_BROWSERSYNC0ports published on php-fpmthe only fragment in environments/ that publishes host ports
ROLL_SELENIUM, ROLL_SELENIUM_DEBUG, ROLL_ALLURE0test infrastructureroll vnc for the debug VNC session
ROLL_TEST_DB0tmp-mysql on tmpfs (MySQL 5.7, hardcoded)Magento integration tests
ROLL_MAGEPACK0Magepack bundlingmagento2 only
ROLL_INCLUDE_GIT0bind-mounts .git into the containerneeded on macOS with Mutagen

For a magento2 project ROLL_VARNISH, ROLL_ELASTICSEARCH and ROLL_RABBITMQ default to 1 via applyEnvTypeDefaults in utils/config.sh, whether or not init.env spelled them out. A value in .env.roll still wins.

Local, test/staging, and production

RollDev is a local-development tool only; nothing here deploys. The one production-shaped concern it carries is that the images it references are published from a separate repository, so the version of an image a project gets depends on the tag in .env.roll and on what has been pushed to ghcr.io/epartment/roll, not on anything in this checkout.

Common tasks

Bring up the shared services (needed once per host boot):

roll svc up

Show every running environment and the state of the shared services:

roll status

Show one environment's services, URLs and versions as a table:

roll env describe

Start, stop, and restart the environment in the current project:

roll env up
roll env down
roll restart

Open a shell in the php-fpm container as www-data:

roll shell

Run a command in the container without a TTY (for scripts and pipes):

roll clinotty php bin/magento cache:flush

Open a SQL prompt on the project's database:

roll db connect

Import a dump, rewriting DEFINER clauses and stripping GTID_PURGED/SQL_LOG_BIN statements:

roll db import < dump.sql

Add a PHP extension to both the php-fpm and php-debug containers at runtime:

roll add-php-ext redis

Sign a wildcard certificate for extra hostnames (needed for multi-store setups):

roll sign-certificate example.test

Archive an environment's volumes and configuration:

roll backup

Clone an environment under a new name and domain:

roll duplicate

Inspect the resolved configuration for the current project:

roll config show

See which version this project pins for every service, and whether that service is enabled:

roll config versions

Change one of them, picking from the versions the image actually has:

roll config version

List every command the registry resolved, including user and third-party packs:

roll registry list

Lint the shell sources exactly as CI does:

shellcheck commands/*.cmd utils/*.sh

Build the documentation locally:

pip install -r docs/requirements.txt
sphinx-build -b html docs docs/_build/html

Machine interface

Everything RollDev does is reachable without a terminal, so it can be driven from CI, a deploy script, the build server, or an AI assistant. The user-facing guide is docs/machine-interface.md; this is the contributor summary of what exists and where it lives.

SurfaceWhere
--format json on status, env describe, registry list, env doctorthe respective .cmd files; values escaped through jsonEscape in utils/core.sh
roll has-command <name> — exit 0/1, no outputcommands/has-command.cmd, resolves through the registry so add-on commands count
roll env up --waitCompose native, made meaningful by the healthchecks in environments/includes/*.base.yml
roll env doctorcommands/doctor.cmd
roll config check-pins / fix-pinscommands/config.cmd
roll config versions [service] — pinned versions, or a service's published versions one per line on stdoutcommands/config.cmd, utils/images.sh
roll config version <service> <version> — set a pin without promptingcommands/config.cmd
Flag-first promptsutils/interact.sh

Two rules bind anything added here:

  1. Machine-readable output carries no secrets and no ANSI, and every value goes through jsonEscape rather than being concatenated raw.
  2. A prompt is never the only route to a value. Flags and environment variables come first, gum runs only on a TTY, and with no terminal the prompt fails naming the flag that would have answered it — so an unattended run cannot hang. .github/scripts/test-interact.sh asserts this and runs in the smoke suite on both platforms.

Note that giving a command a --flag means adding it to ROLL_CMD_ANYARGS in bin/roll, which changes how --help reaches it: roll stops parsing at the first dash and leaves ROLL_PARAMS empty. Such a command must render its own help by sourcing commands/usage.cmd, never by re-invoking roll <self> --help, which recurses until killed. .github/scripts/test-syntax.sh enforces this.

Conventions

These are enforced by review rather than by tooling, except where noted.

  • Every change must work on macOS and Linux. Both are first-class targets — see Supported platforms for the table of real behavioural differences. CI runs a macos-latest + ubuntu-latest matrix (ShellCheck plus the Docker-free smoke script, .github/scripts/smoke.sh), but the smoke set only covers commands that need neither a project checkout nor a running daemon — anything touching roll env/service containers is still on the author: run the affected command under /bin/bash on macOS before merging. The recurring traps are bash 3.2, BSD vs GNU flag semantics (sed, ping, stat), Mutagen-vs-bind-mount file sync, and the SSH agent socket path.
  • Bash 3.2 only. No associative arrays, no ${var^}/${var,} case modification, no mapfile without a fallback (commands/status.cmd:20-28 shows the pattern). macOS ships bash 3.2.57 and bin/roll runs under #!/usr/bin/env bash, so a bash-4 construct fails there while passing on Linux CI.
  • Every .cmd and util script starts with the guard[[ ! ${ROLL_DIR} ]] && >&2 echo … && exit 1. All 50 command files currently carry it (verified). Scripts are meant to be sourced by bin/roll, never run directly.
  • Use the messaging helpers from utils/core.shfatal, error, warning, info, success, boxinfo, boxsuccess, boxerror — rather than raw echo, so output stays consistent and goes to stderr.
  • Gate OS-specific logic on $OSTYPE (darwin* vs Linux) and use sed_inplace (utils/core.sh:167) instead of sed -i, which differs between BSD and GNU. Prefer ${ROLL_ENV_SUBT} where the config has already been loaded, since it also distinguishes wsl. When adding an OS-specific YAML fragment, remember that wsl gets neither the .darwin.yml nor the .linux.yml variant (finding M12).
  • Arithmetic:x=$((x + 1)), never ((x++)) — see Dispatch.
  • Adding a command: create commands/<name>.cmd + commands/<name>.help; the registry picks it up with no central list to edit. If it must accept arbitrary pass-through flags, add it to ROLL_CMD_ANYARGS in bin/roll:43. Add it to commands/usage.help as well — that file is maintained by hand for visibility.
  • Help files define, they do not print. A commands/*.help file only assigns ROLL_USAGE; commands/usage.cmd sources it and does the single echo -e. A .help file that echoes ROLL_USAGE itself makes the help render twice.
  • Adding a service: add the fragment(s) under environments/includes/ (plus per-type overrides), wire a ROLL_<SERVICE> toggle into the assembly block in commands/env.cmd, and register the variable and its default in initConfigSchema (utils/config.sh:58). Do not rely on a ${VAR:-default} fallback inside the YAML — the schema default is exported and wins.
  • Container images: anything requiring a binary, package or PHP extension to exist inside a container belongs in the separate images repository (github.com/epartment/images), not here.
  • Documentation: user-facing docs live in docs/ and publish to GitHub Pages on push to main. Update them in the same change as the behaviour they describe.
  • Releases: run the Tag Release workflow with a version; it writes version, commits to main, tags, and opens a draft release. Publishing the release triggers push-release-to-brew.yml, which regenerates the Homebrew formula in epartment/homebrew-roll.
  • Known gaps found while running the stack unattended are recorded in FEATURE-REQUESTS.md with severities and citations. Read it before proposing improvements — its numbering (H1, M1, …) is independent of this file's.

Known issues & improvement points

Defects found by reading the source at version 0.7.2. Items marked verified were reproduced or confirmed against a running container during this review; the rest were read from the code. Numbering here is independent of FEATURE-REQUESTS.md, which covers missing capabilities rather than defects, and of the boundary findings above.

High

None open.

Medium

None open.

Low

None open.

Audited and clean

Checked during this review and found correct, so the next reader does not re-investigate:

  • The sourcing guard is universal. All 50 .cmd files under commands/ (including the env-type subdirectories) carry the [[ ! ${ROLL_DIR} ]] guard. An earlier suspicion that the newer commands had skipped it was a measurement error.
  • Cross-platform gating is otherwise applied consistently. Every OS-specific code path found branches on $OSTYPE or ${ROLL_ENV_SUBT} rather than assuming a platform: sed_inplace (utils/core.sh:167) handles the BSD/GNU sed -i difference, commands/sync.cmd:33-35 refuses to run off darwin instead of failing obscurely, commands/debug.cmd:13-21 resolves the Xdebug host per platform, commands/status.cmd:20-28 guards mapfile, and commands/tableplus.cmd and commands/vnc.cmd are correctly macOS- and Linux-specific respectively. isOnline now probes with curl instead of ping, avoiding platform flag differences. The missing wsl fragment (M12) is handled correctly.
  • Bash 3.2 compliance. The parallel-indexed-array pattern in utils/config.sh and utils/registry.sh is applied consistently; no associative arrays anywhere. commands/status.cmd correctly guards its mapfile use with a while read fallback. Case modification is always done via tr or the capitalize helper function.
  • Arithmetic increments. No bare ((x++)) remains in commands/ or utils/; the codebase uses x=$((x + 1)), which is what keeps set -e from killing the CLI mid-command.
  • roll redis works with Dragonfly.environments/includes/dragonfly.base.yml defines the service as redis with the Dragonfly image, so commands/redis.cmd:17's lookup of the redis service succeeds under either backend. The mutual-exclusion guard at commands/env.cmd:12-14 is the necessary consequence, not a bug.
  • roll db connect and roll db import survive MariaDB 11. The mysql client symlink is still present in ghcr.io/epartment/roll/mariadb:11.4 (verified in a running container); only mysqldump was removed, which is why H2 is scoped to dump alone.
  • NGINX_TEMPLATE derivation works. It is declared string:optional, so setConfigDefault never fills it in and the ${NGINX_TEMPLATE:-…} chains in postProcessConfig resolve as written. The apparently redundant trailing export lines in the magento1/magento2 blocks are harmless because :- preserves the value set by the branch above.
  • Compose fragment layering.appendEnvPartialIfExists appends rather than replaces, and Compose merges list-valued keys by target path, so environments/magento2/magento2.darwin.yml's appdata:/var/www/html correctly supersedes the bind mount from environments/includes/php-fpm.base.yml on macOS. This is the mechanism behind the Mutagen setup, not an accident.
  • No secrets in the repository. The only credentials present are the local development defaults (app/app, magento/magento in environments/includes/db.base.yml and environments/magento2/db.base.yml) and the documented VNC password in commands/vnc.cmd. No tokens, keys or customer data. .gitignore covers .idea/, and no IDE files are tracked.
  • Release plumbing is consistent.version (0.7.2), commands/version.cmd, the Tag Release workflow and the Homebrew formula update form a closed loop with no hardcoded version elsewhere.

Not systematically recorded for this review: the internal logic of commands/backup.cmd, commands/restore.cmd, commands/restore-full.cmd, commands/duplicate.cmd, commands/magento2-init.cmd and commands/multistore.cmd beyond their structure and interfaces — together roughly 4 900 lines. Their boundary problems are recorded above; their internals were not line-audited, and neither backup nor restore was executed against a live environment.

Troubleshooting

SymptomCauseFix
Unsupported OSTYPE '…'Only macOS and Linux (including WSL2) are supported hostsSee Supported platforms; on Windows run roll inside WSL2, never in PowerShell or Git Bash
Environment config could not be foundNo .env.roll in the current directory or any parentRun roll env-init <name> <type> from the project root
docker compose version should be 2.2.3 or higherCompose v1, or the plugin is missingInstall the Compose v2 plugin; docker-compose (hyphenated) is not used
invalid project name "…" from roll env upROLL_ENV_NAME contains uppercase or an illegal characterLowercase it in .env.roll; note the rename orphans the old volumes
Environment comes up on an empty database after a renameROLL_ENV_NAME is the Compose project name and prefixes every named volumeBring the environment down, copy oldname_dbdata to newname_dbdata, bring it up
port is already allocated on php-fpmROLL_BROWSERSYNC=1 publishes fixed host ports on that service — the only fragment that publishes anySet ROLL_BROWSERSYNC=0; verify with roll env config | grep published. See FEATURE-REQUESTS.md H2
A command run right after roll env up fails to connect to the DB or search engineNo fragment declares a healthcheck: and up does not pass --wait, so it returns when containers startRetry with a wait loop against the service. See FEATURE-REQUESTS.md H1
Mutagen sync sessions are not used on "linux" host environmentsroll sync is macOS-onlyExpected; Linux and WSL bind-mount directly
Mutagen configuration does not exist for environment type "…"Only magento1, magento2 and shopware ship a .mutagen.ymlExpected; that type uses a bind mount on macOS too
In-container composer install reports a missing .git in a vendor/ packageMutagen's ignore.vcs: true strips .git at every depth, so source installs have no checkoutRemove the affected vendor/ directories and re-run composer install to force dist installs
Files written to var/ or pub/static/ on the host are invisible in the containerBoth paths are excluded from the Mutagen sessionWrite through the container (roll cli), or docker cp into the volume
A roll subcommand exits 1 printing only its opening INFO: lines, on Linux but not macOSset -e plus a statement returning non-zero — classically a bare ((x++)) from 0 — kills the sourced command, and the ERR trap is not inherited by functionsUse x=$((x + 1)); see Dispatch
Browser does not trust *.test certificatesThe root CA was not added, or the browser has its own storeImport ~/.roll/ssl/rootca/certs/ca.cert.pem; Firefox and Chrome-on-Linux need it added manually
*.test hostnames do not resolvednsmasq is not running, or the resolver is not configuredroll svc up; on macOS check /etc/resolver/test, on Linux and Windows configure DNS manually — see docs/configuration/dns-resolver.md
roll status says RollDev is not running while containers are upThe shared network name is recovered by text-parsing docker/docker-compose.ymlCheck that networks.default.name: roll is still within three lines of networks: in that file

Full user documentation: epartment.github.io/rolldev (sources in docs/). Container images: github.com/epartment/images. Issues: github.com/epartment/rolldev/issues.

Licence

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages