feat: add rust actions
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
19
crates/cuddle-please-actions/Cargo.toml
Normal file
19
crates/cuddle-please-actions/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "cuddle-please-actions"
|
||||
description = "A release-please inspired release manager tool, built on top of cuddle, but also useful standalone, cuddle-please supports, your ci of choice, as well as gitea, github"
|
||||
repository = "https://git.front.kjuulh.io/kjuulh/cuddle-please"
|
||||
readme = "../../README.md"
|
||||
license-file = "../../LICENSE"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publishable = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
tracing.workspace = true
|
||||
semver.workspace = true
|
||||
toml_edit.workspace = true
|
||||
yaml-rust2.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
9
crates/cuddle-please-actions/src/actions.rs
Normal file
9
crates/cuddle-please-actions/src/actions.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use semver::Version;
|
||||
|
||||
use crate::ActionConfig;
|
||||
|
||||
pub trait Action {
|
||||
fn enabled(&self, config: &ActionConfig) -> anyhow::Result<bool>;
|
||||
fn name(&self) -> String;
|
||||
fn execute(&self, version: &Version) -> anyhow::Result<()>;
|
||||
}
|
49
crates/cuddle-please-actions/src/config.rs
Normal file
49
crates/cuddle-please-actions/src/config.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
pub enum ActionConfig {
|
||||
Actual { doc: yaml_rust2::Yaml },
|
||||
None,
|
||||
}
|
||||
|
||||
impl Deref for ActionConfig {
|
||||
type Target = yaml_rust2::Yaml;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match &self {
|
||||
ActionConfig::Actual { doc } => doc,
|
||||
ActionConfig::None => &yaml_rust2::Yaml::BadValue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ActionConfig {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let mut cuddle = yaml_rust2::YamlLoader::load_from_str(value)?;
|
||||
|
||||
if cuddle.len() != 1 {
|
||||
anyhow::bail!("cuddle.yaml can only be 1 document wide");
|
||||
}
|
||||
|
||||
let doc = cuddle.pop().unwrap();
|
||||
let doc = doc["please"]["actions"].clone();
|
||||
|
||||
if doc.is_badvalue() {
|
||||
return Ok(Self::None);
|
||||
}
|
||||
|
||||
Ok(Self::Actual { doc })
|
||||
}
|
||||
}
|
||||
|
||||
impl ActionConfig {
|
||||
pub fn parse() -> anyhow::Result<Self> {
|
||||
let cuddle_yaml =
|
||||
std::fs::read_to_string("cuddle.yaml").context("failed to read cuddle.yaml")?;
|
||||
|
||||
Self::try_from(cuddle_yaml.as_str())
|
||||
}
|
||||
}
|
45
crates/cuddle-please-actions/src/lib.rs
Normal file
45
crates/cuddle-please-actions/src/lib.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
pub(crate) mod actions;
|
||||
mod config;
|
||||
mod rust_action;
|
||||
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use catalog::RustAction;
|
||||
pub use config::ActionConfig;
|
||||
use semver::Version;
|
||||
pub mod catalog {
|
||||
pub use crate::rust_action::*;
|
||||
}
|
||||
|
||||
pub struct Action(Arc<dyn actions::Action>);
|
||||
|
||||
impl Deref for Action {
|
||||
type Target = Arc<dyn actions::Action>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Actions(Vec<Action>);
|
||||
|
||||
impl Actions {
|
||||
pub fn from_cuddle() -> anyhow::Result<Self> {
|
||||
let config = ActionConfig::parse()?;
|
||||
|
||||
Ok(Self(
|
||||
vec![Action(Arc::new(RustAction::new()))]
|
||||
.into_iter()
|
||||
.filter(|a| a.enabled(&config).unwrap_or_default())
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn execute(&self, version: &Version) -> anyhow::Result<()> {
|
||||
for action in &self.0 {
|
||||
action.execute(version)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
237
crates/cuddle-please-actions/src/rust_action.rs
Normal file
237
crates/cuddle-please-actions/src/rust_action.rs
Normal file
@@ -0,0 +1,237 @@
|
||||
use std::io::Write;
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::{actions::Action, ActionConfig};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct RustAction {}
|
||||
|
||||
impl RustAction {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn execute_content(
|
||||
&self,
|
||||
version: &semver::Version,
|
||||
cargo_content: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
tracing::trace!("parsing Cargo.toml file as tolm");
|
||||
let mut cargo_doc = cargo_content.parse::<toml_edit::DocumentMut>()?;
|
||||
|
||||
tracing::debug!(
|
||||
"updating cargo workspace package version to {}",
|
||||
version.to_string()
|
||||
);
|
||||
|
||||
let workspace = if cargo_doc.contains_table("workspace") {
|
||||
cargo_doc["workspace"].as_table_mut().unwrap()
|
||||
} else {
|
||||
let mut t = toml_edit::Table::new();
|
||||
t.set_implicit(true);
|
||||
cargo_doc["workspace"] = toml_edit::Item::Table(t);
|
||||
cargo_doc["workspace"].as_table_mut().unwrap()
|
||||
};
|
||||
let package = workspace["package"].or_insert(toml_edit::table());
|
||||
package["version"] = toml_edit::value(version.to_string());
|
||||
|
||||
Ok(cargo_doc.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Action for RustAction {
|
||||
fn enabled(&self, config: &ActionConfig) -> anyhow::Result<bool> {
|
||||
if let Ok(v) = std::env::var("CUDDLE_PLEASE_RUST_ACTION") {
|
||||
if let Ok(true) = v.parse::<bool>() {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
let val = &config[self.name().as_str()];
|
||||
|
||||
if val.is_badvalue() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(val.as_bool().unwrap_or(true))
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
"rust".into()
|
||||
}
|
||||
|
||||
fn execute(&self, version: &semver::Version) -> anyhow::Result<()> {
|
||||
tracing::info!(
|
||||
"running rust action for version: {} and file: Cargo.toml",
|
||||
version.to_string()
|
||||
);
|
||||
|
||||
let path = std::path::PathBuf::from("Cargo.toml");
|
||||
|
||||
tracing::trace!("reading Cargo.toml");
|
||||
|
||||
let file = match std::fs::read_to_string(&path) {
|
||||
Ok(file) => file,
|
||||
Err(e) => match e.kind() {
|
||||
std::io::ErrorKind::NotFound => {
|
||||
anyhow::bail!("err: Cargo.toml was not found in dir")
|
||||
}
|
||||
_ => Err(e)?,
|
||||
},
|
||||
};
|
||||
|
||||
let cargo_doc = self.execute_content(version, &file)?;
|
||||
|
||||
let mut cargo_file = std::fs::File::create(&path)?;
|
||||
cargo_file.write_all(cargo_doc.as_bytes())?;
|
||||
cargo_file.sync_all()?;
|
||||
|
||||
tracing::debug!("finished writing cargo file");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use semver::{BuildMetadata, Prerelease};
|
||||
|
||||
use crate::{actions::Action, ActionConfig};
|
||||
|
||||
use super::RustAction;
|
||||
|
||||
#[test]
|
||||
fn test_is_enabled() {
|
||||
let config = ActionConfig::try_from(
|
||||
r#"
|
||||
please:
|
||||
actions:
|
||||
rust: true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let enabled = RustAction::new().enabled(&config).unwrap();
|
||||
|
||||
assert!(enabled)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_disabled_by_default() {
|
||||
let config = ActionConfig::try_from(
|
||||
r#"
|
||||
please:
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let enabled = RustAction::new().enabled(&config).unwrap();
|
||||
|
||||
assert!(!enabled)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_disabled() {
|
||||
let config = ActionConfig::try_from(
|
||||
r#"
|
||||
please:
|
||||
actions:
|
||||
rust: false
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let enabled = RustAction::new().enabled(&config).unwrap();
|
||||
|
||||
assert!(!enabled)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_value_is_enabled() {
|
||||
let config = ActionConfig::try_from(
|
||||
r#"
|
||||
please:
|
||||
actions:
|
||||
rust:
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let enabled = RustAction::new().enabled(&config).unwrap();
|
||||
|
||||
assert!(enabled)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_can_edit_empty_file() {
|
||||
let output = RustAction::default()
|
||||
.execute_content(
|
||||
&semver::Version {
|
||||
major: 0,
|
||||
minor: 1,
|
||||
patch: 0,
|
||||
pre: Prerelease::default(),
|
||||
build: BuildMetadata::default(),
|
||||
},
|
||||
"",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
r#"[workspace.package]
|
||||
version = "0.1.0"
|
||||
"#,
|
||||
&output,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_only_edits_stuff() {
|
||||
let input = r#"
|
||||
[workspace]
|
||||
members = ["."]
|
||||
|
||||
|
||||
[package]
|
||||
something = {some = "something"}
|
||||
|
||||
# Some comment
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.0" # some comment
|
||||
readme = "../../"
|
||||
"#;
|
||||
|
||||
let output = RustAction::default()
|
||||
.execute_content(
|
||||
&semver::Version {
|
||||
major: 0,
|
||||
minor: 1,
|
||||
patch: 0,
|
||||
pre: Prerelease::default(),
|
||||
build: BuildMetadata::default(),
|
||||
},
|
||||
input,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
r#"
|
||||
[workspace]
|
||||
members = ["."]
|
||||
|
||||
|
||||
[package]
|
||||
something = {some = "something"}
|
||||
|
||||
# Some comment
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
readme = "../../"
|
||||
"#,
|
||||
&output,
|
||||
)
|
||||
}
|
||||
}
|
@@ -12,6 +12,7 @@ publishable = true
|
||||
[dependencies]
|
||||
cuddle-please-frontend.workspace = true
|
||||
cuddle-please-misc.workspace = true
|
||||
cuddle-please-actions.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
tracing.workspace = true
|
||||
|
@@ -6,6 +6,7 @@ use std::{
|
||||
};
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use cuddle_please_actions::Actions;
|
||||
use cuddle_please_frontend::{gatheres::ConfigArgs, PleaseConfig, PleaseConfigBuilder};
|
||||
use cuddle_please_misc::{
|
||||
ConsoleUi, DynRemoteGitClient, DynUi, GiteaClient, GlobalArgs, LocalGitClient, StdinFn,
|
||||
@@ -92,16 +93,16 @@ impl Command {
|
||||
pub fn execute(self, current_dir: Option<&Path>) -> anyhow::Result<()> {
|
||||
match &self.commands {
|
||||
Some(Commands::Release {}) => {
|
||||
let (config, git_client, gitea_client) = self.get_deps(current_dir)?;
|
||||
ReleaseCommandHandler::new(self.ui, config, git_client, gitea_client)
|
||||
let (config, git_client, gitea_client, actions) = self.get_deps(current_dir)?;
|
||||
ReleaseCommandHandler::new(self.ui, config, git_client, gitea_client, actions)
|
||||
.execute(self.global.dry_run)?;
|
||||
}
|
||||
Some(Commands::Config { command }) => {
|
||||
let (config, _, _) = self.get_deps(current_dir)?;
|
||||
let (config, _, _, _) = self.get_deps(current_dir)?;
|
||||
ConfigCommandHandler::new(self.ui, config).execute(command)?;
|
||||
}
|
||||
Some(Commands::Gitea { command }) => {
|
||||
let (config, _, gitea_client) = self.get_deps(current_dir)?;
|
||||
let (config, _, gitea_client, _) = self.get_deps(current_dir)?;
|
||||
|
||||
GiteaCommandHandler::new(self.ui, config, gitea_client)
|
||||
.execute(command, self.global.token.expect("token to be set").deref())?;
|
||||
@@ -118,7 +119,7 @@ impl Command {
|
||||
fn get_deps(
|
||||
&self,
|
||||
current_dir: Option<&Path>,
|
||||
) -> anyhow::Result<(PleaseConfig, VcsClient, DynRemoteGitClient)> {
|
||||
) -> anyhow::Result<(PleaseConfig, VcsClient, DynRemoteGitClient, Actions)> {
|
||||
let config = self.build_config(current_dir)?;
|
||||
let git_client =
|
||||
self.get_git(&config, self.global.token.clone().expect("token to be set"))?;
|
||||
@@ -140,7 +141,9 @@ impl Command {
|
||||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||
}
|
||||
|
||||
Ok((config, git_client, gitea_client))
|
||||
let actions = self.get_actions()?;
|
||||
|
||||
Ok((config, git_client, gitea_client, actions))
|
||||
}
|
||||
|
||||
fn build_config(&self, current_dir: Option<&Path>) -> Result<PleaseConfig, anyhow::Error> {
|
||||
@@ -183,6 +186,10 @@ impl Command {
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_actions(&self) -> anyhow::Result<Actions> {
|
||||
Actions::from_cuddle()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
|
@@ -1,3 +1,4 @@
|
||||
use cuddle_please_actions::Actions;
|
||||
use cuddle_please_frontend::PleaseConfig;
|
||||
|
||||
use ::semver::Version;
|
||||
@@ -13,6 +14,7 @@ pub struct ReleaseCommandHandler {
|
||||
config: PleaseConfig,
|
||||
git_client: VcsClient,
|
||||
gitea_client: DynRemoteGitClient,
|
||||
actions: Actions,
|
||||
}
|
||||
|
||||
impl ReleaseCommandHandler {
|
||||
@@ -21,12 +23,14 @@ impl ReleaseCommandHandler {
|
||||
config: PleaseConfig,
|
||||
git_client: VcsClient,
|
||||
gitea_client: DynRemoteGitClient,
|
||||
actions: Actions,
|
||||
) -> Self {
|
||||
Self {
|
||||
ui,
|
||||
config,
|
||||
git_client,
|
||||
gitea_client,
|
||||
actions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +73,8 @@ impl ReleaseCommandHandler {
|
||||
let (changelog_placement, changelog, changelog_last_changes) =
|
||||
compose_changelog(&commit_strs, &next_version, source)?;
|
||||
|
||||
self.actions.execute(&next_version)?;
|
||||
|
||||
if let Some(first_commit) = commit_strs.first() {
|
||||
if first_commit.contains("chore(release): ") {
|
||||
tracing::trace!("creating release");
|
||||
|
Reference in New Issue
Block a user