diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 30a356ff..ce1e3e37 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -72,26 +72,44 @@ jobs: # have passed and the asset is in place, so downstream registration can # never race the upload — and a failed run leaves no release at all rather # than a published one nobody can download. + # The draft is where this workflow stops. Publishing is left to a person, + # because a release published by the workflow would never reach docker.yaml: + # GitHub does not start workflow runs from events created with GITHUB_TOKEN, + # so `release: published` fires as github-actions[bot] and triggers nothing. + # 2.4.3-beta.2 was published that way and got no registration and no images. + # A human pressing Publish fires the event under their own identity, and the + # existing downstream runs untouched. + # + # prerelease and make_latest are set here rather than at publish time, so the + # release is already correct when that button is pressed — GitHub's publish + # dialog otherwise defaults "Set as the latest release" to checked, which + # would let a 3.0.0 alpha displace 2.4.x. - name: Create the draft release + id: draft uses: softprops/action-gh-release@v3 with: files: InvoiceShelf.zip body_path: /tmp/notes.md draft: true prerelease: ${{ contains(github.ref_name, '-') }} + make_latest: ${{ !contains(github.ref_name, '-') && startsWith(github.ref_name, format('{0}.', env.LATEST_MAJOR)) }} - - name: Publish it + # A draft nobody knows about is no use, so the run ends by saying what was + # built and what to do with it. + - name: Say what to do next env: - GH_TOKEN: ${{ github.token }} TAG: ${{ github.ref_name }} - # GitHub's "Latest release" pointer, gated exactly as docker.yaml gates - # its moving image tags — so a 3.0.0 alpha cannot displace 2.4.x as the - # release users are shown first while 2.x is still the stable line. + URL: ${{ steps.draft.outputs.url }} + PRERELEASE: ${{ contains(github.ref_name, '-') }} IS_LATEST: ${{ !contains(github.ref_name, '-') && startsWith(github.ref_name, format('{0}.', env.LATEST_MAJOR)) }} run: | - if [ "$IS_LATEST" = "true" ]; then - gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --draft=false --latest - else - gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --draft=false --latest=false - fi - echo "Published $TAG (latest=$IS_LATEST)" + { + echo "### $TAG is drafted and ready to publish" + echo + echo "- Draft: $URL" + echo "- Pre-release: \`$PRERELEASE\` — a pre-release goes to the insider channel only" + echo "- Mark as latest: \`$IS_LATEST\`" + echo + echo "**Publishing it** builds the Docker images and registers it on the updater." + echo "Nothing reaches installs until you do." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/AGENTS.md b/AGENTS.md index f6af739f..1c7f0408 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -153,6 +153,42 @@ InvoiceShelf follows TDD development style: - Run `vendor/bin/pint --dirty --format agent` after modifying PHP files - After editing `lang/en.json` or any file under `resources/scripts/`, rebuild via `pnpm build` — the bundled chunks (including locale chunks) are content-hashed by Vite, so the browser will pick them up on hard refresh +## Releasing + +Releases are cut by pushing a tag. Nothing is typed into GitHub by hand. + +```bash +# 1. Add a "## " section to CHANGELOG.md and bump version.md, +# in a PR like any other change — the notes are reviewed with the code. +# 2. Once merged, tag the merge commit: +git tag 3.0.0-alpha.2 && git push origin 3.0.0-alpha.2 +``` + +`release.yaml` then runs the tests, reads the `CHANGELOG.md` section for that tag, +builds the package with `make clean dist`, and creates a **draft** release with the +zip attached. It stops there and prints the draft URL in the run summary. + +**You publish the draft yourself.** That is deliberate, not an omission: GitHub does +not start workflow runs from events created with `GITHUB_TOKEN`, so a release +published by the workflow reaches nothing downstream. Pressing Publish fires +`release: published` under your own identity, which triggers `docker.yaml` to +register the release on the updater and build the Docker images. **Until you +publish, no install is offered anything.** + +Notes on the mechanics: + +- **A tag with no `CHANGELOG.md` section fails the run** before anything is built, + so a release can never go out with empty notes. +- **`prerelease` and "mark as latest" are set on the draft**, derived from the tag: a + `-` suffix (`3.0.0-alpha.2`) means pre-release, which routes it to the **insider** + channel so ordinary installs are not offered it. "Latest" is gated on + `LATEST_MAJOR` in the workflows — bump it there when 3.x becomes the stable line. +- **If registration fails**, re-run it without cutting a new release: run the + `Docker Build and Push` workflow manually with `register_tag` set to the version. + That path is idempotent and does not rebuild the Docker images. +- `.github/scripts/changelog-section.php ` prints what the updater will be + sent, so you can check the notes locally before tagging. + ## CI Pipeline GitHub Actions (`check.yaml`): runs Pint style check, then runs Pest tests in parallel (`php artisan test --parallel`) on PHP 8.4 with Xdebug disabled (`coverage: none`). The test job does **not** build the frontend — the suite is API/JSON only and never renders the Vite blade, so no Node/Vite step is needed (release/docker workflows still build assets in their own jobs).