Sykli Elixir SDK
CI pipelines defined in Elixir instead of YAML.
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
endInstallation
Mix.install([{:sykli, "~> 0.2"}])
Or add to your mix.exs:
defp deps do
[{:sykli, "~> 0.2"}]
endQuick Start
Create a sykli.exs file in your project root:
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
endRun it:
sykli run
# or
elixir sykli.exs --emit | sykli run -Core Concepts
Tasks
Tasks are the basic unit of work:
task "test" do
run "mix test"
endDependencies
Define execution order with after_:
task "deploy" do
run "./deploy.sh"
after_ ["build", "test"] # Runs after both complete
endIndependent tasks run in parallel automatically.
Input-Based Caching
Skip unchanged tasks with inputs:
task "test" do
run "mix test"
inputs ["**/*.ex", "**/*.exs", "mix.exs", "mix.lock"]
endOutputs
Declare task outputs for artifact passing:
task "build" do
run "mix release"
output "release", "_build/prod/rel/myapp"
endConditional Execution
Run tasks based on branch, tag, or event:
# 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*"))
endTemplates
Templates eliminate repetition:
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
endContainers
Run tasks in isolated containers:
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
endConvenience Methods
# 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"
endComposition
Parallel Groups
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
endChains
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])
endArtifact Passing
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 ."
endDynamic Pipelines
Since it's real Elixir, use loops, variables, and conditionals:
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
endMatrix Builds
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
endService Containers
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
endSecrets
# 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"
endRetry & Timeout
task "flaky-test" do
run "./integration-test.sh"
retry 3 # Retry up to 3 times
timeout 300 # 5 minute timeout
endKubernetes Execution
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"
endElixir Presets
Built-in macros for common Elixir tasks:
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
endCustomize with options:
mix_test(name: "unit-tests")Examples
See the examples directory for complete working examples:
01-basic/- Tasks, dependencies, parallel execution02-caching/- Input-based caching and conditions03-containers/- Container execution with mounts04-templates/- DRY configuration with templates05-composition/- Parallel groups, chains, artifacts06-dynamic/- Dynamic pipelines with Elixir code
API Reference
See REFERENCE.md for the complete API documentation.
Links
License
MIT