📦 Hello
0368b85 added
.github/workflows/release.yml +82 -0 | new file mode 100644 | ||
| @@ -0,0 +1,82 @@ | ||
| 1 | +name: Release | |
| 2 | + | |
| 3 | +# Creates the release page whenever a tag v* is pushed — what | |
| 4 | +# ./01-release.tag.sh does at its last line. | |
| 5 | +# | |
| 6 | +# hello is a library, so the tag alone already publishes the module: nothing is | |
| 7 | +# built and nothing is uploaded. What this adds is the page a person reads, and | |
| 8 | +# a suite that has run against the exact commit the tag is on. | |
| 9 | +# | |
| 10 | +# Rickub runs this as an ordinary GitHub Actions workflow. Two platform facts | |
| 11 | +# matter here: the job's GITHUB_TOKEN is the ONLY credential the release API | |
| 12 | +# (the /gh shim behind $GITHUB_API_URL) accepts — a personal token is refused — | |
| 13 | +# and it is read-only unless the workflow asks for `contents: write` below. | |
| 14 | +# | |
| 15 | +# No workflow_dispatch on purpose: Rickub's dispatch API fires EVERY | |
| 16 | +# dispatchable workflow of a ref, so a repository should declare at most one. | |
| 17 | +on: | |
| 18 | + push: | |
| 19 | + tags: | |
| 20 | + - "v*" | |
| 21 | + | |
| 22 | +permissions: | |
| 23 | + contents: write | |
| 24 | + | |
| 25 | +concurrency: | |
| 26 | + group: release-${{ github.ref_name }} | |
| 27 | + cancel-in-progress: false | |
| 28 | + | |
| 29 | +jobs: | |
| 30 | + release: | |
| 31 | + name: publish ${{ github.ref_name }} | |
| 32 | + runs-on: ubuntu-latest | |
| 33 | + steps: | |
| 34 | + - name: Checkout | |
| 35 | + uses: actions/checkout@v4 | |
| 36 | + with: | |
| 37 | + # The whole history and the tags: the release notes below are read | |
| 38 | + # from the annotated tag's message. | |
| 39 | + fetch-depth: 0 | |
| 40 | + | |
| 41 | + - name: Set up Go | |
| 42 | + uses: actions/setup-go@v5 | |
| 43 | + with: | |
| 44 | + go-version-file: go.mod | |
| 45 | + cache: true | |
| 46 | + | |
| 47 | + - name: go vet | |
| 48 | + run: go vet ./... | |
| 49 | + | |
| 50 | + - name: go test | |
| 51 | + run: go test ./... -count=1 | |
| 52 | + | |
| 53 | + - name: Release notes | |
| 54 | + id: notes | |
| 55 | + # The message ./01-release.tag.sh put on the annotated tag (ABOUT in | |
| 56 | + # release.env), then the one line that installs the module. A | |
| 57 | + # lightweight tag has no message: the tag name stands in. | |
| 58 | + run: | | |
| 59 | + set -euo pipefail | |
| 60 | + message="$(git for-each-ref "refs/tags/${GITHUB_REF_NAME}" --format='%(contents)' | sed '/^-----BEGIN PGP SIGNATURE-----/,$d')" | |
| 61 | + if [ -z "$(printf '%s' "${message}" | tr -d '[:space:]')" ]; then | |
| 62 | + message="hello ${GITHUB_REF_NAME}" | |
| 63 | + fi | |
| 64 | + { | |
| 65 | + printf '%s\n\n' "${message}" | |
| 66 | + echo '```bash' | |
| 67 | + echo "go get $(go list -m)@${GITHUB_REF_NAME}" | |
| 68 | + echo '```' | |
| 69 | + echo | |
| 70 | + echo "- Commit: \`${GITHUB_SHA}\`" | |
| 71 | + echo "- Published by the Release workflow, run #${GITHUB_RUN_NUMBER}, with $(go env GOVERSION)" | |
| 72 | + } > "${RUNNER_TEMP}/notes.md" | |
| 73 | + echo "path=${RUNNER_TEMP}/notes.md" >> "$GITHUB_OUTPUT" | |
| 74 | + | |
| 75 | + - name: Publish the release | |
| 76 | + uses: softprops/action-gh-release@v2 | |
| 77 | + with: | |
| 78 | + tag_name: ${{ github.ref_name }} | |
| 79 | + name: ${{ github.ref_name }} | |
| 80 | + body_path: ${{ steps.notes.outputs.path }} | |
| 81 | + draft: false | |
| 82 | + prerelease: ${{ contains(github.ref_name, '-') }} | |
| new file mode 100644 | |||
| @@ -0,0 +1,82 @@ | |||
| 1 | +name: Release | ||
| 2 | + | ||
| 3 | +# Creates the release page whenever a tag v* is pushed — what | ||
| 4 | +# ./01-release.tag.sh does at its last line. | ||
| 5 | +# | ||
| 6 | +# hello is a library, so the tag alone already publishes the module: nothing is | ||
| 7 | +# built and nothing is uploaded. What this adds is the page a person reads, and | ||
| 8 | +# a suite that has run against the exact commit the tag is on. | ||
| 9 | +# | ||
| 10 | +# Rickub runs this as an ordinary GitHub Actions workflow. Two platform facts | ||
| 11 | +# matter here: the job's GITHUB_TOKEN is the ONLY credential the release API | ||
| 12 | +# (the /gh shim behind $GITHUB_API_URL) accepts — a personal token is refused — | ||
| 13 | +# and it is read-only unless the workflow asks for `contents: write` below. | ||
| 14 | +# | ||
| 15 | +# No workflow_dispatch on purpose: Rickub's dispatch API fires EVERY | ||
| 16 | +# dispatchable workflow of a ref, so a repository should declare at most one. | ||
| 17 | +on: | ||
| 18 | + push: | ||
| 19 | + tags: | ||
| 20 | + - "v*" | ||
| 21 | + | ||
| 22 | +permissions: | ||
| 23 | + contents: write | ||
| 24 | + | ||
| 25 | +concurrency: | ||
| 26 | + group: release-${{ github.ref_name }} | ||
| 27 | + cancel-in-progress: false | ||
| 28 | + | ||
| 29 | +jobs: | ||
| 30 | + release: | ||
| 31 | + name: publish ${{ github.ref_name }} | ||
| 32 | + runs-on: ubuntu-latest | ||
| 33 | + steps: | ||
| 34 | + - name: Checkout | ||
| 35 | + uses: actions/checkout@v4 | ||
| 36 | + with: | ||
| 37 | + # The whole history and the tags: the release notes below are read | ||
| 38 | + # from the annotated tag's message. | ||
| 39 | + fetch-depth: 0 | ||
| 40 | + | ||
| 41 | + - name: Set up Go | ||
| 42 | + uses: actions/setup-go@v5 | ||
| 43 | + with: | ||
| 44 | + go-version-file: go.mod | ||
| 45 | + cache: true | ||
| 46 | + | ||
| 47 | + - name: go vet | ||
| 48 | + run: go vet ./... | ||
| 49 | + | ||
| 50 | + - name: go test | ||
| 51 | + run: go test ./... -count=1 | ||
| 52 | + | ||
| 53 | + - name: Release notes | ||
| 54 | + id: notes | ||
| 55 | + # The message ./01-release.tag.sh put on the annotated tag (ABOUT in | ||
| 56 | + # release.env), then the one line that installs the module. A | ||
| 57 | + # lightweight tag has no message: the tag name stands in. | ||
| 58 | + run: | | ||
| 59 | + set -euo pipefail | ||
| 60 | + message="$(git for-each-ref "refs/tags/${GITHUB_REF_NAME}" --format='%(contents)' | sed '/^-----BEGIN PGP SIGNATURE-----/,$d')" | ||
| 61 | + if [ -z "$(printf '%s' "${message}" | tr -d '[:space:]')" ]; then | ||
| 62 | + message="hello ${GITHUB_REF_NAME}" | ||
| 63 | + fi | ||
| 64 | + { | ||
| 65 | + printf '%s\n\n' "${message}" | ||
| 66 | + echo '```bash' | ||
| 67 | + echo "go get $(go list -m)@${GITHUB_REF_NAME}" | ||
| 68 | + echo '```' | ||
| 69 | + echo | ||
| 70 | + echo "- Commit: \`${GITHUB_SHA}\`" | ||
| 71 | + echo "- Published by the Release workflow, run #${GITHUB_RUN_NUMBER}, with $(go env GOVERSION)" | ||
| 72 | + } > "${RUNNER_TEMP}/notes.md" | ||
| 73 | + echo "path=${RUNNER_TEMP}/notes.md" >> "$GITHUB_OUTPUT" | ||
| 74 | + | ||
| 75 | + - name: Publish the release | ||
| 76 | + uses: softprops/action-gh-release@v2 | ||
| 77 | + with: | ||
| 78 | + tag_name: ${{ github.ref_name }} | ||
| 79 | + name: ${{ github.ref_name }} | ||
| 80 | + body_path: ${{ steps.notes.outputs.path }} | ||
| 81 | + draft: false | ||
| 82 | + prerelease: ${{ contains(github.ref_name, '-') }} | ||
added
.gitignore +5 -0 | new file mode 100644 | ||
| @@ -0,0 +1,5 @@ | ||
| 1 | +# release.env carries the version being released. It is per-release local | |
| 2 | +# state, not something a clone should inherit — and *.env keeps any file that | |
| 3 | +# ever holds a token out of git by the same rule. | |
| 4 | +*.env | |
| 5 | +.DS_Store | |
| new file mode 100644 | |||
| @@ -0,0 +1,5 @@ | |||
| 1 | +# release.env carries the version being released. It is per-release local | ||
| 2 | +# state, not something a clone should inherit — and *.env keeps any file that | ||
| 3 | +# ever holds a token out of git by the same rule. | ||
| 4 | +*.env | ||
| 5 | +.DS_Store | ||
added
.turbo-go/settings.toml +18 -0 | new file mode 100644 | ||
| @@ -0,0 +1,18 @@ | ||
| 1 | +# turbo-go project settings. | |
| 2 | +# | |
| 3 | +# These apply to everyone who opens this project in turbo-go. Delete this file | |
| 4 | +# and the editor falls back to its own defaults. | |
| 5 | + | |
| 6 | +[editor] | |
| 7 | + | |
| 8 | +# The colour theme to start in. `turbo-go -list-themes` lists them all. | |
| 9 | +# A -theme flag on the command line overrides this. | |
| 10 | +theme = "borland-light" | |
| 11 | + | |
| 12 | +# Write modified files by themselves, a short while after you stop typing. | |
| 13 | +# On, because a project that has gone to the trouble of having a settings file | |
| 14 | +# has said what it wants; set it to false and save, and it stops at once. | |
| 15 | +autosave = true | |
| 16 | + | |
| 17 | +# How long that while is. Any Go duration: "500ms", "2s", "1m". | |
| 18 | +autosave_delay = "2s" | |
| new file mode 100644 | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | +# turbo-go project settings. | ||
| 2 | +# | ||
| 3 | +# These apply to everyone who opens this project in turbo-go. Delete this file | ||
| 4 | +# and the editor falls back to its own defaults. | ||
| 5 | + | ||
| 6 | +[editor] | ||
| 7 | + | ||
| 8 | +# The colour theme to start in. `turbo-go -list-themes` lists them all. | ||
| 9 | +# A -theme flag on the command line overrides this. | ||
| 10 | +theme = "borland-light" | ||
| 11 | + | ||
| 12 | +# Write modified files by themselves, a short while after you stop typing. | ||
| 13 | +# On, because a project that has gone to the trouble of having a settings file | ||
| 14 | +# has said what it wants; set it to false and save, and it stops at once. | ||
| 15 | +autosave = true | ||
| 16 | + | ||
| 17 | +# How long that while is. Any Go duration: "500ms", "2s", "1m". | ||
| 18 | +autosave_delay = "2s" | ||
added
01-release.tag.sh +111 -0 | new file mode 100755 | ||
| @@ -0,0 +1,111 @@ | ||
| 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." | |
| new file mode 100755 | |||
| @@ -0,0 +1,111 @@ | |||
| 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." | ||
added
README.md +28 -0 | new file mode 100644 | ||
| @@ -0,0 +1,28 @@ | ||
| 1 | +# hello | |
| 2 | + | |
| 3 | +A Go library that greets somebody by name. | |
| 4 | + | |
| 5 | +```go | |
| 6 | +import "rickub.com/bots-garden/hello" | |
| 7 | + | |
| 8 | +hello.Greet("Bob") // "Hello, Bob!" | |
| 9 | +hello.Greet(" Bob ") // "Hello, Bob!" — surrounding whitespace is dropped | |
| 10 | +hello.Greet("") // "Hello, world!" — an empty name is greeted as the world | |
| 11 | +``` | |
| 12 | + | |
| 13 | +## Using it from another module | |
| 14 | + | |
| 15 | +`rickub.com` serves neither a `go-import` meta tag nor git over HTTPS, so | |
| 16 | +`go get` cannot resolve this path on its own yet. Until it can, depend on a | |
| 17 | +checkout beside yours — a workspace, which changes no tracked file: | |
| 18 | + | |
| 19 | +```bash | |
| 20 | +go work init . ../hello | |
| 21 | +``` | |
| 22 | + | |
| 23 | +## Build and test | |
| 24 | + | |
| 25 | +```bash | |
| 26 | +go test ./... | |
| 27 | +go vet ./... | |
| 28 | +``` | |
| new file mode 100644 | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | +# hello | ||
| 2 | + | ||
| 3 | +A Go library that greets somebody by name. | ||
| 4 | + | ||
| 5 | +```go | ||
| 6 | +import "rickub.com/bots-garden/hello" | ||
| 7 | + | ||
| 8 | +hello.Greet("Bob") // "Hello, Bob!" | ||
| 9 | +hello.Greet(" Bob ") // "Hello, Bob!" — surrounding whitespace is dropped | ||
| 10 | +hello.Greet("") // "Hello, world!" — an empty name is greeted as the world | ||
| 11 | +``` | ||
| 12 | + | ||
| 13 | +## Using it from another module | ||
| 14 | + | ||
| 15 | +`rickub.com` serves neither a `go-import` meta tag nor git over HTTPS, so | ||
| 16 | +`go get` cannot resolve this path on its own yet. Until it can, depend on a | ||
| 17 | +checkout beside yours — a workspace, which changes no tracked file: | ||
| 18 | + | ||
| 19 | +```bash | ||
| 20 | +go work init . ../hello | ||
| 21 | +``` | ||
| 22 | + | ||
| 23 | +## Build and test | ||
| 24 | + | ||
| 25 | +```bash | ||
| 26 | +go test ./... | ||
| 27 | +go vet ./... | ||
| 28 | +``` | ||
added
go.mod +3 -0 | new file mode 100644 | ||
| @@ -0,0 +1,3 @@ | ||
| 1 | +module rickub.com/bots-garden/hello | |
| 2 | + | |
| 3 | +go 1.26 | |
| new file mode 100644 | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | +module rickub.com/bots-garden/hello | ||
| 2 | + | ||
| 3 | +go 1.26 | ||
added
hello.go +22 -0 | new file mode 100644 | ||
| @@ -0,0 +1,22 @@ | ||
| 1 | +// Package hello greets somebody by name. | |
| 2 | +package hello | |
| 3 | + | |
| 4 | +import "strings" | |
| 5 | + | |
| 6 | +// world is who is greeted when nobody was named. A greeting is the one thing | |
| 7 | +// that should never come out half-written, and "Hello, !" is what an empty | |
| 8 | +// name produces if it is passed straight through. | |
| 9 | +const world = "world" | |
| 10 | + | |
| 11 | +// Greet returns a greeting for name. | |
| 12 | +// | |
| 13 | +// Surrounding whitespace is dropped, so a name that arrives from a form or a | |
| 14 | +// command line reads the same as one written by hand. A name that is empty, or | |
| 15 | +// only whitespace, is greeted as the world. | |
| 16 | +func Greet(name string) string { | |
| 17 | + name = strings.TrimSpace(name) | |
| 18 | + if name == "" { | |
| 19 | + name = world | |
| 20 | + } | |
| 21 | + return "Hello, " + name + "!" | |
| 22 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | +// Package hello greets somebody by name. | ||
| 2 | +package hello | ||
| 3 | + | ||
| 4 | +import "strings" | ||
| 5 | + | ||
| 6 | +// world is who is greeted when nobody was named. A greeting is the one thing | ||
| 7 | +// that should never come out half-written, and "Hello, !" is what an empty | ||
| 8 | +// name produces if it is passed straight through. | ||
| 9 | +const world = "world" | ||
| 10 | + | ||
| 11 | +// Greet returns a greeting for name. | ||
| 12 | +// | ||
| 13 | +// Surrounding whitespace is dropped, so a name that arrives from a form or a | ||
| 14 | +// command line reads the same as one written by hand. A name that is empty, or | ||
| 15 | +// only whitespace, is greeted as the world. | ||
| 16 | +func Greet(name string) string { | ||
| 17 | + name = strings.TrimSpace(name) | ||
| 18 | + if name == "" { | ||
| 19 | + name = world | ||
| 20 | + } | ||
| 21 | + return "Hello, " + name + "!" | ||
| 22 | +} | ||
added
hello_test.go +26 -0 | new file mode 100644 | ||
| @@ -0,0 +1,26 @@ | ||
| 1 | +package hello | |
| 2 | + | |
| 3 | +import "testing" | |
| 4 | + | |
| 5 | +func TestGreet(t *testing.T) { | |
| 6 | + cases := []struct { | |
| 7 | + why string | |
| 8 | + name string | |
| 9 | + want string | |
| 10 | + }{ | |
| 11 | + {"a name is greeted as it is written", "Bob", "Hello, Bob!"}, | |
| 12 | + {"surrounding whitespace is not part of a name", " Bob\t", "Hello, Bob!"}, | |
| 13 | + {"an empty name still produces a whole greeting", "", "Hello, world!"}, | |
| 14 | + {"a name of nothing but whitespace is an empty name", " ", "Hello, world!"}, | |
| 15 | + {"the inside of a name is left alone", "Ada Lovelace", "Hello, Ada Lovelace!"}, | |
| 16 | + {"a name is not assumed to be ASCII", "Émilie", "Hello, Émilie!"}, | |
| 17 | + } | |
| 18 | + | |
| 19 | + for _, c := range cases { | |
| 20 | + t.Run(c.why, func(t *testing.T) { | |
| 21 | + if got := Greet(c.name); got != c.want { | |
| 22 | + t.Errorf("Greet(%q) = %q, want %q", c.name, got, c.want) | |
| 23 | + } | |
| 24 | + }) | |
| 25 | + } | |
| 26 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | +package hello | ||
| 2 | + | ||
| 3 | +import "testing" | ||
| 4 | + | ||
| 5 | +func TestGreet(t *testing.T) { | ||
| 6 | + cases := []struct { | ||
| 7 | + why string | ||
| 8 | + name string | ||
| 9 | + want string | ||
| 10 | + }{ | ||
| 11 | + {"a name is greeted as it is written", "Bob", "Hello, Bob!"}, | ||
| 12 | + {"surrounding whitespace is not part of a name", " Bob\t", "Hello, Bob!"}, | ||
| 13 | + {"an empty name still produces a whole greeting", "", "Hello, world!"}, | ||
| 14 | + {"a name of nothing but whitespace is an empty name", " ", "Hello, world!"}, | ||
| 15 | + {"the inside of a name is left alone", "Ada Lovelace", "Hello, Ada Lovelace!"}, | ||
| 16 | + {"a name is not assumed to be ASCII", "Émilie", "Hello, Émilie!"}, | ||
| 17 | + } | ||
| 18 | + | ||
| 19 | + for _, c := range cases { | ||
| 20 | + t.Run(c.why, func(t *testing.T) { | ||
| 21 | + if got := Greet(c.name); got != c.want { | ||
| 22 | + t.Errorf("Greet(%q) = %q, want %q", c.name, got, c.want) | ||
| 23 | + } | ||
| 24 | + }) | ||
| 25 | + } | ||
| 26 | +} | ||