I've long found it frustrating setting up CI for projects. For a while I've been thinking about what my ideal CI system would look like. Lately I've started wondering if this would work well as a Roc platform.
Below are a couple of problems I've encountered with CI systems I'm familiar with:
Developing a CI harnas has consistenly been an unpleasant development experience.
I'm familiar with systems configured using a mix of Yaml, Bash, and Docker.
Local development often uses different scripts than CI.
Based on the above, my ideal CI system would include the following:
I'm curious if the above resonates with anyone. Some questions (but other questions and thoughts are welcome too):
I definitely resonate with the issues you mention and think Roc would work very well as part of a solution! Similar sentiments have come up before on Zulip. I was about to link to the CI issue Greg created on the book repo but I see that you already found it :smiley:. I think basic-cli would work well as a starting point at least
Reminds me of some work on rbt (Roc Build Tool) which was going to be a build system with roc as the configuration language
https://github.com/roc-lang/rbt
I dont think it's been worked on for quite a while, but just sharing in case this helps in any way.
I totally agree (I made a similar thread here before) for related research I'd suggest checking out dagger.io and occurrent which Ocaml-ci is built on https://github.com/ocurrent/ocurrent
The Ocurrent authors have some great blog posts about how it works internally, very interesting stuff
Some additional thoughts on design of a roc-ci platform.
I think compatibility with platforms like Github Actions is important.
A lot of small projects use free CI platforms because they're free,
having to set up CI servers (and pay for them) would be a deal breaker for these projects.
Another thing important to me is making it possible to run Roc CI locally.
Both goals could be achieved by having separate concepts of job specifications and runners.
The CI job specification is the Roc code describing the steps CI should execute.
The roc-ci platform comes with runners for executing that job specification in different places.
Examples of runners:
The Github Actions runner takes a job specification and produces a Github actions-compatible workflow.
Similar runners could be created for other Cloud CI platforms.
The local runner runs a CI job locally.
Another type of runner to consider is one modeled after Jenkins CI.
Jenkins is self-hosted CI software where you bring your own CI machines that Jenkins joins in a cluster.
It's appealing to have a self-hosted option that does not require running a cluster, a considerable cost.
However, supporting a Jenkins-like CI model adds a lot of requirements the other runner types don't have:
I think the effort it would take to do that correctly might be better directed at creating a Kubernetes alternative.
I'd love a simpler alternative to Kubernetes and even Nomad, and roc-ci could run on top of it.
Scrapping the Jenkins model as a design goal for roc-ci would reduce scope for the project a lot.
Scrapping it as a design goal for roc-ci itself though
What does "it" refer to exactly?
Anton said:
Scrapping it as a design goal for roc-ci itself though
What does "it" refer to exactly?
I meant designing for a Jenkins-like CI setup, where you bring your own servers and the CI software turns them into a CI cluster.
I'll update the message above to clarify. Thanks!
Yeah, not designing for the Jenkins-like setup sounds wise
I've been doing some design experiments on the train today. Here's an example of what a pipeline definition might look like:
https://github.com/jwoudenberg/roc-ci/blob/main/package/Example.roc
(some but not a lot of implementation/validation in other files in that repo)
My goal here was to let steps be plain Task-returning roc functions. The pipeline composes these together into a DAG, but without allowing access to values that will later be passed between steps. Individual steps might run in different machines/containers, so having logic run in the space between them would be tricky implementation-wise (where would that logic run?).
If this is going to become a platform, some of this Roc code might have to be rewritten in a different language eventually. Not doing that now because (1) I like prototyping in Roc, and (2) this is also for the Roc book.
Feedback very welcome!
I wonder if you could use RecordBuilder to help here?
I'm not an expert with applicative functors, but I feel like there is a design somewhere here which might be nice
yeah I think record builder would be a good fit for a build system because then you can generate a graph of it without having to run it! :smiley:
That's exactly what I came here to say. Having a representation of the pipeline that can then be used to generate a gui and visualisations of what steps failed and such is super important
Also, it would be super to have a more real-world example of record builders, so if this works out for CI then that will be a nice use case to show people I think.
Thanks! I definitely want the applicative property of being able to get a representation of the pipeline without needing to run it. One other place where that will come in useful: for generating .github/workflow output files.
I think what I currently have has this applicative property, but it's hard to recognize the record builder pattern in it because the state used during construction needs to account for types between steps, i.e. if you want to run step2 after step1, the return type of step1 needs to be the same as the type of an argument of step2. I'm going to ponder a bit if there's ways to make the record builder pattern more appearant in the code and/or prose for the chapter.
An observation: A common thread in your comments is the leap from "want to get a dependency graph without running any steps" to "applicatives/record builders". I'm wondering if it's a good idea to teach that thought process in the chapter, or whether it'd become too abstract.
It took some helpful guidance from @Agus Zubiaga where he walked me through the RecordBuilder before I understood what was going on.
If you can find a concrete use case that fits well with it, then I think it could be a great way to talk about the concept.
It is more advanced than some topics and involves understanding types. Maybe that would make it a good candidate for a later chapter?
it's hard to recognize the record builder pattern in it because the state used during construction needs to account for types between steps, i.e. if you want to run step2 after step1, the return type of step1 needs to be the same as the type of an argument of step2
I would say this is a good reason why you would reach for the record builder. You use it to make a type safe API by building up the state in steps and each step can have a different type.
I'm definitely not an expert here, as I only recently learnt about these myself and wrote that guide to remember how they work.
Last updated: Jun 16 2026 at 16:19 UTC