cli: test canonical tags

Add a test to exercise adding canonical reference rules and pushing new
canonical tags.

A change in `rad id` was required to allow adding a new payload to the document,
i.e. one that does not already exist.

Adding rules is also documented in the man page for `rad id`.
This commit is contained in:
Fintan Halpenny 2025-06-18 14:36:19 +02:00 committed by Lorenz Leutgeb
parent ff365e2d8b
commit 8201225166
3 changed files with 302 additions and 1 deletions

View File

@ -0,0 +1,213 @@
In this example, we will show how we can make other references become canonical.
To illustrate, we will use Git tags as an example. The storage of the repository
should look something like this by the end of the example:
~~~
storage/z6cFWeWpnZNHh9rUW8phgA3b5yGt/refs
├── heads
│   └── main
├── namespaces
│   ├── z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
│   │   └── refs
│   │   ├── cobs
│   │   │   └── xyz.radicle.id
│   │   │   └── 865c48204bd7bb7f088b8db90ffdccb48cfa0a50
│   │   ├── heads
│   │   │   └── master
│   │   ├── tags
│   │   │ ├── v1.0-hotfix
│   │   │   └── v1.0
│   │   └── rad
│   │   ├── id
│   │   └── sigrefs
│   └── z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
   └── refs
│   ├── heads
│   │   └── master
│      ├── tags
│      │ ├── v1.0-hotfix
│      │   └── v1.0
│      └── rad
│      ├── id
│      └── sigrefs
├── rad
│   └── id
└── tags
├── v1.0-hotfix
   └── v1.0
~~~
Noting that there are tags under `refs/tags` now.
To start, Alice will add a new payload to the repository identity. The
identifier for this payload is `xyz.radicle.crefs`. It contains a single field
with the key `rules`, and the value for this key is an array of rules. In this
case, we will have two rules: one for `refs/tags/*` and one for `refs/tags/qa/*`
(see RIP-0004 for more information on the rules).
``` ~alice
$ rad id update --title "Add canonical reference rules" --payload xyz.radicle.crefs rules '{ "refs/tags/*": { "threshold": 1, "allow": "delegates" }, "refs/tags/qa/*": { "threshold": 1, "allow": "delegates" }}'
✓ Identity revision [..] created
╭────────────────────────────────────────────────────────────────────────╮
│ Title Add canonical reference rules │
│ Revision c3349f07bfe6a82bbeb2989d2de4a918408f9831 │
│ Blob 85fa09e2de93b825d5231778dbb34143004a4bca │
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
│ State accepted │
│ Quorum yes │
├────────────────────────────────────────────────────────────────────────┤
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice (you) │
╰────────────────────────────────────────────────────────────────────────╯
@@ -1,13 +1,25 @@
{
"payload": {
+ "xyz.radicle.crefs": {
+ "rules": {
+ "refs/tags/*": {
+ "allow": "delegates",
+ "threshold": 1
+ },
+ "refs/tags/qa/*": {
+ "allow": "delegates",
+ "threshold": 1
+ }
+ }
+ },
"xyz.radicle.project": {
"defaultBranch": "master",
"description": "Radicle Heartwood Protocol & Stack",
"name": "heartwood"
}
},
"delegates": [
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
],
"threshold": 1
}
```
Now, Alice will create a tag and push it:
``` ~alice
$ git tag v1.0-hotfix
```
``` ~alice (stderr)
$ git push rad --tags
✓ Canonical reference refs/tags/v1.0-hotfix updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
✓ Synced with 1 seed(s)
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new tag] v1.0-hotfix -> v1.0-hotfix
```
Notice that the output included a message about a canonical reference being
updated:
~~~
✓ Canonical reference refs/tags/v1.0-hotfix updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
~~~
On the other side, Bob performs a fetch and now has the tags locally:
``` ~bob (stderr)
$ cd heartwood
$ git fetch rad
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
* [new tag] v1.0-hotfix -> rad/tags/v1.0-hotfix
* [new tag] v1.0-hotfix -> v1.0-hotfix
```
In the next portion of this example, we want to show that using a `threshold` of
`2` requires both delegates. To do this, Bob creates a `master` reference, Alice
adds him as a remote, and adds him to the identity delegates, as well as setting
the `threshold` to `2` for the `refs/tags/*` rule:
``` ~bob
$ rad remote add z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --name alice
✓ Follow policy updated for z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (alice)
Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found 1 potential seed(s).
✓ Target met: 1 seed(s)
✓ Remote alice added
✓ Remote-tracking branch alice/master created for z6MknSL…StBU8Vi
$ git push rad master
```
``` ~alice
$ rad remote add z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob
✓ Follow policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (bob)
Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found 1 potential seed(s).
✓ Target met: 1 seed(s)
✓ Remote bob added
✓ Remote-tracking branch bob/master created for z6Mkt67…v4N1tRk
$ rad id update --title "Add Bob" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --no-confirm -q
27ab0d77a95581c59ca9d30e679ceb06a9f758db
$ rad id update --title "Update canonical reference rules" --payload xyz.radicle.crefs rules '{ "refs/tags/*": { "threshold": 2, "allow": "delegates" }, "refs/tags/qa/*": { "threshold": 1, "allow": "delegates" } }' -q
dace164ba43fa51802697ec28d0b1965a9d7808b
```
**Note:** here we have to specify all the rules again to update the `threshold`.
In reality, you can use `rad id update --edit` and edit the payload in your
editor instead.
``` ~bob
$ rad sync -f
Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found 1 potential seed(s).
✓ Target met: 1 seed(s)
🌱 Fetched from z6MknSL…StBU8Vi
$ rad id accept dace164ba43fa51802697ec28d0b1965a9d7808b -q
```
When Bob creates a new tag and pushes it, we see that there's a warning that
no quorum was found for the new tag:
``` ~bob (stderr)
$ git tag v2.0
$ git push rad --tags
warn: could not determine commit for canonical reference 'refs/tags/v2.0', no commit with at least 2 vote(s) found (threshold not met)
warn: it is recommended to find a commit to agree upon
✓ Synced with 1 seed(s)
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
* [new tag] v1.0-hotfix -> v1.0-hotfix
* [new tag] v2.0 -> v2.0
```
Alice can then fetch and checkout the new tag, create one on her side, and push
it:
``` ~alice (stderr)
$ git fetch bob
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
* [new tag] v1.0-hotfix -> bob/tags/v1.0-hotfix
* [new tag] v2.0 -> bob/tags/v2.0
```
``` ~alice
$ git checkout bob/tags/v2.0
$ git tag v2.0
```
``` ~alice (stderr)
$ git push rad --tags
✓ Canonical reference refs/tags/v2.0 updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
✓ Synced with 1 seed(s)
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new tag] v2.0 -> v2.0
```
Now that Bob has also pushed this tag, we can see that the tag was made
canonical.
For the final portion of the example, we will show that both delegates aren't
required for pushing tags that match the rule `refs/tags/qa/*`. To show this,
Bob will create a tag and push it, and we should see that the canonical
reference is created:
``` ~bob (stderr)
$ git tag qa/v2.1
$ git push rad --tags
✓ Canonical reference refs/tags/qa/v2.1 updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
✓ Synced with 1 seed(s)
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
* [new tag] qa/v2.1 -> qa/v2.1
```

View File

@ -2517,6 +2517,48 @@ fn git_tag() {
.unwrap();
}
#[test]
fn git_push_canonical_tags() {
let mut environment = Environment::new();
let alice = environment.node("alice");
let bob = environment.node("bob");
let rid = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
fixtures::repository(environment.work(&alice));
test(
"examples/rad-init.md",
environment.work(&alice),
Some(&alice.home),
[],
)
.unwrap();
let alice = alice.spawn();
let mut bob = bob.spawn();
bob.connect(&alice).converge([&alice]);
bob.clone(rid, environment.work(&bob)).unwrap();
formula(
&environment.tempdir(),
"examples/git/git-push-canonical-tags.md",
)
.unwrap()
.home(
"alice",
environment.work(&alice),
[("RAD_HOME", alice.home.path().display())],
)
.home(
"bob",
environment.work(&bob),
[("RAD_HOME", bob.home.path().display())],
)
.run()
.unwrap();
}
#[test]
fn rad_workflow() {
let mut environment = Environment::new();

View File

@ -18,7 +18,7 @@ rad-id - Manage changes to a Radicle repository's identity.
*rad id* _edit_ <revision-id> [--title <string>] [--description <string>] [<option>...] +
*rad id* _show_ <revision-id> [<option>...] +
*rad id* _accept_ | _reject_ <revision-id> [<option>...] +
*rad id* _redact_ <revision-id> [<option>...]
*rad id* _redact_ <revision-id> [<option>...] +
== Description
@ -176,3 +176,49 @@ To remove a delegate and update the threshold, use the *--rescind* option:
As with adding a delegate, this change will require approval from the remaining
delegates. Make sure you set an appropriate new threshold when removing
delegates!
=== Adding Canonical References Rules
To update the canonical reference rules of the project, use the `--payload
xyz.radicle.crefs` option while updating, `rules` as the key, and the rules
object as the value. Here is an example below:
$ rad id update --title "Update canonical reference rules" \
--payload xyz.radicle.crefs rules '{
"refs/heads/master": { "threshold": 1, "allow": "delegates" },
"refs/tags/*": { "threshold": 2, "allow": "delegates" },
"refs/tags/qa/*": { "threshold": 1, "allow": "delegates" }
}'
Alternatively, you can use the `--edit` option for the `update` command and edit
the payload directly. Here is an example of what that may look like:
{
"payload": {
"xyz.radicle.crefs": {
"rules": {
"refs/heads/master": {
"allow": "delegates",
"threshold": 1
},
"refs/tags/*": {
"allow": "delegates",
"threshold": 1
},
"refs/tags/qa/*": {
"allow": "delegates",
"threshold": 1
}
}
},
"xyz.radicle.project": {
"defaultBranch": "master",
"description": "Radicle Heartwood Protocol & Stack",
"name": "heartwood"
}
},
"delegates": [
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
],
"threshold": 1
}