- commit
- 8bced0f1bbc97b809fca72fe724a6188e6fd3385
- parent
- 0806076645ebc8789845d971f765c49a26af6654
- parent
- 48a9e57ab9940e40e6316c9b9c073866fac13d7c
- author
- Dami <dami.etoile@gmail.com>
| M | Makefile | 10 | +- |
| M | README.md | 5 | +- |
| M | ROADMAP.md | 2 | +- |
| M | docs/ | 58 | +++++++- |
| M | docs/ | 11 | +- |
| M | docs/ | 29 | ++++- |
| A | scripts/ | 77 | ++++++++++ |
| A | scripts/ | 152 | ++++++++++++++++++++ |
| A | scripts/ | 228 | ++++++++++++++++++++++++++++++ |
| A | scripts/ | 83 | +++++++++++ |
@@ -7,7 +7,7 @@ GHOSTTY_SRC := $(DEPS)/ghostty-src
77 GHOSTTY_OUT := $(DEPS)/ghostty88 GHOSTTY_STAMP := $(GHOSTTY_OUT)/.built-$(GHOSTTY_COMMIT)99 10-10+1111 1212 build:1313 CGO_ENABLED=0 go build -o bin/012 ./cmd/012@@ -147,6 +147,14 @@ SITE_NODE := NODE_OPTIONS=--disable-warning=ExperimentalWarning
147147 site: site-deps148148 cd website && $(SITE_NODE) npm run build149149 150+# The site as published: the build above plus a release's archives and151+# SHA256SUMS under /releases/<version>/ and /releases/latest/, and152+# install.sh and install.ps1 at the root. VERSION is the newest version153+# tag unless set; the archives are dist/'s when it holds that version,154+# else built from the tag. See docs/contributing/releasing.md.155+site-release: site156+ VERSION=$(VERSION) scripts/site-release.sh157+150158 site-serve: site-deps151159 cd website && $(SITE_NODE) npm run start152160 @@ -15,9 +15,12 @@ It is one pure-Go binary.
1515 ## Install1616 1717 ```sh18-go install github.com/FelineStateMachine/012/cmd/012@latest18+curl -fsSL https://f58b.n.zip/install.sh | sh # macOS, Linux: into ~/.local/bin19+go install github.com/FelineStateMachine/012/cmd/012@latest # or build it with Go 1.271920 ```2021 22+On Windows, `irm https://f58b.n.zip/install.ps1 | iex` in PowerShell.23+2124 Then run `012`, or `012 budget.012` to open a sheet. F1 shows every2225 shortcut, F10 or Alt+letter opens the menus, and Ctrl+K searches every2326 command. [Install and run](docs/getting-started/install.md) has the@@ -65,7 +65,6 @@ shared editing in section 3.
6565 | Item | Result | Size |6666 |---|---|---|6767 | A sheet or chart exported as a static HTML page in 012's look | Sharing a sheet with someone without 012 | S |68-| Release archives and an install script served from the owner's nzip server | Installing without Go | S |6968 7069 ### 6. Agents7170 @@ -165,6 +164,7 @@ stream above rather than a design of their own.
165164 166165 - `make check` before every push; demo tapes (`make demos`); annotated version tags with release notes (v0.2.0 onward; v0.1.0 remains on the Go module proxy): [Testing](docs/contributing/testing.md)167166 - A release checklist (XLSX output opened in Excel, LibreOffice and Google Sheets) and `make dist`, release archives cross-compiled locally with SHA256SUMS: [Releasing](docs/contributing/releasing.md)167+- Installing without Go: `curl -fsSL https://f58b.n.zip/install.sh | sh` (and `install.ps1` on Windows) with the release archives served from the docs site by `make site-release`: [Install and run](docs/getting-started/install.md), [Releasing](docs/contributing/releasing.md#publishing)168168 - `make stress-report` flags regressions against the last release's run, allowing for noise: [Observability](docs/contributing/observability.md#regressions-against-the-last-release)169169 - Grafana: recent traces and a trace view of the nested spans: [Observability](docs/contributing/observability.md#the-stack)170170 - Every overlay, prompts and formula suggestions behind narrow hosts, most in packages of their own with fake-host tests: [Architecture](docs/contributing/architecture.md#the-ui)@@ -7,8 +7,9 @@ sidebar_position: 8
77 88 A release is an annotated version tag whose message is the release99 notes, and archives of the binaries built from it on a maintainer's10-machine. The GitHub repository only hosts the code: nothing builds or11-publishes there, and `make dist` uploads nothing.10+machine, served with the docs site on the owner's nzip server along11+with the install scripts that download them. The GitHub repository only12+hosts the code: nothing builds or publishes there.1213 1314 ## Checklist1415 @@ -26,6 +27,9 @@ flowchart TD
2627 tag -->|the module proxy| install[go install ...@v1.2.3]2728 tag --> dist[make dist]2829 dist --> archives[dist/: archives, SHA256SUMS]30+ archives --> site[make site-release]31+ site --> push[nzip site push website/build public:012]32+ push --> live[install.sh, /releases/v1.2.3/ and /releases/latest/ live]2933 ```3034 3135 1. **`make check`** passes on the commit to be tagged (see@@ -56,6 +60,18 @@ flowchart TD
5660 The Go module proxy picks the tag up, so `go install5761 github.com/FelineStateMachine/012/cmd/012@v1.2.3` works from then on.5862 6. **Binaries**: `make dist VERSION=v1.2.3` on the tagged commit.63+7. **Publish** the site with the release64+ ([Publishing](#publishing)):65+66+ ```sh67+ SITE_URL=https://f58b.n.zip make site-release VERSION=v1.2.368+ nzip site push website/build public:01269+ ```70+71+ then check the live copy: `SHA256SUMS` at72+ `https://f58b.n.zip/releases/v1.2.3/` matches `dist/SHA256SUMS`, and73+ `curl -fsSL https://f58b.n.zip/install.sh | PREFIX=$(mktemp -d) sh`74+ installs a `012` whose `012 version` prints v1.2.3.5975 6076 ## File fixtures6177 @@ -106,4 +122,40 @@ dist/SHA256SUMS
106122 refuses a version that isn't `vMAJOR.MINOR.PATCH` (with an optional107123 `-suffix`) or that is tagged at another commit than the one checked108124 out, and warns when the tree has uncommitted changes. The archives and109-`SHA256SUMS` are what a release attaches, wherever it is published.125+`SHA256SUMS` are what a release publishes.126+127+## Publishing128+129+`make site-release` (`scripts/site-release.sh`) builds the docs site as130+`make site` does and adds the release to `website/build`:131+132+```133+website/build/install.sh134+website/build/install.ps1135+website/build/releases/v1.2.3/ archives, SHA256SUMS, an index page136+website/build/releases/latest/ the same files137+```138+139+`VERSION` defaults to the newest version tag. The archives are `dist/`'s140+when its `SHA256SUMS` names that version; otherwise the script checks141+the tag out in a temporary git worktree, runs `make dist` there and142+keeps the result in `dist/`, so the site can be published from a commit143+after the tag. It checks the archives against `SHA256SUMS` before144+copying them. `SITE_URL` also goes into the copied install scripts, so a145+site built for another address serves scripts that download from it.146+147+The site serves one release, `VERSION`, under both addresses; earlier148+releases stay installable with `go install ...@v1.2.3`. Every push149+replaces the whole site, so the docs are always published with150+`make site-release` ([The docs site](site.md#where-it-lives)): a push151+of a plain `make site` build leaves the install script without its152+archives.153+154+The install scripts live in `scripts/install/`. `install.sh` is POSIX155+sh (it runs under dash as well as sh) and needs curl or wget, tar, and156+sha256sum, shasum or openssl. `install_test.go`, in `make test`, runs157+it as a file and piped to the shell against a local server holding a158+fake release: the latest into `~/.local/bin`, a version under `PREFIX`,159+and a refusal of an archive whose checksum doesn't match. `install.ps1`160+has no automated test, as no PowerShell is assumed on a maintainer's161+machine; try it on Windows when it changes.@@ -26,14 +26,19 @@ site`); the site sits at the root of its host.
2626 ## Where it lives2727 2828 The site is published at [f58b.n.zip](https://f58b.n.zip/), a public,29-permanent address on the owner's nzip server. To publish a new build to30-the same address:29+permanent address on the owner's nzip server, which also serves the30+install scripts and the latest release's archives. To publish a new31+build to the same address:3132 3233 ```sh33-SITE_URL=https://f58b.n.zip make site34+SITE_URL=https://f58b.n.zip make site-release3435 nzip site push website/build public:0123536 ```3637 38+`make site-release` is `make site` with the release added39+([Publishing](releasing.md#publishing)); a push of a plain `make site`40+build would take the install script and the archives off the site.41+3742 The target keeps its policies (public, no expiry) between pushes.3843 3944 ## The build is a docs check@@ -5,12 +5,37 @@ sidebar_position: 2
55 66 # Install and run77 8+012 is one binary. On macOS or Linux, the install script puts the latest9+release in `~/.local/bin`:10+11+```sh12+curl -fsSL https://f58b.n.zip/install.sh | sh13+```14+15+It picks the archive for your OS and architecture, checks it against the16+release's `SHA256SUMS` and says where it put `012`. `PREFIX=/opt/012`17+installs into `/opt/012/bin` instead, and `sh -s -- --system` into18+`/usr/local/bin`, running `sudo` (and printing the command first) only19+when that directory isn't writable; `sh -s -- --version v0.3.0` picks a20+release. On Windows, in PowerShell:21+22+```powershell23+irm https://f58b.n.zip/install.ps1 | iex24+```25+26+which installs `012.exe` into `%LOCALAPPDATA%\Programs\012\bin` and adds27+that folder to your PATH, without administrator rights. The archives and28+`SHA256SUMS` are also at29+[f58b.n.zip/releases/latest/](https://f58b.n.zip/releases/latest/) to30+download by hand.31+32+With Go 1.27 or later, build it from source instead:33+834 ```sh935 go install github.com/FelineStateMachine/012/cmd/012@latest1036 ```1137 12-012 needs Go 1.27 to build and is one pure-Go binary. From a clone,13-`make build` puts it in `bin/012`.38+From a clone, `make build` puts it in `bin/012`.1439 1540 ```sh1641 012 # a new sheet@@ -0,0 +1,77 @@
1+# install.ps1 - installs a released 012 binary on Windows. Served at the2+# docs site's root with the release archives under /releases/; see3+# docs/getting-started/install.md.4+#5+# irm https://f58b.n.zip/install.ps1 | iex6+#7+# It picks the archive for this architecture, checks its SHA256 against8+# the release's SHA256SUMS, puts 012.exe in9+# %LOCALAPPDATA%\Programs\012\bin and adds that folder to the user's10+# PATH. It never asks for administrator rights.11+#12+# $env:O12_VERSION a release other than the latest, like v1.2.313+# $env:O12_INSTALL_DIR another folder for 012.exe14+# $env:O12_BASE_URL the site serving /releases/ (https://f58b.n.zip)15+16+$ErrorActionPreference = 'Stop'17+$ProgressPreference = 'SilentlyContinue'18+19+function Install-012 {20+ $base = if ($env:O12_BASE_URL) { $env:O12_BASE_URL } else { 'https://f58b.n.zip' }21+ $version = if ($env:O12_VERSION) { $env:O12_VERSION } else { 'latest' }22+ if ($version -match '^[0-9]') { $version = "v$version" }23+ $dir = if ($env:O12_INSTALL_DIR) { $env:O12_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA 'Programs\012\bin' }24+25+ $arch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }26+ switch ($arch) {27+ 'AMD64' { $arch = 'amd64' }28+ 'ARM64' { $arch = 'arm64' }29+ default { throw "012 install: no release for $arch; build from source: go install github.com/FelineStateMachine/012/cmd/012@latest" }30+ }31+32+ $tmp = Join-Path ([IO.Path]::GetTempPath()) ("012-install-" + [Guid]::NewGuid())33+ New-Item -ItemType Directory -Path $tmp | Out-Null34+ try {35+ $url = "$base/releases/$version"36+ $sums = Join-Path $tmp 'SHA256SUMS'37+ Invoke-WebRequest -UseBasicParsing -Uri "$url/SHA256SUMS" -OutFile $sums38+39+ # A line of SHA256SUMS: the digest, two spaces, 012_1.2.3_windows_amd64.zip.40+ $line = Get-Content $sums | Where-Object { $_ -match "^([0-9a-f]{64}) (012_\S+_windows_$arch\.zip)$" } | Select-Object -First 141+ if (-not $line) { throw "012 install: release $version has no archive for windows/$arch" }42+ $null = $line -match "^([0-9a-f]{64}) (012_\S+_windows_$arch\.zip)$"43+ $want = $Matches[1]44+ $file = $Matches[2]45+ $name = $file -replace '\.zip$', ''46+ $got_version = 'v' + ($name -replace '^012_', '' -replace "_windows_$arch$", '')47+48+ Write-Host "012 install: downloading $url/$file"49+ $zip = Join-Path $tmp $file50+ Invoke-WebRequest -UseBasicParsing -Uri "$url/$file" -OutFile $zip51+ $got = (Get-FileHash -Algorithm SHA256 -Path $zip).Hash.ToLowerInvariant()52+ if ($got -ne $want) { throw "012 install: $file has SHA256 $got, SHA256SUMS says $want; nothing installed" }53+ Write-Host "012 install: SHA256 checked against SHA256SUMS: $want"54+55+ Expand-Archive -Path $zip -DestinationPath $tmp -Force56+ $exe = Join-Path $tmp "$name\012.exe"57+ if (-not (Test-Path $exe)) { throw "012 install: $file holds no 012.exe" }58+ New-Item -ItemType Directory -Force -Path $dir | Out-Null59+ Copy-Item -Force $exe (Join-Path $dir '012.exe')60+ Write-Host "012 install: installed 012 $got_version (windows/$arch) as $(Join-Path $dir '012.exe')"61+62+ $userPath = [Environment]::GetEnvironmentVariable('Path', 'User')63+ $parts = @()64+ if ($userPath) { $parts = $userPath -split ';' | Where-Object { $_ } }65+ if ($parts -notcontains $dir) {66+ [Environment]::SetEnvironmentVariable('Path', (($parts + $dir) -join ';'), 'User')67+ $env:Path = "$env:Path;$dir"68+ Write-Host "012 install: added $dir to your user PATH; new terminals find 012"69+ } else {70+ Write-Host "012 install: run 012 to start"71+ }72+ } finally {73+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue74+ }75+}76+77+Install-012@@ -0,0 +1,152 @@
1+2+# install.sh - installs a released 012 binary on macOS or Linux. Served3+# at the docs site's root with the release archives under /releases/;4+# see docs/getting-started/install.md.5+#6+# curl -fsSL https://f58b.n.zip/install.sh | sh7+# curl -fsSL https://f58b.n.zip/install.sh | sh -s -- --system8+#9+# It picks the archive for this OS and architecture, checks its SHA25610+# against the release's SHA256SUMS, and puts 012 in ~/.local/bin, in11+# $PREFIX/bin when PREFIX is set, or in /usr/local/bin with --system.12+# sudo is only run for --system when that directory isn't writable, and13+# the command is printed first.14+#15+# --version v1.2.3 a release other than the latest (or O12_VERSION)16+# --system install into /usr/local/bin17+# O12_BASE_URL the site serving /releases/ (https://f58b.n.zip)18+set -eu19+20+base=${O12_BASE_URL:-https://f58b.n.zip}21+version=${O12_VERSION:-latest}22+system=023+24+say() { printf '012 install: %s\n' "$*"; }25+die() {26+ printf '012 install: %s\n' "$*" >&227+ exit 128+}29+30+while [ $# -gt 0 ]; do31+ case $1 in32+ --version)33+ [ $# -ge 2 ] || die "--version needs a version, like v1.2.3"34+ version=$235+ shift 236+ ;;37+ --version=*)38+ version=${1#--version=}39+ shift40+ ;;41+ --system)42+ system=143+ shift44+ ;;45+ -h | --help)46+ sed -n '2,17p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//' || true47+ exit 048+ ;;49+ *) die "unknown option $1 (--version v1.2.3, --system)" ;;50+ esac51+done52+53+case $version in54+latest | v[0-9]*) ;;55+[0-9]*) version=v$version ;;56+*) die "--version must be a release like v1.2.3 (got $version)" ;;57+esac58+59+os=$(uname -s)60+case $os in61+Darwin) os=darwin ;;62+Linux) os=linux ;;63+MINGW* | MSYS* | CYGWIN* | Windows*)64+ die "on Windows, run in PowerShell: irm $base/install.ps1 | iex"65+ ;;66+*) die "no release for $os; build from source: go install github.com/FelineStateMachine/012/cmd/012@latest" ;;67+esac68+69+arch=$(uname -m)70+case $arch in71+x86_64 | amd64) arch=amd64 ;;72+arm64 | aarch64) arch=arm64 ;;73+*) die "no release for $arch; build from source: go install github.com/FelineStateMachine/012/cmd/012@latest" ;;74+esac75+# A shell under Rosetta reports x86_64 on an Apple silicon Mac.76+if [ "$os/$arch" = darwin/amd64 ] && [ "$(sysctl -n hw.optional.arm64 2>/dev/null || echo 0)" = 1 ]; then77+ arch=arm6478+fi79+80+if command -v curl >/dev/null 2>&1; then81+ fetch() { curl -fsSL -o "$2" "$1"; }82+elif command -v wget >/dev/null 2>&1; then83+ fetch() { wget -q -O "$2" "$1"; }84+else85+ die "needs curl or wget"86+fi87+88+if command -v sha256sum >/dev/null 2>&1; then89+ sha256() { sha256sum "$1" | cut -d ' ' -f 1; }90+elif command -v shasum >/dev/null 2>&1; then91+ sha256() { shasum -a 256 "$1" | cut -d ' ' -f 1; }92+elif command -v openssl >/dev/null 2>&1; then93+ sha256() { openssl dgst -sha256 -r "$1" | cut -d ' ' -f 1; }94+else95+ die "needs sha256sum, shasum or openssl to check the download"96+fi97+98+if [ "$system" = 1 ]; then99+ bindir=/usr/local/bin100+elif [ -n "${PREFIX:-}" ]; then101+ bindir=$PREFIX/bin102+else103+ [ -n "${HOME:-}" ] || die "HOME is not set; set PREFIX to install into \$PREFIX/bin"104+ bindir=$HOME/.local/bin105+fi106+107+tmp=$(mktemp -d 2>/dev/null || mktemp -d -t 012install)108+trap 'rm -rf "$tmp"' EXIT109+trap 'exit 1' INT TERM110+111+url=$base/releases/$version112+fetch "$url/SHA256SUMS" "$tmp/SHA256SUMS" || die "couldn't download $url/SHA256SUMS"113+114+# A line of SHA256SUMS: the digest, two spaces, 012_1.2.3_linux_amd64.tar.gz.115+line=$(grep " 012_[^ ]*_${os}_${arch}\.tar\.gz\$" "$tmp/SHA256SUMS" | head -n 1 || true)116+[ -n "$line" ] || die "release $version has no archive for $os/$arch"117+want=${line%% *}118+file=${line##* }119+dir=${file%.tar.gz}120+got_version=${dir#012_}121+got_version=v${got_version%_"${os}"_"${arch}"}122+123+say "downloading $url/$file"124+fetch "$url/$file" "$tmp/$file" || die "couldn't download $url/$file"125+got=$(sha256 "$tmp/$file")126+[ "$got" = "$want" ] || die "$file has SHA256 $got, SHA256SUMS says $want; nothing installed"127+say "SHA256 checked against SHA256SUMS: $want"128+129+tar -xzf "$tmp/$file" -C "$tmp" || die "couldn't unpack $file"130+[ -f "$tmp/$dir/012" ] || die "$file holds no 012 binary"131+132+if [ ! -d "$bindir" ] && ! mkdir -p "$bindir" 2>/dev/null; then133+ [ "$system" = 1 ] || die "couldn't create $bindir"134+fi135+if [ -d "$bindir" ] && [ -w "$bindir" ]; then136+ cp "$tmp/$dir/012" "$bindir/012.new"137+ chmod 755 "$bindir/012.new"138+ mv -f "$bindir/012.new" "$bindir/012"139+elif [ "$system" = 1 ]; then140+ command -v sudo >/dev/null 2>&1 || die "$bindir isn't writable and there is no sudo; run as root or leave out --system"141+ say "$bindir isn't writable by $(id -un); running: sudo install -m 755 012 $bindir/012"142+ sudo mkdir -p "$bindir"143+ sudo install -m 755 "$tmp/$dir/012" "$bindir/012"144+else145+ die "$bindir isn't writable; set PREFIX to another directory"146+fi147+148+say "installed 012 $got_version ($os/$arch) as $bindir/012"149+case ":${PATH:-}:" in150+*":$bindir:"*) say "run 012 to start" ;;151+*) say "$bindir is not on your PATH; add it (export PATH=\"$bindir:\$PATH\") or run $bindir/012" ;;152+esac@@ -0,0 +1,228 @@
1+// Package install holds the install scripts served at the docs site's2+// root (install.sh, install.ps1) and the tests that run install.sh3+// against a local server standing in for the site's /releases/.4+package install5+6+import (7+ "archive/tar"8+ "bytes"9+ "compress/gzip"10+ "crypto/sha256"11+ "encoding/hex"12+ "fmt"13+ "net/http"14+ "net/http/httptest"15+ "os"16+ "os/exec"17+ "path/filepath"18+ "runtime"19+ "strings"20+ "testing"21+)22+23+// release is a fake release: a SHA256SUMS and an archive per Unix24+// platform whose 012 is a shell script printing its version.25+type release struct {26+ files map[string][]byte27+}28+29+func newRelease(t *testing.T, version string) release {30+ t.Helper()31+ r := release{files: map[string][]byte{}}32+ var sums strings.Builder33+ for _, goos := range []string{"darwin", "linux"} {34+ for _, arch := range []string{"amd64", "arm64"} {35+ name := fmt.Sprintf("012_%s_%s_%s", strings.TrimPrefix(version, "v"), goos, arch)36+ bin := fmt.Sprintf("#!/bin/sh\necho 012 %s %s/%s\n", version, goos, arch)37+ data := tarball(t, name, bin)38+ r.files[name+".tar.gz"] = data39+ sum := sha256.Sum256(data)40+ fmt.Fprintf(&sums, "%s %s.tar.gz\n", hex.EncodeToString(sum[:]), name)41+ }42+ }43+ r.files["SHA256SUMS"] = []byte(sums.String())44+ return r45+}46+47+func tarball(t *testing.T, dir, bin string) []byte {48+ t.Helper()49+ var buf bytes.Buffer50+ gz := gzip.NewWriter(&buf)51+ tw := tar.NewWriter(gz)52+ for _, f := range []struct {53+ name string54+ body string55+ mode int6456+ }{{dir + "/012", bin, 0o755}, {dir + "/LICENSE", "license\n", 0o644}} {57+ if err := tw.WriteHeader(&tar.Header{Name: f.name, Mode: f.mode, Size: int64(len(f.body)), Typeflag: tar.TypeReg}); err != nil {58+ t.Fatal(err)59+ }60+ if _, err := tw.Write([]byte(f.body)); err != nil {61+ t.Fatal(err)62+ }63+ }64+ if err := tw.Close(); err != nil {65+ t.Fatal(err)66+ }67+ if err := gz.Close(); err != nil {68+ t.Fatal(err)69+ }70+ return buf.Bytes()71+}72+73+// serve stands in for the site: /releases/<version>/<file>, with latest74+// as one of the versions.75+func serve(t *testing.T, releases map[string]release) *httptest.Server {76+ t.Helper()77+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {78+ version, file, ok := strings.Cut(strings.TrimPrefix(r.URL.Path, "/releases/"), "/")79+ data, found := releases[version].files[file]80+ if !ok || !found {81+ http.NotFound(w, r)82+ return83+ }84+ _, _ = w.Write(data)85+ }))86+ t.Cleanup(srv.Close)87+ return srv88+}89+90+// shells are the POSIX shells install.sh runs under: sh, and dash when91+// it's installed, as Debian's /bin/sh is.92+func shells(t *testing.T) []string {93+ t.Helper()94+ if runtime.GOOS == "windows" {95+ t.Skip("install.sh is for macOS and Linux")96+ }97+ out := []string{"sh"}98+ if _, err := exec.LookPath("dash"); err == nil {99+ out = append(out, "dash")100+ }101+ return out102+}103+104+type run struct {105+ shell string106+ env []string107+ args []string108+ stdin bool // the script piped to the shell, as curl | sh does109+}110+111+func (r run) do(t *testing.T, home, base string) (string, error) {112+ t.Helper()113+ script, err := filepath.Abs("install.sh")114+ if err != nil {115+ t.Fatal(err)116+ }117+ var cmd *exec.Cmd118+ if r.stdin {119+ cmd = exec.Command(r.shell, append([]string{"-s", "--"}, r.args...)...)120+ f, err := os.Open(script)121+ if err != nil {122+ t.Fatal(err)123+ }124+ defer f.Close()125+ cmd.Stdin = f126+ } else {127+ cmd = exec.Command(r.shell, append([]string{script}, r.args...)...)128+ }129+ cmd.Dir = home130+ cmd.Env = append([]string{"HOME=" + home, "PATH=" + os.Getenv("PATH"), "O12_BASE_URL=" + base}, r.env...)131+ out, err := cmd.CombinedOutput()132+ return string(out), err133+}134+135+func TestInstallsTheLatestIntoLocalBin(t *testing.T) {136+ srv := serve(t, map[string]release{"latest": newRelease(t, "v9.9.9")})137+ for _, sh := range shells(t) {138+ for _, stdin := range []bool{false, true} {139+ t.Run(fmt.Sprintf("%s/stdin=%v", sh, stdin), func(t *testing.T) {140+ home := t.TempDir()141+ out, err := run{shell: sh, stdin: stdin}.do(t, home, srv.URL)142+ if err != nil {143+ t.Fatalf("install.sh: %v\n%s", err, out)144+ }145+ bin := filepath.Join(home, ".local", "bin", "012")146+ got, err := exec.Command(bin).Output()147+ if err != nil {148+ t.Fatalf("running the installed 012: %v\n%s", err, out)149+ }150+ if !strings.HasPrefix(string(got), "012 v9.9.9 "+runtime.GOOS+"/") {151+ t.Errorf("installed 012 prints %q", got)152+ }153+ for _, want := range []string{"SHA256 checked", "installed 012 v9.9.9", bin, "is not on your PATH"} {154+ if !strings.Contains(out, want) {155+ t.Errorf("output lacks %q:\n%s", want, out)156+ }157+ }158+ })159+ }160+ }161+}162+163+func TestInstallsAVersionUnderPrefix(t *testing.T) {164+ srv := serve(t, map[string]release{165+ "latest": newRelease(t, "v9.9.9"),166+ "v1.2.3": newRelease(t, "v1.2.3"),167+ })168+ for _, sh := range shells(t) {169+ t.Run(sh, func(t *testing.T) {170+ home := t.TempDir()171+ prefix := filepath.Join(home, "opt")172+ out, err := run{shell: sh, env: []string{"PREFIX=" + prefix}, args: []string{"--version", "1.2.3"}}.do(t, home, srv.URL)173+ if err != nil {174+ t.Fatalf("install.sh: %v\n%s", err, out)175+ }176+ got, err := exec.Command(filepath.Join(prefix, "bin", "012")).Output()177+ if err != nil || !strings.HasPrefix(string(got), "012 v1.2.3 ") {178+ t.Fatalf("installed 012 prints %q (%v)\n%s", got, err, out)179+ }180+ if _, err := os.Stat(filepath.Join(home, ".local")); !os.IsNotExist(err) {181+ t.Errorf("PREFIX set, but ~/.local was touched: %v", err)182+ }183+ })184+ }185+}186+187+func TestRefusesAnArchiveThatFailsItsChecksum(t *testing.T) {188+ rel := newRelease(t, "v9.9.9")189+ for name, data := range rel.files {190+ if strings.HasSuffix(name, ".tar.gz") {191+ rel.files[name] = append(append([]byte{}, data...), 0)192+ }193+ }194+ srv := serve(t, map[string]release{"latest": rel})195+ for _, sh := range shells(t) {196+ t.Run(sh, func(t *testing.T) {197+ home := t.TempDir()198+ out, err := run{shell: sh}.do(t, home, srv.URL)199+ if err == nil {200+ t.Fatalf("install.sh succeeded with a bad archive:\n%s", out)201+ }202+ if !strings.Contains(out, "nothing installed") {203+ t.Errorf("output doesn't say why:\n%s", out)204+ }205+ if _, err := os.Stat(filepath.Join(home, ".local", "bin", "012")); !os.IsNotExist(err) {206+ t.Errorf("012 was installed anyway: %v", err)207+ }208+ })209+ }210+}211+212+func TestFailsOnAMissingReleaseOrOption(t *testing.T) {213+ srv := serve(t, map[string]release{"latest": newRelease(t, "v9.9.9")})214+ for _, tc := range []struct {215+ args []string216+ want string217+ }{218+ {[]string{"--version", "v0.0.1"}, "couldn't download"},219+ {[]string{"--version", "next"}, "must be a release"},220+ {[]string{"--frobnicate"}, "unknown option"},221+ } {222+ home := t.TempDir()223+ out, err := run{shell: "sh", args: tc.args}.do(t, home, srv.URL)224+ if err == nil || !strings.Contains(out, tc.want) {225+ t.Errorf("install.sh %v: err %v, output lacks %q:\n%s", tc.args, err, tc.want, out)226+ }227+ }228+}@@ -0,0 +1,83 @@
1+2+# site-release.sh - what `make site-release` does after `make site`:3+# puts a release's archives and SHA256SUMS in the built site under4+# /releases/<version>/ and /releases/latest/, and the install scripts at5+# its root. See docs/contributing/releasing.md.6+#7+# VERSION=v1.2.3 the release to serve; the newest version tag by default8+# SITE_URL the site's address, which the install scripts download from9+#10+# The archives come from dist/ when it holds that version; otherwise11+# `make dist` builds them in a temporary checkout of the tag, so the12+# site can be published from a later commit, and they're kept in dist/.13+set -euo pipefail14+15+root=$(cd "$(dirname "$0")/.." && pwd)16+cd "$root"17+18+site="$root/website/build"19+if [ ! -f "$site/index.html" ]; then20+ echo "site-release: no site in website/build; run make site first" >&221+ exit 122+fi23+24+version=${VERSION:-$(git tag --list 'v[0-9]*' --sort=-v:refname | head -n 1)}25+if [ -z "$version" ]; then26+ echo "site-release: no version tag to serve; set VERSION" >&227+ exit 128+fi29+if ! git rev-parse -q --verify "refs/tags/$version" >/dev/null; then30+ echo "site-release: $version isn't a tag" >&231+ exit 132+fi33+34+# dist/ holds this version when every archive in SHA256SUMS is named for it.35+holds() {36+ [ -f "$1/SHA256SUMS" ] && [ -s "$1/SHA256SUMS" ] &&37+ ! grep -qv " 012_${version#v}_" "$1/SHA256SUMS"38+}39+40+dist="$root/dist"41+if ! holds "$dist"; then42+ tmp=$(mktemp -d)43+ trap 'git -C "$root" worktree remove --force "$tmp/src" 2>/dev/null || true; rm -rf "$tmp"' EXIT44+ echo "site-release: building $version in a checkout of the tag"45+ git worktree add -q --detach "$tmp/src" "$version"46+ make -C "$tmp/src" dist VERSION="$version"47+ holds "$tmp/src/dist" || {48+ echo "site-release: make dist left no archives for $version" >&249+ exit 150+ }51+ rm -rf "$dist"52+ cp -R "$tmp/src/dist" "$dist"53+fi54+(55+ cd "$dist"56+ if command -v sha256sum >/dev/null 2>&1; then57+ sha256sum -c --quiet SHA256SUMS58+ else59+ shasum -a 256 -c --quiet SHA256SUMS60+ fi61+)62+63+# The scripts name https://f58b.n.zip; a site built for another SITE_URL64+# serves scripts that download from itself.65+url=${SITE_URL:-https://f58b.n.zip}66+url=${url%/}67+68+rm -rf "$site/releases"69+for dir in "$version" latest; do70+ mkdir -p "$site/releases/$dir"71+ (cd "$dist" && cp SHA256SUMS 012_* "$site/releases/$dir/")72+ {73+ printf '<!doctype html>\n<meta charset="utf-8">\n<title>012 %s</title>\n' "$version"74+ printf '<h1>012 %s</h1>\n<p>Checksums: <a href="SHA256SUMS">SHA256SUMS</a>. ' "$version"75+ printf 'Install with <code>curl -fsSL %s/install.sh | sh</code>.</p>\n<ul>\n' "$url"76+ (cd "$dist" && for f in 012_*; do printf '<li><a href="%s">%s</a></li>\n' "$f" "$f"; done)77+ printf '</ul>\n'78+ } >"$site/releases/$dir/index.html"79+done80+for f in install.sh install.ps1; do81+ sed "s|https://f58b\.n\.zip|$url|g" "scripts/install/$f" >"$site/$f"82+done83+echo "site-release: $version in website/build/releases/{$version,latest}, install.sh and install.ps1 at the root"