| |
---
|
| |
title: Radicle CI architecture
|
| + |
author: The Radicle Team
|
| |
...
|
| |
|
| |
# Overview
|
| |
|
| + |
[Radicle](https://radicle.xyz/) is a peer-to-peer collaboration system
|
| + |
built on top of the git version control system. Radicle has support
|
| + |
for integrating with continuous integration (CI) systems, using an
|
| + |
architecture where a "broker" listens to events about changes to
|
| + |
repositories stored in a node, and launching the appropriate "adapter"
|
| + |
for each change, according to its configuration.
|
| + |
|
| + |
This means each node can opt into running CI for what projects and
|
| + |
changes according to the interests of the person whose node it is.
|
| + |
|
| + |
* The delegates for a repository might run CI on all patches to make
|
| + |
merge decisions with more confidences.
|
| + |
* Someone whose contributing to a project might only care about
|
| + |
patches they themselves created, and only run CI for those.
|
| + |
* A third party might run CI for projects they use, to know if it's OK
|
| + |
to deploy to production.
|
| + |
|
| + |
Radicle provides its own, very simple "native CI" solution. It's just
|
| + |
good enough for the Radicle project to use itself. In addition, there
|
| + |
are adapters that integrate with external CI systems.
|
| + |
|
| + |
## Components in native CI
|
| + |
|
| |
CI support in Radicle consists of several components. For native CI
|
| |
they are:
|
| |
|
| |
* the external CI instance
|
| |
|
| |
The first three of these run on the same host, but the external CI
|
| - |
instance can run anywhere.
|
| + |
instance can run anywhere. The adapter talks to the CI instance using
|
| + |
whatever protocol the CI instance supports, such as HTTP.
|
| |
|
| - |
The child process is called "the CI adapter" in this document.
|
| - |
|
| - |
CI works like this:
|
| + |
External CI integration works like this:
|
| |
|
| |
* a repository known to the node changes
|
| |
- a git ref is updated
|
| |
- the ref can be a branch, tag, or something else, such as a Radicle
|
| |
COB
|
| |
- the node emits an event describing the change
|
| - |
* the CI broker listens to node event
|
| - |
- the broker subscribers to node events via the node control socket,
|
| + |
* the CI broker listens to node events
|
| + |
- the broker subscribes to node events via the node control socket,
|
| |
which is a Unix domain socket
|
| |
* the CI broker filters events, based on its configuration, and the
|
| |
configuration for the repository involved
|
| |
* for an event that passes its filters, the CI broker spawns the
|
| - |
adapter process
|
| - |
- the adapter process is either the native CI engine, or an
|
| - |
integration with an external CI engine
|
| - |
- the CI broker uses the executable for native CI by default, but
|
| - |
this can be overridden by the broker configuration file, and that
|
| - |
can be overridden by the `.radicle/ci/config.yaml` file in the repository
|
| - |
the event is for
|
| + |
appropriate adapter process
|
| + |
- there are different adapter for different CI implementations
|
| |
* the broker sends a request object to the adapter as a child process,
|
| |
via the child's stdin, and reads any responses from the child's
|
| |
stdout
|
| |
- the request is JSON
|
| |
- the responses are in the form of JSON Lines: a JSON object per
|
| |
line serialized to not contain newline characters
|
| + |
* the adapter communicates with the external CI instance in whatever
|
| + |
way is suitable for that instance
|
| + |
- this is usually over HTTP
|
| + |
- it may involve the CI instance making a web hook request back to
|
| + |
the adapter
|
| |
|
| - |
## Native CI
|
| - |
|
| - |

|
| + |

|
| |
|
| - |
## External CI
|
| - |
|
| - |

|
| + |

|
| |
|
| |
# The adapter
|
| |
|
| |
specific commit in a repository, and responds with the id of the run,
|
| |
then later with the status of the finished run.
|
| |
|
| - |
Note: this is for the first version. This will be expanded later with
|
| - |
other requests and responses, as needed.
|
| - |
|
| |
For native CI, the adapter actually is the CI engine and performs the
|
| |
CI run itself. For external CI, the adapter process does whatever it
|
| |
needs to do to get the external CI engine instance to perform the CI
|
| |
External CI engines allow complex pipelines to be written and support
|
| |
a variety of workflows. Different jobs or tasks can be triggered
|
| |
based on different events (e.g. `push`, `patch created`, `patch
|
| - |
updated`, etc.), to satisfy different workflow needs. Some examples
|
| - |
of real-world use-cases:
|
| + |
updated`, etc.), to satisfy different workflow needs. (Only a few of
|
| + |
these are actually implemented yet, but the scaffolding to support
|
| + |
more is there.)
|
| + |
|
| + |
Some examples of real-world use-cases:
|
| |
|
| |
- trigger "fast" tests on every push to any branch
|
| |
- trigger "relatively fast" tests only when a Patch is created /
|
| |
Note: the JSON objects below are formatted on multiple lines to make
|
| |
them easier to read. The actual wire format is one line per message.
|
| |
|
| + |
Run the `broker-messages` binary to get actual examples produced by
|
| + |
code.
|
| + |
|
| + |
~~~{.sh .numberLines}
|
| + |
$ cargo run -q --bin broker-messages
|
| + |
Trigger request:
|
| + |
{"request":"trigger","event_type":"push","repository":{"id":"rad:zwTxygwuz5LDGBq255RA2CbNGrz8","name":"radicle-ci-broker","description":"Radicle CI broker","private":false,"default_branch":"main","delegates":["did:key:z6MkgEMYod7Hxfy9qCvDv5hYHkZ4ciWmLFgfvm3Wn1b2w2FV"]},"pusher":{"id":"did:key:z6MkgEMYod7Hxfy9qCvDv5hYHkZ4ciWmLFgfvm3Wn1b2w2FV","alias":"liw"},"before":"b4fb1e347be7db19f0859717062f94116b5bec9f","after":"b4fb1e347be7db19f0859717062f94116b5bec9f","branch":"patches/8d8232ddcb217fa1402eec4d955e227ef3bb5881","commits":[]}
|
| + |
|
| + |
Triggered response:
|
| + |
{"response":"triggered","run_id":{"id":"any-string-works-as-run-id"}}
|
| + |
|
| + |
Successful response:
|
| + |
{"response":"finished","result":"success"}
|
| + |
|
| + |
Failure response:
|
| + |
{"response":"finished","result":"failure"}
|
| + |
|
| + |
Error response:
|
| + |
{"response":"finished","result":{"error":"error message\nsecond line"}}
|
| + |
~~~
|
| |
|
| |
## Push Event Request
|
| |
|
| |
- `<RID>` is the repository ID, in its `rad:` URN format,
|
| |
- `<BRANCH_NAME>` is the branch name where the push occurred,
|
| |
- `<AFTER_COMMIT>` is the commit id of the last commit being pushed,
|
| - |
- `<BEFORE_COMMIT>` is the commit id of the **parent** of the first
|
| - |
|
| - |
commit being pushed (i.e. ` <SOME_OTHER_COMMIT_BEING_PUSHED>`),
|
| - |
(the SHA checksum).
|
| + |
- `<BEFORE_COMMIT>` is the commit id of the **parent** of the first
|
| + |
commit being pushed (i.e. ` <SOME_OTHER_COMMIT_BEING_PUSHED>`),
|
| + |
(the SHA checksum).
|
| |
|
| |
The `request` fields allows us to extend this in the future.
|
| |
|
| |
|
| |
- `<RID>` is the reposiatory ID, in its `rad:` URN format,
|
| |
- `<AFTER_COMMIT>` is the commit id of the last commit being pushed,
|
| - |
- `<BEFORE_COMMIT>` is the commit id of the **parent** of the first
|
| - |
|
| - |
commit being pushed (i.e. ` <SOME_OTHER_COMMIT_BEING_PUSHED>`),
|
| - |
(the SHA checksum).
|
| + |
- `<BEFORE_COMMIT>` is the commit id of the **parent** of the first
|
| + |
commit being pushed (i.e. ` <SOME_OTHER_COMMIT_BEING_PUSHED>`),
|
| + |
(the SHA checksum).
|
| |
|
| |
The `request` fields allows us to extend this in the future.
|
| |
|
| |
~~~
|
| |
|
| |
where `<STATUS>` is either the string `success` or `failure`. Note
|
| - |
that the run id is not repeated as the context makes this clear.
|
| + |
that the run id is not repeated as the context makes this clear: the
|
| + |
response comes from the same process, via the same stdout pipe, as the
|
| + |
previous message.
|
| + |
|
| + |
# Report generation
|
| + |
|
| + |
The CI broker has an SQLite database file for persistent storage of
|
| + |
information of the CI runs it triggers. This is used to generate
|
| + |
report pages.
|
| + |
|
| + |
The report pages are HTML, generated from the information in the
|
| + |
database. The broker loads information for all runs when it starts,
|
| + |
from the database, and then pushed information about new runs when
|
| + |
they happens. This avoids the broker having to read everything every
|
| + |
time it updates the report pages.
|
| + |
|
| + |
The report page generation is done in its own thread, separate from
|
| + |
the main thread of the CI broker. This allows the reporting to happens
|
| + |
independently from what the main thread is doing. In particular, it
|
| + |
means the main thread does not need to do anything to trigger reports
|
| + |
from being updated.
|
| + |
|
| + |
When it comes to per-run logs, for external CI these are kept by the
|
| + |
external CI instance and the broker never sees them. For native CI,
|
| + |
the native CI adapter writes them to the report directory, as
|
| + |
`$RUNID/log.html`, and the broker generates report pages that link to
|
| + |
those files.
|