| 📦 Hello 0368b85 k33g 4h ago | 1 | #!/bin/bash |
| 2 | : <<'COMMENT' |
| 3 | Publishing hello is tagging it. There is no binary to build and nothing to |
| 4 | upload: a Go module is published by a tag being reachable from its repository. |
| 5 | |
| 6 | 1. Set TAG and ABOUT in release.env |
| 7 | 2. Run this script: ./01-release.tag.sh (test, commit, push, tag, push the tag) |
| 8 | 3. Watch the "Release" workflow (Actions tab): the tag push starts it, and it |
| 9 | creates the release page with the job's own token. Nothing else to run. |
| 10 | COMMENT |
| 11 | |
| 12 | # Without this, a failing step is ignored and the next one runs anyway. That is |
| 13 | # not theoretical: `git tag` refusing a tag that already existed was skipped in |
| 14 | # silence in a sibling repository, and the `git push` below then pushed the OLD |
| 15 | # tag — so a release was cut from a commit nobody meant. |
| 16 | set -euo pipefail |
| 17 | |
| 18 | if [ ! -f release.env ]; then |
| 19 | echo "❌ release.env is missing" |
| 20 | echo "💡 Create it with the version you are publishing:" |
| 21 | echo ' TAG="v0.1.0"' |
| 22 | echo ' ABOUT="Hello"' |
| 23 | exit 1 |
| 24 | fi |
| 25 | |
| 26 | set -o allexport |
| 27 | # shellcheck source=/dev/null |
| 28 | source release.env |
| 29 | set +o allexport |
| 30 | |
| 31 | : "${TAG:?TAG is not set in release.env}" |
| 32 | ABOUT="${ABOUT:-Hello ${TAG}}" |
| 33 | |
| 34 | # A tag that is not vMAJOR.MINOR.PATCH[-prerelease] is worse than a typo here: |
| 35 | # the module proxy will not serve a tag it cannot read as a version, so the |
| 36 | # release would look fine and fail at every `go get`. |
| 37 | if ! [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then |
| 38 | echo "❌ TAG must look like v1.2.3 or v1.2.3-rc.1, got '${TAG}' (check release.env)" |
| 39 | exit 1 |
| 40 | fi |
| 41 | |
| 42 | echo "Releasing hello ${TAG}: ${ABOUT}" |
| 43 | |
| 44 | # tagExists reports whether TAG is already taken, here or on the remote. The |
| 45 | # remote matters on its own: a tag deleted locally after a failed attempt still |
| 46 | # exists there, and pushing a new one at a different commit is rejected. |
| 47 | tagExists() { |
| 48 | if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then |
| 49 | printf 'locally, on %s\n' "$(git rev-parse --short "${TAG}^{commit}")" |
| 50 | return 0 |
| 51 | fi |
| 52 | if ! remote="$(git ls-remote --tags origin "refs/tags/${TAG}" 2>/dev/null)"; then |
| 53 | return 1 # the remote is unreachable; the push below will say so |
| 54 | fi |
| 55 | if [ -n "${remote}" ]; then |
| 56 | # No commit is named here on purpose: for an annotated tag ls-remote |
| 57 | # gives the tag object, not the commit, and printing that as if it were |
| 58 | # the commit sends the reader looking for a SHA they will never find. |
| 59 | printf 'on origin\n' |
| 60 | return 0 |
| 61 | fi |
| 62 | return 1 |
| 63 | } |
| 64 | |
| 65 | if where="$(tagExists)"; then |
| 66 | echo "❌ ${TAG} already exists ${where}" |
| 67 | echo "💡 A published version is not yours to move: somebody may already pin" |
| 68 | echo " it, and the module proxy caches what it fetched. Bump TAG in" |
| 69 | echo " release.env instead." |
| 70 | exit 1 |
| 71 | fi |
| 72 | |
| 73 | # A published library must not carry a replace directive: the proxy serves the |
| 74 | # go.mod as written, and a consumer would be told to look for hello in a |
| 75 | # directory that does not exist on their machine. |
| 76 | if grep -qE '^[[:space:]]*replace[[:space:]]' go.mod; then |
| 77 | echo "❌ go.mod has a replace directive, which a published module must not" |
| 78 | grep -nE '^[[:space:]]*replace[[:space:]]' go.mod |
| 79 | exit 1 |
| 80 | fi |
| 81 | |
| 82 | # A version somebody may pin is the wrong place to find out the suite was red. |
| 83 | echo "→ go vet ./..." |
| 84 | go vet ./... |
| 85 | echo "→ go test ./..." |
| 86 | go test ./... -count=1 |
| 87 | |
| 88 | find . -name '.DS_Store' -type f -delete |
| 89 | |
| 90 | git add . |
| 91 | |
| 92 | # Nothing to commit is not a failure — the work may already be committed — but |
| 93 | # under `set -e` a plain `git commit` would stop the release right here. |
| 94 | if git diff --cached --quiet; then |
| 95 | echo "Nothing to commit; releasing what is already on HEAD" |
| 96 | else |
| 97 | git commit -m "📦 ${ABOUT}" |
| 98 | fi |
| 99 | |
| 100 | # --show-current rather than `rev-parse --abbrev-ref HEAD`, which fails on a |
| 101 | # branch with no commits yet — exactly the state a first release starts from. |
| 102 | branch="$(git branch --show-current)" |
| 103 | git push -u origin "${branch}" |
| 104 | |
| 105 | # The tag goes on after the push, so a rejected push never leaves a tag behind |
| 106 | # pointing at a commit the remote has never seen. |
| 107 | git tag -a "${TAG}" -m "${ABOUT}" |
| 108 | git push origin "${TAG}" |
| 109 | |
| 110 | echo "✅ hello ${TAG} published" |
| 111 | echo "💡 The tag push started the Release workflow; watch it on the Actions tab." |