- Nix 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| envs | ||
| lib | ||
| modules | ||
| test_threads | ||
| threads | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
moira-modules
Standard library for moira — reusable environments, typed modules, and reference threads.
What's here
| Directory | Contents |
|---|---|
envs/ |
Nix shell environments for common toolchains |
modules/ |
Typed, reusable step implementations |
threads/ |
Ready-to-use pipeline definitions |
lib/threads/ |
Thread constructors — functions producing a thread |
threads/ and lib/threads/ are different things. A file in threads/ is a
concrete thread, exported as moiraPipelines and runnable as-is. A file in
lib/threads/ is a function you apply to get a thread, exported under lib.
Constructors are deliberately kept out of moiraPipelines, since moira runs
what it finds there and a function is not something it can run.
Import
# flake.nix
inputs.moira-modules.url = "git+https://git.hydrar.de/jmarya/moira-modules";
inputs.moira-modules.inputs.nixpkgs.follows = "nixpkgs";
Inside your outputs, bind moiraModules and moiraEnvironments per system:
outputs = { self, nixpkgs, moira-modules, ... }:
flake-utils.lib.eachDefaultSystem (system: {
moiraPipelines = {
ci = moira-modules.moiraPipelines.${system}.ci; # use a reference thread
};
});
Or reference them directly in thread files:
# threads/deploy.nix
{ moiraModules, moiraEnvironments, ... }:
{
name = "deploy";
trigger.on_push.branches = [ "main" ];
env = moiraEnvironments.rust;
steps = [
{ name = "test"; run = "cargo test --workspace"; }
{ name = "build"; run = "cargo build --release"; depends_on = [ "test" ]; }
{
name = "notify";
use = moiraModules."http/http-request";
"with" = {
method = "POST";
url = "https://hooks.example.com/deploy";
body_json = { sha = "${{ git.sha }}"; status = "ok"; };
};
depends_on = [ "build" ];
}
];
}
Thread constructors
Each constructor takes an options attrset and returns a thread. Every option
has a default; call-site values win.
| Constructor | Produces |
|---|---|
rustCi |
Parallel fmt/clippy gates, then test |
container |
Push a flake-built image via container/skopeo-push |
containerManifest |
Merge arch-suffixed tags into one multi-arch manifest list |
s3Site |
Mirror a flake-built directory to an S3 bucket |
# flake.nix
outputs = { self, nixpkgs, flake-utils, moira-modules, ... }:
flake-utils.lib.eachDefaultSystem (system: {
moiraPipelines =
let t = moira-modules.lib.${system};
in {
ci = t.rustCi { };
container = t.container {
archive = "\${{ flake.packages.containerImage }}";
image = "git.example.com/org/app";
};
docs = t.s3Site {
source = "\${{ flake.packages.docs }}/";
bucket = "app-docs";
endpoint = "https://s3.example.com";
};
};
});
needs_flake is derived from the ${{ flake.… }} reference in archive /
source, so it does not have to be repeated. Pass needsFlake explicitly for
references assembled at runtime, which the scanner cannot see.
Site defaults
withDefaults bakes in per-site values once rather than repeating them in
every repo. It composes — the result carries its own withDefaults.
t = moira-modules.lib.${system}.withDefaults {
s3Site.endpoint = "https://s3.example.com";
s3Site.region = "eu-central-1";
};
# endpoint and region are already set
docs = t.s3Site { source = "…"; bucket = "app-docs"; };
Multi-arch images
arches turns container into a thread-level matrix: one independent child
run per architecture, each pushing an arch-suffixed tag and routed by the
agent's system label. containerManifest then merges those tags via
on_workflow.
container = t.container {
archive = "\${{ flake.packages.containerImage }}";
image = "git.example.com/org/app";
arches = [ "x86_64-linux" "aarch64-linux" ];
};
container-manifest = t.containerManifest {
after = "container";
image = "git.example.com/org/app";
};
This needs one registered agent per architecture. The merge is a separate
thread because a thread-level matrix produces independent runs — no step
inside container observes all of them.
Environments
| Name | Provides |
|---|---|
rust |
cargo rustc clippy rustfmt pkg-config openssl |
node |
nodejs npm |
python |
python3 pip virtualenv |
go |
go gopls gotools |
docker |
docker docker-compose |
nix |
nix jq fd |
aws |
awscli2 |
container |
skopeo + an accept-anything signature policy |
buildah |
buildah skopeo — for manifest lists |
Set on a thread (all steps share it) or on an individual step to override.
The rust env provides the toolchain directly. Do not add rustup default …
to a step that uses it: that fetches a toolchain over the network at run time
and discards the pinned, content-addressed one the env exists to supply.
Modules
| Module | What it does |
|---|---|
http/http-request |
HTTP requests — GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS |
git/git |
Git operations — clone, commit, push, pull, tag, branch, merge |
ssh/ssh |
Execute commands or transfer files over SSH |
s3/s3 |
S3-compatible storage — ls, cp, sync (with delete), presign, … |
container/skopeo-push |
Push an OCI/Docker image (docker-archive) to a registry |
compression/compress |
Compress and extract tar, gz, bz2, xz, zip |
crypto/crypto |
Hash, HMAC, sign, verify, key generation |
jwt/jwt |
Sign, verify, and decode JWTs (HS*, RS*, ES*, EdDSA) |
totp/totp |
Generate and verify TOTP tokens |
Modules have a typed interface — declared inputs and outputs. Inputs are passed via with; outputs are available in downstream steps as ${{ steps.NAME.outputs.KEY }}. Secret inputs take a secret name, not the value — the agent fetches and injects it at runtime.
See also
- moira — the engine that runs these
- moira docs → Modules — full module interface spec and built-in modules
- moira docs → Environments — injected env vars and step environment model