Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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: |
Expand All @@ -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
Expand Down
43 changes: 33 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
9 changes: 9 additions & 0 deletions docs/dev/versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
17 changes: 17 additions & 0 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |

Expand All @@ -25,6 +25,19 @@ Output: `dist/godo_<ver>_<os>_<arch>` + `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).
25 changes: 16 additions & 9 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | |
|---------|--|
Expand All @@ -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`
Expand All @@ -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

Expand Down
8 changes: 4 additions & 4 deletions internal/cli/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
47 changes: 46 additions & 1 deletion pkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,61 @@
// 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:
//
// -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

Expand Down
45 changes: 45 additions & 0 deletions version_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading