diff --git a/.github/actions/universal-detect-changes-and-generate-changelog/README.md b/.github/actions/universal-detect-changes-and-generate-changelog/README.md index fe61de1..963033d 100644 --- a/.github/actions/universal-detect-changes-and-generate-changelog/README.md +++ b/.github/actions/universal-detect-changes-and-generate-changelog/README.md @@ -4,6 +4,8 @@ This GitHub Action detects changes since the last built commit and generates a c ## Features +- ✅ **Nested Merge Detection**: Detects ALL merged branches including nested merges (e.g., B→A→develop reports both A and B) +- ✅ **Smart Filtering**: Automatically filters out reverse merges (main→feature) used for conflict resolution - ✅ **Modular Design**: Split into separate bash scripts for better maintainability - ✅ **Customizable Cache Keys**: Support for custom cache key prefixes (format: `latest_builded_commit-` or `{prefix}-latest_builded_commit-`) - ✅ **GitHub-Native**: Leverages GitHub's built-in branch handling (no manual sanitization) @@ -18,6 +20,8 @@ This GitHub Action detects changes since the last built commit and generates a c | `debug` | No | `false` | Enable debug mode for detailed logging | | `fallback_lookback` | No | `"24 hours"` | Time to look back for merge commits when no previous build commit is found | | `cache_key_prefix` | No | - | Custom prefix for cache keys. If not provided, will use `latest_builded_commit-`. If provided, format will be `{prefix}-latest_builded_commit-` | +| `use_git_lfs` | No | `false` | Whether to download Git-LFS files during checkout | +| `exclude_source_branches` | No | `"(main\|develop\|master)"` | Exclude merged commits of given branches. Regex pattern (ERE). Example: `"(release.*\|hotfix.*)"` | ## Outputs @@ -28,6 +32,31 @@ This GitHub Action detects changes since the last built commit and generates a c | `merged_branches` | List of merged branch names | | `cache_key` | Cache key to store latest built commit for this branch | +## Nested Merge Detection + +The action detects **all merged branches** including nested merges where one feature branch merges into another before merging to main. + +### How It Works + +**Example:** Branch B merges into branch A, then A merges into develop +- **Output:** Both `feature-A` and `feature-B` are detected +- **Filtering:** Reverse merges (e.g., `develop→feature-A` for conflict resolution) are automatically excluded + +### Implementation Details + +- **Branch Names**: Uses `git log --merges` (without `--first-parent`) to see all merge commits +- **Changelog Messages**: Uses `git log --merges --first-parent` to follow only main branch history +- **Filtering**: Excludes source branches via `grep -v "Merge branch '(EXCLUDE_SOURCE_BRANCHES)' into"` + +### Performance Considerations + +Removing `--first-parent` for branch detection means git traverses more of the commit graph: +- **Impact**: Minimal for typical workflows (10-100 commits between builds) +- **Large histories**: May add 1-2 seconds for repos with 1000+ commits in the range +- **Recommendation**: Use `checkout_depth` to limit git history fetch depth if needed + +The performance tradeoff is generally acceptable given the improved accuracy in branch detection. + ## Scripts ### `cache-keys.sh` @@ -53,16 +82,17 @@ Determines commit range and skip build logic. - `to_commit`: Ending commit for changelog ### `generate-changelog.sh` -Generates formatted changelog and branch names. +Generates formatted changelog and branch names with nested merge detection. **Environment Variables:** - `FROM_COMMIT`: Starting commit - `TO_COMMIT`: Ending commit +- `EXCLUDE_SOURCE_BRANCHES`: Regex pattern for excluding source branches (default: `(main|develop|master)`) - `DEBUG`: Debug mode flag **Outputs:** -- `changelog_string`: Formatted changelog -- `merged_branches`: List of merged branches +- `changelog_string`: Formatted changelog (from main branch history only) +- `merged_branches`: List of all merged branches (includes nested merges) ## Testing @@ -78,6 +108,7 @@ The action includes comprehensive unit tests using BATS (Bash Automated Testing bats test/test_cache-keys.bats bats test/test_determine-range.bats bats test/test_generate-changelog.bats +bats test/test_merged-branches.bats ``` ### CI Testing @@ -94,6 +125,9 @@ Tests run automatically on pull requests when relevant files change. The CI work - ✅ Commit range determination logic - ✅ Skip build decision making - ✅ Changelog generation and formatting +- ✅ **Nested merge detection** (B→A→develop, C→B→A→develop) +- ✅ **Reverse merge filtering** (conflict resolution exclusion) +- ✅ **Custom target branch patterns** - ✅ Error handling and edge cases - ✅ Debug output functionality - ✅ Git command failure scenarios diff --git a/.github/actions/universal-detect-changes-and-generate-changelog/action.yml b/.github/actions/universal-detect-changes-and-generate-changelog/action.yml index c910133..984178b 100644 --- a/.github/actions/universal-detect-changes-and-generate-changelog/action.yml +++ b/.github/actions/universal-detect-changes-and-generate-changelog/action.yml @@ -24,6 +24,10 @@ inputs: description: 'Whether to download Git-LFS files during checkout. Default is false.' type: boolean default: false + exclude_source_branches: + required: false + description: 'Exclude merged commits of given branches. Regex pattern (ERE). Example: "(release.*|hotfix.*)"' + default: "(main|develop|master)" outputs: skip_build: @@ -91,3 +95,4 @@ runs: FROM_COMMIT: ${{ steps.determine_range.outputs.from_commit }} TO_COMMIT: ${{ steps.determine_range.outputs.to_commit }} DEBUG: ${{ inputs.debug }} + EXCLUDE_SOURCE_BRANCHES: ${{ inputs.exclude_source_branches }} diff --git a/.github/actions/universal-detect-changes-and-generate-changelog/generate-changelog.sh b/.github/actions/universal-detect-changes-and-generate-changelog/generate-changelog.sh index 4e70baf..cef397a 100755 --- a/.github/actions/universal-detect-changes-and-generate-changelog/generate-changelog.sh +++ b/.github/actions/universal-detect-changes-and-generate-changelog/generate-changelog.sh @@ -4,6 +4,7 @@ # Global variables FROM_COMMIT="$FROM_COMMIT" TO_COMMIT="$TO_COMMIT" +EXCLUDE_SOURCE_BRANCHES="${EXCLUDE_SOURCE_BRANCHES:-(main|develop|master)}" FORMATTED_CHANGELOG="" FORMATTED_BRANCH_NAMES="" @@ -15,10 +16,14 @@ debug_log() { } # Get git log for changelog (commit messages) +# NOTE: This intentionally uses --first-parent to follow the main branch history. +# We only want changelog messages from direct merges to the main branch, not from +# nested feature branch merges. Branch name detection (get_branch_names) does NOT +# use --first-parent so it can detect nested branches. get_changelog() { local from_commit="$1" local to_commit="$2" - + if [ "$from_commit" == "$to_commit" ]; then debug_log "FROM_COMMIT is same as HEAD. Using range HEAD~1..HEAD" git log --merges --first-parent --pretty=format:"%b" HEAD~1..HEAD 2>&1 @@ -34,14 +39,16 @@ get_changelog() { get_branch_names() { local from_commit="$1" local to_commit="$2" - + if [ "$from_commit" == "$to_commit" ]; then - git log --merges --first-parent --pretty=format:"%s" HEAD~1..HEAD | \ + git log --merges --pretty=format:"%s" HEAD~1..HEAD | \ + grep -v -E "Merge branch '(${EXCLUDE_SOURCE_BRANCHES})' into" | \ sed -e "s/^Merge branch '//" -e "s/^Merge pull request .* from //" -e "s/' into.*$//" -e "s/ into.*$//" | \ grep -v '^$' 2>&1 || true return 0 else - git log --merges --first-parent --pretty=format:"%s" "${from_commit}..${to_commit}" | \ + git log --merges --pretty=format:"%s" "${from_commit}..${to_commit}" | \ + grep -v -E "Merge branch '(${EXCLUDE_SOURCE_BRANCHES})' into" | \ sed -e "s/^Merge branch '//" -e "s/^Merge pull request .* from //" -e "s/' into.*$//" -e "s/ into.*$//" | \ grep -v '^$' 2>&1 || true return 0 @@ -135,11 +142,14 @@ main() { if [ "$FROM_COMMIT" == "$TO_COMMIT" ]; then debug_log "FROM_COMMIT is same as HEAD. Using range HEAD~1..HEAD" + # Changelog uses --first-parent to follow main branch history only raw_changelog=$(git log --merges --first-parent --pretty=format:"%b" HEAD~1..HEAD 2>&1) git_exit_code=$? - + if [ $git_exit_code -eq 0 ]; then - raw_branch_names=$(git log --merges --first-parent --pretty=format:"%s" HEAD~1..HEAD 2>&1 | \ + # Branch names do NOT use --first-parent to detect nested merges + raw_branch_names=$(git log --merges --pretty=format:"%s" HEAD~1..HEAD 2>&1 | \ + grep -v -E "Merge branch '(${EXCLUDE_SOURCE_BRANCHES})' into" | \ sed -e "s/^Merge branch '//" -e "s/^Merge pull request .* from //" -e "s/' into.*$//" -e "s/ into.*$//" | \ grep -v '^$' 2>&1 || true) git_exit_code=0 @@ -148,11 +158,14 @@ main() { fi else debug_log "Using range ${FROM_COMMIT}..${TO_COMMIT}" + # Changelog uses --first-parent to follow main branch history only raw_changelog=$(git log --merges --first-parent --pretty=format:"%b" "${FROM_COMMIT}..${TO_COMMIT}" 2>&1) git_exit_code=$? - + if [ $git_exit_code -eq 0 ]; then - raw_branch_names=$(git log --merges --first-parent --pretty=format:"%s" "${FROM_COMMIT}..${TO_COMMIT}" 2>&1 | \ + # Branch names do NOT use --first-parent to detect nested merges + raw_branch_names=$(git log --merges --pretty=format:"%s" "${FROM_COMMIT}..${TO_COMMIT}" 2>&1 | \ + grep -v -E "Merge branch '(${EXCLUDE_SOURCE_BRANCHES})' into" | \ sed -e "s/^Merge branch '//" -e "s/^Merge pull request .* from //" -e "s/' into.*$//" -e "s/ into.*$//" | \ grep -v '^$' 2>&1 || true) git_exit_code=0 diff --git a/.github/actions/universal-detect-changes-and-generate-changelog/test/test_merged-branches.bats b/.github/actions/universal-detect-changes-and-generate-changelog/test/test_merged-branches.bats new file mode 100644 index 0000000..d1fa9cc --- /dev/null +++ b/.github/actions/universal-detect-changes-and-generate-changelog/test/test_merged-branches.bats @@ -0,0 +1,470 @@ +#!/usr/bin/env bats + +# Tests specifically for FORMATTED_BRANCH_NAMES output +# These tests focus on the branch name detection and extraction logic, +# separate from the changelog message formatting tests. + +load 'test_helper' + +# ============================================================================= +# NESTED MERGE DETECTION: +# The generate-changelog.sh script now uses `git log --merges` (without --first-parent) +# combined with negative filtering to detect ALL merged branches including nested ones. +# +# The filtering logic: grep -v "Merge branch '(EXCLUDE_SOURCE_BRANCHES)' into" +# - Removes reverse merges (main→feature) used for conflict resolution +# - Keeps all forward merges (feature→feature and feature→main) +# +# This means nested merges (B→A→develop) now correctly detect both A and B! +# The tests below verify this behavior across various scenarios. +# ============================================================================= + +@test "merged-branches: detects nested merge when B is squash-merged to develop via A" { + export FROM_COMMIT="base-commit" + export TO_COMMIT="HEAD" + export DEBUG="false" + export EXCLUDE_SOURCE_BRANCHES="(main|develop|master)" + + # Simulate git history where feature-A squashes commits from feature-B + # Then feature-A merges to develop + # In this case, when A is merged to develop, the changelog includes B's work + # but B never directly merged to develop + git() { + case "$1" in + "log") + if [[ "$*" == *"--pretty=format:%b"* ]]; then + echo "Feature A changelog" + elif [[ "$*" == *"--pretty=format:%s"* ]]; then + # Only the merge to develop is visible since B was squashed into A + echo "Merge branch 'feature-A' into develop" + fi + return 0 + ;; + esac + } + export -f git + + run "$BATS_TEST_DIRNAME/../generate-changelog.sh" + + [ "$status" -eq 0 ] + + # Only feature-A is detected (B was squashed, no separate merge commit) + # This is correct: B's commits are part of A, but B never had its own merge commit + [ "$(grep '^merged_branches=' "$GITHUB_OUTPUT" | cut -d= -f2)" = "feature-A" ] + + # NOTE: If feature-B had a separate merge commit into feature-A, and then + # feature-A merged to develop, BOTH A and B would be detected (see next test). + # This test shows the squash-merge case where B has no separate merge commit. +} + +@test "merged-branches: detects true nested merge B→A→develop with separate merge commits" { + export FROM_COMMIT="base-commit" + export TO_COMMIT="HEAD" + export DEBUG="false" + export EXCLUDE_SOURCE_BRANCHES="(main|develop|master)" + + # Simulate git history where BOTH merges are present: + # 1. feature-B has a merge commit into feature-A + # 2. feature-A has a merge commit into develop + # Without --first-parent, git log sees BOTH merge commits + git() { + case "$1" in + "log") + if [[ "$*" == *"--pretty=format:%b"* ]]; then + echo "Feature A and B work" + elif [[ "$*" == *"--pretty=format:%s"* ]]; then + # Git log --merges (without --first-parent) returns BOTH merges + echo "Merge branch 'feature-B' into feature-A" + echo "Merge branch 'feature-A' into develop" + fi + return 0 + ;; + esac + } + export -f git + + run "$BATS_TEST_DIRNAME/../generate-changelog.sh" + + [ "$status" -eq 0 ] + + # With NEGATIVE filtering: grep -v "Merge branch '(main|develop|master)' into" + # - "Merge branch 'feature-B' into feature-A" is INCLUDED (B is not main/develop/master) + # - "Merge branch 'feature-A' into develop" is INCLUDED (A is not main/develop/master) + # Result: Both feature-A and feature-B are detected + [ "$(grep '^merged_branches=' "$GITHUB_OUTPUT" | cut -d= -f2)" = "feature-A, feature-B" ] + +} + +@test "merged-branches: filters out conflict resolution merges (develop→A, then A→develop)" { + export FROM_COMMIT="base-commit" + export TO_COMMIT="HEAD" + export DEBUG="false" + export EXCLUDE_SOURCE_BRANCHES="(main|develop|master)" + + # Simulate git history: + # 1. develop merges into feature-A (conflict resolution - should be filtered out) + # 2. feature-A merges into develop (actual feature merge - should be detected) + # + # Without --first-parent, git log sees BOTH merges, but negative filtering removes reverse merge + git() { + case "$1" in + "log") + if [[ "$*" == *"--pretty=format:%b"* ]]; then + echo "Feature A implementation" + elif [[ "$*" == *"--pretty=format:%s"* ]]; then + # Both merges are now visible + echo "Merge branch 'develop' into feature-A" + echo "Merge branch 'feature-A' into develop" + fi + return 0 + ;; + esac + } + export -f git + + run "$BATS_TEST_DIRNAME/../generate-changelog.sh" + + [ "$status" -eq 0 ] + + # With NEGATIVE filtering: grep -v "Merge branch '(main|develop|master)' into" + # - "Merge branch 'develop' into feature-A" is FILTERED OUT (develop matches the pattern) + # - "Merge branch 'feature-A' into develop" is INCLUDED (feature-A doesn't match) + # Result: Only feature-A is detected + [ "$(grep '^merged_branches=' "$GITHUB_OUTPUT" | cut -d= -f2)" = "feature-A" ] + + # This is CORRECT behavior - conflict resolution merges (main→feature) are filtered out +} + +@test "merged-branches: multiple nested branches C→B→A→develop" { + export FROM_COMMIT="base-commit" + export TO_COMMIT="HEAD" + export DEBUG="false" + export EXCLUDE_SOURCE_BRANCHES="(main|develop|master)" + + # Simulate git history: + # 1. Branch C merges into branch B + # 2. Branch B merges into branch A + # 3. Branch A merges into develop + # + # Without --first-parent, all merge commits are visible + git() { + case "$1" in + "log") + if [[ "$*" == *"--pretty=format:%b"* ]]; then + echo "Feature A with nested changes" + elif [[ "$*" == *"--pretty=format:%s"* ]]; then + # All merge commits are now visible + echo "Merge branch 'feature-C' into feature-B" + echo "Merge branch 'feature-B' into feature-A" + echo "Merge branch 'feature-A' into develop" + fi + return 0 + ;; + esac + } + export -f git + + run "$BATS_TEST_DIRNAME/../generate-changelog.sh" + + [ "$status" -eq 0 ] + + # Detects all nested branches A, B, and C + [ "$(grep '^merged_branches=' "$GITHUB_OUTPUT" | cut -d= -f2)" = "feature-A, feature-B, feature-C" ] + +} + +@test "merged-branches: parallel merges A and B separately to develop (WORKING CASE)" { + export FROM_COMMIT="base-commit" + export TO_COMMIT="HEAD" + export DEBUG="false" + + # Simulate git history: + # 1. Branch A merges into develop + # 2. Branch B merges into develop + # + # Both are on the first-parent path, so both should be detected + git() { + case "$1" in + "log") + if [[ "$*" == *"--pretty=format:%b"* ]]; then + echo "Feature A implementation" + echo "Feature B implementation" + elif [[ "$*" == *"--pretty=format:%s"* ]]; then + echo "Merge branch 'feature-A' into develop" + echo "Merge branch 'feature-B' into develop" + fi + return 0 + ;; + esac + } + export -f git + + run "$BATS_TEST_DIRNAME/../generate-changelog.sh" + + [ "$status" -eq 0 ] + + # CURRENT BEHAVIOR: Correctly detects both A and B + [ "$(grep '^merged_branches=' "$GITHUB_OUTPUT" | cut -d= -f2)" = "feature-A, feature-B" ] + + # This is the EXPECTED working case - parallel merges work correctly +} + +@test "merged-branches: mixed scenario with nested and parallel merges" { + export FROM_COMMIT="base-commit" + export TO_COMMIT="HEAD" + export DEBUG="false" + export EXCLUDE_SOURCE_BRANCHES="(main|develop|master)" + + # Simulate git history: + # 1. Branch B merges into branch A (nested) + # 2. Branch A merges into develop + # 3. Branch C merges into develop (parallel) + # + # All should be detected now + git() { + case "$1" in + "log") + if [[ "$*" == *"--pretty=format:%b"* ]]; then + echo "Feature A with nested B" + echo "Feature C standalone" + elif [[ "$*" == *"--pretty=format:%s"* ]]; then + echo "Merge branch 'feature-B' into feature-A" + echo "Merge branch 'feature-A' into develop" + echo "Merge branch 'feature-C' into develop" + fi + return 0 + ;; + esac + } + export -f git + + run "$BATS_TEST_DIRNAME/../generate-changelog.sh" + + [ "$status" -eq 0 ] + + # Detects A, B, and C + [ "$(grep '^merged_branches=' "$GITHUB_OUTPUT" | cut -d= -f2)" = "feature-A, feature-B, feature-C" ] + +} + +@test "merged-branches: handles pull request merge format" { + export FROM_COMMIT="base-commit" + export TO_COMMIT="HEAD" + export DEBUG="false" + export EXCLUDE_SOURCE_BRANCHES="(main|develop|master)" + + # Test that pull request merge commits are properly parsed + git() { + case "$1" in + "log") + if [[ "$*" == *"--pretty=format:%b"* ]]; then + echo "PR body content" + elif [[ "$*" == *"--pretty=format:%s"* ]]; then + echo "Merge pull request #123 from org/feature-branch" + fi + return 0 + ;; + esac + } + export -f git + + run "$BATS_TEST_DIRNAME/../generate-changelog.sh" + + [ "$status" -eq 0 ] + + # Should extract the branch name from the pull request format + [ "$(grep '^merged_branches=' "$GITHUB_OUTPUT" | cut -d= -f2)" = "org/feature-branch" ] +} + +@test "merged-branches: handles branch names with special characters" { + export FROM_COMMIT="base-commit" + export TO_COMMIT="HEAD" + export DEBUG="false" + export EXCLUDE_SOURCE_BRANCHES="(main|develop|master)" + + git() { + case "$1" in + "log") + if [[ "$*" == *"--pretty=format:%b"* ]]; then + echo "Feature work" + elif [[ "$*" == *"--pretty=format:%s"* ]]; then + echo "Merge branch 'feature/PROJ-123/add-new-feature' into develop" + echo "Merge branch 'bugfix/HOTFIX-456_critical-fix' into develop" + fi + return 0 + ;; + esac + } + export -f git + + run "$BATS_TEST_DIRNAME/../generate-changelog.sh" + + [ "$status" -eq 0 ] + + # Should handle slashes, hyphens, underscores correctly + [ "$(grep '^merged_branches=' "$GITHUB_OUTPUT" | cut -d= -f2)" = "bugfix/HOTFIX-456_critical-fix, feature/PROJ-123/add-new-feature" ] +} + +@test "merged-branches: deduplicates branch names across merge commits" { + export FROM_COMMIT="base-commit" + export TO_COMMIT="HEAD" + export DEBUG="false" + export EXCLUDE_SOURCE_BRANCHES="(main|develop|master)" + + git() { + case "$1" in + "log") + if [[ "$*" == *"--pretty=format:%b"* ]]; then + echo "Multiple merges" + elif [[ "$*" == *"--pretty=format:%s"* ]]; then + echo "Merge branch 'feature-A' into develop" + echo "Merge branch 'feature-B' into develop" + echo "Merge branch 'feature-A' into develop" # Duplicate + echo "Merge branch 'feature-C' into develop" + echo "Merge branch 'feature-B' into develop" # Duplicate + fi + return 0 + ;; + esac + } + export -f git + + run "$BATS_TEST_DIRNAME/../generate-changelog.sh" + + [ "$status" -eq 0 ] + + # Should deduplicate and sort + [ "$(grep '^merged_branches=' "$GITHUB_OUTPUT" | cut -d= -f2)" = "feature-A, feature-B, feature-C" ] +} + +@test "merged-branches: handles empty output when no merges found" { + export FROM_COMMIT="base-commit" + export TO_COMMIT="HEAD" + export DEBUG="false" + export EXCLUDE_SOURCE_BRANCHES="(main|develop|master)" + + git() { + case "$1" in + "log") + if [[ "$*" == *"--pretty=format:%b"* ]]; then + echo "Some changelog" + elif [[ "$*" == *"--pretty=format:%s"* ]]; then + echo "" # No merge commits + fi + return 0 + ;; + esac + } + export -f git + + run "$BATS_TEST_DIRNAME/../generate-changelog.sh" + + [ "$status" -eq 0 ] + + # Should handle empty branch names gracefully + [ "$(grep '^merged_branches=' "$GITHUB_OUTPUT" | cut -d= -f2)" = "" ] +} + +@test "merged-branches: handles branch names with quotes" { + export FROM_COMMIT="base-commit" + export TO_COMMIT="HEAD" + export DEBUG="false" + export EXCLUDE_SOURCE_BRANCHES="(main|develop|master)" + + git() { + case "$1" in + "log") + if [[ "$*" == *"--pretty=format:%b"* ]]; then + echo "Feature work" + elif [[ "$*" == *"--pretty=format:%s"* ]]; then + echo "Merge branch 'feature-\"quoted\"-name' into develop" + echo "Merge branch 'another-'single'-quoted' into develop" + fi + return 0 + ;; + esac + } + export -f git + + run "$BATS_TEST_DIRNAME/../generate-changelog.sh" + + [ "$status" -eq 0 ] + + # Should preserve quotes in branch names + [ "$(grep '^merged_branches=' "$GITHUB_OUTPUT" | cut -d= -f2)" = "another-'single'-quoted, feature-\"quoted\"-name" ] +} + +@test "merged-branches: uses default target branches when not specified" { + export FROM_COMMIT="base-commit" + export TO_COMMIT="HEAD" + export DEBUG="false" + # Explicitly unset EXCLUDE_SOURCE_BRANCHES to test default fallback + unset EXCLUDE_SOURCE_BRANCHES + + # Test that default (main|develop|master) is used + git() { + case "$1" in + "log") + if [[ "$*" == *"--pretty=format:%b"* ]]; then + echo "Feature work" + elif [[ "$*" == *"--pretty=format:%s"* ]]; then + echo "Merge branch 'develop' into feature-A" # Should be filtered (matches default) + echo "Merge branch 'feature-A' into main" # Should be included + echo "Merge branch 'feature-B' into develop" # Should be included + fi + return 0 + ;; + esac + } + export -f git + + run "$BATS_TEST_DIRNAME/../generate-changelog.sh" + + [ "$status" -eq 0 ] + + # Default pattern (main|develop|master) filters out reverse merges + # - "Merge branch 'develop' into feature-A" is FILTERED OUT (develop matches default) + # - "Merge branch 'feature-A' into main" is INCLUDED + # - "Merge branch 'feature-B' into develop" is INCLUDED + [ "$(grep '^merged_branches=' "$GITHUB_OUTPUT" | cut -d= -f2)" = "feature-A, feature-B" ] + + # This verifies the default fallback works correctly +} + +@test "merged-branches: supports custom target branch pattern" { + export FROM_COMMIT="base-commit" + export TO_COMMIT="HEAD" + export DEBUG="false" + export EXCLUDE_SOURCE_BRANCHES="(release|production)" + + # Test custom target branch filtering + git() { + case "$1" in + "log") + if [[ "$*" == *"--pretty=format:%b"* ]]; then + echo "Release work" + elif [[ "$*" == *"--pretty=format:%s"* ]]; then + echo "Merge branch 'main' into feature-A" # Should be filtered out + echo "Merge branch 'feature-A' into release" # Should be included + echo "Merge branch 'feature-B' into production" # Should be included + echo "Merge branch 'feature-C' into develop" # Should be included (not filtered by custom pattern) + fi + return 0 + ;; + esac + } + export -f git + + run "$BATS_TEST_DIRNAME/../generate-changelog.sh" + + [ "$status" -eq 0 ] + + # The negative filter grep -v "Merge branch '(release|production)' into" + # excludes merges where release or production is the SOURCE branch + # "Merge branch 'main' into feature-A" - source is 'main', doesn't match, INCLUDED + # "Merge branch 'feature-A' into release" - source is 'feature-A', doesn't match, INCLUDED + # Result: All branches are included since none have release/production as source + [ "$(grep '^merged_branches=' "$GITHUB_OUTPUT" | cut -d= -f2)" = "feature-A, feature-B, feature-C, main" ] + + # This demonstrates that EXCLUDE_SOURCE_BRANCHES is configurable for different workflows +}