Avoid new commits when the task already contains the integration baseline

Why: repeated task-sync calls changed HEAD and invalidated CI even when main had not advanced.

Evidence: SELF red-green regression and 58 task lifecycle tests recorded through make evidence in the task card.

Impact: sync still fetches and validates components, creates records only for an actual merge, and reports the complete final source SHA.

Refs: U1; policy/merge-queue.md; .task/feature-merge-queue-policy-draft.md
This commit is contained in:
2026-10-13 07:05:06 +00:00
parent af223189f3
commit 03cbeac8e6
2 changed files with 112 additions and 16 deletions
+27 -16
View File
@@ -278,18 +278,26 @@ cmd_sync() {
need_repo
fetch_target
check_worktree before-sync
git merge --no-edit "origin/${TARGET_BRANCH}" \
-m "Merge authoritative ${TARGET_BRANCH} into the task branch" \
-m "Why: include the current integration baseline before verification." \
-m "Evidence: fetched origin/${TARGET_BRANCH}=$(git rev-parse "origin/${TARGET_BRANCH}")." \
-m "Impact: integrate upstream changes without rewriting task history." \
-m "Refs: task-sync"
git merge-base --is-ancestor "origin/${TARGET_BRANCH}" HEAD || die "sync incomplete"
local base head card branch slug pc changed=0 ancestry_status
base=$(git rev-parse "origin/${TARGET_BRANCH}")
if git merge-base --is-ancestor "$base" HEAD; then
:
else
ancestry_status=$?
[ "$ancestry_status" -eq 1 ] || die "cannot verify target ancestry"
git merge --no-edit "$base" \
-m "Merge authoritative ${TARGET_BRANCH} into the task branch" \
-m "Why: include the current integration baseline before verification." \
-m "Evidence: fetched origin/${TARGET_BRANCH}=${base}." \
-m "Impact: integrate upstream changes without rewriting task history." \
-m "Refs: task-sync"
changed=1
fi
git merge-base --is-ancestor "$base" HEAD || die "sync incomplete"
check_worktree before-update
check_worktree materialize
check_worktree after-sync
local base head card branch slug pc
base=$(git rev-parse "origin/${TARGET_BRANCH}"); head=$(git rev-parse HEAD)
head=$(git rev-parse HEAD)
branch=$(git rev-parse --abbrev-ref HEAD)
slug=$(echo "$branch" | tr '/' '-')
card=".task/${slug}.md"
@@ -304,18 +312,21 @@ cmd_sync() {
done
fi
if [ -n "$card" ] && [ -f "$card" ]; then
printf '| 同步前验收 | %s | %s | %s |\n' "$base" "$head" "$(date -u +%FT%TZ)" >> "$card"
git add "$card"
git commit -q -m "Record the sync checkpoint" \
-m "Why: bind the task validation checkpoint to the latest integration baseline." \
-m "Evidence: SELF recursive component checks; sync_base=${base}; candidate=${head}." \
-m "Impact: task metadata only." -m "Refs: ${card}"
if [ "$changed" -eq 1 ]; then
printf '| 同步(记录前对象) | %s | %s | %s |\n' "$base" "$head" "$(date -u +%FT%TZ)" >> "$card"
git add "$card"
git commit -q -m "Record the sync checkpoint" \
-m "Why: bind the task validation checkpoint to the integration baseline." \
-m "Evidence: SELF recursive component checks; sync_base=${base}; before_record=${head}." \
-m "Impact: task metadata only." -m "Refs: ${card}" -- "$card"
fi
# JSON receipts carry before/after state; older table rows remain readable.
python3 "${SCRIPT_DIR}/check-evidence.py" "$card"
else
echo "[warn] 找不到分支 ${branch} 的任务卡,同步记录未写入" >&2
fi
echo "[synced] sync_base=$(echo "$base" | cut -c1-10) tested_head=$(echo "$head" | cut -c1-10) checked_at=$(date -u +%FT%TZ)"
head=$(git rev-parse HEAD)
echo "[synced] sync_base=${base} tested_head=${head} checked_at=$(date -u +%FT%TZ) changed=${changed}"
echo "[next] 跑正式验收(make test 等),证据写入任务卡"
}
+85
View File
@@ -69,6 +69,20 @@ class TaskLifecycleTests(unittest.TestCase):
self.git(self.repo, "commit", "-m", "Advance main")
self.git(self.repo, "push", "origin", "main")
def record_task_card(self, wt):
branch = self.git(wt, "branch", "--show-current").stdout.strip()
r0 = self.git(wt, "rev-parse", "origin/main").stdout.strip()
card = wt / ".task" / (branch.replace("/", "-") + ".md")
card.parent.mkdir(exist_ok=True)
card.write_text(
f"# {branch}\n任务主记录:fixture;状态:进行中;owner:Lifecycle Test\n"
f"- 权威仓:fixture;目标分支:main;R0(initial_base_sha)= {r0}\n"
f"- 工作区:{wt}(worktree);分支:{branch}\n"
)
self.git(wt, "add", str(card.relative_to(wt)))
self.git(wt, "commit", "-m", "Record fixture task")
return card
def add_submodule(self):
child = self.root / "component"
self.git(self.root, "init", "--initial-branch=main", str(child))
@@ -105,6 +119,76 @@ class TaskLifecycleTests(unittest.TestCase):
reading = next(line for line in result.stdout.splitlines() if line.startswith("[required-reading]"))
self.assertIn("policy/delivery.md", reading)
def test_sync_same_base_preserves_head_and_card(self):
wt = self.worktree()
card = self.record_task_card(wt)
head = self.git(wt, "rev-parse", "HEAD").stdout.strip()
content = card.read_bytes()
index = self.git(wt, "ls-files", "--stage").stdout
for _ in range(2):
result = self.task(wt, "sync")
self.assertEqual(self.git(wt, "rev-parse", "HEAD").stdout.strip(), head)
self.assertEqual(card.read_bytes(), content)
self.assertEqual(self.git(wt, "ls-files", "--stage").stdout, index)
self.assertEqual(self.git(wt, "status", "--porcelain").stdout, "")
self.assertIn("changed=0", result.stdout)
self.assertIn(f"tested_head={head}", result.stdout)
def test_sync_new_base_reports_actual_final_head(self):
wt = self.worktree()
card = self.record_task_card(wt)
self.advance_remote()
base = self.git(self.repo, "rev-parse", "HEAD").stdout.strip()
result = self.task(wt, "sync")
head = self.git(wt, "rev-parse", "HEAD").stdout.strip()
self.git(wt, "merge-base", "--is-ancestor", base, head)
self.assertIn(f"sync_base={base}", result.stdout)
self.assertIn(f"tested_head={head}", result.stdout)
self.assertIn("changed=1", result.stdout)
content = card.read_bytes()
self.task(wt, "sync")
self.assertEqual(self.git(wt, "rev-parse", "HEAD").stdout.strip(), head)
self.assertEqual(card.read_bytes(), content)
def test_sync_preserves_dirty_parent_and_staged_changes(self):
for staged in (False, True):
with self.subTest(staged=staged):
wt = self.worktree(name="feature/staged" if staged else "feature/dirty")
head = self.git(wt, "rev-parse", "HEAD").stdout.strip()
(wt / "tracked").write_text("keep owner work\n")
if staged:
self.git(wt, "add", "tracked")
index = self.git(wt, "ls-files", "--stage").stdout
result = self.task(wt, "sync", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(self.git(wt, "rev-parse", "HEAD").stdout.strip(), head)
self.assertEqual(self.git(wt, "ls-files", "--stage").stdout, index)
self.assertEqual((wt / "tracked").read_text(), "keep owner work\n")
self.git(self.repo, "worktree", "remove", "--force", str(wt))
def test_sync_rejects_unreachable_target_without_changing_head(self):
wt = self.worktree()
self.record_task_card(wt)
head = self.git(wt, "rev-parse", "HEAD").stdout.strip()
self.git(wt, "remote", "set-url", "origin", str(self.root / "missing.git"))
result = self.task(wt, "sync", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertIn("REMOTE_UNVERIFIED", result.stdout)
self.assertEqual(self.git(wt, "rev-parse", "HEAD").stdout.strip(), head)
def test_sync_conflict_is_not_reported_as_synced(self):
wt = self.worktree()
self.record_task_card(wt)
(wt / "tracked").write_text("task choice\n")
self.git(wt, "commit", "-am", "Task edit")
(self.repo / "tracked").write_text("target choice\n")
self.git(self.repo, "commit", "-am", "Target edit")
self.git(self.repo, "push", "origin", "main")
result = self.task(wt, "sync", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertNotIn("[synced]", result.stdout)
self.assertIn("UU tracked", self.git(wt, "status", "--porcelain").stdout)
def test_sync_materializes_updated_component(self):
child = self.add_submodule()
wt = self.worktree()
@@ -396,6 +480,7 @@ class TaskLifecycleTests(unittest.TestCase):
(wt / ".task/feature-test.md").write_text("# Task\n")
self.git(wt, "add", ".task")
self.git(wt, "commit", "-m", "Create task card")
self.advance_remote()
hook = self.repo / ".git/hooks/pre-commit"
hook.write_text("#!/bin/sh\nexit 1\n")
hook.chmod(0o755)