Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/commands/compute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ export const userKnownHostsPath = () => join(homedir(), '.ssh', 'known_hosts')
/** The renewal-hook command prefix. ssh-config.ts appends the validated alias. */
export const ENSURE_CERT_COMMAND = 'insta __ssh-ensure-cert'

type SSHOpts = LifeOpts & { setup?: boolean; ensureCert?: string; json?: boolean }
type SSHOpts = LifeOpts & { setup?: boolean; ensureCert?: string; project?: string; json?: boolean }

/** What an alias stands for. The renewal hook is handed nothing but the alias —
* no positional argument, no guarantee the cwd is even a linked project — so
Expand Down Expand Up @@ -1953,7 +1953,8 @@ export async function computeSSH(serviceName: string | undefined, opts: SSHOpts,
const mint = deps.mint ?? mintCert
const emit = deps.emit ?? info
const api = await (deps.loadApi ?? ApiClient.load)()
const p = await (deps.loadProject ?? requireProject)()
// 'main' is what a link records by default; anything else makes assertAliasFree refuse a setup of the same service from a linked directory.
const p = opts.project !== undefined ? { projectId: opts.project, branch: 'main' } : await (deps.loadProject ?? requireProject)()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: --project always queries main, even when the project’s default branch is trunk, so SSH can miss or target the wrong branch’s service. Resolve the project’s default branch when --branch is omitted, while preserving --branch as the override.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/commands/compute.ts, line 1957:

<comment>`--project` always queries `main`, even when the project’s default branch is `trunk`, so SSH can miss or target the wrong branch’s service. Resolve the project’s default branch when `--branch` is omitted, while preserving `--branch` as the override.</comment>

<file context>
@@ -1953,7 +1953,8 @@ export async function computeSSH(serviceName: string | undefined, opts: SSHOpts,
   const api = await (deps.loadApi ?? ApiClient.load)()
-  const p = await (deps.loadProject ?? requireProject)()
+  // 'main' is what a link records by default; anything else makes assertAliasFree refuse a setup of the same service from a linked directory.
+  const p = opts.project !== undefined ? { projectId: opts.project, branch: 'main' } : await (deps.loadProject ?? requireProject)()
   const branch = opts.branch ?? p.branch
   const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`)
</file context>
Suggested change
const p = opts.project !== undefined ? { projectId: opts.project, branch: 'main' } : await (deps.loadProject ?? requireProject)()
const p = opts.project !== undefined
? {
projectId: opts.project,
branch: opts.branch ?? (await api.request<{ branches?: Array<{ name: string; is_default?: boolean }> }>('GET', `/projects/${opts.project}`)).branches?.find((b) => b.is_default)?.name ?? 'main',
}
: await (deps.loadProject ?? requireProject)()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Declining: there is no project whose default branch isn't main. The platform hard-codes it when it creates a project: src/provisioning/service.ts:55 in insta-platform @ 4a5efe3a runs BranchesRepo.create({ projectId, name: 'main', isDefault: true, ... }). Every CLI link writer records main too (project.ts link, resolve-project.ts auto-resolve, config.ts INSTA_PROJECT_ID). Resolving the default branch over the network would add a request to get a value that is always the same, and --branch already covers any other branch.

const branch = opts.branch ?? p.branch
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`)
const svc = resolveSoleService(services as ComputeRow[], 'compute', serviceName)
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ compute.command('ssh [service]')
.description("Issue a short-lived SSH certificate for a compute service and print the command that uses it -- this command does NOT open the session itself, it makes `ssh` work. `--setup` does the one-time work: it generates a dedicated key under ~/.insta/ssh (your existing keys are never touched), has the platform sign a SHORT-LIVED certificate for it, adds one @cert-authority line to ~/.ssh/known_hosts so every region is trusted without per-node fingerprint prompts, and writes an ssh_config block AT THE TOP of ~/.ssh/config giving each compute service the alias `<service>.insta`. After that it is plain `ssh api.insta`, scp and -L: the block renews that alias's certificate for you while OpenSSH parses the config. Needs an interactive login -- API keys are refused; use `insta compute exec` for one-shot commands from CI")
.option('--setup', 'do the one-time client setup as well as issuing a certificate')
.option('--ensure-cert <alias>', 'renew the certificate for an alias such as api.insta if it is close to expiry, then exit (used by the ssh_config hook; silent by design)')
.option('--project <id>', 'project to use instead of the linked one; the directory is not linked (branch defaults to main)')
.option('-b, --branch <branch>', 'branch (default: linked)')
.option('--json', 'machine-readable output')
.action(guard((service, o) => computeCmd.computeSSH(service, o)))
Expand Down
16 changes: 16 additions & 0 deletions test/ssh-orchestration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,22 @@ d('the branch the alias was set up on is the one it stays on', () => {
expect(readAliasStore()['api.insta']).not.toHaveProperty('branch')
})

it('targets --project without reading the link, on main', async () => {
const paths: string[] = []
const loadProject = async () => { throw new Error('the link was read despite --project') }
const { deps: d } = deps({ loadApi: recordingApi(paths), loadProject })
await computeSSH('api', { project: 'proj-2' }, d)
expect(paths[0]).toBe('/projects/proj-2/services?branch=main')
expect(readAliasStore()['api.insta']).toMatchObject({ projectId: 'proj-2', branch: 'main' })
})

it('combines --project with --branch', async () => {
const paths: string[] = []
const { deps: d } = deps({ loadApi: recordingApi(paths), loadProject: project('proj-1', 'main') })
await computeSSH('api', { project: 'proj-2', branch: 'feature-x' }, d)
expect(paths[0]).toBe('/projects/proj-2/services?branch=feature-x')
})

it('refuses the same alias on a different branch of the same project', async () => {
// The collision this field exists for, and the one case the branch is the
// ONLY thing distinguishing: same project, same service name, two branches.
Expand Down
Loading