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.
This commit is contained in:
1 parent
040c69b57d
commit
7261d46ee2
7 files changed
+724
-1
No files matched your search
@@ -0,0 +1,149 @@
|
||||
# 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}"
|
||||
Reference in new issue
Block a user