Files
dsh-slopo/.gitea/workflows/release.yml
T
James Mackay e0813bcbea Release on every push to master, and make updating possible
A dsh plugin installed as a bare git URL is pinned to a commit in the profile
lockfile. With no tags in the repository there was nothing for an update to move
to, which is why updating appeared to do nothing and a reinstall was the only way
through.

Gitea action on push to master: work out the range since the last tag, decide
whether the push releases at all, bump package.json, prepend a CHANGELOG section,
commit, tag vX.Y.Z, and publish the release with those notes as the body.

The bump is a patch unless a commit says [minor] or [major]; [skip release]
publishes nothing. With no tags yet the declared version is what gets tagged, so
the first release matches the code. Notes come from commit subjects, grouped under
headings only when the range actually uses conventional prefixes.

The logic is a script rather than inline YAML so it runs without a runner:
npm run test:release covers 31 cases, including an empty range, a repository with
no tags, and a patch number already at 9. The empty-range guard is load-bearing:
the release commit is itself a push to master.

Two bugs the tests caught: an empty note section made '[ -n x ] && printf' return
non-zero, which under set -e aborted the whole release, and a bullet beginning
with '-' was read by grep as an option.

README documents the #semver: spec, which tracks releases rather than every
commit, and the two things that look like a failed update: an unfetchable remote,
and the profile restart client bundle bytes require.
2026-09-25 19:03:50 +01:00

122 lines
4.7 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`).
name: Release
on:
push:
branches: [master]
# Manual runs, for the first release or to re-cut one after a failed push.
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
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
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
- 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 "release bot"
git config user.email "release@notexpectedyet.com"
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
# The marker is the second line of defence against releasing this very
# commit: the script also refuses an empty range.
git commit -m "Release v${VERSION} [skip release]"
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: ${{ github.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)"
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}"