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
40 changes: 32 additions & 8 deletions packages/opencode/src/tasks/job-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,21 +84,45 @@ export async function executeStart(projectId: string, params: any, ctx: any): Pr
try {
const { $ } = await import("bun")
const base = await PulseUtils.defaultBranch(cwd)
const result = await $`git checkout -b ${safeFeatureBranch} ${base}`.cwd(cwd).quiet().nothrow()
if (result.exitCode !== 0) {
const stderr = result.stderr ? new TextDecoder().decode(result.stderr) : "Unknown error"
log.error("failed to create feature branch", { issueNumber, featureBranch: safeFeatureBranch, error: stderr })
throw new Error(`Failed to create feature branch ${safeFeatureBranch}: ${stderr}`)

// Check if branch already exists
const branchCheck = await $`git rev-parse --verify ${safeFeatureBranch}`.cwd(cwd).quiet().nothrow()

if (branchCheck.exitCode === 0) {
// Branch exists — check it out instead of creating
const checkoutResult = await $`git checkout ${safeFeatureBranch}`.cwd(cwd).quiet().nothrow()
if (checkoutResult.exitCode !== 0) {
const stderr = checkoutResult.stderr ? new TextDecoder().decode(checkoutResult.stderr) : "Unknown error"
log.error("failed to checkout existing branch", { issueNumber, featureBranch: safeFeatureBranch, error: stderr })
throw new Error(`Failed to checkout existing branch ${safeFeatureBranch}: ${stderr}`)
}
log.info("reusing existing feature branch", { issueNumber, featureBranch: safeFeatureBranch })
} else {
// Branch doesn't exist — create it
const result = await $`git checkout -b ${safeFeatureBranch} ${base}`.cwd(cwd).quiet().nothrow()
if (result.exitCode !== 0) {
const stderr = result.stderr ? new TextDecoder().decode(result.stderr) : "Unknown error"
log.error("failed to create feature branch", { issueNumber, featureBranch: safeFeatureBranch, error: stderr })
throw new Error(`Failed to create feature branch ${safeFeatureBranch}: ${stderr}`)
}
log.info("feature branch created", { issueNumber, featureBranch: safeFeatureBranch })
}

// Push the feature branch to origin - MUST succeed before creating job
const pushResult = await $`git push -u origin ${safeFeatureBranch}`.cwd(cwd).quiet().nothrow()
if (pushResult.exitCode !== 0) {
const stderr = pushResult.stderr ? new TextDecoder().decode(pushResult.stderr) : "Unknown error"
log.error("failed to push feature branch to origin", { issueNumber, featureBranch: safeFeatureBranch, error: stderr })
throw new Error(`Failed to push feature branch ${safeFeatureBranch} to origin: ${stderr}`)
// If push fails because remote branch exists, that's okay — just set upstream
if (stderr.includes("already exists") || stderr.includes("would clobber")) {
log.info("remote branch already exists, setting upstream", { issueNumber, featureBranch: safeFeatureBranch })
await $`git branch --set-upstream-to=origin/${safeFeatureBranch} ${safeFeatureBranch}`.cwd(cwd).quiet().nothrow()
} else {
log.error("failed to push feature branch to origin", { issueNumber, featureBranch: safeFeatureBranch, error: stderr })
throw new Error(`Failed to push feature branch ${safeFeatureBranch} to origin: ${stderr}`)
}
} else {
log.info("feature branch pushed to origin", { issueNumber, featureBranch: safeFeatureBranch })
}
log.info("feature branch created and pushed", { issueNumber, featureBranch: safeFeatureBranch })
} catch (e) {
log.error("error creating feature branch", { issueNumber, featureBranch: safeFeatureBranch, error: String(e) })
throw e
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/tasks/pulse-scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ async function spawnAdversarial(task: Task, jobId: string, projectId: string, pm
try {
adversarialSession = await Session.createNext({
parentID: pmSessionId,
directory: parentSession.directory,
directory: safeWorktree,
title: `Adversarial: ${task.title}`,
permission: [],
})
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/test/fixture/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export async function tmpdir<T>(options?: TmpDirOptions<T>) {
if (options?.git) {
await $`git init`.cwd(dirpath).quiet()
await $`git commit --allow-empty -m "root commit ${dirpath}"`.cwd(dirpath).quiet()
// Ensure we're on a main branch (modern git may auto-create it, or we may be on master/detached)
await $`git checkout -b main`.cwd(dirpath).quiet().nothrow()
}
if (options?.config) {
await Bun.write(
Expand Down
94 changes: 94 additions & 0 deletions packages/opencode/test/tasks/branch-resilience.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, test, expect } from "bun:test"
import { $ } from "bun"
import path from "path"
import { tmpdir } from "../fixture/fixture"
import fs from "fs/promises"

describe("taskctl start: branch creation resilience", () => {
test("checks out existing branch instead of creating duplicate", async () => {
await using tmp = await tmpdir({ git: true })

const branchName = "feature/test-branch"

// Create the branch once
await $`git checkout -b ${branchName}`.cwd(tmp.path).quiet().nothrow()
const firstCheckout = await $`git rev-parse --abbrev-ref HEAD`.cwd(tmp.path).quiet().nothrow()
expect(new TextDecoder().decode(firstCheckout.stdout).trim()).toBe(branchName)

// Make a commit to establish history
await Bun.write(path.join(tmp.path, "test.txt"), "content")
await $`git add test.txt`.cwd(tmp.path).quiet().nothrow()
await $`git commit -m "test commit"`.cwd(tmp.path).quiet().nothrow()

// Switch back to main
await $`git checkout -`.cwd(tmp.path).quiet().nothrow()

// Verify branch exists
const branchCheck = await $`git rev-parse --verify ${branchName}`.cwd(tmp.path).quiet().nothrow()
expect(branchCheck.exitCode).toBe(0)

// Simulate the start command logic: check if branch exists
const existsCheck = await $`git rev-parse --verify ${branchName}`.cwd(tmp.path).quiet().nothrow()

if (existsCheck.exitCode === 0) {
// Branch exists — check it out
const checkoutResult = await $`git checkout ${branchName}`.cwd(tmp.path).quiet().nothrow()
expect(checkoutResult.exitCode).toBe(0)

const currentBranch = await $`git rev-parse --abbrev-ref HEAD`.cwd(tmp.path).quiet().nothrow()
expect(new TextDecoder().decode(currentBranch.stdout).trim()).toBe(branchName)
} else {
throw new Error("Branch should exist")
}
})

test("creates new branch when it doesn't exist", async () => {
await using tmp = await tmpdir({ git: true })

const branchName = "feature/new-branch-test"

// Verify branch doesn't exist
const branchCheck = await $`git rev-parse --verify ${branchName}`.cwd(tmp.path).quiet().nothrow()
expect(branchCheck.exitCode).not.toBe(0)

// Simulate the start command logic: branch doesn't exist, create it
const base = "main"
const createResult = await $`git checkout -b ${branchName} ${base}`.cwd(tmp.path).quiet().nothrow()
expect(createResult.exitCode).toBe(0)

const currentBranch = await $`git rev-parse --abbrev-ref HEAD`.cwd(tmp.path).quiet().nothrow()
expect(new TextDecoder().decode(currentBranch.stdout).trim()).toBe(branchName)
})

test("handles remote branch already exists gracefully", async () => {
await using tmp = await tmpdir({ git: true })

const branchName = "feature/remote-exists-test"

// Create branch
await $`git checkout -b ${branchName}`.cwd(tmp.path).quiet().nothrow()
await Bun.write(path.join(tmp.path, "test.txt"), "content")
await $`git add test.txt`.cwd(tmp.path).quiet().nothrow()
await $`git commit -m "test commit"`.cwd(tmp.path).quiet().nothrow()

// Add a fake remote that points to the current directory
const remotePath = tmp.path
await $`git remote add test-remote ${remotePath}`.cwd(tmp.path).quiet().nothrow()

// Push to the remote
const pushResult = await $`git push test-remote ${branchName}`.cwd(tmp.path).quiet().nothrow().nothrow()

// If push failed with "already exists" error (which it shouldn't in this case, but we test the logic)
// then we would set upstream manually
if (pushResult.exitCode !== 0) {
const stderr = pushResult.stderr ? new TextDecoder().decode(pushResult.stderr) : ""
if (stderr.includes("already exists") || stderr.includes("would clobber")) {
await $`git branch --set-upstream-to=test-remote/${branchName} ${branchName}`.cwd(tmp.path).quiet().nothrow()
}
}

// The branch should be checked out and ready
const currentBranch = await $`git rev-parse --abbrev-ref HEAD`.cwd(tmp.path).quiet().nothrow()
expect(new TextDecoder().decode(currentBranch.stdout).trim()).toBe(branchName)
})
})
Loading