#!/usr/bin/env rust-script
use std::env;
use std::fs;
use std::io::Write;
use std::process::Command;
use regex::Regex;
fn exec(command: &str, args: &[&str]) -> String {
match Command::new(command).args(args).output() {
Ok(output) => {
if output.status.success() {
String::from_utf8_lossy(&output.stdout).trim().to_string()
} else {
eprintln!("Error executing {} {:?}", command, args);
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
String::new()
}
}
Err(e) => {
eprintln!("Failed to execute {} {:?}: {}", command, args, e);
String::new()
}
}
}
fn exec_silent(command: &str, args: &[&str]) {
let _ = Command::new(command)
.args(args)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
}
fn set_output(name: &str, value: &str) {
if let Ok(output_file) = env::var("GITHUB_OUTPUT") {
if let Ok(mut file) = fs::OpenOptions::new().create(true).append(true).open(&output_file) {
let _ = writeln!(file, "{}={}", name, value);
}
}
println!("{}={}", name, value);
}
fn get_changed_files() -> Vec<String> {
let event_name = env::var("GITHUB_EVENT_NAME").unwrap_or_else(|_| "local".to_string());
if event_name == "pull_request" {
let base_sha = env::var("GITHUB_BASE_SHA").ok();
let head_sha = env::var("GITHUB_HEAD_SHA").ok();
if let (Some(base), Some(head)) = (base_sha, head_sha) {
println!("Comparing PR: {}...{}", base, head);
exec_silent("git", &["fetch", "origin", &base]);
let output = exec("git", &["diff", "--name-only", &base, &head]);
if !output.is_empty() {
return output.lines().filter(|s| !s.is_empty()).map(String::from).collect();
}
}
}
println!("Comparing HEAD^ to HEAD");
let output = exec("git", &["diff", "--name-only", "HEAD^", "HEAD"]);
if output.is_empty() {
println!("HEAD^ not available, listing all files in HEAD");
let output = exec("git", &["ls-tree", "--name-only", "-r", "HEAD"]);
return output.lines().filter(|s| !s.is_empty()).map(String::from).collect();
}
output.lines().filter(|s| !s.is_empty()).map(String::from).collect()
}
fn is_excluded_from_code_changes(file_path: &str) -> bool {
if file_path.ends_with(".md") {
return true;
}
let excluded_folders = ["changelog.d/", "docs/", "experiments/", "examples/"];
for folder in &excluded_folders {
if file_path.starts_with(folder) {
return true;
}
}
false
}
fn main() {
println!("Detecting file changes for CI/CD...\n");
let changed_files = get_changed_files();
println!("Changed files:");
if changed_files.is_empty() {
println!(" (none)");
} else {
for file in &changed_files {
println!(" {}", file);
}
}
println!();
let rs_changed = changed_files.iter().any(|f| f.ends_with(".rs"));
set_output("rs-changed", if rs_changed { "true" } else { "false" });
let toml_changed = changed_files.iter().any(|f| f.ends_with(".toml"));
set_output("toml-changed", if toml_changed { "true" } else { "false" });
let mjs_changed = changed_files.iter().any(|f| f.ends_with(".mjs"));
set_output("mjs-changed", if mjs_changed { "true" } else { "false" });
let docs_changed = changed_files.iter().any(|f| f.ends_with(".md"));
set_output("docs-changed", if docs_changed { "true" } else { "false" });
let workflow_changed = changed_files.iter().any(|f| f.starts_with(".github/workflows/"));
set_output("workflow-changed", if workflow_changed { "true" } else { "false" });
let code_changed_files: Vec<&String> = changed_files
.iter()
.filter(|f| !is_excluded_from_code_changes(f))
.collect();
println!("\nFiles considered as code changes:");
if code_changed_files.is_empty() {
println!(" (none)");
} else {
for file in &code_changed_files {
println!(" {}", file);
}
}
println!();
let code_pattern = Regex::new(r"\.(rs|toml|mjs|js|yml|yaml)$|\.github/workflows/").unwrap();
let code_changed = code_changed_files.iter().any(|f| code_pattern.is_match(f));
set_output("any-code-changed", if code_changed { "true" } else { "false" });
println!("\nChange detection completed.");
}