diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index bc938fb..7304a54 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -187,19 +187,22 @@ jobs: # Read the release notes from the tag object itself. # - # This is not paranoia about the notes file. Across five runs of this + # This is not paranoia about the notes file. Across six runs of this # workflow, act_runner produced worktrees where `git ls-files docs` and # `ls docs/releases` both showed docs/releases/v0.1.3.md while the file # itself was absent — intermittently, from the same commit — and then # `git show HEAD:...` came back empty too, because the shallow fetch had - # not brought that blob across. Three separate ways of reading one file - # failed for three different reasons, and v0.1.3 shipped a placeholder - # body three times because of it. + # not brought that blob across. v0.1.3 shipped a placeholder body four + # times before this landed. # - # The tag object is the one thing the runner provably receives: it is - # named in the fetch refspec itself. So the notes live there, written at - # tag time with `git tag -a v0.1.4 -F docs/releases/v0.1.4.md`. The docs - # file stays as the thing a human edits and reads on the web. + # There is a second trap, which cost two more runs: `actions/checkout` + # fetches the tag *by commit sha* into refs/tags/ (see the + # `+:refs/tags/v0.1.3` in its fetch line). That overwrites the + # annotated tag with a lightweight one pointing at the commit, so + # `git tag -l` happily reports the commit message as the tag message. The + # annotation is still in the object database; it just is not what + # `refs/tags/v0.1.3` names any more. Hence the explicit fetch by tag + # object sha below, into a namespace checkout cannot collide with. - name: Collect release notes if: github.ref_type == 'tag' run: | @@ -207,19 +210,30 @@ jobs: TAG="${{ github.ref_name }}" VERSION=$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)".*/\1/') - # Everything after the tag's subject line, with the blank line that - # separates them dropped. `%(contents:body)` is not used here: it - # also treats any line starting with `#` as a comment, which silently - # removed every heading from these notes. - NOTES=$(git tag -l --format='%(contents)' "$TAG" 2>/dev/null | tail -n +2 | sed '/./,$!d' || true) + NOTES="" + TAG_SHA=$(git ls-remote --tags origin "refs/tags/${TAG}" 2>/dev/null | awk '{print $1}') + if [ -n "$TAG_SHA" ]; then + git fetch -q --depth=1 origin "+${TAG_SHA}:refs/release-notes/${TAG}" 2>/dev/null || true + if [ "$(git cat-file -t "refs/release-notes/${TAG}" 2>/dev/null)" = "tag" ]; then + # cat-file prints the object verbatim: three header lines, a + # blank, then the message. `sed '1,/^$/d'` drops the headers, + # `tail -n +2` the one-line subject, and the last sed any blank + # line left between them. Format specifiers like %(contents:body) + # are avoided because they also treat a `#` line as a comment, + # which would delete every heading in these notes. + NOTES=$(git cat-file -p "refs/release-notes/${TAG}" \ + | sed '1,/^$/d' | tail -n +2 | sed '/./,$!d' || true) + fi + fi + if [ -z "$NOTES" ]; then NOTES=$(git show "HEAD:docs/releases/${VERSION}.md" 2>/dev/null || true) fi if [ -n "$NOTES" ]; then - echo "notes: ${#NOTES} bytes from tag ${TAG}" + echo "notes: ${#NOTES} bytes from the ${TAG} annotation" else NOTES=$(printf 'Release %s.\n\nSee the repository README for deployment instructions.' "$VERSION") - echo "::warning::tag ${TAG} has no notes body; using a placeholder" + echo "::warning::tag ${TAG} carries no notes body; using a placeholder" fi printf '%s' "$NOTES" > /tmp/notes.md