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
3 changes: 3 additions & 0 deletions perry/internal/src/commands/entrypoint.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { addSshKeys } from "./add-ssh-key";
import { runInit } from "./init";
import { syncUserWithHost } from "./sync-user";
import { ensureDockerd, monitorServices, startSshd, tailDockerdLogs, waitForDocker } from "../lib/services";

Expand All @@ -18,6 +19,8 @@ export const runEntrypoint = async () => {
process.exit(1);
return;
}
console.log("[entrypoint] Running workspace initialization...");
await runInit();
Comment on lines +22 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The call to await runInit() in the container entrypoint lacks error handling, which can cause an infinite container restart loop on initialization failure, preventing SSH access.
Severity: CRITICAL | Confidence: High

🔍 Detailed Analysis

The runEntrypoint function calls await runInit() without a try...catch block. If runInit() throws an error, for instance due to a failed git clone or a failing bootstrap script, the exception is not caught locally. This causes the entire container process to exit. Because the container is configured with a restartPolicy: 'unless-stopped', Docker will immediately restart it, leading to an infinite loop of startup, failure, and restart. The SSH daemon is started after runInit(), so this failure loop prevents it from ever running, making the workspace inaccessible for debugging.

💡 Suggested Fix

Wrap the await runInit() call within a try...catch block. In the catch block, log the initialization error but do not exit the process. This will allow the container to finish starting up and enable the SSH daemon, letting users connect to the workspace to diagnose the problem.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: perry/internal/src/commands/entrypoint.ts#L22-L23

Potential issue: The `runEntrypoint` function calls `await runInit()` without a
`try...catch` block. If `runInit()` throws an error, for instance due to a failed git
clone or a failing bootstrap script, the exception is not caught locally. This causes
the entire container process to exit. Because the container is configured with a
`restartPolicy: 'unless-stopped'`, Docker will immediately restart it, leading to an
infinite loop of startup, failure, and restart. The SSH daemon is started after
`runInit()`, so this failure loop prevents it from ever running, making the workspace
inaccessible for debugging.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 8299098

console.log("[entrypoint] Starting SSH daemon...");
await startSshd();
void monitorServices();
Expand Down
42 changes: 38 additions & 4 deletions test/integration/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,51 @@ describe('CLI commands', () => {
await agent.api.deleteWorkspace(name);
});

it('creates workspace with --clone option', async () => {
it('creates workspace with --clone option and clones the repository', async () => {
const name = generateTestWorkspaceName();
const result = await runCLI(['start', name, '--clone', 'https://github.com/example/repo'], {
const repoUrl = 'https://github.com/octocat/Hello-World';
const result = await runCLI(['start', name, '--clone', repoUrl], {
env: cliEnv(),
timeout: 30000,
timeout: 90000,
});
expect(result.code).toBe(0);
expect(result.stdout).toContain(`Workspace '${name}' started`);

const { execInContainer } = await import('../../src/docker');
const containerName = `workspace-${name}`;

const waitForInit = async (maxWait = 60000) => {
const start = Date.now();
while (Date.now() - start < maxWait) {
const check = await execInContainer(
containerName,
['test', '-f', '/home/workspace/.workspace-initialized'],
{ user: 'workspace' }
);
if (check.exitCode === 0) return true;
await new Promise((r) => setTimeout(r, 1000));
}
return false;
};

const initComplete = await waitForInit();
expect(initComplete).toBe(true);

const lsResult = await execInContainer(containerName, ['ls', '-la', '/home/workspace'], {
user: 'root',
});
expect(lsResult.exitCode).toBe(0);
expect(lsResult.stdout).toContain('Hello-World');

const gitDirResult = await execInContainer(
containerName,
['test', '-d', '/home/workspace/Hello-World/.git'],
{ user: 'root' }
);
expect(gitDirResult.exitCode).toBe(0);

await agent.api.deleteWorkspace(name);
});
}, 120000);

it('starts existing workspace without error', async () => {
const name = generateTestWorkspaceName();
Expand Down