acencyril / rehearsal-bundle
Try a change in your Symfony admin before production: proposed changes with their notes and test results, Validate or Reject with a comment, manual or automatic validation, an event to merge and deploy.
Package info
github.com/acencyril/rehearsal-bundle
Type:symfony-bundle
pkg:composer/acencyril/rehearsal-bundle
Requires
- php: >=8.1
- psr/clock: ^1.0
- symfony/config: ^6.4 || ^7.0 || ^8.0
- symfony/console: ^6.4 || ^7.0 || ^8.0
- symfony/dependency-injection: ^6.4 || ^7.0 || ^8.0
- symfony/event-dispatcher-contracts: ^3.0
- symfony/framework-bundle: ^6.4 || ^7.0 || ^8.0
- symfony/http-kernel: ^6.4 || ^7.0 || ^8.0
- symfony/routing: ^6.4 || ^7.0 || ^8.0
- symfony/security-bundle: ^6.4 || ^7.0 || ^8.0
- symfony/security-csrf: ^6.4 || ^7.0 || ^8.0
- symfony/translation: ^6.4 || ^7.0 || ^8.0
- symfony/twig-bundle: ^6.4 || ^7.0 || ^8.0
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.5
- symfony/browser-kit: ^6.4 || ^7.0 || ^8.0
- symfony/css-selector: ^6.4 || ^7.0 || ^8.0
- symfony/dom-crawler: ^6.4 || ^7.0 || ^8.0
- symfony/event-dispatcher: ^6.4 || ^7.0 || ^8.0
- symfony/mailer: ^6.4 || ^7.0 || ^8.0
- symfony/yaml: ^6.4 || ^7.0 || ^8.0
Suggests
- acencyril/uplift-bundle: proposes dependency upgrades as Rehearsal changes (uplift:apply --rehearsal)
- symfony/mailer: for the recap emails (rehearsal.mailer)
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-26 22:31:02 UTC
README
Try a change in your Symfony admin before production.
Rehearsal adds a section to your admin that lists the changes proposed for the site (a dependency upgrade, a fix, a new page), each with its notes, what to check and its test results. For each one: Validate (with an optional comment) or Reject with a comment. A validation dispatches an event your application hooks to merge and deploy; Rehearsal itself only records the decision.
It is meant for a copy of the site built from a branch (a staging or sandbox instance): you open its admin, try the change for real, and decide there.
Documentation française : README.fr.md
composer require acencyril/rehearsal-bundle
How it works
- Something writes the proposed changes to
.rehearsal/changes.jsonon the branch: a person, a script, or Uplift (uplift:apply --rehearsal=.rehearsal/changes.json). - The branch is deployed to a staging copy.
rehearsal:sync(or the first visit of the section) notices the new changes, sends the recap email if enabled and, inautomode, validates those whose tests passed. - You open the section, try each change and decide. Each decision is written to the
decisions file and dispatches
ChangeValidatedorChangeRejected. - Your listener, or CI reading
rehearsal:status --json, does the rest: merge, deploy, tag, or send the rejection back to whoever proposed the change.
A decision holds for one revision of a change. When the change comes back with a new revision (fixed after a rejection, or tested again), it is pending again, with the earlier decision and its comment shown.
The changes file
{
"title": "November maintenance",
"summary": "Dependency upgrades and one fix.",
"link": "https://github.com/example/site/pull/12",
"revision": "abc1234",
"tests": {"status": "passed", "summary": "355 journeys"},
"changes": [
{
"id": "contact-form",
"title": "Shorter contact form",
"detail": "Two fields instead of five.",
"replaced": "The five-field form",
"check": "Send a message from the contact page.",
"url": "/en/contact",
"source": "manual",
"revision": "abc1234",
"tests": {"status": "failed", "summary": "1 failure", "failures": ["ContactTest::testSend"], "output": "…"}
}
]
}
| Field | |
|---|---|
id |
a-z 0-9 . _ -, 64 at most, the same across revisions of a change |
title, detail, replaced, check |
text; line breaks are kept, HTML is escaped |
url |
a path of this site to open; anything else (another site, javascript:) is dropped |
source |
who proposed it (uplift, manual…) |
revision |
the code the change was tested on; defaults to the file's revision |
tests |
status: passed, failed or not_run; summary, failures, output; defaults to the file's tests |
summary |
optional facts of an upgrade, shown as a card (below) |
link (file) |
where to read more, https:// only (pr is read too) |
Entries without a valid id or a title are ignored; a missing or broken file is an
empty list. The older test form {"ok": 350, "ko": 2, "failures": [...]} (automatic
journeys) is read as well.
The summary card (optional)
A change may carry a summary: the facts of an upgrade. Rehearsal then shows a card
at the top of the change, with the main move in large type, the counts, the tests, the
deprecations fixed, the new features adopted, what the policy refused and the way
ahead, and the decision state. Without summary the change looks as before.
Uplift fills it; any tool can. Every field
is optional: leave out what was not measured, the card shows only what is there.
"summary": { "main": {"package": "symfony/*", "label": "Symfony", "from": "7.3.11", "to": "7.4.21"}, "php": {"from": "8.2", "to": "8.3"}, "packages_moved": 40, "constraints_rewritten": 11, "tests": {"before": "passed", "after": "passed"}, "deprecations_fixed": {"rules": ["RenameAttributeRector"], "files": ["src/Controller/HomeController.php"]}, "adoptions": [{"id": "as-command", "title": "Name commands with #[AsCommand]", "status": "applied", "places": 2}], "blocked": [{"name": "doctrine/doctrine-bundle", "version": "3.3.3", "reason": "requires PHP ^8.4, the project is on 8.2"}], "path_ahead": [{"name": "symfony/*", "versions": ["8.1.9"]}] }
| Field | |
|---|---|
main |
the main move: package, label (shown; defaults to package), from, to (versions) |
php |
from, to: shown only when they differ |
packages_moved, constraints_rewritten |
counts (packages whose locked version changed; composer.json constraints rewritten) |
tests |
before, after: passed, failed or not_run each, both optional |
deprecations_fixed |
rules, files: what was rewritten for deprecations (Rector rules, files) |
adoptions |
id, title, status (applied, untested, proposed, reverted, rector_failed, unchanged), places |
blocked |
moves refused by the policy: name, version, reason |
path_ahead |
the steps left for later runs: name, versions; the main package's line reads "Then 8.1.9 in later runs" |
Bad values are dropped, never guessed (a version that is not one, an unknown status).
The card's colours are CSS variables falling back to the page's: set --rh-sum-bg,
--rh-sum-tile, --rh-sum-line, --rh-sum-ink, --rh-sum-muted, --rh-sum-accent,
--rh-sum-ok, --rh-sum-ko, --rh-sum-wait on your admin to give the card colours
of its own; otherwise it follows the section's (see "The section's look" below).
Validation: manual or auto
rehearsal: validation: manual
manual(default): nothing goes further until someone clicks Validate.auto: a pending change is validated as soon as its tests passed (by: "auto",auto: true). A change without passing tests (failed, not run, none) still waits for someone. Set it per site:autowhere the tests are trusted,manualelsewhere.
Hooking the decision
use Acencyril\RehearsalBundle\Event\ChangeValidated; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; #[AsEventListener] final class DeployValidatedChange { public function __invoke(ChangeValidated $event): void { // $event->change: id, title, revision, source, tests… // $event->decision: comment, by, at, auto // $event->set: the whole changes file // e.g. write a file your CI watches, call your deploy webhook… } }
ChangeRejected carries the comment; ChangesProposed fires once per new change
revision. Events are dispatched under their class name: listen to each one you need.
Comments are what someone typed: treat them as remarks to read, never as commands to run.
Installation
With Symfony Flex the recipe registers the bundle and writes
config/packages/rehearsal.yaml and config/routes/rehearsal.yaml. Without Flex:
// config/bundles.php Acencyril\RehearsalBundle\RehearsalBundle::class => ['all' => true],
# config/routes/rehearsal.yaml rehearsal: resource: '@RehearsalBundle/config/routes.php' prefix: '/{_locale}/admin/rehearsal' trailing_slash_on_root: false requirements: _locale: 'en|fr'
The prefix is yours: put the section where your admin lives. The controller checks
rehearsal.role itself (403 otherwise); keep the path behind your firewall so that
visitors are asked to sign in. The bundle needs SecurityBundle, the translator, Twig
and CSRF protection (framework.csrf_protection).
Configuration
rehearsal: changes_file: '%kernel.project_dir%/.rehearsal/changes.json' decisions_file: '%kernel.project_dir%/var/rehearsal/decisions.json' validation: manual # or auto role: ROLE_ADMIN parent_template: 'admin/layout.html.twig' mailer: enabled: true from: 'no-reply@example.com' to: ['you@example.com'] subject_prefix: '[Rehearsal]' locale: fr
The page
The section extends parent_template, your admin layout, which must define the
title and body blocks (default: a bare page). To change the markup, override
templates/bundles/RehearsalBundle/index.html.twig, or only its rehearsal_content
or rehearsal_styles block. Texts are translation keys (domain rehearsal, English
and French).
The section's frame (header, tabs, page width) belongs to your layout, around its
body block: the section replaces that block's content. It renders in a <div>, so
your layout keeps the page's <main>.
The section's look
No CDN, no framework. The defaults derive from the page's own text colour
(currentColor): lines, cards, muted text and the state colours are mixes of it, so
the section reads on a light and on a dark admin alike, in the admin's font, whatever
the visitor's system theme. Map these to your palette on your admin (:root, body or
your layout's wrapper); two lines are usually enough:
:root { --rehearsal-accent: #ff5a1f; --rehearsal-ok: #1db97a; }
| Property | Default | Used for |
|---|---|---|
--rehearsal-font |
inherit |
all text |
--rehearsal-ink |
currentColor |
text |
--rehearsal-muted |
64 % of the text colour | labels, secondary text |
--rehearsal-line |
13 % of the text colour | borders, neutral pills |
--rehearsal-card |
3 % of the text colour | cards, counters |
--rehearsal-accent |
#3a5bd9 |
links, focus ring, the summary's arrow |
--rehearsal-ok |
#16a34a |
validated, tests passed |
--rehearsal-ko |
#dc2626 |
rejected, tests failed |
--rehearsal-wait |
#d97706 |
to decide |
--rehearsal-ok-solid |
#15803d |
the Validate button (white text) |
--rehearsal-max-width |
60rem |
the section's width |
--rehearsal-padding |
0 |
space around the section |
State colours are mixed with the text colour, which keeps them readable on both
themes. An admin that sets data-theme="dark" on <html> gets brighter default
tones.
The emails
With mailer.enabled and Symfony Mailer installed: one recap when new changes appear
(with the ones validated automatically), and one email per decision, with a link to
the section. Set default_uri (framework.router.default_uri) for the link to be
right from the console.
Commands
| Command | |
|---|---|
rehearsal:sync |
notices new changes (recap email, ChangesProposed) and auto-validates in auto mode; run it after a deploy or from cron |
rehearsal:status |
where each change stands; --json for CI (counts, all_validated, each change with its decision) |
Tests
vendor/bin/phpunit
License
MIT