diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b9385d..1114ceb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.2.2 + +### Fixes + +- Forward sync no longer skips the entire action when the checked-out HEAD commit has the configured sync trailer. This allows valid multi-hop setups where one repo receives sync commits from an upstream repository and forwards its filtered subtree to another repository. Reverse sync still filters bot commits by trailer for loop prevention. + ## 2.2.1 ### Fixes diff --git a/VERSION b/VERSION index c043eea..b1b25a5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.1 +2.2.2 diff --git a/action.yml b/action.yml index 30d7bb2..37b4ffa 100644 --- a/action.yml +++ b/action.yml @@ -38,21 +38,7 @@ runs: command -v "$cmd" &>/dev/null || { echo "::error::Missing required tool: $cmd"; exit 1; } done - - name: Loop guard (forward) - id: guard - if: inputs.direction == 'forward' || inputs.direction == 'both' - shell: bash - run: | - TRAILER=$(yq '.bot.trailer' "${{ inputs.config }}" 2>/dev/null || echo "Josh-Sync-Origin") - if git log -1 --format=%B | grep -q "^${TRAILER}:"; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "::notice::Skipping sync — HEAD commit has sync trailer (loop prevention)" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - name: Sync - if: steps.guard.outputs.skip != 'true' shell: bash env: JOSH_SYNC_DEBUG: ${{ inputs.debug == 'true' && '1' || '0' }} diff --git a/docs/adr/005-git-trailer-loop-prevention.md b/docs/adr/005-git-trailer-loop-prevention.md index c4b79e7..791d49f 100644 --- a/docs/adr/005-git-trailer-loop-prevention.md +++ b/docs/adr/005-git-trailer-loop-prevention.md @@ -17,7 +17,7 @@ Bidirectional sync creates an infinite loop risk: forward sync pushes commit A t ## Decision -All bot commits include a git trailer with a configurable key (default: `Josh-Sync-Origin`). Both sync directions filter out commits containing this trailer. +All bot commits include a git trailer with a configurable key (default: `Josh-Sync-Origin`). Reverse sync filters out commits containing this trailer so bot-generated forward-sync commits are not proposed back to the monorepo. ### Format @@ -32,7 +32,7 @@ The trailer value encodes: direction, branch, and timestamp. This aids debugging ### Filtering - **Reverse sync**: `git log --invert-grep --grep="^${BOT_TRAILER}:"` excludes all commits with the trailer -- **CI loop guard**: The composite action checks if HEAD commit has the trailer before running sync at all +- **Forward sync**: the composite action still runs even when HEAD has the trailer; forward sync uses tree comparison, sync state, and merge checks to decide whether to push ### Configuration @@ -44,7 +44,7 @@ The trailer key is set in `.josh-sync.yml` under `bot.trailer`. This allows mult - Reliable loop prevention — trailer is part of the immutable commit object - Configurable key avoids conflicts between multiple sync bots - Human-readable — `git log` shows the trailer in commit messages -- CI loop guard prevents unnecessary sync runs entirely +- Repositories can act as middle hops, receiving a sync commit from one relationship and forwarding relevant changes to another **Negative:** - Commits with manually-added trailers matching the key would be incorrectly filtered diff --git a/docs/guide.md b/docs/guide.md index fdac918..6ce78fd 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -554,7 +554,9 @@ Runs on a cron schedule (never triggered by subrepo pushes): ### Loop prevention -Bot commits include a git trailer like `Josh-Sync-Origin: forward/main/2024-02-12T10:30:00Z`. Each sync direction filters out commits with this trailer, preventing changes from bouncing back and forth. The CI action also has a loop guard that skips entirely if the HEAD commit has the trailer. +Bot commits include a git trailer like `Josh-Sync-Origin: forward/main/2024-02-12T10:30:00Z`. Reverse sync filters out commits with this trailer, preventing changes from bouncing back from the target repo into the source repo. + +The CI action does not skip forward sync solely because the checked-out HEAD commit has a sync trailer. A repo can be a valid middle hop: it may receive a sync commit from one repository and still need to forward its filtered subtree to another repository. Forward sync relies on tree comparison, sync state, and merge checks to decide whether anything should be pushed. ### State tracking diff --git a/tests/unit/action.bats b/tests/unit/action.bats new file mode 100644 index 0000000..6aa5e88 --- /dev/null +++ b/tests/unit/action.bats @@ -0,0 +1,17 @@ +#!/usr/bin/env bats +# tests/unit/action.bats — Composite action regression tests + +setup() { + JOSH_SYNC_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" + ACTION_FILE="${JOSH_SYNC_ROOT}/action.yml" +} + +@test "forward sync is not globally skipped when HEAD has the sync trailer" { + ! grep -q "Loop guard (forward)" "$ACTION_FILE" + ! grep -q "HEAD commit has sync trailer" "$ACTION_FILE" + ! grep -q "steps.guard.outputs.skip" "$ACTION_FILE" +} + +@test "composite action still invokes the josh-sync CLI" { + grep -q "josh-sync sync" "$ACTION_FILE" +}