diff --git a/Cargo.lock b/Cargo.lock index 6380d363..24db798f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,6 +115,15 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -253,6 +262,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "cookie" version = "0.18.1" @@ -288,6 +303,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -363,6 +387,15 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "deranged" version = "0.5.8" @@ -372,6 +405,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -422,7 +466,7 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "ferrflow" -version = "2.2.0" +version = "2.2.2" dependencies = [ "anyhow", "cargo-husky", @@ -431,11 +475,13 @@ dependencies = [ "colored", "criterion", "git2", + "hex", "json5", "regex", "semver", "serde", "serde_json", + "sha2", "tempfile", "toml_edit", "ureq", @@ -565,6 +611,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "1.4.0" @@ -581,6 +633,15 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hybrid-array" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a79f2aff40c18ab8615ddc5caa9eb5b96314aef18fe5823090f204ad988e813" +dependencies = [ + "typenum", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -1218,6 +1279,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1379,6 +1451,12 @@ version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "ucd-trie" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index 1b322a98..239c9290 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,8 @@ clap = { version = "4", features = ["derive", "env"], optional = true } colored = { version = "3", optional = true } git2 = { version = "0.20", features = ["vendored-libgit2", "vendored-openssl"], optional = true } ureq = { version = "3", features = ["json"], optional = true } +sha2 = "0.11.0" +hex = "0.4.3" [dev-dependencies] cargo-husky = { version = "1", default-features = false, features = ["user-hooks"] } diff --git a/src/config.rs b/src/config.rs index 67e97154..cd8663cf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -728,7 +728,7 @@ pub fn init(format: Option) -> Result<()> { println!("Run: ferrflow check"); if config.workspace.telemetry { - telemetry::send_event(telemetry::EventType::Init, None, None, None); + telemetry::send_event(telemetry::EventType::Init, None, None, None, None); } Ok(()) diff --git a/src/monorepo.rs b/src/monorepo.rs index f47f1ac1..69a88661 100644 --- a/src/monorepo.rs +++ b/src/monorepo.rs @@ -28,7 +28,7 @@ pub fn check(config_path: Option<&Path>, verbose: bool) -> Result<()> { let result = run_release_logic(&root, &config, true, verbose); if config.workspace.telemetry { - telemetry::send_event(telemetry::EventType::Check, None, None, None); + telemetry::send_event(telemetry::EventType::Check, None, None, None, None); } result @@ -79,8 +79,8 @@ fn run_release_logic(root: &Path, config: &Config, dry_run: bool, verbose: bool) let mut any_bumped = false; let mut files_to_commit: Vec = Vec::new(); - // (tag_name, tag_msg, body, pkg_name, version) - let mut tags_to_create: Vec<(String, String, String, String, String)> = Vec::new(); + // (tag_name, tag_msg, body, pkg_name, version, commits_count) + let mut tags_to_create: Vec<(String, String, String, String, String, i32)> = Vec::new(); let mut hook_contexts: Vec<(HookContext, usize)> = Vec::new(); // (ctx, pkg_index) for (pkg_idx, pkg) in config.packages.iter().enumerate() { @@ -280,6 +280,7 @@ fn run_release_logic(root: &Path, config: &Config, dry_run: bool, verbose: bool) Some(&pkg.name), Some(&new_version), None, + Some(commits.len() as i32), ); } @@ -290,6 +291,7 @@ fn run_release_logic(root: &Path, config: &Config, dry_run: bool, verbose: bool) body, pkg.name.clone(), new_version.clone(), + commits.len() as i32, )); } @@ -327,7 +329,7 @@ fn run_release_logic(root: &Path, config: &Config, dry_run: bool, verbose: bool) // Build the release commit message. let release_parts: Vec = tags_to_create .iter() - .map(|(_, _, _, name, ver)| format!("{name} v{ver}")) + .map(|(_, _, _, name, ver, _)| format!("{name} v{ver}")) .collect(); let skip_ci = if config.workspace.effective_skip_ci() { " [skip ci]" @@ -362,7 +364,7 @@ fn run_release_logic(root: &Path, config: &Config, dry_run: bool, verbose: bool) "Automated release commit.\n\n{}", tags_to_create .iter() - .map(|(tag, _, _, _, _)| format!("- `{tag}`")) + .map(|(tag, _, _, _, _, _)| format!("- `{tag}`")) .collect::>() .join("\n") ); @@ -400,7 +402,7 @@ fn run_release_logic(root: &Path, config: &Config, dry_run: bool, verbose: bool) } // Tags are always created on the current HEAD. - for (tag_name, tag_msg, _, _, _) in &tags_to_create { + for (tag_name, tag_msg, _, _, _, _) in &tags_to_create { create_tag(&repo, tag_name, tag_msg)?; println!(" ✓ Created tag {}", tag_name.cyan()); } @@ -429,7 +431,7 @@ fn run_release_logic(root: &Path, config: &Config, dry_run: bool, verbose: bool) // Push tags (and branch for commit mode). let tag_refs: Vec<&str> = tags_to_create .iter() - .map(|(t, _, _, _, _)| t.as_str()) + .map(|(t, _, _, _, _, _)| t.as_str()) .collect(); match mode { ReleaseCommitMode::Commit => { @@ -458,12 +460,13 @@ fn run_release_logic(root: &Path, config: &Config, dry_run: bool, verbose: bool) } if config.workspace.telemetry { - for (_, _, _, pkg_name, version) in &tags_to_create { + for (_, _, _, pkg_name, version, commit_count) in &tags_to_create { telemetry::send_event( telemetry::EventType::Release, Some(pkg_name), Some(version), None, + Some(*commit_count), ); } } @@ -471,7 +474,7 @@ fn run_release_logic(root: &Path, config: &Config, dry_run: bool, verbose: bool) if let Ok(token) = std::env::var("GITHUB_TOKEN") && let Some(slug) = get_repo_slug(&repo, &config.workspace.remote) { - for (tag_name, _, body, _, _) in &tags_to_create { + for (tag_name, _, body, _, _, _) in &tags_to_create { match create_github_release(&token, &slug, tag_name, body) { Ok(()) => println!(" ✓ GitHub Release {}", tag_name.cyan()), Err(err) => eprintln!( @@ -493,7 +496,7 @@ fn run_release_logic(root: &Path, config: &Config, dry_run: bool, verbose: bool) .open(&summary_path) { let _ = writeln!(file, "## Released\n"); - for (tag_name, _, body, _, _) in &tags_to_create { + for (tag_name, _, body, _, _, _) in &tags_to_create { let _ = writeln!(file, "### {tag_name}\n"); let _ = writeln!(file, "{body}"); } diff --git a/src/telemetry.rs b/src/telemetry.rs index 3beeae2d..6895f726 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -1,4 +1,5 @@ use serde::Serialize; +use sha2::Digest; const DEFAULT_API_URL: &str = "https://api.ferrflow.com"; @@ -22,6 +23,10 @@ struct EventPayload { package_version: Option, #[serde(skip_serializing_if = "Option::is_none")] metadata: Option, + #[serde(skip_serializing_if = "Option::is_none")] + repo_hash: Option, + #[serde(skip_serializing_if = "Option::is_none")] + commits_count: Option, } fn is_enabled() -> bool { @@ -39,11 +44,28 @@ fn api_url() -> String { std::env::var("FERRFLOW_API_URL").unwrap_or_else(|_| DEFAULT_API_URL.to_string()) } +fn get_repo_hash() -> Option { + let output = std::process::Command::new("git") + .args(["remote", "get-url", "origin"]) + .output() + .ok()?; + if !output.status.success() { + return None; + } + let url = String::from_utf8(output.stdout).ok()?.trim().to_string(); + if url.is_empty() { + return None; + } + let hash = sha2::Sha256::digest(url.as_bytes()); + Some(hex::encode(hash)) +} + pub fn send_event( event_type: EventType, package_name: Option<&str>, package_version: Option<&str>, metadata: Option, + commits_count: Option, ) { if !is_enabled() { return; @@ -54,6 +76,8 @@ pub fn send_event( package_name: package_name.map(String::from), package_version: package_version.map(String::from), metadata, + repo_hash: get_repo_hash(), + commits_count, }; let url = format!("{}/events", api_url()); @@ -99,11 +123,15 @@ mod tests { package_name: None, package_version: None, metadata: None, + repo_hash: None, + commits_count: None, }; let json = serde_json::to_value(&payload).unwrap(); assert!(!json.as_object().unwrap().contains_key("package_name")); assert!(!json.as_object().unwrap().contains_key("package_version")); assert!(!json.as_object().unwrap().contains_key("metadata")); + assert!(!json.as_object().unwrap().contains_key("repo_hash")); + assert!(!json.as_object().unwrap().contains_key("commits_count")); } #[test] @@ -113,11 +141,15 @@ mod tests { package_name: Some("my-pkg".into()), package_version: Some("1.0.0".into()), metadata: None, + repo_hash: Some("abc123".into()), + commits_count: Some(42), }; let json = serde_json::to_value(&payload).unwrap(); assert_eq!(json["event_type"], "release"); assert_eq!(json["package_name"], "my-pkg"); assert_eq!(json["package_version"], "1.0.0"); + assert_eq!(json["repo_hash"], "abc123"); + assert_eq!(json["commits_count"], 42); } #[test]