Cianity
Add a bit of sanity to your CI. Cianity (pronounced sanity) is a set of tools for writing CI workflows using the ciane (pronounced sane) DSL. Ciane workflows can then be built to a target CI platform's CI definition format. Currently only GitLab pipelines are supported.
It's all very early days still, so things may not work as intended in all cases as the support for workflow features is still pretry minimal.
[!WARNING] The implementation of Cianity has made significant use of AI assisted coding. I know that this is a deal-breaker for some people, so I want to be up front about it. For me, it has been the difference between Cianity existing at all and not existing (which is what has happened for the last couple of years since I had the idea).
Having said that, the documentation (such as it is) is completely written by hand.
Quick Start
Everything is pre-release right now, so first you'll need to clone this repo and build with cargo. If you don't have a Rust toolchain installed, then head to rustup.rs to remedy that situation.
cargo build --release --bin cianity
The executable can be found in target/release/cianity.
Follow the links to get started with the cianity command line tool and the Ciane DSL.
Install the Ciane language plugin for Vim/NeoVim or VSCode.
Why?
Why would you want to write in some other format, just to end up with the same configuration files at the end of the process? GitLab's pipelines are described in YAML. We all know that "YAML Ain't Markup Language", but YAML Ain't Code Either. We were sold Infrastructure as Code, and what we got was Infrastructure as Config files, the acronym is the same, but it's not the same.
On the other hand, Ciane is code. So you get the nice things we've become accustomed to, a linter, a formatter, keyword syntax highlighting, LSP support with auto-completion, jump to definition, and references capabilities. As of today, there are plugins for Vim/NeoVim and VSCode.
Ciane workflows give you explicit templates that jobs can inherit from including cross-file imports. The output GitLab pipeline configuration doesn't use extends, the configuration for every job is right there in the job so that you don't have to go hunting across different files to work out what's going on.
For people who don't write CI workflows every day, remembering convensions and every key used in a YAML map just isn't feasible. So Ciane workflows prefer configuration over convension and the IDE plugins auto-conplete attributes for you (although the current implementation is lacking).
Example
Here's a simple Ciane workflow that's used right here in this repository (workflow.ci):
workflow main ( strategy = default_branch_and_reviews )
template rust_slim ( image = rust:1.96-slim-trixie )
stage build {
template build (
inherit = rust_slim,
variables = ( RELEASE_FLAG = "" ),
) { cargo build $RELEASE_FLAG --workspace }
job build_debug ( inherit = build ) [
steps,
]
job build_release (
inherit = build,
variables = ( RELEASE_FLAG = --release ),
) [
steps,
] -> [ target/release/cianity ]
}
stage test {
job test ( image = rust:1.96-trixie ) [
step install_nextest { curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin }
step run_tests { cargo nextest run --workspace }
]
job fmt ( inherit = rust_slim ) [
step install_rustfmt { rustup component add rustfmt }
step fmt_check { cargo fmt --check }
]
job clippy ( inherit = rust_slim ) [
step install_clippy { rustup component add clippy }
step clippy { cargo clippy }
]
}
stage cianity_check {
job check (
image = rust:1.96-trixie,
dependencies = [ build.build_release ],
) [
step check { ./target/release/cianity check }
step format { ./target/release/cianity format --check }
step build { ./target/release/cianity build -t gitlab --check }
]
}
For a breakdown of this example, see Cianity's Own Workflow under Examples.
Ciane Language Guide
Ciane is a Domain-Specific Language (DSL) for writing CI workflows.
File extensions are typically .ci, but .ciane is also considered idiomatic.
Index
Objects
Object types in Ciane are keywords, followed optionally by key/value attributes in parentheses ( ) and a body surrounded by braces { } or for multi-step jobs brackets [ ].
Workflow
A workflow is the top level object of a Ciane file. It accepts one attribute strategy which determines when jobs in that workflow are run by default.
workflow main ( strategy = default_branch ) {
stage build {
...
}
}
A workflow's body braces can be ommitted, in which case the body of the workflow extends until the end of the file. This reduces the indentation level on most of the file by one.
workflow main ( strategy = default_branch )
stage build {
...
}
Use
In order to access templates from other workflows, they must be specified with use, providing a name by which the external workflow will be referenced and a path to where it is located.
use deploy ( path = ./ci/cianity/deploy.ci )
Stage
A stage is a group of jobs which run concurrently (unless there are explicit dependencies between them). A stage will only start running when the prior stage completes.
stage build {
job build_debug {
...
}
}
Job
A job is a single instance of work in CI which will either pass or fail.
A job's body is a list of steps which will be executed in sequence.
job build_debug [
step rustup { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh }
step build { cargo build }
]
A job can also provide a single step with brace shorthand.
job build_debug { cargo build }
The job's attributes can be used to specify explicit dependencies on other jobs, to inherit from a template, or to specify the container image the job will run on.
To specify output, artifacts or variables, job output notation is used.
job build_debug { cargo build } -> [ target/debug/bin ]
Step
A step in a job or a template provides one or more commands to be run together.
Step body
job build_debug [
step rustup { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh }
step build { cargo build }
]
A step's body is surrounded by braces and can contain multiple lines of script.
job build_debug [
step rustup { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh }
step build {
cargo build
cargo build --release
}
]
Steps
When ineriting from a template, a job may specifying steps in its body to use all the steps from the template.
Job output
The output of a job, files and environment variables are specified in a list after the job body and the output notation arrow ->.
job build_config { source config.sh } -> [ build_config.json, etc/**, $MAKE ]
File artifacts can be soecified exactly or with globs (generally this will be passed directly to the target pipeline configuration format.
Environment variables are prefixed with a dollar sign $.
Template
A template can contain all the same attributes and body options as a job, but will not be run.
Instead, a job can use the inherit attribute to inherit the attributes and body from the template.
When using inherit, if the template has steps, they can be reused in full with the steps object, or only partially by name.
Consider the template:
template base [
step setup { rustup update }
step build { cargo build }
step run { cargo test }
]
This job uses all the steps as they are:
job unit ( inherit = base ) [
steps,
]
Whereas this job uses only some as well as specifying others.
job integration ( inherit = base ) [
step install_nextest { curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin }
step setup,
step build,
step run { cargo nextest run --workspace }
]
Attributes
Attributes are key/value pairs which can be attached to many objects. They are surrounded in parentheses.
Strategy
- Accepted on:
workflow
The strategy attribute on a workflow determines when jobs are run by default. The 4 options are:
none(default): no rules, runs on all branches on GitLab by defaultdefault_branch: run only on the default branchreviews: run only on reviews (MRs in GitLab)default_branch_and_reviews: run on the default branch and also on reviews
Path
- Accepted on:
use
The path to the file containing the workflow to be imported.
Inherit
Inherit attributes and/or steps from a the template named.
If the template is in another stage, then it must be specified with dot notation stage.template_name. If the template is coming from another workflow, then the workflow name needs to be prefixed with a slash external_workflow/template_name and also with the stage if it isn't a top level template external_workflow/stage.template_name.
Multiple templates can be specified in a list. If at least one of the templates has a body, then the keyword steps can be used to take all the templates' defined steps.
job build ( inherit = [ rust_slim, base ] ) [ steps ]
Image
Specify the container image that the job will run on.
Dependencies
In order to have access to output from previously executed jobs, the dependencies attribute is used to specify a list of all jobs for which we want dependencies.
stage build {
job build_release (
image = rust:1.96-slim-trixie,
) {
cargo build $RELEASE_FLAG --workspace
} -> [ target/release/cianity ]
}
stage test {
job test_help (
image = rust:1.96-slim-trixie,
dependencies = [ build.build_release ],
) { ./target/release/cianity -h }
}
In generated GitLab pipeline, jobs that don't specify dependencies will be explicitly set with an empty list to avoid unncessary downloads.
Variables
A job or template can specify environment variables which will be available to the scripts running in the body of the job. Variables are key/value pairs and are specified in the same way as attributes.
job build ( variables = ( RUST_BACKTRACE = 1, CARGO_TERM_COLOR = always ) ) { cargo build }
Currently, variables are passed into a GitLab pipeline config files pretty much as-is.
Cianity Command Line Tool
The cianity command line tool contains everything there is to work with ciane workflows.
Default workflow
Cianity will look for a file called workflow.ci (or .workflow.ci as a second choice) in the current directory and then going up towards the root of the file system.
In this case, no file needs to be specified. The rest of this guide will assume that a workflow.ci file is used.
Most commands also accept -w/--workspace to specify a different workflow root.
Check (lint)
To check the validity of a workflow use the check command.
cianity check
As well as ensuring that the workflow can be built, a few other warning lints are also checked.
Format
To format files, use the format command.
cianity format
To validate that a workspace has already been formatted (on CI for example), use the --check flag.
cianity format --check
There is no configuration for the formatting. It is what it is.
Build
It is currently possible to build a ciane workflow to a GitLab pipeline.
cianity build -t gitlab
By default this will result in a .gitlab-ci.yml file which is picked up by GitLab by default.
To specify a different output file, use the -o flag.
cianity build -t gitlab -o main.gitlab-ci.yml
In order to check that the built pipeline is up to date with the ciane workflow, the --check flag can be used. It is recommended that this is used on CI to make sure that both checked in files match.
cianity build -t gitlab -o main.gitlab-ci.yml --check
LSP
Cianity comes with a built in LSP, just like a grown up programming language.
cianity lsp
The lsp sub-command will also accept the --stdio flag which some IDEs pass unconditionally. This changes nothing as the cianity LSP only works over stdin/out.
Help!
Use the -h flag or the help sub-command to see available sub-commands. The -h flag can be used with sub-commands to see all available flags.
Editor Integration
Vim/NeoVim
There is a Plugin for the Ciane DSL available for Vim and NeoVim.
Currently this plugin isn't set up to be installed from a Git reposity (like GitHub), instead it needs to be comfigures from a local clone of the cianity repository.
vim-plug
Plug '~/path/to/cianity/vim-ciane'
lazy.nvim
require("lazy").setup({
{ dir = '~/path/to/cianity/vim-ciane', },
})
VSCode
There is an extension for VS Code to add support for the Ciane DSL.
To build and install locally run the following from the vscode-ciane directory in the cianity repository.
npm install
npm run package
Then in VSCode: Extensions > "Install from VSIX".
Examples
Cianity's Own Workflow
This example shows the workflow that is used for Cianity's own CI pipeline, (workflow.ci).
Let's break it down.
The first line is the workflow definition. The strategy determines when the jobs are run (default branch and reviews - MRs - in this case).
The next section is a template, albeit a simple one. The template has the name rust_slim and it defines the container image to use.
Then we get to our first stage, the build stage. The name will be used in the GitLab pipeline. There are 2 jobs in this stage, which will run concurrently. They both inherit from the rust_slim template thst we defined earlier and specify a single command to run, cargo build --workspace with --release on the end for the build_release job. That release job also specifies outputs, in this case a path to an artifact, the relese build of the cianity binary.
Our second stage has jobs which are a little more complex. The test job defines 2 steps, first cargo-nextest is installed and then in the second step it is used to run all our tests. Since curl isn't available in the slim image, this job doesn't inherit from the template, nd instead specifies the image directly.
The remaining 2 jobs in the stage also use 2 steps, first they install thr necessary rust conponent and then execute with it.
Each step is converted into a single command line invocation in the GitLab pipeline definition.
The final stage is the cianity chreck that could normally be placed at the beginning of a workflow to ensure that the checked in workflow is correct and that the generated GitLab pipeline configuration matches what has been checked into the repo.