Files
NotExpectedYet cd43e87684
Release / release (push) Successful in 6s
feat: tag and publish a Gitea release on every push to master
A plugin installed as a bare git+https URL is pinned to a commit, so with no
tags there is nothing for `dsh plugin update` to move to. The workflow reads
the commit subjects since the last tag, bumps package.json, prepends
CHANGELOG.md, tags vX.Y.Z, and publishes the Gitea release with those subjects
as the body. The decision logic lives in .gitea/scripts/release.sh so it runs
and is tested without a runner (`npm run test:release`).
2026-09-26 06:45:52 +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}"