Skip to content

Conversation

@gotalab
Copy link
Owner

@gotalab gotalab commented Dec 6, 2025

Summary

  • Add skillport update CLI command for updating skills from their origin (GitHub)
  • Track origin metadata (type, url, ref, tree_sha) in .origin.json
  • Use GitHub Tree API for efficient change detection before downloading
  • Configure ruff linting with F, E, W, I (isort), UP (pyupgrade) rules
  • Fix circular imports by using deep module imports

Changes

  • New CLI command: skillport update [SKILL_ID] [--all] [--dry-run] [--force]
  • Origin tracking with TypedDict for type safety
  • Tree SHA comparison to avoid unnecessary downloads
  • Modernized type hints (List → list, Optional → X | None)

Test plan

  • Unit tests for update logic (test_update.py)
  • Unit tests for origin handling (test_origin_v2.py)
  • All 362 existing tests pass
  • Ruff lint passes

Follow-up

  • Extract common GitHub logic between add.py and update.py (refactor)

🤖 Generated with Claude Code

- Implement `skillport update` CLI command for updating skills from origin
- Add origin tracking with TypedDict (type, url, ref, tree_sha)
- Support GitHub origin updates with tree SHA comparison
- Configure ruff with F, E, W, I (isort), UP (pyupgrade) rules
- Fix circular imports by using deep module imports
- Modernize type hints (List -> list, Optional -> |)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copilot AI review requested due to automatic review settings December 6, 2025 09:44
@gotalab gotalab merged commit c6f0ab9 into main Dec 6, 2025
7 checks passed
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a comprehensive skill update system with a new skillport update CLI command that enables updating skills from their original sources (GitHub, local). The implementation tracks origin metadata in .origin.json v2 format with content hashes for efficient change detection and uses the GitHub Tree API to check for updates without downloading tarballs.

Key changes include:

  • New update command with --all, --dry-run, --force, and --check options
  • Origin v2 tracking with content hashes, commit SHAs, and update history
  • Tree-based change detection for GitHub sources to avoid unnecessary downloads

Reviewed changes

Copilot reviewed 65 out of 66 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
src/skillport/modules/skills/public/update.py Core update logic with local/GitHub source handlers and change detection
src/skillport/modules/skills/internal/origin.py Origin v2 format with content hash computation and migration from v1
src/skillport/modules/skills/internal/github.py Enhanced GitHub integration with tree API, commit SHA extraction, and remote hash computation
src/skillport/modules/skills/public/types.py TypedDict definitions for origin types and UpdateResult models
src/skillport/interfaces/cli/commands/update.py CLI command implementation with interactive update flow and JSON output support
src/skillport/modules/skills/public/add.py Enhanced to record content_hash and commit_sha during skill installation
src/skillport/modules/indexing/public/index.py Integrated orphan origin pruning into index build process
tests/unit/test_update.py Comprehensive test coverage for update functionality (404 lines)
tests/unit/test_origin_v2.py Test coverage for origin v2 migration and operations (314 lines)
pyproject.toml Configured ruff with F, E, W, I (isort), UP (pyupgrade) rules
Multiple test files Import statement reordering per isort configuration
Multiple source files Type hint modernization (List → list, Optional → X

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +156 to +161
def compute_content_hash_with_reason(skill_path: Path) -> tuple[str, str | None]:
"""Compute directory content hash with safeguards.
Hash = sha256( join(relpath + NUL + file_sha1 + NUL) for files sorted by relpath )
file_sha1 is sha1 over file bytes (matches Git blob hash).
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

The comment references "Git blob format" and Git's SHA1 algorithm, but this creates SHA256 hashes. While individual file hashes use SHA1 (matching Git), the aggregate hash is SHA256. Consider clarifying the comment to explain: "Uses Git blob SHA1 format for individual files (sha1('blob ' + length + '\0' + contents)), then combines them into an aggregate SHA256 hash."

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +36
# Union type for any origin
Origin = OriginKind | OriginLocal | OriginGitHub
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

In the Origin union type definition, the order matters for type checking. Since OriginKind has total=False (all fields optional), it will match any dict, making the more specific types (OriginLocal, OriginGitHub) unreachable. Consider reordering as OriginGitHub | OriginLocal | OriginKind to check specific types first.

Copilot uses AI. Check for mistakes.
Comment on lines +198 to +212
for p in files:
rel = p.relative_to(skill_path)
try:
data = p.read_bytes()
except OSError:
return "", "unreadable"
# Use Git blob format: sha1("blob " + length + "\0" + contents)
# This matches the SHA returned by GitHub's tree API
blob_header = f"blob {len(data)}\x00".encode()
blob_sha = hashlib.sha1(blob_header + data).hexdigest()
hasher.update(str(rel).encode("utf-8"))
hasher.update(b"\x00")
hasher.update(blob_sha.encode("utf-8"))
hasher.update(b"\x00")

Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

[nitpick] The hash computation uses git blob format with SHA1, but doesn't handle files that may be Git LFS pointers. If a repository uses Git LFS, the hash will be computed over the pointer file content (typically ~120 bytes) rather than the actual large file. Consider documenting this limitation or adding a check for LFS pointer files (they start with "version https://git-lfs.github.com/spec/").

Copilot uses AI. Check for mistakes.
Comment on lines +296 to +299
try:
tree = _fetch_tree(parsed, token)
except Exception:
return ""
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

The GitHub API call catches all exceptions and returns empty string, which loses valuable debugging information. Consider logging the exception before returning empty string, especially for authentication failures (401/403) vs. other errors. This would help users diagnose issues with GITHUB_TOKEN.

Copilot uses AI. Check for mistakes.
effective_keep_structure = (
False if effective_keep_structure is None else effective_keep_structure
)
# 単一スキル: path をスキル名で固定
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

Another Japanese comment: "単一スキル: path をスキル名で固定" should be translated to English ("Single skill: fix path with skill name").

Copilot uses AI. Check for mistakes.
source_path = renamed
temp_dir = renamed
skills = detect_skills(source_path)
# 単一スキルの場合は origin.path をスキル名で確定させる
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

Similarly, the comment "単一スキルの場合は origin.path をスキル名で確定させる" should be translated to English. It means "For single skill case, fix origin.path with skill name".

Copilot uses AI. Check for mistakes.
Comment on lines +213 to +215
prefix = origin_payload.get("path", "").rstrip("/")
if prefix and rel_path and rel_path != prefix and not prefix.endswith(f"/{rel_path}"):
enriched_payload["path"] = f"{prefix}/{rel_path}"
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

[nitpick] The function attempts to narrow the path by trying f"{prefix}/{rel_path}" without first checking if this causes the path to exceed GitHub's path length limits (typically 4096 characters). While unlikely in practice, this could cause issues with deeply nested directory structures. Consider adding a length check or documenting the assumption.

Copilot uses AI. Check for mistakes.
resp = requests.get(url, headers=headers, timeout=10)
if resp.ok:
return resp.json().get("sha", "")[:40] # Full SHA
except Exception:
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
remote_hash = alt_hash
try:
update_origin(skill_id, {"path": candidate}, config=config)
except Exception:
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
try:
rel = source_path.relative_to(source_base)
update_origin(skill_id, {"path": str(rel)}, config=config)
except Exception:
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
@gotalab gotalab deleted the feature/skill-update branch December 6, 2025 09:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants