magus v0.3.0 is out. See what's new
¶ View markdown source · ✎ Suggest an edit
5 min read

Recommendations

magus enforces one target name (ci) and reserves three charms (rw, cd, gha). Everything else about your layout is yours. That leaves real questions unanswered: what to call the charm that publishes, whether two charms may be combined, where a workspace's own error codes come from.

This page answers them the way magus's own workspace does. None of it is checked by the tool. Where a recommendation here contradicts something magus actually enforces, the enforcement wins and this page is the bug.

Most of it is about charms, where magus supplies a mechanism and no vocabulary at all. For target names, see the canonical seven and the four tests a new one has to pass; what this page adds about them is at the end, including the one case where it declines to recommend anything.

A charm is an adverb

A target is the verb. A charm changes how it runs, never what runs: magus run build:static is still build. If you find yourself wanting magus run publish, the target is build and the charm is cd.

So charm names read as modifiers, not actions. rw is "read-write", not "write". static is "the static one", not "build static".

One charm answers one question

Charms are an unordered set, and magus gives none precedence over another. A magusfile that reads two charms in sequence therefore invents a precedence the engine does not have, and the loser is discarded silently:

// Wrong: :amd64,arm64 returns amd64 and drops the arm64 the caller asked for.
if (ctx.has_charm("amd64")) { return "linux/amd64"; }
if (ctx.has_charm("arm64")) { return "linux/arm64"; }

Two charms that answer one question are a mistake worth reporting, not resolving. Group them on an axis and reject the pair:

axis charms question
deliver cd publish the artifact, or only build it?
channel stable, unstable which stream does this land in?
platform amd64, arm64 build for which architecture?
variant static which build of the same artifact?

An axis with one charm is binary and needs no guard. An axis with two needs one.

Drawn as axes rather than a list of charms, magus's own image-build reads like this:

flowchart LR
    R["magus run image-build"] --> D{"cd?"}
    D -- "no" --> L["load locally, push nothing"]
    D -- "yes" --> C{"channel"}
    C -- "(none)" --> K["commit: ghcr, tagged by hash"]
    C -- "unstable" --> U["prerelease: both registries, no floating tag"]
    C -- "stable" --> S["release: both registries, version + latest"]
Diagram source - renders with JavaScript enabled.

Every leaf is reachable, and no two charms lead to the same one. That is the shape to aim for: if two combinations land on one leaf, one of the charms is not earning its place.

Name a charm for the value, not the ceremony

stable and unstable name a channel, the same vocabulary Rust (stable/beta/nightly), Debian (stable/testing/unstable) and npm dist-tags already use. release would read well too, but it is a target in this workspace, and magus doctor fails a name that is both a charm and a target because target:charm then reads ambiguously.

Avoid snapshot. It means opposite things in the two tools that popularized it - GoReleaser's --snapshot builds and publishes nothing, while Maven's -SNAPSHOT publishes to a different repository. A reader cannot know which you meant.

A charm that makes a claim should check it

stable is not a label the author gets to assert. It tells a consumer following latest that this artifact is for them, so the artifact has to qualify. The check is one semver field. Press Run:

import "std";
import "semver";

// The channel a version qualifies for. `git describe` renders an untagged commit as
// v0.4.0-3-gabc123, whose prerelease component is 3-gabc123 - so one field answers both
// "is this a prerelease" and "is this even a tagged build", and stable rejects both.
fun channelFor(v: str) > str {
    if (semver\parse(v).prerelease != "") { return "unstable"; }
    return "stable";
}

std\print("v0.4.0            -> " + channelFor("v0.4.0"));
std\print("v0.5.0-rc.1       -> " + channelFor("v0.5.0-rc.1"));
std\print("v0.4.0-3-gabc123  -> " + channelFor("v0.4.0-3-gabc123"));

In a magusfile the mismatch raises rather than returns, so magus run image-build:cd,stable on an untagged commit fails before it pushes anything.

Check the claim or drop the charm. An unchecked one is a comment that happens to run.

Raise your own diagnostics

A magusfile can throw a string, which leaves every caller substring-matching prose. Prefer a code, which is stable and branchable:

magus\raise("WS1001", message: "the `amd64` and `arm64` charms both answer which platform to build for");
catch (e) {
    final d: magus\Diagnostic = e;
    if (d.code == "WS1001") { ... }
}

Pick a prefix for your workspace and keep it. MGS is refused: it is a closed catalog that magus explain, the knowledge graph and the docs URL map all resolve against, so a workspace code that rendered like one would document nothing. Pass cause: when you are wrapping a failure so the original stays readable and reachable, the way Go's %w does.

Where this page stops

The canonical target names are worth adopting: build, test, lint, format, generate, preflight and clean mean the same thing in every toolchain, which is the test they had to pass to get in. ci you get either way, since magus reserves it.

Releasing is different, and this page does not recommend a shape for it. magus's own workspace splits it into a release target that picks versions and cuts tags, plus release-build and release-sign - and that split exists because this repository publishes several independently versioned Go modules from one tree. Yours may cut one tag, or none, or hand the whole job to a service. Everyone's release process differs enough that a recommendation would be someone else's constraint, so what is written above is a description of what works here, not advice.

The same caution applies to deploy and serve. They are real phases and they stay custom on purpose, for the reason targets gives: which environment, which registry, which port is workspace-specific enough that one shape would be more prescriptive than useful.

Read the real thing

The most useful example is the workspace that generated this page. magus builds itself, so its magusfile is a working reference rather than a sample:

  • magusfile.buzz at the root: image_build and channel() are the two-axis model above, publish_registries is the single choke point three targets share, and exclusive_charms is the guard.
  • console/magusfile.buzz: a smaller one, and a second language, if the root is too much at once.

See also

  • Charms: the mechanism, and what a charm can patch.
  • Targets: the canonical names and the four tests a new one has to pass.
  • Debugging: --step walks a target one command at a time, which is how you find out what a charm actually did to the argv.
  • Tips: stepping through a volatile build, and the auth realm, which is the registry-vocabulary problem the channel charms sit next to.
  • GitHub Actions: the same "one question, one answer" idea applied to workflows.
recommendationsconventionscharmstargetsnamingchannelsdiagnosticsbest practice
Last updated (a103255f)
Glossary

Workspace

The magus root directory that owns a set of projects and shared config; the unit magus operates over. 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.

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.

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.

Engine

The interpreter a magusfile runs on; magus embeds the Buzz engine. See engines.

Service

A long-running or shared process magus manages across runs, distinct from a one-shot target. See services.

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.

Snapshot

A point-in-time view of live state - the pool's occupancy or a tick of exported metrics - as opposed to accumulated history. See daemon.

Knowledge graph

The queryable graph of a workspace's spells, targets, docs, and code relationships; query it with magus query/explain/path. See knowledge.

Conventions

This page uses none of the site's convention markers. The full set is on the conventions page.