bake 0.18.0

Composable, typed development tasks for Rust projects
Documentation
# Structuring Bake Tasks in Crates

Define reusable automation as semantic Rust APIs and expose those operations as
Bake tasks with stable command names.

For Socketry projects, follow the
[crate and module conventions](https://github.com/socketry/socketry-project-rust/blob/main/context/conventions.md)
and [source layout](https://github.com/socketry/socketry-project-rust/blob/main/context/layout.md)
provided by `socketry-project`. This guide explains how those conventions apply
to Bake task libraries. See [Testing Task Libraries](testing-task-libraries.md)
for verification at the library, task, and executable boundaries.

Bake has two common places for task functions:

- A project's unpublished `bake/` binary crate holds tasks specific to that
  project. The `cargo-bake` launcher compiles and runs it.
- A normal library crate can export reusable tasks alongside its Rust API. Other
  Bake binaries discover those tasks when they depend on and link that library.

## Keep project tasks in a small binary crate

For project-specific automation, put the task functions in the task binary or
its child modules. The crate can stay unpublished and separate from the project's
released libraries:

```text
project/
├── Cargo.toml
├── src/
└── bake/
    ├── Cargo.toml
    └── src/
        ├── main.rs
        └── release.rs
```

Install the launcher once and bootstrap the task crate with:

```sh
cargo install socketry-cargo-bake --locked
cargo bake --regenerate
```

The command creates `bake/`, adds it to the workspace, and generates a minimal
binary. Later runs refresh its generated task-library links while preserving
the project's task source.

`main.rs` can declare modules and start discovery:

```rust,ignore
mod release;

fn main() -> bake::Result<()> {
    bake::Registry::discover()?.run()
}
```

Task functions in `release.rs` are ordinary Rust functions annotated with
`#[bake::task]`. Keep them next to the project automation they implement. Split
growing modules by domain, and preserve existing command names when moving
functions between modules.

## Start with a semantic Rust interface

Put reusable tasks in the library crate that owns the behavior they automate.
Give ordinary Rust callers an interface expressed in the library's domain:

- Use functions for operations such as `extract_notes(document, version)` or
  `update_document(document, version)`.
- Use objects when they own meaningful state, such as an `Installer` that holds
  discovered packages and supports `install_all` and `install_package`.
- Accept the paths, documents, options, and collaborators an operation needs.
  Keep project-root resolution and task invocation at the Bake boundary.
- Return domain values and errors that callers can inspect. Use descriptive
  result types when several related values form one result.

Standardize vocabulary, inputs, results, and failure semantics where libraries
perform the same kind of operation. For example, `list` enumerates available
items, `show` retrieves selected content, and `update` maintains an existing
resource. Use `plan` and `apply` when inspecting proposed changes separately
from applying them is useful. Add only the operations the domain needs.

Preserve domain-specific signatures. Add a shared trait when real consumers
need interchangeable implementations. Use options structs for coherent sets
of options and objects for meaningful state; small operations can remain
functions.

Follow Socketry's rule that the crate name supplies the root Rust namespace.
Re-export the main API from private implementation modules. Public modules
should identify a meaningful part of the domain. Do not add a public wrapper
such as `bake_releases::releases` solely to obtain a command prefix.

## Expose operations as Bake tasks

A task function adapts command arguments and execution context to the semantic
API. Keep substantial parsing, transformations, and filesystem or subprocess
operations in the implementation that ordinary Rust callers use. The adapter
can resolve paths, select options, obtain context state, invoke hooks, and
choose the task result.

When an operation already has a suitable task signature, annotate it directly.
Separate adapters are useful when the library accepts borrowed values, owns
state, or needs a different calling interface. Avoid forwarding layers that
add no meaning. Task functions remain callable as ordinary Rust functions;
their visibility should reflect the intended Rust API.

For example, this illustrative `bake_releases` library puts the task in
`lib.rs` and keeps file reading and document parsing in private modules:

```text
src/
├── lib.rs
├── document.rs
└── notes.rs
```

```rust,ignore
// src/lib.rs
use bake::{Context, Result};
use std::path::PathBuf;

mod document;
mod notes;

pub use document::extract_notes;
pub use notes::read_notes;

/// Extract the notes for an exact release heading.
#[bake::task]
pub fn notes(
    context: &mut Context,
    version: String,
    #[bake(
        default = "releases.md",
        help = "Release document relative to the project root."
    )]
    path: PathBuf,
) -> Result<String> {
    read_notes(&context.root().join(path), &version)
}
```

```rust,ignore
// src/notes.rs
use bake::{Error, Result};
use std::path::Path;

/// Read the notes for an exact release heading from a document file.
pub fn read_notes(path: &Path, version: &str) -> Result<String> {
    let document = std::fs::read_to_string(path)
        .map_err(|error| Error::new(format!("{}: {error}", path.display())))?;
    Ok(crate::extract_notes(&document, version)?.to_owned())
}
```

The parser in `document.rs` supplies
`extract_notes<'a>(document: &'a str, version: &str) -> Result<&'a str>`.
The example illustrates a proposed library layout; it does not describe the
current exports of `bake-releases`.
Rust callers can use the parser, call `read_notes` with an explicit path, or
call the Bake adapter. They do not need to construct a `Context` to read a file.

## Let the library define the default task namespace

Use domain namespaces and meaningful operations, such as `releases:notes`,
`license:update`, and `cargo:version:bump`. Existing task names and argument
contracts are compatibility boundaries.

For a library crate whose Rust name starts with `bake_`, `#[bake::task]`
derives a namespace by removing that prefix and replacing the remaining
underscores with colons. It then appends nested modules and the function name:

| Rust function | Default task name |
| --- | --- |
| `bake_releases::notes` | `releases:notes` |
| `bake_agent_context::install` | `agent:context:install` |
| `bake_cargo::version::bump` | `cargo:version:bump` |

The crate supplies the Rust namespace and the default command prefix. An
additional public `releases` module would repeat the domain already named by
`bake_releases`. Keep task placement close to the semantic operations it
exposes. Use the default attribute when the defining crate, modules, and
function already express the intended task name.

Underscores in module names retain their existing hyphen conversion; function
names remain unchanged. The namespace uses the defining Rust crate name, even
when a consumer renames its dependency. Re-exporting a function does not change
its defining module path. When existing modules already start with the complete
crate-derived namespace, that prefix appears only once:
`bake_releases::releases::notes` still registers as `releases:notes`.

Project binary targets keep their existing module-based naming, including
binaries whose names start with `bake_`. Cargo identifies those targets through
`CARGO_BIN_NAME`. Libraries without the `bake_` prefix also keep module-based
naming; their crate name does not become a command prefix.

Use an explicit `name` when a command intentionally differs from the default
or needs to remain stable across implementation moves. Explicit names retain
their existing behavior: a name containing `:` is used as-is, while a short
name receives only its defining module's namespace. Neither receives a new
crate-derived prefix. Thus a root function marked `#[bake::task(name = "test")]`
still exports `test` from `bake_test_rust`. A function marked
`#[bake::task(name = "releases:notes")]` keeps that command from any module.

The attribute generates argument conversion, a descriptor, and registration.
The library depends on `bake` to use these facilities. No additional registration
framework or common task-library trait is needed.

Reusable task libraries should declare `bake = "0"` so Cargo can resolve their
registry dependency to the Bake 0.x version selected by the consuming task
binary. Project-local task binaries can select a specific minor release, such
as `bake = "0.18"`.

Names are resolved during compilation from the defining `module_path!()`, the
attribute, and Cargo's binary-target metadata. The generated `notes_task()`
descriptor already contains `releases:notes`; registering it manually produces
the same name as `Registry::discover()`. Discovery collects descriptors,
validates their metadata, and detects collisions across linked crates.

## Keep task contracts predictable

- Use typed parameters. Required scalars are positional by default; defaults,
  `Option`, and `Vec` produce named options. Use `#[bake(named)]` for a required
  named option and document the purpose of arguments with `help`.
- Resolve project-relative paths against `context.root()`. Use
  `context.command(...)` for project subprocesses, or pass an explicit working
  directory to an operation acting on another checkout. Avoid changing the
  process-wide working directory or environment.
- Validate inputs before making changes. Include the affected file, package,
  or command in errors when that context helps identify the failure. Propagate
  failures from operations and hooks.
- Return the semantic result. Text is appropriate for release notes; a list
  or serializable result struct suits package metadata or an update summary.
  Use `()` when there is no useful result. Avoid making callers parse a success
  message to recover counts, paths, or other structured data. Existing output
  shapes also need compatibility consideration when migrating a library.
- Let Bake's `output` task format the final result. Send progress and diagnostics
  to stderr. Use `#[bake::task(output)]` only when the task owns its final output;
  subprocess progress alone does not require it.

Use ordinary Rust calls to compose implementation operations. Use
`context.call(...)` where composition is intentionally through registered
tasks, including project-defined hooks. Use `call_if_registered` for optional
hooks and document when they run and which arguments they receive. Libraries
can cache project-scoped discovery in `Context` when several tasks need it;
the underlying API should also accept the discovered state directly.

## Consume task libraries

Add a reusable task library as a dependency of the local task binary, then run
`cargo bake --regenerate` so its registration entries are linked:

```toml
[dependencies]
bake = "0.18"
socketry_executor = { package = "socketry-executor", version = "0.1" }
bake_agent_context = "0.1"
```

The first `cargo bake --regenerate` creates the private `bake/` workspace member
and its minimal binary if they do not exist. Each run regenerates a small source
file that links unconditional, non-optional, platform-independent direct
dependencies in `bake/Cargo.toml` (apart from `bake` itself), and adds a module
declaration to the selected binary if needed. It preserves the rest of the task
source. Put reusable task libraries in `[dependencies]`; ordinary dependencies
used by task code are also linked.

No per-task imports or registration calls are needed. The task attributes and
defining modules determine command names. `Registry::discover()` reports an
error if two linked crates register the same task name. A crate used only as a
normal Rust dependency does not need to expose a Bake executable; Cargo does
not run a dependency's binary target when building your application.

The `bake-agent-context` task library provides dependency context discovery and
installation in the same executable. Run `cargo bake agent:context:install`
to install guidance from resolved dependencies. Follow that package's context
for the generated index, skills, and repository-owned agent instructions.

If ordinary users of a library should not inherit its Bake dependency, put the
tasks in a separate companion crate, such as `socketry-executor-bake`. The
project's task binary then depends on and references that companion crate instead.
This keeps the main library's dependencies smaller, at the cost of one extra
package dependency for projects that want its tasks.

## Use shared project composition

In Socketry repositories, depend on `socketry-project` in the private `bake/`
package to obtain the standard development tasks and release hook. Its
`cargo:after_version_bump` hook composes license, release-note, and readme
updates. Keep this composition in the shared project crate, and keep the local
executable focused on project-specific additions. A reusable task library
should not depend on `socketry-project` merely to obtain its own development
tooling.

During development of a task library, ensure the private executable resolves
that library to the current checkout, including when it arrives transitively
through `socketry-project`. For example, the root workspace manifest of
`bake-releases` can contain:

```toml
[patch.crates-io]
bake-releases = { path = "." }
```

The local package version must satisfy the dependency requirements. Inspect
Cargo's resolved dependency graph; adding a direct path dependency alone can
leave a second registry copy linked through another dependency. Refresh the
lockfile when resolution changes, and regenerate task links when dependencies
of the private executable change.

## Align an existing library

Start by identifying the public Rust operations, registered command names,
arguments, results, and hook behavior. Preserve those contracts while moving
implementation into semantic modules and adding root re-exports. Keep existing
Rust import paths as compatibility re-exports when needed, and document any
intentional breaking API or output change.

Review unnamed tasks in `bake_*` libraries when adopting crate-derived
namespaces. Tasks at the crate root gain a prefix, and tasks under a different
domain gain the crate prefix as well. For example, the default name for
`bake_cargo::releases::github::release` is `cargo:releases:github:release`.
Bake Cargo 0.2.9 exposes this command and keeps `releases:github:release` as a
temporary, explicitly named compatibility alias while callers migrate.
Use explicit names to preserve existing command contracts where needed.
Existing module paths that already start with the crate's domain are unchanged.

Generated descriptors now carry their complete names before registration,
including module namespaces for libraries without a `bake_` prefix and for
explicit short names. Review manual registries that previously added those
namespaces using `Registry::include`: it still prepends the supplied namespace,
so importing an already namespaced descriptor can repeat that prefix. Register
the descriptor directly when its inferred name is the intended command.

Then align the task adapters, local dependency resolution, and tests with this
guide. Reuse Socketry's layout and testing guidance, and record domain-specific
exceptions in the library's own context. Apply the conventions as each library
is reviewed; the illustrative APIs above do not imply that all current task
libraries have already adopted them.