Files
dsh-recall/.gitea/workflows/release.yml
T
NotExpectedYet 7261d46ee2
Release / release (push) Successful in 5s
ci: publish a release on every push to master
Tags are what make `dsh plugin update` work at all: a plugin installed as a
bare `git+https://…/repo.git` is pinned to a commit in the profile's lockfile,
so with no tags there is nothing for an update to move to. This adds the
pipeline that creates them.

`.gitea/workflows/release.yml` runs on a push to master and on manual dispatch:
it reads the commit subjects since the last tag, derives the bump, prepends a
`CHANGELOG.md` section, commits and tags `vX.Y.Z`, and publishes the Gitea
release with those subjects as the body. The range with no tags yet is the
whole history, so the first release describes everything it contains.

The logic lives in `.gitea/scripts/release.sh` rather than in the YAML so it can
run — and be checked — without a runner: `npm run test:release` exercises it
(43 checks), `npm run release:plan` says whether HEAD would release, and
`npm run release:notes` prints the notes it would publish. `AGENTS.md` states
the subject conventions a contributor has to follow, because the subject is both
the version decision and the sentence a user reads in the release notes.

One property worth knowing: with no tags yet the version released is the one
`package.json` already declares — no bump is applied to a version that has never
been published — so the first tag is `v0.1.0`, and the bump rules take effect
from the second release onward.
2026-09-26 06:42:46 +01:00

150 lines
6.0 KiB
YAML

# Publish a release every time master moves.
#
# What this is for, beyond tidiness: a dsh plugin installed as a bare
# `git+https://…/repo.git` is resolved to a commit and pinned in the profile's
# lockfile. With no tags, there is nothing for `dsh plugin update` to move to —
# which is why updating appeared to do nothing and a reinstall was the only way
# through. Tags are what make an update possible; this workflow is what creates
# them.
#
# The work lives in `.gitea/scripts/release.sh` so it can be run and tested
# without a runner (`bash .gitea/scripts/release.test.sh`, or `npm run
# test:release`).
#
# The bump comes from the commit subjects in the range being released:
# `feat` is a minor, `!:` (or an explicit `[major]`) is a major, and everything
# else — `fix:`, `docs:`, prose — is a patch. `[skip release]` in a subject
# suppresses the release for that push.
name: Release
on:
push:
branches: [master]
# Manual runs, for the first release (no tags yet) or to publish a release
# whose push failed after the tag was created.
workflow_dispatch:
# One release at a time. Two pushes in quick succession would otherwise race to
# tag the same commit and one of them would lose.
concurrency:
group: release-master
cancel-in-progress: false
permissions:
# The job token is what pushes the version commit, the tag, and the release.
contents: write
jobs:
release:
runs-on: ubuntu-latest
env:
BRANCH: master
steps:
- uses: actions/checkout@v4
with:
# Tags are the whole point, so the clone must carry them.
fetch-depth: 0
ref: master
# Gitea mints a token for every job and exposes it as
# secrets.GITEA_TOKEN; `permissions: contents: write` above is what
# lets it push. No hand-made secret is needed.
token: ${{ secrets.GITEA_TOKEN }}
- name: Decide whether this push releases
id: decide
run: |
set -euo pipefail
tag="$(bash .gitea/scripts/release.sh last-tag)"
range="$(bash .gitea/scripts/release.sh range)"
echo "range=${range}" >> "$GITHUB_OUTPUT"
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
verdict="$(bash .gitea/scripts/release.sh should-release "$range")"
echo "release=${verdict}" >> "$GITHUB_OUTPUT"
echo "last tag: ${tag:-none} range: ${range:-everything} release: ${verdict}"
- name: Work out the version and the notes
if: steps.decide.outputs.release == 'yes'
id: version
run: |
set -euo pipefail
bump="$(bash .gitea/scripts/release.sh bump "${{ steps.decide.outputs.range }}")"
version="$(bash .gitea/scripts/release.sh next-version "$bump")"
bash .gitea/scripts/release.sh notes "${{ steps.decide.outputs.range }}" > /tmp/notes.md
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "bump=${bump}" >> "$GITHUB_OUTPUT"
echo "releasing v${version} (${bump})"
cat /tmp/notes.md
- name: Bump, changelog, commit and tag
if: steps.decide.outputs.release == 'yes'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
git config user.name "gitea-actions"
git config user.email "actions@notexpectedyet.com"
# Idempotency: a run that pushed the tag and then failed at the
# release call must not fail here on the re-run.
if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then
echo "tag v${VERSION} already exists — publishing the release for it"
exit 0
fi
bash .gitea/scripts/release.sh set-version "$VERSION"
bash .gitea/scripts/release.sh changelog-prepend "$VERSION" "$(date -u +%Y-%m-%d)" /tmp/notes.md
git add package.json CHANGELOG.md
# `[skip release]` is the guard that has to hold, because the script
# is what decides. `[skip actions]` is the cheap extra: Gitea reads it
# and can skip queueing a run for this commit at all.
git commit -m "Release v${VERSION} [skip release] [skip actions]"
git tag "v${VERSION}"
git push origin "HEAD:${BRANCH}"
git push origin "v${VERSION}"
- name: Publish the release
if: steps.decide.outputs.release == 'yes'
env:
VERSION: ${{ steps.version.outputs.version }}
SERVER: ${{ github.server_url }}
REPO: ${{ github.repository }}
TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -euo pipefail
# The body is built by an encoder rather than by string interpolation:
# release notes are multi-line markdown and quoting them into JSON by
# hand is a reliable way to publish a malformed release.
python3 - <<'PY' > /tmp/release.json
import json, os
with open('/tmp/notes.md', encoding='utf-8') as handle:
body = handle.read()
version = os.environ['VERSION']
print(json.dumps({
'tag_name': 'v' + version,
'name': 'v' + version,
'body': body,
'draft': False,
'prerelease': False,
}))
PY
status="$(curl -sS -o /tmp/release-response.json -w '%{http_code}' \
-X POST "${SERVER}/api/v1/repos/${REPO}/releases" \
-H "Authorization: token ${TOKEN}" \
-H 'Content-Type: application/json' \
--data-binary @/tmp/release.json)"
# 409 is "a release for this tag already exists": the job has nothing
# left to do rather than something to report.
if [ "$status" = "409" ]; then
echo "v${VERSION} is already released"
exit 0
fi
if [ "$status" != "201" ] && [ "$status" != "200" ]; then
echo "release creation failed (HTTP ${status})" >&2
cat /tmp/release-response.json >&2
exit 1
fi
echo "published v${VERSION}"