From a5e0f93f39a7b2c6405d9234e29b2169dd27a85e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=9E=E3=82=A4=E3=83=8E=E3=83=AB?= <97069334+MY-RV@users.noreply.github.com> Date: Sun, 20 Sep 2026 22:19:53 -0600 Subject: [PATCH] chore: release v0.3.0-preview.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A preview, because plugin loading is new and has only been run on macOS and Linux. It is a GitHub pre-release, which is the whole mechanism: `godo -e update` reads the newest *release*, and the Homebrew cask and Scoop manifest are left alone, so nobody arrives at a preview without going to get it. Also here because the preview is otherwise a trap: - `go install …/cmd/godo@v0.3.0-preview.1` links none of our -ldflags, so the binary reported 0.1.0-dev and `engine.version: ">=0.3.0"` refused a catalog it actually satisfied. The tag now comes from Go's build info, read through godo.Release(). A build from a working tree still says 0.1.0-dev: Go names it with a pseudo-version, and nobody released that. - The changelog still said "Nothing loads plugins yet", and never said plugins run — the release's whole point. - A rebase had copied the --flag= quoting fix into the released [0.1.0] section. A released section does not gain entries. --- .goreleaser.yaml | 8 +++++-- CHANGELOG.md | 43 +++++++++++++++++++++++++++++--------- docs/dev/versioning.md | 9 ++++++++ docs/install.md | 17 +++++++++++++++ docs/release.md | 15 +++++++++++++- docs/roadmap.md | 25 ++++++++++++++-------- internal/cli/app.go | 8 +++---- pkg.go | 47 +++++++++++++++++++++++++++++++++++++++++- version_test.go | 45 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 190 insertions(+), 27 deletions(-) create mode 100644 version_test.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 3f1f2c7..21d2b10 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -58,6 +58,10 @@ checksum: # Packaging homes. Both pushes need a PAT with write access to those repos, # exposed to the release workflow as TAP_GITHUB_TOKEN; without it the release # still publishes, it just does not update the tap or the bucket. +# A pre-release is skipped as well: `brew install` and `scoop install` are how +# people get the version they are meant to run, so a preview must not land +# there. Spelled out with .Prerelease rather than the `auto` shorthand, so the +# rule reads the same in both pipes and cannot be read as anything else. # Homebrew ships pre-built binaries as casks now; `brews` is deprecated and # makes `goreleaser check` fail. Casks are macOS-only — Linux installs go # through `go install` or the release binary (docs/install.md). @@ -72,7 +76,7 @@ homebrew_casks: directory: Casks homepage: "https://github.com/MY-RV/godo" description: "Thin repo command catalog" - skip_upload: '{{ if .Env.TAP_GITHUB_TOKEN }}false{{ else }}true{{ end }}' + skip_upload: '{{ if and .Env.TAP_GITHUB_TOKEN (not .Prerelease) }}false{{ else }}true{{ end }}' hooks: post: install: | @@ -91,7 +95,7 @@ scoops: homepage: "https://github.com/MY-RV/godo" description: "Thin repo command catalog" license: "MIT" - skip_upload: '{{ if .Env.TAP_GITHUB_TOKEN }}false{{ else }}true{{ end }}' + skip_upload: '{{ if and .Env.TAP_GITHUB_TOKEN (not .Prerelease) }}false{{ else }}true{{ end }}' changelog: sort: asc diff --git a/CHANGELOG.md b/CHANGELOG.md index de10ef3..46d3649 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ ## [Unreleased] +## [0.3.0-preview.1] — 2026-09-20 + +**A preview.** It is a GitHub pre-release, so `godo -e update` does not offer +it and neither Homebrew nor Scoop will hand it to you — download it from +[Releases](https://github.com/MY-RV/godo/releases), or +`go install github.com/my-rv/godo/cmd/godo@v0.3.0-preview.1`. On a preview +binary `godo -e update check` reports no update, because the newest *release* +is still 0.2.0. + +It is a preview because plugin loading is new and has been run on macOS and +Linux only. Everything below is what 0.3.0 will promise; the preview is +where it gets found out. + Breaking. The shell that runs your scripts changed. ### Changed @@ -41,10 +54,19 @@ Breaking. The shell that runs your scripts changed. (minimum binary, enforced before anything runs), `dialect`, `runner`, and `plugins`. `engine.plugins` takes `source`, a **required** `sha256`, `provides: [runner:name]`, and an optional `config` that is entirely the - plugin's — godo carries it without reading it. Nothing loads plugins yet; - they are parsed and validated so the shape is settled, and a script asking - for a runner a plugin provides fails by naming that plugin instead of reading - as a typo. + plugin's — godo carries it across and reads only `fs.mount`, which says which + directories the sandbox can see. +- **Plugins run.** A plugin is a WebAssembly (WASI) program; godo runs it with + [wazero](https://wazero.io), which is pure Go, so the binary you already have + is the whole runtime — no cgo, no toolchain, nothing to install. The artifact + is checked against its `sha256` **before** it is compiled, so what runs is + what was reviewed. A script picks one by name (`# @runner micropy`) and godo + hands it the body; the plugin asks godo back for what it cannot do itself, + over four ops — `exec`, `out`, `slink` and `fetch`. Its only view of the disk + is the directories `config.fs.mount` names, the catalog's own directory by + default. `--preview` prints the body and never starts the plugin. + The first one is [godo-micropy](https://github.com/MY-RV/godo-micropy): + MicroPython, so a script can be Python on every machine godo runs on. - **`${godo:file(path)}` as a whole script value** puts the body in a file, so a Python or shell script gets an editor that understands it. Inclusion rather than expansion: it happens when the body is read, works for every runner, and @@ -69,6 +91,13 @@ Breaking. The shell that runs your scripts changed. `DefaultRunners`, `EffectiveRunner`, `WithRunners`, `ErrUnknownRunner`. ### Fixed +- **A `go install …@v0.3.0` binary reports the version it was installed at.** + Nothing links our `-ldflags` on that path, so the version stayed at its + `0.1.0-dev` default — cosmetic until `engine.version` arrived, and then + enough to make a catalog refuse a binary that actually satisfied it. The tag + now comes from Go's build info. A build from a working tree still says + `0.1.0-dev`: Go describes it with a pseudo-version, and that is not a release + anyone made. - A value shaped like `--flag=…` is quoted from the `=` onward, so `--preview` shows `git commit --am='two words'` instead of `git commit '--am=two words'`, which read as though the flag name were part of the message. Identical single @@ -143,12 +172,6 @@ Full rules in [docs/contract.md](./docs/contract.md). - Product docs (overview, getting started, guides, reference, distribution, roadmap). - Origin story (EN/ES), SECURITY.md, CONTRIBUTING, GitHub issue/PR templates, CODEOWNERS. -### Fixed -- A value shaped like `--flag=…` is quoted from the `=` onward, so `--preview` - shows `git commit --am='two words'` instead of `git commit '--am=two words'`, - which read as though the flag name were part of the message. Identical single - argument to the shell — rendering only. - ### Notes - Product display name: **GoDo**; identifiers remain lowercase `godo`. - Pre-1.0: APIs and CLI may still change. Treat `v0.x` as evolving. diff --git a/docs/dev/versioning.md b/docs/dev/versioning.md index fc2e725..fc0c6f4 100644 --- a/docs/dev/versioning.md +++ b/docs/dev/versioning.md @@ -18,6 +18,15 @@ Module: `github.com/my-rv/godo` Do not confuse them. +Read the first one through `godo.Release()`, never `godo.Version` directly. +`go install …/cmd/godo@vX.Y.Z` links none of our flags, so `Version` stays at +its `0.1.0-dev` default; `Release()` takes the tag from Go's build info when it +finds one there. A build from a working tree keeps saying `0.1.0-dev` — Go +describes it with a pseudo-version, and no one released that. + +`engine.version` is compared against `Release()`, so a binary installed at a +tag is judged by the tag. + ## Stability today (v0.1) | Surface | Stability | diff --git a/docs/install.md b/docs/install.md index 743c02b..9b91bad 100644 --- a/docs/install.md +++ b/docs/install.md @@ -34,6 +34,23 @@ Local build: go build -ldflags "-X github.com/my-rv/godo.Version=v0.2.0" -o godo ./cmd/godo ``` +## Preview releases + +A preview is published as a GitHub **pre-release**. `godo -e update` reads the +*latest release*, and package managers carry the same, so a preview never +arrives on its own — you go and get it: + +```bash +go install github.com/my-rv/godo/cmd/godo@v0.3.0-preview.1 +``` + +or download the `v0.3.0-preview.1` assets from +[Releases](https://github.com/MY-RV/godo/releases). + +On a preview binary `godo -e update check` reports no update available: the +newest release really is older than what you are running. Reinstalling the +stable line (`brew`, `scoop`, or `@latest`) is how you leave a preview. + ## Package managers ```bash diff --git a/docs/release.md b/docs/release.md index e28c51c..c7cf001 100644 --- a/docs/release.md +++ b/docs/release.md @@ -4,7 +4,7 @@ | What | Where | |------|--------| -| Binary / module | `godo.Version` (ldflags `-X github.com/my-rv/godo.Version=…`) | +| Binary / module | `godo.Version` (ldflags `-X github.com/my-rv/godo.Version=…`), read through `godo.Release()` | | Catalog file | `version:` inside `godo.yaml` | | Git tag | `vX.Y.Z` | @@ -25,6 +25,19 @@ Output: `dist/godo___` + `SHA256SUMS`. 3. Workflow **release** runs GoReleaser → archives **and** bare binaries + checksums 4. Users: download, `go install …@vX.Y.Z`, or `godo -e update` +## Preview (pre-release) + +Tag `vX.Y.Z-preview.N`. The suffix is the whole mechanism: + +- GoReleaser marks the GitHub release **pre-release** (`release.prerelease: auto`) +- the Homebrew cask and the Scoop manifest are **not** updated — `skip_upload` + is false only for a tag with no pre-release part +- `godo -e update` reads `/releases/latest`, which GitHub answers with the newest + *release*, so a preview is never offered to anyone + +So a preview reaches only the people who go and get it, which is the point of +one. Everything else is the same as above. + ## After first release Optional packaging homes (family): `MY-RV/homebrew-tap`, `MY-RV/scoop-bucket` — [distribution.md](./distribution.md). diff --git a/docs/roadmap.md b/docs/roadmap.md index bf675dc..923bb45 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -16,11 +16,16 @@ This is what we **commit to communicate**. Pre-1.0 APIs can still change within **Not promised in v0.1:** Homebrew/Scoop installs, dialects `nscript`/`matchns`, stable Go API. -## v0.3 — in progress, unreleased +## v0.3 — previewed as `v0.3.0-preview.1` Breaking: the shell that runs your scripts changes, and file fields move into -`engine:`. **Held until plugin loading works** — `engine:` without a loader is -half a promise, and this release is where the promise gets made. +`engine:`. It was held until plugin loading worked — `engine:` without a loader +is half a promise, and this is the release where the promise gets made. + +A preview is a GitHub pre-release: `godo -e update` does not offer it, and +neither Homebrew nor Scoop carries it. It is on +[Releases](https://github.com/MY-RV/godo/releases) and on +`go install github.com/my-rv/godo/cmd/godo@v0.3.0-preview.1`. | Promise | | |---------|--| @@ -30,17 +35,21 @@ half a promise, and this release is where the promise gets made. | `engine:` block | `version`, `dialect`, `runner`, `plugins` — what godo needs, apart from what the scripts are | | `engine.version` | Minimum binary, enforced before anything runs | | `godo -e runners` | What is usable here, and how to check which shell you are in | +| Plugin loading | WASM via `wazero`, digest-pinned — see [runners and plugins](./dev/runners-and-plugins.md) | +| `godo -e plugins` | Declare, install and list plugins; the digest is computed, never typed | | Compatibility | Top-level `dialect:` keeps working | ### Landed Everything in the table above. -### Still required before v0.3 ships +### Before v0.3.0 ships -- **Plugin loading.** `engine.plugins` parses and validates; nothing reads the - artifact yet. WASM via `wazero`, digest-pinned — see - [runners and plugins](./dev/runners-and-plugins.md). +- Preview feedback. Plugin loading is new, and the preview is where it gets + found out. +- The plugin protocol is **not** frozen by this preview. A plugin is pinned by + digest, so a protocol change cannot silently break a catalog — it fails by + naming the plugin. **Not promised in v0.3:** Windows shell detection is written from the documented behavior of those shells; it compiles and vets for `windows/amd64` @@ -52,8 +61,6 @@ but is unverified on a real Windows host. `GODO_SHELL` overrides it. - Optional: winget (`MY-RV.Godo`), later choco / AUR / Nix as demand appears - Engine command registry polish; more e2e - Dialects backlog only if explicitly promoted here -- Plugin loading (WASM via wazero, digest-pinned) — see - [runners and plugins](./dev/runners-and-plugins.md) ## v1.0 — future promise diff --git a/internal/cli/app.go b/internal/cli/app.go index 9800eb0..de1a275 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -60,7 +60,7 @@ func (a *App) Run(args []string) error { return nil } if mode == modeVersion { - fmt.Fprintln(a.Stdout, godo.Version) + fmt.Fprintln(a.Stdout, godo.Release()) return nil } if mode == modeRunners { @@ -85,7 +85,7 @@ func (a *App) Run(args []string) error { if err != nil { return err } - if err := requireEngineVersion(cat, godo.Version); err != nil { + if err := requireEngineVersion(cat, godo.Release()); err != nil { return err } root := filepath.Dir(path) @@ -182,11 +182,11 @@ func (a *App) runUpdate(checkOnly bool) error { if err != nil { return err } - newer, err := update.Newer(godo.Version, tag) + newer, err := update.Newer(godo.Release(), tag) if err != nil { return err } - fmt.Fprintf(a.Stdout, "current: %s\nlatest: %s\n", godo.Version, tag) + fmt.Fprintf(a.Stdout, "current: %s\nlatest: %s\n", godo.Release(), tag) if !newer { fmt.Fprintln(a.Stdout, "already up to date") return nil diff --git a/pkg.go b/pkg.go index d4a649d..6f2b57a 100644 --- a/pkg.go +++ b/pkg.go @@ -5,7 +5,13 @@ // internal/catalog; the CLI entrypoint is cmd/godo. package godo -import "github.com/my-rv/godo/internal/catalog" +import ( + "regexp" + "runtime/debug" + "strings" + + "github.com/my-rv/godo/internal/catalog" +) // Version is the CLI/module version (not godo.yaml file.version). // Override at link time: @@ -13,8 +19,47 @@ import "github.com/my-rv/godo/internal/catalog" // -ldflags "-X github.com/my-rv/godo.Version=v0.1.0" // // Default stays -dev until a release build injects a tag. +// The initializer is a constant on purpose: -X only reaches a string variable +// that has one. var Version = "0.1.0-dev" +const devVersion = "0.1.0-dev" + +// Release is the version this binary should report: Version when a release +// build stamped it, and otherwise the tag `go install pkg@tag` recorded. +// +// It matters because `go install github.com/my-rv/godo/cmd/godo@v0.3.0` runs +// no linker flags of ours, so Version stays at its default and the binary +// would claim to be 0.1.0 — old enough for `engine.version` to refuse a +// catalog the binary actually satisfies. +func Release() string { + if Version != devVersion { + return Version + } + bi, ok := debug.ReadBuildInfo() + if !ok || !taggedVersion(bi.Main.Version) { + return Version + } + return bi.Main.Version +} + +// reTimestamp matches the 14-digit stamp inside a pseudo-version. +var reTimestamp = regexp.MustCompile(`[0-9]{14}`) + +// taggedVersion reports whether v is a version someone tagged, as opposed to +// "(devel)" or a pseudo-version Go derived from a commit. A build from a +// working tree is a dev build however Go describes it, and saying so is more +// use than a number nobody released. +func taggedVersion(v string) bool { + if v == "" || v == "(devel)" { + return false + } + if strings.ContainsAny(v, "+ ") { // +dirty, +incompatible + return false + } + return !reTimestamp.MatchString(v) +} + // Re-exported names and sentinels. const FileName = catalog.FileName diff --git a/version_test.go b/version_test.go new file mode 100644 index 0000000..70d9c39 --- /dev/null +++ b/version_test.go @@ -0,0 +1,45 @@ +package godo + +import "testing" + +func TestTaggedVersion(t *testing.T) { + cases := []struct { + in string + want bool + }{ + {"v0.3.0", true}, + {"v0.3.0-preview.1", true}, + {"", false}, + {"(devel)", false}, + // What `go build` in a working tree records. + {"v0.2.1-0.20260921040944-a467864ac83c+dirty", false}, + // The same without the dirty marker: still nothing anyone tagged. + {"v0.2.1-0.20260921040944-a467864ac83c", false}, + {"v2.0.0+incompatible", false}, + } + for _, c := range cases { + if got := taggedVersion(c.in); got != c.want { + t.Errorf("taggedVersion(%q) = %v, want %v", c.in, got, c.want) + } + } +} + +// A stamped build wins over anything the build info says: the release is the +// authority on what it released. +func TestReleasePrefersStampedVersion(t *testing.T) { + old := Version + defer func() { Version = old }() + + Version = "v0.3.0" + if got := Release(); got != "v0.3.0" { + t.Fatalf("Release() = %q, want the stamped version", got) + } +} + +// Unstamped, in a test binary, there is no tag to find, so the default stands +// rather than a pseudo-version pretending to be a release. +func TestReleaseFallsBackToDefault(t *testing.T) { + if got := Release(); got != devVersion { + t.Fatalf("Release() = %q, want %q", got, devVersion) + } +}