bots-garden/hellopublic Fork 0
0368b85
Commits
Clone
git clone https://git.rickub.com/bots-garden/hello.git
git clone ssh://git@rickub.com/bots-garden/hello.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

📦 Hello

k33g committed 2026-09-19T04:51:17+02:00 Browse files
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+}