# 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}"