Radish alpha
r
rad:z3qg5TKmN83afz2fj9z3fQjU8vaYE
Radicle CI adapter for native CI
Radicle
Git
fix: cargo update
Merged liw opened 1 year ago

fix: use –locked with cargo install in debian/rules

Signed-off-by: Lars Wirzenius liw@liw.fi

feat: add –version option

Signed-off-by: Lars Wirzenius liw@liw.fi

feat: add –show-config to write out configuration as JSON

Signed-off-by: Lars Wirzenius liw@liw.fi

feat: allow an optional base URL in the configuration file

Signed-off-by: Lars Wirzenius liw@liw.fi

feat: return URL to build log if base URL is configured

Signed-off-by: Lars Wirzenius liw@liw.fi

7 files changed +127 -14 e7efad95 7947dd65
modified Cargo.lock
@@ -1721,7 +1721,7 @@ dependencies = [

[[package]]
name = "radicle-native-ci"
-
version = "0.1.0"
+
version = "0.2.0"
dependencies = [
 "html-page",
 "radicle",
modified debian/rules
@@ -7,7 +7,7 @@ override_dh_auto_build:
	true

override_dh_auto_install:
-
	cargo install --offline --path=. --root=debian/radicle-native-ci
+
	cargo install --offline --locked --path=. --root=debian/radicle-native-ci
	rm -f debian/*/.crates*.*

override_dh_auto_test:
modified src/bin/radicle-native-ci.rs
@@ -12,7 +12,7 @@
//!   cargo test --locked --workspace
//! ```

-
use std::{error::Error, process::exit};
+
use std::{env::args, error::Error, process::exit};

use radicle_native_ci::engine::{Engine, EngineError};

@@ -45,6 +45,25 @@ fn main() {
}

fn fallible_main() -> Result<bool, EngineError> {
-
    let mut engine = Engine::new()?;
-
    engine.run()
+
    let args: Vec<String> = args().skip(1).collect();
+

+
    if args.is_empty() {
+
        let mut engine = Engine::new()?;
+
        engine.run()
+
    } else {
+
        for arg in args.iter() {
+
            match arg.as_str() {
+
                "--version" => println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
+
                "--show-config" => {
+
                    let engine = Engine::new()?;
+
                    println!("{}", engine.config().as_json());
+
                }
+
                _ => {
+
                    eprintln!("unknown argument {arg}");
+
                    return Ok(false);
+
                }
+
            }
+
        }
+
        Ok(true)
+
    }
}
modified src/config.rs
@@ -19,6 +19,11 @@ pub struct Config {

    /// Optional maximum duration of a CI run.
    pub timeout: Option<usize>,
+

+
    /// Optional base URL to information about each run. The run ID is
+
    /// appended, with a slash if needed.
+
    #[serde(skip_serializing_if = "Option::is_none")]
+
    pub base_url: Option<String>,
}

impl Config {
@@ -50,6 +55,13 @@ impl Config {
    pub fn timeout(&self) -> usize {
        self.timeout.unwrap_or(DEFAULT_TIMEOUT)
    }
+

+
    /// Return configuration serialized to JSON.
+
    pub fn as_json(&self) -> String {
+
        // We don't check the result: we know a configuration can
+
        // always be serialized to JSON.
+
        serde_json::to_string_pretty(self).unwrap()
+
    }
}

#[derive(Debug, thiserror::Error)]
modified src/engine.rs
@@ -48,6 +48,11 @@ impl Engine {
        })
    }

+
    /// Return config that has been loaded for the engine.
+
    pub fn config(&self) -> &Config {
+
        &self.config
+
    }
+

    /// Set up and run CI on a project once: read the trigger request
    /// from stdin, write responses to stdout. Update node admin log
    /// with any problems that aren't inherent in the git repository
@@ -197,7 +202,16 @@ impl Engine {
        let storage = profile.storage.path();

        // Write response to indicate run has been triggered.
-
        write_triggered(&run_id)?;
+
        if let Some(url) = &self.config.base_url {
+
            let url = if url.ends_with('/') {
+
                format!("{url}{}/log.html", run_id)
+
            } else {
+
                format!("{url}/{}/log.html", run_id)
+
            };
+
            write_triggered(&run_id, Some(&url))?;
+
        } else {
+
            write_triggered(&run_id, None)?;
+
        }

        // Create and set up the run.
        let mut run = Run::new(&run_dir, &run_log_filename)?;
modified src/msg.rs
@@ -18,10 +18,13 @@ fn write_response(resp: &Response) -> Result<(), NativeMessageError> {
}

/// Write a "triggered" response to stdout.
-
pub fn write_triggered(run_id: &RunId) -> Result<(), NativeMessageError> {
-
    write_response(&Response::triggered(RunId::from(
-
        run_id.to_string().as_str(),
-
    )))?;
+
pub fn write_triggered(run_id: &RunId, info_url: Option<&str>) -> Result<(), NativeMessageError> {
+
    let response = if let Some(url) = info_url {
+
        Response::triggered_with_url(run_id.clone(), url)
+
    } else {
+
        Response::triggered(run_id.clone())
+
    };
+
    write_response(&response)?;
    Ok(())
}

modified tests/integration.rs
@@ -30,7 +30,7 @@ use std::{
use radicle::{git::Oid, identity::Did, prelude::RepoId};
use radicle_ci_broker::msg::{
    Author, EventCommonFields, EventType, Patch, PatchAction, PatchEvent, Repository, Request,
-
    Response, RunResult, State,
+
    Response, RunId, RunResult, State,
};
use tempfile::{tempdir, TempDir};

@@ -68,11 +68,34 @@ impl AdapterResult {
        assert_eq!(self.exit, wanted);
    }

+
    // Return run id from adapter, if one was returned.
+
    fn run_id(&self) -> Option<RunId> {
+
        let msgs = self.messages().unwrap();
+
        assert!(!msgs.is_empty());
+
        match &msgs[0] {
+
            Response::Triggered { run_id, .. } => Some(run_id.clone()),
+
            _ => None,
+
        }
+
    }
+

    // Assert that the adapter sent a message with a run ID.
    fn assert_got_run_id(&self) {
+
        assert!(self.run_id().is_some());
+
    }
+

+
    // Assert that the adapter sent a message the expected info URL.
+
    fn assert_url_is(&self, expected: &str) {
        let msgs = self.messages().unwrap();
        assert!(!msgs.is_empty());
-
        assert!(matches!(msgs[0], Response::Triggered { .. }));
+
        match &msgs[0] {
+
            Response::Triggered {
+
                run_id: _,
+
                info_url: Some(url),
+
            } => {
+
                assert_eq!(url, expected);
+
            }
+
            _ => panic!("unexpected message {:#?}", msgs[0]),
+
        }
    }

    // Assert that the adapter wrote a message indicating the CI run
@@ -209,8 +232,11 @@ enum ConfigKind {
    // No config: file is empty.
    EmptyConfig(PathBuf),

-
    // Valid config.
+
    // Valid config, without info base URL.
    Valid(PathBuf),
+

+
    // Valid, but with info base URL.
+
    ValidWithUrl(PathBuf, String),
}

// What kind of trigger message should we give the adapter?
@@ -277,6 +303,14 @@ impl TestCaseBuilder {
        self
    }

+
    fn with_config_with_url(mut self, url: &str) -> Self {
+
        self.config = Some(ConfigKind::ValidWithUrl(
+
            self.tmp.path().join("config.yaml"),
+
            url.into(),
+
        ));
+
        self
+
    }
+

    fn with_empty_request(mut self) -> Self {
        self.request = Some(TriggerKind::Empty);
        self
@@ -334,6 +368,16 @@ impl TestCaseBuilder {
                state: tmp.join("state"),
                log: tmp.join("log.html"),
                timeout: Some(TIMEOUT),
+
                base_url: None,
+
            };
+
            let config = serde_yaml::to_string(&config).unwrap();
+
            std::fs::write(filename, config)?;
+
        } else if let ConfigKind::ValidWithUrl(filename, url) = &config {
+
            let config = Config {
+
                state: tmp.join("state"),
+
                log: tmp.join("log.html"),
+
                timeout: Some(TIMEOUT),
+
                base_url: Some(url.into()),
            };
            let config = serde_yaml::to_string(&config).unwrap();
            std::fs::write(filename, config)?;
@@ -353,7 +397,8 @@ impl TestCaseBuilder {
            ConfigKind::UnsetEnvVar => (),
            ConfigKind::DoesNotExist(filename)
            | ConfigKind::EmptyConfig(filename)
-
            | ConfigKind::Valid(filename) => {
+
            | ConfigKind::Valid(filename)
+
            | ConfigKind::ValidWithUrl(filename, _) => {
                envs.insert("RADICLE_NATIVE_CI".into(), filename.display().to_string());
            }
        }
@@ -690,3 +735,23 @@ fn happy_path() -> TestResult {
    result.assert_got_success();
    Ok(())
}
+

+
// Does the adapter construct a URL to the build log?
+
#[test]
+
fn happy_path_with_log_url() -> TestResult {
+
    let result = TestCaseBuilder::new()?
+
        .with_config_with_url("https://ci.radicle.liw.fi") // note lack of trailing slash
+
        .with_request()
+
        .with_shell("echo hello, world")
+
        .build()?
+
        .run()?;
+

+
    result.assert_got_exit(0);
+
    result.assert_got_run_id();
+
    result.assert_got_success();
+

+
    let run_id = result.run_id().unwrap();
+
    let expected = format!("https://ci.radicle.liw.fi/{}/log.html", run_id);
+
    result.assert_url_is(&expected);
+
    Ok(())
+
}