diff --git a/.github/workflows/generate-diff.yml b/.github/workflows/generate-diff-exmaple-change.yml similarity index 92% rename from .github/workflows/generate-diff.yml rename to .github/workflows/generate-diff-exmaple-change.yml index 24daf855..f65a8793 100644 --- a/.github/workflows/generate-diff.yml +++ b/.github/workflows/generate-diff-exmaple-change.yml @@ -1,11 +1,11 @@ -name: Generate Diff +name: Generate Diff Example Change on: pull_request: branches: - "main" paths: - - ".github/workflows/generate-diff.yml" + - ".github/workflows/generate-diff-exmaple-change.yml" - "examples/**" jobs: diff --git a/.github/workflows/generate-diff-tool-change.yml b/.github/workflows/generate-diff-tool-change.yml new file mode 100644 index 00000000..99798075 --- /dev/null +++ b/.github/workflows/generate-diff-tool-change.yml @@ -0,0 +1,49 @@ +name: Generate Diff Tool Change + +on: + pull_request: + branches: + - "main" + paths: + - "src/**" + - Cargo.toml + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - uses: actions/checkout@v4 + with: + path: pull-request + + - uses: actions/checkout@v4 + with: + ref: main + path: main + + - name: Print Dirs + run: | + ls -la pull-request + ls -la main + + - name: Generate Diff + run: | + docker build pull-request -f pull-request/Dockerfile_AMD64 -t image + docker run \ + --network=host \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v $(pwd)/main:/base-branch \ + -v $(pwd)/pull-request:/target-branch \ + -v $(pwd)/output:/output \ + image --debug + + - name: Post diff as comment + run: | + gh pr comment ${{ github.event.number }} --repo ${{ github.repository }} --body-file output/diff.md --edit-last || \ + gh pr comment ${{ github.event.number }} --repo ${{ github.repository }} --body-file output/diff.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Dockerfile_AMD64 b/Dockerfile_AMD64 index 28f700e5..3f123bbd 100644 --- a/Dockerfile_AMD64 +++ b/Dockerfile_AMD64 @@ -52,5 +52,8 @@ RUN apt-get update && apt-get install -y git # copy argocd helm chart values COPY ./argocd-config ./argocd-config +RUN git config --global --add safe.directory /base-branch +RUN git config --global --add safe.directory /target-branch + # set the startup command to run your binary ENTRYPOINT ["./argocd-diff-preview"] \ No newline at end of file diff --git a/Dockerfile_ARM64 b/Dockerfile_ARM64 index 4845502e..f7b682da 100644 --- a/Dockerfile_ARM64 +++ b/Dockerfile_ARM64 @@ -52,5 +52,8 @@ RUN apt-get update && apt-get install -y git # copy argocd helm chart values COPY ./argocd-config ./argocd-config +RUN git config --global --add safe.directory /base-branch +RUN git config --global --add safe.directory /target-branch + # set the startup command to run your binary ENTRYPOINT ["./argocd-diff-preview"] \ No newline at end of file diff --git a/makefile b/makefile index a1da078d..f32d38cb 100644 --- a/makefile +++ b/makefile @@ -7,11 +7,11 @@ timeout := 120 pull-repostory: @rm -rf base-branch || true && mkdir -p base-branch @rm -rf target-branch || true && mkdir -p target-branch - cd base-branch && gh repo clone $(github_org)/$(gitops_repo) -- --depth=1 --branch "$(base_branch)" && cp -r $(gitops_repo)/. . && rm -rf .git && echo "*" > .gitignore && rm -rf $(gitops_repo) && cd - - cd target-branch && gh repo clone $(github_org)/$(gitops_repo) -- --depth=1 --branch "$(target_branch)" && cp -r $(gitops_repo)/. . && rm -rf .git && echo "*" > .gitignore && rm -rf $(gitops_repo) && cd - + cd base-branch && gh repo clone $(github_org)/$(gitops_repo) -- --depth=1 --branch "$(base_branch)" && cp -r $(gitops_repo)/. . && echo "*" > .gitignore && rm -rf $(gitops_repo) && cd - + cd target-branch && gh repo clone $(github_org)/$(gitops_repo) -- --depth=1 --branch "$(target_branch)" && cp -r $(gitops_repo)/. . && echo "*" > .gitignore && rm -rf $(gitops_repo) && cd - local-test-cargo: pull-repostory - cargo run -- -b "$(base_branch)" -t "$(target_branch)" --repo $(github_org)/$(gitops_repo) -r "$(regex)" --debug --diff-ignore "$(diff-ignore)" --timeout $(timeout) -l "$(selector)" + cargo run -- -r "$(regex)" --debug --diff-ignore "$(diff-ignore)" --timeout $(timeout) -l "$(selector)" local-test-docker: pull-repostory docker build . -f $(docker_file) -t image @@ -23,9 +23,6 @@ local-test-docker: pull-repostory -v $(PWD)/target-branch:/target-branch \ -v $(PWD)/output:/output \ -v $(PWD)/secrets:/secrets \ - -e BASE_BRANCH=$(base_branch) \ - -e TARGET_BRANCH=$(target_branch) \ - -e REPO=$(github_org)/$(gitops_repo) \ -e FILE_REGEX="$(regex)" \ -e DIFF_IGNORE="$(diff-ignore)" \ -e TIMEOUT=$(timeout) \ diff --git a/src/argocd.rs b/src/argocd.rs index 908ddb3f..1be7a80d 100644 --- a/src/argocd.rs +++ b/src/argocd.rs @@ -103,7 +103,7 @@ pub async fn install_argo_cd(options: ArgoCDOptions<'_>) -> Result<(), Box = None; let mut counter = 0; while password_encoded.is_none() { - password_encoded = match run_command(&command, None).await { + password_encoded = match run_command(command, None).await { Ok(a) => Some(a), Err(e) => { if counter == 5 { @@ -121,10 +121,8 @@ pub async fn install_argo_cd(options: ArgoCDOptions<'_>) -> Result<(), Box String { .trim_start() .to_string() } + +fn folder_is_git_repo(branch: &Branch) -> bool { + check_if_folder_exists(&format!("{}/.git", branch.folder())) +} + +pub async fn get_repo_name(branch: Branch) -> Option { + if !folder_is_git_repo(&branch) { + debug!("Folder {} does not contain a .git folder", branch.folder()); + return None; + } + + let repo_name = match run_command("git remote get-url origin", Some(branch.folder())).await { + Ok(o) => o.stdout, + Err(e) => { + debug!( + "Error getting repo name for {}: {}", + branch, + String::from_utf8_lossy(&e.stderr) + ); + return None; + } + }; + + let repo_name = String::from_utf8_lossy(&repo_name); + + debug!("Repo name: {} for branch: {}", repo_name, branch); + + let repo_url_regex: Regex = Regex::new(r"[:/](?P[^/]+/[^/]+)(\.git)?$").unwrap(); + + match repo_url_regex.captures(repo_name.as_ref()) { + Some(capture) => { + let repo_name = capture.name("repo").unwrap().as_str(); + Some(repo_name.trim().to_string()) + } + None => { + debug!("Failed to capture repo name from: {}", repo_name); + None + } + } +} + +pub async fn get_branch_name(branch: Branch) -> Option { + if !folder_is_git_repo(&branch) { + return None; + } + + let branch_name = + match run_command("git rev-parse --abbrev-ref HEAD", Some(branch.folder())).await { + Ok(o) => o.stdout, + Err(e) => { + debug!( + "Error getting branch name for {}: {}", + branch, + String::from_utf8_lossy(&e.stderr) + ); + return None; + } + }; + + Some(String::from_utf8_lossy(&branch_name).trim().to_string()) +} diff --git a/src/main.rs b/src/main.rs index 4464be0f..997af1b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use crate::utils::{check_if_folder_exists, create_folder_if_not_exists, run_command}; -use log::{debug, error, info}; +use log::{debug, error, info, warn}; use regex::Regex; use std::fs; use std::path::PathBuf; @@ -48,17 +48,17 @@ struct Opt { #[structopt(long, env)] argocd_chart_version: Option, - /// Base branch name - #[structopt(short, long, default_value = "main", env)] - base_branch: String, + /// Base branch name. If not provided, it will be auto-detected from .git folder in base-branch folder + #[structopt(short, long, env)] + base_branch: Option, - /// Target branch name + /// Target branch name. If not provided, it will be auto-detected from .git folder in target-branch folder #[structopt(short, long, env)] - target_branch: String, + target_branch: Option, - /// Git Repository. Format: OWNER/REPO - #[structopt(long = "repo", env)] - repo: String, + /// Git Repository. Format: OWNER/REPO. If not provided, it will be auto-detected from .git folder in base-branch folder + #[structopt(long, env)] + repo: Option, /// Output folder where the diff will be saved #[structopt(short, long, default_value = "./output", env)] @@ -91,6 +91,15 @@ enum Branch { Target, } +impl Branch { + fn folder(&self) -> &str { + match self { + Branch::Base => "base-branch", + Branch::Target => "target-branch", + } + } +} + impl std::fmt::Display for Branch { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { @@ -133,8 +142,6 @@ fn apps_file(branch: &Branch) -> &'static str { } } -const BASE_BRANCH_FOLDER: &str = "base-branch"; -const TARGET_BRANCH_FOLDER: &str = "target-branch"; const CLUSTER_NAME: &str = "argocd-diff-preview"; #[tokio::main] @@ -161,19 +168,7 @@ async fn main() -> Result<(), Box> { .filter(|f| !f.trim().is_empty()) .map(|f| Regex::new(&f).unwrap()); - let base_branch_name = opt.base_branch; - let target_branch_name = opt.target_branch; - let repo = opt.repo; - let diff_ignore = opt.diff_ignore.filter(|f| !f.trim().is_empty()); - let timeout = opt.timeout; - let output_folder = opt.output_folder.as_str(); - let secrets_folder = opt.secrets_folder.as_str(); - let line_count = opt.line_count; - let argocd_version = opt - .argocd_chart_version - .as_deref() - .filter(|f| !f.trim().is_empty()); - let max_diff_length = opt.max_diff_length; + info!("✨ Running with:"); // select local cluster tool let tool = match opt.local_cluster_tool { @@ -187,19 +182,24 @@ async fn main() -> Result<(), Box> { } }; - let repo_regex = Regex::new(r"^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$").unwrap(); - if !repo_regex.is_match(&repo) { - error!("❌ Invalid repository format. Please use OWNER/REPO"); - panic!("Invalid repository format"); - } - - info!("✨ Running with:"); info!("✨ - local-cluster-tool: {:?}", tool); - info!("✨ - base-branch: {}", base_branch_name); - info!("✨ - target-branch: {}", target_branch_name); + + let (repo_name, base_branch_name, target_branch_name) = + repo_and_branch_config(opt.repo, opt.base_branch, opt.target_branch).await; + + let diff_ignore = opt.diff_ignore.filter(|f| !f.trim().is_empty()); + let timeout = opt.timeout; + let output_folder = opt.output_folder.as_str(); + let secrets_folder = opt.secrets_folder.as_str(); + let line_count = opt.line_count; + let argocd_version = opt + .argocd_chart_version + .as_deref() + .filter(|f| !f.trim().is_empty()); + let max_diff_length = opt.max_diff_length; + info!("✨ - secrets-folder: {}", secrets_folder); info!("✨ - output-folder: {}", output_folder); - info!("✨ - repo: {}", repo); info!("✨ - timeout: {} seconds", timeout); if let Some(a) = file_regex.clone() { info!("✨ - file-regex: {}", a.as_str()); @@ -273,18 +273,18 @@ async fn main() -> Result<(), Box> { ); } - if !check_if_folder_exists(&BASE_BRANCH_FOLDER) { + if !check_if_folder_exists(Branch::Base.folder()) { error!( "❌ Base branch folder does not exist: {}", - BASE_BRANCH_FOLDER + Branch::Base.folder() ); panic!("Base branch folder does not exist"); } - if !check_if_folder_exists(&TARGET_BRANCH_FOLDER) { + if !check_if_folder_exists(Branch::Target.folder()) { error!( "❌ Target branch folder does not exist: {}", - TARGET_BRANCH_FOLDER + Branch::Target.folder() ); panic!("Target branch folder does not exist"); } @@ -292,10 +292,10 @@ async fn main() -> Result<(), Box> { let cluster_name = CLUSTER_NAME; match tool { - ClusterTool::Kind => kind::create_cluster(&cluster_name).await?, + ClusterTool::Kind => kind::create_cluster(cluster_name).await?, ClusterTool::Minikube => minikube::create_cluster().await?, } - + argocd::install_argo_cd(argocd::ArgoCDOptions { version: argocd_version, debug: opt.debug, @@ -312,22 +312,20 @@ async fn main() -> Result<(), Box> { } } - // remove .git from repo - let repo = repo.trim_end_matches(".git"); let base_apps = parsing::get_applications_as_string( - BASE_BRANCH_FOLDER, + Branch::Base.folder(), &base_branch_name, &file_regex, &selector, - repo, + &repo_name, ) .await?; let target_apps = parsing::get_applications_as_string( - TARGET_BRANCH_FOLDER, + Branch::Target.folder(), &target_branch_name, &file_regex, &selector, - repo, + &repo_name, ) .await?; @@ -343,7 +341,7 @@ async fn main() -> Result<(), Box> { extract::get_resources(&Branch::Target, timeout, output_folder).await?; match tool { - ClusterTool::Kind => kind::delete_cluster(&cluster_name), + ClusterTool::Kind => kind::delete_cluster(cluster_name), ClusterTool::Minikube => minikube::delete_cluster(), } @@ -404,3 +402,70 @@ fn apply_folder(folder_name: &str) -> Result { } Ok(count) } + +async fn repo_and_branch_config( + base_repo_option: Option, + base_branch_name_option: Option, + target_branch_name_option: Option, +) -> (String, String, String) { + let repo_regex = Regex::new(r"^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$").unwrap(); + + let repo_name = match (base_repo_option, diff::get_repo_name(Branch::Base).await) { + (Some(r), _) if repo_regex.is_match(&r) => { + info!("✨ - repo: {}", r); + r + } + (Some(_), _) => { + error!("❌ Invalid repository format. Please use OWNER/REPO"); + panic!("Invalid repository format"); + } + (None, Some(r)) => { + info!("✨ - repo: {} (Auto Detected)", r); + r + } + _ => { + warn!("🚨 Failed to autodetect repository from .git folder"); + error!("❌ Please provide the repository with --repo in the format OWNER/REPO"); + panic!("Repository not provided and not autodetected in in .git folder") + } + }; + + let base_branch_name = match ( + base_branch_name_option, + diff::get_branch_name(Branch::Base).await, + ) { + (Some(b), _) => { + info!("✨ - base-branch: {}", b); + b + } + (None, Some(b)) => { + info!("✨ - base-branch: {} (Auto Detected)", b); + b + } + _ => { + warn!("🚨 Failed to autodetect base-branch name from .git folder"); + error!("❌ Please provide the base branch name with --base-branch"); + panic!("Base branch name not provided and not found in git remotes") + } + }; + + let target_branch_name = match ( + target_branch_name_option, + diff::get_branch_name(Branch::Target).await, + ) { + (Some(b), _) => { + info!("✨ - target-branch: {}", b); + b + } + (None, Some(b)) => { + info!("✨ - base-branch: {} (Auto Detected)", b); + b + } + _ => { + warn!("🚨 Failed to autodetect target-branch name from .git folder"); + error!("❌ Please provide the target branch name with --target-branch"); + panic!("Target branch name not provided and not found in git remotes") + } + }; + (repo_name, base_branch_name, target_branch_name) +}