Public: anyone can read this; the crew can change it.

A repository's SSH clone address comes from the one SSH builder: never `:0`, and absent when SSH is off or the forge is hosted #7207

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

The SSH clone address is now built in one place, so it can no longer show `:0`. It is left out when SSH is off or when this forge runs as an app hosted by another forge.

shipped production is serving it deploy b2c98db

opened this

The SSH clone address is now built in one place, so it can no longer show :0. It is left out when SSH is off or when this forge runs as an app hosted by another forge.

  • The builder: src/ssh/command.ts gains sshCloneUrl(app, owner, name). It returns undefined when sshCommand(app) does, which covers port unset or 0, and also when appSsh is set (a forge hosted by another). Otherwise it gives ssh://git@<host>:<port>/<owner>/<name>.git, with no port shown when the port is 22. The host lookup it shares with sshCommand moved into a small sshHost helper, and sshCommand's output is unchanged.
  • Repository page (src/web/pages/repo.ts): the About card uses sshCloneUrl for the clone-ssh field. The hand-built line and the outdated "An SSH listener is not built yet" comment are gone.
  • Terminal's About (cloneFacts in src/tui/screens/about.ts): the ssh line comes from sshCloneUrl. The HTTP line is now <origin of config.url>/<owner>/<name>.git, the same address the web page shows. It falls back to the old http://host:port only when no public URL is configured.
  • Pins API (GET /api/v1/user/pins, src/http/api.ts): its sshUrl also uses sshCloneUrl. The issue didn't list it, but it was a third hand-built copy. On a hosted forge with a non-zero port it now returns null.

One behaviour change to check: when neither host nor url is configured, the web page and the pins API now name the host the way sshCommand does (localhost), not the request's hostname. Every surface now gives the same answer.

Tests: the new test/ssh-clone.test.ts checks the builder on its own for ports unset, 0, 2222 and 22, and for a hosted forge. Against a served public repository with one commit, it checks:

  • with sshPort: 0, the page has no clone-ssh field and the terminal's About has no ssh line;
  • with ports 2222 and 22, both show the same address;
  • the terminal's HTTP clone line is https://forge.example/ada/notes.git.

With the old code, the sshPort: 0 case fails. tsc and the design lint are clean, and the new file plus the related suites pass: ssh-site, web-browse, repo-about-card, api-pins-timing, mcp-pins, the tui archive/provenance/upstream/code suites, and web-ui. No existing test was changed.

To try it, run bun test test/ssh-clone.test.ts, or open any repository with code on a forge started with FORGE_SSH_PORT=0: the About card shows only the HTTPS clone.

🤖 Generated with Claude Code

Closes #7201

commented

The SSH clone address is now built in one place, so it can no longer show :0. It is left out when SSH is off or when this forge runs as an app hosted by another forge.

  • The builder: src/ssh/command.ts gains sshCloneUrl(app, owner, name). It returns undefined when sshCommand(app) does, which covers port unset or 0, and also when appSsh is set (a forge hosted by another). Otherwise it gives ssh://git@<host>:<port>/<owner>/<name>.git, with no port shown when the port is 22. The host lookup it shares with sshCommand moved into a small sshHost helper, and sshCommand's output is unchanged.
  • Repository page (src/web/pages/repo.ts): the About card uses sshCloneUrl for the clone-ssh field. The hand-built line and the outdated "An SSH listener is not built yet" comment are gone.
  • Terminal's About (cloneFacts in src/tui/screens/about.ts): the ssh line comes from sshCloneUrl. The HTTP line is now <origin of config.url>/<owner>/<name>.git, the same address the web page shows. It falls back to the old http://host:port only when no public URL is configured.
  • Pins API (GET /api/v1/user/pins, src/http/api.ts): its sshUrl also uses sshCloneUrl. The issue didn't list it, but it was a third hand-built copy. On a hosted forge with a non-zero port it now returns null.

One behaviour change to check: when neither host nor url is configured, the web page and the pins API now name the host the way sshCommand does (localhost), not the request's hostname. Every surface now gives the same answer.

Tests: the new test/ssh-clone.test.ts checks the builder on its own for ports unset, 0, 2222 and 22, and for a hosted forge. Against a served public repository with one commit, it checks:

  • with sshPort: 0, the page has no clone-ssh field and the terminal's About has no ssh line;
  • with ports 2222 and 22, both show the same address;
  • the terminal's HTTP clone line is https://forge.example/ada/notes.git.

With the old code, the sshPort: 0 case fails. tsc and the design lint are clean, and the new file plus the related suites pass: ssh-site, web-browse, repo-about-card, api-pins-timing, mcp-pins, the tui archive/provenance/upstream/code suites, and web-ui. No existing test was changed.

To try it, run bun test test/ssh-clone.test.ts, or open any repository with code on a forge started with FORGE_SSH_PORT=0: the About card shows only the HTTPS clone.

🤖 Generated with Claude Code

Changes requested — before the latest push

Security: clean. sshCloneUrl builds its address only from operator config (host, url, sshPort) and from owner and repository names that are already validated. The page writes it through copyField, which escapes it. No new input, no write path, no secrets. Gating a hosted forge (appSsh set) and port 0 is right, and moving all three copies onto one builder is the right shape. The preview has no public repository, so the About card can't be seen there. As a hosted app it should show HTTPS only, and that is the case sshCloneUrl covers.

One thing to fix before this lands (the review list asks that every behaviour the change adds has a test):

  1. The pins API change has no test. GET /api/v1/user/pins now returns sshUrl: null on a hosted forge (appSsh set, port non-zero). It also names localhost/config.host rather than the request's hostname. Nothing in test/ reads sshUrl, and the header of the new test file says the pins API is covered when it isn't. Add one case to test/ssh-clone.test.ts: sign in, pin ada/notes, GET /api/v1/user/pins, and assert that items[0].sshUrl is ssh://[email protected]:2222/ada/notes.git at port 2222, and null at port 0 and with appSsh set. Without your change that test fails.

Low, optional: the pins API encodes the owner and name with encodeURIComponent before calling the builder, but the page and the terminal pass them raw. Names are validated, so today the output is identical. Picking one place to encode (inside sshCloneUrl, or nowhere) would keep the single-builder promise exact.

commented

The SSH clone address is built in one place, sshCloneUrl(app, owner, name) in src/ssh/command.ts. It returns nothing when the SSH port is unset or 0, and nothing when this forge runs as an app hosted by another forge (appSsh set). Otherwise it gives ssh://git@<host>:<port>/<owner>/<name>.git, with the port left out at 22.

  • Callers: three places that each built the address by hand now use it:

- the repository page's About card, where the clone-ssh field only appears when there is an address (src/web/pages/repo.ts); - the terminal's About (cloneFacts in src/tui/screens/about.ts); - the sshUrl field of GET /api/v1/user/pins (src/http/api.ts).

  • Terminal HTTP clone: the terminal now shows <origin of the forge's configured URL>/<owner>/<name>.git, the address the web page shows. It falls back to http://host:port only when no URL is configured. The outdated "An SSH listener is not built yet" comment is gone.
  • Host: when neither host nor url is configured, the page and the pins API now name localhost as sshCommand does, not the request's hostname.
  • Review follow-up: the pins API now passes owner and name to the builder without encoding them, as the page and terminal already did. Names are validated, so the output is the same as before.

Tests: test/ssh-clone.test.ts checks the builder on its own for ports unset, 0, 2222 and 22, and for a hosted forge. Against a served public repository with one commit that the signed-in user has pinned, it checks the page, the terminal and GET /api/v1/user/pins together:

  • at port 0, none of them shows an ssh address and sshUrl is null;
  • on a hosted forge with port 2222, again none shows one and sshUrl is null;
  • at 2222 and at 22, all three show the same address;
  • the terminal's HTTP line is https://forge.example/ada/notes.git.

The cases at port 0 and on a hosted forge fail against the old code. The file's header now matches what it tests. tsc and the design lint are clean, and the new file passes along with the related suites (ssh-site, web-browse, repo-about-card, api-pins-timing, mcp-pins, the terminal suites, web-ui). No existing test was changed.

To try it, run bun test test/ssh-clone.test.ts, or start a forge with FORGE_SSH_PORT=0 and open a repository that has code: the About card shows only the HTTPS clone.

🤖 Generated with Claude Code

commented

The SSH clone address is now built in one place, sshCloneUrl(app, owner, name) in src/ssh/command.ts. Its result is the same everywhere:

  • SSH off (port unset or 0): no address.
  • Hosted by another forge (appSsh set): no address, whatever the port.
  • Otherwise: ssh://git@<host>:<port>/<owner>/<name>.git, with the port left out at 22.

Three places that built the address by hand now use it:

  • the repository page's About card, where the clone-ssh field only appears when there is an address (src/web/pages/repo.ts);
  • the terminal's About, in cloneFacts (src/tui/screens/about.ts);
  • the sshUrl field of GET /api/v1/user/pins (src/http/api.ts).

The terminal's HTTP clone line is now <origin of the forge's configured URL>/<owner>/<name>.git, the address the web page shows. It falls back to http://host:port only when no URL is configured. The pins API now passes owner and name to the builder unencoded, as the page and terminal do; names are validated, so the output is unchanged. The outdated "An SSH listener is not built yet" comment is gone.

One behaviour change: when neither host nor url is configured, the page and the pins API now name localhost as sshCommand does, where they used to use the request's hostname.

test/ssh-clone.test.ts checks the builder on its own, then the page, the terminal and the pins API together on a public repository with one commit:

  • Port 0, or a hosted forge at port 2222: none of the three shows an SSH address, and sshUrl is null.
  • Ports 2222 and 22: all three show the same address.
  • HTTP line: the terminal shows https://forge.example/ada/notes.git.

The port-0 and hosted cases fail against the old code. No existing test was changed.

The previous check failed two timing tests in test/cloud-org-apps-timing.test.ts (/organizations/new and /apps/open). They pass here alone and with six copies running at once. Those pages don't reach this change: the only shared piece is the page chrome's call to sshCommand, whose output is the same. The same check run killed three other shards after 5–9 s, so the machine was overloaded. The last commit is empty and only re-runs the check; the tests were not loosened.

To try it, run bun test test/ssh-clone.test.ts, or start a forge with FORGE_SSH_PORT=0 and open a repository that has code: the About card shows only the HTTPS clone.

🤖 Generated with Claude Code

Approved

My earlier asks are both met.

  1. The pins API has a test (blocking): met. test/ssh-clone.test.ts now signs in, pins ada/notes and reads items[0].sshUrl from GET /api/v1/user/pins in every case. It is null at port 0, null on a hosted forge (appSsh set, port 2222), and the same address as the page and the terminal at ports 2222 and 22. The hosted and port-0 cases fail against the old hand-built copy, and the file's header now matches what it tests.
  2. Encode the names in one place (optional): met. api.ts passes repo.ownerName and repo.name raw to sshCloneUrl, as the page and the terminal do, so the single builder really is the single answer.

The only lines changed since my last review are that call and the test file. They add no input and no write path, and the output still goes through copyField's escaping on the page and through JSON in the API. Security: clean.

The preview answers / with 200, but it still has no public repository, so the About card can't be seen there. The served test covers the page, the terminal and the API together.

Non-blocking: the empty commit that re-ran the check is noise in the history; squash it away if the landing keeps commits.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs