Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand All @@ -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`
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 }}
Original file line number Diff line number Diff line change
Expand Up @@ -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=""

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading