No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-03 18:29:14 +02:00
envs feat(lib): thread constructors, shared envs, s3 sync --delete 2026-08-03 18:25:10 +02:00
lib feat(lib): thread constructors, shared envs, s3 sync --delete 2026-08-03 18:25:10 +02:00
modules feat(lib): thread constructors, shared envs, s3 sync --delete 2026-08-03 18:25:10 +02:00
test_threads test: add complex_dag and dag-matrix test threads 2026-05-23 01:15:55 +02:00
threads fix(modules): use pythonPackages directly and disable style checks 2026-05-23 02:01:34 +02:00
flake.lock init: standard library foundation 2026-04-19 20:37:56 +02:00
flake.nix feat(lib): thread constructors, shared envs, s3 sync --delete 2026-08-03 18:25:10 +02:00
README.md docs: document thread constructors, new envs, and s3 delete 2026-08-03 18:29:14 +02:00

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