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
73 changes: 73 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,73 @@

## [Unreleased]

Breaking. The shell that runs your scripts changed.

### Changed
- **`dialect` moved to `engine.dialect`.** A top-level `dialect:` keeps
working — it shipped in 0.1 and 0.2 — and `engine.dialect` wins if both are
present. `runner` only ever existed as `engine.runner`.
- **The default runner is the shell you are already in**, not `sh -c` /
`cmd /C`. A zsh user gets zsh, a PowerShell user gets PowerShell. The shell
someone uses is their own business; godo proxying to a different one was a
choice that was not godo's to make.
*Consequence, and it is deliberate:* a catalog is read by the shell of
whoever runs it, so zsh syntax behaves differently for a teammate on bash.
godo is a proxy and promises neither cross-OS nor cross-shell. To pin one
shell for everyone, name it (`runner: sh`, `runner: bash`).
*Library:* an unset runner means the `Runner` injected into the engine, so
`NewEngine(cat, myRunner)` is unaffected.

### Added
- **Runner axis.** `{file}.runner` and `# @runner` say *how* a script's body
becomes a process, the way `dialect` says *which* script answers the tokens.
It exists so a plugin's body — which is not a shell line at all — has a way
to say so; see the [design note](./docs/dev/runners-and-plugins.md).
- **`inherit`** (the default): the shell you are in. `GODO_SHELL` overrides
detection; otherwise `$SHELL` on Unix, and on Windows the parent process when
it is a shell, else `%ComSpec%`. Windows reads the parent because the
environment cannot answer — PowerShell sets `PSModulePath` and everything it
starts inherits it, so a `cmd.exe` opened from PowerShell would look like
PowerShell.
- **A shell by name:** `# @runner sh` / `bash` / `zsh` / `dash` / `ksh` / `ash`
/ `fish` / `nu` / `cmd` / `pwsh` / `powershell`. godo does not manage these —
it resolves the name on `PATH` and hands the line over. The name is logical,
never a path: `cmd`, not `cmd.exe`; `pwsh`, not `ps1`. A name outside the
list is refused rather than run, so `# @runner git` cannot quietly become
`git -c <line>`.
- **`engine:` block** — every dial godo turns while reading and running a
catalog, kept apart from `scripts:`, which is the data. Holds `version`
(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.
- **`godo -e runners`** lists what is usable on the machine you are on, and how
to confirm which shell you are in when the detected one looks wrong.
- `--ls <tokens>` prints `@runner` beside `@dialect`.
- `ArgsAwareRunner`: whether a script takes the tokens left over after the
match is the runner's question. A shell keeps the `${godo:args…}` rule,
message included; a runner whose body is a program answers for itself.
- Public `RunnerName`, `RunnerInherit`, `RunnerRegistry`, `NewRunnerRegistry`,
`DefaultRunners`, `EffectiveRunner`, `WithRunners`, `ErrUnknownRunner`.

### 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
- An unknown runner fails when the plan is built (`--preview` included), not at
load: a dialect must resolve before a script can be matched at all, a runner
only to execute. Nothing executes either way.
- A shell is started non-interactively and without a profile, so you get your
shell's grammar, not your shell's setup — aliases and functions are not there.
- The Windows detection cross-compiles and vets but is unverified on a real
Windows host.

## [0.2.0] — 2026-09-19

Breaking. Placeholder syntax inside script bodies changed; catalogs need editing.
Expand Down Expand Up @@ -44,6 +105,12 @@ Full rules in [docs/contract.md](./docs/contract.md).
body. Both existed only to rescue host environment variables from being read
as captures.

### 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
- The Windows quoting path is **untested**: CI runs Linux only, and the runner
tests skip on Windows. `cmd.exe` also expands `%VAR%` before a command sees
Expand All @@ -62,6 +129,12 @@ 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.
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ One topic per file (copy-paste examples):
| [Preview and ls](./guide/preview-and-ls.md) | `--preview`, `--ls` |
| [Package dialect](./guide/package-dialect.md) | Exact names (default) |
| [Matcher dialect](./guide/matcher-dialect.md) | Pattern keys + captures |
| [Runners](./guide/runners.md) | Which shell runs your line; `runner:` / `# @runner` |
| [Deps](./guide/deps.md) | `# @deps` |
| [Placeholders](./guide/placeholders.md) | `${…}` / `${godo:args…}` |
| [Working directory](./guide/working-directory.md) | Catalog root, walk-up |
Expand Down
188 changes: 179 additions & 9 deletions docs/contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,177 @@ version: "0.1"

Contract version of the file (not the `godo` binary).

## Engine

File field: `{file}.engine`. Optional.

```yaml
engine:
version: ">=0.3.0"
dialect: matcher
runner: bash
plugins:
- source: https://github.com/my-rv/godo-micropy@v1.2.0
sha256: "661471…"
provides: [runner:micropy]
config:
proc: {exec: true, spawn: false}
fs: {slink: true}
```

`scripts:` is the catalog — the data. `engine:` is every dial godo turns while
reading and running it: the binary it expects, the dialect, the runner, the
plugins. Keeping them apart is what lets the toolchain side grow without the
script side growing with it.

| Field | |
|-------|--|
| `version` | Minimum `godo` binary |
| `dialect` | How keys match tokens. Default `package` |
| `runner` | How a body becomes a process. Default: the shell you are in |
| `plugins` | Declared plugins (nothing loads them yet) |

### `engine.version`

The minimum `godo` binary, as `"0.3.0"` or `">=0.3.0"`. Only a minimum — no
ranges, no `^`, no `~`. A binary below it refuses the catalog before running
anything:

```
godo: ./godo.yaml needs godo 0.3.0 or newer; this is 0.2.0 (godo -e update)
```

Comparison drops any pre-release suffix, so a `-dev` build is judged by its
numbers.

### `engine.plugins`

**No build loads plugins yet.** Entries are parsed and validated so the shape
is settled and a catalog can already declare what it expects.

| Field | |
|-------|--|
| `source` | Required. Where the plugin comes from |
| `sha256` | **Required.** A plugin is third-party code that runs when someone types `godo test`; without a digest there is nothing to verify it is the code that was reviewed |
| `provides` | Required. `"<kind>:<name>"` entries, kind being `runner` or `dialect`. Two plugins may not provide the same one |
| `config` | Optional, and entirely the plugin's: its keys, its meaning, its defaults. godo carries it across without reading it |

A script asking for a runner a plugin provides fails by naming that plugin:

```
godo: unknown runner: script "wt" asks for runner "micropy", provided by
plugin https://github.com/my-rv/godo-micropy@v1.2.0 — this build cannot load
plugins
```

`godo -e runners` lists what a catalog declares, beside what the machine has.

## Dialect

File field: `{file}.dialect`. Optional; default `package`.
File field: `{file}.engine.dialect`. Optional; default `package`.

```yaml
dialect: package # omit → package
engine:
dialect: package # omit → package
```

File default. Scripts may override with `# @dialect`.

A top-level `dialect:` is still read — it shipped in 0.1 and 0.2 — and
`engine.dialect` wins if both are present.

## Runner

File field: `{file}.engine.runner`. Optional; default `inherit`.

```yaml
engine:
runner: inherit # omit → inherit
```

File default. Scripts may override with `# @runner`.

`dialect` answers *which script responds to these tokens*; `runner` answers
*how the resolved body becomes a process*. They are independent: any dialect
may be paired with any runner.

| Runner | |
|--------|--|
| `inherit` | The shell you are already in. Default |
| *a shell name* | `sh`, `bash`, `zsh`, `dash`, `ksh`, `ash`, `fish`, `nu`, `cmd`, `pwsh`, `powershell` |

`godo -e runners` lists what is usable on the machine you are on.

godo does not manage these shells — it resolves the name on `PATH` and hands
the line over. The line is yours and the shell is yours; godo is the proxy.

### `inherit`

The default. Your shell runs your line:

```yaml
arr: "arr=(a b c); echo ${arr[1]}"
```

```
zsh → a (zsh indexes arrays from 1)
sh → b (sh indexes arrays from 0)
```

Selection, in order:

| | |
|--|--|
| `GODO_SHELL` | Always wins |
| Unix | `$SHELL`, else `/bin/sh` |
| Windows | the parent process when it is a shell, else `%ComSpec%` |

godo answers "which shell am I in" from the parent process, which is the only
thing that knows. No command run *inside* a shell can report it — it would only
describe the shell godo just started. To confirm it yourself, in your own
terminal: `echo $0` (sh, bash, zsh, dash, ksh), `echo $version` (fish),
`$PSVersionTable.PSVersion` (PowerShell), `echo %COMSPEC%` (cmd). `godo -e
runners` prints these too.

Windows reads the parent process because the environment cannot answer:
PowerShell sets `PSModulePath` and everything it starts inherits it, so a
`cmd.exe` opened from PowerShell would look like PowerShell. On Unix, `$SHELL`
is the login shell rather than the one running right now — bash started inside
zsh still reports zsh. `GODO_SHELL` is how you disagree with either.

The shell is started non-interactively and without a profile (`-c`, `/C`, or
`-NoProfile -Command`), so this gives you your shell's **grammar**, not your
shell's **setup** — your aliases and functions are not there.

**Consequence:** a catalog is read by the shell of whoever runs it, so a script
written in zsh syntax behaves differently for a teammate on bash. That is
deliberate — godo is a proxy and promises neither cross-OS nor cross-shell.

### A shell by name

To pin one shell for everyone, name it:

```yaml
# @runner bash
ci: shopt -s globstar && echo **/*.go
```

The name is **logical, never a path**: write `cmd`, not `cmd.exe`; `pwsh`, not
`pwsh.exe` or `ps1` (`.ps1` is a script extension, not the program). The
platform's extension is `PATH`'s business, so the same `godo.yaml` reads the
same everywhere.

A name outside the list above is refused rather than run — otherwise
`# @runner git` would quietly become `git -c <line>`. For a shell not on the
list, set `GODO_SHELL` and use `inherit`.

A shell that is not installed here fails when the plan is built, so nothing
executes.

Whether a script accepts the tokens left over after the match is also the
runner's question. A shell accepts them only when the body references
`${godo:args…}`; a runner whose body is a program answers for itself.

## Bind

Godo expands placeholders **in-process** before `exec` / preview, in two spaces:
Expand All @@ -91,13 +252,17 @@ no escape syntax.
`${godo:argv[i]}` with a number is an error pointing at `${godo:args[i]}`.
`${godo:…}` inside a matcher key or a `@deps` entry is rejected.

Expanded values are **shell-quoted** for the host shell (`sh` / `cmd`): one
argument in is one argument out. `:raw` opts a single placeholder out.
Expanded values are **shell-quoted** (`sh` rules on POSIX, `cmd` rules on
Windows): one argument in is one argument out. `:raw` opts a single placeholder
out.

A value shaped like `--flag=…` is quoted from the `=` onward, so a preview
reads `--am='two words'` rather than `'--am=two words'`. Same single argument
to the shell; the flag name is not part of the value.

Those rules also hold for bash, zsh, dash, ksh and fish. PowerShell quotes
differently, so a value containing a backtick or `$` may not survive there.

Windows caveat: `cmd.exe` expands `%VAR%` and `!VAR!` before a command sees its
arguments, and no quoting on the command line fully suppresses that.

Expand All @@ -118,6 +283,7 @@ YAML comment block **immediately above** the script key. Apply to scripts only.
|------|--|
| `# text` (no `@`) | Doc |
| `# @dialect <name>` | Match this script with another dialect (override of `{file}.dialect`) |
| `# @runner <name>` | Run this script with another runner (override of `{file}.runner`) |
| `# @deps a, b` | Run `a`, then `b`, then the value |
| `# @dependencies a, b` | Alias of `@deps` |

Expand All @@ -134,10 +300,12 @@ test ${MODULE}: go test ./${godo:argv[MODULE]}/...
```

- `@dialect` — per-script override; without it, uses `{file}.dialect`
- `@runner` — per-script override; without it, uses `{file}.runner`
- `@deps` / `@dependencies` — invocation like `godo …` (space-separated tokens; entries separated by `,`)
- literals and bare `${NAME}` from captures **already bound** by the match (godo space)
- no `${godo:…}` in `@deps`; entries resolve to tokens, so a capture holding a space stays one token
- order = list order; stop on first failure
- each step keeps **its own** runner: a dep declaring `@runner` runs under that runner, not the caller's
- cycle → error (on the **expanded** invocation)
- caller args are **not** forwarded to deps
- diamond (A→B,C and B→C): C runs **once**, at its first (deepest-first) position
Expand All @@ -155,7 +323,8 @@ scripts.<name>: string | string[]

```yaml
version: "0.1"
dialect: package
engine:
dialect: package

scripts:
# Unit tests
Expand All @@ -178,7 +347,7 @@ scripts:
- `string` — one command
- `string[]` — in order; stop on first failure
- `${godo:args}` / `${godo:args[i]}` / `${godo:args[i..j]}` — optional in the value
- without `${godo:args}` → extra tokens error
- without `${godo:args}` → extra tokens error (see **Runner**)
- no captures in keys (that is `matcher`)

## Dialect `matcher`
Expand All @@ -192,7 +361,8 @@ scripts.<pattern>: string | string[]

```yaml
version: "0.1"
dialect: matcher
engine:
dialect: matcher

scripts:
"${GRP} ${SCR}": go run -C scripts/${GRP}/${SCR} . ${godo:args}
Expand Down Expand Up @@ -222,7 +392,7 @@ Reserved: cannot be used as `{file}.dialect` / `@dialect` until implemented and
## Common semantics

1. Resolve `godo.yaml` from cwd upward through parents.
2. Read `{file}.version` and `{file}.dialect` (omit dialect → `package`; dialect must be implemented).
3. Match → bind captures → expand `@deps` → run deps → expand value → execute (or `--preview` / `--ls`).
2. Read `{file}.version` and `{file}.engine` (omit `engine.dialect` → `package`, must be implemented; omit `engine.runner` → `inherit`).
3. Match → bind captures → expand `@deps` → run deps → expand value → execute with the step's runner (or `--preview` / `--ls`).
4. Exit code = of the command (or the first failure in a list / deps); catalog/match errors → `1`.
5. Exec cwd = directory of the `godo.yaml` (see **Exec cwd**).
1 change: 1 addition & 0 deletions docs/dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Internals and contributor conventions. Product docs live in [`docs/`](../README.
| [Architecture](./architecture.md) | Facade / internal / CLI |
| [Standards](./standards.md) | Coding bar |
| [Versioning](./versioning.md) | SemVer + binary vs file `version:` |
| [Runners and plugins](./runners-and-plugins.md) | Premise: godo is a proxy; cross-OS is a plugin's job |
| [Archive](./archive/README.md) | Brainstorm notes (historical) |

Start with root [`CONTRIBUTING.md`](../../CONTRIBUTING.md).
Loading
Loading