Radish alpha
r
rad:z371PVmDHdjJucejRoRYJcDEvD5pp
Radicle website including documentation and guides
Radicle
Git
add blog post by Lars on how he uses Radicle and its CI
Lars Wirzenius committed 9 months ago
commit b1a9df5f3dc85dbf982afb384e73541efe63f49c
parent 5bb3188
4 files changed +531 -0
added _posts/2025-07-23-using-radicle-ci-for-development.md
@@ -0,0 +1,503 @@
+
---
+
title: "Using Radicle CI for Development"
+
subtitle: "In this blog post I show how I use Radicle and its CI support for my own software development."
+
author: "lars"
+
authorUrl: "https://app.radicle.xyz/nodes/seed.radicle.xyz/users/did:key:z6MkgEMYod7Hxfy9qCvDv5hYHkZ4ciWmLFgfvm3Wn1b2w2FV"
+
layout: "blog"
+
---
+

+
In this blog post I show how I use Radicle and its CI support for my
+
own software development. I show how I start a project, add it to
+
Radicle, add CI support for it, and manage patches and issues.
+

+
I have been working full time on Radicle CI for a couple of years now.
+
All my personal Git repositories are hosted on Radicle. Radicle CI is
+
the only CI I now use.
+

+
There are instructions to install the software I mention here at the
+
end.
+

+
These days, I'm not a typical software developer. I usually work in
+
Emacs and the command line instead of an IDE. In this blog post I'll
+
concentrate on the parts of my development process that relate to
+
Radicle, and not my other tooling.
+

+
# Overview of Radicle CI
+

+
The Radicle node process opens a Unix domain socket to which it sends
+
events describing changes in the node. One of these events represents
+
changes to a repository in the node's storage.
+

+
<img src="/assets/images/blog/components.svg" class="screenshot" style="background-color: #f5f5ff;"/>
+

+
Support for CI in Radicle is built around the repository change event.
+
The Radicle CI broker (`cib`), listens for the events and matches them
+
against its configuration to decide when to run CI. The node operator
+
gets to decide for what repositories they run CI.
+

+
The CI broker does not itself run CI. It invokes a separate program,
+
the "adapter", which is given the event that triggered CI. The adapter either
+
executes the run itself, or uses an external CI system to execute it.
+
This allows Radicle to support a variety of CI systems, by writing a
+
simple adapter for each.
+

+
I have written a CI engine for myself,
+
[Ambient](https://ambient.liw.fi/), and the adapter for that
+
(`radicle-ci-ambient`), and that is what I use.
+

+
There are adapters for running CI locally on the host or in a
+
container, GitHub actions, Woodpecker, and several others. See [`CI
+
broker
+
README.md`](https://app.radicle.xyz/nodes/radicle.liw.fi/rad:zwTxygwuz5LDGBq255RA2CbNGrz8/tree/README.md)
+
and [integration
+
documentation](https://explorer.radicle.gr/nodes/seed.radicle.gr/rad:z4Uh671FzoooaHjLvmtW9BtGMF9qm)
+
for a more complete list. The adapter interface is intentionally easy
+
to implement: it needs to read one line of JSON and write up to two
+
lines of JSON.
+

+
# The sample project
+

+
This blog post is about Radicle, so I'm going to use a "hello world"
+
program as an example. This avoids getting mired into the details of
+
implementing something useful.
+

+
First I create a Git repository with a Rust project. I choose Rust,
+
because I like Rust, but the programming language is irrelevant here.
+

+
```
+
$ cargo init liw-hello
+
    Creating binary (application) package
+
... some text removed
+
$ cd liw-hello
+
$ git add .
+
$ git commit -m "chore: cargo init"
+
[main (root-commit) 5037847] chore: cargo init
+
 3 files changed, 10 insertions(+)
+
 create mode 100644 .gitignore
+
 create mode 100644 Cargo.toml
+
 create mode 100644 src/main.rs
+
```
+

+
Then I edit the `src/main.rs` file to have some useful content,
+
including unit tests:
+

+
```
+
fn main() {
+
    let greeting = Greeting::default()
+
        .greeting("hello")
+
        .whom("world");
+
    println!("{}", greeting.greet());
+
}
+

+
struct Greeting {
+
    greeting: String,
+
    whom: String,
+
}
+

+
impl Default for Greeting {
+
    fn default() -> Self {
+
        Self {
+
            greeting: "howdy".into(),
+
            whom: "partner".into(),
+
        }
+
    }
+
}
+

+
impl Greeting {
+
    fn greeting(mut self, s: &str) -> Self {
+
        self.greeting = s.into();
+
        self
+
    }
+

+
    fn whom(mut self, s: &str) -> Self {
+
        self.whom = s.into();
+
        self
+
    }
+

+
    fn greet(&self) -> String {
+
        format!("{} {}", self.greeting, self.whom)
+
    }
+
}
+

+
#[cfg(test)]
+
mod test {
+
    use super::*;
+

+
    #[test]
+
    fn default() {
+
        let g = Greeting::default();
+
        assert!(!g.greeting.is_empty());
+
        assert!(!g.whom.is_empty());
+
    }
+

+
    #[test]
+
    fn sets_greeting() {
+
        let g = Greeting::default().greeting("hi");
+
        assert_eq!(g.greet(), "hi partner");
+
    }
+

+
    #[test]
+
    fn sets_whom() {
+
        let g = Greeting::default().whom("there");
+
        assert_eq!(g.greet(), "howdy there");
+
    }
+
}
+
```
+

+
To commit that, I actually use Emacs with Magit for this, but I also often use
+
the command line, which I show here.
+

+
```
+
git commit -am "feat: implement greeting"
+
```
+

+
Once I have a Git repository with at least one commit, I can create a
+
Radicle repository for that. I do that on the command line. The `rad
+
init` command asks the user some questions. The answers could be
+
provided via option, which is useful for testing, but not something I
+
usually do when using the program.
+

+
```
+
$ rad init
+

+
Initializing radicle ๐Ÿ‘พ repository in /home/liw/radicle/liw-hello..
+

+
โœ“ Name liw-hello
+
โœ“ Description Sample program for blog post about Radicle and its CI
+
โœ“ Default branch main
+
โœ“ Visibility public
+
โœ“ Repository liw-hello created.
+

+
Your Repository ID (RID) is rad:z3dhWQMH8J6nX3Qo97o5oSFMTfgyr.
+
You can show it any time by running `rad .` from this directory.
+

+
โ—ค Uploaded to z6MksCgjxU4VZt6qgtZntdikhtXFbsfvKRLPzpKtfCY4rAHR, 0 peer(s) remaining..
+
โœ“ Repository successfully synced to z6MksCgjxU4VZt6qgtZntdikhtXFbsfvKRLPzpKtfCY4rAHR
+
โœ“ Repository successfully synced to 1 node(s).
+

+
Your repository has been synced to the network and is now discoverable by peers.
+
Unfortunately, you were unable to replicate your repository to your preferred seeds.
+
To push changes, run `git push`.
+
```
+

+
There you go. I now have a Radicle repository to play with. As of
+
publishing this blog post, the repository is alive on the Radicle
+
network, if you want to [look at
+
it](https://app.radicle.xyz/nodes/radicle.liw.fi/rad:z3dhWQMH8J6nX3Qo97o5oSFMTfgyr)
+
or clone it.
+

+
# CI configuration in the repository
+

+
To use Radicle CI with Ambient, I need to create
+
`.radicle/ambient.yaml`:
+

+
```
+
plan:
+
  - action: cargo_clippy
+
  - action: cargo_test
+
```
+

+
This tells Ambient to run `cargo clippy` and `cargo test`, albeit with
+
additional command line arguments.
+

+
This is specific to Ambient, and the Ambient adapter for Radicle CI,
+
but similar files are needed for every CI system. The Radicle CI
+
broker does not try hide this variance: it's important that you, as
+
the developer using a specific CI system, get full access to it, even
+
when you use it through Radicle CI. If the CI broker added a layer
+
above that it would only cause confusion and irritation.
+

+
# Running CI locally
+

+
I find the most frustrating part of using CI to be to wait for a CI
+
run to finish on a server and then try to deduce from the run log what
+
went wrong. I've alleviated this by writing an extension to `rad` to
+
run CI locally:
+
[`rad-ci`](https://app.radicle.xyz/nodes/radicle.liw.fi/rad%3Az6QuhJTtgFCZGyQZhRMZmZKJ3SVG).
+
It can produce a huge amount of output, so I've abbreviated that
+
below.
+

+
`rad` supports extensions like `git` does: if you run `rad foo` and
+
`foo` isn't built into `rad`, then `rad` will try to run `rad-foo`
+
instead. `rad-ci` can thus be invoked as `rad ci`, which I use in the
+
example below.
+

+
```
+
$ rad ci
+
...
+
    RUN: Action CargoClippy
+
    SPAWN: argv=["cargo", "clippy", "--offline", "--locked", "--workspace", "--all-targets", "--no-deps", "--", "--deny", "warnings"]
+
           cwd=/workspace/src (exists? true)
+
           extra_env=[("CARGO_TARGET_DIR", "/workspace/cache"), ("CARGO_HOME", "/workspace/deps"), ("PATH", "/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")]
+
        Checking liw-hello v0.1.0 (/workspace/src)
+
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.15s
+
    RUN: Action finished OK
+
    RUN: Action CargoTest
+
    SPAWN: argv=["cargo", "test", "--offline", "--locked", "--workspace"]
+
           cwd=/workspace/src (exists? true)
+
           extra_env=[("CARGO_TARGET_DIR", "/workspace/cache"), ("CARGO_HOME", "/workspace/deps"), ("PATH", "/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")]
+
       Compiling liw-hello v0.1.0 (/workspace/src)
+
        Finished `test` profile [unoptimized + debuginfo] target(s) in 0.18s
+
         Running unittests src/main.rs (/workspace/cache/debug/deps/liw_hello-9c44d33bbe6cdc80)
+

+
    running 3 tests
+
    test test::default ... ok
+
    test test::sets_greeting ... ok
+
    test test::sets_whom ... ok
+

+
    test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
+

+
    RUN: Action finished OK
+
    RUN: Action TarCreate {
+
        archive: "/dev/vde",
+
        directory: "/workspace/cache",
+
    }
+
    RUN: Action finished OK
+
    RUN: Action TarCreate {
+
        archive: "/dev/vdd",
+
        directory: "/workspace/artifacts",
+
    }
+
    RUN: Action finished OK
+
    ambient-execute-plan ends
+
    EXIT CODE: 0
+
    [2025-07-04T05:48:23Z INFO  ambient] ambient ends successfully
+

+
Everything went fine.
+
```
+

+
(I've used the voluminous output to help debug `rad-ci`, but now that
+
it is stable, I should reduce the volume by default. A cobbler's
+
children may have no shoes but a programmer's tool has unnecessary
+
debug output.)
+

+
I find this ability to emulate what happens in CI on a server to be
+
very useful. To start with, I can use the resources I have locally, on
+
my laptop. I don't need to compete with the shared server with other
+
people. I don't have to wait for the CI server to have time for me. I
+
also don't need to commit changes, which is another little source of
+
friction removed from the edit-ci-debug cycle.
+

+
For Ambient I intend to add support when it's run locally (as `rad-ci`
+
does), and there's a failure, the developer can log into the
+
environment and have hands-on access. This will make debugging a
+
failure under CI much easier than pushing changes to add more output
+
to the run log to help figure out what the problem is. But that isn't
+
implemented yet: I only have 86400 seconds per day, most days.
+

+
# CI configuration on my CI node
+

+
I love being able to run CI locally, but it is not sufficient. One
+
important aspect of a shared CI is that everyone uses the same
+
environment, with the same versions of everything. A server can also
+
deliver or deploy changes, as needed.
+

+
I've configured a second node, [ci0](https://ci0.liw.fi/), where I run
+
the CI broker and Ambient for all the public projects I have or
+
participate in. The actual server is a small desktop PC I have, which
+
is quiet and uses fairly little power, especially when idle. The HTML
+
report pages get published on a public server, for the amusement of
+
others.
+

+
My CI broker configuration is such that I don't need to change it for
+
every new project. I only need to make sure the repository is on the
+
CI node, and the repository has a `.radicle/ambient.yaml` file.
+

+
To seed, I run this on the CI node:
+

+
```
+
rad seed rad:z3dhWQMH8J6nX3Qo97o5oSFMTfgyr
+
```
+

+
That's the repository ID for my sample project. I run `rad .` in the
+
working directory to find out what it is. Because finding out the ID
+
is so easy, I never bother to make note of it when creating a repository.
+

+
# Reporting an issue
+

+
The `rad` tool can open issues from the command line, but for issue
+
management I've moved to using [the desktop
+
application](https://radicle.xyz/desktop). In the screenshot below I
+
open an issue about the default greeting.
+

+
<img src="/assets/images/blog/radicle-desktop-new-issue-scaled.png" class="screenshot"/>
+

+
In the above picture I show how I open a new issue for the sample
+
repository, saying the greeting is not the usual "hello world"
+
greeting.
+

+
# Making a change
+

+
To make a change to the project, I make a branch, commit some changes,
+
then create a Radicle patch.
+

+
```
+
$ git switch -c change
+
Switched to a new branch 'change'
+
$ git commit -am "feat: change greeting"
+
[change d19c898] feat: change greeting
+
 1 file changed, 2 insertions(+), 2 deletions(-)
+
$ git push rad HEAD:refs/patches
+
โœ“ Patch fd552417cc9a66c6aac1b6c8c717996bea741bfd opened
+
โœ“ Synced with 11 seed(s)
+

+
 * [new reference]   HEAD -> refs/patches
+
```
+

+
The last command above pushes the branch to Radicle, via the special
+
`rad` remote, and instructs the `rad` Git remote helper to create a
+
Radicle patch instead of a branch. The `refs/patches` name is special
+
and magic. The `git-remote-rad` helper program understands it as a
+
request to create a new patch.
+

+
This makes a change in the local node, which by default then
+
automatically syncs it with other nodes it's connected to, if they
+
have the same repository. My laptop node is connected to the CI node,
+
so that happens immediately.
+

+
As soon as the new patch lands in the CI node, the CI broker triggers
+
a new CI run, which fails. I can go to the [web page updated by the CI
+
broker](https://ci0.liw.fi/z3dhWQMH8J6nX3Qo97o5oSFMTfgyr.html) and see
+
what the problem is. The patch diff is:
+

+

+
```
+
diff --git a/src/main.rs b/src/main.rs
+
index a79818f..216bab7 100644
+
--- a/src/main.rs
+
+++ b/src/main.rs
+
@@ -11,8 +11,8 @@ struct Greeting {
+
 impl Default for Greeting {
+
     fn default() -> Self {
+
         Self {
+
-            greeting: "howdy".into(),
+
-            whom: "partner".into(),
+
+            greeting: "hello".into(),
+
+            whom: "world".into(),
+
         }
+
     }
+
 }
+
```
+

+
The problem is that tests assume the original default:
+

+
```
+
---- test::sets_greeting stdout ----
+

+
thread 'test::sets_greeting' panicked at src/main.rs:50:9:
+
assertion `left == right` failed
+
  left: "hi world"
+
 right: "hi partner"
+
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
+

+
---- test::sets_whom stdout ----
+

+
thread 'test::sets_whom' panicked at src/main.rs:56:9:
+
assertion `left == right` failed
+
  left: "hello there"
+
 right: "howdy there"
+

+

+
failures:
+
    test::sets_greeting
+
    test::sets_whom
+

+
test result: FAILED. 1 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
+
```
+

+
I change the tests, run the tests locally, run `rad ci` locally, and
+
commit the fix..
+

+
I then push the fix to the patch. The push default for this branch was
+
set to the Radicle patch, which makes pushing easier.
+

+
```
+
$ git push
+
โœ“ Patch fd55241 updated to revision 8d1f8c69dc0f8028d8b1bb9e336240febaf2d1f4
+
To compare against your previous revision 3180ddd, run:
+

+
   git range-diff c3f02b43830578c93edd83a23ee2902899fdb159 17cda244d2e78bdeffd0647b20f315726bebf605 2a82eb0326179b60664ffeeac3ee062a5adfdcd6
+

+
โœ“ Synced with 13 seed(s)
+

+
  https://app.radicle.xyz/nodes/ci0/rad:z3dhWQMH8J6nX3Qo97o5oSFMTfgyr/patches/fd552417cc9a66c6aac1b6c8c717996bea741bfd
+

+
To rad://z3dhWQMH8J6nX3Qo97o5oSFMTfgyr/z6MkgEMYod7Hxfy9qCvDv5hYHkZ4ciWmLFgfvm3Wn1b2w2FV
+
   17cda24..2a82eb0  change -> patches/fd552417cc9a66c6aac1b6c8c717996bea741bfd
+
```
+

+
I wait for CI to run. It is a SUCCESS!
+

+
I still need to merge the fix to the `main` branch. This will also
+
automatically mark the branch as merged for Radicle.
+

+
```
+
$ rad patch
+
โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+
โ”‚ โ—  ID       Title                                    Author         Reviews  Head     +    -   Updatโ€ฆ โ”‚
+
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
+
โ”‚ โ—  fd55241  ci: add configuration Radicle + Ambient  liw     (you)  -        2a82eb0  +14  -4  1 minโ€ฆ โ”‚
+
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
$ git switch main
+
Switched to branch 'main'
+
$ git merge change
+
Updating 54d2c9c..2a82eb0
+
Fast-forward
+
 Cargo.lock  | 7 +++++++
+
 src/main.rs | 8 ++++----
+
 2 files changed, 11 insertions(+), 4 deletions(-)
+
 create mode 100644 Cargo.lock
+
$ git push
+
โœ“ Patch fd552417cc9a66c6aac1b6c8c717996bea741bfd merged
+
โœ“ Canonical head updated to 2a82eb0326179b60664ffeeac3ee062a5adfdcd6
+
โœ“ Synced with 13 seed(s)
+

+
  https://app.radicle.xyz/nodes/ci0/rad:z3dhWQMH8J6nX3Qo97o5oSFMTfgyr/tree/2a82eb0326179b60664ffeeac3ee062a5adfdcd6
+

+
To rad://z3dhWQMH8J6nX3Qo97o5oSFMTfgyr/z6MkgEMYod7Hxfy9qCvDv5hYHkZ4ciWmLFgfvm3Wn1b2w2FV
+
   c3f02b4..2a82eb0  main -> main
+
$ rad patch
+
Nothing to show.
+
$ delete-merged
+
Deleted branch change (was 2a82eb0).
+
```
+

+
(The last command is a little helper script that deletes any local
+
branches that have been merged into the default branch. I don't like
+
to have a lot of merged branches around to confuse me.)
+

+
I could have avoided this round trip via the server by running `rad
+
ci`, or at least `cargo test`, before creating the patch, but I was
+
confident that I can't make a mistake in an example this simple. This
+
is why CI is needed: to keep in control the hubris of someone who has
+
been programming for decades.
+

+
# Installing
+

+
To install Radicle itself, the [official
+
instructions](https://radicle.xyz/#get-started) will get you `rad` and
+
`radicle-node`. The [Radicle desktop
+
application](https://radicle.xyz/desktop) has it's own installation
+
instructions.
+

+
There are instructions for installing [Radicle CI (for
+
Debian)](https://radicle-ci.liw.fi/radicle-ci-broker/userguide.html#installing-radicle-ci-with-ambient-on-debian),
+
but not other systems, since I only use Debian. I would very much
+
appreciate help with expanding that documentation.
+

+
It's probably easiest to install
+
[`rad-ci`](https://app.radicle.xyz/nodes/radicle.liw.fi/rad:z6QuhJTtgFCZGyQZhRMZmZKJ3SVG)
+
from source code or with `cargo install`, but I have a `deb` package
+
for those using Debian or derivatives in my [APT
+
repository](http://apt.liw.fi/).
+

+
# Conclusion
+

+
I've used CI systems since 2010, starting with Jenkins, just after it
+
got renamed from Hudson. I've written about four CI engines myself,
+
depending on how you count rewrites. With Radicle and Ambient I am
+
finally getting to a development experience where CI is not actively
+
irritating, even if is not yet fun.
+

+
A CI system that's a joy to use, that sounds like a fantasy. What
+
would it even be like? What would make using a CI system joyful to
+
you?
added assets/images/blog/components.svg
@@ -0,0 +1,27 @@
+
<svg xmlns='http://www.w3.org/2000/svg' viewBox="0 0 427.582 364.32">
+
<ellipse cx="56.16" cy="38.16" rx="54" ry="36"  style="fill:rgb(245,245,220);stroke-width:2.16;stroke:rgb(0,0,0);" />
+
<text x="56.16" y="38.16" text-anchor="middle" fill="rgb(0,0,0)" dominant-baseline="central">radicle-node</text>
+
<ellipse cx="308.16" cy="38.16" rx="54" ry="36"  style="fill:rgb(240,255,255);stroke-width:2.16;stroke:rgb(0,0,0);" />
+
<text x="308.16" y="38.16" text-anchor="middle" fill="rgb(0,0,0)" dominant-baseline="central">cib</text>
+
<path d="M254.16,218.16L362.16,218.16L362.16,146.16L254.16,146.16Z"  style="fill:rgb(240,255,255);stroke-width:2.16;stroke:rgb(0,0,0);" />
+
<text x="308.16" y="182.16" text-anchor="middle" fill="rgb(0,0,0)" dominant-baseline="central">adapter</text>
+
<path d="M254.16,362.16L362.16,362.16L362.16,290.16L254.16,290.16Z"  style="fill:rgb(127,255,212);stroke-width:2.16;stroke:rgb(0,0,0);" />
+
<text x="308.16" y="306" text-anchor="middle" fill="rgb(0,0,0)" dominant-baseline="central">external</text>
+
<text x="308.16" y="326.16" text-anchor="middle" fill="rgb(0,0,0)" dominant-baseline="central">CI</text>
+
<text x="308.16" y="346.32" text-anchor="middle" fill="rgb(0,0,0)" dominant-baseline="central">system</text>
+
<polygon points="254.16,38.16 246.442,41.0544 246.442,35.2656" style="fill:rgb(0,0,0)"/>
+
<path d="M110.16,38.16L250.301,38.16"  style="fill:none;stroke-width:1.4472;stroke:rgb(0,0,0);" />
+
<text x="182.16" y="26.9946" text-anchor="middle" fill="rgb(0,0,0)" dominant-baseline="central">node</text>
+
<text x="182.16" y="49.3254" text-anchor="middle" fill="rgb(0,0,0)" dominant-baseline="central">event</text>
+
<polygon points="254.16,146.16 247.432,141.398 252.222,138.148" style="fill:rgb(0,0,0)"/>
+
<path d="M269.976,63.6158Q220.796,96.9798 251.993,142.966"  style="fill:none;stroke-width:1.4472;stroke:rgb(0,0,0);stroke-dasharray:7.2,7.2;" />
+
<text x="215.551" y="96.8239" text-anchor="middle" fill="rgb(0,0,0)" font-size="80%" dominant-baseline="central">stdin</text>
+
<text x="215.551" y="112.952" text-anchor="middle" fill="rgb(0,0,0)" font-size="80%" dominant-baseline="central">trigger</text>
+
<polygon points="346.344,63.6158 354.356,65.5538 351.106,70.3442" style="fill:rgb(0,0,0)"/>
+
<path d="M362.16,146.16Q395.524,96.9798 349.537,65.7824"  style="fill:none;stroke-width:1.4472;stroke:rgb(0,0,0);stroke-dasharray:7.2,7.2;" />
+
<text x="398.419" y="96.8239" text-anchor="middle" fill="rgb(0,0,0)" font-size="80%" dominant-baseline="central">stdout</text>
+
<text x="398.419" y="112.952" text-anchor="middle" fill="rgb(0,0,0)" font-size="80%" dominant-baseline="central">result</text>
+
<polygon points="308.16,290.16 305.266,282.442 311.054,282.442" style="fill:rgb(0,0,0)"/>
+
<polygon points="308.16,218.16 311.054,225.878 305.266,225.878" style="fill:rgb(0,0,0)"/>
+
<path d="M308.16,286.301L308.16,222.019"  style="fill:none;stroke-width:1.4472;stroke:rgb(0,0,0);stroke-dasharray:7.2,7.2;" />
+
</svg>
added assets/images/blog/radicle-desktop-new-issue-scaled.png
modified index.md
@@ -131,6 +131,7 @@ updated, join our community on ๐Ÿ’ฌ [Zulip][zulip], or <a href="{{ site.feed.pat

## Blog

+
- 23.07.2025 [Using Radicle CI for Development](/2025/07/23/using-radicle-ci-for-development.html)
- 30.05.2025 [How we used Radicle with GitHub Actions](/2025/05/30/radicle-with-github-actions.html)

# Feedback