README.md

# Sykli Elixir SDK

CI pipelines defined in Elixir instead of YAML.

```elixir
Mix.install([{:sykli, "~> 0.2"}])

use Sykli

pipeline do
  task "test" do
    run "mix test"
    inputs ["**/*.ex", "mix.exs"]
  end

  task "build" do
    run "mix release"
    after_ ["test"]
  end
end
```

## Installation

```elixir
Mix.install([{:sykli, "~> 0.2"}])
```

Or add to your `mix.exs`:

```elixir
defp deps do
  [{:sykli, "~> 0.2"}]
end
```

## Quick Start

Create a `sykli.exs` file in your project root:

```elixir
Mix.install([{:sykli, "~> 0.2"}])

use Sykli

pipeline do
  task "lint" do
    run "mix credo --strict"
  end

  task "test" do
    run "mix test"
  end

  task "build" do
    run "mix release"
    after_ ["lint", "test"]
  end
end
```

Run it:

```bash
sykli run
# or
elixir sykli.exs --emit | sykli run -
```

## Core Concepts

### Tasks

Tasks are the basic unit of work:

```elixir
task "test" do
  run "mix test"
end
```

### Dependencies

Define execution order with `after_`:

```elixir
task "deploy" do
  run "./deploy.sh"
  after_ ["build", "test"]  # Runs after both complete
end
```

Independent tasks run in parallel automatically.

### Input-Based Caching

Skip unchanged tasks with `inputs`:

```elixir
task "test" do
  run "mix test"
  inputs ["**/*.ex", "**/*.exs", "mix.exs", "mix.lock"]
end
```

### Outputs

Declare task outputs for artifact passing:

```elixir
task "build" do
  run "mix release"
  output "release", "_build/prod/rel/myapp"
end
```

### Conditional Execution

Run tasks based on branch, tag, or event:

```elixir
# String-based conditions
task "deploy" do
  run "./deploy.sh"
  when_ "branch == 'main'"
end

# Type-safe conditions
alias Sykli.Condition

task "release" do
  run "./release.sh"
  when_cond Condition.branch("main") |> Condition.or_cond(Condition.tag("v*"))
end
```

## Templates

Templates eliminate repetition:

```elixir
pipeline do
  # Define template
  elixir = template("elixir")
    |> template_container("elixir:1.16")
    |> template_mount("src:.", "/app")
    |> template_workdir("/app")

  # Tasks inherit from template
  task "lint" do
    from elixir
    run "mix credo"
  end

  task "test" do
    from elixir
    run "mix test"
  end
end
```

## Containers

Run tasks in isolated containers:

```elixir
pipeline do
  # Register resources
  src = dir(".")
  deps_cache = cache("mix-deps")

  task "test" do
    container "elixir:1.16"
    mount src, "/app"
    mount_cache deps_cache, "/app/deps"
    workdir "/app"
    env "MIX_ENV", "test"
    run "mix test"
  end
end
```

### Convenience Methods

```elixir
# Mount current dir to /work
task "test" do
  container "elixir:1.16"
  mount_cwd()
  run "mix test"
end

# Mount to custom path
task "build" do
  container "elixir:1.16"
  mount_cwd_at("/app")
  run "mix release"
end
```

## Composition

### Parallel Groups

```elixir
pipeline do
  checks = parallel("checks", [
    task_ref("lint") |> run_cmd("mix credo"),
    task_ref("fmt") |> run_cmd("mix format --check-formatted"),
    task_ref("test") |> run_cmd("mix test")
  ])

  task "build" do
    run "mix release"
    after_group checks
  end
end
```

### Chains

```elixir
pipeline do
  deps = task_ref("deps") |> run_cmd("mix deps.get")
  compile = task_ref("compile") |> run_cmd("mix compile")
  test = task_ref("test") |> run_cmd("mix test")

  # deps -> compile -> test
  chain([deps, compile, test])
end
```

### Artifact Passing

```elixir
task "build" do
  run "mix release"
  output "release", "_build/prod/rel/myapp"
end

# Automatically depends on "build"
task "package" do
  input_from "build", "release", "/app"
  run "docker build -t myapp ."
end
```

## Dynamic Pipelines

Since it's real Elixir, use loops, variables, and conditionals:

```elixir
pipeline do
  apps = ["api", "web", "worker"]

  for app <- apps do
    task "test-#{app}" do
      run "mix test apps/#{app}"
    end
  end

  task "deploy" do
    run "./deploy.sh"
    after_ Enum.map(apps, &"test-#{&1}")
    when_ "branch == 'main'"
  end
end
```

## Matrix Builds

```elixir
pipeline do
  # Test across Elixir versions
  versions = matrix_tasks("elixir-versions", ["1.14", "1.15", "1.16"], fn version ->
    task_ref("test-#{version}")
    |> run_cmd("mix test")
    |> with_container("elixir:#{version}")
  end)

  task "deploy" do
    run "mix release"
    after_group versions
  end
end
```

## Service Containers

```elixir
task "integration" do
  container "elixir:1.16"
  mount_cwd()
  service "postgres:15", "db"
  service "redis:7", "cache"
  env "DATABASE_URL", "postgres://postgres:postgres@db:5432/test"
  env "REDIS_URL", "redis://cache:6379"
  run "mix test --only integration"
  timeout 300
end
```

## Secrets

```elixir
# Simple secret
task "deploy" do
  secret "HEX_API_KEY"
  run "mix hex.publish"
end

# Typed secrets
alias Sykli.SecretRef

task "deploy" do
  secret_from "GITHUB_TOKEN", SecretRef.from_env("GH_TOKEN")
  secret_from "DB_PASSWORD", SecretRef.from_vault("secret/db#password")
  run "./deploy.sh"
end
```

## Retry & Timeout

```elixir
task "flaky-test" do
  run "./integration-test.sh"
  retry 3      # Retry up to 3 times
  timeout 300  # 5 minute timeout
end
```

## Kubernetes Execution

```elixir
alias Sykli.K8s

task "train-model" do
  container "pytorch/pytorch:2.0"
  run "python train.py"
  k8s K8s.options()
       |> K8s.namespace("ml-jobs")
       |> K8s.memory("32Gi")
       |> K8s.gpu(1)
       |> K8s.node_selector(%{"gpu" => "nvidia-a100"})
end

# Hybrid: some tasks local, some on K8s
task "test" do
  run "mix test"
  target "local"
end

task "train" do
  run "python train.py"
  target "k8s"
end
```

## Elixir Presets

Built-in macros for common Elixir tasks:

```elixir
pipeline do
  mix_deps()      # mix deps.get
  mix_test()      # mix test
  mix_credo()     # mix credo --strict
  mix_format()    # mix format --check-formatted
  mix_dialyzer()  # mix dialyzer
end
```

Customize with options:

```elixir
mix_test(name: "unit-tests")
```

## Examples

See the [examples directory](./examples/) for complete working examples:

- `01-basic/` - Tasks, dependencies, parallel execution
- `02-caching/` - Input-based caching and conditions
- `03-containers/` - Container execution with mounts
- `04-templates/` - DRY configuration with templates
- `05-composition/` - Parallel groups, chains, artifacts
- `06-dynamic/` - Dynamic pipelines with Elixir code

## API Reference

See [REFERENCE.md](./REFERENCE.md) for the complete API documentation.

## Links

- [GitHub](https://github.com/yairfalse/sykli)
- [Documentation](https://hexdocs.pm/sykli)

## License

MIT