Skip to content

fix(skill): 移除 scanRemoteGithub 的 SSH-only 限制,支持 HTTPS 自托管仓库#157

Merged
legeling merged 1 commit into
legeling:mainfrom
chung1912:fix/skill-store-gitea-https
May 29, 2026
Merged

fix(skill): 移除 scanRemoteGithub 的 SSH-only 限制,支持 HTTPS 自托管仓库#157
legeling merged 1 commit into
legeling:mainfrom
chung1912:fix/skill-store-gitea-https

Conversation

@chung1912
Copy link
Copy Markdown
Contributor

@chung1912 chung1912 commented May 28, 2026

Summary

  • 移除 scanRemoteGithub 方法中硬编码的 protocol !== "ssh" 检查,该限制导致 Gitea 等自托管 HTTPS 仓库在技能商店添加时报错 scanRemoteGithub only supports SSH repository URLs
  • gitClone 函数已同时支持 HTTPS 和 SSH URL,SSH-only 限制是多余的
  • 新增 3 个单元测试验证 HTTPS Gitea URL、SSH Gitea URL 和无效 URL 的处理行为

Root Cause

当用户在技能商店添加 Gitea 仓库时,store-remote-sync.tsloadGitHubRepoSkills 判断 !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

  • 新功能

    • 技能安装器现已支持HTTPS和SSH两种Git仓库URL格式,扩展了之前仅支持SSH协议的限制。
  • 测试

    • 更新测试用例以覆盖多种Git URL格式的场景。

Review Change Stack

- scanRemoteGithub 方法硬编码要求 protocol === "ssh",导致 Gitea 等
  自托管 HTTPS 仓库在技能商店添加时报错
- gitClone 已同时支持 HTTPS 和 SSH URL,该限制是多余的
- 新增 3 个测试验证 HTTPS/SSH Gitea URL 和无效 URL 的处理
@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@vercel
Copy link
Copy Markdown

vercel Bot commented May 28, 2026

@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.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

📝 Walkthrough

总览

移除了 SkillInstaller.scanRemoteGithub 中对 SSH 协议的强制检查,允许 HTTPS 和 SSH 两种格式的 Git 仓库 URL;相应调整了测试用例以覆盖两种协议的接受行为。

变更内容

Git 协议支持扩展

层级 / 文件 摘要
移除 SSH 协议强制检查与测试更新
apps/desktop/src/main/services/skill-installer.ts, apps/desktop/tests/unit/main/skill-installer.test.ts
删除 scanRemoteGithub 中的 parsedRepo.protocol !== "ssh" 拒绝分支,允许 HTTPS URL 继续执行克隆流程;新增 HTTPS 和 SSH Gitea URL 的测试用例,验证两种格式均被正确接受与解析;移除不再适用的平台 ID 唯一性和 Kilo/Roo 代码平台的测试断言。

预估审查工作量

🎯 2 (Simple) | ⏱️ ~12 分钟

诗歌

🐰 协议不再单调,
HTTPS 与 SSH 齐奔跑,
测试全面覆盖妙,
Git 仓库任你挑!✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR标题准确总结了主要变更:移除SSH-only限制并支持HTTPS自托管仓库,与代码改动和PR目标完全对应。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4079d7e and 2074030.

📒 Files selected for processing (2)
  • apps/desktop/src/main/services/skill-installer.ts
  • apps/desktop/tests/unit/main/skill-installer.test.ts
💤 Files with no reviewable changes (1)
  • apps/desktop/src/main/services/skill-installer.ts

Comment on lines +297 to 325
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();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
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.

Comment on lines +327 to 353
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");
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
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.

@legeling legeling merged commit 43d0262 into legeling:main May 29, 2026
1 of 3 checks passed
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