A SaturnCI pipeline is a Ruby file called a workflow. It lives in
your repository at
.saturnci/workflows/default_workflow.rb and it decides what
runs on a commit.
How a workflow runs
When you push, SaturnCI creates a workflow run for the commit and evaluates your workflow: it runs the file in a container that already has the SDK installed, with the workflow run's id in the environment.
SaturnCI evaluates the workflow again every time a job in that workflow run finishes, and periodically while the run is still going. Each evaluation is a fresh process. The file doesn't wait for anything: it looks at the current state of the workflow run, starts whatever is now ready to start, and exits. When the pipeline is complete, it tells SaturnCI the workflow run is finished.
Because the same file is evaluated many times per commit, everything it
creates is created with idempotent: true. The first
evaluation creates the job run; every later evaluation gets back that
same job run and its current status.
Inside the container, the environment contains:
WORKFLOW_RUN_ID— the id of the workflow run being evaluatedSATURNCI_ACCESS_TOKEN— an API token scoped to this evaluationBRANCH_NAME,COMMIT_HASH,COMMIT_MESSAGE,AUTHOR_NAME— the commit's git metadata
A minimal workflow
This workflow runs the test suite and finishes.
# .saturnci/workflows/default_workflow.rb
class DefaultWorkflow
def initialize(env: ENV)
@env = env
end
def perform(io: $stdout)
test_suite_run = workflow_run.test_suite_runs.create(
job_name: "test_suite",
task_adapter_name: "rails_rspec",
environment_name: "rspec",
service_name: "saturnci_test_app",
command: "bundle exec rspec",
idempotent: true
)
test_suite_run.start
io.puts "Started test_suite run: id=#{test_suite_run.id} url=#{test_suite_run.url}"
workflow_run.finish if test_suite_run_finished?
end
private
def test_suite_run_finished?
run = workflow_run.job_runs.list(job_name: "test_suite").first
return false unless run
run.passed? || run.failed?
end
def workflow_run
@workflow_run ||= SaturnCI::WorkflowRun.find(
client: saturnci_client,
id: @env.fetch("WORKFLOW_RUN_ID")
)
end
def saturnci_client
SaturnCI::Client.new(
SaturnCI::Credentials.new(api_token: @env.fetch("SATURNCI_ACCESS_TOKEN"))
)
end
end
DefaultWorkflow.new.perform if $PROGRAM_NAME == __FILE__
Read it as a sequence of evaluations:
- On push: the test suite run is created and started. It isn't finished yet, so the file returns.
-
When the test suite finishes:
createreturns the existing run, now passed or failed, so the workflow run is finished.
test_suite is a job defined in
your repository under .saturnci/jobs/.
Running jobs in parallel
Create and start several job runs in the same evaluation and they run concurrently. Later evaluations check each one.
rubocop_job_run = workflow_run.job_runs.create(
job_name: "rubocop",
task_adapter_name: "shell",
idempotent: true
)
rubocop_job_run.start
javascript_job_run = workflow_run.job_runs.create(
job_name: "javascript",
task_adapter_name: "shell",
idempotent: true
)
javascript_job_run.start
Running a job only after another one passes
Look up the earlier run and return if it hasn't passed yet.
test_suite_run = workflow_run.job_runs.list(job_name: "test_suite").first
return unless test_suite_run&.passed?
list returns [] when nothing with that name has
been created yet.
Running a job only on one branch
return unless workflow_run.branch_name == "main"
Passing values to a job
Extra keyword arguments to create become the job's params,
and reach the job's container as upper-cased environment variables.
deploy_job_run = workflow_run.job_runs.create(
job_name: "deploy",
task_adapter_name: "shell",
container_image_url: "registry.example.com/app:abc123",
idempotent: true
)
# The deploy job sees CONTAINER_IMAGE_URL=registry.example.com/app:abc123
Params can also be set after creation:
deploy_job_run.update(container_image_url: image_url)
Passing values between jobs
A job can read another job's params.
production_image_build_job_run = workflow_run.job_runs
.list(job_name: "production_image_build")
.find(&:passed?)
image_url = SaturnCI::JobRun
.find(client: saturnci_client, id: production_image_build_job_run.id)
.params["container_image_url"]
Nesting a run under another
Pass parent_job_run_id: to create to show a run
nested under another one in the SaturnCI UI.
workflow_run.job_runs.create(
job_name: "rubocop",
task_adapter_name: "shell",
parent_job_run_id: deploy_job_run.id,
idempotent: true
)