fix(skill): 移除 scanRemoteGithub 的 SSH-only 限制,支持 HTTPS 自托管仓库#157
Conversation
- scanRemoteGithub 方法硬编码要求 protocol === "ssh",导致 Gitea 等 自托管 HTTPS 仓库在技能商店添加时报错 - gitClone 已同时支持 HTTPS 和 SSH URL,该限制是多余的 - 新增 3 个测试验证 HTTPS/SSH Gitea URL 和无效 URL 的处理
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
|
@chung1912 is attempting to deploy a commit to the legeling's projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 Walkthrough总览移除了 变更内容Git 协议支持扩展
预估审查工作量🎯 2 (Simple) | ⏱️ ~12 分钟 诗歌
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@apps/desktop/tests/unit/main/skill-installer.test.ts`:
- Around line 327-353: The test for SSH Gitea URLs does not assert that the SSH
URL is passed unchanged to the git clone helper; update the test that spies on
skillInstallerUtils.gitClone (used by SkillInstaller.scanRemoteGithub) to assert
the spy was called with the exact SSH string
"git@gitea.example.com:icelemon/skills.git" (use vi.spyOn(...).mockResolvedValue
and
expect(skillInstallerUtils.gitClone).toHaveBeenCalledWith("git@gitea.example.com:icelemon/skills.git",
/* other args as appropriate */) ) so you verify the SSH URL is forwarded
correctly by SkillInstaller.scanRemoteGithub.
- Around line 297-325: The test currently only asserts gitClone was called;
update the test for scanRemoteGithub to also assert the exact HTTPS repo URL was
passed to skillInstallerUtils.gitClone by checking the mock call arguments
(e.g., via skillInstallerUtils.gitClone.mock.calls or
expect(skillInstallerUtils.gitClone).toHaveBeenCalledWith(...)) after calling
SkillInstaller.scanRemoteGithub so the HTTPS
"https://gitea.example.com/icelemon/skills" input is validated; keep existing
mocks (SkillInstaller.init and SkillInstaller.scanLocalPreview) and only add the
assertion against skillInstallerUtils.gitClone's first argument.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e792c025-27e5-4e5c-9f11-0009b8181cdc
📒 Files selected for processing (2)
apps/desktop/src/main/services/skill-installer.tsapps/desktop/tests/unit/main/skill-installer.test.ts
💤 Files with no reviewable changes (1)
- apps/desktop/src/main/services/skill-installer.ts
| it("accepts HTTPS Gitea URLs (not just SSH)", async () => { | ||
| await SkillInstaller.init(); | ||
|
|
||
| it("platform IDs are unique", () => { | ||
| const ids = SkillInstaller.getSupportedPlatforms().map((p) => p.id); | ||
| expect(new Set(ids).size).toBe(ids.length); | ||
| vi.spyOn(skillInstallerUtils, "gitClone").mockResolvedValue(undefined); | ||
| vi.spyOn(SkillInstaller, "scanLocalPreview").mockResolvedValue([ | ||
| { | ||
| name: "gitea-skill", | ||
| description: "A skill from Gitea", | ||
| version: "1.0.0", | ||
| author: "icelemon", | ||
| tags: ["gitea"], | ||
| instructions: "# Gitea skill\n\nContent", | ||
| filePath: "/tmp/gitea-skill/SKILL.md", | ||
| localPath: "/tmp/gitea-skill", | ||
| platforms: ["claude"], | ||
| protocol_type: "skill", | ||
| }, | ||
| ]); | ||
|
|
||
| const result = await SkillInstaller.scanRemoteGithub( | ||
| "https://gitea.example.com/icelemon/skills", | ||
| [], | ||
| ); | ||
|
|
||
| expect(result).toHaveLength(1); | ||
| expect(result[0].slug).toBe("gitea-skill"); | ||
| expect(result[0].author).toBe("icelemon"); | ||
| expect(skillInstallerUtils.gitClone).toHaveBeenCalled(); | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
建议验证传递给 gitClone 的实际 URL 参数。
当前测试仅检查 gitClone 是否被调用,但未验证传递的 URL 是否正确。鉴于此 PR 的核心目标是支持 HTTPS URL,应当验证 HTTPS URL "https://gitea.example.com/icelemon/skills" 确实被传递给了 gitClone,以确保端到端行为正确。
♻️ 建议的改进
const result = await SkillInstaller.scanRemoteGithub(
"https://gitea.example.com/icelemon/skills",
[],
);
expect(result).toHaveLength(1);
expect(result[0].slug).toBe("gitea-skill");
expect(result[0].author).toBe("icelemon");
- expect(skillInstallerUtils.gitClone).toHaveBeenCalled();
+ expect(skillInstallerUtils.gitClone).toHaveBeenCalledWith(
+ "https://gitea.example.com/icelemon/skills",
+ expect.stringContaining("icelemon-skills")
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it("accepts HTTPS Gitea URLs (not just SSH)", async () => { | |
| await SkillInstaller.init(); | |
| it("platform IDs are unique", () => { | |
| const ids = SkillInstaller.getSupportedPlatforms().map((p) => p.id); | |
| expect(new Set(ids).size).toBe(ids.length); | |
| vi.spyOn(skillInstallerUtils, "gitClone").mockResolvedValue(undefined); | |
| vi.spyOn(SkillInstaller, "scanLocalPreview").mockResolvedValue([ | |
| { | |
| name: "gitea-skill", | |
| description: "A skill from Gitea", | |
| version: "1.0.0", | |
| author: "icelemon", | |
| tags: ["gitea"], | |
| instructions: "# Gitea skill\n\nContent", | |
| filePath: "/tmp/gitea-skill/SKILL.md", | |
| localPath: "/tmp/gitea-skill", | |
| platforms: ["claude"], | |
| protocol_type: "skill", | |
| }, | |
| ]); | |
| const result = await SkillInstaller.scanRemoteGithub( | |
| "https://gitea.example.com/icelemon/skills", | |
| [], | |
| ); | |
| expect(result).toHaveLength(1); | |
| expect(result[0].slug).toBe("gitea-skill"); | |
| expect(result[0].author).toBe("icelemon"); | |
| expect(skillInstallerUtils.gitClone).toHaveBeenCalled(); | |
| }); | |
| it("accepts HTTPS Gitea URLs (not just SSH)", async () => { | |
| await SkillInstaller.init(); | |
| vi.spyOn(skillInstallerUtils, "gitClone").mockResolvedValue(undefined); | |
| vi.spyOn(SkillInstaller, "scanLocalPreview").mockResolvedValue([ | |
| { | |
| name: "gitea-skill", | |
| description: "A skill from Gitea", | |
| version: "1.0.0", | |
| author: "icelemon", | |
| tags: ["gitea"], | |
| instructions: "# Gitea skill\n\nContent", | |
| filePath: "/tmp/gitea-skill/SKILL.md", | |
| localPath: "/tmp/gitea-skill", | |
| platforms: ["claude"], | |
| protocol_type: "skill", | |
| }, | |
| ]); | |
| const result = await SkillInstaller.scanRemoteGithub( | |
| "https://gitea.example.com/icelemon/skills", | |
| [], | |
| ); | |
| expect(result).toHaveLength(1); | |
| expect(result[0].slug).toBe("gitea-skill"); | |
| expect(result[0].author).toBe("icelemon"); | |
| expect(skillInstallerUtils.gitClone).toHaveBeenCalledWith( | |
| "https://gitea.example.com/icelemon/skills", | |
| expect.stringContaining("icelemon-skills") | |
| ); | |
| }); |
🤖 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 `@apps/desktop/tests/unit/main/skill-installer.test.ts` around lines 297 - 325,
The test currently only asserts gitClone was called; update the test for
scanRemoteGithub to also assert the exact HTTPS repo URL was passed to
skillInstallerUtils.gitClone by checking the mock call arguments (e.g., via
skillInstallerUtils.gitClone.mock.calls or
expect(skillInstallerUtils.gitClone).toHaveBeenCalledWith(...)) after calling
SkillInstaller.scanRemoteGithub so the HTTPS
"https://gitea.example.com/icelemon/skills" input is validated; keep existing
mocks (SkillInstaller.init and SkillInstaller.scanLocalPreview) and only add the
assertion against skillInstallerUtils.gitClone's first argument.
| it("accepts SSH Gitea URLs", async () => { | ||
| await SkillInstaller.init(); | ||
|
|
||
| vi.spyOn(skillInstallerUtils, "gitClone").mockResolvedValue(undefined); | ||
| vi.spyOn(SkillInstaller, "scanLocalPreview").mockResolvedValue([ | ||
| { | ||
| name: "ssh-skill", | ||
| description: "SSH skill", | ||
| version: "1.0.0", | ||
| author: "owner", | ||
| tags: ["ssh"], | ||
| instructions: "# SSH skill", | ||
| filePath: "/tmp/ssh-skill/SKILL.md", | ||
| localPath: "/tmp/ssh-skill", | ||
| platforms: ["claude"], | ||
| protocol_type: "skill", | ||
| }, | ||
| skillsRelativePath: "skills", | ||
| }); | ||
| ]); | ||
|
|
||
| const result = await SkillInstaller.scanRemoteGithub( | ||
| "git@gitea.example.com:icelemon/skills.git", | ||
| [], | ||
| ); | ||
|
|
||
| expect(result).toHaveLength(1); | ||
| expect(result[0].slug).toBe("ssh-skill"); | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
建议验证传递给 gitClone 的 SSH URL 参数。
与 HTTPS 测试类似,此测试也应验证 SSH URL "git@gitea.example.com:icelemon/skills.git" 是否正确传递给了 gitClone,以确保 SSH 格式 URL 未被意外修改。
♻️ 建议的改进
const result = await SkillInstaller.scanRemoteGithub(
"git@gitea.example.com:icelemon/skills.git",
[],
);
expect(result).toHaveLength(1);
expect(result[0].slug).toBe("ssh-skill");
+ expect(skillInstallerUtils.gitClone).toHaveBeenCalledWith(
+ "git@gitea.example.com:icelemon/skills.git",
+ expect.stringContaining("icelemon-skills")
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it("accepts SSH Gitea URLs", async () => { | |
| await SkillInstaller.init(); | |
| vi.spyOn(skillInstallerUtils, "gitClone").mockResolvedValue(undefined); | |
| vi.spyOn(SkillInstaller, "scanLocalPreview").mockResolvedValue([ | |
| { | |
| name: "ssh-skill", | |
| description: "SSH skill", | |
| version: "1.0.0", | |
| author: "owner", | |
| tags: ["ssh"], | |
| instructions: "# SSH skill", | |
| filePath: "/tmp/ssh-skill/SKILL.md", | |
| localPath: "/tmp/ssh-skill", | |
| platforms: ["claude"], | |
| protocol_type: "skill", | |
| }, | |
| skillsRelativePath: "skills", | |
| }); | |
| ]); | |
| const result = await SkillInstaller.scanRemoteGithub( | |
| "git@gitea.example.com:icelemon/skills.git", | |
| [], | |
| ); | |
| expect(result).toHaveLength(1); | |
| expect(result[0].slug).toBe("ssh-skill"); | |
| }); | |
| it("accepts SSH Gitea URLs", async () => { | |
| await SkillInstaller.init(); | |
| vi.spyOn(skillInstallerUtils, "gitClone").mockResolvedValue(undefined); | |
| vi.spyOn(SkillInstaller, "scanLocalPreview").mockResolvedValue([ | |
| { | |
| name: "ssh-skill", | |
| description: "SSH skill", | |
| version: "1.0.0", | |
| author: "owner", | |
| tags: ["ssh"], | |
| instructions: "# SSH skill", | |
| filePath: "/tmp/ssh-skill/SKILL.md", | |
| localPath: "/tmp/ssh-skill", | |
| platforms: ["claude"], | |
| protocol_type: "skill", | |
| }, | |
| ]); | |
| const result = await SkillInstaller.scanRemoteGithub( | |
| "git@gitea.example.com:icelemon/skills.git", | |
| [], | |
| ); | |
| expect(result).toHaveLength(1); | |
| expect(result[0].slug).toBe("ssh-skill"); | |
| expect(skillInstallerUtils.gitClone).toHaveBeenCalledWith( | |
| "git@gitea.example.com:icelemon/skills.git", | |
| expect.stringContaining("icelemon-skills") | |
| ); | |
| }); |
🤖 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 `@apps/desktop/tests/unit/main/skill-installer.test.ts` around lines 327 - 353,
The test for SSH Gitea URLs does not assert that the SSH URL is passed unchanged
to the git clone helper; update the test that spies on
skillInstallerUtils.gitClone (used by SkillInstaller.scanRemoteGithub) to assert
the spy was called with the exact SSH string
"git@gitea.example.com:icelemon/skills.git" (use vi.spyOn(...).mockResolvedValue
and
expect(skillInstallerUtils.gitClone).toHaveBeenCalledWith("git@gitea.example.com:icelemon/skills.git",
/* other args as appropriate */) ) so you verify the SSH URL is forwarded
correctly by SkillInstaller.scanRemoteGithub.
Summary
scanRemoteGithub方法中硬编码的protocol !== "ssh"检查,该限制导致 Gitea 等自托管 HTTPS 仓库在技能商店添加时报错scanRemoteGithub only supports SSH repository URLsgitClone函数已同时支持 HTTPS 和 SSH URL,SSH-only 限制是多余的Root Cause
当用户在技能商店添加 Gitea 仓库时,
store-remote-sync.ts的loadGitHubRepoSkills判断!isGitHubHost(parsedRepo.host)为 true(Gitea 不是 github.com),于是调用scanRemoteGithub走 git clone 路径。但scanRemoteGithub内部硬编码要求protocol === "ssh",导致 HTTPS URL 被拒绝。Changes
apps/desktop/src/main/services/skill-installer.ts: 删除 SSH-only 检查(4 行)apps/desktop/tests/unit/main/skill-installer.test.ts: 新增scanRemoteGithub测试组(3 个测试)Summary by CodeRabbit
新功能
测试