diff --git a/Dockerfile b/Dockerfile index 6da91d6..7efd731 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,10 @@ -FROM alpine:3.21.2 +FROM alpine:3.21.3 RUN apk update && apk --no-cache add bash curl git SHELL ["/bin/bash", "-o", "pipefail", "-c"] -ARG KUSTOMIZE=5.4.3 +ARG KUSTOMIZE=5.6.0 RUN curl -sL https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${KUSTOMIZE}/kustomize_v${KUSTOMIZE}_linux_amd64.tar.gz | \ tar xz && mv kustomize /usr/local/bin/kustomize diff --git a/Makefile b/Makefile index 912b493..5cd38bb 100644 --- a/Makefile +++ b/Makefile @@ -16,4 +16,4 @@ build: docker buildx build --build-arg BUILDPLATFORM=$(BUILDPLATFORM) --build-arg TARGETARCH=$(GOARCH) -t local/$(PROJNAME) . scan: build - trivy --light -s "UNKNOWN,MEDIUM,HIGH,CRITICAL" --exit-code 1 local/$(PROJNAME) + trivy image -s "UNKNOWN,MEDIUM,HIGH,CRITICAL" --exit-code 1 local/$(PROJNAME) diff --git a/README.md b/README.md index 1e05108..969635d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ Managing Kubernetes configurations across multiple environments and PRs can be c ## Features - Automatically builds Kustomize configurations from both PR branches -- Generates a diff between base and head configurations +- Generates a diff showing what would actually change upon merge (not just differences between branches) +- Intelligently handles parallel changes to base and feature branches - Configurable root directory and search depth for Kustomize files - Commits must meet [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) - Automated with GitHub Actions ([commit-lint](https://github.com/conventional-changelog/commitlint/#what-is-commitlint)) @@ -22,6 +23,16 @@ Managing Kubernetes configurations across multiple environments and PRs can be c - Commits must be signed with [Developer Certificate of Origin (DCO)](https://developercertificate.org/) - Automated with GitHub App ([DCO](https://github.com/apps/dco)) +## How It Works +Unlike simple diff tools, this action: + +1. Builds Kustomize output from the base branch +1. Creates a temporary merge of the head branch into base (simulating the actual PR merge) +1. Builds Kustomize output from the merged state +1. Compares these outputs to show exactly what would change after merging + +This approach correctly handles cases where changes have been made to the base branch in parallel to the feature branch, avoiding misleading diffs that incorrectly suggest changes would be reverted. + ## Inputs | Name | Description | Required | Default | diff --git a/kustdiff b/kustdiff index 0ff25ef..c659440 100755 --- a/kustdiff +++ b/kustdiff @@ -39,23 +39,22 @@ function safe_filename() { echo "$1" | sed 's/[^a-zA-Z0-9.]/_/g' } -function build { +function build_ref { local ref="$1" - local safe_ref=$(safe_filename "$ref") + local output_dir="$2" echo "Checking out ref: $ref" git checkout "$ref" --quiet - mkdir -p "$TMP_DIR/$safe_ref" + mkdir -p "$output_dir" for envpath in $(get_targets); do local relative_path="${envpath#$ROOT_DIR/}" local safe_path=$(safe_filename "$relative_path") - local output_file="$TMP_DIR/$safe_ref/${safe_path}.yaml" + local output_file="$output_dir/${safe_path}.yaml" echo "Running kustomize for $envpath" kustomize build "$envpath" -o "$output_file" -# debug_log "Contents of $output_file:" -# debug_log "$(cat "$output_file")" -# debug_log "End of $output_file" -# debug_log "------------------------------------" + if [ "$DEBUG" = "true" ]; then + debug_log "Built kustomize for $envpath to $output_file" + fi done } @@ -64,30 +63,99 @@ function main { validate_root_dir validate_max_depth - git config --global --add safe.directory "$GITHUB_WORKSPACE" - local diff escaped_output output - build "$INPUT_HEAD_REF" - build "$INPUT_BASE_REF" + # Set up git identity for merge operations + git config --global user.email "github-actions@github.com" || true + git config --global user.name "GitHub Actions" || true + + git config --global --add safe.directory "$GITHUB_WORKSPACE" || true + + # Save current state to restore later + local current_branch + current_branch=$(git rev-parse --abbrev-ref HEAD || echo "detached") + + # Check if the branch exists locally or as a remote reference + function resolve_branch_ref() { + local branch_name="$1" + + # First check if it's a local branch + if git show-ref --verify --quiet "refs/heads/$branch_name"; then + echo "$branch_name" + return 0 + fi + + # Next check if it's a remote branch + if git show-ref --verify --quiet "refs/remotes/origin/$branch_name"; then + echo "origin/$branch_name" + return 0 + fi + + # Finally check if it's a valid commit SHA + if git cat-file -e "$branch_name^{commit}" 2>/dev/null; then + echo "$branch_name" + return 0 + fi + + # If we get here, we couldn't resolve the reference + echo "Error: Could not resolve reference: $branch_name" + return 1 + } + + # Resolve the BASE and HEAD references + local base_ref_resolved + base_ref_resolved=$(resolve_branch_ref "$INPUT_BASE_REF") || exit 1 + debug_log "Resolved base reference: $base_ref_resolved (from $INPUT_BASE_REF)" + + # Build BASE output + local safe_base_ref=$(safe_filename "$INPUT_BASE_REF") + local base_output_dir="$TMP_DIR/base" + build_ref "$base_ref_resolved" "$base_output_dir" + + # Resolve HEAD reference + local head_ref_resolved + head_ref_resolved=$(resolve_branch_ref "$INPUT_HEAD_REF") || exit 1 + debug_log "Resolved head reference: $head_ref_resolved (from $INPUT_HEAD_REF)" + + # Create a temporary merge branch + local merge_branch="temp-merge-$RANDOM" + git checkout -b "$merge_branch" "$base_ref_resolved" --quiet + + debug_log "Creating temporary merge of $head_ref_resolved into $base_ref_resolved (via $merge_branch)" + + # Attempt to merge HEAD into BASE + if ! git merge "$head_ref_resolved" --quiet; then + echo "Merge conflict detected. Cannot automatically merge $INPUT_HEAD_REF into $INPUT_BASE_REF." + git merge --abort || true + git checkout "$current_branch" --quiet || git checkout "$base_ref_resolved" --quiet || true + exit 1 + fi - local safe_head_ref=$(safe_dirname "$INPUT_HEAD_REF") - local safe_base_ref=$(safe_dirname "$INPUT_BASE_REF") + # Build merged output + local merged_output_dir="$TMP_DIR/merged" + build_ref "$merge_branch" "$merged_output_dir" + # Compare outputs set +e - diff=$(git diff --no-index "$TMP_DIR/$safe_base_ref" "$TMP_DIR/$safe_head_ref") + diff=$(git diff --no-index "$base_output_dir" "$merged_output_dir" 2>&1) + local diff_exit_code=$? + debug_log "Git diff exit code: $diff_exit_code" debug_log "Git diff output:" debug_log "$diff" debug_log "End of git diff output" debug_log "------------------------------------" - if [[ -z "$diff" ]]; then - output="No differences found between $INPUT_BASE_REF and $INPUT_HEAD_REF" + # Clean up temporary branches + git checkout "$current_branch" --quiet || git checkout "$base_ref_resolved" --quiet || true + git branch -D "$merge_branch" --quiet || true + + if [[ $diff_exit_code -eq 0 ]]; then + output="No differences found in kustomize output after merging $INPUT_HEAD_REF into $INPUT_BASE_REF" else # Just pass through the raw git diff output output="$diff" fi - escaped_output=${output//$'\n'/'%0A'} + local escaped_output=${output//$'\n'/'%0A'} if [ ${#escaped_output} -gt 65000 ]; then escaped_output="Output is greater than 65000 characters, and therefore too large to print as a github comment."