How code in packages/<name> reaches github.com/utopia-php/<name> and Packagist.
On every push to main, CI splits each packages/<name> directory into a standalone history and pushes it to utopia-php/<name>. Libraries are imported with git subtree add (full history preserved); the split itself is computed by bin/monorepo — starting from the latest git-subtree-* annotation, it re-synthesizes one commit per mainline commit that changed the package, byte-identical to git subtree split output but immune to its fatal cache collision when a package is removed and re-imported. Splits are deterministic and push fast-forward on top of each mirror's existing history.
The distribution repositories become read-only mirrors: archive their open PRs, enable branch protection, and point contributors to the monorepo.
Libraries that predate the monorepo arrive with bin/monorepo import, which brings their history and their existing mirror with them. A package written here has neither, so three things have to exist before its first split:
- The mirror repository, created empty at
utopia-php/<name>. The split pushesmainfast-forward and without--force, so an auto-initialised repository (a README, a licence, any commit at all) rejects that push and theSplitjob fails on merge. The split App is installed org-wide, so a new repository needs no separate grant. packages/<name>/.github/workflows/mirror.yml, calling the sharedmirror-redirectworkflow with the package name. The package directory becomes the mirror root, so this is how a pull request or issue opened on the mirror gets closed with a pointer back here.packages/<name>/.gitignore, because the monorepo's root ignore file is not part of the split and the mirror would otherwise have none.
Then submit the mirror to Packagist so composer require utopia-php/<name> resolves; Packagist follows the mirror's branches and tags from there. Finish with the same hygiene as any mirror: a description pointing at the monorepo, and a ruleset protecting the default branch.
Verify before merging with bin/monorepo split <name> --dry-run, which synthesizes the history and prints its head without pushing.
Before the monorepo split, an in-flight PR branch on a library's own repository was directly installable (composer require utopia-php/http:dev-my-branch) — handy for unblocking a consumer without waiting for a release. The Split Dev workflow restores that: dispatch it (Actions → Split Dev → Run workflow) on the feature branch, naming the package(s) to publish, and it pushes the branch's split to the mirror under the same branch name. Packagist tracks the mirror's branches, so the consumer can then require:
{ "require": { "utopia-php/http": "dev-my-branch as 2.1.999" } }The inline alias (aliasing into the latest released minor) keeps other packages' constraints on the library resolving; the workflow's run summary prints the exact line to copy. Notes:
- The mirror branch is force-pushed on every dispatch (rebases change the synthesized history), so re-dispatch after pushing new commits and run
composer updatein the consumer. - Mirror rulesets only protect the default branch, so dev branches need no bypass — but they also never publish to
mainor tags; the workflow refuses to run onmain. - When the branch merges (or is abandoned), re-dispatch with action: delete to remove the mirror branch. Stale branches are harmless but noisy.
A monorepo tag shaped <package>/<semver> (e.g. http/2.1.0) triggers CI to push tag 2.1.0 to utopia-php/http (Packagist picks it up as usual) and publish a GitHub release on the mirror whose notes are every monorepo commit that touched packages/http since the previous release, with a compare link back to the monorepo.
The Split workflow authenticates as a GitHub App with contents: write, installed on the utopia-php org. In the monorepo's Settings → Secrets and variables → Actions, set:
- variable
SPLIT_APP_ID— the App ID (or client ID) - secret
SPLIT_APP_PRIVATE_KEY— the app's private key (.pemcontents, generated in the app's settings)