magus v0.4.3 is out. See what's new
¶ View generated markdown
9 min read

go

The go spell wires the Go toolchain into a magusfile: each op forks a go (or gofmt) subcommand directly, with no shell. Lint and vulnerability scanning run as go tool invocations so they resolve from the module's tool block rather than PATH.

Runtime name: go (source spells/golang/)

Version probe (go): go version

Version probe (golangci-lint): golangci-lint --version

Passing arguments to ops

Every op is invoked as go["<op>"](ctx, opts?). The first argument is the target's context, which is what carries the execution environment; the optional options map shapes the command itself:

Key Type Description Source
args [str] Extra arguments appended to the resolved command, replacing any trailing defaults the op declares (go-test's ./...), so passing args also states the scope. Omit it and a bare go["<op>"]() keeps the defaults and forwards magus run <target> -- <extra> to the tool automatically; pass it to set the arguments explicitly, which replaces that passthrough. To keep the passthrough too, append the target's own args parameter: {"args": ["-race", "./..."] + args}. source
stdin str Data written to the command's standard input. source

Working directory and environment are NOT options: they ride the context, as go["<op>"](ctx.withCwd("sub")) and go["<op>"](ctx.withEnv({"CGO_ENABLED": "0"})). Only the context reaches the cache key, so an option-table cwd or env would change what the tool did while the key said otherwise; passing either as an option is an error.

Charms (the :charm suffix, e.g. magus run test:rw) are orthogonal: they patch the base argv, while these options add to it. See Charms.

go-build

Command: go build

Example

// Wire go-build into a `build` target. `magus run build` forks `go build`.
import "magus";
import "magus/spell/go";

magus\project({ "spells": [go] });

export fun build(ctx: magus\Context, args: [str]) > void {
    go["go-build"](ctx);
}

go-clean

go rejects -cache/-testcache/-modcache/-fuzzcache combined with ANY package pattern ("cannot be used with package arguments"), in any order, so "./..." can only be safe on a fully bare invocation. defaultArgs cannot express that: it rides along even when magus run <t> -- <extra> forwards a flag. trailingArgs drops out the moment any arg is forwarded, keeping a bare magus run go::go-clean . identical to today (go clean ./...) while -- -cache (or -i/-r/-n/-x) reaches go clean with no pattern at all.

Command: go clean ./...

Example

// Wire go-clean into a `clean` target: `magus run clean` forks `go clean ./...`.
import "magus";
import "magus/spell/go";

magus\project({ "spells": [go] });

export fun clean(ctx: magus\Context, args: [str]) > void {
    go["go-clean"](ctx);
}

go-fmt

Command: gofmt -l .

rw

Replaces -l with -w.

JSON Patch
[
  {
    "op": "replace",
    "path": "/0",
    "value": "-w"
  }
]

Example

// go-fmt lists misformatted files; the rw charm rewrites them in place.
// `magus run format` checks, `magus run format:rw` applies gofmt.
import "magus";
import "magus/spell/go";

magus\project({ "spells": [go] });

export fun format(ctx: magus\Context, args: [str]) > void {
    go["go-fmt"](ctx);
}

go-generate

Command: go generate ./...

Example

// Wire go-generate into a `generate` target: `magus run generate` forks
// `go generate ./...`.
import "magus";
import "magus/spell/go";

magus\project({ "spells": [go] });

export fun generate(ctx: magus\Context, args: [str]) > void {
    go["go-generate"](ctx);
}

go-mod-download

Command: go mod download

update

Replaces mod with get, replaces download with -u, inserts ./....

JSON Patch
[
  {
    "op": "replace",
    "path": "/0",
    "value": "get"
  },
  {
    "op": "replace",
    "path": "/1",
    "value": "-u"
  },
  {
    "op": "add",
    "path": "/2",
    "value": "./..."
  }
]

go-mod-edit

Edit is offline: it "reads only go.mod; it does not look up information about the modules involved" (go help mod edit), so the same tree always yields the same bytes. The write charm is therefore rw, not update.

Command: go mod edit -print

rw

Drops -print.

JSON Patch
[
  {
    "op": "remove",
    "path": "/2"
  }
]

go-mod-json

Captures Go's structured module view for the spell's higher-level Buzz helper. This is deliberately a separate read-only op: -json and -print are distinct Go modes, while go-mod-edit remains the one command that applies derived edits.

Command: go mod edit -json

go-mod-tidy

Tidy resolves against the module proxy, and an import go.mod does not require yet arrives at latest, so its result turns on what upstream serves today rather than on this tree alone. The write charm is therefore update, not rw.

Command: go mod tidy --diff

update

Drops --diff.

JSON Patch
[
  {
    "op": "remove",
    "path": "/2"
  }
]

Example

// go-mod-tidy checks go.mod/go.sum with --diff (CI-safe); the rw charm drops
// --diff so `magus run tidy:rw` applies the changes.
import "magus";
import "magus/spell/go";

magus\project({ "spells": [go] });

export fun tidy(ctx: magus\Context, args: [str]) > void {
    go["go-mod-tidy"](ctx);
}

go-run

Command: go run

Example

// Run a repo-local Go tool through the spell instead of proc.exec. go-run has no
// useful bare form: name the package and its flags via the "args" option, which
// append after `go run`. This forks `go run ./cmd/gen-docs -out ./docs`.
import "magus";
import "magus/spell/go";

magus\project({ "spells": [go] });

export fun generate(ctx: magus\Context, args: [str]) > void {
    go["go-run"](ctx, {"args": ["./cmd/gen-docs", "-out", "./docs"]});
}

go-test

./... is a default, not a fixed arg: a magusfile that passes args replaces it, so one package's tests need not compile every test binary in the module. A bare go.test() (and magus run <t> -- <extra> forwarding) still runs the whole tree.

Command: go test ./...

cd

Appends -covermode=atomic, appends -coverprofile=coverage.out.

JSON Patch
[
  {
    "op": "add",
    "path": "/-",
    "value": "-covermode=atomic"
  },
  {
    "op": "add",
    "path": "/-",
    "value": "-coverprofile=coverage.out"
  }
]

debug

Appends -v.

JSON Patch
[
  {
    "op": "add",
    "path": "/-",
    "value": "-v"
  }
]

Example

// go-test runs the suite; here with the race detector. Explicit args replace the
// op's `./...` default, so the scope rides along with the flags and `magus run test`
// forks `go test -race ./...`. The cd charm (`magus run test:cd`) adds the atomic
// coverage profile a CD pipeline ships.
import "magus";
import "magus/spell/go";

magus\project({ "spells": [go] });

export fun test(ctx: magus\Context, args: [str]) > void {
    go["go-test"](ctx, { "args": ["-race", "./..."] });
}

go-vet

Command: go vet ./...

Example

// go-vet is static analysis, so it composes into the canonical `lint` target
// (alongside golangci-lint) rather than a bespoke `vet` target. `magus run lint`
// forks `go vet ./...`.
import "magus";
import "magus/spell/go";

magus\project({ "spells": [go] });

export fun lint(ctx: magus\Context, args: [str]) > void {
    go["go-vet"](ctx);
}

golangci-lint

Invoked directly rather than through go tool: golangci-lint generates no code, so it has none of the generator/runtime lockstep that keeps protoc-gen-go pinned in go.mod. go tool golangci-lint also required the binary in the module's tool block, and a workspace that had not put it there got "no such tool" - the op could not run at all. On PATH it is pinned by whatever the workspace uses (mise, asdf, a system package), and the spell's version probe records which.

Command: golangci-lint run ./...

debug

Appends -v.

JSON Patch
[
  {
    "op": "add",
    "path": "/-",
    "value": "-v"
  }
]

rw

Inserts --fix.

JSON Patch
[
  {
    "op": "add",
    "path": "/1",
    "value": "--fix"
  }
]

Example

// golangci-lint runs as a `go tool` (resolved from go.mod's tool block). The rw
// charm inserts --fix, so `magus run lint:rw` applies the autofixable findings.
import "magus";
import "magus/spell/go";

magus\project({ "spells": [go] });

export fun lint(ctx: magus\Context, args: [str]) > void {
    go["golangci-lint"](ctx);
}

govulncheck

Invoked directly rather than through go tool, for the same reason as golangCILint above: go tool govulncheck requires the binary in the module's tool block, and a workspace that had not put it there got "no such tool" - so the op could not run at all. On PATH it is pinned by whatever the workspace uses, and the OBSERVE probe records which, alongside the database date, for the targets that compose this op. There is no version probe: see the tools table above for why one would key every target in the project.

Command: govulncheck ./...

Example

// govulncheck scans the module's call graph for known vulnerabilities, run as a
// `go tool` so it resolves from go.mod's tool block. Security scanning is static
// analysis, so it composes into the canonical `lint` target - not a bespoke
// `audit`/`security` target. (A slow scan can instead be gated in `ci`.)
import "magus";
import "magus/spell/go";

magus\project({ "spells": [go] });

export fun lint(ctx: magus\Context, args: [str]) > void {
    go["govulncheck"](ctx);
}

scip

magus injects MAGUS_SYMBOL_INDEX with the cache destination, so the index never lands in the tree; scip-go writes there via --output. The runner resolves the bare $MAGUS_SYMBOL_INDEX token against that destination, so no shell is needed to expand it.

Command: scip-go --output $MAGUS_SYMBOL_INDEX

generatedspells/golang/spell.buzzgospellgolangbuildtestlinttools
Last updated (63e73856)
Earlier changes on this page (7)

Full history ↗ · Blame source ↗

Glossary

Workspace

The magus root directory that owns a set of projects and shared config; the unit magus operates over. See workspace.

Project

A directory magus recognizes as a unit of work (it has a magusfile); the unit of caching, scheduling, and dependency tracking. See workspace.

Magusfile

The magusfile.buzz that declares a project's targets (as export funs) and binds its spells. See targets.

Target

A named operation (build, test, ...) you invoke with magus run <target>; it may compose a spell's tool-native operations and depend on other targets. See targets.

Op

A single tool-native command a target composes (long form: operation); the middle of the work hierarchy (Spell to Op to Target). See operations.

Spell

A language/runtime adapter (e.g. go, md) that maps generic targets onto a toolchain's real commands. See spells.

Charm

An execution modifier attached with : (lint:rw) that changes how a target runs, not which one; the built-in rw flips a check-only target to mutate in place, and ci always strips it. See charms.

Ward

A coded diagnostic that inspects a resolved op and nudges or blocks an anti-pattern before it runs. See wards.

Module

A magus stdlib namespace a magusfile imports for host capabilities: filesystem, exec, vcs, and more. See the module reference.

Buzz

The language magusfiles are written in (the .buzz engine). See engines.

Cache

The content-addressed store magus consults before running a target, so unchanged work is skipped. See cache.

CI

An ordinary magusfile-defined target you compose yourself with magus\needs - magus does not hardcode its stages. Magus.RunCI treats it specially only in that it strips the rw charm, it is the anchor magus affected ci keys off, and a selected scope with no project declaring it is a load error rather than a silent no-op. See targets.

Invocation

One magus process's recorded facts - the targets it finished, their outcomes, the lease it acted as, and the session it ran in when a host delivered one - kept in a repo-scoped store every worktree shares. magus session lists them; the store prunes itself by last-fact age.

Run

One target executing under one magus invocation, such as magus run test web or magus affected ci. A run keeps its captured output behind an output reference. Every magus run is a run whether or not any job asked for it; see Job for how the two relate.

Conventions

Placeholders

Angle brackets mark a value you replace with your own - never type the brackets:

magus run <target>
magus completion <shell>    # e.g. bash, zsh, fish

<target>, <path>, <shell>, <name> and the like are stand-ins, not literal text.