From fea4d460890e6c2dbf17aff5c25d7573781e4dc1 Mon Sep 17 00:00:00 2001 From: Andres Contreras Date: Sat, 26 Sep 2026 21:32:47 -0700 Subject: [PATCH] Prepare single-package publication under the framework namespace --- .gitattributes | 8 +++ .github/workflows/release.yml | 49 ++++++++++++++++- CHANGELOG.md | 10 +++- README.md | 14 ++--- book/src-es/00-quickstart.md | 13 ++--- book/src/00-quickstart.md | 13 ++--- composer.json | 3 +- docs/contributing.md | 2 +- docs/getting-started.md | 12 ++--- docs/installation.md | 8 +-- docs/modules.md | 2 +- docs/modules/installer.md | 4 +- docs/publishing.md | 32 ++++++----- docs/versioning.md | 11 ++-- .../Skeleton/CreateProjectOfflineTest.php | 2 +- packages/firefly/README.md | 4 +- packages/installer/README.md | 2 +- packages/installer/src/ArchetypeApplier.php | 6 +-- packages/installer/src/CapabilityCatalog.php | 2 +- packages/installer/tests/ArchetypeTest.php | 10 ++-- packages/kernel/src/Version.php | 2 +- samples/lumen/composer.json | 4 +- scripts/check-package-install.php | 53 ++++++++++++------- skeleton/README.md | 2 +- skeleton/app/Http/WelcomeController.php | 2 +- skeleton/composer.json | 4 +- tests/BrowserFixtureProvidersTest.php | 2 +- tests/OnePackageTest.php | 6 +-- tests/ReleaseWorkflowTest.php | 24 ++++++++- 29 files changed, 207 insertions(+), 99 deletions(-) diff --git a/.gitattributes b/.gitattributes index 353cacaf..4e9e2f07 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,12 @@ * text=auto eol=lf +/.* export-ignore +/vendor export-ignore +/node_modules export-ignore +/site export-ignore +/coverage export-ignore +/composer.lock export-ignore +/packages/*/vendor export-ignore +/packages/*/tests export-ignore /.github export-ignore /.gitignore export-ignore /.gitattributes export-ignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1489a03b..02dc655e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,7 @@ name: Release (single package) # Packagist reads tags from this repository's root composer.json. -# This gate validates the tagged distribution; indexing is checked separately (docs/publishing.md). +# The webhook indexes the tag; publication succeeds only after a clean consumer installs it. on: push: tags: @@ -31,3 +31,50 @@ jobs: - run: composer check - run: composer mono-validate - run: composer test:package + + publish: + name: Verify Packagist and publish release + needs: package + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: write + env: + RELEASE_TAG: ${{ github.ref_name }} + RELEASE_SHA: ${{ github.sha }} + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + coverage: none + tools: composer:v2 + - run: composer install --no-interaction --prefer-dist + - name: Wait for Packagist to index this exact tag + run: | + for attempt in $(seq 1 20); do + if curl --fail --silent --show-error https://packagist.org/packages/fireflyframework/larafly.json -o "$RUNNER_TEMP/larafly.json" && + php -r '$p = json_decode(file_get_contents($argv[1]), true); exit(($p["package"]["versions"][getenv("RELEASE_TAG")]["source"]["reference"] ?? null) === getenv("RELEASE_SHA") ? 0 : 1);' "$RUNNER_TEMP/larafly.json"; then + exit 0 + fi + sleep 15 + done + echo 'Packagist has not indexed the tagged commit. Check the repository webhook and rerun this job.' >&2 + exit 1 + - name: Install the public release and exercise its installer + run: php scripts/check-package-install.php --published + - name: Publish the verified release notes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + python3 - <<'PY' + import os, pathlib, re + version = os.environ['RELEASE_TAG'].removeprefix('v') + changelog = pathlib.Path('CHANGELOG.md').read_text() + notes = re.search(r'^## \[' + re.escape(version) + r'\].*?\n(.*?)(?=^## \[|\Z)', changelog, re.M | re.S) + if notes is None: + raise SystemExit('The tag has no changelog entry') + pathlib.Path(os.environ['RUNNER_TEMP'], 'release-notes.md').write_text(notes[1].strip() + '\n') + PY + gh release view "$RELEASE_TAG" >/dev/null 2>&1 || + gh release create "$RELEASE_TAG" --verify-tag --title "LaraFly ${RELEASE_TAG#v}" --notes-file "$RUNNER_TEMP/release-notes.md" diff --git a/CHANGELOG.md b/CHANGELOG.md index b9ad1422..054ea778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,17 @@ All notable changes to LaraFly are documented here. This project uses CalVer (`Y ## [Unreleased] +## [26.09.9] - 2026-09-27 + ### Changed -- Publish the repository root as the `firefly/firefly` library, replacing component names at the same +- Publish the repository root as the `fireflyframework/larafly` library, replacing component names at the same version. Runtime dependencies, autoloading and Laravel discovery now belong to that package; no split - mirrors or cross-repository release credential are needed. + mirrors or cross-repository release credential are needed. The public package uses the + `fireflyframework` namespace because Packagist reserves `firefly` for another publisher. +- Gate GitHub releases on PHP 8.3–8.5 validation and a clean installation of the exact tagged commit + from Packagist, including the bundled application installer. +- Exclude local development dependencies from both release archives and copied path installations. - Keep Lumen and component test support in development autoloading, and validate exported and copied consumer installations in CI. The installer uses the bundled skeleton and adds Testbench explicitly when the testing capability is requested. PostgreSQL outbox migrations are registered only for that diff --git a/README.md b/README.md index 2efc8645..acb15e6a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ PHP 8.3+ Laravel 13 License: Apache 2.0 - Version: 26.09.5 + Version: 26.09.9 PHPStan: max Code Style: Pint

@@ -202,7 +202,7 @@ will have — that they are worth naming outright: ```bash # 1 · Install the global installer once, then scaffold a new app -composer global require firefly/firefly +composer global require fireflyframework/larafly firefly new my-app cd my-app @@ -986,22 +986,22 @@ LaraFly layers onto Laravel, it is not a standalone runtime. Full details in **New project — the global installer:** ```bash -composer global require firefly/firefly +composer global require fireflyframework/larafly firefly new my-app ``` **New project — bundled installer:** ```bash -composer global require firefly/firefly +composer global require fireflyframework/larafly firefly new my-app ``` -**Adding LaraFly to an existing Laravel app** — `firefly/firefly` is the complete framework library, +**Adding LaraFly to an existing Laravel app** — `fireflyframework/larafly` is the complete framework library, developer console included: ```bash -composer require firefly/firefly +composer require fireflyframework/larafly ``` The browser dashboard (`firefly/admin`) and the API-documentation package (`firefly/openapi`) come with it. The @@ -1056,7 +1056,7 @@ Full flag reference and generated-file contents: [CLI](docs/cli.md). ## Modules -One published library, `firefly/firefly`, contains the components under `packages/*` and the bundled +One published library, `fireflyframework/larafly`, contains the components under `packages/*` and the bundled application skeleton. Its `replace` entries satisfy component requirements at the framework's version; component names remain useful module boundaries, with their own test suites. The 32 [module guides](docs/modules/) below group them by concern: diff --git a/book/src-es/00-quickstart.md b/book/src-es/00-quickstart.md index 24ebfe64..91b91b43 100644 --- a/book/src-es/00-quickstart.md +++ b/book/src-es/00-quickstart.md @@ -32,7 +32,8 @@ composer --version La forma más rápida de iniciar una nueva aplicación LaraFly es `composer create-project`, apuntando a la plantilla `firefly/skeleton`: ```bash -composer create-project firefly/skeleton my-app +composer global require fireflyframework/larafly +firefly new my-app cd my-app ``` @@ -50,7 +51,7 @@ cd my-app Para cuando ese comando termina, `.env` ya existe, se ha creado el archivo de base de datos SQLite, `APP_KEY` está configurada y — el paso que más importa para este libro — **`firefly:cache` ya ha compilado los manifiestos de tu aplicación**. Todavía no has escrito una sola línea de PHP, y la ruta de arranque sin reflexión sobre la que se construye todo este framework ya está en su sitio. !!! tip "Un instalador global, si lo prefieres" - `composer global require firefly/installer` te da un comando `firefly` en tu `PATH`. `firefly new my-app` envuelve la misma llamada a `composer create-project firefly/skeleton`, y luego ejecuta `git init` y un commit inicial por ti — el equivalente en LaraFly de `laravel new`. + `composer global require fireflyframework/larafly` te da un comando `firefly` en tu `PATH`. `firefly new my-app` entrega a Composer la plantilla incluida en la biblioteca, y luego ejecuta `git init` y un commit inicial por ti — el equivalente en LaraFly de `laravel new`. ### Qué acabas de instalar @@ -60,15 +61,15 @@ El `composer.json` del andamiaje requiere solo dos paquetes de Firefly directame "require": { "php": "^8.3", "firefly/cli": "*@dev", - "firefly/firefly": "*@dev", + "fireflyframework/larafly": "*@dev", "laravel/framework": "^13.0" } ``` -`firefly/cli` te da los comandos `artisan firefly:*` que usarás a lo largo de este libro. `firefly/firefly` es el **metapaquete de tiempo de ejecución** — el análogo en Composer de una lista de materiales (BOM) de Maven — que arrastra toda la familia Firefly (contenedor, contexto, configuración, web, datos, cqrs, eda, seguridad, validación, resiliencia, programación, observabilidad, actuator y más) en una sola línea `require`, de modo que tu propio `composer.json` nunca tiene que enumerarlos uno a uno. +`firefly/cli` te da los comandos `artisan firefly:*` que usarás a lo largo de este libro. `fireflyframework/larafly` es el **biblioteca completa del framework**, que contiene toda la familia Firefly (contenedor, contexto, configuración, web, datos, cqrs, eda, seguridad, validación, resiliencia, programación, observabilidad, actuator y más) en una sola línea `require`, de modo que tu propio `composer.json` nunca tiene que enumerarlos uno a uno. !!! laravel "Paridad con Laravel" - `composer create-project firefly/skeleton` es el equivalente en LaraFly de `laravel new` — y `firefly/firefly` es el equivalente de instalar el propio `laravel/framework`: una línea de dependencia que trae una pila completa y coherente en lugar de una colección de piezas versionadas de forma independiente. + `firefly new` es el equivalente en LaraFly de `laravel new` — y `fireflyframework/larafly` es el equivalente de instalar el propio `laravel/framework`: una línea de dependencia que trae una pila completa y coherente en lugar de una colección de piezas versionadas de forma independiente. --- @@ -87,7 +88,7 @@ declare(strict_types=1); return []; ``` -No hay ningún proveedor de servicios que registrar a mano. Los propios proveedores de `firefly/cli` y `firefly/firefly` se descubren automáticamente mediante el descubrimiento de paquetes de Composer/Laravel — nunca añades una línea aquí por un paquete de Firefly. +No hay ningún proveedor de servicios que registrar a mano. Los propios proveedores de `firefly/cli` y `fireflyframework/larafly` se descubren automáticamente mediante el descubrimiento de paquetes de Composer/Laravel — nunca añades una línea aquí por un paquete de Firefly. Segundo, `routes/web.php` también está casi vacío: diff --git a/book/src/00-quickstart.md b/book/src/00-quickstart.md index 3c4eb01d..65cc842e 100644 --- a/book/src/00-quickstart.md +++ b/book/src/00-quickstart.md @@ -32,7 +32,8 @@ composer --version The fastest way to start a new LaraFly application is `composer create-project`, pointed at the `firefly/skeleton` template: ```bash -composer create-project firefly/skeleton my-app +composer global require fireflyframework/larafly +firefly new my-app cd my-app ``` @@ -50,7 +51,7 @@ cd my-app By the time that command returns, `.env` exists, a SQLite database file has been touched, `APP_KEY` is set, and — the step that matters most for this book — **`firefly:cache` has already compiled your application's manifests**. You have not written a single line of PHP yet, and the zero-reflection boot path this whole framework is built around is already in place. !!! tip "A global installer, if you prefer it" - `composer global require firefly/installer` gives you a `firefly` command on your `PATH`. `firefly new my-app` wraps the same `composer create-project firefly/skeleton` call, then runs `git init` and an initial commit for you — the LaraFly equivalent of `laravel new`. + `composer global require fireflyframework/larafly` gives you a `firefly` command on your `PATH`. `firefly new my-app` supplies the bundled skeleton to Composer, then runs `git init` and an initial commit for you — the LaraFly equivalent of `laravel new`. ### What you just installed @@ -60,15 +61,15 @@ The skeleton's `composer.json` requires only two Firefly packages directly: "require": { "php": "^8.3", "firefly/cli": "*@dev", - "firefly/firefly": "*@dev", + "fireflyframework/larafly": "*@dev", "laravel/framework": "^13.0" } ``` -`firefly/cli` gives you the `artisan firefly:*` commands you will use throughout this book. `firefly/firefly` is the **runtime metapackage** — the Composer analogue of a Maven BOM — that pulls in the whole Firefly family (container, context, config, web, data, cqrs, eda, security, validation, resilience, scheduling, observability, actuator, and more) in a single `require` line, so your own `composer.json` never has to enumerate them one at a time. +`firefly/cli` gives you the `artisan firefly:*` commands you will use throughout this book. `fireflyframework/larafly` is the **complete framework library**, which contains the whole Firefly family (container, context, config, web, data, cqrs, eda, security, validation, resilience, scheduling, observability, actuator, and more) in a single `require` line, so your own `composer.json` never has to enumerate them one at a time. !!! laravel "Laravel parity" - `composer create-project firefly/skeleton` is LaraFly's counterpart to `laravel new` — and `firefly/firefly` is the counterpart of installing `laravel/framework` itself: one dependency line that brings in an entire, coherent stack rather than a collection of independently-versioned pieces. + `firefly new` is LaraFly's counterpart to `laravel new` — and `fireflyframework/larafly` is the counterpart of installing `laravel/framework` itself: one dependency line that brings in an entire, coherent stack rather than a collection of independently-versioned pieces. --- @@ -87,7 +88,7 @@ declare(strict_types=1); return []; ``` -There is no service provider to register by hand. `firefly/cli` and `firefly/firefly`'s own providers are discovered automatically by Composer/Laravel package discovery — you never add a line here for a Firefly package. +There is no service provider to register by hand. `firefly/cli` and `fireflyframework/larafly`'s own providers are discovered automatically by Composer/Laravel package discovery — you never add a line here for a Firefly package. Second, `routes/web.php` is almost empty too: diff --git a/composer.json b/composer.json index f9a7dac5..b42a99b2 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "firefly/firefly", + "name": "fireflyframework/larafly", "description": "LaraFly — the Firefly Framework for PHP.", "type": "library", "license": "Apache-2.0", @@ -130,6 +130,7 @@ "firefly/eda-kafka": "self.version", "firefly/eda-postgres": "self.version", "firefly/eda-rabbitmq": "self.version", + "firefly/firefly": "self.version", "firefly/installer": "self.version", "firefly/kernel": "self.version", "firefly/messaging": "self.version", diff --git a/docs/contributing.md b/docs/contributing.md index a3d1cf70..351a12ea 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -2,7 +2,7 @@ ## Monorepo layout -`fireflyframework-php` publishes the root `firefly/firefly` library. All component code stays under +`fireflyframework-php` publishes the root `fireflyframework/larafly` library. All component code stays under `packages/*`; their manifests document module requirements and namespaces and are validated together. They are not published or installed separately. `skeleton/` is the bundled application template used by `firefly new`. `samples/lumen` consumes the root through a copied path repository. diff --git a/docs/getting-started.md b/docs/getting-started.md index ecf62785..1a063fd4 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -7,7 +7,7 @@ application pre-wired with the Firefly family, a `#[Controller]` welcome page, a `#[RestController]`/`#[Service]` pair, and `firefly:cache` already wired into `post-create-project-cmd`: ```bash -composer global require firefly/firefly +composer global require fireflyframework/larafly firefly new my-app cd my-app php artisan firefly:cache @@ -29,7 +29,7 @@ installed) — see [CLI](cli.md) for the full command reference. Install the complete framework library, including the CLI, dashboard and API documentation: ```bash -composer require firefly/firefly +composer require fireflyframework/larafly ``` All component code is included. Optional adapters are activated by configuration and may require a PHP @@ -55,10 +55,10 @@ php artisan firefly:cache php artisan firefly:serve ``` -## The two packages the skeleton requires +## The library and component requirement `firefly/skeleton` is itself a `type: project` create-project template rather than something you require, and -its `composer.json` asks for exactly two Firefly packages beside `php: ^8.3` and `laravel/framework: ^13.0`: +its `composer.json` asks for the library and a CLI component requirement beside `php: ^8.3` and `laravel/framework: ^13.0`: @@ -66,7 +66,7 @@ its `composer.json` asks for exactly two Firefly packages beside `php: ^8.3` and "require": { "php": "^8.3", "firefly/cli": "*@dev", - "firefly/firefly": "*@dev", + "fireflyframework/larafly": "*@dev", "laravel/framework": "^13.0" }, ``` @@ -74,7 +74,7 @@ its `composer.json` asks for exactly two Firefly packages beside `php: ^8.3` and - **`firefly/cli`** — the developer-experience console: `firefly:cache`/`:clear`, actuator-over-CLI `firefly:about`/`:routes`/`:health`/`:metrics`, `firefly:oauth2:keys`, the `make:firefly-*` generator family, and thin `firefly:serve`/`:schedule`/`:db` passthroughs. See [CLI](cli.md). -- **`firefly/firefly`** — the complete `type: library` distribution. It supplies `firefly/cli` through +- **`fireflyframework/larafly`** — the complete `type: library` distribution. It supplies `firefly/cli` through `replace`, so both requirements resolve to the same installation. The template's CLI requirement explicitly records its use of `firefly:cache`. diff --git a/docs/installation.md b/docs/installation.md index f9c24181..a2cbba99 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -12,7 +12,7 @@ The bundled `firefly new` installer supplies the application template to Composer and prepares the app: ```bash -composer global require firefly/firefly +composer global require fireflyframework/larafly firefly new my-app cd my-app php artisan firefly:serve @@ -29,7 +29,7 @@ off `NewCommand::configure()`: | `-f`, `--force` | **Empties the target directory first**, then scaffolds into it. | | `--git` / `--no-git` | Whether to `git init` and make an initial commit. On by default. | -Every component's code is included in `firefly/firefly`. `--with` records an explicit component +Every component's code is included in `fireflyframework/larafly`. `--with` records an explicit component requirement in the generated manifest; it does not fetch a separate framework package. Configuration selects the active transport. `eda-postgres` needs `ext-pdo_pgsql`, `eda-kafka` needs `ext-rdkafka`, `eda-rabbitmq` uses the included AMQP client, and `scheduling-postgres` needs a PostgreSQL connection. @@ -46,7 +46,7 @@ cd my-app php artisan firefly:serve ``` -The generated application installs `firefly/firefly` from Packagist; before first publication, supply a +The generated application installs `fireflyframework/larafly` from Packagist; before first publication, supply a root VCS or path repository as described in [Publishing](publishing.md). `firefly/skeleton`'s `composer.json` wires `post-create-project-cmd` to run automatically, so by the time the @@ -61,7 +61,7 @@ command above finishes you already have, in this order: ## What the template requires -`skeleton/composer.json` requires PHP, Laravel, `firefly/firefly` and `firefly/cli`. The library supplies +`skeleton/composer.json` requires PHP, Laravel, `fireflyframework/larafly` and `firefly/cli`. The library supplies all framework code; the CLI requirement is satisfied by `replace`. Providers are discovered from the root package's `extra.laravel.providers` metadata. diff --git a/docs/modules.md b/docs/modules.md index 184286cc..ab1208ac 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -1,6 +1,6 @@ # Modules -LaraFly publishes one `firefly/firefly` library containing the **29 component directories** under +LaraFly publishes one `fireflyframework/larafly` library containing the **29 component directories** under `packages/*`, each with its own tests or metadata. The **32 guides** below explain their public surfaces. Several components carry more than one guide: `firefly/data` answers for *Data & Repositories*, *Relational Data* and *Transactions*. Laravel discovers the root library's providers, and configuration diff --git a/docs/modules/installer.md b/docs/modules/installer.md index e0b16e63..a8d8f77e 100644 --- a/docs/modules/installer.md +++ b/docs/modules/installer.md @@ -1,12 +1,12 @@ # Installer -The installer is bundled in the `firefly/firefly` library. Its `firefly new` Symfony Console binary +The installer is bundled in the `fireflyframework/larafly` library. Its `firefly new` Symfony Console binary uses the skeleton shipped in that same distribution, so creating an application needs no skeleton mirror. ## `firefly new ` ```bash -composer global require firefly/firefly +composer global require fireflyframework/larafly firefly new my-app ``` diff --git a/docs/publishing.md b/docs/publishing.md index 2c262c90..74b35a68 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -1,26 +1,27 @@ # Publishing LaraFly -LaraFly publishes **one library, `firefly/firefly`**, from this repository's root `composer.json`. +LaraFly publishes **one library, `fireflyframework/larafly`**, from this repository's root `composer.json`. Its tagged archive contains every component's code, configuration, migrations, views, compiled manifests, the `firefly` installer binary and the application skeleton. No component mirror repositories or cross-repository `ACCESS_TOKEN` are needed. ## Package identity and compatibility -The root package replaces the 28 other component names with `self.version`. Together with its own -`firefly/firefly` name, that covers the 29 former packages. `firefly/lumen` is a sample, not a replacement; +The root package replaces all 29 former component names, including `firefly/firefly`, with +`self.version`. Packagist's `firefly` vendor namespace belongs to another publisher, so the public +package uses the Firefly Framework organization's namespace. `firefly/lumen` is a sample, not a replacement; `firefly/skeleton` is a bundled project template, not a second published package. Consumers first require the provider package: ```bash -composer require firefly/firefly +composer require fireflyframework/larafly composer require firefly/eda-kafka ``` The second requirement is satisfied by the installed framework at a compatible version. Composer does not automatically discover an unknown provider from a replacement name alone: an empty project must -require `firefly/firefly` explicitly. `self.version` prevents a framework from satisfying component +require `fireflyframework/larafly` explicitly. `self.version` prevents a framework from satisfying component constraints from another release line. See [Composer's replace documentation](https://getcomposer.org/doc/04-schema.md#replace). The component manifests under `packages/*` remain internal dependency and namespace descriptors. @@ -62,14 +63,21 @@ CI runs these package checks on PHP 8.3, 8.4 and 8.5 for PRs to `main` and pushe 2. Update the framework version, README version badge and changelog together, then run the gates again. 3. Tag that release commit with its new `vYY.MM.Patch` version. Do not move an existing tag. Tags through `v26.09.8` describe the old development aggregator and cannot serve as this new library. -4. Register `firefly/firefly` on Packagist using +4. Register `fireflyframework/larafly` on Packagist using `https://github.com/fireflyframework/fireflyframework-php` and enable the repository webhook. This is a one-time publisher action; subsequent releases use tags from this same repository. -5. Push the new release tag. **Release (single package)** validates the tagged distribution with read-only - repository permissions. A green workflow proves the archive checks passed; it does not prove Packagist - has indexed the tag. -6. Confirm the exact new version is visible on Packagist and install it in a fresh project with default - stable resolution, no custom repositories, and matching `firefly/firefly` and `firefly/eda-kafka` - constraints. Publication is complete only when that install succeeds. +5. Push the new release tag. **Release (single package)** validates the tagged distribution on PHP + 8.3, 8.4 and 8.5, waits for Packagist to index that exact commit, then installs it in a fresh stable + consumer without custom repositories and exercises the bundled installer. Only after those checks + pass does its final job create the GitHub release from the changelog. That job uses the repository's + built-in `GITHUB_TOKEN` with `contents: write`; validation jobs remain read-only. +6. If indexing times out, repair the Packagist webhook or trigger an update on the package page, then + rerun the failed job. Do not move the tag or create a GitHub release to bypass the public install gate. + +To repeat the public install check locally: + +```bash +RELEASE_TAG=v26.09.9 RELEASE_SHA="$(git rev-parse 'v26.09.9^{commit}')" php scripts/check-package-install.php --published +``` An unmerged branch or a local consumer check is not a published release. diff --git a/docs/versioning.md b/docs/versioning.md index 5957c4ca..09d57d5c 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -9,7 +9,7 @@ in July 2026. A new patch bumps the trailing number; the first release of a new ## No `version` field -No `composer.json` in this monorepo — not the aggregator, not any `packages/*/composer.json` — carries a +No `composer.json` in this monorepo — not the root library, not any `packages/*/composer.json` — carries a `version` key. Packagist derives a package's version from the **git tag** at publish time (and from the `extra.branch-alias` entry for dev-branch resolution — see [Constraints](#constraints) below); hand-writing a `version` field would just be a second, driftable source of truth. @@ -21,7 +21,7 @@ The single place the current version *is* asserted in code is: ```php final class Version { - public const string VERSION = '26.09.8'; + public const string VERSION = '26.09.9'; } ``` @@ -62,14 +62,13 @@ Application `composer.json` files depend on Firefly packages with a constraint a ```json { "require": { - "firefly/firefly": "^26.09" + "fireflyframework/larafly": "^26.09" } } ``` -`^26.09` is the constraint the release runbook writes into every package's sibling requirements -(`monorepo-builder bump-interdependency`, see [Publishing](publishing.md)) — and it is the **widest** of the -three shapes below, not the narrowest. Composer normalises `26.09` to `26.09.0.0` and expands a caret to "up +`^26.09` is the **widest** of the three shapes below. The published root library replaces component +names with `self.version`; application constraints choose which framework releases to accept. Composer normalises `26.09` to `26.09.0.0` and expands a caret to "up to the next major", so `^26.09` accepts `26.10.x`, `26.12.x` and every other line released in the `26` year. That matters more under CalVer than it would under semver, because a CalVer number carries no promise that a bump in anything but the last segment is backward compatible — a month bump is exactly where an incompatible diff --git a/packages/cli/tests/Skeleton/CreateProjectOfflineTest.php b/packages/cli/tests/Skeleton/CreateProjectOfflineTest.php index 024c17b9..97622f86 100644 --- a/packages/cli/tests/Skeleton/CreateProjectOfflineTest.php +++ b/packages/cli/tests/Skeleton/CreateProjectOfflineTest.php @@ -80,7 +80,7 @@ try { expect($process->isSuccessful())->toBeTrue($process->getOutput().$process->getErrorOutput()) ->and(is_file($target.'/artisan'))->toBeTrue() // BLOCKER-2: runnable app - ->and(is_dir($target.'/vendor/firefly/firefly/packages/web'))->toBeTrue() // firefly family installed + ->and(is_dir($target.'/vendor/fireflyframework/larafly/packages/web'))->toBeTrue() // firefly family installed ->and(is_file($target.'/bootstrap/cache/firefly/component.php'))->toBeTrue() // post-create firefly:cache ran ->and(is_file($target.'/bootstrap/cache/firefly/context.php'))->toBeTrue() ->and(is_file($target.'/bootstrap/cache/firefly/routes.php'))->toBeTrue(); // the sample #[RestController] compiled diff --git a/packages/firefly/README.md b/packages/firefly/README.md index 8e68afd8..ba83ec9b 100644 --- a/packages/firefly/README.md +++ b/packages/firefly/README.md @@ -1,11 +1,11 @@ # firefly/firefly -LaraFly publishes the repository root as the `firefly/firefly` library, containing all framework +LaraFly publishes the repository root as the `fireflyframework/larafly` library, containing all framework components, the installer and the application skeleton. The root's `replace` entries satisfy compatible component requirements without split repositories. ```bash -composer require firefly/firefly +composer require fireflyframework/larafly ``` This directory retains the former runtime dependency list as internal module metadata. It is not the diff --git a/packages/installer/README.md b/packages/installer/README.md index 73a57438..caa6e131 100644 --- a/packages/installer/README.md +++ b/packages/installer/README.md @@ -4,7 +4,7 @@ The LaraFly global installer — the `laravel/installer` analog, with a Spring-I picker. ```bash -composer global require firefly/firefly +composer global require fireflyframework/larafly firefly new my-app ``` diff --git a/packages/installer/src/ArchetypeApplier.php b/packages/installer/src/ArchetypeApplier.php index 3672b8e3..1782dd2e 100644 --- a/packages/installer/src/ArchetypeApplier.php +++ b/packages/installer/src/ArchetypeApplier.php @@ -187,7 +187,7 @@ private function invalidateCompiledManifests(string $directory): array * Read from the generated manifest rather than hard-coded, because the right answer changes over the * * project's life: the skeleton pins `*@dev` while the family is pre-Packagist and will pin `^26.0` after - * the first tagged release. Copying whatever firefly/firefly (or, failing that, any firefly/* package) + * the first tagged release. Copying whatever fireflyframework/larafly (or, failing that, any firefly/* package) * is already pinned to means the added lines always agree with the ones the skeleton shipped, and this * file never has to be edited for a release. * @@ -195,8 +195,8 @@ private function invalidateCompiledManifests(string $directory): array */ private function fireflyConstraint(array $require): string { - if (isset($require['firefly/firefly'])) { - return $require['firefly/firefly']; + if (isset($require['fireflyframework/larafly'])) { + return $require['fireflyframework/larafly']; } foreach ($require as $package => $version) { if (str_starts_with($package, 'firefly/')) { diff --git a/packages/installer/src/CapabilityCatalog.php b/packages/installer/src/CapabilityCatalog.php index c48f9931..3363f68d 100644 --- a/packages/installer/src/CapabilityCatalog.php +++ b/packages/installer/src/CapabilityCatalog.php @@ -76,7 +76,7 @@ public static function corePackages(): array 'firefly/autoconfigure' => 'the conditional auto-configuration engine', 'firefly/web' => 'the HTTP layer; both the api and web archetypes route through it', 'firefly/cli' => 'the developer console the skeleton already requires directly', - 'firefly/firefly' => 'the root library containing every component', + 'firefly/firefly' => 'the former runtime metapackage, replaced by fireflyframework/larafly', 'firefly/installer' => 'this tool', ]; } diff --git a/packages/installer/tests/ArchetypeTest.php b/packages/installer/tests/ArchetypeTest.php index 979dc96b..4aa996d4 100644 --- a/packages/installer/tests/ArchetypeTest.php +++ b/packages/installer/tests/ArchetypeTest.php @@ -132,17 +132,17 @@ function cleanUp(string $dir): void $require = Skeleton::requirements($dir); expect($require)->toHaveKey('firefly/security')->toHaveKey('firefly/eda') - // the constraint is copied off firefly/firefly, so it tracks the skeleton across releases + // the constraint is copied off fireflyframework/larafly, so it tracks the skeleton across releases // instead of pinning a literal that goes stale the day the family reaches 1.0 - ->and($require['firefly/security'])->toBe($require['firefly/firefly']) - ->and($require['firefly/eda'])->toBe($require['firefly/firefly']) + ->and($require['firefly/security'])->toBe($require['fireflyframework/larafly']) + ->and($require['firefly/eda'])->toBe($require['fireflyframework/larafly']) // nothing else crept in ->and(array_values(array_filter(array_keys($require), fn (string $p): bool => str_starts_with($p, 'firefly/')))) - ->toBe(['firefly/cli', 'firefly/eda', 'firefly/firefly', 'firefly/security']) + ->toBe(['firefly/cli', 'firefly/eda', 'firefly/security']) ->and(Skeleton::stamp($dir)['capabilities'])->toBe(['security', 'eda']); // composer's sort-packages ordering: platform first, then natural case-insensitive name. - expect(array_keys($require))->toBe(['php', 'firefly/cli', 'firefly/eda', 'firefly/firefly', 'firefly/security', 'laravel/framework']); + expect(array_keys($require))->toBe(['php', 'firefly/cli', 'firefly/eda', 'firefly/security', 'fireflyframework/larafly', 'laravel/framework']); } finally { cleanUp($dir); } diff --git a/packages/kernel/src/Version.php b/packages/kernel/src/Version.php index 749dc8c1..50920bf1 100644 --- a/packages/kernel/src/Version.php +++ b/packages/kernel/src/Version.php @@ -15,5 +15,5 @@ */ final class Version { - public const string VERSION = '26.09.8'; + public const string VERSION = '26.09.9'; } diff --git a/samples/lumen/composer.json b/samples/lumen/composer.json index a72cf147..f4457d1f 100644 --- a/samples/lumen/composer.json +++ b/samples/lumen/composer.json @@ -32,9 +32,9 @@ "firefly/security": "*@dev", "firefly/validation": "*@dev", "firefly/web": "*@dev", + "fireflyframework/larafly": "*@dev", "illuminate/database": "^13.0", - "illuminate/support": "^13.0", - "firefly/firefly": "*@dev" + "illuminate/support": "^13.0" }, "require-dev": { "firefly/eda-postgres": "*@dev", diff --git a/scripts/check-package-install.php b/scripts/check-package-install.php index f407e65e..07666ea7 100644 --- a/scripts/check-package-install.php +++ b/scripts/check-package-install.php @@ -7,6 +7,12 @@ require dirname(__DIR__).'/vendor/autoload.php'; $root = dirname(__DIR__); +$published = in_array('--published', $argv, true); +$version = $published ? getenv('RELEASE_TAG') : '26.9.99'; +$reference = $published ? getenv('RELEASE_SHA') : null; +if ($published && (! is_string($version) || ! preg_match('/^v\d{2}\.\d{2}\.\d+$/', $version) || ! is_string($reference) || ! preg_match('/^[a-f0-9]{40}$/', $reference))) { + throw new InvalidArgumentException('Published verification needs RELEASE_TAG=vYY.MM.Patch and RELEASE_SHA=.'); +} $work = sys_get_temp_dir().'/larafly-package-'.bin2hex(random_bytes(6)); mkdir($work, 0755, true); @@ -32,16 +38,18 @@ function writePackageJson(string $path, array $value): void try { $manifest = json_decode(file_get_contents($root.'/composer.json'), true, flags: JSON_THROW_ON_ERROR); - packageAssert($manifest['name'] === 'firefly/firefly', 'The repository root must publish firefly/firefly.'); - runPackageCommand(['composer', 'archive', '--format=zip', '--dir='.$work, '--file=firefly'], $root); - $manifest['version'] = '26.9.99'; - $manifest['dist'] = ['type' => 'zip', 'url' => $work.'/firefly.zip']; - unset($manifest['source']); + packageAssert($manifest['name'] === 'fireflyframework/larafly', 'The repository root must publish fireflyframework/larafly.'); + if (! $published) { + runPackageCommand(['composer', 'archive', '--format=zip', '--dir='.$work, '--file=firefly'], $root); + $manifest['version'] = $version; + $manifest['dist'] = ['type' => 'zip', 'url' => $work.'/firefly.zip']; + unset($manifest['source']); + } // Only this distribution supplies firefly/*; Packagist is used for third-party dependencies. - $repositories = [ + $repositories = $published ? [] : [ ['type' => 'package', 'package' => $manifest], - ['type' => 'composer', 'url' => 'https://repo.packagist.org', 'exclude' => ['firefly/*']], + ['type' => 'composer', 'url' => 'https://repo.packagist.org', 'exclude' => ['firefly/*', 'fireflyframework/larafly']], ['packagist.org' => false], ]; $app = $work.'/app'; @@ -53,30 +61,35 @@ function writePackageJson(string $path, array $value): void } $consumer = json_decode(file_get_contents($app.'/composer.json'), true, flags: JSON_THROW_ON_ERROR); $consumer['repositories'] = $repositories; - $consumer['require']['firefly/firefly'] = '26.9.99'; + $consumer['require']['fireflyframework/larafly'] = $version; // Every former component must resolve to the same installed library. foreach (array_keys($manifest['replace']) as $name) { - $consumer['require'][$name] = '^26.9'; + $consumer['require'][$name] = $version; } unset($consumer['require-dev'], $consumer['minimum-stability']); writePackageJson($app.'/composer.json', $consumer); runPackageCommand(['composer', 'update', '--no-dev', '--prefer-dist', '--no-scripts', '--no-progress'], $app); $installed = json_decode(file_get_contents($app.'/vendor/composer/installed.json'), true, flags: JSON_THROW_ON_ERROR)['packages']; $names = array_column($installed, 'name'); - packageAssert(array_values(array_filter($names, fn ($name) => str_starts_with($name, 'firefly/'))) === ['firefly/firefly'], 'A component was installed separately.'); + packageAssert(in_array('fireflyframework/larafly', $names, true), 'The root library was not installed.'); + packageAssert(array_filter($names, fn ($name) => str_starts_with($name, 'firefly/')) === [], 'A component was installed separately.'); + if ($published) { + $library = array_values(array_filter($installed, fn ($package) => $package['name'] === 'fireflyframework/larafly'))[0]; + packageAssert(($library['source']['reference'] ?? null) === $reference, 'Packagist installed a different commit than the release tag.'); + } foreach (['orchestra/testbench', 'pestphp/pest', 'phpunit/phpunit'] as $dev) { packageAssert(! in_array($dev, $names, true), $dev.' leaked into a production install.'); } - packageAssert(! is_link($app.'/vendor/firefly/firefly'), 'Consumer must use copied sources.'); + packageAssert(! is_link($app.'/vendor/fireflyframework/larafly'), 'Consumer must use copied sources.'); copy($app.'/.env.example', $app.'/.env'); touch($app.'/database/database.sqlite'); foreach ([['package:discover'], ['key:generate'], ['migrate', '--force'], ['firefly:cache']] as $arguments) { runPackageCommand([PHP_BINARY, 'artisan', ...$arguments], $app); } runPackageCommand([PHP_BINARY, 'vendor/bin/firefly', '--help'], $app); - runPackageCommand([PHP_BINARY, 'vendor/firefly/firefly/packages/installer/bin/firefly', '--help'], $app); - packageAssert(! is_dir($app.'/vendor/firefly/firefly/vendor'), 'Development vendor directory leaked into the archive.'); - packageAssert(! is_dir($app.'/vendor/firefly/firefly/tests'), 'Repository tests leaked into the archive.'); + runPackageCommand([PHP_BINARY, 'vendor/fireflyframework/larafly/packages/installer/bin/firefly', '--help'], $app); + packageAssert(! is_dir($app.'/vendor/fireflyframework/larafly/vendor'), 'Development vendor directory leaked into the archive.'); + packageAssert(! is_dir($app.'/vendor/fireflyframework/larafly/tests'), 'Repository tests leaked into the archive.'); file_put_contents($app.'/probe.php', <<<'PROBE' 'path', 'url' => $root, 'options' => ['symlink' => false, 'versions' => ['firefly/firefly' => '26.9.99']]], - $repositories[1], $repositories[2], + ['type' => 'path', 'url' => $root, 'options' => ['symlink' => false, 'versions' => ['fireflyframework/larafly' => '26.9.99']]], + ['type' => 'composer', 'url' => 'https://repo.packagist.org', 'exclude' => ['firefly/*', 'fireflyframework/larafly']], + ['packagist.org' => false], ]; // Source/test namespaces in the sample remain relative to the sample project. $sample['autoload']['psr-4']['Lumen\\'] = $root.'/samples/lumen/src/'; writePackageJson($lumen.'/composer.json', $sample); runPackageCommand(['composer', 'update', '--no-dev', '--prefer-dist', '--no-scripts', '--no-progress'], $lumen); $sampleInstalled = json_decode(file_get_contents($lumen.'/vendor/composer/installed.json'), true, flags: JSON_THROW_ON_ERROR)['packages']; - packageAssert(array_values(array_filter(array_column($sampleInstalled, 'name'), fn ($name) => str_starts_with($name, 'firefly/'))) === ['firefly/firefly'], 'Lumen installed a split package.'); - packageAssert(! is_link($lumen.'/vendor/firefly/firefly'), 'Lumen must exercise copied path sources.'); + packageAssert(array_filter(array_column($sampleInstalled, 'name'), fn ($name) => str_starts_with($name, 'firefly/')) === [], 'Lumen installed a split package.'); + packageAssert(! is_link($lumen.'/vendor/fireflyframework/larafly'), 'Lumen must exercise copied path sources.'); + foreach (['vendor', 'node_modules', '.venv-docs', 'tests'] as $developmentDirectory) { + packageAssert(! is_dir($lumen.'/vendor/fireflyframework/larafly/'.$developmentDirectory), 'Copied library includes development directory '.$developmentDirectory.'.'); + } runPackageCommand([PHP_BINARY, '-r', 'require "vendor/autoload.php"; if (! class_exists("Lumen\\Domain\\Money")) { exit(1); }'], $lumen); $consumer['require']['firefly/eda-kafka'] = '^27.0'; diff --git a/skeleton/README.md b/skeleton/README.md index a80e34b1..57c49aac 100644 --- a/skeleton/README.md +++ b/skeleton/README.md @@ -1,7 +1,7 @@ # LaraFly Skeleton A [Laravel 13](https://laravel.com) application skeleton pre-wired with the **Firefly** framework family -(`firefly/firefly` + `firefly/cli`). It ships a sample slice — a `#[Controller]` welcome page, a +(`fireflyframework/larafly` + `firefly/cli`). It ships a sample slice — a `#[Controller]` welcome page, a `#[RestController]`, a `#[Service]`, and a `#[ConfigProperties]` DTO — plus the `firefly:cache` compile step in `post-create-project-cmd`, so a freshly created app boots on the zero-reflection cached path with **zero external infrastructure** (sqlite + array/sync drivers by default). diff --git a/skeleton/app/Http/WelcomeController.php b/skeleton/app/Http/WelcomeController.php index bbefc0cf..02d53fb8 100644 --- a/skeleton/app/Http/WelcomeController.php +++ b/skeleton/app/Http/WelcomeController.php @@ -55,7 +55,7 @@ public function index(): View 'debug' => $this->config->bool('app.debug', false), 'phpVersion' => PHP_VERSION, 'laravelVersion' => $this->packageVersion('laravel/framework'), - 'fireflyVersion' => $this->packageVersion('firefly/firefly'), + 'fireflyVersion' => $this->packageVersion('fireflyframework/larafly'), 'bootMode' => AppScan::cachedFile($this->container, AppScan::ROUTES) !== null ? 'compiled' : 'scanned', 'phases' => $this->phases(), 'beanCount' => $this->beanCount(), diff --git a/skeleton/composer.json b/skeleton/composer.json index 64412c48..d99a0090 100644 --- a/skeleton/composer.json +++ b/skeleton/composer.json @@ -1,6 +1,6 @@ { "name": "firefly/skeleton", - "description": "LaraFly application skeleton \u2014 a Laravel 13 app pre-wired with the Firefly family, a sample #[RestController]/#[Service], and firefly:cache in post-create-project-cmd.", + "description": "LaraFly application skeleton — a Laravel 13 app pre-wired with the Firefly family, a sample #[RestController]/#[Service], and firefly:cache in post-create-project-cmd.", "type": "project", "license": "Apache-2.0", "homepage": "https://github.com/fireflyframework/fireflyframework-php", @@ -23,7 +23,7 @@ "require": { "php": "^8.3", "firefly/cli": "*@dev", - "firefly/firefly": "*@dev", + "fireflyframework/larafly": "*@dev", "laravel/framework": "^13.0" }, "extra": { diff --git a/tests/BrowserFixtureProvidersTest.php b/tests/BrowserFixtureProvidersTest.php index 2622206e..564b3a36 100644 --- a/tests/BrowserFixtureProvidersTest.php +++ b/tests/BrowserFixtureProvidersTest.php @@ -11,7 +11,7 @@ /** * The browser suite boots the skeleton with the provider set a CREATED app gets from Laravel's package - * discovery — derived from the root manifest rather than typed by hand, so adding a provider to firefly/firefly + * discovery — derived from the root manifest rather than typed by hand, so adding a provider to fireflyframework/larafly * changes the browser fixture without anyone remembering to. This test runs in the default gate (no * browser) and pins the derivation. */ diff --git a/tests/OnePackageTest.php b/tests/OnePackageTest.php index 280073c7..c0cd6138 100644 --- a/tests/OnePackageTest.php +++ b/tests/OnePackageTest.php @@ -5,7 +5,7 @@ it('ships the dependencies, autoloads and discovery metadata required by its component code', function () { /** @var array $root */ $root = json_decode((string) file_get_contents(dirname(__DIR__).'/composer.json'), true, flags: JSON_THROW_ON_ERROR); - expect($root['name'])->toBe('firefly/firefly')->and($root['type'])->toBe('library'); + expect($root['name'])->toBe('fireflyframework/larafly')->and($root['type'])->toBe('library'); /** @var array $replaced */ $replaced = $root['replace']; @@ -19,9 +19,7 @@ foreach (glob(dirname(__DIR__).'/packages/*/composer.json') ?: [] as $file) { /** @var array{name: string, require?: array, autoload?: array{psr-4?: array, files?: list}, extra?: array{laravel?: array{providers?: list}}} $module */ $module = json_decode((string) file_get_contents($file), true, flags: JSON_THROW_ON_ERROR); - if ($module['name'] !== 'firefly/firefly') { - expect($replaced[$module['name']] ?? null)->toBe('self.version'); - } + expect($replaced[$module['name']] ?? null)->toBe('self.version'); foreach ($module['require'] ?? [] as $name => $constraint) { if (str_starts_with($name, 'firefly/') || in_array($name, ['ext-pdo_pgsql', 'orchestra/testbench', 'illuminate/testing'], true)) { continue; diff --git a/tests/ReleaseWorkflowTest.php b/tests/ReleaseWorkflowTest.php index 2a3ca4cb..06ee108b 100644 --- a/tests/ReleaseWorkflowTest.php +++ b/tests/ReleaseWorkflowTest.php @@ -67,7 +67,7 @@ function releaseWorkflowYaml(): array it('validates the tagged single package without cross-repository credentials', function () { $yaml = releaseWorkflowYaml(); $jobs = asYamlMap($yaml['jobs'] ?? null, 'jobs'); - expect(array_keys($jobs))->toBe(['package']); + expect(array_keys($jobs))->toBe(['package', 'publish']); $package = asYamlMap($jobs['package'], 'package'); $steps = asYamlList($package['steps'] ?? null, 'steps'); $commands = []; @@ -84,3 +84,25 @@ function releaseWorkflowYaml(): array $blob = (string) file_get_contents(dirname(__DIR__).'/.github/workflows/release.yml'); expect($blob)->not->toContain('ACCESS_TOKEN', 'monorepo-split', 'git push'); }); + +it('publishes a GitHub release only after the tag installs from Packagist', function () { + $jobs = asYamlMap(releaseWorkflowYaml()['jobs'] ?? null, 'jobs'); + $publish = asYamlMap($jobs['publish'] ?? null, 'publish'); + expect($publish['needs'] ?? null)->toBe('package'); + $steps = asYamlList($publish['steps'] ?? null, 'publish.steps'); + $commands = []; + foreach ($steps as $step) { + $map = asYamlMap($step, 'step'); + if (is_string($map['run'] ?? null)) { + $commands[] = $map['run']; + } + } + $commands = implode("\n", $commands); + expect($commands)->toContain('scripts/check-package-install.php --published', 'gh release create'); + $verifyPosition = strpos($commands, '--published'); + $releasePosition = strpos($commands, 'gh release create'); + if ($verifyPosition === false || $releasePosition === false) { + throw new RuntimeException('The publication steps are missing.'); + } + expect($verifyPosition)->toBeLessThan($releasePosition); +});