Skip to content
Open
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
8 changes: 8 additions & 0 deletions packages/control-plane/src/db/integration-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,14 @@ export class IntegrationSettingsStore {
throw new IntegrationSettingsValidationError("commentActionInstructions must be a string");
}

if (settings.prLabel !== undefined && typeof settings.prLabel !== "string") {
throw new IntegrationSettingsValidationError("prLabel must be a string");
}

if (settings.prLabel !== undefined) {
settings = { ...settings, prLabel: settings.prLabel.trim() || undefined };
}

if (settings.allowedTriggerUsers !== undefined) {
if (
!Array.isArray(settings.allowedTriggerUsers) ||
Expand Down
1 change: 1 addition & 0 deletions packages/control-plane/src/routes/integration-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ async function handleGetResolvedConfig(
allowedTriggerUsers: githubSettings.allowedTriggerUsers ?? null,
codeReviewInstructions: githubSettings.codeReviewInstructions ?? null,
commentActionInstructions: githubSettings.commentActionInstructions ?? null,
prLabel: githubSettings.prLabel ?? null,
},
});
}
Expand Down
16 changes: 16 additions & 0 deletions packages/control-plane/src/session/durable-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,21 @@ export class SessionDO extends DurableObject<Env> {
return webAppUrl + "/session/" + sessionId;
},
createPullRequest: async (input) => {
let prLabel: string | undefined;
const session = this.getSession();
if (session && this.env.DB) {
try {
const settingsStore = new IntegrationSettingsStore(this.env.DB);
const { settings } = await settingsStore.getResolvedConfig(
"github",
`${session.repo_owner}/${session.repo_name}`
);
prLabel = settings.prLabel || undefined;
} catch {
// Best-effort — don't fail PR creation if settings read fails
}
}

const pullRequestService = new SessionPullRequestService({
repository: this.repository,
sourceControlProvider: this.sourceControlProvider,
Expand All @@ -500,6 +515,7 @@ export class SessionDO extends DurableObject<Env> {
});
},
appName: resolveAppName(this.env),
prLabel,
});

return pullRequestService.createPullRequest(input);
Expand Down
3 changes: 3 additions & 0 deletions packages/control-plane/src/session/pull-request-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export interface PullRequestServiceDeps {
broadcastArtifactCreated: (artifact: SessionArtifact) => void;
/** Display name used in the PR body footer (e.g. "Created with [name](url)"). */
appName: string;
/** Label applied to agent-created PRs. Created in the target repo if absent. */
prLabel?: string;
}

/**
Expand Down Expand Up @@ -197,6 +199,7 @@ export class SessionPullRequestService {
body: fullBody,
sourceBranch: sanitizedHeadBranch,
targetBranch: baseBranch,
labels: this.deps.prLabel ? [this.deps.prLabel] : undefined,
});

const artifactId = this.deps.generateId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@ export class GitHubSourceControlProvider implements SourceControlProvider {

// Add labels if requested
if (config.labels && config.labels.length > 0) {
await this.ensureLabels(
auth.token,
config.repository.owner,
config.repository.name,
config.labels
);
await this.addLabels(
auth.token,
config.repository.owner,
Expand Down Expand Up @@ -372,6 +378,41 @@ export class GitHubSourceControlProvider implements SourceControlProvider {
};
}

/**
* Ensure labels exist in a repository, creating any that are missing.
* This is a best-effort operation - failures are logged but don't fail the PR creation.
*/
private async ensureLabels(
accessToken: string,
owner: string,
repo: string,
labels: string[]
): Promise<void> {
for (const label of labels) {
try {
const response = await fetchWithTimeout(
`${GITHUB_API_BASE}/repos/${owner}/${repo}/labels`,
{
method: "POST",
headers: {
Accept: "application/vnd.github.v3+json",
Authorization: `Bearer ${accessToken}`,
"User-Agent": this.userAgent,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: label, color: "ededed" }),
}
);
// 201 = created, 422 = already exists — both are fine
if (!response.ok && response.status !== 422) {
console.warn(`Failed to ensure label "${label}" in ${owner}/${repo}: ${response.status}`);
}
} catch (error) {
console.warn(`Failed to ensure label "${label}" in ${owner}/${repo}:`, error);
}
}
}

/**
* Add labels to a pull request.
* This is a best-effort operation - failures are logged but don't fail the PR creation.
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/types/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export interface GitHubBotSettings {
allowedTriggerUsers?: string[];
codeReviewInstructions?: string;
commentActionInstructions?: string;
/** Label applied to every agent-created PR. The label is created in the target repo if it doesn't exist. */
prLabel?: string;
}

/** Overridable behavior settings for the Linear bot. Used at both global (defaults) and per-repo (overrides) levels. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ function GlobalSettingsSection({
const [commentActionInstructions, setCommentActionInstructions] = useState(
settings?.defaults?.commentActionInstructions ?? ""
);
const [prLabel, setPrLabel] = useState(settings?.defaults?.prLabel ?? "");
const [newUsername, setNewUsername] = useState("");
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
Expand All @@ -161,6 +162,7 @@ function GlobalSettingsSection({
);
setCodeReviewInstructions(settings.defaults?.codeReviewInstructions ?? "");
setCommentActionInstructions(settings.defaults?.commentActionInstructions ?? "");
setPrLabel(settings.defaults?.prLabel ?? "");
}
setInitialized(true);
}
Expand Down Expand Up @@ -188,6 +190,7 @@ function GlobalSettingsSection({
setTriggerUserMode("write_access");
setCodeReviewInstructions("");
setCommentActionInstructions("");
setPrLabel("");
setNewUsername("");
setDirty(false);
toast.success("Settings reset to defaults.");
Expand All @@ -205,13 +208,15 @@ function GlobalSettingsSection({
const handleSave = async () => {
setSaving(true);
setError("");
const normalizedPrLabel = prLabel.trim();

const body: GitHubGlobalConfig = {
defaults: {
autoReviewOnOpen,
...(triggerUserMode === "specific" ? { allowedTriggerUsers } : {}),
...(codeReviewInstructions ? { codeReviewInstructions } : {}),
...(commentActionInstructions ? { commentActionInstructions } : {}),
...(normalizedPrLabel ? { prLabel: normalizedPrLabel } : {}),
},
};

Expand All @@ -228,6 +233,7 @@ function GlobalSettingsSection({

if (res.ok) {
mutate(GLOBAL_SETTINGS_KEY);
setPrLabel(normalizedPrLabel);
toast.success("Settings saved.");
setDirty(false);
} else {
Expand Down Expand Up @@ -473,6 +479,26 @@ function GlobalSettingsSection({
/>
</div>

<div className="mb-4">
<label htmlFor="pr-label" className="block text-sm font-medium text-foreground mb-1">
PR Label
</label>
<p className="text-xs text-muted-foreground mb-2">
Label applied to every agent-created pull request. The label is created in the target
repository if it does not already exist. Leave blank to apply no label.
</p>
<Input
id="pr-label"
value={prLabel}
onChange={(e) => {
setPrLabel(e.target.value);
setDirty(true);
setError("");
}}
placeholder="e.g., contractor"
/>
</div>

Comment thread
coderabbitai[bot] marked this conversation as resolved.
<div className="flex items-center gap-2">
<Button onClick={handleSave} disabled={saving || !dirty}>
{saving ? "Saving..." : "Save"}
Expand Down