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