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.
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.
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: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; do gh repo create "univeros/$pkg" \ --public \ --description "[READ ONLY] Subtree split of the Univeros $pkg component" \ --homepage "https://univeros.io" done # Plus the dedicated docs mirror gh 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: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: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:for pkg in $(gh repo list univeros --json name -q '.[].name' | grep -v '^framework$\|^univeros$'); do gh api -X PATCH "repos/univeros/$pkg" -f default_branch=master 2>/dev/null || true done
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 pushing
gh 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=scaffold
gh workflow run split.yml -f package=docs
gh workflow run split.yml -f package=univerosTail it:
gh run watchTags 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.0
gh workflow run split.yml # triggers split + tag propagationVerify the tag landed on a sample sub-repo:
gh release list --repo univeros/cache
gh api repos/univeros/cache/git/refs/tags/v2.0.0Each 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
- 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.
-
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:gh api -X PATCH repos/univeros/<name> -f archived=true
-
Mark the package as abandoned on Packagist, pointing to its replacement if any.
| 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". |