diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..6e8a37a --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "enabledPlugins": { + "release-notes@ReleaseTools": true + }, + "extraKnownMarketplaces": { + "ReleaseTools": { + "source": { + "source": "github", + "repo": "releasetools/agent-plugins" + } + } + } +} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0406e60..6c42fc6 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -27,11 +27,19 @@ jobs: run: make all if: startsWith(github.ref, 'refs/tags/') + # What somebody wrote for this version, not a list of commit subjects. Run before + # the release step, so a tag whose version has no changelog section fails here + # rather than publishing an empty description. + - name: Read the changelog entry + if: startsWith(github.ref, 'refs/tags/') + run: | + bash dist/releasetools.bash changelog::section "${GITHUB_REF_NAME#v}" >RELEASE_NOTES.md + - name: Release GHA uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3 if: startsWith(github.ref, 'refs/tags/') with: - generate_release_notes: true + body_path: RELEASE_NOTES.md make_latest: true files: | dist/releasetools.bash diff --git a/.github/workflows/test-release.yaml b/.github/workflows/test-release.yaml index 817ee64..1219b96 100644 --- a/.github/workflows/test-release.yaml +++ b/.github/workflows/test-release.yaml @@ -10,13 +10,20 @@ jobs: name: Wait for Release to complete runs-on: ubuntu-latest steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + # Built from the tag under test rather than installed through + # 'uses: releasetools/cli@v0'. That action downloads the release this job is waiting + # for, which by definition does not exist yet. + - name: Build the dist + run: make all + - name: Wait for release - uses: lewagon/wait-on-check-action@369769072fe522a3a8a85c03c96af1e5242a1994 # v1.9.1 - with: - ref: ${{ github.ref }} - check-name: "Release GHA" - repo-token: ${{ secrets.GITHUB_TOKEN }} - wait-interval: 5 + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: bash dist/releasetools.bash github::await_workflow "$GITHUB_SHA" release.yaml test-default: name: Test simple install diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8e63aa6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,54 @@ +# Changelog + +Newest first. Each entry says what changed for somebody running these commands. Dates are +ISO 8601, versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and +the shape follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +An entry earns its place by being observable. If running a command produces no different +result, no different output and no different exit code, it is not in here, whatever it cost +to build. + +`release.yaml` reads the section for the version being released and uses it as the GitHub +release body, and refuses a tag whose version has no section. `/release-notes:draft ` +writes one; the plugin is declared in `.claude/settings.json`. + +## [Unreleased] + +### Added + +- `git::remote` prints the remote this repository belongs to, resolved from + `checkout.defaultRemote`, then the current branch's remote, then a sole remote. It refuses + when several exist and nothing says which. +- `git::tags_at_head` prints the release tags on HEAD, highest version first. +- `git::assert_tag_version ` refuses unless a tag on HEAD names that version. +- `git::assert_tag_free ` refuses unless the remote definitely does not carry the tag. + A failure to ask the remote is a refusal, not a yes. +- `git::assert_on_branch [branch]` refuses unless the branch already took HEAD, and refuses + outright on a shallow clone rather than answering from truncated history. +- `github::assert_on_branch [branch] [commit]` answers the same question over the compare + API, which needs no local history. +- `github::await_workflow ` waits for one workflow's run on a commit. +- `github::assert_release_absent ` refuses a tag whose release already carries assets. +- `github::dispatch [key=value ...]` sends a repository_dispatch event. +- `changelog::section [file]` prints one version's changelog section. +- `net::status ` prints an HTTP status code; `net::await_url ` waits for a URL to + serve, backing off across about 7.75 minutes. +- `release::prechecks ` runs the shape, the working tree, the tag, the ordering and + optionally the branch and a registry URL. +- `install.sh` links `rt` alongside `releasetools`, and leaves a name it did not create. + +### Changed + +- `git::latest_version` and `git::release --push` use `git::remote` instead of assuming + `origin`. +- `git::release` reuses a tag that already points at HEAD instead of failing, so a retry + after a failed push no longer needs `--force`. It refuses a tag pointing anywhere else, + and refuses to reuse an unsigned tag when `--sign` was asked for. +- `base::check_deps` now reports on `changelog::`, `net::`, `release::` and a `github::` + that requires `gh`, so it fails on a machine without the GitHub CLI. + +### Removed + +- `github::get_version`. A GitHub-wide code search found its definition and one docs page + and no caller. `v$(rt git::version_tag)` replaces it, and `--env` becomes + `echo "VERSION=v$(rt git::version_tag)" >>"$GITHUB_ENV"`. diff --git a/scripts/README.md b/scripts/README.md index 67572da..23f3dbe 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -4,11 +4,49 @@ This directory contains scripts used to generate artifacts and trigger releases. ## Release a new version -After coding and testing new code, run the following command to tag and trigger a new release. +Four steps, in order. Each one is checked by something, so skipping one fails the release +rather than shipping a wrong one. + +1\. Bump the `default:` version in [action.yml](../action.yml) and commit it. + +`git::release --major` force-moves the `v0` tag onto every release, so a consumer writing +`uses: releasetools/cli@v0` installs whatever that default names. Left stale, `@v0` keeps +serving the previous release while every test passes. `scripts/assert-action-version.sh` +runs inside `tag.sh` and refuses to tag until the two agree. + +2\. Write the changelog entry. ```shell -scripts/tag.sh vX.Y.Z --push # X, Y, Z are integers +/release-notes:draft X.Y.Z +``` + +That rules on every commit since the previous tag and writes the entry into +[CHANGELOG.md](../CHANGELOG.md) under `## X.Y.Z - `. The plugin is declared in +`.claude/settings.json`; installing it by hand is +`claude plugin marketplace add releasetools/agent-plugins` then +`claude plugin install release-notes@ReleaseTools`. + +`release.yaml` reads that section back out as the GitHub release body and fails when the +version has no section, so this is not optional. + +3\. Tag and push. -# if you are replacing a release, use the --force flag, e.g.: -scripts/tag.sh vX.Y.Z --force --push +```shell +scripts/tag.sh vX.Y.Z --major --sign --push # X, Y, Z are integers ``` + +`--major` is what moves `v0` onto the new release, and `uses: releasetools/cli@v0` resolves +through that tag, so without it every consumer keeps the previous release no matter what +step 1 said. `--sign` because every release tag here carries a signature. + +4\. Watch `release.yaml`, then `test-release.yaml`. + +## Retrying a release + +A tag names one commit forever. When a push fails partway, run the same command again: +`git::release` reuses a tag that already points at HEAD and goes straight to the push, and +refuses one that points at any other commit. + +`--force` moves the tag on the remote. Reach for it only to correct a tag that was pushed +to the wrong commit and that nobody has installed, and prefer the next patch version to +anything else.