Split publishing
Operator runbook for publishing each
src/Altair/*sub-package asuniveros/<name>, plus the framework’sdocs/and starter (src/Altair/Bootstrap/resources/skeleton/) as the dedicateduniveros/docsanduniveros/univerosrepos. Driven by.github/workflows/split.yml.
Source of truth: the univeros/framework monorepo. The matrix has two kinds of entries:
- Package splits — every
src/Altair/<Package>/directory that ships acomposer.json. The workflow’s drift guard fails if any are missing from the matrix; new packages can’t slip through unpublished. - Non-package splits —
docs(fromdocs/) anduniveros(fromsrc/Altair/Bootstrap/resources/skeleton/). Managed by hand and excluded from the drift check.
All three top-level repos — univeros/univeros (starter), univeros/framework (library), univeros/docs (documentation) — are visible on the org page; the package splits are read-only mirrors used by Composer.
How the workflow works
Section titled “How the workflow works”splitsh/lite rewrites the monorepo’s history for a single subtree (e.g. src/Altair/Cache/ or docs/) into a synthetic commit graph whose root is that subtree. The resulting SHA is force-pushed to master (or the tag ref, on tag pushes) of github.com/univeros/<name>. Each sub-repo therefore looks like it had always lived at the top level of its own repository — composer require univeros/cache pulls the package alone, not the whole framework, and composer create-project univeros/univeros myapp materialises the starter.
The split is reproducible: re-running it on the same commit produces the same SHA. The split SHAs are not the same as the monorepo’s commit SHAs.
Initial setup (one-time)
Section titled “Initial setup (one-time)”Before the workflow can push anything, the following must exist:
-
GitHub repositories for each of the 36 packages plus the three top-level repos (
univeros/univeros,univeros/framework,univeros/docs). The original 16 sub-package repos (cache,common,configuration,container,cookie,courier,data,filesystem,happen,http,middleware,sanitation,security,session,structure,validation) plusuniveros/univerosanduniveros/frameworkalready exist. Create the remaining 19 sub-package repos anduniveros/docs:Terminal window for pkg in agent-spec bootstrap cli doctor eval events index introspection \mcp messaging migration-intelligence observability observatory \persistence profiling scaffold suggest test-reporter tinker; dogh repo create "univeros/$pkg" \--public \--description "[READ ONLY] Subtree split of the Univeros $pkg component" \--homepage "https://univeros.io"done# Plus the dedicated docs mirrorgh repo create univeros/docs \--public \--description "[READ ONLY] Subtree split of the Univeros framework documentation" \--homepage "https://univeros.io"univeros/univerosalready exists from the 2017-era stub — the first workflow run will force-push the current starter contents over it. Other new repos can be empty; the first run createsmaster. -
Delete stale repositories that no longer correspond to a
src/Altair/*directory:Terminal window gh repo delete univeros/queue --yes # replaced by univeros/messaging in 2026-05 -
Authentication token. Generate a fine-grained personal access token with
Contents: writeon all repositories under theuniverosorg (35 sub-repos +univeros/docs+univeros/univeros= 37 push targets). Use “All repositories” rather than “Selected repositories” — the latter forgets to include each new sub-package until you remember to edit the token. Store it onuniveros/frameworkas theSPLIT_TOKENrepository secret:Terminal window gh secret set SPLIT_TOKEN --repo univeros/framework --body "<the-token>"Deploy keys (one keypair per sub-repo) are the alternative if PAT rotation is a concern; the workflow would need to be adapted to use SSH URLs in that case.
-
Default branch on each sub-repo must be
master(matching the monorepo). Newgh repo createdefaults tomain— fix it:Terminal window for pkg in $(gh repo list univeros --json name -q '.[].name' | grep -v '^framework$\|^univeros$'); dogh api -X PATCH "repos/univeros/$pkg" -f default_branch=master 2>/dev/null || truedone
Running the workflow
Section titled “Running the workflow”Triggered manually via workflow_dispatch — the comment at the top of split.yml explains the rationale (the framework is not yet 1.0; auto-on-push will be enabled once it is).
# Dry-run: compute splits for every entry without pushinggh workflow run split.yml -f dry_run=true
# Real run: split + push all 38 entries (36 packages + docs + univeros)gh workflow run split.yml
# Single split (matches the workflow_dispatch dropdown)gh workflow run split.yml -f package=scaffoldgh workflow run split.yml -f package=docsgh workflow run split.yml -f package=univerosTail it:
gh run watchFirst release: tagging v2.0.0
Section titled “First release: tagging v2.0.0”Tags propagate the same way branches do — push v2.0.0 to the monorepo, the workflow re-splits each package at the tagged commit and pushes the same tag to each sub-repo.
git tag -a v2.0.0 -m "Univeros 2.0.0 — PHP 8.3 modernization"git push origin v2.0.0gh workflow run split.yml # triggers split + tag propagationVerify the tag landed on a sample sub-repo:
gh release list --repo univeros/cachegh api repos/univeros/cache/git/refs/tags/v2.0.0Packagist registration
Section titled “Packagist registration”Each sub-package must be submitted to packagist.org once. After that, Packagist subscribes to the GitHub webhook and picks up future tags automatically.
Submit in dependency order so each upload finds its declared deps already present on Packagist:
- Leaf packages (no inter-deps):
common,structure container(depends onstructure)configuration(depends oncontainer)middleware,security(nouniveros/*deps)- Middle layer:
cache,cookie,data,events,happen,session - Top of the original stack:
filesystem,sanitation,validation,courier,http - The 2026 additions, in alphabetical order — none of them are required by the original 16, so the order inside this batch does not matter:
agent-spec,bootstrap,cli,doctor,eval,index,introspection,mcp,messaging,migration-intelligence,observability,observatory,persistence,profiling,scaffold,suggest,test-reporter,tinker
Adding a new sub-package
Section titled “Adding a new sub-package”- Create the package under
src/Altair/<Name>/with its owncomposer.jsondeclaring"name": "univeros/<name>". - Add the matrix entry to
.github/workflows/split.ymlin thefull=JSON block. The drift guard will refuse to run until this is done. - Add the package name to the
workflow_dispatch.inputs.package.optionslist so the dropdown stays in sync. - Create the GitHub repository (
gh repo create univeros/<name> --public ...) and grantSPLIT_TOKENwrite access to it. - Submit to Packagist after the next tagged release.
Removing a sub-package
Section titled “Removing a sub-package”-
Delete
src/Altair/<Name>/(the matrix drift guard will block the workflow until you also drop its entry). -
Remove the matrix entry from
split.ymland from the dropdown options. -
Archive (don’t delete) the GitHub repository so existing
composer.lockfiles keep resolving:Terminal window gh api -X PATCH repos/univeros/<name> -f archived=true -
Mark the package as abandoned on Packagist, pointing to its replacement if any.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause |
|---|---|
splitsh-lite produced an empty SHA | The path in the matrix doesn’t exist, or the subtree is empty at the chosen ref. |
403 from github.com when pushing | SPLIT_TOKEN doesn’t have write access to that target repo. Fine-grained PATs scoped to “Selected repositories” need every new sub-repo added explicitly — switch to “All repositories” to avoid this. Also check the workflow hasn’t accidentally re-enabled persist-credentials or set extraheader. |
split.yml matrix is out of sync with src/Altair/* | A package directory exists with a composer.json but is missing from the matrix, or the matrix references a src/Altair/<name> path that no longer exists. The error message includes a diff. Non-package entries like docs and univeros are exempt — only src/Altair/<name> paths are checked. |
| Tag propagated to some sub-repos but not others | fail-fast: false is set, so individual sub-repos can fail independently. Check the matrix job logs and re-run the failed jobs once the cause is fixed. |
| Packagist shows an old version after a fresh tag | The GitHub webhook may not be wired up. Trigger a manual update at https://packagist.org/packages/univeros/<name> → “Update”. |