📦 Turbo Core
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, | ||
| 5 | 5 | and the module proxy fetches it the first time somebody asks. |
| 6 | 6 | |
| 7 | 7 | 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 | |
| 10 | 14 | COMMENT |
| 11 | 15 | |
| 12 | 16 | # 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 | 103 | git push origin "${TAG}" |
| 100 | 104 | |
| 101 | 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 | 108 | echo "💡 Now point each editor at it, one at a time:" |
| 103 | 109 | echo " go mod edit -require=codeberg.org/turbo-editors/turbo-core@${TAG}" |
| 104 | 110 | 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.env | 7 | 1. Set TAG and ABOUT in release.env |
| 8 | -2. Run this script: ./01-release.tag.sh | 8 | +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.md | 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 | ||
| 10 | COMMENT | 14 | COMMENT |
| 11 | 15 | ||
| 12 | # Without this, a failing step is ignored and the next one runs anyway. That is | 16 | # 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: | ||
| 6 | 6 | |
| 7 | 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 | 10 | - **[Turbo Rust](https://codeberg.org/turbo-editors/turbo-rust)** — `codeberg.org/turbo-editors/turbo-rust` |
| 11 | 11 | - **[Turbo Python](https://codeberg.org/turbo-editors/turbo-python)** — `codeberg.org/turbo-editors/turbo-python` |
| 12 | 12 | - **[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 @@ | ||
| 2 | 2 | |
| 3 | 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 | 7 | ## Steps |
| 8 | 8 | |
| 9 | 9 | ### 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 | 13 | ```sh |
| 14 | 14 | TAG="v0.1.0" |
| 15 | 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 | 20 | ### 2. Run the script |
| 19 | 21 | |
| 20 | 22 | ```bash |
| @@ -25,20 +27,27 @@ It runs `make check`, refuses a tag already taken here or on origin, refuses a ` | ||
| 25 | 27 | |
| 26 | 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 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. | |
| 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 | 42 | ```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 | |
| 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 | 52 | ### 4. Point the editors at it |
| 44 | 53 | |
| @@ -64,7 +73,7 @@ git tag -a v0.1.0 -m "The Turbo editor library" | ||
| 64 | 73 | 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 | 78 | ### 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 | 97 | ## 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 | 103 | ## See also |
| 93 | 104 | |
| @@ -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 | ## Steps | 7 | ## Steps |
| 8 | 8 | ||
| 9 | ### 1. Say which version | 9 | ### 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 | ```sh | 13 | ```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 script | 20 | ### 2. Run the script |
| 19 | 21 | ||
| 20 | ```bash | 22 | ```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 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. | ||
| 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 | ```bash | 42 | ```bash |
| 33 | -./02-release.publish.sh --dry-run # show the request, send nothing | 43 | +./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 it | 52 | ### 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.0 | 73 | 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 repositories | 78 | ### 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 for | 97 | ## 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 also | 103 | ## 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 | ||
| 79 | 79 | |
| 80 | 80 | ## 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 | 86 | ```bash |
| 85 | -TAG=v0.3.0 ./01-release.tag.sh | |
| 86 | -TAG=v0.3.0 ./02-release.publish.sh | |
| 87 | +./01-release.tag.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 | 92 | Then, in each editor, with **no** workspace and **no** replace: |
| 90 | 93 | |
| 91 | 94 | ```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 code | 80 | ## 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 | ```bash | 86 | ```bash |
| 85 | -TAG=v0.3.0 ./01-release.tag.sh | 87 | +./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 | ```bash | 94 | ```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 | ||
| 10 | 10 | - A checkout of turbo-core beside where you are about to work. If you do not have one: |
| 11 | 11 | |
| 12 | 12 | ```bash |
| 13 | -git clone https://codeberg.org/turbo-editors/turbo-core.git | |
| 13 | +git clone ssh://git@rickub.com/turbo-editors/turbo-core.git | |
| 14 | 14 | ``` |
| 15 | 15 | |
| 16 | 16 | ## 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 | ```bash | 12 | ```bash |
| 13 | -git clone https://codeberg.org/turbo-editors/turbo-core.git | 13 | +git clone ssh://git@rickub.com/turbo-editors/turbo-core.git |
| 14 | ``` | 14 | ``` |
| 15 | 15 | ||
| 16 | ## Step 1 — Make the module | 16 | ## Step 1 — Make the module |
modified
docs/fr/how-to/release-the-library.md +22 -11 | @@ -2,19 +2,21 @@ | ||
| 2 | 2 | |
| 3 | 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 | 7 | ## Étapes |
| 8 | 8 | |
| 9 | 9 | ### 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 | 13 | ```sh |
| 14 | 14 | TAG="v0.1.0" |
| 15 | 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 | 20 | ### 2. Lancer le script |
| 19 | 21 | |
| 20 | 22 | ```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 | 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 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. | |
| 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 | 42 | ```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 | |
| 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 | 52 | ### 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 | 73 | 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 | 78 | ### 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 | 97 | ## 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 | 103 | ## Voir aussi |
| 93 | 104 | |
| @@ -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 | ## Étapes | 7 | ## Étapes |
| 8 | 8 | ||
| 9 | ### 1. Dire quelle version | 9 | ### 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 | ```sh | 13 | ```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 script | 20 | ### 2. Lancer le script |
| 19 | 21 | ||
| 20 | ```bash | 22 | ```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 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. | ||
| 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 | ```bash | 42 | ```bash |
| 33 | -./02-release.publish.sh --dry-run # montre la requête, n'envoie rien | 43 | +./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 éditeurs | 52 | ### 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.0 | 73 | 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ôts | 78 | ### 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 attention | 97 | ## 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 aussi | 103 | ## 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 | ||
| 79 | 79 | |
| 80 | 80 | ## 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 | 86 | ```bash |
| 85 | -TAG=v0.3.0 ./01-release.tag.sh | |
| 86 | -TAG=v0.3.0 ./02-release.publish.sh | |
| 87 | +./01-release.tag.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 | 92 | Puis, dans chaque éditeur, **sans** espace de travail et **sans** replace : |
| 90 | 93 | |
| 91 | 94 | ```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 code | 80 | ## 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 | ```bash | 86 | ```bash |
| 85 | -TAG=v0.3.0 ./01-release.tag.sh | 87 | +./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 | ```bash | 94 | ```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 | ||
| 10 | 10 | - Une copie de turbo-core à côté de l'endroit où vous allez travailler. Si vous n'en avez pas : |
| 11 | 11 | |
| 12 | 12 | ```bash |
| 13 | -git clone https://codeberg.org/turbo-editors/turbo-core.git | |
| 13 | +git clone ssh://git@rickub.com/turbo-editors/turbo-core.git | |
| 14 | 14 | ``` |
| 15 | 15 | |
| 16 | 16 | ## É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 | ```bash | 12 | ```bash |
| 13 | -git clone https://codeberg.org/turbo-editors/turbo-core.git | 13 | +git clone ssh://git@rickub.com/turbo-editors/turbo-core.git |
| 14 | ``` | 14 | ``` |
| 15 | 15 | ||
| 16 | ## Étape 1 — Créer le module | 16 | ## Étape 1 — Créer le module |
modified
release_test.go +177 -92 | @@ -193,9 +193,14 @@ func TestTheReleaseScriptRefusesATagItAlreadyPublished(t *testing.T) { | ||
| 193 | 193 | // copyModuleInto copies the module's source into a directory, so the script can |
| 194 | 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 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. | |
| 199 | 204 | func copyModuleInto(t *testing.T, target string) { |
| 200 | 205 | t.Helper() |
| 201 | 206 | |
| @@ -207,7 +212,51 @@ func copyModuleInto(t *testing.T, target string) { | ||
| 207 | 212 | if entry.Name() == ".git" || strings.HasSuffix(entry.Name(), ".env") { |
| 208 | 213 | 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. | |
| 245 | -func readPublishScript(t *testing.T) string { | |
| 293 | +// readBuildScript returns the staging script's text. | |
| 294 | +func readBuildScript(t *testing.T) string { | |
| 246 | 295 | t.Helper() |
| 247 | 296 | |
| 248 | - data, err := os.ReadFile("02-release.publish.sh") | |
| 297 | + data, err := os.ReadFile("02-build-releases.sh") | |
| 249 | 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 | 301 | return string(data) |
| 253 | 302 | } |
| 254 | 303 | |
| 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) | |
| 271 | 311 | } |
| 312 | + return string(data) | |
| 272 | 313 | } |
| 273 | 314 | |
| 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") { | |
| 277 | 319 | t.Error("the script does not stop on failure") |
| 278 | 320 | } |
| 279 | 321 | } |
| 280 | 322 | |
| 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") | |
| 294 | 328 | } |
| 295 | 329 | } |
| 296 | 330 | |
| 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) | |
| 301 | 335 | |
| 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) | |
| 306 | 346 | } |
| 307 | 347 | } |
| 308 | 348 | |
| 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. | |
| 312 | 353 | 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") | |
| 315 | 356 | } |
| 316 | 357 | |
| 317 | 358 | dir := t.TempDir() |
| 318 | 359 | 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") | |
| 324 | 365 | |
| 366 | + out, err := runAllowingFailure(t, dir, "./02-build-releases.sh", "v0.0.1-test") | |
| 325 | 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") { | |
| 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 | + } | |
| 330 | 376 | } |
| 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") | |
| 333 | 382 | } |
| 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) | |
| 338 | 385 | } |
| 339 | 386 | } |
| 340 | 387 | |
| 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 | +} | |
| 343 | 396 | |
| 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) | |
| 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 { | |
| 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") | |
| 353 | 419 | } |
| 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 | + } | |
| 356 | 431 | } |
| 357 | 432 | } |
| 358 | 433 | |
| 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) | |
| 363 | 438 | |
| 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") | |
| 366 | 444 | } |
| 367 | 445 | } |
| 368 | 446 | |
| 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") | |
| 376 | 462 | } |
| 377 | - return out | |
| 378 | 463 | } |
| @@ -193,9 +193,14 @@ func TestTheReleaseScriptRefusesATagItAlreadyPublished(t *testing.T) { | |||
| 193 | // copyModuleInto copies the module's source into a directory, so the script can | 193 | // 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 hold | 196 | +// .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 a | 197 | +// 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 | continue | 213 | 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 a | 305 | +func readWorkflow(t *testing.T) string { |
| 257 | - // quote, a newline or a backtick — and the release notes are exactly where | 306 | + 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 reach | 324 | + // 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 with | 325 | + // 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 to | 332 | + // 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 on | 350 | + // 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 the | 383 | + 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 from | 435 | + // 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 []string | 449 | + // 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 | } |