diff --git a/packages/experimental/forge-github/README.i18n.yaml b/packages/experimental/forge-github/README.i18n.yaml index a38adba694..5562ab4c54 100644 --- a/packages/experimental/forge-github/README.i18n.yaml +++ b/packages/experimental/forge-github/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 2f8a621b991260a1 /deepseek-ai-dsh-experimental-forge-github/use-this-package: - en: 3ce1c5d33f0abb6d - zh: 3ff3b4d8f3ed860f + en: 5bb5561b23d85467 + zh: 4d65d9c776983419 /deepseek-ai-dsh-experimental-forge-github/understand-the-implementation: en: 194951c8b5093f7d zh: 9c5ffaa9e531bd1f diff --git a/packages/experimental/forge-github/README.md b/packages/experimental/forge-github/README.md index 2aa4aa1e11..b39f390815 100644 --- a/packages/experimental/forge-github/README.md +++ b/packages/experimental/forge-github/README.md @@ -36,7 +36,7 @@ Mount the provider after the forge registry and list every GitHub Enterprise hos The provider runs `gh` as a host process, even when Sessions execute commands in another execution world. Sign in once per host with `gh auth login --hostname `. The child receives the host environment without `DSH_*` variables and without variables whose names contain `KEY`, `PASSWORD`, `SECRET`, or `TOKEN`, so `GH_TOKEN` and `GITHUB_TOKEN` are not forwarded and `gh` uses its own credential store. Prompts, update notices, pagers, and colors are disabled. `ghCommand`, `hosts`, `timeoutMs`, and `maxOutputBytes` are listed in the [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-forge-github). -The provider serves a remote URL in `https`, `http`, `ssh`, or `git` form, or in scp form such as `git@github.com:owner/name.git`, when its host is configured. It serves pull request URLs of the form `https://///pull/` with any trailing path, query, or fragment, and builds that form without the suffix as the canonical URL. A head lookup returns up to 20 open pull requests whose head is the branch, newest first, excluding pull requests from forks; a lookup of the default branch returns none. +The provider serves a remote URL in `https`, `http`, `ssh`, or `git` form, or in scp form such as `git@github.com:owner/name.git`, when its host is configured. HTTP(S) remotes retain custom ports in the host identity; SSH and Git transport ports are excluded. It serves pull request URLs of the form `https://///pull/` with any trailing path, query, or fragment, and builds that form without the suffix as the canonical URL. A head lookup returns up to 20 open pull requests whose head is the branch, newest first, excluding pull requests from forks; a lookup of the default branch returns none. Check runs map to `pending` until completed, then to `success`, `failure` (failure, timed out, startup failure, or action required), `cancelled`, `skipped`, or `neutral`. Commit status contexts map `SUCCESS` to `success`, `FAILURE` and `ERROR` to `failure`, and every other state to `pending`. A GraphQL `NOT_FOUND` error becomes a `not-found` failure. A response without GraphQL data or errors, including an HTTP error JSON body, uses the first stderr diagnostic or the body's message. Authentication diagnostics become `unauthenticated`; rate limits and other failures become `unavailable`. Head lookups preserve the same diagnostic in their rejection. diff --git a/packages/experimental/forge-github/README.zh.md b/packages/experimental/forge-github/README.zh.md index 821b468f72..9b090019cb 100644 --- a/packages/experimental/forge-github/README.zh.md +++ b/packages/experimental/forge-github/README.zh.md @@ -36,7 +36,7 @@ kind: "package-reference" 即使 Session 在其他执行环境中运行命令,提供方也以宿主进程的形式运行 `gh`。每个主机执行一次 `gh auth login --hostname ` 完成登录。子进程继承的宿主环境去掉了 `DSH_*` 变量以及名称包含 `KEY`、`PASSWORD`、`SECRET` 或 `TOKEN` 的变量,因此 `GH_TOKEN` 与 `GITHUB_TOKEN` 不会被转发,`gh` 使用自己的凭据存储。交互提示、更新提醒、分页器与颜色输出均被关闭。`ghCommand`、`hosts`、`timeoutMs` 与 `maxOutputBytes` 见[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-forge-github)。 -当主机在配置中时,提供方服务 `https`、`http`、`ssh`、`git` 形式的远端 URL,以及 `git@github.com:owner/name.git` 这类 scp 形式。它服务 `https://///pull/` 形式的 pull request URL,允许其后附带任意路径、查询或片段,并以去掉这些后缀的形式作为规范 URL。按 head 分支查找时,最多返回 20 个以该分支为 head 的打开状态 pull request,按创建时间从新到旧排列,不包含来自 fork 的 pull request;查找默认分支时返回空结果。 +当主机在配置中时,提供方服务 `https`、`http`、`ssh`、`git` 形式的远端 URL,以及 `git@github.com:owner/name.git` 这类 scp 形式。HTTP(S) 远端在主机标识中保留自定义端口;SSH 和 Git 传输端口不计入主机标识。它服务 `https://///pull/` 形式的 pull request URL,允许其后附带任意路径、查询或片段,并以去掉这些后缀的形式作为规范 URL。按 head 分支查找时,最多返回 20 个以该分支为 head 的打开状态 pull request,按创建时间从新到旧排列,不包含来自 fork 的 pull request;查找默认分支时返回空结果。 检查运行(check run)在完成前映射为 `pending`,完成后映射为 `success`、`failure`(失败、超时、启动失败或需要操作)、`cancelled`、`skipped` 或 `neutral`。提交状态上下文把 `SUCCESS` 映射为 `success`,把 `FAILURE` 与 `ERROR` 映射为 `failure`,其余状态均映射为 `pending`。GraphQL 的 `NOT_FOUND` 错误转为 `not-found` 失败。响应不含 GraphQL data 或 errors 时(包括 HTTP 错误 JSON),使用 stderr 的首条诊断或响应体的 message。认证诊断转为 `unauthenticated`;限流与其他失败转为 `unavailable`。head 查找在拒绝时保留相同诊断。 diff --git a/packages/experimental/forge-github/src/urls.ts b/packages/experimental/forge-github/src/urls.ts index 8349f9b591..64b42988d1 100644 --- a/packages/experimental/forge-github/src/urls.ts +++ b/packages/experimental/forge-github/src/urls.ts @@ -26,13 +26,15 @@ export function isSegment(value: string): boolean { * Parse a Git remote URL. Accepts `http(s)://[user@]host[:port]/owner/name`, * `ssh://[user@]host[:port]/owner/name`, `git://host/owner/name`, and the * scp-like `[user@]host:owner/name`, each with an optional `.git` suffix or - * trailing slash. + * trailing slash. HTTP(S) ports belong to the host identity; SSH and Git + * transport ports do not. * @param url - fetch or push URL as Git reports it. * @returns the location, or undefined for other URL shapes. */ export function parseRemoteUrl(url: string): GitHubLocation | undefined { const trimmed = url.trim() - const scheme = /^(?:https?|ssh|git):\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?\/(.+)$/i.exec(trimmed) + const scheme = /^https?:\/\/(?:[^@/]+@)?([^/:]+(?::\d+)?)\/(.+)$/i.exec(trimmed) + ?? /^(?:ssh|git):\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?\/(.+)$/i.exec(trimmed) const scp = scheme === null ? /^(?:[^@/:]+@)?([^/:]+):(?!\/\/)(.+)$/.exec(trimmed) : null const match = scheme ?? scp if (match === null) return undefined diff --git a/packages/experimental/forge-github/tests/provider.spec.ts b/packages/experimental/forge-github/tests/provider.spec.ts index d80b8203f3..da0df8fba4 100644 --- a/packages/experimental/forge-github/tests/provider.spec.ts +++ b/packages/experimental/forge-github/tests/provider.spec.ts @@ -42,6 +42,16 @@ describe('GitHub provider addressing', () => { it('builds canonical pull request URLs', () => { expect(subject.changeRequestUrl(pull(4, enterprise))).toBe('https://ghe.corp.example/team/svc/pull/4') }) + + it('uses a configured HTTP port for URL mapping and GitHub queries', async () => { + const gh = fakeGh(() => output({ data: { r0: { p0: pullRequestNode(1) } } })) + const subject = new GitHubProvider(gh.run, ['ghe.example.com:8443']) + const repository = subject.repositoryFromRemote('https://ghe.example.com:8443/team/repo.git')! + const ref = { repository, number: 1 } + expect(subject.changeRequestFromUrl(subject.changeRequestUrl(ref))).toEqual(ref) + await subject.fetch([ref], signal) + expect(gh.calls[0]?.[3]).toBe('ghe.example.com:8443') + }) }) describe('GitHub provider fetch', () => { diff --git a/packages/experimental/forge-github/tests/urls.spec.ts b/packages/experimental/forge-github/tests/urls.spec.ts index e202507e7a..b8299f7fef 100644 --- a/packages/experimental/forge-github/tests/urls.spec.ts +++ b/packages/experimental/forge-github/tests/urls.spec.ts @@ -9,6 +9,9 @@ describe('remote URLs', () => { ['https://user:secret@GitHub.com/octo/demo', demo], ['http://github.com/octo/demo/', demo], ['ssh://git@github.com:22/octo/demo.git', demo], + ['https://ghe.example.com:8443/team/repo.git', { host: 'ghe.example.com:8443', owner: 'team', name: 'repo' }], + ['http://ghe.example.com:8080/team/repo', { host: 'ghe.example.com:8080', owner: 'team', name: 'repo' }], + ['SSH://git@ghe.example.com:7999/team/repo.git', { host: 'ghe.example.com', owner: 'team', name: 'repo' }], ['git://github.com/octo/demo', demo], ['git@github.com:octo/demo.git', demo], ['github.com:octo/demo.git', demo], @@ -40,6 +43,10 @@ describe('remote URLs', () => { }) describe('pull request URLs', () => { + it('uses the same host identity as an HTTP remote with a custom port', () => { + const remote = parseRemoteUrl('https://ghe.example.com:8443/team/repo.git') + expect(parsePullRequestUrl('https://ghe.example.com:8443/team/repo/pull/1')).toEqual({ ...remote, number: 1 }) + }) it.each([ ['https://github.com/octo/demo/pull/12', { ...demo, number: 12 }], ['https://GITHUB.com/octo/demo/pull/12/files?diff=split#top', { ...demo, number: 12 }],