use std::sync::Arc; use models::{CuddlePleaseArgs, CuddlePleaseSrcArgs}; use traits::CuddlePlease; use crate::models::{LogLevel, Server, SrcServer}; pub mod models { #[derive(Clone, Debug)] pub struct CuddlePleaseArgs { /// Which repository, example: "dagger-components" pub repository: String, /// Whom owns the repository, example: "kjuulh" pub owner: String, /// which branch to fetch, example: "main" pub branch: String, /// Which image to use for cuddle-please, example: "docker.io/kasperhermansen/cuddle-please:latest" pub cuddle_image: String, /// Which server to use, see inner field for more docs pub server: Server, /// Which log level to output for cuddle-please. Defaults to INFO pub log_level: Option, /// Whether to include an ssh socket, if not set will default to the token set in server and http/https depending on the server pub use_ssh_socket: bool, } #[derive(Clone, Debug)] pub struct CuddlePleaseSrcArgs { /// Which image to use for cuddle-please, example: "docker.io/kasperhermansen/cuddle-please:latest" pub cuddle_image: String, /// Which log level to output for cuddle-please. Defaults to INFO pub log_level: Option, /// Which server to use, see inner field for more docs pub server: SrcServer, } #[derive(Clone, Debug)] pub enum LogLevel { Trace, Debug, Info, Warn, Error, } #[derive(Clone, Debug)] pub enum Server { Gitea { url: String, user: String, token: String, insecure: Option, }, GitHub { token: String, }, } #[derive(Clone, Debug)] pub enum SrcServer { Gitea { token: String }, GitHub { token: String }, } } pub mod traits { use crate::models::{CuddlePleaseArgs, CuddlePleaseSrcArgs}; #[async_trait::async_trait] pub trait CuddlePlease { async fn execute(&self, args: &CuddlePleaseArgs) -> eyre::Result<()>; async fn execute_src(&self, args: &CuddlePleaseSrcArgs) -> eyre::Result<()>; } } pub struct DaggerCuddlePleaseAction(Arc); impl DaggerCuddlePleaseAction { /// Create a [`traits::CuddlePlease`] client based on dagger pub fn dagger(client: Arc) -> Self { Self(Arc::new(DaggerCuddlePlease::new(client))) } /// Executes the cuddle-please action. This is a slow operation, but async, so make sure to not spawn blocking /// This relies exclively on arguments, as such this pull the repository directly, instead of using local src pub async fn execute(&self, args: &CuddlePleaseArgs) -> eyre::Result<()> { self.0.execute(args).await } /// Executes the cuddle-please action. This is a slow operation, but async, so make sure to not spawn blocking /// This function relies exclusively on cuddle.yaml and cuddle.please.yaml /// For private repository access, make sure to use [`CuddlePleaseSrcArgs::use_ssh_agent`] otherwise we may not able to push to your repository pub async fn execute_src(&self, args: &CuddlePleaseSrcArgs) -> eyre::Result<()> { self.0.execute_src(args).await } } #[derive(Clone)] struct DaggerCuddlePlease { client: Arc, } #[async_trait::async_trait] impl CuddlePlease for DaggerCuddlePlease { async fn execute(&self, args: &CuddlePleaseArgs) -> eyre::Result<()> { self.cuddle_please(self.client.clone(), args).await } async fn execute_src(&self, args: &CuddlePleaseSrcArgs) -> eyre::Result<()> { self.cuddle_please_src(self.client.clone(), args).await } } impl DaggerCuddlePlease { pub fn new(client: Arc) -> Self { Self { client } } pub async fn cuddle_please( &self, client: Arc, args: &CuddlePleaseArgs, ) -> eyre::Result<()> { let build_image = client.container().from(&args.cuddle_image); let repo_url = match &args.server { Server::Gitea { url, user, token, insecure, } => { if args.use_ssh_socket { format!( "ssh://{}:{}@{}/{}/{}", user, token, url, &args.owner, &args.repository ) } else { format!( "{}://{}:{}@{}/{}/{}", if insecure.unwrap_or(false) { "http" } else { "https" }, user, token, url, &args.owner, &args.repository ) } } Server::GitHub { .. } => { if args.use_ssh_socket { format!("git@github.com:{}/{}", &args.owner, &args.repository) } else { format!("https://github.com/{}/{}", &args.owner, &args.repository) } } }; let api_url = match &args.server { Server::Gitea { url, insecure, .. } => { format!( "{}://{}", if insecure.unwrap_or(false) { "http" } else { "https" }, url, ) } Server::GitHub { .. } => "https://github.com".into(), }; let src = if args.use_ssh_socket { let socket = client .host() .unix_socket(std::env::var("SSH_AGENT").expect("SSH_AGENT to be set")); client .git_opts( &repo_url, dagger_sdk::QueryGitOpts { experimental_service_host: None, keep_git_dir: Some(true), }, ) .branch("main") .tree_opts(dagger_sdk::GitRefTreeOpts { ssh_auth_socket: Some(socket.id().await?), ssh_known_hosts: None, }) } else { client .git_opts( &repo_url, dagger_sdk::QueryGitOpts { experimental_service_host: None, keep_git_dir: Some(true), }, ) .branch("main") .tree() }; let res = build_image .with_secret_variable( "CUDDLE_PLEASE_TOKEN", client .set_secret( "CUDDLE_PLEASE_TOKEN", match &args.server { Server::Gitea { token, .. } => token, Server::GitHub { token } => token, }, ) .id() .await?, ) .with_workdir("/mnt/app") .with_directory(".", src.id().await?) .with_exec(vec!["git", "remote", "set-url", "origin", &repo_url]) .with_exec(vec![ "cuddle-please", "release", &format!( "--engine={}", match &args.server { Server::Gitea { .. } => "gitea", Server::GitHub { .. } => "github", } ), "--owner", &args.owner, "--repo", &args.repository, "--branch", &args.branch, "--api-url", &api_url, "--log-level", match args.log_level.as_ref().unwrap_or(&LogLevel::Info) { LogLevel::Trace => "trace", LogLevel::Debug => "debug", LogLevel::Info => "info", LogLevel::Warn => "warn", LogLevel::Error => "error", }, ]); let exit_code = res.exit_code().await?; if exit_code != 0 { eyre::bail!("failed to run cuddle-please"); } let please_out = res.stdout().await?; println!("{please_out}"); let please_out = res.stderr().await?; println!("{please_out}"); Ok(()) } pub async fn cuddle_please_src( &self, client: Arc, args: &CuddlePleaseSrcArgs, ) -> eyre::Result<()> { let build_image = client.container().from(&args.cuddle_image); let res = build_image .with_secret_variable( "CUDDLE_PLEASE_TOKEN", client .set_secret( "CUDDLE_PLEASE_TOKEN", match &args.server { SrcServer::Gitea { token, .. } => token, SrcServer::GitHub { token } => token, }, ) .id() .await?, ) .with_workdir("/mnt/app") .with_directory(".", client.host().directory(".").id().await?) .with_unix_socket( "/tmp/ssh.sock", client .host() .unix_socket( std::env::var("SSH_AUTH_SOCK").expect("expect SSH_AUTH_SOCK to be present"), ) .id() .await?, ) .with_env_variable("SSH_AUTH_SOCK", "/tmp/ssh.sock") .with_new_file_opts( "/root/.ssh/config", dagger_sdk::ContainerWithNewFileOpts { contents: Some( " Host * User git StrictHostKeyChecking no UserKnownHostsFile /dev/null ", ), owner: Some("root"), permissions: Some(700), }, ); let remote_url = res .with_exec(vec!["git", "config", "--get", "remote.origin.url"]) .stdout() .await?; let res = if remote_url.starts_with("http") { res.with_exec(vec![ "git", "config", "--global", &format!( "url.ssh://git@{}/.insteadOf", remote_url .trim_start_matches("https://") .trim_start_matches("http://") ), &remote_url, ]) } else { res }; let res = res.with_exec(vec![ "cuddle-please", "release", &format!( "--engine={}", match &args.server { SrcServer::Gitea { .. } => "gitea", SrcServer::GitHub { .. } => "github", } ), "--log-level", match args.log_level.as_ref().unwrap_or(&LogLevel::Info) { LogLevel::Trace => "trace", LogLevel::Debug => "debug", LogLevel::Info => "info", LogLevel::Warn => "warn", LogLevel::Error => "error", }, ]); let exit_code = res.exit_code().await?; if exit_code != 0 { eyre::bail!("failed to run cuddle-please"); } let please_out = res.stdout().await?; println!("{please_out}"); let please_out = res.stderr().await?; println!("{please_out}"); Ok(()) } }