<div align="center">
<img src="https://raw.githubusercontent.com/tracel-ai/xtask/main/assets/xtask.png" width="256px"/>
<h1>Tracel Xtask</h1>
[](https://discord.gg/uPEBbYYDB6)
[](https://crates.io/crates/tracel-xtask)
[](https://crates.io/crates/tracel-xtask)
[](https://github.com/tracel-ai/xtask/actions/workflows/ci.yml)

---
<br/>
</div>
A collection of easy-to-use and extensible commands to be used in your [xtask CLI][1] based on [clap][2].
We rely on these commands in each of our Tracel repositories. By centralizing our redundant commands we save a big amount
of code duplication, boilerplate and considerably lower their maintenance cost. This also provides a unified interface across
all of our repositories.
These commands are not specific to Tracel repositories and they should be pretty much usable in any Rust repositories with
a cargo workspace as well as other repositories where Rust is not necessarily the only language. The commands can be easily
extended using handy proc macros and by following some patterns described in this README.
## Getting Started
### Install xtask CLI
```bash
cargo install tracel-xtask-cli
```
### Setting Up a Cargo Workspace with an xtask binary crate
#### Using the CLI
**Not available yet**
#### Manually
1. **Create a new Cargo workspace:**
```bash
cargo new my_workspace
cd my_workspace
```
2. **Create the `xtask` binary crate:**
```bash
cargo new xtask --bin
```
3. **Configure the workspace:**
Edit the `Cargo.toml` file in the root of the workspace to include the following:
```toml
[workspace]
members = ["xtask"]
```
4. **Add the `tracel-xtask` dependency:**
In the `xtask/Cargo.toml` file, add the following under `[dependencies]`:
```toml
[dependencies]
tracel-xtask = "3.0"
```
5. **Build the workspace:**
```bash
cargo build
```
Your workspace is now set up with a `xtask` binary crate that depends on `tracel-xtask` version 3.0.x.
### Bootstrap main.rs
1. In the `main.rs` file of the newly created `xtask` crate, import the `tracel_xtask` prelude module and then declare
a `Command` enum. Select the base commands you want to use by adding the `macros::base_commands` attribute:
```rust
use tracel_xtask::prelude::*;
#[macros::base_commands(
Build,
Check,
Fix,
Test,
)]
pub enum Command {}
```
2. Update the `main` function to initialize xtask and dispatch the base commands:
```rust
fn main() -> anyhow::Result<()> {
let (args, environment) = init_xtask::<Command>(parse_args::<Command>()?)?;
match args.command {
// dispatch_base_commands function is generated by the commands macro
_ => dispatch_base_commands(args, environment),
}
}
```
3. Build the workspace with `cargo build` at the root of the repository to verify that everything compiles.
4. You should now be able to display the main help screen which lists the commands you selected previously:
```sh
xtask --help
```
## Conventions
### Repository structure
All our repositories follow the same directory hierarchy:
- a `crates` directory which contains all the crates of the workspace,
- an `examples` directory which holds all the examples crates,
- a `xtask` directory which contains the repository-specific xtask CLI using the base commands.
### About tests
As per Cargo convention, [Integration tests][3] are tests contained in a `tests` directory of a crate besides its `src` directory.
Inline tests in `src` directory are called [Unit tests][4].
`tracel-xtask` allows to easily execute them separately using the `test` command.
## Interface generalities
### Target
There are 4 default targets provided by `tracel-xtask`:
- `workspace` which targets the cargo workspace, this is the default target,
- `crates` are all the binary crates and library crates,
- `examples` are all the example crates,
- `all-packages` are both `crates` and `examples` targets.
`workspace` and `all-packages` are different because `workspace` uses the `--workspace` flag of cargo whereas `all-packages`
relies on `crates` and `examples` targets which use the `--package` flag. So `all-packages` executes a command for each crate
or example individually.
Here are some examples:
```sh
# run all the crates tests
xtask test --target crates all
# check format for examples, binaries and libs
xtask check --target all-packages unit
# build the workspace
xtask build --target workspace
# workspace is the default target so this has the same effect
xtask build
```
### Global options
The following options are global and precede the actual command on the command line.
#### Environment
`-e`, `--environment`
```sh
xtask -e production build
```
This `--environment` parameter is passed to all the `handle_command` functions as `env`.
Its main role is to inform your custom commands or dispatch functions about the targeted environment. An environment
has 3 different display names: a short one, a medium one and a long one.
The available environments are listed below:
| Development | d | dev | development |
| Test | t | test | test |
| Staging | s | stag | staging |
| Production | p | prod | production |
It also automatically loads the following environment variables files if they exist in the working directory:
- `.env` for any set environment,
- `.env.secrets` containing secrets for any set environment,
- `.env.{environment}` (example: `.env.dev`) for the non-sensitive configuration,
- `.env.{environment}.secrets` (example `.env.dev.secrets`) for the sensitive configuration like passwords. These
files must be added to the ignore file of your VCS tool. For git, add `.env.*.secrets` to the `.gitignore` file
at the root of your repository.
The `.secrets` component in `.env.secrets` is called a `dotenv family`. There exist two additional `dotenv families`:
- `.infra` for infrastructure related variables, which allows application config and infra config to be split cleanly,
- `.infra.secrets` for infrastructure secrets.
For each family both `.env.{family}` and `.env.{environment}.{family}` are sourced if they exist.
**Note:** If an environment variable is already defined in the process environment, it takes precedence over the
value defined in `.env` files.
At last, the display form used in dotenv filenames for the environment is the `medium` one (see table above).
#### Environment Index
By default the index is transparent because it is set to 1 and the first index does not appear in the environment name.
So for instance the `stag` environment is actually the `stag1` environment but it is always printed `stag`.
Any provided index greater than 1 is always explicit and always appears in the environment name.
To turn an environment into an explicit environment you can call the function `into_explicit` on the environment
which returns a new environment. This new environment always prints the index so instead of printing `s`, `stag`
or `staging`, it will print `s1`, `stag1` and `staging1` explicitly.
#### Context
`-c`, `--context`
```sh
xtask --context no-std build
```
This argument has no effect in the base commands. Its purpose is to provide additional context to your custom commands
or dispatch functions — for example, to indicate whether the current build context is `std` or `no-std`.
This parameter is passed to all the `handle_command` functions as `context`.
#### Coverage
`--enable-coverage`
It sets up the Rust toolchain to generate coverage information.
## Standard repository vs. Monorepo
`xtask` CLI is capable of handling traditional repositories with a single Rust workspace at the root of the repository and
monorepos which are a collection of Cargo workspaces in their own directories called `subrepos`.
A `subrepo` is basically the same as a standard repository with its own `Cargo.toml` and `xtask` crate. For instance this
simple monorepo contains the following files and directories at the root:
```text
.git
.gitignore
Dependencies.toml
[backend]
|__Cargo.toml
|__[crates]
|__[xtask]
[frontend]
|__Cargo.toml
|__[xtask]
```
`backend` and `frontend` are subrepos with separate Cargo workspaces with their own members. They all have an `xtask` member
to manage them.
The root `Dependencies.toml` does not represent a real Cargo workspace, its only purpose is to centralize all the dependency
versions and features. `xtask` CLI will push the dependency versions in `Dependencies.toml` to the subrepos `Cargo.toml`
files whenever a dependency is used in the subrepo.
For instance, say we have this `Dependencies.toml` file:
```toml
[workspace.dependencies]
log = "0.4.29"
rand = "0.9.2"
tokio = { version = "1.48.0", features = ["full"] }
```
and `backend` subrepo `Cargo.toml` is:
```toml
[workspace]
resolver = "2"
members = ["crates/*", "xtask"]
[workspace.dependencies]
rand = "0.8.0"
tokio = "1.46.0"
```
Whenever the `xtask` CLI executes a command it will enforce the dependency versions to be in sync with the `Dependencies.toml`,
which means that the `backend` subrepo `Cargo.toml` will be modified to be:
```toml
[workspace]
resolver = "2"
members = ["crates/*", "xtask"]
[workspace.dependencies]
rand = "0.9.2"
tokio = { version = "1.48.0", features = ["full"] }
```
Note that it is possible to drop `features = ["full"]` from the `Dependencies.toml` file and make the decision about which
feature to use at the subrepo or even crate level.
## Anatomy of a base command
We use the derive API of clap which is based on structs, enums and attribute proc macros. Each base command is a
submodule of the `base_commands` module. If the command accepts arguments there is a corresponding struct named `<command>CmdArgs`
which declares the options, arguments and subcommands. In the case of subcommands a corresponding enum named `<command>SubCommand`
is defined.
Here is an example with a `foo` command:
```rust
#[macros::declare_command_args(Target, FooSubCommand)]
struct FooCmdArgs {}
pub enum FooSubCommand {
/// A sub command for foo (usage on the command line: xtask foo print-something)
PrintSomething,
}
```
Note that it is possible to have an arbitrary level of nested subcommands but deeper nested subcommands cannot be extended,
in other words, only the first level of subcommands can be extended. If possible, try to design commands with only one
level of subcommands to keep the interface simple.
In the following sections we will see how to create completely new commands as well as how to extend existing base commands.
## Customization
The crate `xtask-tests` contains examples of the following sections.
### Create a new command
1. First, organize commands by creating a `commands` module. Create a file `xtask/src/commands/my_command.rs` as well
as the corresponding `mod.rs` file to declare the module contents.
2. Then, in `my_command.rs` define the arguments struct with the `declare_command_args` macro and define the `handle_command`
function. The `declare_command_args` macro takes two parameters, the first is the type of the target enum and the second
is the type of the subcommand enum if any. If the command has no target or no subcommand then put `None` for each argument
respectively. `Target` is the default target type provided by `tracel-xtask`. This type can be extended to support more
targets as we will see in a later section.
```rust
use tracel_xtask::prelude::*;
#[macros::declare_command_args(Target, None)]
struct MyCommandCmdArgs {}
pub fn handle_command(_args: MyCommandCmdArgs, _env: Environment, _ctx: Context) -> anyhow::Result<()> {
println!("Hello from my-command");
Ok(())
}
```
3. Make sure to update the `mod.rs` file to declare the command module:
```rust
pub(crate) mod my_command;
```
4. We can now add a new variant to the `Command` enum in `main.rs`:
```rust
mod commands;
use tracel_xtask::prelude::*;
#[macros::base_commands(
Bump,
Check,
Fix,
Test,
)]
pub enum Command {
MyCommand(commands::my_command::MyCommandCmdArgs),
}
```
5. And dispatch its handling to our new command module:
```rust
fn main() -> anyhow::Result<()> {
let (args, environment) = init_xtask::<Command>(parse_args::<Command>()?)?;
match args.command {
Command::MyCommand(cmd_args) => commands::my_command::handle_command(cmd_args, environment, args.context),
_ => dispatch_base_commands(args, environment),
}
}
```
6. You can now test your new command with:
```sh
xtask my-command --help
xtask my-command
```
### Extend the default Target enum
Let's implement a new command called `extended-target` to illustrate how to extend the default `Target` enum.
1. Create a `commands/extended_target.rs` file and update the `mod.rs` file as we saw in the previous section.
2. We also need to add a new `strum` dependency to our `Cargo.toml` file:
```toml
[dependencies]
strum = { version = "0.26.3", features = ["derive"] }
```
3. Then we can extend the `Target` enum with the `macros::extend_targets` attribute in our `extended_target.rs` file.
Here we choose to add a new target called `frontend` which targets the frontend component we could find for instance
in a monorepo:
```rust
use tracel_xtask::prelude::*;
#[macros::extend_targets]
pub enum MyTarget {
/// Target the frontend component of the monorepo.
Frontend,
}
```
4. Then we define our command arguments by referencing our newly created `MyTarget` enum in the `declare_command_args` attribute:
```rust
#[macros::declare_command_args(MyTarget, None)]
struct ExtendedTargetCmdArgs {}
```
5. Our new target is then available for use in the `handle_command` function:
```rust
pub fn handle_command(args: ExtendedTargetCmdArgs, _env: Environment, _ctx: Context) -> anyhow::Result<()> {
match args.target {
// Default targets
MyTarget::AllPackages => println!("You chose the target: all-packages"),
MyTarget::Crates => println!("You chose the target: crates"),
MyTarget::Examples => println!("You chose the target: examples"),
MyTarget::Workspace => println!("You chose the target: workspace"),
// Additional target
MyTarget::Frontend => println!("You chose the target: frontend"),
};
Ok(())
}
```
6. Register our new command the usual way by adding it to our `Command` enum and dispatch it
in the `main` function:
```rust
mod commands;
use tracel_xtask::prelude::*;
#[macros::base_commands(
Bump,
Check,
Fix,
Test,
)]
pub enum Command {
ExtendedTarget(commands::extended_target::ExtendedTargetCmdArgs),
}
fn main() -> anyhow::Result<()> {
let (args, environment) = init_xtask::<Command>(parse_args::<Command>()?)?;
match args.command {
Command::ExtendedTarget(cmd_args) => commands::extended_target::handle_command(cmd_args, environment, args.context),
_ => dispatch_base_commands(args, environment),
}
}
```
7. Test the command with:
```sh
xtask extended-target --help
xtask extended-target --target frontend
```
### Target aliases
When extending a command we can mark a new variant as an _alias_ of a base target.
This example defines `Backend` target as the same as `Workspace` target for all the base commands:
```rust
use tracel_xtask::prelude::*;
#[macros::extend_targets]
pub enum MyTarget {
/// Target the backend component of the monorepo, same thing as Workspace.
#[alias(Workspace)]
Backend,
/// Target the frontend component of the monorepo.
Frontend,
}
```
### Extend a base command
To extend an existing command we use the `macros::extend_command_args` attribute which takes three parameters:
- first argument is the type of the base command arguments struct to extend,
- second argument is the target type (or `None` if there is no target),
- third argument is the subcommand type (or `None` if there is no subcommand).
Let's use two examples to illustrate this, the first is a command to extend the `build` base command with
a new `--debug` argument; and the second is a new command to extend the subcommands of the `check` base command
to add a new `my-check` subcommand.
Note that you can find more examples in the `xtask` crate of this repository.
#### Extend the arguments of a base command
We create a new command called `extended-build-args` which will have an additional argument called `--debug`.
1. Create the `commands/extended_build_args.rs` file and update the `mod.rs` file as we saw in the previous section.
2. Extend the `BuildCmdArgs` struct using the attribute `macros::extend_command_args` and define the `handle_command` function.
Note that the macro automatically implements the `TryInto` trait which makes it easy to dispatch back to the base command
own `handle_command` function. Also note that if the base command requires a target then you need to provide a target as well
in your extension, i.e. the target parameter of the macro cannot be `None` if the base command has a `Target`.
```rust
use tracel_xtask::prelude::*;
#[macros::extend_command_args(BuildCmdArgs, Target, None)]
pub struct ExtendedBuildArgsCmdArgs {
/// Print additional debug info when set.
#[arg(short, long)]
pub debug: bool,
}
pub fn handle_command(args: ExtendedBuildArgsCmdArgs, env: Environment, ctx: Context) -> anyhow::Result<()> {
if args.debug {
println!("Debug is enabled");
}
base_commands::build::handle_command(args.try_into().unwrap(), env, ctx)
}
```
3. Register the new command the usual way by adding it to the `Command` enum and dispatch it
in the `main` function:
```rust
mod commands;
use tracel_xtask::prelude::*;
#[macros::base_commands(
Bump,
Check,
Fix,
Test,
)]
pub enum Command {
ExtendedBuildArgs(commands::extended_build_args::ExtendedBuildArgsCmdArgs),
}
fn main() -> anyhow::Result<()> {
let (args, environment) = init_xtask::<Command>(parse_args::<Command>()?)?;
match args.command {
Command::ExtendedBuildArgs(cmd_args) => commands::extended_build_args::handle_command(cmd_args, environment, args.context),
_ => dispatch_base_commands(args, environment),
}
}
```
4. Test the command with:
```sh
xtask extended-build-args --help
xtask extended-build-args --debug
```
#### Extend the subcommands of a base command
For this one we create a new command called `extended-check-subcommands` which will have an additional subcommand.
1. Create a `commands/extended_check_subcommands.rs` file and update the `mod.rs` file as we saw in the previous section.
2. Extend the `CheckCmdArgs` struct using the attribute `macros::extend_command_args`:
```rust
use tracel_xtask::prelude::*;
#[macros::extend_command_args(CheckCmdArgs, Target, ExtendedCheckSubcommand)]
pub struct ExtendedCheckedArgsCmdArgs {}
```
3. Implement the `ExtendedCheckSubcommand` enum by extending the `CheckSubCommand` base enum with the macro `extend_subcommands`.
It takes the name of the type of the subcommand enum to extend:
```rust
#[macros::extend_subcommands(CheckSubCommand)]
pub enum ExtendedCheckSubcommand {
/// An additional subcommand for our extended check command.
MySubcommand,
}
```
4. Implement the `handle_command` function to handle the new subcommand. Note that we must handle the `All` subcommand as well:
```rust
use strum::IntoEnumIterator;
pub fn handle_command(args: ExtendedCheckedArgsCmdArgs, env: Environment, ctx: Context) -> anyhow::Result<()> {
match args.get_command() {
ExtendedCheckSubcommand::MySubcommand => run_my_subcommand(args.clone()),
ExtendedCheckSubcommand::All => {
ExtendedCheckSubcommand::iter()
.filter(|c| *c != ExtendedCheckSubcommand::All)
.try_for_each(|c| {
handle_command(
ExtendedCheckedArgsCmdArgs {
command: Some(c),
target: args.target.clone(),
exclude: args.exclude.clone(),
only: args.only.clone(),
},
env.clone(),
ctx.clone(),
)
})
}
_ => base_commands::check::handle_command(args.try_into().unwrap(), env, ctx),
}
}
fn run_my_subcommand(_args: ExtendedCheckedArgsCmdArgs) -> Result<(), anyhow::Error> {
println!("Executing new subcommand");
Ok(())
}
```
5. Register the new command the usual way by adding it to the `Command` enum and dispatch it
in the `main` function:
```rust
mod commands;
use tracel_xtask::prelude::*;
#[macros::base_commands(
Bump,
Check,
Fix,
Test,
)]
pub enum Command {
ExtendedCheckSubcommand(commands::extended_check_subcommands::ExtendedCheckedArgsCmdArgs),
}
fn main() -> anyhow::Result<()> {
let (args, environment) = init_xtask::<Command>(parse_args::<Command>()?)?;
match args.command {
Command::ExtendedCheckSubcommand(cmd_args) => commands::extended_check_subcommands::handle_command(cmd_args, environment, args.context),
_ => dispatch_base_commands(args, environment),
}
}
```
6. Test the command with:
```sh
xtask extended-check-subcommands --help
xtask extended-check-subcommands my-check
```
## Custom builds and tests
`tracel-xtask` provides helper functions to easily execute custom builds or tests with specific features or build targets.
Do not confuse Rust build targets, which are an argument of the `cargo build` command, with the xtask target introduced previously.
For instance we can extend the `build` command to build additional crates with custom features or build targets using the helper function:
```rust
pub fn handle_command(args: tracel_xtask::commands::build::BuildCmdArgs, env: Environment, ctx: Context) -> anyhow::Result<()> {
// regular execution of the build command
tracel_xtask::commands::build::handle_command(args, env, ctx)?;
// additional crate builds
tracel_xtask::utils::helpers::custom_crates_build(vec!["my-crate"], vec!["--all-features"], None, None, "all features")?;
tracel_xtask::utils::helpers::custom_crates_build(vec!["my-crate"], vec!["--features", "myfeature1,myfeature2"], None, None, "myfeature1,myfeature2")?;
tracel_xtask::utils::helpers::custom_crates_build(vec!["my-crate"], vec!["--target", "thumbv7m-none-eabi"], None, None, "thumbv7m-none-eabi target")?;
Ok(())
}
```
## Enable and generate coverage information
Here is an example GitHub job which shows how to setup coverage, enable it and upload coverage information to codecov:
```yaml
env:
GRCOV_LINK: "https://github.com/mozilla/grcov/releases/download"
GRCOV_VERSION: "0.8.19"
jobs:
my-job:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: install rust
uses: dtolnay/rust-toolchain@master
with:
components: rustfmt, clippy
toolchain: stable
- name: Install grcov
shell: bash
run: |
curl -L "$GRCOV_LINK/v$GRCOV_VERSION/grcov-x86_64-unknown-linux-musl.tar.bz2" |
tar xj -C $HOME/.cargo/bin
xtask coverage install
- name: Build
shell: bash
run: xtask build
- name: Tests
shell: bash
run: xtask --enable-coverage test all
- name: Generate lcov.info
shell: bash
# /* is to exclude std library code coverage from analysis
run: xtask coverage generate --ignore "/*,xtask/*,examples/*"
- name: Codecov upload lcov.info
uses: codecov/codecov-action@v4
with:
files: lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
```
## Special command 'validate'
By convention this command is responsible to run all the checks, builds, and/or tests that validate the code
before opening a pull request or merge request.
The command `Validate` can been added via the macro `tracel_xtask_macros::commands` like the other commands.
By default all the checks from the `check` command are run as well as both unit and integration tests from
the `test` command.
You can make your own `handle_command` function if you need to perform more validations. Ideally this function
should only call the other commands `handle_command` functions.
For quick reference here is a simple example to perform all checks and tests against the workspace:
```rust
pub fn handle_command(args: ValidateCmdArgs, env: Environment, ctx: Context) -> anyhow::Result<()> {
let target = Target::Workspace;
let exclude = vec![];
let only = vec![];
[
CheckSubCommand::Audit,
CheckSubCommand::Format,
CheckSubCommand::Lint,
CheckSubCommand::Typos,
]
.iter()
.try_for_each(|c| {
super::check::handle_command(
CheckCmdArgs {
target: target.clone(),
exclude: exclude.clone(),
only: only.clone(),
command: Some(c.clone()),
ignore_audit: args.ignore_audit,
},
env.clone(),
ctx.clone(),
)
})?;
super::test::handle_command(
TestCmdArgs {
target: target.clone(),
exclude: exclude.clone(),
only: only.clone(),
threads: None,
jobs: None,
command: Some(TestSubCommand::All),
},
env,
ctx,
)?;
Ok(())
}
```
## Base commands list
### Check and Fix
The `check` and `fix` commands are designed to help you maintain code quality during development.
They run various checks and fix issues, ensuring that your code is clean and follows best practices.
`check` and `fix` contain the same subcommands to audit, format, lint or proofread a code base.
While the `check` command only reports issues, the `fix` command attempts to fix them as they are encountered.
Each check can be executed separately or all of them can be executed sequentially using `all`.
Usage to lint the code base:
```sh
xtask check lint
xtask fix lint
xtask fix all
```
### Running Tests
Testing is a crucial part of development, and the `test` command is designed to make this process easy.
This command makes the distinction between unit tests and integration tests. [Unit tests][4] are inline tests under the
`src` directory of a crate. [Integration tests][3] are tests defined in files under the `tests` directory of a crate besides
the `src` directory.
Usage:
```sh
# execute workspace unit tests
xtask test unit
# execute workspace integration tests
xtask test integration
# execute workspace both unit tests and integration tests
xtask test all
```
Note that documentation tests are supported by the `doc` command.
### Documentation
Command to build and test the documentation in a workspace.
### Bumping Versions
This is a command reserved for repository maintainers.
The `bump` command is used to update the version numbers of all first-party crates in the repository.
This is particularly useful when you're preparing for a new release and need to ensure that all crates have the correct version.
You can bump the version by major, minor, or patch levels, depending on the changes made.
For example, if you’ve made breaking changes, you should bump the major version.
For new features that are backwards compatible, bump the minor version.
For bug fixes, bump the patch version.
Usage:
```sh
xtask bump <SUBCOMMAND>
```
### Publishing Crates
This is a command reserved for repository maintainers and is typically used in `publish` GitHub workflows.
This command automates the process of publishing crates to `crates.io`, the Rust package registry.
By specifying the name of the crate, `xtask` handles the publication process, ensuring that the crate is available for others to use.
Usage:
```sh
xtask publish <NAME>
```
As mentioned, this command is often used in a GitHub workflow.
We provide Tracel's reusable [publish-crate][8] workflow that makes use of this command.
Here is a simple example with a workflow that publishes two crates A and B with A depending on B.
```yaml
name: publish all crates
on:
push:
tags:
- "v*"
jobs:
publish-B:
uses: tracel-ai/github-actions/.github/workflows/publish-crate.yml@v6
with:
crate: B
secrets:
CRATES_IO_API_TOKEN: ${{ secrets.CRATES_IO_API_TOKEN }}
publish-A:
uses: tracel-ai/github-actions/.github/workflows/publish-crate.yml@v6
with:
crate: A
needs:
- publish-B
secrets:
CRATES_IO_API_TOKEN: ${{ secrets.CRATES_IO_API_TOKEN }}
```
### Coverage
This command provides a subcommand to install the necessary dependencies for performing code coverage and a subcommand to generate the
coverage info file that can then be uploaded to a service provider like codecov. See dedicated section `Enable and generate coverage information`.
### Containers
The container interface is shared across the supported cloud environments, with provider-specific command names:
| `aws-container` (`container` alias) | AWS | Docker, ECR, EC2 Auto Scaling Groups, CloudWatch Logs, SSM Session Manager |
| `gcp-container` | GCP | Docker, Artifact Registry, Compute Engine regional Managed Instance Groups |
Both variants use the same deployment model:
**Build container** -> **Push to registry** -> **Promote to `<env_medium>`** -> **Roll out hosts**
Rollback promotes `rollback_<env_medium>` back to `<env_medium>` and then runs the same rollout step.
Both variants keep two mutable deployment tags by default:
- `<env_medium>`: the currently promoted image for the target environment,
- `rollback_<env_medium>`: the previously promoted image for the target environment.
Build tags are usually immutable commit SHA tags. Promotion moves the environment tag to the selected build tag and moves the previous environment tag to the rollback tag.
#### Prerequisites
- **Docker** installed locally or on your CI runners.
- For `aws-container`: **AWS CLI** configured with permissions for ECR and EC2 Auto Scaling, with ECR repositories available or creatable by the command. CloudWatch Logs, EC2, and SSM permissions are also needed for the AWS-only `logs` and `host` subcommands.
- For `gcp-container`: **gcloud CLI** installed and authenticated, Artifact Registry enabled, a Docker Artifact Registry repository available or creatable in the target location, and a regional Compute Engine Managed Instance Group if you use the `rollout` subcommand.
#### Shared Subcommands
| `build` | Build the container locally. The command skips the Docker build if the build tag already exists remotely, unless `--force` is set. |
| `push` | Push a local image tag to the provider registry. |
| `promote` | Promote a build tag to the target environment tag and update the rollback tag. |
| `rollback` | Promote the rollback tag back to the target environment tag. |
| `rollout` | Roll out the promoted image to the runtime group: ASG instance refresh on AWS, MIG rolling action on GCP. |
| `list` | Show the currently promoted, rollback, and last pushed images. |
| `pull` | Pull an image from the provider registry. |
| `run` | Run a fully qualified image locally with Docker. |
`aws-container` also provides `logs` for CloudWatch Logs and `host` for SSM Session Manager access to an ASG instance.
#### Examples
Build and push a commit-tagged image:
```sh
xtask -e stag aws-container build \
Dockerfile \
--context-dir . \
--image my_image \
--build-tag SHA \
--region AWS_REGION
xtask -e stag gcp-container build \
Dockerfile \
--context-dir . \
--image my_image \
--build-tag SHA \
--project GCP_PROJECT_ID \
--location northamerica-northeast1 \
--repository my_repository
```
```sh
xtask -e stag aws-container push \
--image my_image \
--local-tag SHA \
--repository my_image \
--region AWS_REGION \
--auto-remote-tag
xtask -e stag gcp-container push \
--image my_image \
--local-tag SHA \
--project GCP_PROJECT_ID \
--location northamerica-northeast1 \
--repository my_repository \
--auto-remote-tag
```
Promote, roll out, and roll back:
```sh
xtask -e stag aws-container promote \
--repository my_image \
--build-tag SHA \
--region AWS_REGION
xtask -e stag aws-container rollout \
--region AWS_REGION \
--asg ASG_NAME \
--repository my_image \
--wait
xtask -e stag aws-container rollback \
--region AWS_REGION \
--repository my_image
```
```sh
xtask -e stag gcp-container promote \
--project GCP_PROJECT_ID \
--location northamerica-northeast1 \
--repository my_repository \
--image my_image \
--build-tag SHA
xtask -e stag gcp-container rollout \
--project GCP_PROJECT_ID \
--region northamerica-northeast1 \
--mig MIG_NAME \
--location northamerica-northeast1 \
--repository my_repository \
--image my_image \
--wait
xtask -e stag gcp-container rollback \
--project GCP_PROJECT_ID \
--location northamerica-northeast1 \
--repository my_repository \
--image my_image
```
Run a promoted image locally:
```sh
xtask -e stag aws-container run \
--image 123456789012.dkr.ecr.us-east-1.amazonaws.com/my_image:SHA \
--env-file .env.stag \
--name my-container
xtask -e stag gcp-container run \
--image northamerica-northeast1-docker.pkg.dev/GCP_PROJECT_ID/my_repository/my_image:SHA \
--env-file .env.stag \
--name my-container
```
### Images
The `image` command standardizes how we build, publish, promote, roll back, roll out, inspect, and clean virtual machine images.
It currently targets **AWS AMIs** and relies on **Terraform-managed EC2 baker instances** to produce those images. The rollout step is also Terraform-based: once an AMI is promoted, the application infrastructure state is applied so consumers of the promoted image can pick it up.
The image workflow is intentionally modeled after the container workflow. The goal is to keep the operational model familiar:
- build produces a new immutable artifact,
- promote moves the environment tag to that artifact,
- rollback moves the environment tag back to the previous artifact,
- rollout applies the promoted artifact to the running infrastructure,
- list shows the currently promoted and rollback artifacts.
#### Conceptual Model
##### Deploy
**Apply baker instances Terraform state** -> **Bake EC2 instance** -> **Create AMI** -> **Promote to `latest_<env_medium>`** -> **Apply application Terraform state**
##### Rollback
**Rollback by promoting `rollback_<env_medium>` to `latest_<env_medium>`** -> **Apply application Terraform state**
#### Prerequisites
- **AWS CLI** setup with permissions for EC2, AMIs, tags, and SSM.
- **Terraform** installed locally or on your CI runners.
- A Terraform state/root capable of creating one or more baker EC2 instances.
- Baker instances tagged so they can be discovered by logical image name.
- Application Terraform state configured to consume the promoted AMI tags.
#### Examples
The `build` command applies the baker Terraform state in `--tf-root`, waits for the baker instances to stop, and then creates AMIs from them:
```sh
xtask -e stag image build \
--region AWS_REGION \
--tf-root infra/states/images \
--image backend \
--image frontend
```
Additional AMI tags can be applied at creation time:
```sh
xtask -e stag image build \
--region AWS_REGION \
--tf-root infra/states/images \
--image backend \
--tag service=backend \
--tag owner=infra
```
If the baker Terraform state was already applied or you want to apply it yourself in a custom command that wraps the base `image` command,
then skip the Terraform apply step:
```sh
xtask -e stag image build \
--region AWS_REGION \
--tf-root infra/states/images \
--image backend \
--skip-apply
```
Retrieve the AMI id from the output of the build command and promote it:
```sh
xtask -e stag image promote \
--region AWS_REGION \
--image backend \
--ami-id AMI_ID
```
By default this applies `latest_<environment>=true` to the promoted AMI and moves the previously promoted AMI, if any, to `rollback_<environment>=true`.
Then roll out the promoted image by applying the Terraform state that consumes it:
```sh
xtask -e stag image rollout \
--tf-root infra/states/application
```
To see the current promoted and rollback AMIs:
```sh
xtask -e stag image list \
--region AWS_REGION \
--image backend
```
To roll back to the previous promoted AMI:
```sh
xtask -e stag image rollback \
--region AWS_REGION \
--image backend
```
Then apply the consuming Terraform state again:
```sh
xtask -e stag image rollout \
--tf-root infra/states/application
```
To inspect or debug a baker instance, open an SSM shell:
```sh
xtask -e stag image host \
--region AWS_REGION \
--image backend
```
To stream the system log for a baker instance instead:
```sh
xtask -e stag image host \
--region AWS_REGION \
--image backend \
--system-log
```
To clean obsolete AMIs for the current environment, first run a dry run:
```sh
xtask -e stag image clean \
--region AWS_REGION \
--image backend
```
Then deregister the obsolete AMIs with:
```sh
xtask -e stag image clean \
--region AWS_REGION \
--image backend \
--force
```
Obsolete AMIs are AMIs matching the logical image name and current environment, excluding the current promoted AMI and the current rollback AMI.
### Secrets
The secrets interface is shared across the supported cloud environments, with provider-specific command names:
| `aws-secrets` (`secrets` alias) | AWS | AWS Secrets Manager |
| `gcp-secrets` | GCP | GCP Secret Manager |
Both variants provide the same high-level subcommands:
| `create` | Create a secret with an initial empty JSON value. |
| `copy` | Copy one secret value to another secret. |
| `edit` | Open the secret value in an editor and write a new version. |
| `env-file` | Fetch one or more secrets and write an environment file, or stdout when `--output` is omitted. |
| `list` | List versions for a secret. |
| `push` | Push key-value updates to an existing JSON secret. |
| `view` | Print a secret value. |
#### Examples
View secrets:
```sh
xtask -e stag aws-secrets view --region AWS_REGION my_secret
xtask -e stag gcp-secrets view \
--project GCP_PROJECT_ID \
--location northamerica-northeast1 \
my_secret
```
Create an env file from one or more secrets:
```sh
xtask -e stag aws-secrets env-file \
--region AWS_REGION \
--output .env.stag.secrets \
my_secret
xtask -e stag gcp-secrets env-file \
--project GCP_PROJECT_ID \
--location northamerica-northeast1 \
--output .env.stag.secrets \
my_secret
```
### Docker Compose
The `docker-compose` command provides `up` and `down` commands to start and stop stacks. The command is integrated with the environment
configuration mechanism of `tracel-xtask`.
The name of the compose file must follow the template `docker-compose.{env}.yml` with `env` being the shorthand environment name.
For instance for the development environment the file is named `docker-compose.dev.yml`.
The command also requires a mandatory project name for the stack in order to have idempotent `up` commands.
### Dependencies
Various additional subcommands about dependencies.
`deny` makes sure that all dependencies meet requirements using [cargo-deny][6].
`unused` detects dependencies in the workspace that are not used.
### Vulnerabilities
This command makes it easier to execute sanitizers as described in [the Rust unstable book][7].
These sanitizers require a nightly toolchain.
```text
Run the specified vulnerability check locally. These commands must be called with 'cargo +nightly'
Usage: xtask vulnerabilities <COMMAND>
Commands:
all Run all most useful vulnerability checks
address-sanitizer Run Address sanitizer (memory error detector)
control-flow-integrity Run LLVM Control Flow Integrity (CFI) (provides forward-edge control flow protection)
hw-address-sanitizer Run newer variant of Address sanitizer (memory error detector similar to AddressSanitizer, but based on partial hardware assistance)
kernel-control-flow-integrity Run Kernel LLVM Control Flow Integrity (KCFI) (provides forward-edge control flow protection for operating systems kerneljs)
leak-sanitizer Run Leak sanitizer (run-time memory leak detector)
memory-sanitizer Run memory sanitizer (detector of uninitialized reads)
mem-tag-sanitizer Run another address sanitizer (like AddressSanitizer and HardwareAddressSanitizer but with lower overhead suitable for use as hardening for production binaries)
nightly-checks Run nightly-only checks through cargo-careful `<https://crates.io/crates/cargo-careful>`
safe-stack Run SafeStack check (provides backward-edge control flow protection by separating stack into safe and unsafe regions)
shadow-call-stack Run ShadowCall check (provides backward-edge control flow protection - aarch64 only)
thread-sanitizer Run Thread sanitizer (data race detector)
help Print this message or the help of the given subcommand(s)
```
## Utilities
### Easy CTRL+c management
`tracel-xtask` gives access to two useful macros `register_cleanup` and `handle_cleanup` to easily define some cleanup functions to be executed at
a given time during the program as well as whenever the user presses <kbd>CTRL+c</kbd>.
It is very useful to guard processes while executing some tests and make sure that the state is still cleaned up even if the program is interrupted by
the user.
Since the cleanup handler is stored in a static variable, its `drop` function is not automatically called when the program exits normally and the `handle_cleanup` macro needs to be called manually. However, when the program is interrupted by the user with <kbd>CTRL+c</kbd>, the cleanup is automatically called.
Example:
Register cleanup functions in your commands, say you have a customized `test` command that spins up some container.
```rust
pub(crate) async fn handle_command(
args: TestCmdArgs,
env: Environment,
ctx: Context,
) -> anyhow::Result<()> {
match args.get_command() {
TestSubCommand::Integration => {
register_cleanup!("Integration tests: Docker compose stack", move || {
base_commands::docker::handle_command(
DockerCmdArgs {
build: false,
project: super::DOCKER_COMPOSE_PROJECT_NAME.to_string(),
command: Some(DockerSubCommand::Down),
services: vec![],
},
Environment::Test,
ctx.clone(),
)
.expect("docker compose stack should stop");
});
base_commands::test::run_integration(&args.target, &args)
}
TestSubCommand::All => {
// ...
Ok(())
}
_ => Ok(()),
}
}
```
Then call the `handle_cleanup` macro at the end of your main function to force a cleanup:
```rust
use tracel_xtask::prelude::*;
#[macros::base_commands(
Build,
Check,
Fix,
Test
)]
pub enum Command {}
fn main() -> anyhow::Result<()> {
let (args, environment) = init_xtask::<Command>(parse_args::<Command>()?)?;
match args.command {
Command::Test(cmd_args) => {
commands::test::handle_command(cmd_args, environment, args.context)
}
_ => dispatch_base_commands(args, environment),
}?;
handle_cleanup!();
Ok(())
}
```
[1]: https://github.com/matklad/cargo-xtask
[2]: https://github.com/clap-rs/clap
[3]: https://doc.rust-lang.org/book/ch11-03-test-organization.html#integration-tests
[4]: https://doc.rust-lang.org/book/ch11-03-test-organization.html#unit-tests
[5]: https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html
[6]: https://embarkstudios.github.io/cargo-deny/
[7]: https://doc.rust-lang.org/beta/unstable-book/compiler-flags/sanitizer.html
[8]: https://github.com/tracel-ai/github-actions/blob/main/.github/workflows/publish-crate.yml