turbo-editors/turbo-corepublic Fork 0
d662ceb
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

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

📦 Turbo Core

k33g committed 2026-09-19T11:41:42+02:00 Browse files
d662ceb parent: 28d5985
added .github/workflows/release.yml +124 -0
new file mode 100644
@@ -0,0 +1,124 @@
1+name: Release
2+
3+# Publishes a release with the staged artefacts whenever a tag v* is pushed —
4+# what ./01-release.tag.sh does at its last line. They are staged by
5+# ./02-build-releases.sh, the same script one runs on a laptop, so a local
6+# staging and a published one are the same pipeline.
7+#
8+# turbo-core is a library, so the tag alone already publishes the module: the
9+# proxy serves `go get …@TAG` the moment 01 has run, with or without this
10+# workflow. What this adds is the page a person reads, and an archive of the
11+# tagged source with a checksum to verify it against.
12+#
13+# Rickub runs this as an ordinary GitHub Actions workflow. Two platform facts
14+# matter here: the job's GITHUB_TOKEN is the ONLY credential the release API
15+# (the /gh shim behind $GITHUB_API_URL) accepts — a personal token is refused —
16+# and it is read-only unless the workflow asks for `contents: write` below.
17+# That is why there is no longer a token file to keep out of git.
18+#
19+# No workflow_dispatch on purpose: Rickub's dispatch API fires EVERY
20+# dispatchable workflow of a ref, so a repository should declare at most one.
21+on:
22+ push:
23+ tags:
24+ - "v*"
25+
26+permissions:
27+ contents: write
28+
29+concurrency:
30+ group: release-${{ github.ref_name }}
31+ cancel-in-progress: false
32+
33+jobs:
34+ release:
35+ name: publish ${{ github.ref_name }}
36+ runs-on: ubuntu-latest
37+ steps:
38+ - name: Checkout
39+ uses: actions/checkout@v4
40+ with:
41+ # The whole history and the tags: the release notes below are read
42+ # from the annotated tag's message, and ./02-build-releases.sh takes
43+ # its archive from the commit the tag is on.
44+ fetch-depth: 0
45+
46+ - name: Set up Go
47+ uses: actions/setup-go@v5
48+ with:
49+ go-version-file: go.mod
50+ cache: true
51+
52+ - name: go test
53+ # The suite includes tests that run ./01-release.tag.sh against a
54+ # throwaway clone. They skip themselves when they see this, exactly as
55+ # they do when the script itself calls make check — without it, a
56+ # release job would start a release inside itself.
57+ env:
58+ TURBO_CORE_RELEASING: "1"
59+ run: go test ./... -count=1
60+
61+ - name: Stage the release
62+ # release.env is git-ignored, so the tag is passed explicitly and the
63+ # script falls back to "Turbo Core <tag>" for the description.
64+ run: bash ./02-build-releases.sh "${GITHUB_REF_NAME}"
65+
66+ - name: Release notes
67+ id: notes
68+ # The message ./01-release.tag.sh put on the annotated tag (ABOUT in
69+ # release.env), then the one line that installs the module and the
70+ # links to the documentation AT THAT TAG — a release page is not inside
71+ # the repository tree, so a relative path from it 404s, and a link to
72+ # the branch would rot as the branch moves. A lightweight tag has no
73+ # message: the tag name stands in.
74+ run: |
75+ set -euo pipefail
76+ message="$(git for-each-ref "refs/tags/${GITHUB_REF_NAME}" --format='%(contents)' | sed '/^-----BEGIN PGP SIGNATURE-----/,$d')"
77+ if [ -z "$(printf '%s' "${message}" | tr -d '[:space:]')" ]; then
78+ message="Turbo Core ${GITHUB_REF_NAME}"
79+ fi
80+ tree="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/blob/${GITHUB_REF_NAME}"
81+ {
82+ printf '%s\n\n' "${message}"
83+ echo '```bash'
84+ echo "go get $(go list -m)@${GITHUB_REF_NAME}"
85+ echo '```'
86+ echo
87+ echo "Documentation: [English](${tree}/docs/en/README.md) · [Français](${tree}/docs/fr/README.md)"
88+ echo
89+ echo "- Commit: \`${GITHUB_SHA}\`"
90+ echo "- Published by the Release workflow, run #${GITHUB_RUN_NUMBER}, with $(go env GOVERSION)"
91+ echo
92+ echo '## Checksums'
93+ echo
94+ echo 'The archive below is the tagged source. `go get` does not download it — the module proxy serves the module straight from the tag — so it is here to verify against, and for anyone who cannot reach the proxy.'
95+ echo
96+ echo '```'
97+ cat "release/${GITHUB_REF_NAME}/SHA256SUMS"
98+ echo '```'
99+ } > "${RUNNER_TEMP}/notes.md"
100+ echo "path=${RUNNER_TEMP}/notes.md" >> "$GITHUB_OUTPUT"
101+
102+ - name: Keep the artefacts as a run artifact
103+ # Downloadable from the run page even if the publish step below fails
104+ # (an old CI node that does not forward /gh answers 403 there).
105+ uses: actions/upload-artifact@v4
106+ with:
107+ name: turbo-core-${{ github.ref_name }}
108+ path: release/${{ github.ref_name }}/
109+ if-no-files-found: error
110+ retention-days: 14
111+
112+ - name: Publish the release
113+ uses: softprops/action-gh-release@v2
114+ with:
115+ tag_name: ${{ github.ref_name }}
116+ name: ${{ github.ref_name }}
117+ body_path: ${{ steps.notes.outputs.path }}
118+ draft: false
119+ prerelease: ${{ contains(github.ref_name, '-') }}
120+ files: |
121+ release/${{ github.ref_name }}/turbo-core-*.tar.gz
122+ release/${{ github.ref_name }}/SHA256SUMS
123+ release/${{ github.ref_name }}/README.md
124+ fail_on_unmatched_files: true
new file mode 100644
@@ -0,0 +1,124 @@
1+name: Release
2+
3+# Publishes a release with the staged artefacts whenever a tag v* is pushed —
4+# what ./01-release.tag.sh does at its last line. They are staged by
5+# ./02-build-releases.sh, the same script one runs on a laptop, so a local
6+# staging and a published one are the same pipeline.
7+#
8+# turbo-core is a library, so the tag alone already publishes the module: the
9+# proxy serves `go get …@TAG` the moment 01 has run, with or without this
10+# workflow. What this adds is the page a person reads, and an archive of the
11+# tagged source with a checksum to verify it against.
12+#
13+# Rickub runs this as an ordinary GitHub Actions workflow. Two platform facts
14+# matter here: the job's GITHUB_TOKEN is the ONLY credential the release API
15+# (the /gh shim behind $GITHUB_API_URL) accepts — a personal token is refused —
16+# and it is read-only unless the workflow asks for `contents: write` below.
17+# That is why there is no longer a token file to keep out of git.
18+#
19+# No workflow_dispatch on purpose: Rickub's dispatch API fires EVERY
20+# dispatchable workflow of a ref, so a repository should declare at most one.
21+on:
22+ push:
23+ tags:
24+ - "v*"
25+
26+permissions:
27+ contents: write
28+
29+concurrency:
30+ group: release-${{ github.ref_name }}
31+ cancel-in-progress: false
32+
33+jobs:
34+ release:
35+ name: publish ${{ github.ref_name }}
36+ runs-on: ubuntu-latest
37+ steps:
38+ - name: Checkout
39+ uses: actions/checkout@v4
40+ with:
41+ # The whole history and the tags: the release notes below are read
42+ # from the annotated tag's message, and ./02-build-releases.sh takes
43+ # its archive from the commit the tag is on.
44+ fetch-depth: 0
45+
46+ - name: Set up Go
47+ uses: actions/setup-go@v5
48+ with:
49+ go-version-file: go.mod
50+ cache: true
51+
52+ - name: go test
53+ # The suite includes tests that run ./01-release.tag.sh against a
54+ # throwaway clone. They skip themselves when they see this, exactly as
55+ # they do when the script itself calls make check — without it, a
56+ # release job would start a release inside itself.
57+ env:
58+ TURBO_CORE_RELEASING: "1"
59+ run: go test ./... -count=1
60+
61+ - name: Stage the release
62+ # release.env is git-ignored, so the tag is passed explicitly and the
63+ # script falls back to "Turbo Core <tag>" for the description.
64+ run: bash ./02-build-releases.sh "${GITHUB_REF_NAME}"
65+
66+ - name: Release notes
67+ id: notes
68+ # The message ./01-release.tag.sh put on the annotated tag (ABOUT in
69+ # release.env), then the one line that installs the module and the
70+ # links to the documentation AT THAT TAG — a release page is not inside
71+ # the repository tree, so a relative path from it 404s, and a link to
72+ # the branch would rot as the branch moves. A lightweight tag has no
73+ # message: the tag name stands in.
74+ run: |
75+ set -euo pipefail
76+ message="$(git for-each-ref "refs/tags/${GITHUB_REF_NAME}" --format='%(contents)' | sed '/^-----BEGIN PGP SIGNATURE-----/,$d')"
77+ if [ -z "$(printf '%s' "${message}" | tr -d '[:space:]')" ]; then
78+ message="Turbo Core ${GITHUB_REF_NAME}"
79+ fi
80+ tree="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/blob/${GITHUB_REF_NAME}"
81+ {
82+ printf '%s\n\n' "${message}"
83+ echo '```bash'
84+ echo "go get $(go list -m)@${GITHUB_REF_NAME}"
85+ echo '```'
86+ echo
87+ echo "Documentation: [English](${tree}/docs/en/README.md) · [Français](${tree}/docs/fr/README.md)"
88+ echo
89+ echo "- Commit: \`${GITHUB_SHA}\`"
90+ echo "- Published by the Release workflow, run #${GITHUB_RUN_NUMBER}, with $(go env GOVERSION)"
91+ echo
92+ echo '## Checksums'
93+ echo
94+ echo 'The archive below is the tagged source. `go get` does not download it — the module proxy serves the module straight from the tag — so it is here to verify against, and for anyone who cannot reach the proxy.'
95+ echo
96+ echo '```'
97+ cat "release/${GITHUB_REF_NAME}/SHA256SUMS"
98+ echo '```'
99+ } > "${RUNNER_TEMP}/notes.md"
100+ echo "path=${RUNNER_TEMP}/notes.md" >> "$GITHUB_OUTPUT"
101+
102+ - name: Keep the artefacts as a run artifact
103+ # Downloadable from the run page even if the publish step below fails
104+ # (an old CI node that does not forward /gh answers 403 there).
105+ uses: actions/upload-artifact@v4
106+ with:
107+ name: turbo-core-${{ github.ref_name }}
108+ path: release/${{ github.ref_name }}/
109+ if-no-files-found: error
110+ retention-days: 14
111+
112+ - name: Publish the release
113+ uses: softprops/action-gh-release@v2
114+ with:
115+ tag_name: ${{ github.ref_name }}
116+ name: ${{ github.ref_name }}
117+ body_path: ${{ steps.notes.outputs.path }}
118+ draft: false
119+ prerelease: ${{ contains(github.ref_name, '-') }}
120+ files: |
121+ release/${{ github.ref_name }}/turbo-core-*.tar.gz
122+ release/${{ github.ref_name }}/SHA256SUMS
123+ release/${{ github.ref_name }}/README.md
124+ fail_on_unmatched_files: true
modified 01-release.tag.sh +8 -2
@@ -5,8 +5,12 @@ upload: a Go module is published by a tag being reachable from its repository,
55 and the module proxy fetches it the first time somebody asks.
66
77 1. Set TAG and ABOUT in release.env
8-2. Run this script: ./01-release.tag.sh
9-3. Point the editors at the new version — see docs/*/how-to/release-the-library.md
8+2. Run this script: ./01-release.tag.sh (commit, push, tag, push the tag)
9+3. Watch the "Release" workflow on Rickub (Actions tab): the tag push starts
10+ it; it stages the source archive with ./02-build-releases.sh and publishes
11+ the release page with it, using the job's own token. No personal token is
12+ needed, and nothing else has to be run by hand.
13+4. Point the editors at the new version — see docs/*/how-to/release-the-library.md
1014 COMMENT
1115
1216 # Without this, a failing step is ignored and the next one runs anyway. That is
@@ -99,6 +103,8 @@ git tag -a "${TAG}" -m "${ABOUT}"
99103 git push origin "${TAG}"
100104
101105 echo "✅ turbo-core ${TAG} published"
106+echo "💡 The tag push started the Release workflow; it stages the archive and"
107+echo " creates the release page. Watch it on the repository's Actions tab."
102108 echo "💡 Now point each editor at it, one at a time:"
103109 echo " go mod edit -require=codeberg.org/turbo-editors/turbo-core@${TAG}"
104110 echo " go mod edit -dropreplace=codeberg.org/turbo-editors/turbo-core"
@@ -5,8 +5,12 @@ upload: a Go module is published by a tag being reachable from its repository,
5 and the module proxy fetches it the first time somebody asks.5 and the module proxy fetches it the first time somebody asks.
6 6
7 1. Set TAG and ABOUT in release.env7 1. Set TAG and ABOUT in release.env
8-2. Run this script: ./01-release.tag.sh8+2. Run this script: ./01-release.tag.sh (commit, push, tag, push the tag)
9-3. Point the editors at the new version — see docs/*/how-to/release-the-library.md9+3. Watch the "Release" workflow on Rickub (Actions tab): the tag push starts
10+ it; it stages the source archive with ./02-build-releases.sh and publishes
11+ the release page with it, using the job's own token. No personal token is
12+ needed, and nothing else has to be run by hand.
13+4. Point the editors at the new version — see docs/*/how-to/release-the-library.md
10 COMMENT14 COMMENT
11 15
12 # Without this, a failing step is ignored and the next one runs anyway. That is16 # Without this, a failing step is ignored and the next one runs anyway. That is
@@ -99,6 +103,8 @@ git tag -a "${TAG}" -m "${ABOUT}"
99 git push origin "${TAG}"103 git push origin "${TAG}"
100 104
101 echo "✅ turbo-core ${TAG} published"105 echo "✅ turbo-core ${TAG} published"
106+echo "💡 The tag push started the Release workflow; it stages the archive and"
107+echo " creates the release page. Watch it on the repository's Actions tab."
102 echo "💡 Now point each editor at it, one at a time:"108 echo "💡 Now point each editor at it, one at a time:"
103 echo " go mod edit -require=codeberg.org/turbo-editors/turbo-core@${TAG}"109 echo " go mod edit -require=codeberg.org/turbo-editors/turbo-core@${TAG}"
104 echo " go mod edit -dropreplace=codeberg.org/turbo-editors/turbo-core"110 echo " go mod edit -dropreplace=codeberg.org/turbo-editors/turbo-core"
added 02-build-releases.sh +166 -0
new file mode 100755
@@ -0,0 +1,166 @@
1+#!/bin/bash
2+: <<'COMMENT'
3+Stage the release artefacts of turbo-core under release/${TAG}/
4+
5+Usage:
6+ ./02-build-releases.sh # TAG and ABOUT come from release.env
7+ ./02-build-releases.sh v0.2.0 # override the tag for this run (what CI does)
8+
9+turbo-core is a library, so where an editor's build cross-compiles one binary
10+per platform this stages the one artefact a Go module has: an archive of the
11+source the tag is on. `go get` never downloads it — the module proxy serves the
12+module straight from the tag, which is why 01 alone already publishes it. The
13+archive is here so a release page has something to verify against, and so the
14+library can be had by somebody who does not reach the proxy.
15+
16+Only the Go toolchain and git are needed. The same command works on a laptop
17+and in a Rickub CI job.
18+
19+What ends up in release/${TAG}/:
20+ turbo-core-<version>.tar.gz the tagged source, under turbo-core-<version>/
21+ SHA256SUMS checksum of the archive
22+ README.md the go get line, the packages, how to verify
23+COMMENT
24+
25+set -euo pipefail
26+
27+# release.env carries TAG ("v0.2.0") and ABOUT (the one-line description). It
28+# is git-ignored (*.env), so a CI job does not have it: there the tag comes
29+# from the command line and ABOUT from the environment, or defaults to the
30+# tag. A tag given on the command line always wins, so a test build never
31+# edits the file.
32+if [ -f release.env ]; then
33+ # shellcheck source=/dev/null
34+ source release.env
35+fi
36+TAG="${1:-${TAG:-}}"
37+ABOUT="${ABOUT:-Turbo Core ${TAG}}"
38+
39+# A tag that is not vMAJOR.MINOR.PATCH[-prerelease] is a typo — and for a Go
40+# module it is worse than a typo: the proxy will not serve a tag it cannot read
41+# as a version, so `go get` would fail on a release that staged perfectly.
42+if ! [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
43+ echo "❌ TAG must look like v1.2.3 or v1.2.3-rc.1, got '${TAG}' (check release.env)"
44+ exit 1
45+fi
46+
47+# 01 refuses this too, but CI runs *this* script on a tag that is already
48+# pushed, without ever running 01. The proxy serves go.mod as written, so a
49+# published library carrying a replace tells every consumer to look for
50+# turbo-core in a directory that does not exist on their machine.
51+if grep -qE '^[[:space:]]*replace[[:space:]]' go.mod; then
52+ echo "❌ go.mod has a replace directive, which a published module must not"
53+ grep -nE '^[[:space:]]*replace[[:space:]]' go.mod
54+ exit 1
55+fi
56+
57+# The tag is "v0.2.0"; the assets carry the bare version, "0.2.0".
58+VERSION="${TAG#v}"
59+RELEASES_DIR="release/${TAG}"
60+ARCHIVE="turbo-core-${VERSION}.tar.gz"
61+MODULE="$(go list -m)"
62+
63+echo "🚀 Staging turbo-core ${TAG}${ABOUT}"
64+echo "🐹 $(go version)"
65+
66+# The archive comes from HEAD, not from the working directory: git archive
67+# writes what is committed. A dirty tree is therefore not an error here, but it
68+# does mean the archive is not what you are looking at — worth saying once.
69+if ! git diff --quiet HEAD 2>/dev/null; then
70+ echo "⚠️ the working tree has uncommitted changes; the archive is HEAD, not what is on disk"
71+fi
72+
73+rm -rf "${RELEASES_DIR}"
74+mkdir -p "${RELEASES_DIR}"
75+
76+echo ""
77+echo "🔨 Checking what ships..."
78+
79+# Every package has to compile and pass vet before it is archived. A library
80+# has no binary whose start proves anything, so this is the equivalent: the
81+# whole module, built the way a consumer's build will build it.
82+if ! go build ./...; then
83+ echo " ❌ go build ./..."
84+ exit 1
85+fi
86+echo " ✅ go build ./..."
87+
88+if ! go vet ./...; then
89+ echo " ❌ go vet ./..."
90+ exit 1
91+fi
92+echo " ✅ go vet ./..."
93+
94+# The prefix is what the archive unpacks into, so extracting it beside other
95+# downloads does not scatter a go.mod and eighteen directories into the
96+# current one.
97+git archive --format=tar.gz --prefix="turbo-core-${VERSION}/" -o "${RELEASES_DIR}/${ARCHIVE}" HEAD
98+echo "${ARCHIVE}"
99+
100+# Extracting the archive and building it is the one proof that what ships
101+# builds on its own — that nothing needed was left untracked, gitignored, or
102+# only present in this checkout. The dependencies come from the shared module
103+# cache, which the build above has just filled.
104+extracted="$(mktemp -d)"
105+trap 'rm -rf "${extracted}"' EXIT
106+tar -xzf "${RELEASES_DIR}/${ARCHIVE}" -C "${extracted}"
107+if ! (cd "${extracted}/turbo-core-${VERSION}" && go build ./... >/dev/null); then
108+ echo " ❌ the extracted archive does not build on its own"
109+ exit 1
110+fi
111+echo " ✅ the extracted archive builds on its own"
112+
113+# checksum runs whichever of the two tools this machine has: sha256sum on
114+# Linux, shasum on macOS.
115+checksum() {
116+ if command -v sha256sum >/dev/null 2>&1; then
117+ sha256sum "$@"
118+ else
119+ shasum -a 256 "$@"
120+ fi
121+}
122+
123+# Names only (no directory), which is what `sha256sum -c` expects to read next
124+# to the downloaded file.
125+(cd "${RELEASES_DIR}" && checksum "${ARCHIVE}" >SHA256SUMS)
126+echo " ✅ SHA256SUMS"
127+
128+# packageTable lists the library's packages with the first line of each one's
129+# doc comment, so the README grows and shrinks with the module rather than
130+# repeating it by hand. Packages with no doc comment — the module root, which
131+# holds nothing but this tooling's tests — are left out, and a pipe in a
132+# synopsis is escaped so it cannot break the table.
133+packageTable() {
134+ printf '| Package | What it is |\n|---|---|\n'
135+ go list -f '{{.ImportPath}}|{{.Doc}}' ./... | while IFS='|' read -r path doc; do
136+ [ -z "${doc}" ] && continue
137+ printf '| `%s` | %s |\n' "${path#"${MODULE}"/}" "${doc//|/\\|}"
138+ done
139+}
140+
141+cat >"${RELEASES_DIR}/README.md" <<EOM
142+# Turbo Core ${TAG}
143+
144+${ABOUT}
145+
146+The library the Turbo editors are built from. Built and checked with $(go env GOVERSION).
147+
148+## Using it
149+
150+ go get ${MODULE}@${TAG}
151+
152+The module proxy serves this straight from the tag; the archive beside this file is the same source, for verifying against or for working without the proxy.
153+
154+## What is in it
155+
156+$(packageTable)
157+
158+## Verifying the download
159+
160+ sha256sum -c SHA256SUMS --ignore-missing # shasum -a 256 -c on macOS
161+EOM
162+echo " ✅ README.md"
163+
164+echo ""
165+echo "✨ Staging complete!"
166+ls -lh "${RELEASES_DIR}"
new file mode 100755
@@ -0,0 +1,166 @@
1+#!/bin/bash
2+: <<'COMMENT'
3+Stage the release artefacts of turbo-core under release/${TAG}/
4+
5+Usage:
6+ ./02-build-releases.sh # TAG and ABOUT come from release.env
7+ ./02-build-releases.sh v0.2.0 # override the tag for this run (what CI does)
8+
9+turbo-core is a library, so where an editor's build cross-compiles one binary
10+per platform this stages the one artefact a Go module has: an archive of the
11+source the tag is on. `go get` never downloads it — the module proxy serves the
12+module straight from the tag, which is why 01 alone already publishes it. The
13+archive is here so a release page has something to verify against, and so the
14+library can be had by somebody who does not reach the proxy.
15+
16+Only the Go toolchain and git are needed. The same command works on a laptop
17+and in a Rickub CI job.
18+
19+What ends up in release/${TAG}/:
20+ turbo-core-<version>.tar.gz the tagged source, under turbo-core-<version>/
21+ SHA256SUMS checksum of the archive
22+ README.md the go get line, the packages, how to verify
23+COMMENT
24+
25+set -euo pipefail
26+
27+# release.env carries TAG ("v0.2.0") and ABOUT (the one-line description). It
28+# is git-ignored (*.env), so a CI job does not have it: there the tag comes
29+# from the command line and ABOUT from the environment, or defaults to the
30+# tag. A tag given on the command line always wins, so a test build never
31+# edits the file.
32+if [ -f release.env ]; then
33+ # shellcheck source=/dev/null
34+ source release.env
35+fi
36+TAG="${1:-${TAG:-}}"
37+ABOUT="${ABOUT:-Turbo Core ${TAG}}"
38+
39+# A tag that is not vMAJOR.MINOR.PATCH[-prerelease] is a typo — and for a Go
40+# module it is worse than a typo: the proxy will not serve a tag it cannot read
41+# as a version, so `go get` would fail on a release that staged perfectly.
42+if ! [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
43+ echo "❌ TAG must look like v1.2.3 or v1.2.3-rc.1, got '${TAG}' (check release.env)"
44+ exit 1
45+fi
46+
47+# 01 refuses this too, but CI runs *this* script on a tag that is already
48+# pushed, without ever running 01. The proxy serves go.mod as written, so a
49+# published library carrying a replace tells every consumer to look for
50+# turbo-core in a directory that does not exist on their machine.
51+if grep -qE '^[[:space:]]*replace[[:space:]]' go.mod; then
52+ echo "❌ go.mod has a replace directive, which a published module must not"
53+ grep -nE '^[[:space:]]*replace[[:space:]]' go.mod
54+ exit 1
55+fi
56+
57+# The tag is "v0.2.0"; the assets carry the bare version, "0.2.0".
58+VERSION="${TAG#v}"
59+RELEASES_DIR="release/${TAG}"
60+ARCHIVE="turbo-core-${VERSION}.tar.gz"
61+MODULE="$(go list -m)"
62+
63+echo "🚀 Staging turbo-core ${TAG}${ABOUT}"
64+echo "🐹 $(go version)"
65+
66+# The archive comes from HEAD, not from the working directory: git archive
67+# writes what is committed. A dirty tree is therefore not an error here, but it
68+# does mean the archive is not what you are looking at — worth saying once.
69+if ! git diff --quiet HEAD 2>/dev/null; then
70+ echo "⚠️ the working tree has uncommitted changes; the archive is HEAD, not what is on disk"
71+fi
72+
73+rm -rf "${RELEASES_DIR}"
74+mkdir -p "${RELEASES_DIR}"
75+
76+echo ""
77+echo "🔨 Checking what ships..."
78+
79+# Every package has to compile and pass vet before it is archived. A library
80+# has no binary whose start proves anything, so this is the equivalent: the
81+# whole module, built the way a consumer's build will build it.
82+if ! go build ./...; then
83+ echo " ❌ go build ./..."
84+ exit 1
85+fi
86+echo " ✅ go build ./..."
87+
88+if ! go vet ./...; then
89+ echo " ❌ go vet ./..."
90+ exit 1
91+fi
92+echo " ✅ go vet ./..."
93+
94+# The prefix is what the archive unpacks into, so extracting it beside other
95+# downloads does not scatter a go.mod and eighteen directories into the
96+# current one.
97+git archive --format=tar.gz --prefix="turbo-core-${VERSION}/" -o "${RELEASES_DIR}/${ARCHIVE}" HEAD
98+echo "${ARCHIVE}"
99+
100+# Extracting the archive and building it is the one proof that what ships
101+# builds on its own — that nothing needed was left untracked, gitignored, or
102+# only present in this checkout. The dependencies come from the shared module
103+# cache, which the build above has just filled.
104+extracted="$(mktemp -d)"
105+trap 'rm -rf "${extracted}"' EXIT
106+tar -xzf "${RELEASES_DIR}/${ARCHIVE}" -C "${extracted}"
107+if ! (cd "${extracted}/turbo-core-${VERSION}" && go build ./... >/dev/null); then
108+ echo " ❌ the extracted archive does not build on its own"
109+ exit 1
110+fi
111+echo " ✅ the extracted archive builds on its own"
112+
113+# checksum runs whichever of the two tools this machine has: sha256sum on
114+# Linux, shasum on macOS.
115+checksum() {
116+ if command -v sha256sum >/dev/null 2>&1; then
117+ sha256sum "$@"
118+ else
119+ shasum -a 256 "$@"
120+ fi
121+}
122+
123+# Names only (no directory), which is what `sha256sum -c` expects to read next
124+# to the downloaded file.
125+(cd "${RELEASES_DIR}" && checksum "${ARCHIVE}" >SHA256SUMS)
126+echo " ✅ SHA256SUMS"
127+
128+# packageTable lists the library's packages with the first line of each one's
129+# doc comment, so the README grows and shrinks with the module rather than
130+# repeating it by hand. Packages with no doc comment — the module root, which
131+# holds nothing but this tooling's tests — are left out, and a pipe in a
132+# synopsis is escaped so it cannot break the table.
133+packageTable() {
134+ printf '| Package | What it is |\n|---|---|\n'
135+ go list -f '{{.ImportPath}}|{{.Doc}}' ./... | while IFS='|' read -r path doc; do
136+ [ -z "${doc}" ] && continue
137+ printf '| `%s` | %s |\n' "${path#"${MODULE}"/}" "${doc//|/\\|}"
138+ done
139+}
140+
141+cat >"${RELEASES_DIR}/README.md" <<EOM
142+# Turbo Core ${TAG}
143+
144+${ABOUT}
145+
146+The library the Turbo editors are built from. Built and checked with $(go env GOVERSION).
147+
148+## Using it
149+
150+ go get ${MODULE}@${TAG}
151+
152+The module proxy serves this straight from the tag; the archive beside this file is the same source, for verifying against or for working without the proxy.
153+
154+## What is in it
155+
156+$(packageTable)
157+
158+## Verifying the download
159+
160+ sha256sum -c SHA256SUMS --ignore-missing # shasum -a 256 -c on macOS
161+EOM
162+echo " ✅ README.md"
163+
164+echo ""
165+echo "✨ Staging complete!"
166+ls -lh "${RELEASES_DIR}"
deleted 02-release.publish.sh +0 -175
deleted file mode 100755
@@ -1,175 +0,0 @@
1-#!/bin/bash
2-: <<'COMMENT'
3-Create the release page on Codeberg for a tag 01-release.tag.sh already pushed.
4-
5- ./02-release.publish.sh create it
6- ./02-release.publish.sh --dry-run show what would be sent, send nothing
7-
8-This does NOT publish the module. A Go module is published by its tag being
9-reachable, and `go get codeberg.org/turbo-editors/turbo-core@TAG` already works
10-the moment 01 has run. What this adds is the page a person reads: the notes, and
11-somewhere to link to.
12-
13-There is deliberately no 03 or 04 here. Those build and attach binaries, and a
14-library has none — its artefact is the tag.
15-COMMENT
16-
17-# `set -e` is safe here, unlike in the editors' own 02, which uses the
18-# `read -r -d '' DATA` idiom — that always exits non-zero by design, so adding
19-# -e there kills the script on its first line. The JSON below is built with jq
20-# instead, which both avoids that trap and escapes the values properly.
21-set -euo pipefail
22-
23-dry_run=false
24-case "${1:-}" in
25---dry-run) dry_run=true ;;
26-"") ;;
27-*)
28- echo "usage: $0 [--dry-run]"
29- exit 1
30- ;;
31-esac
32-
33-for tool in curl jq; do
34- if ! command -v "${tool}" >/dev/null; then
35- echo "${tool} is needed and is not installed"
36- exit 1
37- fi
38-done
39-
40-if [ ! -f release.env ]; then
41- echo "❌ release.env is missing"
42- echo "💡 It holds the version and the repository:"
43- echo ' TAG="v0.1.0"'
44- echo ' ABOUT="The Turbo editor library"'
45- echo ' OWNER="turbo-editors"'
46- echo ' REPO="turbo-core"'
47- exit 1
48-fi
49-
50-set -o allexport
51-# shellcheck source=/dev/null
52-source release.env
53-# The token lives in its own file so that release.env can be pasted into an
54-# issue without leaking it. Both are gitignored by *.env.
55-if [ -f turbo-core.token.env ]; then
56- # shellcheck source=/dev/null
57- source turbo-core.token.env
58-fi
59-set +o allexport
60-
61-: "${TAG:?TAG is not set in release.env}"
62-: "${OWNER:?OWNER is not set in release.env}"
63-: "${REPO:?REPO is not set in release.env}"
64-ABOUT="${ABOUT:-${TAG}}"
65-
66-if [ -z "${TOKEN:-}" ]; then
67- echo "❌ TOKEN is not set"
68- echo "💡 Put it in turbo-core.token.env (gitignored):"
69- echo ' TOKEN=your-codeberg-application-token'
70- echo " Codeberg → Settings → Applications → Generate token, scope: repository"
71- exit 1
72-fi
73-
74-# A release page for a tag the remote has never seen is a page nobody can use:
75-# the download links 404 and `go get` fails. Ask origin rather than trusting the
76-# local ref, which is exactly the state 01 exists to get out of.
77-# "Not there" and "could not ask" are different answers and must not be
78-# conflated: ls-remote exits 2 when the ref is absent and 128 when it cannot
79-# reach the remote at all. Treating the second as the first refuses a perfectly
80-# good release from any machine with no key loaded.
81-set +e
82-git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1
83-lookup=$?
84-set -e
85-
86-case ${lookup} in
87-0) ;;
88-2)
89- echo "${TAG} is not on origin"
90- echo "💡 Run ./01-release.tag.sh first — it pushes the branch, then the tag."
91- exit 1
92- ;;
93-*)
94- echo "⚠️ cannot reach origin to check that ${TAG} is pushed; carrying on."
95- echo " If it is not, Codeberg will refuse the release below."
96- ;;
97-esac
98-
99-# The body is the notes plus the two lines a reader actually needs. A library's
100-# release page whose only content is its own version number tells nobody how to
101-# use it.
102-#
103-# The links are absolute: a release page is not inside the repository tree, so a
104-# relative path from it 404s.
105-module="$(go list -m)"
106-repo_url="https://codeberg.org/${OWNER}/${REPO}"
107-body="$(
108- cat <<-EOM
109- ${ABOUT}
110-
111- \`\`\`bash
112- go get ${module}@${TAG}
113- \`\`\`
114-
115- Documentation: [English](${repo_url}/src/tag/${TAG}/docs/en/README.md) · [Français](${repo_url}/src/tag/${TAG}/docs/fr/README.md)
116- EOM
117-)"
118-
119-# jq builds the JSON, so a quote, a newline or a backtick in ABOUT cannot break
120-# the request — which hand-written JSON in a heredoc does silently.
121-payload="$(jq -n \
122- --arg tag "${TAG}" \
123- --arg name "${TAG}" \
124- --arg body "${body}" \
125- '{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')"
126-
127-url="https://codeberg.org/api/v1/repos/${OWNER}/${REPO}/releases"
128-
129-if ${dry_run}; then
130- echo "POST ${url}"
131- echo "${payload}"
132- echo "✅ dry run: nothing was sent"
133- exit 0
134-fi
135-
136-echo "Creating the release page for ${TAG} on ${OWNER}/${REPO}"
137-
138-# The status is separated from the body so the script can say which failure this
139-# is. A bare curl prints the API's JSON and leaves the reader to guess.
140-response="$(mktemp)"
141-trap 'rm -f "${response}"' EXIT
142-status="$(curl -sS -o "${response}" -w '%{http_code}' \
143- -X POST \
144- -H "Authorization: token ${TOKEN}" \
145- -H "Content-Type: application/json" \
146- "${url}" \
147- -d "${payload}")"
148-
149-case "${status}" in
150-201)
151- echo "${TAG} published: $(jq -r '.html_url' "${response}")"
152- ;;
153-409)
154- echo "❌ a release for ${TAG} already exists"
155- echo "💡 Delete it on Codeberg, or bump TAG in release.env. A published"
156- echo " version is not worth moving: consumers pin it and the module proxy"
157- echo " caches what it fetched."
158- exit 1
159- ;;
160-401 | 403)
161- echo "${status}: the token was refused"
162- echo "💡 Check TOKEN in turbo-core.token.env, and that its scope covers this repository."
163- exit 1
164- ;;
165-404)
166- echo "❌ 404: no repository ${OWNER}/${REPO}"
167- echo "💡 Check OWNER and REPO in release.env."
168- exit 1
169- ;;
170-*)
171- echo "${status} from Codeberg:"
172- cat "${response}"
173- exit 1
174- ;;
175-esac
deleted file mode 100755
@@ -1,175 +0,0 @@
1-#!/bin/bash
2-: <<'COMMENT'
3-Create the release page on Codeberg for a tag 01-release.tag.sh already pushed.
4-
5- ./02-release.publish.sh create it
6- ./02-release.publish.sh --dry-run show what would be sent, send nothing
7-
8-This does NOT publish the module. A Go module is published by its tag being
9-reachable, and `go get codeberg.org/turbo-editors/turbo-core@TAG` already works
10-the moment 01 has run. What this adds is the page a person reads: the notes, and
11-somewhere to link to.
12-
13-There is deliberately no 03 or 04 here. Those build and attach binaries, and a
14-library has none — its artefact is the tag.
15-COMMENT
16-
17-# `set -e` is safe here, unlike in the editors' own 02, which uses the
18-# `read -r -d '' DATA` idiom — that always exits non-zero by design, so adding
19-# -e there kills the script on its first line. The JSON below is built with jq
20-# instead, which both avoids that trap and escapes the values properly.
21-set -euo pipefail
22-
23-dry_run=false
24-case "${1:-}" in
25---dry-run) dry_run=true ;;
26-"") ;;
27-*)
28- echo "usage: $0 [--dry-run]"
29- exit 1
30- ;;
31-esac
32-
33-for tool in curl jq; do
34- if ! command -v "${tool}" >/dev/null; then
35- echo "${tool} is needed and is not installed"
36- exit 1
37- fi
38-done
39-
40-if [ ! -f release.env ]; then
41- echo "❌ release.env is missing"
42- echo "💡 It holds the version and the repository:"
43- echo ' TAG="v0.1.0"'
44- echo ' ABOUT="The Turbo editor library"'
45- echo ' OWNER="turbo-editors"'
46- echo ' REPO="turbo-core"'
47- exit 1
48-fi
49-
50-set -o allexport
51-# shellcheck source=/dev/null
52-source release.env
53-# The token lives in its own file so that release.env can be pasted into an
54-# issue without leaking it. Both are gitignored by *.env.
55-if [ -f turbo-core.token.env ]; then
56- # shellcheck source=/dev/null
57- source turbo-core.token.env
58-fi
59-set +o allexport
60-
61-: "${TAG:?TAG is not set in release.env}"
62-: "${OWNER:?OWNER is not set in release.env}"
63-: "${REPO:?REPO is not set in release.env}"
64-ABOUT="${ABOUT:-${TAG}}"
65-
66-if [ -z "${TOKEN:-}" ]; then
67- echo "❌ TOKEN is not set"
68- echo "💡 Put it in turbo-core.token.env (gitignored):"
69- echo ' TOKEN=your-codeberg-application-token'
70- echo " Codeberg → Settings → Applications → Generate token, scope: repository"
71- exit 1
72-fi
73-
74-# A release page for a tag the remote has never seen is a page nobody can use:
75-# the download links 404 and `go get` fails. Ask origin rather than trusting the
76-# local ref, which is exactly the state 01 exists to get out of.
77-# "Not there" and "could not ask" are different answers and must not be
78-# conflated: ls-remote exits 2 when the ref is absent and 128 when it cannot
79-# reach the remote at all. Treating the second as the first refuses a perfectly
80-# good release from any machine with no key loaded.
81-set +e
82-git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1
83-lookup=$?
84-set -e
85-
86-case ${lookup} in
87-0) ;;
88-2)
89- echo "${TAG} is not on origin"
90- echo "💡 Run ./01-release.tag.sh first — it pushes the branch, then the tag."
91- exit 1
92- ;;
93-*)
94- echo "⚠️ cannot reach origin to check that ${TAG} is pushed; carrying on."
95- echo " If it is not, Codeberg will refuse the release below."
96- ;;
97-esac
98-
99-# The body is the notes plus the two lines a reader actually needs. A library's
100-# release page whose only content is its own version number tells nobody how to
101-# use it.
102-#
103-# The links are absolute: a release page is not inside the repository tree, so a
104-# relative path from it 404s.
105-module="$(go list -m)"
106-repo_url="https://codeberg.org/${OWNER}/${REPO}"
107-body="$(
108- cat <<-EOM
109- ${ABOUT}
110-
111- \`\`\`bash
112- go get ${module}@${TAG}
113- \`\`\`
114-
115- Documentation: [English](${repo_url}/src/tag/${TAG}/docs/en/README.md) · [Français](${repo_url}/src/tag/${TAG}/docs/fr/README.md)
116- EOM
117-)"
118-
119-# jq builds the JSON, so a quote, a newline or a backtick in ABOUT cannot break
120-# the request — which hand-written JSON in a heredoc does silently.
121-payload="$(jq -n \
122- --arg tag "${TAG}" \
123- --arg name "${TAG}" \
124- --arg body "${body}" \
125- '{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')"
126-
127-url="https://codeberg.org/api/v1/repos/${OWNER}/${REPO}/releases"
128-
129-if ${dry_run}; then
130- echo "POST ${url}"
131- echo "${payload}"
132- echo "✅ dry run: nothing was sent"
133- exit 0
134-fi
135-
136-echo "Creating the release page for ${TAG} on ${OWNER}/${REPO}"
137-
138-# The status is separated from the body so the script can say which failure this
139-# is. A bare curl prints the API's JSON and leaves the reader to guess.
140-response="$(mktemp)"
141-trap 'rm -f "${response}"' EXIT
142-status="$(curl -sS -o "${response}" -w '%{http_code}' \
143- -X POST \
144- -H "Authorization: token ${TOKEN}" \
145- -H "Content-Type: application/json" \
146- "${url}" \
147- -d "${payload}")"
148-
149-case "${status}" in
150-201)
151- echo "${TAG} published: $(jq -r '.html_url' "${response}")"
152- ;;
153-409)
154- echo "❌ a release for ${TAG} already exists"
155- echo "💡 Delete it on Codeberg, or bump TAG in release.env. A published"
156- echo " version is not worth moving: consumers pin it and the module proxy"
157- echo " caches what it fetched."
158- exit 1
159- ;;
160-401 | 403)
161- echo "${status}: the token was refused"
162- echo "💡 Check TOKEN in turbo-core.token.env, and that its scope covers this repository."
163- exit 1
164- ;;
165-404)
166- echo "❌ 404: no repository ${OWNER}/${REPO}"
167- echo "💡 Check OWNER and REPO in release.env."
168- exit 1
169- ;;
170-*)
171- echo "${status} from Codeberg:"
172- cat "${response}"
173- exit 1
174- ;;
175-esac
modified README.md +1 -1
@@ -6,7 +6,7 @@ turbo-core is a Turbo C-style terminal IDE with a hole where the language goes:
66
77 An editor built on it is a command, a `profile.Profile`, and a scanner:
88
9-- **[Turbo Go](https://codeberg.org/turbo-editors/turbo-go)** — `codeberg.org/turbo-editors/turbo-go`
9+- **[Turbo Go](https://rickub.com/turbo-editors/turbo-go)** — `codeberg.org/turbo-editors/turbo-go`
1010 - **[Turbo Rust](https://codeberg.org/turbo-editors/turbo-rust)** — `codeberg.org/turbo-editors/turbo-rust`
1111 - **[Turbo Python](https://codeberg.org/turbo-editors/turbo-python)** — `codeberg.org/turbo-editors/turbo-python`
1212 - **[Turbo MoonBit](https://codeberg.org/turbo-editors/turbo-moonbit)** — `codeberg.org/turbo-editors/turbo-moonbit`
@@ -6,7 +6,7 @@ turbo-core is a Turbo C-style terminal IDE with a hole where the language goes:
6 6
7 An editor built on it is a command, a `profile.Profile`, and a scanner:7 An editor built on it is a command, a `profile.Profile`, and a scanner:
8 8
9-- **[Turbo Go](https://codeberg.org/turbo-editors/turbo-go)** — `codeberg.org/turbo-editors/turbo-go`9+- **[Turbo Go](https://rickub.com/turbo-editors/turbo-go)** — `codeberg.org/turbo-editors/turbo-go`
10 - **[Turbo Rust](https://codeberg.org/turbo-editors/turbo-rust)** — `codeberg.org/turbo-editors/turbo-rust`10 - **[Turbo Rust](https://codeberg.org/turbo-editors/turbo-rust)** — `codeberg.org/turbo-editors/turbo-rust`
11 - **[Turbo Python](https://codeberg.org/turbo-editors/turbo-python)** — `codeberg.org/turbo-editors/turbo-python`11 - **[Turbo Python](https://codeberg.org/turbo-editors/turbo-python)** — `codeberg.org/turbo-editors/turbo-python`
12 - **[Turbo MoonBit](https://codeberg.org/turbo-editors/turbo-moonbit)** — `codeberg.org/turbo-editors/turbo-moonbit`12 - **[Turbo MoonBit](https://codeberg.org/turbo-editors/turbo-moonbit)** — `codeberg.org/turbo-editors/turbo-moonbit`
modified docs/en/how-to/release-the-library.md +22 -11
@@ -2,19 +2,21 @@
22
33 This guide shows how to publish a version of turbo-core that the editors can depend on. It assumes commit access to the repository.
44
5-turbo-core is a Go module with no binary and no release artefacts: publishing it is tagging it. `01-release.tag.sh` does the whole thing.
5+turbo-core is a Go module with no binary: publishing it is tagging it, and the module proxy serves `go get …@TAG` the moment the tag is reachable. You run one script; pushing the tag starts a workflow that does the rest.
66
77 ## Steps
88
99 ### 1. Say which version
1010
11-Create `release.env` — it is gitignored, because a release token belongs in one too:
11+Create `release.env` — it is gitignored, so CI never sees it:
1212
1313 ```sh
1414 TAG="v0.1.0"
1515 ABOUT="The Turbo editor library"
1616 ```
1717
18+`ABOUT` becomes the tag's message, and the workflow reads it back off the tag to head the release page.
19+
1820 ### 2. Run the script
1921
2022 ```bash
@@ -25,20 +27,27 @@ It runs `make check`, refuses a tag already taken here or on origin, refuses a `
2527
2628 That order matters: a tag pushed before the branch points at a commit the remote has never seen, and a tag created before a failed push is left behind for somebody to find.
2729
28-### 3. Create the release page
30+That is the last thing you run by hand. The library is published once the tag is on origin.
31+
32+### 3. Watch the Release workflow
33+
34+The tag push starts `.github/workflows/release.yml`. Follow it on the repository's Actions tab; nothing here needs you unless it goes red.
2935
30-The tag is enough for `go get`; this adds the page a person reads.
36+It runs the suite, stages the artefacts with `./02-build-releases.sh`, and creates the release page with them. The notes are the tag's message, the one line that installs the module, and links to the documentation **at that tag** rather than at the branch — a release page is not inside the repository tree, so a relative link from it 404s and a link to the branch rots as the branch moves.
37+
38+The job publishes with its own `GITHUB_TOKEN`, which is the only credential Rickub's release API accepts — a personal token is refused. There is nothing to configure and no secret to keep.
39+
40+You can see what it will stage without publishing anything:
3141
3242 ```bash
33-./02-release.publish.sh --dry-run # show the request, send nothing
34-./02-release.publish.sh # create it
43+./02-build-releases.sh v0.1.0 # writes release/v0.1.0/, pushes nothing
3544 ```
3645
37-It needs `curl` and `jq`, `OWNER` and `REPO` in `release.env`, and a Codeberg application token in `turbo-core.token.env` — a separate file so `release.env` can be shown to somebody without leaking it. Both are covered by `*.env` in `.gitignore`.
46+It compiles every package, vets them, archives the source the tag is on, extracts that archive and builds it again — the one proof that what ships builds on its own, with nothing left untracked — then checksums it and writes the README that goes on the page.
3847
39-The notes it writes are `ABOUT`, the one line that installs the module, and links to the documentation **at that tag** rather than at the branch.
48+That archive is not how anybody installs the library: the proxy serves the module straight from the tag. It is there to verify a release against, and for anyone who cannot reach the proxy.
4049
41-There is no `03` or `04` here. Those build and attach binaries in the editors; a library has none — its artefact is the tag.
50+There is no `03` or `04` here. Those build and attach binaries in the editors; a library has none.
4251
4352 ### 4. Point the editors at it
4453
@@ -64,7 +73,7 @@ git tag -a v0.1.0 -m "The Turbo editor library"
6473 git push origin v0.1.0
6574 ```
6675
67-Push the branch **before** the tag, for the reason above.
76+Push the branch **before** the tag, for the reason above. The workflow triggers on the tag push however it was made, so the release page still appears.
6877
6978 ### You are developing across the three repositories
7079
@@ -87,7 +96,9 @@ You do not. Several editors may pin it and the module proxy caches what it fetch
8796
8897 ## What to watch out for
8998
90-The script runs `make check`, and the suite it runs includes tests that run *this script* against a throwaway clone. They skip themselves when `TURBO_CORE_RELEASING` is set, which the script exports before calling make. Removing that line makes a release recurse until something runs out.
99+The script runs `make check`, and the suite it runs includes tests that run *this script* against a throwaway clone. They skip themselves when `TURBO_CORE_RELEASING` is set, which the script exports before calling make. Removing that line makes a release recurse until something runs out. The workflow sets the same variable for its own `go test` step, because there nothing calls the script that would have set it.
100+
101+The workflow is the repository's only one, on purpose: Rickub's dispatch API fires every dispatchable workflow of a ref, so a repository should declare at most one — which is also why this one has no `workflow_dispatch` and is reached only by pushing a tag.
91102
92103 ## See also
93104
@@ -2,19 +2,21 @@
2 2
3 This guide shows how to publish a version of turbo-core that the editors can depend on. It assumes commit access to the repository.3 This guide shows how to publish a version of turbo-core that the editors can depend on. It assumes commit access to the repository.
4 4
5-turbo-core is a Go module with no binary and no release artefacts: publishing it is tagging it. `01-release.tag.sh` does the whole thing.5+turbo-core is a Go module with no binary: publishing it is tagging it, and the module proxy serves `go get …@TAG` the moment the tag is reachable. You run one script; pushing the tag starts a workflow that does the rest.
6 6
7 ## Steps7 ## Steps
8 8
9 ### 1. Say which version9 ### 1. Say which version
10 10
11-Create `release.env` — it is gitignored, because a release token belongs in one too:11+Create `release.env` — it is gitignored, so CI never sees it:
12 12
13 ```sh13 ```sh
14 TAG="v0.1.0"14 TAG="v0.1.0"
15 ABOUT="The Turbo editor library"15 ABOUT="The Turbo editor library"
16 ```16 ```
17 17
18+`ABOUT` becomes the tag's message, and the workflow reads it back off the tag to head the release page.
19+
18 ### 2. Run the script20 ### 2. Run the script
19 21
20 ```bash22 ```bash
@@ -25,20 +27,27 @@ It runs `make check`, refuses a tag already taken here or on origin, refuses a `
25 27
26 That order matters: a tag pushed before the branch points at a commit the remote has never seen, and a tag created before a failed push is left behind for somebody to find.28 That order matters: a tag pushed before the branch points at a commit the remote has never seen, and a tag created before a failed push is left behind for somebody to find.
27 29
28-### 3. Create the release page30+That is the last thing you run by hand. The library is published once the tag is on origin.
31+
32+### 3. Watch the Release workflow
33+
34+The tag push starts `.github/workflows/release.yml`. Follow it on the repository's Actions tab; nothing here needs you unless it goes red.
29 35
30-The tag is enough for `go get`; this adds the page a person reads.36+It runs the suite, stages the artefacts with `./02-build-releases.sh`, and creates the release page with them. The notes are the tag's message, the one line that installs the module, and links to the documentation **at that tag** rather than at the branch — a release page is not inside the repository tree, so a relative link from it 404s and a link to the branch rots as the branch moves.
37+
38+The job publishes with its own `GITHUB_TOKEN`, which is the only credential Rickub's release API accepts — a personal token is refused. There is nothing to configure and no secret to keep.
39+
40+You can see what it will stage without publishing anything:
31 41
32 ```bash42 ```bash
33-./02-release.publish.sh --dry-run # show the request, send nothing43+./02-build-releases.sh v0.1.0 # writes release/v0.1.0/, pushes nothing
34-./02-release.publish.sh # create it
35 ```44 ```
36 45
37-It needs `curl` and `jq`, `OWNER` and `REPO` in `release.env`, and a Codeberg application token in `turbo-core.token.env` — a separate file so `release.env` can be shown to somebody without leaking it. Both are covered by `*.env` in `.gitignore`.46+It compiles every package, vets them, archives the source the tag is on, extracts that archive and builds it again — the one proof that what ships builds on its own, with nothing left untracked — then checksums it and writes the README that goes on the page.
38 47
39-The notes it writes are `ABOUT`, the one line that installs the module, and links to the documentation **at that tag** rather than at the branch.48+That archive is not how anybody installs the library: the proxy serves the module straight from the tag. It is there to verify a release against, and for anyone who cannot reach the proxy.
40 49
41-There is no `03` or `04` here. Those build and attach binaries in the editors; a library has none — its artefact is the tag.50+There is no `03` or `04` here. Those build and attach binaries in the editors; a library has none.
42 51
43 ### 4. Point the editors at it52 ### 4. Point the editors at it
44 53
@@ -64,7 +73,7 @@ git tag -a v0.1.0 -m "The Turbo editor library"
64 git push origin v0.1.073 git push origin v0.1.0
65 ```74 ```
66 75
67-Push the branch **before** the tag, for the reason above.76+Push the branch **before** the tag, for the reason above. The workflow triggers on the tag push however it was made, so the release page still appears.
68 77
69 ### You are developing across the three repositories78 ### You are developing across the three repositories
70 79
@@ -87,7 +96,9 @@ You do not. Several editors may pin it and the module proxy caches what it fetch
87 96
88 ## What to watch out for97 ## What to watch out for
89 98
90-The script runs `make check`, and the suite it runs includes tests that run *this script* against a throwaway clone. They skip themselves when `TURBO_CORE_RELEASING` is set, which the script exports before calling make. Removing that line makes a release recurse until something runs out.99+The script runs `make check`, and the suite it runs includes tests that run *this script* against a throwaway clone. They skip themselves when `TURBO_CORE_RELEASING` is set, which the script exports before calling make. Removing that line makes a release recurse until something runs out. The workflow sets the same variable for its own `go test` step, because there nothing calls the script that would have set it.
100+
101+The workflow is the repository's only one, on purpose: Rickub's dispatch API fires every dispatchable workflow of a ref, so a repository should declare at most one — which is also why this one has no `workflow_dispatch` and is reached only by pushing a tag.
91 102
92 ## See also103 ## See also
93 104
modified docs/en/how-to/test-without-publishing.md +6 -3
@@ -79,13 +79,16 @@ If you do use it, put the `require` line back to a published version and delete
7979
8080 ## Testing the published shape, not just the code
8181
82-A workspace proves your code works. It does not prove the *module* works: it says nothing about whether the tag contains what you think, whether `go.sum` is right, or whether a clean clone builds. For that you have to publish — first to a tag, then to a release page:
82+A workspace proves your code works. It does not prove the *module* works: it says nothing about whether the tag contains what you think, whether `go.sum` is right, or whether a clean clone builds.
83+
84+The last of those you can answer without publishing anything — `./02-build-releases.sh v0.3.0` archives the tracked source, extracts it somewhere else and builds it there. The rest needs a real tag:
8385
8486 ```bash
85-TAG=v0.3.0 ./01-release.tag.sh
86-TAG=v0.3.0 ./02-release.publish.sh
87+./01-release.tag.sh
8788 ```
8889
90+The release page follows on its own: pushing the tag starts the Release workflow, which stages the same artefacts and publishes them. See [Release the library](release-the-library.md).
91+
8992 Then, in each editor, with **no** workspace and **no** replace:
9093
9194 ```bash
@@ -79,13 +79,16 @@ If you do use it, put the `require` line back to a published version and delete
79 79
80 ## Testing the published shape, not just the code80 ## Testing the published shape, not just the code
81 81
82-A workspace proves your code works. It does not prove the *module* works: it says nothing about whether the tag contains what you think, whether `go.sum` is right, or whether a clean clone builds. For that you have to publish — first to a tag, then to a release page:82+A workspace proves your code works. It does not prove the *module* works: it says nothing about whether the tag contains what you think, whether `go.sum` is right, or whether a clean clone builds.
83+
84+The last of those you can answer without publishing anything — `./02-build-releases.sh v0.3.0` archives the tracked source, extracts it somewhere else and builds it there. The rest needs a real tag:
83 85
84 ```bash86 ```bash
85-TAG=v0.3.0 ./01-release.tag.sh87+./01-release.tag.sh
86-TAG=v0.3.0 ./02-release.publish.sh
87 ```88 ```
88 89
90+The release page follows on its own: pushing the tag starts the Release workflow, which stages the same artefacts and publishes them. See [Release the library](release-the-library.md).
91+
89 Then, in each editor, with **no** workspace and **no** replace:92 Then, in each editor, with **no** workspace and **no** replace:
90 93
91 ```bash94 ```bash
modified docs/en/tutorials/build-an-editor.md +1 -1
@@ -10,7 +10,7 @@ No knowledge of turbo-core is required. You need Go 1.26 or later and a terminal
1010 - A checkout of turbo-core beside where you are about to work. If you do not have one:
1111
1212 ```bash
13-git clone https://codeberg.org/turbo-editors/turbo-core.git
13+git clone ssh://git@rickub.com/turbo-editors/turbo-core.git
1414 ```
1515
1616 ## Step 1 — Make the module
@@ -10,7 +10,7 @@ No knowledge of turbo-core is required. You need Go 1.26 or later and a terminal
10 - A checkout of turbo-core beside where you are about to work. If you do not have one:10 - A checkout of turbo-core beside where you are about to work. If you do not have one:
11 11
12 ```bash12 ```bash
13-git clone https://codeberg.org/turbo-editors/turbo-core.git13+git clone ssh://git@rickub.com/turbo-editors/turbo-core.git
14 ```14 ```
15 15
16 ## Step 1 — Make the module16 ## Step 1 — Make the module
modified docs/fr/how-to/release-the-library.md +22 -11
@@ -2,19 +2,21 @@
22
33 Ce guide montre comment publier une version de turbo-core dont les éditeurs peuvent dépendre. Il suppose un accès en écriture au dépôt.
44
5-turbo-core est un module Go sans binaire et sans artefact de publication : le publier, c'est le taguer. `01-release.tag.sh` fait tout.
5+turbo-core est un module Go sans binaire : le publier, c'est le taguer, et le proxy de modules sert `go get …@TAG` dès que le tag est joignable. Vous lancez un seul script ; le push du tag déclenche un workflow qui fait le reste.
66
77 ## Étapes
88
99 ### 1. Dire quelle version
1010
11-Créez `release.env` — il est ignoré par git, car un jeton de publication a aussi sa place dans un fichier de ce genre :
11+Créez `release.env` — il est ignoré par git, la CI ne le voit donc jamais :
1212
1313 ```sh
1414 TAG="v0.1.0"
1515 ABOUT="La bibliothèque des éditeurs Turbo"
1616 ```
1717
18+`ABOUT` devient le message du tag, et le workflow le relit sur le tag pour en faire l'en-tête de la page de release.
19+
1820 ### 2. Lancer le script
1921
2022 ```bash
@@ -25,20 +27,27 @@ Il lance `make check`, refuse un tag déjà pris ici ou sur origin, refuse un `g
2527
2628 Cet ordre compte : un tag poussé avant la branche désigne un commit que le dépôt distant n'a jamais vu, et un tag créé avant un push refusé reste derrière, à la charge de qui le trouvera.
2729
28-### 3. Créer la page de release
30+C'est la dernière chose que vous lancez à la main. La bibliothèque est publiée dès que le tag est sur origin.
31+
32+### 3. Surveiller le workflow Release
33+
34+Le push du tag déclenche `.github/workflows/release.yml`. Suivez-le dans l'onglet Actions du dépôt ; il n'attend rien de vous, sauf s'il passe au rouge.
2935
30-Le tag suffit pour `go get` ; ceci ajoute la page qu'une personne lit.
36+Il lance la suite de tests, met en scène les artefacts avec `./02-build-releases.sh`, et crée la page de release avec eux. Les notes sont le message du tag, la ligne unique qui installe le module, et des liens vers la documentation **à ce tag** plutôt qu'à la branche — une page de release n'est pas dans l'arborescence du dépôt, donc un lien relatif depuis elle donne un 404, et un lien vers la branche pourrit à mesure que la branche avance.
37+
38+Le job publie avec son propre `GITHUB_TOKEN`, seul identifiant que l'API de release de Rickub accepte — un jeton personnel est refusé. Il n'y a rien à configurer ni aucun secret à conserver.
39+
40+Vous pouvez voir ce qu'il mettra en scène sans rien publier :
3141
3242 ```bash
33-./02-release.publish.sh --dry-run # montre la requête, n'envoie rien
34-./02-release.publish.sh # la crée
43+./02-build-releases.sh v0.1.0 # écrit release/v0.1.0/, ne pousse rien
3544 ```
3645
37-Il lui faut `curl` et `jq`, `OWNER` et `REPO` dans `release.env`, et un jeton d'application Codeberg dans `turbo-core.token.env` — un fichier à part pour que `release.env` puisse être montré à quelqu'un sans fuite. Les deux sont couverts par `*.env` dans `.gitignore`.
46+Il compile chaque paquet, les passe à `vet`, archive la source du commit tagué, extrait cette archive et la reconstruit — la seule preuve que ce qui est livré compile seul, sans rien oublier hors de git — puis en calcule la somme de contrôle et écrit le README qui ira sur la page.
3847
39-Les notes qu'il écrit sont `ABOUT`, la ligne unique qui installe le module, et des liens vers la documentation **à ce tag** plutôt qu'à la branche.
48+Cette archive n'est pas la façon dont on installe la bibliothèque : le proxy sert le module directement depuis le tag. Elle est là pour vérifier une release, et pour qui n'atteint pas le proxy.
4049
41-Il n'y a ni `03` ni `04` ici. Ceux-là construisent et attachent des binaires dans les éditeurs ; une bibliothèque n'en a pas — son artefact est le tag.
50+Il n'y a ni `03` ni `04` ici. Ceux-là construisent et attachent des binaires dans les éditeurs ; une bibliothèque n'en a pas.
4251
4352 ### 4. Y brancher les éditeurs
4453
@@ -64,7 +73,7 @@ git tag -a v0.1.0 -m "La bibliothèque des éditeurs Turbo"
6473 git push origin v0.1.0
6574 ```
6675
67-Poussez la branche **avant** le tag, pour la raison ci-dessus.
76+Poussez la branche **avant** le tag, pour la raison ci-dessus. Le workflow se déclenche sur le push du tag quelle qu'en soit la façon : la page de release apparaît quand même.
6877
6978 ### Vous développez à travers les trois dépôts
7079
@@ -87,7 +96,9 @@ Non. Plusieurs éditeurs peuvent l'épingler et le proxy de modules met en cache
8796
8897 ## Ce à quoi faire attention
8998
90-Le script lance `make check`, et la suite qu'il lance contient des tests qui lancent *ce script* contre une copie jetable. Ils se sautent eux-mêmes quand `TURBO_CORE_RELEASING` est défini, ce que le script exporte avant d'appeler make. Retirer cette ligne fait récurser une publication jusqu'à épuisement de quelque chose.
99+Le script lance `make check`, et la suite qu'il lance contient des tests qui lancent *ce script* contre une copie jetable. Ils se sautent eux-mêmes quand `TURBO_CORE_RELEASING` est défini, ce que le script exporte avant d'appeler make. Retirer cette ligne fait récurser une publication jusqu'à épuisement de quelque chose. Le workflow définit la même variable pour son propre `go test`, car là-bas rien n'appelle le script qui l'aurait définie.
100+
101+Le workflow est le seul du dépôt, volontairement : l'API de dispatch de Rickub déclenche *tous* les workflows dispatchables d'une ref, un dépôt ne devrait donc en déclarer qu'un — c'est aussi pourquoi celui-ci n'a pas de `workflow_dispatch` et ne s'atteint qu'en poussant un tag.
91102
92103 ## Voir aussi
93104
@@ -2,19 +2,21 @@
2 2
3 Ce guide montre comment publier une version de turbo-core dont les éditeurs peuvent dépendre. Il suppose un accès en écriture au dépôt.3 Ce guide montre comment publier une version de turbo-core dont les éditeurs peuvent dépendre. Il suppose un accès en écriture au dépôt.
4 4
5-turbo-core est un module Go sans binaire et sans artefact de publication : le publier, c'est le taguer. `01-release.tag.sh` fait tout.5+turbo-core est un module Go sans binaire : le publier, c'est le taguer, et le proxy de modules sert `go get …@TAG` dès que le tag est joignable. Vous lancez un seul script ; le push du tag déclenche un workflow qui fait le reste.
6 6
7 ## Étapes7 ## Étapes
8 8
9 ### 1. Dire quelle version9 ### 1. Dire quelle version
10 10
11-Créez `release.env` — il est ignoré par git, car un jeton de publication a aussi sa place dans un fichier de ce genre :11+Créez `release.env` — il est ignoré par git, la CI ne le voit donc jamais :
12 12
13 ```sh13 ```sh
14 TAG="v0.1.0"14 TAG="v0.1.0"
15 ABOUT="La bibliothèque des éditeurs Turbo"15 ABOUT="La bibliothèque des éditeurs Turbo"
16 ```16 ```
17 17
18+`ABOUT` devient le message du tag, et le workflow le relit sur le tag pour en faire l'en-tête de la page de release.
19+
18 ### 2. Lancer le script20 ### 2. Lancer le script
19 21
20 ```bash22 ```bash
@@ -25,20 +27,27 @@ Il lance `make check`, refuse un tag déjà pris ici ou sur origin, refuse un `g
25 27
26 Cet ordre compte : un tag poussé avant la branche désigne un commit que le dépôt distant n'a jamais vu, et un tag créé avant un push refusé reste derrière, à la charge de qui le trouvera.28 Cet ordre compte : un tag poussé avant la branche désigne un commit que le dépôt distant n'a jamais vu, et un tag créé avant un push refusé reste derrière, à la charge de qui le trouvera.
27 29
28-### 3. Créer la page de release30+C'est la dernière chose que vous lancez à la main. La bibliothèque est publiée dès que le tag est sur origin.
31+
32+### 3. Surveiller le workflow Release
33+
34+Le push du tag déclenche `.github/workflows/release.yml`. Suivez-le dans l'onglet Actions du dépôt ; il n'attend rien de vous, sauf s'il passe au rouge.
29 35
30-Le tag suffit pour `go get` ; ceci ajoute la page qu'une personne lit.36+Il lance la suite de tests, met en scène les artefacts avec `./02-build-releases.sh`, et crée la page de release avec eux. Les notes sont le message du tag, la ligne unique qui installe le module, et des liens vers la documentation **à ce tag** plutôt qu'à la branche — une page de release n'est pas dans l'arborescence du dépôt, donc un lien relatif depuis elle donne un 404, et un lien vers la branche pourrit à mesure que la branche avance.
37+
38+Le job publie avec son propre `GITHUB_TOKEN`, seul identifiant que l'API de release de Rickub accepte — un jeton personnel est refusé. Il n'y a rien à configurer ni aucun secret à conserver.
39+
40+Vous pouvez voir ce qu'il mettra en scène sans rien publier :
31 41
32 ```bash42 ```bash
33-./02-release.publish.sh --dry-run # montre la requête, n'envoie rien43+./02-build-releases.sh v0.1.0 # écrit release/v0.1.0/, ne pousse rien
34-./02-release.publish.sh # la crée
35 ```44 ```
36 45
37-Il lui faut `curl` et `jq`, `OWNER` et `REPO` dans `release.env`, et un jeton d'application Codeberg dans `turbo-core.token.env` — un fichier à part pour que `release.env` puisse être montré à quelqu'un sans fuite. Les deux sont couverts par `*.env` dans `.gitignore`.46+Il compile chaque paquet, les passe à `vet`, archive la source du commit tagué, extrait cette archive et la reconstruit — la seule preuve que ce qui est livré compile seul, sans rien oublier hors de git — puis en calcule la somme de contrôle et écrit le README qui ira sur la page.
38 47
39-Les notes qu'il écrit sont `ABOUT`, la ligne unique qui installe le module, et des liens vers la documentation **à ce tag** plutôt qu'à la branche.48+Cette archive n'est pas la façon dont on installe la bibliothèque : le proxy sert le module directement depuis le tag. Elle est là pour vérifier une release, et pour qui n'atteint pas le proxy.
40 49
41-Il n'y a ni `03` ni `04` ici. Ceux-là construisent et attachent des binaires dans les éditeurs ; une bibliothèque n'en a pas — son artefact est le tag.50+Il n'y a ni `03` ni `04` ici. Ceux-là construisent et attachent des binaires dans les éditeurs ; une bibliothèque n'en a pas.
42 51
43 ### 4. Y brancher les éditeurs52 ### 4. Y brancher les éditeurs
44 53
@@ -64,7 +73,7 @@ git tag -a v0.1.0 -m "La bibliothèque des éditeurs Turbo"
64 git push origin v0.1.073 git push origin v0.1.0
65 ```74 ```
66 75
67-Poussez la branche **avant** le tag, pour la raison ci-dessus.76+Poussez la branche **avant** le tag, pour la raison ci-dessus. Le workflow se déclenche sur le push du tag quelle qu'en soit la façon : la page de release apparaît quand même.
68 77
69 ### Vous développez à travers les trois dépôts78 ### Vous développez à travers les trois dépôts
70 79
@@ -87,7 +96,9 @@ Non. Plusieurs éditeurs peuvent l'épingler et le proxy de modules met en cache
87 96
88 ## Ce à quoi faire attention97 ## Ce à quoi faire attention
89 98
90-Le script lance `make check`, et la suite qu'il lance contient des tests qui lancent *ce script* contre une copie jetable. Ils se sautent eux-mêmes quand `TURBO_CORE_RELEASING` est défini, ce que le script exporte avant d'appeler make. Retirer cette ligne fait récurser une publication jusqu'à épuisement de quelque chose.99+Le script lance `make check`, et la suite qu'il lance contient des tests qui lancent *ce script* contre une copie jetable. Ils se sautent eux-mêmes quand `TURBO_CORE_RELEASING` est défini, ce que le script exporte avant d'appeler make. Retirer cette ligne fait récurser une publication jusqu'à épuisement de quelque chose. Le workflow définit la même variable pour son propre `go test`, car là-bas rien n'appelle le script qui l'aurait définie.
100+
101+Le workflow est le seul du dépôt, volontairement : l'API de dispatch de Rickub déclenche *tous* les workflows dispatchables d'une ref, un dépôt ne devrait donc en déclarer qu'un — c'est aussi pourquoi celui-ci n'a pas de `workflow_dispatch` et ne s'atteint qu'en poussant un tag.
91 102
92 ## Voir aussi103 ## Voir aussi
93 104
modified docs/fr/how-to/test-without-publishing.md +6 -3
@@ -79,13 +79,16 @@ Si vous l'employez malgré tout, remettez la ligne `require` sur une version pub
7979
8080 ## Tester la forme publiée, pas seulement le code
8181
82-Un espace de travail prouve que votre code fonctionne. Il ne prouve pas que le *module* fonctionne : il ne dit rien de ce que contient le tag, de la justesse du `go.sum`, ni de la capacité d'un clone propre à compiler. Pour cela il faut publier — d'abord un tag, puis une page de release :
82+Un espace de travail prouve que votre code fonctionne. Il ne prouve pas que le *module* fonctionne : il ne dit rien de ce que contient le tag, de la justesse du `go.sum`, ni de la capacité d'un clone propre à compiler.
83+
84+À cette dernière question, vous pouvez répondre sans rien publier — `./02-build-releases.sh v0.3.0` archive la source suivie par git, l'extrait ailleurs et l'y compile. Le reste demande un vrai tag :
8385
8486 ```bash
85-TAG=v0.3.0 ./01-release.tag.sh
86-TAG=v0.3.0 ./02-release.publish.sh
87+./01-release.tag.sh
8788 ```
8889
90+La page de release suit toute seule : le push du tag déclenche le workflow Release, qui met en scène les mêmes artefacts et les publie. Voir [Publier la bibliothèque](release-the-library.md).
91+
8992 Puis, dans chaque éditeur, **sans** espace de travail et **sans** replace :
9093
9194 ```bash
@@ -79,13 +79,16 @@ Si vous l'employez malgré tout, remettez la ligne `require` sur une version pub
79 79
80 ## Tester la forme publiée, pas seulement le code80 ## Tester la forme publiée, pas seulement le code
81 81
82-Un espace de travail prouve que votre code fonctionne. Il ne prouve pas que le *module* fonctionne : il ne dit rien de ce que contient le tag, de la justesse du `go.sum`, ni de la capacité d'un clone propre à compiler. Pour cela il faut publier — d'abord un tag, puis une page de release :82+Un espace de travail prouve que votre code fonctionne. Il ne prouve pas que le *module* fonctionne : il ne dit rien de ce que contient le tag, de la justesse du `go.sum`, ni de la capacité d'un clone propre à compiler.
83+
84+À cette dernière question, vous pouvez répondre sans rien publier — `./02-build-releases.sh v0.3.0` archive la source suivie par git, l'extrait ailleurs et l'y compile. Le reste demande un vrai tag :
83 85
84 ```bash86 ```bash
85-TAG=v0.3.0 ./01-release.tag.sh87+./01-release.tag.sh
86-TAG=v0.3.0 ./02-release.publish.sh
87 ```88 ```
88 89
90+La page de release suit toute seule : le push du tag déclenche le workflow Release, qui met en scène les mêmes artefacts et les publie. Voir [Publier la bibliothèque](release-the-library.md).
91+
89 Puis, dans chaque éditeur, **sans** espace de travail et **sans** replace :92 Puis, dans chaque éditeur, **sans** espace de travail et **sans** replace :
90 93
91 ```bash94 ```bash
modified docs/fr/tutorials/build-an-editor.md +1 -1
@@ -10,7 +10,7 @@ Aucune connaissance de turbo-core n'est requise. Il vous faut Go 1.26 ou plus r
1010 - Une copie de turbo-core à côté de l'endroit où vous allez travailler. Si vous n'en avez pas :
1111
1212 ```bash
13-git clone https://codeberg.org/turbo-editors/turbo-core.git
13+git clone ssh://git@rickub.com/turbo-editors/turbo-core.git
1414 ```
1515
1616 ## Étape 1 — Créer le module
@@ -10,7 +10,7 @@ Aucune connaissance de turbo-core n'est requise. Il vous faut Go 1.26 ou plus r
10 - Une copie de turbo-core à côté de l'endroit où vous allez travailler. Si vous n'en avez pas :10 - Une copie de turbo-core à côté de l'endroit où vous allez travailler. Si vous n'en avez pas :
11 11
12 ```bash12 ```bash
13-git clone https://codeberg.org/turbo-editors/turbo-core.git13+git clone ssh://git@rickub.com/turbo-editors/turbo-core.git
14 ```14 ```
15 15
16 ## Étape 1 — Créer le module16 ## Étape 1 — Créer le module
modified release_test.go +177 -92
@@ -193,9 +193,14 @@ func TestTheReleaseScriptRefusesATagItAlreadyPublished(t *testing.T) {
193193 // copyModuleInto copies the module's source into a directory, so the script can
194194 // be run against a real checkout without touching this one.
195195 //
196-// .git is left out because the target has its own, and *.env because those hold
197-// the release token — a test has no use for it, and copying a secret into a
198-// temporary directory is how it ends up somewhere nobody looks.
196+// .git is left out because the target has its own, and *.env because a test
197+// writes its own release.env — copying this checkout's would release whatever
198+// version happens to be in it.
199+//
200+// The copying is done here rather than by shelling out to cp, which on a
201+// network-backed working copy has been seen to write the right number of bytes
202+// and the wrong ones: every file in the copy came out NUL-filled, and the
203+// tests below then failed on a go build that had nothing to do with them.
199204 func copyModuleInto(t *testing.T, target string) {
200205 t.Helper()
201206
@@ -207,7 +212,51 @@ func copyModuleInto(t *testing.T, target string) {
207212 if entry.Name() == ".git" || strings.HasSuffix(entry.Name(), ".env") {
208213 continue
209214 }
210- run(t, ".", "cp", "-r", entry.Name(), target)
215+ copyTree(t, entry.Name(), filepath.Join(target, entry.Name()))
216+ }
217+}
218+
219+// copyTree copies a file or a directory to a new path.
220+//
221+// Anything that is neither a regular file nor a directory is skipped: the tool
222+// directories beside the source hold symlinks into caches that do not exist in
223+// a temporary copy, and the release scripts have no use for them.
224+func copyTree(t *testing.T, from, to string) {
225+ t.Helper()
226+
227+ err := filepath.WalkDir(from, func(path string, entry os.DirEntry, err error) error {
228+ if err != nil {
229+ return err
230+ }
231+ relative, err := filepath.Rel(from, path)
232+ if err != nil {
233+ return err
234+ }
235+ destination := filepath.Join(to, relative)
236+
237+ if entry.IsDir() {
238+ return os.MkdirAll(destination, 0o755)
239+ }
240+ if !entry.Type().IsRegular() {
241+ return nil
242+ }
243+ info, err := entry.Info()
244+ if err != nil {
245+ return err
246+ }
247+ data, err := os.ReadFile(path)
248+ if err != nil {
249+ return err
250+ }
251+ if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil {
252+ return err
253+ }
254+ // The mode carries the execute bit, without which the scripts these
255+ // tests exist to run cannot be run.
256+ return os.WriteFile(destination, data, info.Mode().Perm())
257+ })
258+ if err != nil {
259+ t.Fatalf("copying %s: %v", from, err)
211260 }
212261 }
213262
@@ -241,138 +290,174 @@ func writeFile(t *testing.T, path, contents string) {
241290 }
242291 }
243292
244-// readPublishScript returns the release-page script's text.
245-func readPublishScript(t *testing.T) string {
293+// readBuildScript returns the staging script's text.
294+func readBuildScript(t *testing.T) string {
246295 t.Helper()
247296
248- data, err := os.ReadFile("02-release.publish.sh")
297+ data, err := os.ReadFile("02-build-releases.sh")
249298 if err != nil {
250- t.Fatalf("reading the publish script: %v", err)
299+ t.Fatalf("reading the build script: %v", err)
251300 }
252301 return string(data)
253302 }
254303
255-func TestThePublishScriptBuildsItsJSONWithJq(t *testing.T) {
256- // Hand-written JSON in a heredoc breaks silently the day ABOUT contains a
257- // quote, a newline or a backtick — and the release notes are exactly where
258- // somebody puts a backtick.
259- script := readPublishScript(t)
260-
261- if !strings.Contains(script, "jq -n") {
262- t.Error("the script builds its payload by hand rather than with jq")
263- }
264- // Comment lines are skipped: the script's own comment explains why it
265- // avoids that idiom, and a grep over the whole file matches the
266- // explanation. This test failed on its own prose before it was narrowed.
267- for _, line := range codeLines(script) {
268- if strings.Contains(line, `read -r -d ''`) {
269- t.Errorf("the script uses the read -r -d '' idiom, which always exits non-zero: %s", line)
270- }
304+// readWorkflow returns the release workflow's text.
305+func readWorkflow(t *testing.T) string {
306+ t.Helper()
307+
308+ data, err := os.ReadFile(filepath.Join(".github", "workflows", "release.yml"))
309+ if err != nil {
310+ t.Fatalf("reading the release workflow: %v", err)
271311 }
312+ return string(data)
272313 }
273314
274-func TestThePublishScriptStopsOnTheFirstFailure(t *testing.T) {
275- // Safe here precisely because it does not use that idiom.
276- if got := readPublishScript(t); !strings.Contains(got, "set -euo pipefail") {
315+func TestTheBuildScriptStopsOnTheFirstFailure(t *testing.T) {
316+ // It runs in CI on a tag that is already pushed. A step failing in silence
317+ // there publishes a release page for artefacts that were never staged.
318+ if got := readBuildScript(t); !strings.Contains(got, "set -euo pipefail") {
277319 t.Error("the script does not stop on failure")
278320 }
279321 }
280322
281-func TestThePublishScriptTellsUnreachableFromAbsent(t *testing.T) {
282- // git ls-remote exits 2 when a ref is absent and 128 when it cannot reach
283- // the remote. Conflating them refuses a good release from any machine with
284- // no key loaded — which is how this was found.
285- script := readPublishScript(t)
286-
287- if !strings.Contains(script, "lookup=$?") {
288- t.Error("the script does not look at ls-remote's exit code")
289- }
290- for _, want := range []string{"is not on origin", "cannot reach origin"} {
291- if !strings.Contains(script, want) {
292- t.Errorf("the script never says %q", want)
293- }
323+func TestTheBuildScriptRefusesAReplaceDirective(t *testing.T) {
324+ // 01 checks this too, but CI runs *this* script without ever running 01,
325+ // and the proxy serves go.mod as written.
326+ if got := readBuildScript(t); !strings.Contains(got, "replace") {
327+ t.Error("the script does not check go.mod for a replace directive")
294328 }
295329 }
296330
297-func TestThePublishScriptTellsTheHTTPStatusesApart(t *testing.T) {
298- // "❌ something went wrong" plus the API's raw JSON leaves the reader to
299- // guess which of four quite different problems this is.
300- script := readPublishScript(t)
331+func TestTheBuildScriptRefusesATagThatIsNotAVersion(t *testing.T) {
332+ // The proxy will not serve a tag it cannot read as a version, so a typo
333+ // here stages perfectly and then fails at every `go get`.
334+ skipInsideARelease(t)
301335
302- for _, status := range []string{"201", "409", "401", "404"} {
303- if !strings.Contains(script, status) {
304- t.Errorf("the script does not handle HTTP %s", status)
305- }
336+ dir := t.TempDir()
337+ copyModuleInto(t, dir)
338+
339+ out, err := runAllowingFailure(t, dir, "./02-build-releases.sh", "v0.o.0")
340+
341+ if err == nil {
342+ t.Fatalf("the script staged a release for a tag that is not a version:\n%s", out)
343+ }
344+ if !strings.Contains(out, "v1.2.3") {
345+ t.Errorf("the refusal does not say what a tag should look like:\n%s", out)
306346 }
307347 }
308348
309-func TestThePublishScriptDryRunSendsNothing(t *testing.T) {
310- // The only way to exercise this script here: the real call publishes on
311- // somebody's behalf, which a test may not do.
349+func TestTheBuildScriptStagesAnArchiveAndItsChecksum(t *testing.T) {
350+ // Reading the script is not the same as running it: the archive is taken
351+ // from git, built again once extracted, and checksummed, and each of those
352+ // can break on its own.
312353 skipInsideARelease(t)
313- if _, err := exec.LookPath("jq"); err != nil {
314- t.Skip("jq is not installed")
354+ if _, err := exec.LookPath("git"); err != nil {
355+ t.Skip("git is not available")
315356 }
316357
317358 dir := t.TempDir()
318359 copyModuleInto(t, dir)
319- writeFile(t, filepath.Join(dir, "release.env"),
320- "TAG=\"v0.0.1-test\"\nABOUT=\"notes with a \\\" quote\"\nOWNER=\"turbo-editors\"\nREPO=\"turbo-core\"\n")
321- writeFile(t, filepath.Join(dir, "turbo-core.token.env"), "TOKEN=not-a-real-token\n")
322-
323- out, err := runAllowingFailure(t, dir, "./02-release.publish.sh", "--dry-run")
360+ run(t, dir, "git", "init", "--initial-branch=main")
361+ run(t, dir, "git", "config", "user.email", "test@example.test")
362+ run(t, dir, "git", "config", "user.name", "Release Test")
363+ run(t, dir, "git", "add", ".")
364+ run(t, dir, "git", "commit", "-m", "a throwaway commit")
324365
366+ out, err := runAllowingFailure(t, dir, "./02-build-releases.sh", "v0.0.1-test")
325367 if err != nil {
326- t.Fatalf("the dry run failed:\n%s", out)
368+ t.Fatalf("staging failed:\n%s", out)
327369 }
328- if !strings.Contains(out, "nothing was sent") {
329- t.Errorf("the dry run does not say it sent nothing:\n%s", out)
370+
371+ staged := filepath.Join(dir, "release", "v0.0.1-test")
372+ for _, name := range []string{"turbo-core-0.0.1-test.tar.gz", "SHA256SUMS", "README.md"} {
373+ if _, err := os.Stat(filepath.Join(staged, name)); err != nil {
374+ t.Errorf("%s was not staged: %v", name, err)
375+ }
330376 }
331- if !strings.Contains(out, "POST https://codeberg.org/api/v1/repos/turbo-editors/turbo-core/releases") {
332- t.Errorf("the dry run does not show the request it would make:\n%s", out)
377+
378+ // A checksum file that does not match what is beside it is worse than
379+ // none: it is checked once, by somebody who then trusts it.
380+ if _, err := exec.LookPath("sha256sum"); err != nil {
381+ t.Skip("sha256sum is not available to verify SHA256SUMS")
333382 }
334- // The quote in ABOUT must have survived as data rather than breaking the
335- // JSON, which is the whole reason jq is in there.
336- if !strings.Contains(out, `notes with a \" quote`) {
337- t.Errorf("the quote in the notes was not escaped:\n%s", out)
383+ if out, err := runAllowingFailure(t, staged, "sha256sum", "-c", "SHA256SUMS"); err != nil {
384+ t.Errorf("SHA256SUMS does not match the archive:\n%s", out)
338385 }
339386 }
340387
341-func TestThePublishScriptRefusesWithoutAToken(t *testing.T) {
342- skipInsideARelease(t)
388+func TestTheBuildScriptREADMEInstallsTheModule(t *testing.T) {
389+ // The page a person lands on has to say the one line that uses the
390+ // library. A release page whose only content is a version number tells
391+ // nobody how to depend on it.
392+ if got := readBuildScript(t); !strings.Contains(got, "go get ${MODULE}@${TAG}") {
393+ t.Error("the staged README never says how to get the module")
394+ }
395+}
343396
344- dir := t.TempDir()
345- copyModuleInto(t, dir)
346- writeFile(t, filepath.Join(dir, "release.env"),
347- "TAG=\"v0.0.1-test\"\nABOUT=\"notes\"\nOWNER=\"turbo-editors\"\nREPO=\"turbo-core\"\n")
397+func TestTheWorkflowPublishesOnATagPush(t *testing.T) {
398+ // The tag push is the trigger: ./01-release.tag.sh ends by pushing one,
399+ // and nothing else starts a release.
400+ workflow := readWorkflow(t)
348401
349- out, err := runAllowingFailure(t, dir, "./02-release.publish.sh", "--dry-run")
402+ for _, want := range []string{"push:", "tags:", `- "v*"`} {
403+ if !strings.Contains(workflow, want) {
404+ t.Errorf("the workflow never declares %q", want)
405+ }
406+ }
407+ // A workflow with the default read-only token cannot create a release, and
408+ // fails at its last step after doing all the work.
409+ if !strings.Contains(workflow, "contents: write") {
410+ t.Error("the workflow does not ask for contents: write")
411+ }
412+}
350413
351- if err == nil {
352- t.Fatalf("the script ran without a token:\n%s", out)
414+func TestTheWorkflowStagesWithTheSameScriptAPersonRuns(t *testing.T) {
415+ // A CI job that stages its own way is a second pipeline nobody tests, and
416+ // the local one is then only ever exercised by accident.
417+ if got := readWorkflow(t); !strings.Contains(got, "./02-build-releases.sh") {
418+ t.Error("the workflow does not stage the release with ./02-build-releases.sh")
353419 }
354- if !strings.Contains(out, "TOKEN is not set") {
355- t.Errorf("the refusal does not say the token is missing:\n%s", out)
420+}
421+
422+func TestTheWorkflowAttachesWhatWasStaged(t *testing.T) {
423+ // Publishing a release page with no files attached is a silent half-job:
424+ // the page exists and the links are not there.
425+ workflow := readWorkflow(t)
426+
427+ for _, want := range []string{"turbo-core-*.tar.gz", "SHA256SUMS", "fail_on_unmatched_files: true"} {
428+ if !strings.Contains(workflow, want) {
429+ t.Errorf("the workflow never mentions %q", want)
430+ }
356431 }
357432 }
358433
359-func TestThePublishScriptLinksToTheDocumentationAtThatTag(t *testing.T) {
360- // A release page is not inside the repository tree, so a relative path from
361- // it 404s — and a link to the branch would rot as the branch moves.
362- script := readPublishScript(t)
434+func TestTheWorkflowLinksToTheDocumentationAtThatTag(t *testing.T) {
435+ // A release page is not inside the repository tree, so a relative path
436+ // from it 404s — and a link to the branch would rot as the branch moves.
437+ workflow := readWorkflow(t)
363438
364- if !strings.Contains(script, "/src/tag/${TAG}/docs/") {
365- t.Error("the release notes do not link to the documentation at the released tag")
439+ if !strings.Contains(workflow, "blob/${GITHUB_REF_NAME}") {
440+ t.Error("the release notes do not link into the repository at the released tag")
441+ }
442+ if !strings.Contains(workflow, "/docs/en/README.md") {
443+ t.Error("the release notes do not link to the documentation")
366444 }
367445 }
368446
369-// codeLines returns the lines of a shell script that are not comments.
370-func codeLines(script string) []string {
371- var out []string
372- for _, line := range strings.Split(script, "\n") {
373- if trimmed := strings.TrimSpace(line); trimmed != "" && !strings.HasPrefix(trimmed, "#") {
374- out = append(out, line)
375- }
447+func TestTheWorkflowNeedsNoPersonalToken(t *testing.T) {
448+ // The release API behind Rickub's /gh shim accepts the job's own
449+ // GITHUB_TOKEN and refuses a personal one, so a secret referenced here is
450+ // a credential that cannot work and still has to be kept somewhere.
451+ if got := readWorkflow(t); strings.Contains(got, "secrets.") {
452+ t.Error("the workflow reads a secret; the job's own token is the only credential the release API takes")
453+ }
454+}
455+
456+func TestTheWorkflowDoesNotStartAReleaseInsideItself(t *testing.T) {
457+ // The suite it runs includes tests that run ./01-release.tag.sh against a
458+ // throwaway clone. Locally the script exports this before calling make;
459+ // in CI nothing calls the script, so the job has to set it itself.
460+ if got := readWorkflow(t); !strings.Contains(got, "TURBO_CORE_RELEASING") {
461+ t.Error("the workflow runs the suite without TURBO_CORE_RELEASING set")
376462 }
377- return out
378463 }
@@ -193,9 +193,14 @@ func TestTheReleaseScriptRefusesATagItAlreadyPublished(t *testing.T) {
193 // copyModuleInto copies the module's source into a directory, so the script can193 // copyModuleInto copies the module's source into a directory, so the script can
194 // be run against a real checkout without touching this one.194 // be run against a real checkout without touching this one.
195 //195 //
196-// .git is left out because the target has its own, and *.env because those hold196+// .git is left out because the target has its own, and *.env because a test
197-// the release token — a test has no use for it, and copying a secret into a197+// writes its own release.env — copying this checkout's would release whatever
198-// temporary directory is how it ends up somewhere nobody looks.198+// version happens to be in it.
199+//
200+// The copying is done here rather than by shelling out to cp, which on a
201+// network-backed working copy has been seen to write the right number of bytes
202+// and the wrong ones: every file in the copy came out NUL-filled, and the
203+// tests below then failed on a go build that had nothing to do with them.
199 func copyModuleInto(t *testing.T, target string) {204 func copyModuleInto(t *testing.T, target string) {
200 t.Helper()205 t.Helper()
201 206
@@ -207,7 +212,51 @@ func copyModuleInto(t *testing.T, target string) {
207 if entry.Name() == ".git" || strings.HasSuffix(entry.Name(), ".env") {212 if entry.Name() == ".git" || strings.HasSuffix(entry.Name(), ".env") {
208 continue213 continue
209 }214 }
210- run(t, ".", "cp", "-r", entry.Name(), target)215+ copyTree(t, entry.Name(), filepath.Join(target, entry.Name()))
216+ }
217+}
218+
219+// copyTree copies a file or a directory to a new path.
220+//
221+// Anything that is neither a regular file nor a directory is skipped: the tool
222+// directories beside the source hold symlinks into caches that do not exist in
223+// a temporary copy, and the release scripts have no use for them.
224+func copyTree(t *testing.T, from, to string) {
225+ t.Helper()
226+
227+ err := filepath.WalkDir(from, func(path string, entry os.DirEntry, err error) error {
228+ if err != nil {
229+ return err
230+ }
231+ relative, err := filepath.Rel(from, path)
232+ if err != nil {
233+ return err
234+ }
235+ destination := filepath.Join(to, relative)
236+
237+ if entry.IsDir() {
238+ return os.MkdirAll(destination, 0o755)
239+ }
240+ if !entry.Type().IsRegular() {
241+ return nil
242+ }
243+ info, err := entry.Info()
244+ if err != nil {
245+ return err
246+ }
247+ data, err := os.ReadFile(path)
248+ if err != nil {
249+ return err
250+ }
251+ if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil {
252+ return err
253+ }
254+ // The mode carries the execute bit, without which the scripts these
255+ // tests exist to run cannot be run.
256+ return os.WriteFile(destination, data, info.Mode().Perm())
257+ })
258+ if err != nil {
259+ t.Fatalf("copying %s: %v", from, err)
211 }260 }
212 }261 }
213 262
@@ -241,138 +290,174 @@ func writeFile(t *testing.T, path, contents string) {
241 }290 }
242 }291 }
243 292
244-// readPublishScript returns the release-page script's text.293+// readBuildScript returns the staging script's text.
245-func readPublishScript(t *testing.T) string {294+func readBuildScript(t *testing.T) string {
246 t.Helper()295 t.Helper()
247 296
248- data, err := os.ReadFile("02-release.publish.sh")297+ data, err := os.ReadFile("02-build-releases.sh")
249 if err != nil {298 if err != nil {
250- t.Fatalf("reading the publish script: %v", err)299+ t.Fatalf("reading the build script: %v", err)
251 }300 }
252 return string(data)301 return string(data)
253 }302 }
254 303
255-func TestThePublishScriptBuildsItsJSONWithJq(t *testing.T) {304+// readWorkflow returns the release workflow's text.
256- // Hand-written JSON in a heredoc breaks silently the day ABOUT contains a305+func readWorkflow(t *testing.T) string {
257- // quote, a newline or a backtick — and the release notes are exactly where306+ t.Helper()
258- // somebody puts a backtick.307+
259- script := readPublishScript(t)308+ data, err := os.ReadFile(filepath.Join(".github", "workflows", "release.yml"))
260-309+ if err != nil {
261- if !strings.Contains(script, "jq -n") {310+ t.Fatalf("reading the release workflow: %v", err)
262- t.Error("the script builds its payload by hand rather than with jq")
263- }
264- // Comment lines are skipped: the script's own comment explains why it
265- // avoids that idiom, and a grep over the whole file matches the
266- // explanation. This test failed on its own prose before it was narrowed.
267- for _, line := range codeLines(script) {
268- if strings.Contains(line, `read -r -d ''`) {
269- t.Errorf("the script uses the read -r -d '' idiom, which always exits non-zero: %s", line)
270- }
271 }311 }
312+ return string(data)
272 }313 }
273 314
274-func TestThePublishScriptStopsOnTheFirstFailure(t *testing.T) {315+func TestTheBuildScriptStopsOnTheFirstFailure(t *testing.T) {
275- // Safe here precisely because it does not use that idiom.316+ // It runs in CI on a tag that is already pushed. A step failing in silence
276- if got := readPublishScript(t); !strings.Contains(got, "set -euo pipefail") {317+ // there publishes a release page for artefacts that were never staged.
318+ if got := readBuildScript(t); !strings.Contains(got, "set -euo pipefail") {
277 t.Error("the script does not stop on failure")319 t.Error("the script does not stop on failure")
278 }320 }
279 }321 }
280 322
281-func TestThePublishScriptTellsUnreachableFromAbsent(t *testing.T) {323+func TestTheBuildScriptRefusesAReplaceDirective(t *testing.T) {
282- // git ls-remote exits 2 when a ref is absent and 128 when it cannot reach324+ // 01 checks this too, but CI runs *this* script without ever running 01,
283- // the remote. Conflating them refuses a good release from any machine with325+ // and the proxy serves go.mod as written.
284- // no key loaded — which is how this was found.326+ if got := readBuildScript(t); !strings.Contains(got, "replace") {
285- script := readPublishScript(t)327+ t.Error("the script does not check go.mod for a replace directive")
286-
287- if !strings.Contains(script, "lookup=$?") {
288- t.Error("the script does not look at ls-remote's exit code")
289- }
290- for _, want := range []string{"is not on origin", "cannot reach origin"} {
291- if !strings.Contains(script, want) {
292- t.Errorf("the script never says %q", want)
293- }
294 }328 }
295 }329 }
296 330
297-func TestThePublishScriptTellsTheHTTPStatusesApart(t *testing.T) {331+func TestTheBuildScriptRefusesATagThatIsNotAVersion(t *testing.T) {
298- // "❌ something went wrong" plus the API's raw JSON leaves the reader to332+ // The proxy will not serve a tag it cannot read as a version, so a typo
299- // guess which of four quite different problems this is.333+ // here stages perfectly and then fails at every `go get`.
300- script := readPublishScript(t)334+ skipInsideARelease(t)
301 335
302- for _, status := range []string{"201", "409", "401", "404"} {336+ dir := t.TempDir()
303- if !strings.Contains(script, status) {337+ copyModuleInto(t, dir)
304- t.Errorf("the script does not handle HTTP %s", status)338+
305- }339+ out, err := runAllowingFailure(t, dir, "./02-build-releases.sh", "v0.o.0")
340+
341+ if err == nil {
342+ t.Fatalf("the script staged a release for a tag that is not a version:\n%s", out)
343+ }
344+ if !strings.Contains(out, "v1.2.3") {
345+ t.Errorf("the refusal does not say what a tag should look like:\n%s", out)
306 }346 }
307 }347 }
308 348
309-func TestThePublishScriptDryRunSendsNothing(t *testing.T) {349+func TestTheBuildScriptStagesAnArchiveAndItsChecksum(t *testing.T) {
310- // The only way to exercise this script here: the real call publishes on350+ // Reading the script is not the same as running it: the archive is taken
311- // somebody's behalf, which a test may not do.351+ // from git, built again once extracted, and checksummed, and each of those
352+ // can break on its own.
312 skipInsideARelease(t)353 skipInsideARelease(t)
313- if _, err := exec.LookPath("jq"); err != nil {354+ if _, err := exec.LookPath("git"); err != nil {
314- t.Skip("jq is not installed")355+ t.Skip("git is not available")
315 }356 }
316 357
317 dir := t.TempDir()358 dir := t.TempDir()
318 copyModuleInto(t, dir)359 copyModuleInto(t, dir)
319- writeFile(t, filepath.Join(dir, "release.env"),360+ run(t, dir, "git", "init", "--initial-branch=main")
320- "TAG=\"v0.0.1-test\"\nABOUT=\"notes with a \\\" quote\"\nOWNER=\"turbo-editors\"\nREPO=\"turbo-core\"\n")361+ run(t, dir, "git", "config", "user.email", "test@example.test")
321- writeFile(t, filepath.Join(dir, "turbo-core.token.env"), "TOKEN=not-a-real-token\n")362+ run(t, dir, "git", "config", "user.name", "Release Test")
322-363+ run(t, dir, "git", "add", ".")
323- out, err := runAllowingFailure(t, dir, "./02-release.publish.sh", "--dry-run")364+ run(t, dir, "git", "commit", "-m", "a throwaway commit")
324 365
366+ out, err := runAllowingFailure(t, dir, "./02-build-releases.sh", "v0.0.1-test")
325 if err != nil {367 if err != nil {
326- t.Fatalf("the dry run failed:\n%s", out)368+ t.Fatalf("staging failed:\n%s", out)
327 }369 }
328- if !strings.Contains(out, "nothing was sent") {370+
329- t.Errorf("the dry run does not say it sent nothing:\n%s", out)371+ staged := filepath.Join(dir, "release", "v0.0.1-test")
372+ for _, name := range []string{"turbo-core-0.0.1-test.tar.gz", "SHA256SUMS", "README.md"} {
373+ if _, err := os.Stat(filepath.Join(staged, name)); err != nil {
374+ t.Errorf("%s was not staged: %v", name, err)
375+ }
330 }376 }
331- if !strings.Contains(out, "POST https://codeberg.org/api/v1/repos/turbo-editors/turbo-core/releases") {377+
332- t.Errorf("the dry run does not show the request it would make:\n%s", out)378+ // A checksum file that does not match what is beside it is worse than
379+ // none: it is checked once, by somebody who then trusts it.
380+ if _, err := exec.LookPath("sha256sum"); err != nil {
381+ t.Skip("sha256sum is not available to verify SHA256SUMS")
333 }382 }
334- // The quote in ABOUT must have survived as data rather than breaking the383+ if out, err := runAllowingFailure(t, staged, "sha256sum", "-c", "SHA256SUMS"); err != nil {
335- // JSON, which is the whole reason jq is in there.384+ t.Errorf("SHA256SUMS does not match the archive:\n%s", out)
336- if !strings.Contains(out, `notes with a \" quote`) {
337- t.Errorf("the quote in the notes was not escaped:\n%s", out)
338 }385 }
339 }386 }
340 387
341-func TestThePublishScriptRefusesWithoutAToken(t *testing.T) {388+func TestTheBuildScriptREADMEInstallsTheModule(t *testing.T) {
342- skipInsideARelease(t)389+ // The page a person lands on has to say the one line that uses the
390+ // library. A release page whose only content is a version number tells
391+ // nobody how to depend on it.
392+ if got := readBuildScript(t); !strings.Contains(got, "go get ${MODULE}@${TAG}") {
393+ t.Error("the staged README never says how to get the module")
394+ }
395+}
343 396
344- dir := t.TempDir()397+func TestTheWorkflowPublishesOnATagPush(t *testing.T) {
345- copyModuleInto(t, dir)398+ // The tag push is the trigger: ./01-release.tag.sh ends by pushing one,
346- writeFile(t, filepath.Join(dir, "release.env"),399+ // and nothing else starts a release.
347- "TAG=\"v0.0.1-test\"\nABOUT=\"notes\"\nOWNER=\"turbo-editors\"\nREPO=\"turbo-core\"\n")400+ workflow := readWorkflow(t)
348 401
349- out, err := runAllowingFailure(t, dir, "./02-release.publish.sh", "--dry-run")402+ for _, want := range []string{"push:", "tags:", `- "v*"`} {
403+ if !strings.Contains(workflow, want) {
404+ t.Errorf("the workflow never declares %q", want)
405+ }
406+ }
407+ // A workflow with the default read-only token cannot create a release, and
408+ // fails at its last step after doing all the work.
409+ if !strings.Contains(workflow, "contents: write") {
410+ t.Error("the workflow does not ask for contents: write")
411+ }
412+}
350 413
351- if err == nil {414+func TestTheWorkflowStagesWithTheSameScriptAPersonRuns(t *testing.T) {
352- t.Fatalf("the script ran without a token:\n%s", out)415+ // A CI job that stages its own way is a second pipeline nobody tests, and
416+ // the local one is then only ever exercised by accident.
417+ if got := readWorkflow(t); !strings.Contains(got, "./02-build-releases.sh") {
418+ t.Error("the workflow does not stage the release with ./02-build-releases.sh")
353 }419 }
354- if !strings.Contains(out, "TOKEN is not set") {420+}
355- t.Errorf("the refusal does not say the token is missing:\n%s", out)421+
422+func TestTheWorkflowAttachesWhatWasStaged(t *testing.T) {
423+ // Publishing a release page with no files attached is a silent half-job:
424+ // the page exists and the links are not there.
425+ workflow := readWorkflow(t)
426+
427+ for _, want := range []string{"turbo-core-*.tar.gz", "SHA256SUMS", "fail_on_unmatched_files: true"} {
428+ if !strings.Contains(workflow, want) {
429+ t.Errorf("the workflow never mentions %q", want)
430+ }
356 }431 }
357 }432 }
358 433
359-func TestThePublishScriptLinksToTheDocumentationAtThatTag(t *testing.T) {434+func TestTheWorkflowLinksToTheDocumentationAtThatTag(t *testing.T) {
360- // A release page is not inside the repository tree, so a relative path from435+ // A release page is not inside the repository tree, so a relative path
361- // it 404s — and a link to the branch would rot as the branch moves.436+ // from it 404s — and a link to the branch would rot as the branch moves.
362- script := readPublishScript(t)437+ workflow := readWorkflow(t)
363 438
364- if !strings.Contains(script, "/src/tag/${TAG}/docs/") {439+ if !strings.Contains(workflow, "blob/${GITHUB_REF_NAME}") {
365- t.Error("the release notes do not link to the documentation at the released tag")440+ t.Error("the release notes do not link into the repository at the released tag")
441+ }
442+ if !strings.Contains(workflow, "/docs/en/README.md") {
443+ t.Error("the release notes do not link to the documentation")
366 }444 }
367 }445 }
368 446
369-// codeLines returns the lines of a shell script that are not comments.447+func TestTheWorkflowNeedsNoPersonalToken(t *testing.T) {
370-func codeLines(script string) []string {448+ // The release API behind Rickub's /gh shim accepts the job's own
371- var out []string449+ // GITHUB_TOKEN and refuses a personal one, so a secret referenced here is
372- for _, line := range strings.Split(script, "\n") {450+ // a credential that cannot work and still has to be kept somewhere.
373- if trimmed := strings.TrimSpace(line); trimmed != "" && !strings.HasPrefix(trimmed, "#") {451+ if got := readWorkflow(t); strings.Contains(got, "secrets.") {
374- out = append(out, line)452+ t.Error("the workflow reads a secret; the job's own token is the only credential the release API takes")
375- }453+ }
454+}
455+
456+func TestTheWorkflowDoesNotStartAReleaseInsideItself(t *testing.T) {
457+ // The suite it runs includes tests that run ./01-release.tag.sh against a
458+ // throwaway clone. Locally the script exports this before calling make;
459+ // in CI nothing calls the script, so the job has to set it itself.
460+ if got := readWorkflow(t); !strings.Contains(got, "TURBO_CORE_RELEASING") {
461+ t.Error("the workflow runs the suite without TURBO_CORE_RELEASING set")
376 }462 }
377- return out
378 }463 }