Skip to content

feat: add GitHub Marketplace readiness, bug fixes, and expanded test coverage - #1

Merged
dongjiang1989 merged 7 commits into
masterfrom
change-to-github-marketplace
May 29, 2026
Merged

feat: add GitHub Marketplace readiness, bug fixes, and expanded test coverage #1
dongjiang1989 merged 7 commits into
masterfrom
change-to-github-marketplace

Conversation

@dongjiang1989

@dongjiang1989 dongjiang1989 commented May 29, 2026

Copy link
Copy Markdown
Member

Summary

Transforms the HF-MS Sync project into a production-ready GitHub Marketplace Action. Adds Docker entrypoint with output support, CI/CD pipelines, preflight checks in the sync workflow, 5 critical bug fixes, and expands test coverage from 48 to 90 cases.

What Changed

GitHub Marketplace Readiness

  • Added entrypoint.sh — bridges Docker INPUT_* env vars to CLI args, writes $GITHUB_OUTPUT (sync_status, files_synced, bytes_transferred) and $GITHUB_STEP_SUMMARY
  • Updated action.yml — added state_dir, hf_token, modelscope_token inputs
  • Updated Dockerfile — entrypoint.sh COPY, OCI labels

CI/CD Pipelines

  • Added .github/workflows/ci.yml — PR pipeline: ruff lint + format + pytest (Python 3.10/3.11/3.12) + action validation + Docker build
  • Added .github/workflows/release.yml — auto-tagging, major version management, GHCR Docker image publishing
  • Updated .github/workflows/sync.yml — added preflight job (lint + tests + config validation) before sync; new skip_preflight input for emergency bypass

Bug Fixes (5)

File Fix
src/adapters/huggingface_adapter.py Operator precedence bug in file size parsing; incorrect LFS detection
src/sync_engine.py MS_TO_HF used HF snapshot as source when MS snapshot failed
src/sync_engine.py last_results.json was never written (broke report step)
src/sync_engine.py synced_files dict in state was never populated (weakened cross-run change detection)

Test Coverage: 48 → 90 cases (+42)

  • Added tests/test_report.py (10 cases) — report formatting, JSON output, stdout/summary
  • Added tests/test_sync_engine_extended.py (19 cases) — MS→HF, bidirectional, partial/full failures, datasets, GITHUB_OUTPUT
  • Added tests/test_adapters.py (9 cases) — adapter init, SDK fallback, state-based change detection
  • Added GITHUB_OUTPUT verification tests (4 cases)

Community Health Files

  • CHANGELOG.md, CONTRIBUTING.md
  • .github/ISSUE_TEMPLATE/bug_report.yml, feature_request.yml
  • .github/PULL_REQUEST_TEMPLATE.md

Code Quality

  • Applied ruff format across all source and test files
  • Fixed line length violations, naming conventions, unused imports

Summary by CodeRabbit

  • New Features

    • CI and Release workflows with automated tagging, Docker build/publish, and optional preflight for sync runs
    • Action entrypoint script, new action inputs for persisted state and tokens, and sync summary written to GitHub Actions outputs
    • Container startup switched to an entrypoint script; image metadata added
  • Documentation

    • Added CONTRIBUTING, CHANGELOG, PR template, and Issue Forms for bug reports and feature requests
  • Tests

    • Expanded CI matrix, E2E and unit tests, including validation of GitHub outputs

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds GitHub workflows (CI, release, sync/preflight), action input/entrypoint wiring and Docker ENTRYPOINT, emits GitHub Actions outputs from SyncEngine, supplies issue/PR templates, CHANGELOG/CONTRIBUTING docs, and applies formatting/import cleanup across code and tests.

Changes

GitHub Actions Marketplace Integration

Layer / File(s) Summary
Issue & PR templates
.github/ISSUE_TEMPLATE/*, .github/PULL_REQUEST_TEMPLATE.md
Add Bug Report and Feature Request issue forms and a PR template with checklists and metadata.
Release workflow
.github/workflows/release.yml
Manual dispatch validation, tag creation/release, optional major-tag update, and tag-triggered GHCR Docker build/push.
Sync workflow & preflight
.github/workflows/sync.yml
Add skip_preflight input, gated preflight job (lint/tests/config/import checks), and make sync depend on/conditionally run after preflight.
CI workflow
.github/workflows/ci.yml
New CI with ruff lint/format checks, pytest matrix, action/Docker validation, and a docker-build dry-run job.
Action metadata & inputs wiring
action.yml
Update action author; add state_dir, hf_token, modelscope_token; remove runs.args forwarding.
Entrypoint script
entrypoint.sh
New entrypoint maps INPUT_* to CLI args, exports tokens when provided, registers cleanup trap, runs the sync engine, and returns its exit code.
Docker image configuration
Dockerfile
Add OCI/OpenContainers labels and switch ENTRYPOINT to /entrypoint.sh (copy + chmod + entrypoint).
SyncEngine outputs & report generation
src/sync_engine.py, src/report.py
Add _write_github_outputs() and update sync_all()/results writing to emit sync_status, files_synced, and bytes_transferred to GITHUB_OUTPUT; refactor results JSON formatting.
Adapters & change detection formatting
src/adapters/*, src/change_detector.py
Minor import/formatting changes; behavior preserved.
Report & utils formatting
src/report.py, src/utils.py
Refactor JSON item construction and last_synced_at serialization formatting only.
E2E tooling: verify script
tests/e2e/verify_sync.py, config/ci_smoke_test.yaml
Formatting and file-filter readability updates; minimal CI smoke config added.
Tests: formatting & cleanup
tests/test_*.py, tests/test_sync_engine_extended.py
Clean imports, rewrap constructor calls, add TestGitHubOutputs to validate GITHUB_OUTPUT behavior.

🎯 3 (Moderate) | ⏱️ ~22 minutes

🐰 "Workflows hum, entrypoint hops in place,

Outputs saved, templates guide the space,
Docs penned, tests tidy, images build with care,
The rabbit synced changes everywhere."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: GitHub Marketplace readiness (entrypoint.sh, action.yml, workflows), bug fixes, and expanded test coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch change-to-github-marketplace

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request prepares the HF-MS Sync action for the GitHub Marketplace by introducing a Docker entrypoint script (entrypoint.sh), adding action inputs for state directory and API tokens, and implementing direct writing of GitHub outputs and step summaries from the Python sync engine. It also adds standard repository templates, contribution guidelines, and comprehensive tests. The review feedback highlights two important issues in entrypoint.sh: the potential for word-splitting bugs when constructing CLI arguments as a single unquoted string, and redundant code blocks that duplicate the writing of GitHub outputs and job summaries already handled by the Python engine.

Comment thread entrypoint.sh Outdated
Comment thread entrypoint.sh Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (3)
src/sync_engine.py (1)

376-378: ⚡ Quick win

Write outputs even when no sync items are selected.

Returning early on empty config/target-filter miss skips _write_github_outputs, so downstream steps may see missing outputs instead of sync_status=success and zero counters.

Proposed fix
     items = self._build_sync_items()
     if not items:
         logger.warning("No sync items configured")
-        return []
+        results: list[SyncResult] = []
+        self._write_results(results)
+        self._write_github_outputs(results)
+        return results

Also applies to: 394-395

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/sync_engine.py` around lines 376 - 378, The early returns when items is
empty skip writing GitHub outputs; instead of returning immediately in the
blocks around the shown if-not-items checks, call _write_github_outputs (pass an
empty list and zeroed counters / sync_status='success') before returning [] so
downstream steps receive explicit outputs; update both occurrences (the if block
at the shown snippet and the similar block at lines ~394-395) to invoke
_write_github_outputs with appropriate empty/zero values and then return [].
tests/test_sync_engine_extended.py (1)

547-638: ⚡ Quick win

Add a GitHub output test for the empty-config path.

TestGitHubOutputs should include models=[] and datasets=[] with GITHUB_OUTPUT set, asserting output keys are still written (sync_status=success, files_synced=0, bytes_transferred=0).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_sync_engine_extended.py` around lines 547 - 638, Add a new test
method in the TestGitHubOutputs class (e.g., test_github_output_empty_config)
that sets GITHUB_OUTPUT to a temp file, creates hf and ms MockAdapter instances
with no files, builds a config using make_config(direction="hf_to_ms",
models=[], datasets=[]), constructs a SyncEngine with that config and the
adapters, calls engine.sync_all(), then reads the output file and asserts the
expected keys are present: "sync_status=success", "files_synced=0",
"bytes_transferred=0" and that "models=[]" and "datasets=[]" are written;
reference make_config, SyncEngine, and MockAdapter to locate where to add the
test.
.github/workflows/ci.yml (1)

99-103: ⚡ Quick win

Gate Docker build on action validation as well.

Line 102 should include action-validation in needs so image build doesn’t run when action contract checks fail.

Suggested fix
-    needs: [lint, test]
+    needs: [lint, test, action-validation]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 99 - 103, The docker-build workflow
job named "docker-build" currently depends on [lint, test]; update its needs
configuration to also include "action-validation" so the Docker image build is
gated by the action-contract checks; locate the docker-build job definition and
add "action-validation" to the needs array for the job (the "docker-build" job
and its "needs" key).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 4-7: CI is configured to trigger only on branch "main" for both
the push and pull_request workflows, but this PR targets "master"; update the
branches arrays under the push and pull_request keys in .github/workflows/ci.yml
(the branches: [main] entries) to include "master" (e.g., branches: [main,
master]) so CI runs for the master-targeted PR and pushes.
- Around line 17-19: Replace the floating action tags with immutable commit
SHAs: locate every usage of actions/checkout@v4 and actions/setup-python@v5 (and
any other floating tags in the CI workflow) and pin them to the corresponding
commit SHA for the desired release (e.g., actions/checkout@<commit-sha>,
actions/setup-python@<commit-sha>); update all occurrences (including the ones
referenced by actions/checkout and actions/setup-python in the workflow) and
verify the SHAs are the official commit hashes from the actions' repositories to
eliminate supply-chain risk.
- Line 17: The checkout steps use actions/checkout@v4 without disabling
credential persistence; update every actions/checkout@v4 invocation to include a
with: block setting persist-credentials: false (i.e., add "with:" then
"persist-credentials: false" under the actions/checkout@v4 steps) so credentials
are not left available to subsequent steps.

In @.github/workflows/release.yml:
- Around line 95-96: The release action is using raw inputs.version for tag_name
and name while the "Create tag" step normalizes the tag (prefixing with "v"),
causing a mismatch; update the release step to use the normalized tag output
from the tag-creation step (e.g., replace uses of inputs.version in tag_name and
name with the create-tag step's output such as steps.create_tag.outputs.tag_name
or steps.create_tag.outputs.tag) so both tag_name and name reference the exact
normalized tag produced earlier.
- Around line 28-30: Replace mutable action tags with immutable commit SHAs for
each third-party action referenced (actions/checkout@v4,
actions/setup-python@v5, softprops/action-gh-release@v2,
docker/setup-buildx-action@v3, docker/login-action@v3,
docker/metadata-action@v5, docker/build-push-action@v5): for each action, look
up the corresponding repository on GitHub, find the commit that corresponds to
the tagged release you currently reference, and update the workflow to use that
action as @<commit-sha> instead of `@vX`; keep the rest of the step configuration
unchanged and ensure you pin every occurrence of those action names to their
full SHA.
- Line 28: Current checkout steps use actions/checkout@v4 without scoping
credentials; update the checkout steps used by the validate and docker jobs to
set persist-credentials: false to avoid persisting tokens, while leaving the
release job’s checkout unchanged (release still needs the default behavior for
pushing refs). Locate the actions/checkout@v4 steps in the validate and docker
jobs and add persist-credentials: false to their step inputs; do not modify the
release job’s checkout step.
- Around line 62-66: Replace direct interpolation of ${{ inputs.version }} with
a single normalized env var (e.g., VERSION) and perform shell-safe regex
validation before any git or release steps: read inputs.version into an env (not
reusing ${{ inputs.version }} later), validate/normalize it to ensure it matches
SemVer or '^v?\\d+\\.\\d+\\.\\d+$' and prepend 'v' if missing, abort with a
clear error on mismatch; then use that validated env variable for git tag
operations and set the GitHub Release fields (tag_name and name) from this
normalized VERSION rather than the raw input. Reference: the script block that
sets VERSION, the places invoking git tag, and the Release step fields
tag_name/name.
- Around line 19-21: The workflow currently grants workflow-wide write access
via the top-level permissions keys (contents: write, packages: write); change
the global permissions to least-privilege (e.g., contents: read, packages: read)
and add explicit permissions overrides only on jobs that actually need write
(for example add a permissions block with contents: write and/or packages: write
to the release/publish job(s) that perform pushes or package uploads, leaving
jobs like validate with read-only permissions). Locate the top-level permissions
block and each affected job (e.g., the release/publish job names and the
validate job) and move write privileges from global scope into per-job
permissions overrides.

In @.github/workflows/sync.yml:
- Around line 30-33: Add explicit least-privilege permissions to the sync job by
setting a job-level permissions map (e.g., issues: write and any read perms you
actually need) so actions/github-script@v7 can list/create issues with the
GITHUB_TOKEN; pin external actions to specific commit SHAs instead of tags for
both actions/github-script and actions/checkout; and harden both
actions/checkout steps by adding persist-credentials: false to avoid leaking the
token to subsequent steps. Ensure these changes are applied to the sync job
definition (referencing job name "sync"), the actions/github-script usage, and
every actions/checkout invocation.

In `@CONTRIBUTING.md`:
- Line 50: Update the "Target Python version: 3.11" line in CONTRIBUTING.md to
reflect the supported range mentioned in the PR (e.g., "Target Python versions:
3.10–3.12"); locate the exact phrase "Target Python version: 3.11" and replace
it with a pluralized, ranged form so documentation matches the tested matrix.
- Around line 56-69: In CONTRIBUTING.md, the fenced code block that shows the
project structure (the block containing entries like src/, adapters/, config.py,
change_detector.py, tests/, examples/) is missing a language identifier; update
the opening fence for that block to include a language (e.g., add "text" or
"bash" after the triple backticks) so the snippet is
rendered/accessibility-compliant (MD040).

In `@entrypoint.sh`:
- Around line 34-42: Change ARGS from a flat string to a Bash array and append
options with ARGS+=(--config "$CONFIG") etc, then invoke the Python process with
the array expansion python -m src.sync_engine "${ARGS[@]}" to avoid
word-splitting and globbing; additionally, make the job-summary generation
resilient by replacing direct dict indexing r['item_name'] and
r['resource_type'] with safe lookups like r.get('item_name', '') and
r.get('resource_type', '') (or guard with conditional checks) so missing keys
won’t raise KeyError and break the run.

In `@src/sync_engine.py`:
- Around line 426-457: The _write_github_outputs function currently opens and
writes to the file referenced by GITHUB_OUTPUT without handling I/O errors; wrap
the file open/write block in a try/except that catches
OSError/IOError/Exception, log a warning via logger (include the exception
message and the output_file value) and return without re-raising so a file write
failure does not flip a successful sync into a failed run; keep the computation
of total_synced/total_bytes/statuses as-is and ensure logger.info is only called
for the successful write path or still logs a message indicating the write was
skipped on exception.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 99-103: The docker-build workflow job named "docker-build"
currently depends on [lint, test]; update its needs configuration to also
include "action-validation" so the Docker image build is gated by the
action-contract checks; locate the docker-build job definition and add
"action-validation" to the needs array for the job (the "docker-build" job and
its "needs" key).

In `@src/sync_engine.py`:
- Around line 376-378: The early returns when items is empty skip writing GitHub
outputs; instead of returning immediately in the blocks around the shown
if-not-items checks, call _write_github_outputs (pass an empty list and zeroed
counters / sync_status='success') before returning [] so downstream steps
receive explicit outputs; update both occurrences (the if block at the shown
snippet and the similar block at lines ~394-395) to invoke _write_github_outputs
with appropriate empty/zero values and then return [].

In `@tests/test_sync_engine_extended.py`:
- Around line 547-638: Add a new test method in the TestGitHubOutputs class
(e.g., test_github_output_empty_config) that sets GITHUB_OUTPUT to a temp file,
creates hf and ms MockAdapter instances with no files, builds a config using
make_config(direction="hf_to_ms", models=[], datasets=[]), constructs a
SyncEngine with that config and the adapters, calls engine.sync_all(), then
reads the output file and asserts the expected keys are present:
"sync_status=success", "files_synced=0", "bytes_transferred=0" and that
"models=[]" and "datasets=[]" are written; reference make_config, SyncEngine,
and MockAdapter to locate where to add the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 02e7d627-afd0-4981-904d-2587e35caa67

📥 Commits

Reviewing files that changed from the base of the PR and between 855810b and a96ab1a.

📒 Files selected for processing (24)
  • .github/ISSUE_TEMPLATE/bug_report.yml
  • .github/ISSUE_TEMPLATE/feature_request.yml
  • .github/PULL_REQUEST_TEMPLATE.md
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • .github/workflows/sync.yml
  • CHANGELOG.md
  • CONTRIBUTING.md
  • Dockerfile
  • action.yml
  • entrypoint.sh
  • src/adapters/huggingface_adapter.py
  • src/adapters/modelscope_adapter.py
  • src/change_detector.py
  • src/report.py
  • src/sync_engine.py
  • src/utils.py
  • tests/e2e/verify_sync.py
  • tests/test_adapters.py
  • tests/test_change_detector.py
  • tests/test_config.py
  • tests/test_sync_engine.py
  • tests/test_sync_engine_extended.py
  • tests/test_utils.py
💤 Files with no reviewable changes (1)
  • tests/test_utils.py

Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/release.yml
Comment thread .github/workflows/release.yml
Comment thread .github/workflows/sync.yml
Comment thread CONTRIBUTING.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread entrypoint.sh Outdated
Comment thread src/sync_engine.py
dongjiang1989 added a commit that referenced this pull request May 29, 2026
Addresses PR #1 review from gemini-code-assist:

1. Replace string-based CLI arg building with a bash array to prevent
   word-splitting and glob expansion when paths contain spaces or
   special characters.

2. Remove the two large Python heredoc blocks that re-read
   last_results.json to write $GITHUB_OUTPUT and $GITHUB_STEP_SUMMARY.
   These are already written directly by the sync engine
   (_write_github_outputs / _write_results in src/sync_engine.py),
   so the duplication was unnecessary and risked conflicting writes.
Signed-off-by: dongjiang <dongjiang1989@126.com>
Addresses PR #1 review from gemini-code-assist:

1. Replace string-based CLI arg building with a bash array to prevent
   word-splitting and glob expansion when paths contain spaces or
   special characters.

2. Remove the two large Python heredoc blocks that re-read
   last_results.json to write $GITHUB_OUTPUT and $GITHUB_STEP_SUMMARY.
   These are already written directly by the sync engine
   (_write_github_outputs / _write_results in src/sync_engine.py),
   so the duplication was unnecessary and risked conflicting writes.

Signed-off-by: dongjiang <dongjiang1989@126.com>
@dongjiang1989
dongjiang1989 force-pushed the change-to-github-marketplace branch from 029ff97 to 0c0bdb6 Compare May 29, 2026 05:23
- ci.yml: add 'master' to branch triggers so CI runs for this PR target
- ci.yml: add persist-credentials: false to all checkout steps
- ci.yml: gate docker-build on action-validation job
- sync_engine.py: write GitHub outputs even when no items configured
  (prevents downstream steps from seeing missing outputs)
- tests: add test_github_output_empty_config for empty config path
- CONTRIBUTING.md: update Python version to 3.10-3.12, add code fence lang

Signed-off-by: dongjiang <dongjiang1989@126.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/sync.yml (1)

131-145: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Harden sync workflow command construction and permissions

  • .github/workflows/sync.yml (Run sync step, lines 131-145) builds ARGS using unquoted ${{ inputs.* }} and then runs python -m src.sync_engine $ARGS, which allows argument splitting and potential shell injection via sync_target (since it’s interpolated into the bash script unquoted).
  • The workflow has no top-level/job-level permissions: while it uses actions/github-script@v7 to list/create issues on failure; with restricted org defaults, the failure-reporting step can fail.
Suggested fix
       - name: Run sync
         env:
           HF_TOKEN: ${{ secrets.HF_TOKEN }}
           MODELSCOPE_TOKEN: ${{ secrets.MODELSCOPE_TOKEN }}
+          INPUT_DRY_RUN: ${{ inputs.dry_run }}
+          INPUT_DIRECTION: ${{ inputs.direction }}
+          INPUT_SYNC_TARGET: ${{ inputs.sync_target }}
         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"
+          if [ "$INPUT_DRY_RUN" = "true" ]; then
+            ARGS+=(--dry-run "true")
           fi
 
-          if [ -n "${{ inputs.direction }}" ] && [ "${{ inputs.direction }}" != "config" ]; then
-            ARGS="$ARGS --direction ${{ inputs.direction }}"
+          if [ -n "$INPUT_DIRECTION" ] && [ "$INPUT_DIRECTION" != "config" ]; then
+            ARGS+=(--direction "$INPUT_DIRECTION")
           fi
 
-          if [ -n "${{ inputs.sync_target }}" ]; then
-            ARGS="$ARGS --target ${{ inputs.sync_target }}"
+          if [ -n "$INPUT_SYNC_TARGET" ]; then
+            ARGS+=(--target "$INPUT_SYNC_TARGET")
           fi
 
-          python -m src.sync_engine $ARGS
+          python -m src.sync_engine "${ARGS[@]}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/sync.yml around lines 131 - 145, The ARGS construction
uses unquoted interpolations (ARGS, inputs.sync_target, inputs.direction) and is
passed to python via word-splitting, enabling argument-splitting/shell
injection; fix by quoting all GitHub input expansions and argument
interpolations (use "${{ inputs.sync_target }}", "${{ inputs.direction }}", and
quote when appending to ARGS or, better, build a safe arg array and pass it to
python -m src.sync_engine without unquoted expansion) so values cannot inject
extra tokens, and add explicit workflow- or job-level permissions (e.g.,
permissions: issues: write) so actions/github-script@v7 can reliably list/create
issues on failure.
♻️ Duplicate comments (7)
.github/workflows/ci.yml (2)

5-7: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

CI won’t run for the current target branch.

This PR targets master, but CI is configured for main only.

Suggested fix
 on:
   push:
-    branches: [main]
+    branches: [main, master]
   pull_request:
-    branches: [main]
+    branches: [main, master]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 5 - 7, The CI config only triggers for
branch name "main" under the YAML keys "branches:" for push and the
"pull_request:" block, but the PR targets "master"; update the branch filters in
the CI workflow so they include the target branch—either add "master" alongside
"main" in both branches arrays (branches: [main, master]) or replace "main" with
"master" in the push and pull_request "branches:" entries to match your repo's
default branch.

17-19: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Pin actions to immutable SHAs and disable persisted checkout credentials.

Using floating tags and default credential persistence weakens workflow security.

#!/bin/bash
# Verify floating action refs and checkout credential persistence in CI workflow
set -euo pipefail
rg -nP '^\s*-\s*uses:\s*[^@\s]+@v[0-9]+' .github/workflows/ci.yml
rg -n -A2 'uses:\s*actions/checkout@' .github/workflows/ci.yml

Also applies to: 40-42, 57-57, 104-104

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 17 - 19, Replace floating action tags
with their immutable SHA digests and disable persisted checkout credentials:
update uses: actions/checkout@v4 and uses: actions/setup-python@v5 occurrences
to the corresponding commit SHAs (replace the `@vX` refs with full @<sha>) and add
persist-credentials: false to the checkout step (the actions/checkout step).
Apply the same SHA-pinning to any other uses: actions/checkout@... and
actions/setup-python@... occurrences referenced in the workflow.
.github/workflows/release.yml (2)

60-66: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize and validate version once, then reuse it everywhere.

Current flow mixes normalized (v...) and raw values and interpolates raw input directly in shell.

Suggested fix
+      - name: Normalize version
+        id: ver
+        env:
+          VERSION_INPUT: ${{ inputs.version }}
+        run: |
+          VERSION="$VERSION_INPUT"
+          [[ "$VERSION" == v* ]] || VERSION="v$VERSION"
+          [[ "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]] || exit 1
+          echo "version=$VERSION" >> "$GITHUB_OUTPUT"
@@
       - name: Create tag
         run: |
-          VERSION="${{ inputs.version }}"
-          # Ensure version starts with 'v'
-          if [[ ! "$VERSION" == v* ]]; then
-            VERSION="v${VERSION}"
-          fi
+          VERSION="${{ steps.ver.outputs.version }}"
@@
       - name: Update major version tag
         if: inputs.update_major_tag
         run: |
-          VERSION="${{ inputs.version }}"
-          if [[ ! "$VERSION" == v* ]]; then
-            VERSION="v${VERSION}"
-          fi
+          VERSION="${{ steps.ver.outputs.version }}"
@@
       - name: Create GitHub Release
         uses: softprops/action-gh-release@v2
         with:
-          tag_name: ${{ inputs.version }}
-          name: ${{ inputs.version }}
+          tag_name: ${{ steps.ver.outputs.version }}
+          name: ${{ steps.ver.outputs.version }}

Also applies to: 76-82, 95-96

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 60 - 66, Normalize and validate
the incoming version once at the start of the workflow (e.g., inside the "Create
tag" step) by reading inputs.version into a shell variable, prepend "v" if
missing and validate the format (semantic version regex), then export that
normalized value to the environment (via GITHUB_ENV) so all subsequent steps use
the same canonical $VERSION; update any later steps that currently reference the
raw inputs.version to use the exported $VERSION instead to avoid mixing raw and
normalized values.

19-21: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move write permissions to the specific jobs that need them.

Workflow-level write access is broader than necessary.

Suggested fix
 permissions:
-  contents: write
-  packages: write
+  contents: read
+  packages: read
 
 jobs:
   validate:
+    permissions:
+      contents: read
@@
   release:
+    permissions:
+      contents: write
@@
   docker:
+    permissions:
+      contents: read
+      packages: write
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 19 - 21, Remove broad
workflow-level write permissions (the top-level permissions block granting
contents: write and packages: write) and instead set workflow-level permissions
to the least privilege (e.g., contents: read). For each job that actually
requires write access (e.g., the job that creates/releases GH Releases or
publishes packages—look for jobs named "release", "publish", "publish-package",
or similar), add a job-level permissions block with the exact scopes needed
(contents: write and/or packages: write). Ensure no other jobs inherit write
access and verify any step using actions that require package or contents write
has the corresponding job-level permissions.
CONTRIBUTING.md (1)

50-50: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align documented target Python versions with CI support.

This line conflicts with the CI matrix tested in this PR.

Suggested fix
-- Target Python version: 3.11
+- Target Python versions: 3.10–3.12
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CONTRIBUTING.md` at line 50, The CONTRIBUTING line "Target Python version:
3.11" conflicts with the CI matrix in this PR; update that exact string to match
the CI-tested Python versions (e.g., replace the single-version entry with the
list or range used in the CI matrix such as "Target Python versions: 3.10, 3.11"
or the specific set from the PR) so the documented target versions align with
CI.
src/sync_engine.py (1)

447-450: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle $GITHUB_OUTPUT write failures without failing the sync run.

A filesystem error here can flip a successful sync into a failed action step.

Suggested fix
-        with open(output_file, "a", encoding="utf-8") as f:
-            f.write(f"sync_status={overall}\n")
-            f.write(f"files_synced={total_synced}\n")
-            f.write(f"bytes_transferred={total_bytes}\n")
+        try:
+            with open(output_file, "a", encoding="utf-8") as f:
+                f.write(f"sync_status={overall}\n")
+                f.write(f"files_synced={total_synced}\n")
+                f.write(f"bytes_transferred={total_bytes}\n")
+        except OSError as e:
+            logger.warning("Failed to write GitHub outputs to %s: %s", output_file, e)
+            return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/sync_engine.py` around lines 447 - 450, Wrap the block that writes to
output_file (the lines writing sync_status={overall},
files_synced={total_synced}, bytes_transferred={total_bytes}) in a try/except
that catches OSError/IOError and logs a non-fatal warning instead of allowing
the exception to propagate and fail the sync run; reference the same variables
(output_file, overall, total_synced, total_bytes) and use the module's logger
(or logging.warning) to record the failure and the exception details, then
continue without re-raising.
.github/workflows/sync.yml (1)

30-33: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Set explicit least-privilege permissions for the sync job.

actions/github-script issue creation can fail under read-only default token permissions.

Suggested fix
 name: HF-MS Sync
+permissions:
+  contents: read
@@
   sync:
     needs: preflight
+    permissions:
+      contents: read
+      issues: write

Also applies to: 96-100, 189-191

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/sync.yml around lines 30 - 33, Add an explicit
least-privilege permissions block to the job(s) that call actions/github-script
(e.g., the preflight/sync job) so the workflow token can create issues; inside
the job definition (e.g., job name "preflight" / "sync") add a permissions map
such as `permissions: contents: read issues: write` (or the minimal scopes your
script needs) and mirror the same change for the other jobs referenced in the
comment so they also declare explicit minimal permissions instead of relying on
defaults.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In @.github/workflows/sync.yml:
- Around line 131-145: The ARGS construction uses unquoted interpolations (ARGS,
inputs.sync_target, inputs.direction) and is passed to python via
word-splitting, enabling argument-splitting/shell injection; fix by quoting all
GitHub input expansions and argument interpolations (use "${{ inputs.sync_target
}}", "${{ inputs.direction }}", and quote when appending to ARGS or, better,
build a safe arg array and pass it to python -m src.sync_engine without unquoted
expansion) so values cannot inject extra tokens, and add explicit workflow- or
job-level permissions (e.g., permissions: issues: write) so
actions/github-script@v7 can reliably list/create issues on failure.

---

Duplicate comments:
In @.github/workflows/ci.yml:
- Around line 5-7: The CI config only triggers for branch name "main" under the
YAML keys "branches:" for push and the "pull_request:" block, but the PR targets
"master"; update the branch filters in the CI workflow so they include the
target branch—either add "master" alongside "main" in both branches arrays
(branches: [main, master]) or replace "main" with "master" in the push and
pull_request "branches:" entries to match your repo's default branch.
- Around line 17-19: Replace floating action tags with their immutable SHA
digests and disable persisted checkout credentials: update uses:
actions/checkout@v4 and uses: actions/setup-python@v5 occurrences to the
corresponding commit SHAs (replace the `@vX` refs with full @<sha>) and add
persist-credentials: false to the checkout step (the actions/checkout step).
Apply the same SHA-pinning to any other uses: actions/checkout@... and
actions/setup-python@... occurrences referenced in the workflow.

In @.github/workflows/release.yml:
- Around line 60-66: Normalize and validate the incoming version once at the
start of the workflow (e.g., inside the "Create tag" step) by reading
inputs.version into a shell variable, prepend "v" if missing and validate the
format (semantic version regex), then export that normalized value to the
environment (via GITHUB_ENV) so all subsequent steps use the same canonical
$VERSION; update any later steps that currently reference the raw inputs.version
to use the exported $VERSION instead to avoid mixing raw and normalized values.
- Around line 19-21: Remove broad workflow-level write permissions (the
top-level permissions block granting contents: write and packages: write) and
instead set workflow-level permissions to the least privilege (e.g., contents:
read). For each job that actually requires write access (e.g., the job that
creates/releases GH Releases or publishes packages—look for jobs named
"release", "publish", "publish-package", or similar), add a job-level
permissions block with the exact scopes needed (contents: write and/or packages:
write). Ensure no other jobs inherit write access and verify any step using
actions that require package or contents write has the corresponding job-level
permissions.

In @.github/workflows/sync.yml:
- Around line 30-33: Add an explicit least-privilege permissions block to the
job(s) that call actions/github-script (e.g., the preflight/sync job) so the
workflow token can create issues; inside the job definition (e.g., job name
"preflight" / "sync") add a permissions map such as `permissions: contents: read
issues: write` (or the minimal scopes your script needs) and mirror the same
change for the other jobs referenced in the comment so they also declare
explicit minimal permissions instead of relying on defaults.

In `@CONTRIBUTING.md`:
- Line 50: The CONTRIBUTING line "Target Python version: 3.11" conflicts with
the CI matrix in this PR; update that exact string to match the CI-tested Python
versions (e.g., replace the single-version entry with the list or range used in
the CI matrix such as "Target Python versions: 3.10, 3.11" or the specific set
from the PR) so the documented target versions align with CI.

In `@src/sync_engine.py`:
- Around line 447-450: Wrap the block that writes to output_file (the lines
writing sync_status={overall}, files_synced={total_synced},
bytes_transferred={total_bytes}) in a try/except that catches OSError/IOError
and logs a non-fatal warning instead of allowing the exception to propagate and
fail the sync run; reference the same variables (output_file, overall,
total_synced, total_bytes) and use the module's logger (or logging.warning) to
record the failure and the exception details, then continue without re-raising.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 18a419b0-dc93-4d8d-b0a6-78ce9536724e

📥 Commits

Reviewing files that changed from the base of the PR and between 029ff97 and 0c0bdb6.

📒 Files selected for processing (24)
  • .github/ISSUE_TEMPLATE/bug_report.yml
  • .github/ISSUE_TEMPLATE/feature_request.yml
  • .github/PULL_REQUEST_TEMPLATE.md
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • .github/workflows/sync.yml
  • CHANGELOG.md
  • CONTRIBUTING.md
  • Dockerfile
  • action.yml
  • entrypoint.sh
  • src/adapters/huggingface_adapter.py
  • src/adapters/modelscope_adapter.py
  • src/change_detector.py
  • src/report.py
  • src/sync_engine.py
  • src/utils.py
  • tests/e2e/verify_sync.py
  • tests/test_adapters.py
  • tests/test_change_detector.py
  • tests/test_config.py
  • tests/test_sync_engine.py
  • tests/test_sync_engine_extended.py
  • tests/test_utils.py
💤 Files with no reviewable changes (1)
  • tests/test_utils.py
✅ Files skipped from review due to trivial changes (9)
  • .github/ISSUE_TEMPLATE/bug_report.yml
  • CHANGELOG.md
  • src/adapters/huggingface_adapter.py
  • src/change_detector.py
  • tests/e2e/verify_sync.py
  • src/report.py
  • tests/test_sync_engine.py
  • tests/test_config.py
  • src/utils.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • .github/ISSUE_TEMPLATE/feature_request.yml
  • action.yml
  • tests/test_change_detector.py
  • Dockerfile
  • src/adapters/modelscope_adapter.py
  • tests/test_adapters.py
  • tests/test_sync_engine_extended.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/sync_engine.py (1)

380-380: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard $GITHUB_OUTPUT writes so reporting failures don’t fail the run.

Line 380 now executes this path even for empty configs, so an OSError while writing outputs can still fail the action step despite no sync failure. Please wrap the file write in try/except OSError and log a warning.

Suggested fix
-        with open(output_file, "a", encoding="utf-8") as f:
-            f.write(f"sync_status={overall}\n")
-            f.write(f"files_synced={total_synced}\n")
-            f.write(f"bytes_transferred={total_bytes}\n")
+        try:
+            with open(output_file, "a", encoding="utf-8") as f:
+                f.write(f"sync_status={overall}\n")
+                f.write(f"files_synced={total_synced}\n")
+                f.write(f"bytes_transferred={total_bytes}\n")
+        except OSError as e:
+            logger.warning("Failed to write GitHub outputs to %s: %s", output_file, e)
+            return

Also applies to: 450-454

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/sync_engine.py` at line 380, Wrap the code that writes to the GitHub
Actions outputs (the _write_github_outputs(...) implementation and the other
output-write site around lines where outputs are written, e.g., the block
invoked at lines 450-454) in a try/except OSError block so an OSError does not
kill the run; catch OSError and call self.logger.warning (or process logger used
in this module) with a clear message including the exception, and ensure normal
execution continues when writing $GITHUB_OUTPUT fails. Ensure both the
_write_github_outputs function and the other output-write call are guarded the
same way.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/sync_engine.py`:
- Line 380: Wrap the code that writes to the GitHub Actions outputs (the
_write_github_outputs(...) implementation and the other output-write site around
lines where outputs are written, e.g., the block invoked at lines 450-454) in a
try/except OSError block so an OSError does not kill the run; catch OSError and
call self.logger.warning (or process logger used in this module) with a clear
message including the exception, and ensure normal execution continues when
writing $GITHUB_OUTPUT fails. Ensure both the _write_github_outputs function and
the other output-write call are guarded the same way.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5881ed48-70cb-4e02-a125-d1c642a3de8f

📥 Commits

Reviewing files that changed from the base of the PR and between 0c0bdb6 and 7cce9f7.

📒 Files selected for processing (4)
  • .github/workflows/ci.yml
  • CONTRIBUTING.md
  • src/sync_engine.py
  • tests/test_sync_engine_extended.py
✅ Files skipped from review due to trivial changes (1)
  • CONTRIBUTING.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_sync_engine_extended.py

The original `docker run ... || true` silently swallowed all errors,
masking entrypoint bugs (argument parsing, import failures).

Changes:
- Add GITHUB_OUTPUT and GITHUB_STEP_SUMMARY env vars to test the
  output-writing code path inside the container
- Capture docker output via tee instead of discarding it
- Use set +e / set -e to tolerate non-zero exit (expected without
  real HF/MS tokens) while still asserting the entrypoint started
  correctly and all INPUT_* arguments were propagated
- Grep for config, dry_run, and direction in output to verify
  argument bridging from env vars to CLI flags

Signed-off-by: dongjiang <dongjiang1989@126.com>
The default sync_config.yaml contains a real model (qwen2.5-7b-instruct)
which triggers API calls to HuggingFace and ModelScope, failing without
real tokens.

New approach:
- Add config/ci_smoke_test.yaml with empty models/datasets
- Docker smoke test uses this config, exercises the full dry-run flow:
  entrypoint → CLI parsing → config loading → sync_all() → GitHub outputs
- Expects clean exit 0 and asserts 4 checkpoints in output:
  config summary, dry_run flag, direction flag, "No sync items configured"

This means the CI Docker test now fully passes without any tokens,
validating the complete pipeline end-to-end.

Signed-off-by: dongjiang <dongjiang1989@126.com>
HubApi.list_repo_files() doesn't exist in the ModelScope SDK — it's a
HuggingFace method name. This caused a silent fallback to HF endpoint
on every MS snapshot, losing file sizes and SHA hashes.

Fix:
- Use get_model_files(repo_id) for models
- Use get_dataset_files(repo_id) for datasets
- Parse dict responses with keys 'Name', 'Size', 'Revision'
- Keep getattr fallback for potential future object returns

Verified: Qwen/Qwen2.5-7B-Instruct now returns 14 files with
proper sizes and SHA-256 hashes directly from the MS API.

Signed-off-by: dongjiang <dongjiang1989@126.com>
The `docker run hf-ms-sync:ci cat /entrypoint.sh` command failed because
Dockerfile ENTRYPOINT is `/entrypoint.sh`, so `cat` was passed as an
argument to the sync engine instead of overriding the command.

Use `--entrypoint cat` to properly override the entrypoint for the
verification step.

Signed-off-by: dongjiang <dongjiang1989@126.com>
@dongjiang1989
dongjiang1989 merged commit 03b11f0 into master May 29, 2026
12 checks passed
@dongjiang1989
dongjiang1989 deleted the change-to-github-marketplace branch May 29, 2026 12:07
dongjiang1989 added a commit that referenced this pull request May 29, 2026
sync.yml:
- Fix word-splitting bug: replace string ARGS with bash array,
  same issue as entrypoint.sh (PR #1 review)

sync_engine.py:
- Wrap HF snapshot fetch in try/except (was unprotected, unlike MS)
- Add explicit guard: HF_TO_MS fails clearly when HF snapshot unavailable
- Add explicit guard: BIDIRECTIONAL fails clearly when both snapshots unavailable
- Fix _update_state: handle None hf_snapshot (was causing AttributeError)
- Change exit code: only exit(1) when ALL items fail, not on partial failure

tests:
- test_both_snapshots_fail_bidirectional: both adapters fail → FAILED
- test_hf_snapshot_fails_ms_to_hf_ok: HF fails but MS_TO_HF only
  needs MS snapshot → SUCCESS (exposes _update_state bug that was fixed)

Signed-off-by: dongjiang <dongjiang1989@126.com>
dongjiang1989 added a commit that referenced this pull request May 29, 2026
* fix: harden sync workflow and snapshot error handling (#3)

sync.yml:
- Fix word-splitting bug: replace string ARGS with bash array,
  same issue as entrypoint.sh (PR #1 review)

sync_engine.py:
- Wrap HF snapshot fetch in try/except (was unprotected, unlike MS)
- Add explicit guard: HF_TO_MS fails clearly when HF snapshot unavailable
- Add explicit guard: BIDIRECTIONAL fails clearly when both snapshots unavailable
- Fix _update_state: handle None hf_snapshot (was causing AttributeError)
- Change exit code: only exit(1) when ALL items fail, not on partial failure

tests:
- test_both_snapshots_fail_bidirectional: both adapters fail → FAILED
- test_hf_snapshot_fails_ms_to_hf_ok: HF fails but MS_TO_HF only
  needs MS snapshot → SUCCESS (exposes _update_state bug that was fixed)

Signed-off-by: dongjiang <dongjiang1989@126.com>

* fix: bidirectional snapshot guard uses 'or', revert partial exit code

Address PR #4 review from gemini-code-assist:

1. BIDIRECTIONAL: change `and` to `or` in snapshot None check.
   Both snapshots are required for bidirectional sync — if either
   is None, detect_bidirectional() crashes with AttributeError on
   the None snapshot. Now raises RuntimeError with specific status
   per adapter (e.g. "HF=OK, MS=FAILED").

2. Exit code: revert to exit(1) on any failure (not just all-fail).
   CI/CD downstream steps already use `if: always()` so they run
   regardless of exit code. Returning 0 on partial failure would
   silently mask errors in automated pipelines.

+1 test: test_one_snapshot_fails_bidirectional

Signed-off-by: dongjiang <dongjiang1989@126.com>

---------

Signed-off-by: dongjiang <dongjiang1989@126.com>
dongjiang1989 added a commit that referenced this pull request May 29, 2026
* fix: harden sync workflow and snapshot error handling (#3)

sync.yml:
- Fix word-splitting bug: replace string ARGS with bash array,
  same issue as entrypoint.sh (PR #1 review)

sync_engine.py:
- Wrap HF snapshot fetch in try/except (was unprotected, unlike MS)
- Add explicit guard: HF_TO_MS fails clearly when HF snapshot unavailable
- Add explicit guard: BIDIRECTIONAL fails clearly when both snapshots unavailable
- Fix _update_state: handle None hf_snapshot (was causing AttributeError)
- Change exit code: only exit(1) when ALL items fail, not on partial failure

tests:
- test_both_snapshots_fail_bidirectional: both adapters fail → FAILED
- test_hf_snapshot_fails_ms_to_hf_ok: HF fails but MS_TO_HF only
  needs MS snapshot → SUCCESS (exposes _update_state bug that was fixed)

Signed-off-by: dongjiang <dongjiang1989@126.com>

* fix: bidirectional snapshot guard uses 'or', revert partial exit code

Address PR #4 review from gemini-code-assist:

1. BIDIRECTIONAL: change `and` to `or` in snapshot None check.
   Both snapshots are required for bidirectional sync — if either
   is None, detect_bidirectional() crashes with AttributeError on
   the None snapshot. Now raises RuntimeError with specific status
   per adapter (e.g. "HF=OK, MS=FAILED").

2. Exit code: revert to exit(1) on any failure (not just all-fail).
   CI/CD downstream steps already use `if: always()` so they run
   regardless of exit code. Returning 0 on partial failure would
   silently mask errors in automated pipelines.

+1 test: test_one_snapshot_fails_bidirectional

Signed-off-by: dongjiang <dongjiang1989@126.com>

* fix: disk space, create_repo, and partial state persistence

Problem analysis from failed sync run (151s, exit code 1, empty .sync_state/):

1. Disk space: 4 parallel downloads of Qwen2.5-7B (~14GB total) fills
   up the GitHub Actions runner (~14GB). Fix: large files (>100MB)
   transfer sequentially (download→upload→delete→next), small files
   still use parallel.

2. create_repo_if_needed: was calling get_repo_snapshot (full file
   listing) just to check if repo exists. Fix: use repo_exists() which
   is a lightweight API call. Also wrap in try/except to be non-fatal.

3. _update_state: only called on full success, so partial transfers
   were lost. Fix: call _update_state whenever files_synced is non-empty,
   so successfully transferred files are recorded and won't be re-synced.

Signed-off-by: dongjiang <dongjiang1989@126.com>

* fix: add repo_type to create_repo and cross-reference file maps in state update

PR #6 review fixes (2 of 3 comments valid):

1. modelscope_adapter.py: Pass repo_type parameter to create_repo()
   - Without this, dataset repos were incorrectly created as model repos
   - ModelScope SDK defaults repo_type='model' when not specified

2. sync_engine.py: Cross-reference file maps when updating sync state
   - When syncing MS→HF, the synced file's SHA is in ms_file_map, not hf_file_map
   - Without fallback, synced_files wouldn't be recorded in state
   - Now tries both maps: hf_file_map.get(fp) or ms_file_map.get(fp)
   - Same logic for MS state: ms_file_map.get(fp) or hf_file_map.get(fp)

Comment 1 (repo_exists method) was invalid: HubApi.repo_exists() does exist
in the ModelScope SDK (verified: returns bool, accepts repo_type parameter).

Signed-off-by: dongjiang <dongjiang1989@126.com>

---------

Signed-off-by: dongjiang <dongjiang1989@126.com>
dongjiang1989 added a commit that referenced this pull request May 29, 2026
sync.yml:
- Fix word-splitting bug: replace string ARGS with bash array,
  same issue as entrypoint.sh (PR #1 review)

sync_engine.py:
- Wrap HF snapshot fetch in try/except (was unprotected, unlike MS)
- Add explicit guard: HF_TO_MS fails clearly when HF snapshot unavailable
- Add explicit guard: BIDIRECTIONAL fails clearly when both snapshots unavailable
- Fix _update_state: handle None hf_snapshot (was causing AttributeError)
- Change exit code: only exit(1) when ALL items fail, not on partial failure

tests:
- test_both_snapshots_fail_bidirectional: both adapters fail → FAILED
- test_hf_snapshot_fails_ms_to_hf_ok: HF fails but MS_TO_HF only
  needs MS snapshot → SUCCESS (exposes _update_state bug that was fixed)

Signed-off-by: dongjiang <dongjiang1989@126.com>
dongjiang1989 added a commit that referenced this pull request May 29, 2026
* fix: harden sync workflow and snapshot error handling (#3)

sync.yml:
- Fix word-splitting bug: replace string ARGS with bash array,
  same issue as entrypoint.sh (PR #1 review)

sync_engine.py:
- Wrap HF snapshot fetch in try/except (was unprotected, unlike MS)
- Add explicit guard: HF_TO_MS fails clearly when HF snapshot unavailable
- Add explicit guard: BIDIRECTIONAL fails clearly when both snapshots unavailable
- Fix _update_state: handle None hf_snapshot (was causing AttributeError)
- Change exit code: only exit(1) when ALL items fail, not on partial failure

tests:
- test_both_snapshots_fail_bidirectional: both adapters fail → FAILED
- test_hf_snapshot_fails_ms_to_hf_ok: HF fails but MS_TO_HF only
  needs MS snapshot → SUCCESS (exposes _update_state bug that was fixed)

Signed-off-by: dongjiang <dongjiang1989@126.com>

* fix: bidirectional snapshot guard uses 'or', revert partial exit code

Address PR #4 review from gemini-code-assist:

1. BIDIRECTIONAL: change `and` to `or` in snapshot None check.
   Both snapshots are required for bidirectional sync — if either
   is None, detect_bidirectional() crashes with AttributeError on
   the None snapshot. Now raises RuntimeError with specific status
   per adapter (e.g. "HF=OK, MS=FAILED").

2. Exit code: revert to exit(1) on any failure (not just all-fail).
   CI/CD downstream steps already use `if: always()` so they run
   regardless of exit code. Returning 0 on partial failure would
   silently mask errors in automated pipelines.

+1 test: test_one_snapshot_fails_bidirectional

Signed-off-by: dongjiang <dongjiang1989@126.com>

* fix: disk space, create_repo, and partial state persistence

Problem analysis from failed sync run (151s, exit code 1, empty .sync_state/):

1. Disk space: 4 parallel downloads of Qwen2.5-7B (~14GB total) fills
   up the GitHub Actions runner (~14GB). Fix: large files (>100MB)
   transfer sequentially (download→upload→delete→next), small files
   still use parallel.

2. create_repo_if_needed: was calling get_repo_snapshot (full file
   listing) just to check if repo exists. Fix: use repo_exists() which
   is a lightweight API call. Also wrap in try/except to be non-fatal.

3. _update_state: only called on full success, so partial transfers
   were lost. Fix: call _update_state whenever files_synced is non-empty,
   so successfully transferred files are recorded and won't be re-synced.

Signed-off-by: dongjiang <dongjiang1989@126.com>

* fix: add repo_type to create_repo and cross-reference file maps in state update

PR #6 review fixes (2 of 3 comments valid):

1. modelscope_adapter.py: Pass repo_type parameter to create_repo()
   - Without this, dataset repos were incorrectly created as model repos
   - ModelScope SDK defaults repo_type='model' when not specified

2. sync_engine.py: Cross-reference file maps when updating sync state
   - When syncing MS→HF, the synced file's SHA is in ms_file_map, not hf_file_map
   - Without fallback, synced_files wouldn't be recorded in state
   - Now tries both maps: hf_file_map.get(fp) or ms_file_map.get(fp)
   - Same logic for MS state: ms_file_map.get(fp) or hf_file_map.get(fp)

Comment 1 (repo_exists method) was invalid: HubApi.repo_exists() does exist
in the ModelScope SDK (verified: returns bool, accepts repo_type parameter).

Signed-off-by: dongjiang <dongjiang1989@126.com>

* fix(modelscope): improve error message for permission failures

When uploading to ModelScope fails due to lack of write access, the API
returns a misleading "resource does not exist" error. This commit adds
explicit error detection for permission failures (401/403/"does not exist")
and raises a clear PermissionError with actionable guidance:

- Tells the user they don't have write access
- Suggests using their own namespace (e.g., 'username/model-name')
- Applies to both SDK upload and HF fallback upload paths

This prevents confusing errors when users try to sync to repos they
don't own (e.g., 'Qwen/Qwen2.5-7B-Instruct' instead of their own repo).

Signed-off-by: dongjiang <dongjiang1989@126.com>

* chore(config): use own ModelScope namespace for sync target

The previous ms_repo_id 'Qwen/Qwen2.5-7B-Instruct' belongs to the
Qwen organization and requires write access that external users do
not have. Changed to 'dongjiang1989/Qwen2.5-7B-Instruct' so the
sync can create and upload to a repo under the user's own namespace.

Signed-off-by: dongjiang <dongjiang@kubeservice.com>
Signed-off-by: dongjiang <dongjiang1989@126.com>

* fix(modelscope): case-insensitive permission error detection

Use str(e).lower() when matching error messages so that variants
like "Does Not Exist", "Forbidden", or "FORBIDDEN" are all caught.
Also added "forbidden" as an additional keyword to match.

Addresses gemini-code-assist review on PR #8.

Signed-off-by: dongjiang <dongjiang@kubeservice.com>
Signed-off-by: dongjiang <dongjiang1989@126.com>

* style(modelscope): fix E501 line-too-long in permission checks

Refactor long `or` chain into `any()` with a tuple of keywords to
keep lines under 100 chars.

Signed-off-by: dongjiang <dongjiang@kubeservice.com>
Signed-off-by: dongjiang <dongjiang1989@126.com>

---------

Signed-off-by: dongjiang <dongjiang1989@126.com>
Signed-off-by: dongjiang <dongjiang@kubeservice.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant