Files
radicle-ci-broker/ci-broker.md
T
Lars Wirzenius 0e5c1d7dc9 refactor(ci-broker.md): rename trigger.json to be more correct
It's a "refsFetched" node event, not a trigger message. The name was
the last remaining remnant of an earlier approach to verifying the CI
broker.

Signed-off-by: Lars Wirzenius <liw@liw.fi>
2024-07-08 17:35:06 +03:00

20 KiB

Introduction

This document describes the acceptance criteria for the Radicle CI broker, as well as how to verify that they are met. Acceptance criteria here means a requirement that must be met for the software to be acceptable to its stakeholders.

This file is used by Subplot to generate and run test code as part of running cargo test.

Data files shared between scenarios

Broker configuration

db: ci-broker.db
report_dir: reports
default_adapter: mcadapterface
adapters:
  mcadapterface:
    command: ./adapter.sh
    env:
      RADICLE_NATIVE_CI: native-ci.yaml
    sensitive_env: {}
filters:
  - !Branch "main"
db: ci-broker.db
report_dir: reports
default_adapter: mcadapterface
adapters:
  mcadapterface:
    command: ./adapter.sh
    env:
      RADICLE_NATIVE_CI: native-ci.yaml
    sensitive_env: {}
filters:
  - !Branch "this-branch-does-not-exist"

A dummy adapter

This adapter does nothing, just reports a run ID and a successful run.

#!/bin/bash
set -euo pipefail
echo '{"response":"triggered","run_id":{"id":"xyzzy"}}'
echo '{"response":"finished","result":"success"}'

A refsFetched node event

This is a node event from the node to signal that some git refs have changed in repository.

{
  "type": "refsFetched",
  "remote": "z6MkgEMYod7Hxfy9qCvDv5hYHkZ4ciWmLFgfvm3Wn1b2w2FV",
  "rid": "rad:zwTxygwuz5LDGBq255RA2CbNGrz8",
  "updated": [
    {
      "updated": {
        "name": "refs/namespaces/DUMMYNID/refs/heads/main",
        "old": "0000000000000000000000000000000000000000",
        "new": "0000000000000000000000000000000000000000"
      }
    }
  ]
}

A shutdown event

This asks the CI broker to shut down cleanly.

{
  "type": "refsFetched",
  "remote": "z6MkgEMYod7Hxfy9qCvDv5hYHkZ4ciWmLFgfvm3Wn1b2w2FV",
  "rid": "rad:zwTxygwuz5LDGBq255RA2CbNGrz8",
  "updated": [
    {
      "skipped": {
        "name": "shutdown",
        "oid": "0000000000000000000000000000000000000000"
      }
    }
  ]
}

Set rid in refsFetched event

This is a helper script that reads a refsFetched event and changes its rid field to be the id of the given repository. It also sets the name field for updated refs to include the repository ID. It writes the message back to its file.

#!/usr/bin/python3

import json, sys
from subprocess import run, PIPE

filename = sys.argv[1]
cwd = sys.argv[2]

p = run(["rad", "."], check=True, capture_output=True, cwd=cwd)
rid = p.stdout.decode().strip()

p = run(["rad", "self", "--nid"], check=True, capture_output=True, cwd=cwd)
nid = p.stdout.decode().strip()

p = run(["git", "rev-parse", "HEAD"], check=True, capture_output=True, cwd=cwd)
oid = p.stdout.decode().strip()

o = json.load(open(filename))

o["rid"] = rid

if "updated" in o:
    x = o["updated"]
    for oo in x:
        name = oo["updated"]["name"]
        oo["updated"]["name"] = nid.join(name.split("DUMMYNID"))
        oo["updated"]["new"] = oid
    o["updated"] = x

with open(filename, "w") as f:
    json.dump(o, fp=f, indent=4)

Set environment variables and run command

To avoid having to repeat environment variables to set up and use a Radicle node for verification scenarios, we provide a script that sets them and runs a command.

#!/bin/bash

set -euo pipefail

homedir="$(pwd)/homedir"

env \
	HOME="$homedir" \
	RAD_PASSPHRASE=secret \
	RAD_HOME="$homedir/.radicle" \
	RAD_SOCKET=synt.sock \
	RUST_LOG=trace \
	RADICLE_CI_BROKER_LOG=trace \
	"$@"

Set up a node with repository

Most of our verification scenarios will need to set up a Radicle node and a test repository there. Rather than repeat it in every scenario, we use this helper script.

#!/bin/bash

set -xeuo pipefail

mkdir -p "$HOME"

env | grep RAD
rad auth --alias brokertest

git config --global user.email radicle@example.com
git config --global user.name TestyMcTestFace
git init -b main testy

cd testy
echo "test file" > test.txt

git add .
git commit -am test
git status
git show
rad init --name testy --description test --default-branch main --private --no-confirm --no-seed
rad inspect --identity
rad id list

Acceptance criteria

Shows config as JSON

Requirement: The CI broker can write out the configuration is uses at run time as JSON.

Justification: This is helpful for the node operator to verify that they have configured the program correctly.

Stakeholder: Lars

Our verification here is quite simplistic, and only checks that the output is in the JSON format. It does not try to make sure the JSON matches the YAML semantically.

given an installed cib
given file broker.yaml
when I run cib --config broker.yaml config --output actual.json
when I run jq . actual.json
then command is successful

Smoke test: Runs adapter

Requirement: CI broker can run its adapter.

Justification: This is obviously necessary. If this doesn't work, nothing else has a hope of working.

Stakeholder: Lars.

given file radenv.sh
given file setup-node.sh
when I run bash radenv.sh bash setup-node.sh

given an installed synthetic-events
given file refsfetched.json
given file shutdown.json
given file set-rid
when I run env HOME=../homedir python3 set-rid refsfetched.json testy
when I run synthetic-events synt.sock refsfetched.json shutdown.json --log log.txt

given an installed cib
given a directory reports
given file broker.yaml
given file adapter.sh from dummy.sh
when I run chmod +x adapter.sh

when I try to run bash radenv.sh RAD_SOCKET=synt.sock cib --config broker.yaml insert
then command is successful
when I run cibtool --db ci-broker.db event shutdown

given an installed cibtool
when I run cibtool --db ci-broker.db event list
when I run cibtool --db ci-broker.db run list

when I try to run bash radenv.sh cib --config broker.yaml queued
then command is successful

when I run cibtool --db ci-broker.db run list
then stdout contains "id: "xyzzy""

Adapter can provide URL for info on run

Requirement: The adapter can provide a URL for information about the run, such a run log. This optional.

Justification: The CI broker does not itself store the run log, but it's useful to be able to point users at one. The CI broker can put that into a Radicle COB or otherwise store it so that users can see it. Note, however, that the adapter gets to decide which URL to provide: it need not be the run log. It might, for example, be a URL to the web view of a "pipeline" in GitLab CI instead, from which the user can access individual logs.

Stakeholder: Lars.

given file radenv.sh
given file setup-node.sh
when I run bash radenv.sh bash setup-node.sh

given an installed synthetic-events
given file refsfetched.json
given file shutdown.json
given file set-rid
when I run env HOME=../homedir python3 set-rid refsfetched.json testy
when I run synthetic-events synt.sock refsfetched.json shutdown.json --log log.txt

given file adapter.sh from adapter-with-url.sh
when I run chmod +x adapter.sh

given an installed cib
given file broker.yaml
given a directory reports

when I try to run bash radenv.sh RAD_SOCKET=synt.sock cib --config broker.yaml insert
then command is successful
#!/bin/bash
set -euo pipefail
echo '{"response":"triggered","run_id":{"id":"xyzzy"},"info_url":"https://ci.example.com/xyzzy"}'
echo '{"response":"finished","result":"success"}'

Gives helpful error message if node socket can't be found

Requirement: If the CI broker can't connect to the Radicle node control socket, it gives an error message that helps the user to understand the problem.

Justification: This helps users deal with problems themselves and reduces the support burden on the Radicle project.

Stakeholder: Lars.

given file radenv.sh
given file setup-node.sh
when I run bash radenv.sh bash setup-node.sh

given an installed cib
given file broker.yaml
when I try to run bash radenv.sh RAD_SOCKET=xyzzy.sock cib --config broker.yaml insert
then command fails
then stderr contains "node control socket does not exist: xyzzy.sock"

Gives helpful error message if it doesn't understand its configuration file

Requirement: If the CI broker is given a configuration file that it can't understand, it gives an error message that explains the problem to the user.

Justification: This helps users deal with problems themselves and reduces the support burden on the Radicle project.

Stakeholder: Lars.

Comment: This is a very basic scenario. Error handling is by nature a thing that can always be made better. We can later add more scenarios if we tighten the acceptance criteria.

given file radenv.sh
given file setup-node.sh
when I run bash radenv.sh bash setup-node.sh

given an installed cib
given file broker.yaml
given file not-yaml.yaml
when I try to run env HOME=homedir cib --config not-yaml.yaml config
then command fails
then stderr contains "failed to parse configuration file as YAML: not-yaml.yaml"
This file is not YAML.

Stops if the node connection breaks

Requirement: If the connection to the Radicle node, via its control socket, breaks, the CI broker terminates with a message saying why.

Justification: The CI broker can either keep running and trying to re-connect, or it can terminate. Either is workable. However, it's a simpler design and less code to terminate and allow re-starting to be handled by a dedicated system, such as systemd.

Stakeholder: Lars.

given file radenv.sh
given file setup-node.sh
when I run bash radenv.sh bash setup-node.sh

given an installed cib
given an installed synthetic-events
when I run synthetic-events synt.sock --log log.txt
given file broker.yaml
when I try to run bash radenv.sh RAD_SOCKET=synt.sock cib --config broker.yaml insert
then stderr contains "connection to the node control socket broke"

Shuts down when requested

Requirement: The test suite can request the CI broker to shut down cleanly, and it doesn't result in an error.

Justification: In the integration test suite, we need to start and stop the CI broker many times. We need to easily detect errors.

Stakeholder: Lars.

We use a special magic fake node event to signal shutdown: a RefsFetched event with a skipped update for a ref "shutdown" and an object id of all zeros. This should be sufficiently impossible to happen in real life.

given file radenv.sh
given file setup-node.sh
when I run bash radenv.sh bash setup-node.sh

given an installed cib
given an installed synthetic-events
given file shutdown.json
given file broker.yaml
when I run synthetic-events synt.sock shutdown.json --log synt.log
when I try to run bash radenv.sh RAD_SOCKET=synt.sock cib --config broker.yaml insert
then command is successful

Acceptance criteria for test tooling

The event synthesizer is a helper to feed the CI broker node events in a controlled fashion.

Dummy adapter runs successfully

Requirement: The dummy adapter (in embedded file dummy.sh) runs successfully.

Justification: Test scenarios using the dummy adapter need to be able to rely that it works.

Stakeholder: Lars

given file dummy.sh
when I run chmod +x dummy.sh
when I try to run ./dummy.sh
then command is successful

Adapter with URL runs successfully

Requirement: The adapter with a URL (in embedded file adapter-with-url.sh) runs successfully.

Justification: Test scenarios using this adapter need to be able to rely that it works.

Stakeholder: Lars

given file adapter-with-url.sh
when I run chmod +x adapter-with-url.sh
when I try to run ./adapter-with-url.sh
then command is successful

Event synthesizer terminates after first connection

Requirement: The event synthesizer runs in the background, but terminates after the first connection.

Justification: This is needed so that it can be invoked in Subplot scenarios.

Stakeholder: Lars.

The following scenario may only work on Linux, as it's using pgrep and nc and those may not be portable. If so, this may need to be changed for other platforms.

Note also that the version of nc must support the -U option, which in Debian means using the netcat-openbsd package.

We sleep for a very short time to make sure the synthetic-events daemon has time to remove the socket file before we check that it's been deleted.

given an installed synthetic-events

then file synt.sock does not exist

when I run synthetic-events synt.sock
then file synt.sock exists

when I run nc -U synt.sock
when I run sleep 0.1
then file synt.sock does not exist

Acceptance criteria for persistent database

The CI broker uses an SQLite database for persistent data. Many processes may need to access or modify the database at the same time. While SQLite is good at managing that, it needs to be used in the right way for everything to work correctly. The acceptance criteria in this chapter address that.

To enable the verification of these acceptance criteria, the CI broker database allows for a "counter", as a single row in a dedicated table. Concurrency is tested by having multiple processes update the counter at the same time and verifying the end result is as intended and that every value is set exactly once.

Count in a single process

Requirement: A single process can increment the test counter correctly.

Justification: If this doesn't work with a single process, it won't work of multiple processes, either.

Stakeholder: Lars.

given an installed cibtool
then file count.db does not exist
when I run cibtool --db count.db counter show
then stdout is exactly "0\n"
when I run cibtool --db count.db counter count --goal 1000
when I run cibtool --db count.db counter show
then stdout is exactly "1000\n"

Insert events into queue

Requirement: Insert broker events generated from node events into persistent event queue in the database, when allowed by the CI broker event filter.

Justification: This is fundamental for running CI when repositories in a node change.

Stakeholder: Lars.

given file radenv.sh
given file setup-node.sh
when I run bash radenv.sh bash setup-node.sh

given an installed cib
given an installed synthetic-events

given file refsfetched.json
given file shutdown.json
when I run synthetic-events synt.sock refsfetched.json shutdown.json --log synt.log

given file broker.yaml
when I try to run bash radenv.sh env RAD_SOCKET=synt.sock cib --config broker.yaml insert
then command is successful

when I run cibtool --db ci-broker.db event list --verbose
then stdout contains "RefChanged"
then stdout contains "oid: Oid(0000000000000000000000000000000000000000)"
then stdout contains "old: Some(Oid(0000000000000000000000000000000000000000))"

Insert many events into queue

Requirement: Insert many events that arrive quickly.

Justification: We need at least some rudimentary performance testing.

Stakeholder: Lars.

given file radenv.sh
given file setup-node.sh
when I run bash radenv.sh bash setup-node.sh

given an installed cib
given an installed synthetic-events

given file refsfetched.json
given file shutdown.json
when I run synthetic-events synt.sock refsfetched.json shutdown.json --log synt.log --repeat 1000

given file broker.yaml
when I try to run bash radenv.sh env RAD_SOCKET=synt.sock cib --config broker.yaml insert
then command is successful


when I run cibtool --db ci-broker.db event count
then stdout is exactly "1000\n"

Don't insert events into queue when not allowed by filter

Requirement: Nothing is inserted into the persistent event queue then the CI broker's filter does not allow any events.

Justification: This is fundamental for running CI when repositories in a node change.

Stakeholder: Lars.

given file radenv.sh
given file setup-node.sh
when I run bash radenv.sh bash setup-node.sh

given an installed cib
given an installed synthetic-events

given file refsfetched.json
given file shutdown.json
when I run synthetic-events synt.sock refsfetched.json shutdown.json --log synt.log

given file broker.yaml from broker-allow-nothing.yaml
when I try to run bash radenv.sh env RAD_SOCKET=synt.sock cib --config broker.yaml insert
then command is successful

when I run cibtool --db ci-broker.db event count
then stdout is exactly "0\n"

Process queued events

Requirement: It's possible to run the CI broker in a mode where it only processes events from its persistent event queue.

Justification: This is primarily useful for testing the CI broker queuing implementation.

Stakeholders: Lars.

We verify this by adding events to the queue with cibtool, and then running the CI broker and verifying it terminates after processing the events. We carefully add a shutdown event so that the CI broker shuts down.

given file radenv.sh
given file setup-node.sh
when I run bash radenv.sh bash setup-node.sh

given file adapter.sh from dummy.sh
when I run chmod +x adapter.sh

given an installed cib
given an installed cibtool

when I run bash radenv.sh cibtool --db ci-broker.db event add --repo testy --ref main --commit HEAD
when I run cibtool --db ci-broker.db event shutdown

given file broker.yaml
when I run ls -l adapter.sh
when I run bash radenv.sh cib --config broker.yaml queued
then stderr contains "Action: run:"
then stderr contains "Action: shutdown"

when I run cibtool --db ci-broker.db event list
then stdout is exactly ""

when I run cibtool --db ci-broker.db run list
then stdout contains "Success"

Count in concurrent processes

Requirement: Two process can concurrently increment the test counter correctly.

Justification: This is necessary, if not necessarily sufficient, for concurrent database use to work correctly.

Stakeholder: Lars.

Due to limitations in Subplot we mange the concurrent processes using a helper shell script,k count.sh, found below. It runs two concurrent cibtool processes that update the same database file, and count to a desired goal. The script then verifies that everything went correctly.

given an installed cibtool
given file count.sh
when I run bash -x count.sh 100 10
then stdout contains "OK\n"
#!/bin/bash

set -euo pipefail

run() {
	cibtool --db "$DB" counter count --goal "$goal"
}

DB=count.db

goal="$1"
reps="$2"

for x in $(seq "$reps"); do
	echo "Repetition $x"

	rm -f "$DB" ./?.out

	run >1.out 2>&1 &
	one=$!

	run >2.out 2>&1 &
	two=$!

	if ! wait "$one"; then
		echo "first run failed"
		cat 1.out
		exit 1
	fi

	if ! wait "$two"; then
		echo "second run failed"
		cat 2.out
		exit 1
	fi

	if grep ERROR ./?.out; then
		echo found ERRORs
		exit 1
	fi

	n="$(sqlite3 "$DB" 'select counter from counter_test')"
	[ "$n" == "$goal" ] || (
		echo "wrong count $n"
		exit 1
	)

	if awk '/increment to/ { print $NF }' ./?.out | sort -n | uniq -d | grep .; then
		echo "duplicate increments"
		exit 1
	fi
done

echo OK

Acceptance criteria for event queue management

The CI builder database contains a queue of broker events. The cibtool management tool can be used to examine and manipulate the queue.

Events can be queued and removed from queue

Requirement: cibtool can show the queued events, can inject an event, and remove an event.

Justification: This is the minimum functionality needed to manage the event queue.

Stakeholder: Lars.

We verify that this works by adding a new broker event, and then removing it. We randomly choose the repository id for the CI broker itself for this test, but the id shouldn't matter, it just needs to be of the correct form.

given file radenv.sh
given file setup-node.sh
when I run bash radenv.sh bash setup-node.sh

given an installed cibtool
when I run cibtool --db x.db event list
then stdout is exactly ""

when I run bash radenv.sh cibtool --db x.db event add --repo testy --ref main --commit HEAD --base c0ffee --id-file id.txt

when I run cibtool --db x.db event show --id-file id.txt
then stdout contains "rad:"
then stdout contains "main"
then stdout contains "c0ffee"

when I run cibtool --db x.db event remove --id-file id.txt

when I run cibtool --db x.db event list
then stdout is exactly ""

Can add shutdown event to queue

Requirement: cibtool can add a shutdown event to the queued events.

Justification: This is needed for testing, and for the node operator to be able to do this cleanly.

given an installed cibtool
when I run cibtool --db x.db event list
then stdout is exactly ""

when I run cibtool --db x.db event shutdown --id-file id.txt

when I run cibtool --db x.db event show --id-file id.txt
then stdout contains "Shutdown"