Skip to content

fix(sync): zero-op sync still opens an anchor-only PR when upstream touched only carved-out paths #235

Description

@MarkMichaelis

Problem

A sync whose upstream range touches only consumer-owned / carved-out paths
still produces a commit -- and, on a protected branch, a branch + PR whose entire
diff is a one-line syncedAt bump in .sdlc-ai-sync.json.

This is the same empty-PR complaint as #224. The guard added by #230 only covers
the case where the anchor already equals the upstream head:

$syncStateChanged = ($ops.Count -gt 0) -or ($mergedPaths.Count -gt 0) -or ($anchorSha -ne $upstreamHead)

When the anchor is behind the head but every upstream commit in the range
touched a carved-out path (run.ps1, run.Tests.ps1,
.github/workflows/copilot-setup-steps.yml, .github/agents/tests/), the third
clause is $true, so the state file is rewritten, the tree goes dirty, and an
empty sync commit/PR is manufactured.

Reproduced live in a consuming repo: anchor 42d3b44, upstream head 5264227.
The two intervening upstream PRs (#232, #234) touched only run.ps1 /
run.Tests.ps1, both consumer-owned. Files to update: 0, yet a real run would
open a content-free PR.

Second defect -- dishonest message

Write-SyncPlan prints the zero-op line unconditionally:

Files to update: 0 (already at upstream 5264227 -- nothing to sync)

The consumer is not at 5264227; its anchor is 42d3b44. Two upstream
commits exist, they simply carry no managed-path changes. The message should not
claim an anchor position that is false.

Proposed fix

  1. Drop the ($anchorSha -ne $upstreamHead) clause so a zero-op sync never
    rewrites the state file. This is safe: the anchor feeds only
    Test-LocalDriftOnManagedPaths and Get-UpstreamOps, both of which compare
    managed-path content. Zero ops means no managed path differs between anchor
    and head, so an older anchor yields identical results -- it just re-diffs a
    slightly wider (and still cheap) range until real managed content lands.
  2. Make the zero-op message distinguish "anchor == head" from "upstream moved but
    nothing managed changed."

Acceptance criteria

  • Anchor behind head + zero ops => no commit, no branch, no PR, syncedAt
    untouched.
  • Anchor == head + zero ops => unchanged behavior (still no commit).
  • Ops > 0 => anchor advances to the upstream head exactly as before.
  • The zero-op message does not claim the consumer is at a head it is not at.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions