Agentless CI/CD Platform Powered by Nix
GitHub Actions execution model + Nix reproducibility + Agentless deployment
✅ GitHub Actions-style execution - Parallel by default, explicit dependencies via needs
✅ Test without pushing - Run nix run .#ci locally, no more "fix CI" commits
✅ Reproducible environments - Nix guarantees exact dependency versions
✅ Agentless - No infrastructure needed, run anywhere (SSH/containers/local)
✅ Composable DSL - Nix functions instead of YAML copy-paste
✅ Local-first - CI works locally first, remote is optional
# flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nixactions.url = "github:yourorg/nixactions";
};
outputs = { nixpkgs, nixactions, ... }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
nixactionsLib = nixactions.lib.${system};
in {
packages.${system}.ci = import ./ci.nix {
inherit pkgs nixactions;
};
};
}# ci.nix
{ pkgs, nixactions }:
nixactions.mkWorkflow {
name = "ci";
jobs = {
# Runs immediately (Level 0)
lint = {
executor = nixactions.executors.local;
steps = [
{ bash = "npm run lint"; }
];
};
# Runs after lint succeeds (Level 1)
test = {
needs = [ "lint" ];
executor = nixactions.executors.local;
steps = [
{ bash = "npm test"; }
];
};
# Runs after test succeeds (Level 2)
build = {
needs = [ "test" ];
steps = [
{ bash = "npm run build"; }
];
};
};
}# Local execution
$ nix run .#ci
# Remote execution (SSH)
$ nix build .#ci
$ ssh server < resultExamples are organized into categories:
simple.nix - Most basic workflow
$ nix run .#example-simpleSingle job with basic actions (checkout, greet, system-info).
parallel.nix - Parallel execution with dependencies
$ nix run .#example-parallelJobs running in parallel (Level 0), sequential dependencies via needs, multi-level DAG execution.
env-sharing.nix - Environment variable sharing between actions
$ nix run .#example-env-sharingActions writing to $JOB_ENV, variable persistence across actions, multi-step calculations.
artifacts.nix / artifacts-paths.nix - Artifact passing between jobs
$ nix run .#example-artifacts
$ nix run .#example-artifacts-pathsoutputs to save artifacts, inputs to restore, custom restore paths, directory artifacts.
retry.nix - Automatic retry on failures
$ nix run .#example-retryThree backoff strategies (exponential, linear, constant), configurable delays, retry at workflow/job/action levels.
secrets.nix - Secrets management with env-providers
$ nix run .#example-secretsMultiple providers (SOPS, file, static, required), environment precedence, runtime override.
nix-shell.nix - Dynamic package loading
$ nix run .#example-nix-shellAdd packages on-the-fly with nixactions.actions.nixShell, no executor modification needed.
matrix-builds.nix - Compile-time matrix job generation
$ nix run .#example-matrix-buildsnixactions.mkMatrixJobs for cross-platform testing, multi-version testing, auto-generated job names.
structured-logging.nix - Log formats for observability
$ nix run .#example-structured-loggingThree formats: structured (default), JSON, simple. Duration tracking, exit code reporting.
test-action-conditions.nix - Action-level conditions
$ nix run .#example-test-action-conditionssuccess(), failure(), always() conditions, bash script conditions, continue-on-error.
multi-executor.nix - Multiple executors in one workflow
$ nix run .#example-multi-executorDifferent jobs using different executors, executor-specific configuration.
test-env.nix / test-isolation.nix - Testing environment behavior
$ nix run .#example-test-env
$ nix run .#example-test-isolationEnvironment propagation, job isolation, workspace isolation.
complete.nix - Full-featured CI/CD pipeline
$ nix run .#example-completeComplete workflow: parallel validation → testing → building → conditional deployment → notifications → cleanup.
python-ci.nix / python-ci-simple.nix - Python CI/CD
$ nix run .#example-python-ci
$ nix run .#example-python-ci-simpleReal Python project: pytest, flake8, mypy, coverage, Docker building, deployment.
nodejs-full-pipeline.nix - Node.js CI/CD with OCI executor
$ nix run .#example-nodejs-full-pipelineFull Node.js pipeline in containers: lint → test → build → artifacts.
buildah-pipeline.nix / buildah-multi-image.nix - Container image building
$ nix run .#example-buildah-pipeline
$ nix run .#example-buildah-multi-imageBuild OCI images with Buildah (no Docker daemon), multi-arch builds.
# Retry mechanism (10 test jobs)
$ nix run .#test-retry-comprehensive
# Action conditions (10 test jobs)
$ nix run .#test-conditions-comprehensiveExecutors define where code runs:
# Local machine (default)
executor = nixactions.executors.local
# Local with options
executor = nixactions.executors.local {
copyRepo = false; # Don't copy repository to job directory
name = "build-env"; # Custom name for isolated workspace
}
# Docker container
executor = nixactions.executors.oci {
image = "node:20";
copyRepo = true; # Copy repo to job directory (default)
}
# Custom executor name (creates separate workspace)
executor = nixactions.executors.oci {
image = "nixos/nix";
name = "test-env"; # Custom name
}Available executors:
local- Current machine with isolated job directoriesoci- Docker/OCI containers with volume mounts
Planned executors:
ssh- Remote via SSH with nix-copy-closurek8s- Kubernetes podsnixos-container- systemd-nspawnnomad- Nomad jobs
Executor Architecture:
- Workspace-level hooks (
setupWorkspace,cleanupWorkspace) - called once per unique executor - Job-level hooks (
setupJob,executeJob,cleanupJob) - called per job - Jobs with same executor share workspace but get isolated directories
Actions define what to do:
{
name = "test";
deps = [ pkgs.nodejs ]; # Nix packages for PATH
bash = "npm test"; # Script to run
}Standard actions library:
# Setup actions
nixactions.actions.checkout
nixactions.actions.setupNode { version = "20"; }
nixactions.actions.setupPython { version = "3.11"; }
nixactions.actions.setupRust
# Dynamic package loading
nixactions.actions.nixShell [ "curl" "jq" "git" ]
# NPM actions
nixactions.actions.npmInstall
nixactions.actions.npmTest
nixactions.actions.npmBuild
nixactions.actions.npmLintThe nixShell action allows you to dynamically add any Nix package to your job environment without modifying the executor:
{
jobs = {
api-test = {
executor = nixactions.executors.local;
steps = [
# Add packages on-the-fly
(nixactions.actions.nixShell [ "curl" "jq" ])
# Use them in subsequent actions
{
bash = ''
curl -s https://api.github.com/rate_limit | jq '.rate'
'';
}
];
};
# Different tools in different jobs
file-processing = {
executor = nixactions.executors.local;
steps = [
(nixactions.actions.nixShell [ "ripgrep" "fd" "bat" ])
{
bash = "fd -e nix -x rg -l 'TODO'";
}
];
};
};
}Benefits:
- No executor modification needed - Add packages per-job
- Reproducible - Packages come from nixpkgs
- Scoped - Packages only available in that job
- Composable - Can call multiple times in same job
See examples/nix-shell.nix for more examples.
{
steps = [
# Load from SOPS
(nixactions.actions.sopsLoad {
file = ./secrets.sops.yaml;
})
# Validate required vars
(nixactions.actions.requireEnv [
"API_KEY"
"DB_PASSWORD"
])
# Use secrets
{
bash = ''
kubectl create secret generic app \
--from-literal=api-key="$API_KEY"
'';
}
];
}Supported secrets managers:
- SOPS (recommended) -
nixactions.actions.sopsLoad - HashiCorp Vault -
nixactions.actions.vaultLoad - 1Password -
nixactions.actions.opLoad - Age -
nixactions.actions.ageDecrypt - Bitwarden -
nixactions.actions.bwLoad
Variables can be set at three levels (with precedence):
nixactions.mkWorkflow {
# Workflow level (lowest priority)
env = {
PROJECT = "myapp";
};
jobs = {
deploy = {
# Job level (medium priority)
env = {
ENVIRONMENT = "production";
};
steps = [
{
# Action level (highest priority)
env = {
LOG_LEVEL = "debug";
};
bash = "deploy.sh";
}
];
};
};
}Runtime override (highest priority):
$ API_KEY=xyz123 nix run .#deployJobs without dependencies run in parallel:
jobs = {
lint-js = { ... }; # Level 0
lint-css = { ... }; # Level 0 (parallel with lint-js)
test = { # Level 1 (after both lints)
needs = [ "lint-js" "lint-css" ];
...
};
}NixActions uses condition (not if) for unified semantics at both job and action levels:
# Built-in conditions (workflow-aware)
{
condition = "success()"; # Default - run if all deps succeeded
condition = "failure()"; # Run if any dep failed
condition = "always()"; # Always run (notifications, cleanup)
condition = "cancelled()"; # Run if workflow cancelled
}
# Bash script conditions (full power)
{
condition = ''[ "$BRANCH" = "main" ]''; # Environment check
condition = ''[ -f package.json ]''; # File check
condition = ''git diff --quiet main..HEAD''; # Git condition
condition = ''[ "$CI" = "true" ] && test -n "$API_KEY"''; # Combined
}Example:
jobs = {
test = { ... };
# Only runs if test succeeded (default)
deploy = {
needs = [ "test" ];
condition = "success()";
...
};
# Only runs if test failed
rollback = {
needs = [ "test" ];
condition = "failure()";
...
};
# Always runs (cleanup, notifications)
notify = {
needs = [ "test" ];
condition = "always()";
...
};
# Only on main branch
deploy-prod = {
needs = [ "test" ];
condition = ''[ "$BRANCH" = "main" ]'';
...
};
}steps = [
{ bash = "npm test"; }
# Only deploy on main branch
{
name = "deploy";
condition = ''[ "$BRANCH" = "main" ]'';
bash = "deploy.sh";
}
# Always notify
{
name = "notify";
condition = "always()";
bash = "curl -X POST $WEBHOOK";
}
];{
continueOnError = true; # Don't stop workflow if this job fails
...
}nixactions/
├── flake.nix # Nix flake entry point
├── lib/
│ ├── default.nix # Main API export
│ ├── mk-executor.nix # Executor contract (5-hook model)
│ ├── mk-workflow.nix # Workflow compiler
│ ├── mk-matrix-jobs.nix # Matrix job generator
│ ├── retry.nix # Retry mechanism
│ ├── timeout.nix # Timeout handling
│ ├── logging.nix # Structured logging
│ ├── runtime-helpers.nix # Runtime bash helpers
│ ├── executors/ # Built-in executors
│ │ ├── local.nix # Local machine
│ │ ├── oci.nix # Docker/OCI containers
│ │ ├── action-runner.nix # Action execution engine
│ │ ├── local-helpers.nix # Local executor bash functions
│ │ └── oci-helpers.nix # OCI executor bash functions
│ ├── actions/ # Standard actions
│ │ ├── checkout.nix # Repository checkout
│ │ ├── nix-shell.nix # Dynamic package loading
│ │ ├── npm.nix # NPM actions
│ │ └── setup.nix # Setup actions
│ ├── env-providers/ # Environment variable providers
│ │ ├── file.nix # .env file loading
│ │ ├── sops.nix # SOPS encrypted files
│ │ ├── static.nix # Hardcoded values
│ │ └── required.nix # Validation
│ └── jobs/ # Pre-built job templates
│ └── buildah-build-push.nix # OCI image building
└── examples/ # Working examples (30+)
├── 01-basic/ # Core concepts
├── 02-features/ # Advanced features
├── 03-real-world/ # Production pipelines
└── 99-untested/ # Reference examples
nixactions :: {
# Core constructors
mkExecutor :: ExecutorConfig -> Executor,
mkWorkflow :: WorkflowConfig -> Derivation,
mkMatrixJobs :: MatrixConfig -> AttrSet Job,
# Built-in executors
executors :: {
local :: { copyRepo? :: Bool, name? :: String } -> Executor,
oci :: { image :: String, copyRepo? :: Bool, name? :: String } -> Executor,
},
# Standard actions
actions :: {
# Setup actions
checkout :: Action,
setupNode :: { version :: String } -> Action,
setupPython :: { version :: String } -> Action,
# Package management
nixShell :: [String] -> Action,
# NPM actions
npmInstall :: Action,
npmTest :: Action,
npmBuild :: Action,
npmLint :: Action,
},
# Environment providers
envProviders :: {
file :: { path :: String, required? :: Bool } -> Derivation,
sops :: { file :: Path, format? :: String, required? :: Bool } -> Derivation,
static :: AttrSet String -> Derivation,
required :: [String] -> Derivation,
},
# Pre-built jobs
jobs :: {
buildahBuildPush :: { ... } -> Job,
},
}{
name :: String,
jobs :: AttrSet Job,
env :: AttrSet String = {},
}{
executor :: Executor,
actions :: [Action],
needs :: [String] = [],
condition :: Condition = "success()", # success() | failure() | always() | cancelled() | BashScript
continueOnError :: Bool = false,
env :: AttrSet String = {},
envFrom :: [Derivation] = [], # Environment providers
inputs :: [String | { name, path }] = [], # Artifacts to restore
outputs :: AttrSet String = {}, # Artifacts to save
retry :: RetryConfig | Null = null,
timeout :: Int | Null = null, # Seconds
}{
name :: String = "action",
bash :: String,
deps :: [Derivation] = [],
env :: AttrSet String = {},
workdir :: Path | Null = null,
condition :: Condition | Null = null, # Action-level condition
retry :: RetryConfig | Null = null,
timeout :: Int | Null = null,
}{
max_attempts :: Int = 1, # Total attempts (1 = no retry)
backoff :: "exponential" | "linear" | "constant" = "exponential",
min_time :: Int = 1, # Minimum delay (seconds)
max_time :: Int = 60, # Maximum delay (seconds)
}| Feature | GitHub Actions | NixActions |
|---|---|---|
| Execution model | Parallel + needs | ✅ Same |
| Dependencies | needs: [...] |
✅ Same |
| Conditions | if: success() |
✅ Same (condition) |
| Configuration | YAML | Nix DSL (composable) |
| Environment | Container images | Nix derivations (hermetic) |
| Infrastructure | GitHub.com | ✅ None (agentless) |
| Local execution | act (partial compat) |
✅ Native nix run |
| Test without push | ❌ Must push to repo | ✅ nix run .#ci locally |
| Build environments | Variable (network, time) | Reproducible (Nix store) |
| Cost | $21/month+ | ✅ $0 |
| Feature | GitLab CI | NixActions |
|---|---|---|
| Execution | Sequential by default | ✅ Parallel |
| Infrastructure | GitLab instance | ✅ None |
| Local testing | Limited | ✅ Native |
| Test without push | ❌ Must push | ✅ Local nix run |
- Local-first - CI should work locally first, remote is optional. No more "fix CI" commits.
- Agentless - No agents, no polling, no registration
- Reproducible - Nix guarantees exact build environments (not execution results)
- Composable - Everything is a Nix function, reuse without copy-paste
- Simple - Minimal abstractions, maximum power
- Parallel - Jobs run in parallel by default (like GitHub Actions)
Reproducible:
- Workflow script compilation (same inputs → same output)
- Build environments (exact dependency versions)
- Action derivations (cached in Nix store)
Not guaranteed (same as any CI):
- Network call results (
curl, API responses) - External service state
# Enter development environment
$ nix develop
# Run basic examples
$ nix run .#example-simple
$ nix run .#example-parallel
$ nix run .#example-env-sharing
# Run feature examples
$ nix run .#example-artifacts
$ nix run .#example-retry
$ nix run .#example-secrets
$ nix run .#example-matrix-builds
# Run real-world examples
$ nix run .#example-complete
$ nix run .#example-python-ci
$ nix run .#example-nodejs-full-pipeline
# Run comprehensive test suites
$ nix run .#test-retry-comprehensive
$ nix run .#test-conditions-comprehensive
# Compile all examples to bash scripts
$ ./scripts/compile-examples.shSee TODO.md for detailed implementation plan.
Phase 1 (MVP): ✅ COMPLETED
- Core execution engine with DAG-based parallel execution
- Local executor with workspace/job isolation
- OCI executor with per-job containers
- Actions as Derivations (build-time compilation)
- Retry mechanism (3 backoff strategies)
- Timeout handling
- Condition system (built-in + bash scripts)
- Artifacts management with custom restore paths
- Environment providers (file, sops, static, required)
- Matrix job generation
- Structured logging (3 formats)
- 30+ working examples
Phase 2 (Next): Remote Executors & Ecosystem
- SSH executor with nix-copy-closure
- Kubernetes executor
- Extended actions library
- Production hardening
- Documentation improvements
MIT
Full documentation is available in docs/:
| Document | Description |
|---|---|
| User Guide | Quick start, installation, first workflow |
| Architecture | System design, layered structure |
| Actions | Actions as Derivations, build-time compilation |
| Executors | Local, OCI, SSH, K8s executors |
| Conditions | Conditional execution system |
| Environment | Env providers, secrets management |
| API Reference | Complete API documentation |
Additional resources:
- TODO.md - Implementation roadmap
- CONTRIBUTING.md - Contribution guidelines
- compiled-examples/ - Compiled bash scripts showing generated code
Contributions are welcome! Please:
- Read the documentation for architecture details
- Check TODO.md for planned features
- Submit PRs with clear descriptions
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- License: MIT (see LICENSE)