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
13 changes: 8 additions & 5 deletions .github/workflows/sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,21 +128,24 @@ jobs:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
MODELSCOPE_TOKEN: ${{ secrets.MODELSCOPE_TOKEN }}
run: |
ARGS="--config config/sync_config.yaml --state-dir .sync_state/"
ARGS=(
"--config" "config/sync_config.yaml"
"--state-dir" ".sync_state/"
)

if [ "${{ inputs.dry_run }}" = "true" ]; then
ARGS="$ARGS --dry-run true"
ARGS+=("--dry-run" "true")
fi

if [ -n "${{ inputs.direction }}" ] && [ "${{ inputs.direction }}" != "config" ]; then
ARGS="$ARGS --direction ${{ inputs.direction }}"
ARGS+=("--direction" "${{ inputs.direction }}")
fi

if [ -n "${{ inputs.sync_target }}" ]; then
ARGS="$ARGS --target ${{ inputs.sync_target }}"
ARGS+=("--target" "${{ inputs.sync_target }}")
fi

python -m src.sync_engine $ARGS
python -m src.sync_engine "${ARGS[@]}"

- name: Save sync state
uses: actions/upload-artifact@v4
Expand Down
27 changes: 23 additions & 4 deletions src/sync_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,12 @@ def sync_item(self, item: SyncItem) -> SyncResult:
self.hf.create_repo_if_needed(item.hf_repo_id, item.resource_type)

# Fetch snapshots
hf_snapshot = self.hf.get_repo_snapshot(item.hf_repo_id, item.resource_type)
hf_snapshot = None
try:
hf_snapshot = self.hf.get_repo_snapshot(item.hf_repo_id, item.resource_type)
except Exception as e:
logger.warning("[HF] Could not fetch snapshot for %s: %s", item.hf_repo_id, e)

ms_snapshot = None
try:
ms_snapshot = self.ms.get_repo_snapshot(item.ms_repo_id, item.resource_type)
Expand All @@ -150,6 +155,12 @@ def sync_item(self, item: SyncItem) -> SyncResult:
)

if item.direction == SyncDirection.BIDIRECTIONAL:
if hf_snapshot is None or ms_snapshot is None:
raise RuntimeError(
f"Cannot sync BIDIRECTIONAL: failed to fetch snapshot — "
f"HF ({item.hf_repo_id})={'OK' if hf_snapshot else 'FAILED'}, "
f"MS ({item.ms_repo_id})={'OK' if ms_snapshot else 'FAILED'}"
)
bd = BidirectionalChangeDetector(**detector_kwargs)
hf_state = self.states.get(
SyncState.make_key("hf", item.resource_type, item.hf_repo_id)
Expand All @@ -165,6 +176,11 @@ def sync_item(self, item: SyncItem) -> SyncResult:
)
all_actions = hf_to_ms_actions + ms_to_hf_actions
elif item.direction == SyncDirection.HF_TO_MS:
if hf_snapshot is None:
raise RuntimeError(
f"Cannot sync HF_TO_MS: failed to fetch HuggingFace "
f"snapshot for {item.hf_repo_id}"
)
detector = ChangeDetector(**detector_kwargs)
state = self.states.get(
SyncState.make_key("hf", item.resource_type, item.hf_repo_id)
Expand Down Expand Up @@ -350,7 +366,8 @@ def _update_state(
# Update HF state
hf_key = SyncState.make_key("hf", item.resource_type, item.hf_repo_id)
hf_state = self.states.get(hf_key, SyncState(repo_key=hf_key))
hf_state.last_synced_commit = hf_snapshot.last_commit_hash
if hf_snapshot:
hf_state.last_synced_commit = hf_snapshot.last_commit_hash
hf_state.last_synced_at = now
for fp in synced_files:
hf_file = hf_file_map.get(fp)
Expand Down Expand Up @@ -516,8 +533,10 @@ def main() -> None:

print_summary(results)

# Exit with error code if any failures
if any(r.status == SyncStatus.FAILED for r in results):
# Exit with error code if any item failed.
# Downstream CI steps (report, issue creation) use `if: always()`.
any_failed = any(r.status == SyncStatus.FAILED for r in results)
if any_failed:
sys.exit(1)


Expand Down
59 changes: 59 additions & 0 deletions tests/test_sync_engine_extended.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,65 @@ def test_snapshot_exception_caught(self, tmp_path):
results = engine.sync_all()
assert results[0].status == SyncStatus.FAILED

def test_both_snapshots_fail_bidirectional(self, tmp_path):
"""Both snapshots fail in bidirectional mode — item should be FAILED."""
hf = MockAdapter("hf")
hf._snapshot_fail = True
ms = MockAdapter("ms")
ms._snapshot_fail = True

config = make_config(direction="bidirectional")
engine = SyncEngine(
config=config,
hf_adapter=hf,
ms_adapter=ms,
state_dir=tmp_path,
)

results = engine.sync_all()
assert results[0].status == SyncStatus.FAILED
err = results[0].error_message.lower()
assert "failed" in err

def test_one_snapshot_fails_bidirectional(self, tmp_path):
"""Only one snapshot fails in bidirectional mode — should still be FAILED."""
hf = MockAdapter("hf")
hf._snapshot_fail = True
ms = MockAdapter("ms")
ms._files = {"model.bin": b"data"}

config = make_config(direction="bidirectional")
engine = SyncEngine(
config=config,
hf_adapter=hf,
ms_adapter=ms,
state_dir=tmp_path,
)

results = engine.sync_all()
assert results[0].status == SyncStatus.FAILED
err = results[0].error_message.lower()
assert "hf" in err and "failed" in err

def test_hf_snapshot_fails_ms_to_hf_ok(self, tmp_path):
"""HF snapshot fails but MS_TO_HF only needs MS snapshot — should succeed."""
hf = MockAdapter("hf")
hf._snapshot_fail = True
hf._files = {}
ms = MockAdapter("ms")
ms._files = {"model.bin": b"data"}

config = make_config(direction="ms_to_hf")
engine = SyncEngine(
config=config,
hf_adapter=hf,
ms_adapter=ms,
state_dir=tmp_path,
)

results = engine.sync_all()
assert results[0].status == SyncStatus.SUCCESS


# ── Results JSON writing ────────────────────────────────────────────

Expand Down
Loading