diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4ac655a..4431ee4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -26,12 +26,11 @@ env: jobs: npm: runs-on: ubuntu-latest - outputs: - tag: ${{ steps.version.outputs.tag }} - version: ${{ steps.version.outputs.version }} steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 - name: Read version id: version @@ -122,9 +121,53 @@ jobs: set -euo pipefail git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then + tag_commit="$(git rev-parse "$TAG^{commit}")" + head_commit="$(git rev-parse HEAD)" + if [[ "$tag_commit" != "$head_commit" ]]; then + echo "::error::Tag $TAG already points to $tag_commit, not $head_commit" + exit 1 + fi + exit 0 + fi + git tag -a "$TAG" -m "$TAG" git push origin "$TAG" + - name: Resolve release notes range + if: github.event_name == 'push' || inputs.publish_to_npm == true + id: release-notes-range + env: + RELEASE_TAG: ${{ steps.version.outputs.tag }} + run: | + set -euo pipefail + previous_stable="$( + git tag --list 'v[0-9]*.[0-9]*.[0-9]*' \ + --sort=-v:refname \ + | awk -v release_tag="$RELEASE_TAG" ' + /^v[0-9]+\.[0-9]+\.[0-9]+$/ && $0 != release_tag { + print + exit + } + ' + )" + if [[ -z "$previous_stable" ]]; then + first_commit="$(git rev-list --max-parents=0 HEAD | tail -n 1)" + echo "args=--tag ${RELEASE_TAG} --strip header ${first_commit}..HEAD" >> "$GITHUB_OUTPUT" + else + echo "args=--tag ${RELEASE_TAG} --strip header ${previous_stable}..HEAD" >> "$GITHUB_OUTPUT" + fi + + - name: Generate release notes from Conventional Commits + if: github.event_name == 'push' || inputs.publish_to_npm == true + uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4.8.0 + with: + config: cliff.toml + args: ${{ steps.release-notes-range.outputs.args }} + env: + OUTPUT: NOTES.md + - name: Create or update GitHub release if: github.event_name == 'push' || inputs.publish_to_npm == true env: @@ -148,19 +191,10 @@ jobs: if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber + gh release edit "$RELEASE_TAG" --notes-file NOTES.md else gh release create "$RELEASE_TAG" "${assets[@]}" \ "${prerelease_flag[@]}" \ - --title "$RELEASE_TAG" + --title "$RELEASE_TAG" \ + --notes-file NOTES.md fi - - update-changelog: - name: Update CHANGELOG - needs: npm - if: github.event_name == 'push' || inputs.publish_to_npm == true - uses: stella/.github/.github/workflows/changelog-update.yml@d11bdc933dec609e291f6685f470b039d8342b6a - with: - tag: ${{ needs.npm.outputs.tag }} - permissions: - contents: write - pull-requests: write diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..bdae487 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,61 @@ +# git-cliff configuration: https://git-cliff.org/docs/configuration +# +# Generates GitHub release notes from Conventional Commits since the +# previous tag. Used by `publish.yml` to write `NOTES.md` and pass it +# to `gh release create --notes-file`. The repo enforces Conventional +# Commits in PR titles (`feat`, `fix`, `chore`, `docs`); anything that +# slips through unconventional is filtered out below. + +[changelog] +header = "" +body = """ +{% if version %}\ +{% else %}\ +## Unreleased +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} +### {{ group | split(pat=":") | last }} +{% for commit in commits %} +- {% if commit.scope %}**{{ commit.scope }}**: {% endif %}{{ commit.message | split(pat="\n") | first | trim }}\ +{% endfor %} +{% endfor %}\n +""" +footer = "" +trim = true +postprocessors = [ + { pattern = "\\(#([0-9]+)\\)", replace = "([#${1}](https://github.com/stella/tooling/pull/${1}))" }, +] + +[git] +conventional_commits = true +filter_unconventional = true +split_commits = false +filter_commits = false +tag_pattern = "^v[0-9]+\\.[0-9]+\\.[0-9]+(-rc\\.[0-9]+)?$" +sort_commits = "newest" + +# Order matters twice over: +# - First matching parser wins (so put more specific patterns first, +# e.g. `chore(deps)` before `chore`). +# - Rendered section order follows the lexicographic order of `group`. +# The two-digit `NN:` prefix pins the order (00 โ†’ 99); the body +# template above strips everything before the colon, so the prefix +# never reaches the rendered markdown โ€” keeps GitHub release notes, +# stll.app/changelog, and any other downstream renderer clean. Two +# digits leave headroom for new sections without breaking sort +# (single-digit `10` would otherwise sort before `2`). +commit_parsers = [ + { message = "^feat", group = "00:๐Ÿš€ Features" }, + { message = "^fix", group = "01:๐Ÿ› Bug Fixes" }, + { message = "^perf", group = "02:โšก Performance" }, + { message = "^refactor", group = "03:โ™ป๏ธ Refactor" }, + { message = "^docs", group = "04:๐Ÿ“š Documentation" }, + { message = "^test", group = "05:๐Ÿงช Tests" }, + { message = "^chore\\(deps[^)]*\\)", group = "06:๐Ÿ“ฆ Dependencies" }, + { message = "^chore\\(release\\)", skip = true }, + { message = "^chore", group = "07:โš™๏ธ Miscellaneous" }, + { message = "^style", skip = true }, + { message = "^ci", skip = true }, + { message = "^build", skip = true }, + { message = "^revert", group = "08:โช Reverts" }, +]