This monorepo ships three packages to three different registries:
| Package | Registry | Source path |
|---|---|---|
eslint-plugin-aaa-pattern |
npm | packages/eslint-plugin-aaa-pattern |
rubocop-aaa |
RubyGems | packages/rubocop-aaa |
babu-ch/phpcs-aaa |
Packagist | packages/phpcs-aaa |
Releases are manual on purpose — release frequency is low and we want a human to confirm each cut. All commands below run from the monorepo root.
| Step | npm | RubyGems | Packagist |
|---|---|---|---|
| Bump version | edit package.json |
edit .gemspec |
(none — Packagist reads git tags) |
| Build artifact | npm packs src/ |
gem build produces .gem |
git subtree split |
| Push | npm publish |
gem push |
push to split mirror + tag |
| User installs via | npm install eslint-plugin-aaa-pattern@<v> |
gem install rubocop-aaa -v <v> |
composer require babu-ch/phpcs-aaa:<v> |
Each release script also creates a per-package tag in the monorepo (eslint-plugin-aaa-pattern-v<v>, etc.) so git log --tags shows the release history.
make test— all three suites green.- Bump the version in the relevant manifest:
- npm:
packages/eslint-plugin-aaa-pattern/package.json(version) - RubyGems:
packages/rubocop-aaa/rubocop-aaa.gemspec(spec.version) - Packagist:
packages/phpcs-aaa/composer.json(version)
- npm:
- Commit and push the bump (
chore: bump <pkg> to <v>). - Run the release command. Each script reads the version straight from the manifest — no argument needed.
All run from the monorepo root. Each script picks up the version from the package's manifest.
make release-eslint # eslint-plugin-aaa-pattern -> npm
make release-rubocop # rubocop-aaa -> RubyGems
make release-phpcs # phpcs-aaa -> Packagist (via split mirror)
make release-all # all three sequentiallyEach script is also runnable directly: scripts/release-eslint.sh.
All scripts:
- Refuse to run if the working tree is dirty.
- Refuse to run if you're not on
main. - Refuse to run if the per-package tag (
<package>-v<version>) already exists locally or onorigin— that means the version was already cut, you need to bump the manifest first. - Push the tag back to
originafter a successful publish.
release-eslint.sh:
- Reads the version from
packages/eslint-plugin-aaa-pattern/package.json. - Lets
npm publish'sprepublishOnlyhook run the test suite. - Adds
--access publicautomatically on the first publish (when the package does not yet exist on npm).
release-rubocop.sh:
- Reads the version from
packages/rubocop-aaa/rubocop-aaa.gemspec. - Re-runs
rspecvia Docker before pushing. - Cleans up the built
.gemfile even on failure.
release-phpcs.sh:
- Reads the version from
packages/phpcs-aaa/composer.json. - Verifies the
phpcs-aaa-splitgit remote is configured. - Splits
packages/phpcs-aaa/into a synthetic commit, pushes it to the split mirror'smain, and tagsv<version>there.
| Registry | Setup |
|---|---|
| npm | npm login (npm.js account with publish access). 2FA enabled. |
| RubyGems | gem signin (RubyGems account with push access). 2FA enabled — gemspec sets rubygems_mfa_required = 'true'. |
| Packagist | git remote add phpcs-aaa-split https://github.com/babu-ch/phpcs-aaa.git. Packagist itself only needs to be told about the split repo once at the very first release (Submit the URL on packagist.org). |
- Packagist auto-discovers versions from git tags on the registered repo's root
composer.json. There is no per-path option. - A split mirror is the standard pattern for monorepo PHP packages (Symfony, Laravel, etc. all do this).
- The mirror is distribution-only — PRs and issues are not accepted there. Both its repo description and the in-package
README.mdlink contributors back to this monorepo.
If the manual subtree push ever becomes a chore (release frequency goes up), the same flow can be automated via GitHub Actions + a deploy key on the split mirror. For now we keep it manual to avoid the secret-management overhead.