#!/bin/bash : <<'COMMENT' Publishing hello is tagging it. There is no binary to build and nothing to upload: a Go module is published by a tag being reachable from its repository. 1. Set TAG and ABOUT in release.env 2. Run this script: ./01-release.tag.sh (test, commit, push, tag, push the tag) 3. Watch the "Release" workflow (Actions tab): the tag push starts it, and it creates the release page with the job's own token. Nothing else to run. COMMENT # Without this, a failing step is ignored and the next one runs anyway. That is # not theoretical: `git tag` refusing a tag that already existed was skipped in # silence in a sibling repository, and the `git push` below then pushed the OLD # tag — so a release was cut from a commit nobody meant. set -euo pipefail if [ ! -f release.env ]; then echo "❌ release.env is missing" echo "💡 Create it with the version you are publishing:" echo ' TAG="v0.1.0"' echo ' ABOUT="Hello"' exit 1 fi set -o allexport # shellcheck source=/dev/null source release.env set +o allexport : "${TAG:?TAG is not set in release.env}" ABOUT="${ABOUT:-Hello ${TAG}}" # A tag that is not vMAJOR.MINOR.PATCH[-prerelease] is worse than a typo here: # the module proxy will not serve a tag it cannot read as a version, so the # release would look fine and fail at every `go get`. if ! [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then echo "❌ TAG must look like v1.2.3 or v1.2.3-rc.1, got '${TAG}' (check release.env)" exit 1 fi echo "Releasing hello ${TAG}: ${ABOUT}" # tagExists reports whether TAG is already taken, here or on the remote. The # remote matters on its own: a tag deleted locally after a failed attempt still # exists there, and pushing a new one at a different commit is rejected. tagExists() { if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then printf 'locally, on %s\n' "$(git rev-parse --short "${TAG}^{commit}")" return 0 fi if ! remote="$(git ls-remote --tags origin "refs/tags/${TAG}" 2>/dev/null)"; then return 1 # the remote is unreachable; the push below will say so fi if [ -n "${remote}" ]; then # No commit is named here on purpose: for an annotated tag ls-remote # gives the tag object, not the commit, and printing that as if it were # the commit sends the reader looking for a SHA they will never find. printf 'on origin\n' return 0 fi return 1 } if where="$(tagExists)"; then echo "❌ ${TAG} already exists ${where}" echo "💡 A published version is not yours to move: somebody may already pin" echo " it, and the module proxy caches what it fetched. Bump TAG in" echo " release.env instead." exit 1 fi # A published library must not carry a replace directive: the proxy serves the # go.mod as written, and a consumer would be told to look for hello in a # directory that does not exist on their machine. if grep -qE '^[[:space:]]*replace[[:space:]]' go.mod; then echo "❌ go.mod has a replace directive, which a published module must not" grep -nE '^[[:space:]]*replace[[:space:]]' go.mod exit 1 fi # A version somebody may pin is the wrong place to find out the suite was red. echo "→ go vet ./..." go vet ./... echo "→ go test ./..." go test ./... -count=1 find . -name '.DS_Store' -type f -delete git add . # Nothing to commit is not a failure — the work may already be committed — but # under `set -e` a plain `git commit` would stop the release right here. if git diff --cached --quiet; then echo "Nothing to commit; releasing what is already on HEAD" else git commit -m "📦 ${ABOUT}" fi # --show-current rather than `rev-parse --abbrev-ref HEAD`, which fails on a # branch with no commits yet — exactly the state a first release starts from. branch="$(git branch --show-current)" git push -u origin "${branch}" # The tag goes on after the push, so a rejected push never leaves a tag behind # pointing at a commit the remote has never seen. git tag -a "${TAG}" -m "${ABOUT}" git push origin "${TAG}" echo "✅ hello ${TAG} published" echo "💡 The tag push started the Release workflow; watch it on the Actions tab."