Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"enabledPlugins": {
"release-notes@ReleaseTools": true
},
"extraKnownMarketplaces": {
"ReleaseTools": {
"source": {
"source": "github",
"repo": "releasetools/agent-plugins"
}
}
}
}
10 changes: 9 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 13 additions & 6 deletions .github/workflows/test-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 <version>`
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 <version>` refuses unless a tag on HEAD names that version.
- `git::assert_tag_free <tag>` 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 <sha> <workflow>` waits for one workflow's run on a commit.
- `github::assert_release_absent <tag>` refuses a tag whose release already carries assets.
- `github::dispatch <owner/repo> <event> [key=value ...]` sends a repository_dispatch event.
- `changelog::section <version> [file]` prints one version's changelog section.
- `net::status <url>` prints an HTTP status code; `net::await_url <url>` waits for a URL to
serve, backing off across about 7.75 minutes.
- `release::prechecks <version>` 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"`.
46 changes: 42 additions & 4 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 - <date>`. 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.