Files
devclaw-gitea/lib/services/pipeline.ts
Lauren ten Hoor d87b9f68a2 feat: enhance review process and role management
- Refactor reviewPass function to identify states with review checks instead of specific review types.
- Introduce review policies (HUMAN, AGENT, AUTO) to control PR review processes based on developer levels.
- Update projectTick to handle review policies and step routing labels for reviewers and testers.
- Add detailed reviewer instructions to templates for clarity on review responsibilities.
- Implement role:level label management, allowing dynamic creation of labels based on project configuration.
- Enhance task_update tool to support state and level updates, ensuring at least one parameter is provided.
- Update work_finish tool to include reviewer actions (approve, reject) in task completion.
- Modify work_start tool to utilize role-level detection for better level assignment.
- Add tests for new functionalities, including review routing and level detection from labels.
2026-02-16 18:09:53 +08:00

199 lines
6.3 KiB
TypeScript

/**
* Pipeline service — declarative completion rules.
*
* Uses workflow config to determine transitions and side effects.
*/
import type { PluginRuntime } from "openclaw/plugin-sdk";
import type { StateLabel, IssueProvider } from "../providers/provider.js";
import { deactivateWorker } from "../projects.js";
import { runCommand } from "../run-command.js";
import { notify, getNotificationConfig } from "../notify.js";
import { log as auditLog } from "../audit.js";
import { loadConfig } from "../config/index.js";
import { detectStepRouting } from "./queue-scan.js";
import {
DEFAULT_WORKFLOW,
Action,
getCompletionRule,
getNextStateDescription,
getCompletionEmoji,
type CompletionRule,
type WorkflowConfig,
} from "../workflow.js";
export type { CompletionRule };
export type CompletionOutput = {
labelTransition: string;
announcement: string;
nextState: string;
prUrl?: string;
issueUrl?: string;
issueClosed?: boolean;
issueReopened?: boolean;
};
/**
* Get completion rule for a role:result pair.
* Uses workflow config when available.
*/
export function getRule(
role: string,
result: string,
workflow: WorkflowConfig = DEFAULT_WORKFLOW,
): CompletionRule | undefined {
return getCompletionRule(workflow, role, result) ?? undefined;
}
/**
* Execute the completion side-effects for a role:result pair.
*/
export async function executeCompletion(opts: {
workspaceDir: string;
groupId: string;
role: string;
result: string;
issueId: number;
summary?: string;
prUrl?: string;
provider: IssueProvider;
repoPath: string;
projectName: string;
channel?: string;
pluginConfig?: Record<string, unknown>;
/** Plugin runtime for direct API access (avoids CLI subprocess timeouts) */
runtime?: PluginRuntime;
/** Workflow config (defaults to DEFAULT_WORKFLOW) */
workflow?: WorkflowConfig;
}): Promise<CompletionOutput> {
const {
workspaceDir, groupId, role, result, issueId, summary, provider,
repoPath, projectName, channel, pluginConfig, runtime,
workflow = DEFAULT_WORKFLOW,
} = opts;
const key = `${role}:${result}`;
const rule = getCompletionRule(workflow, role, result);
if (!rule) throw new Error(`No completion rule for ${key}`);
const { timeouts } = await loadConfig(workspaceDir, projectName);
let prUrl = opts.prUrl;
// Execute pre-notification actions
for (const action of rule.actions) {
switch (action) {
case Action.GIT_PULL:
try { await runCommand(["git", "pull"], { timeoutMs: timeouts.gitPullMs, cwd: repoPath }); } catch (err) {
auditLog(workspaceDir, "pipeline_warning", { step: "gitPull", issue: issueId, role, error: (err as Error).message ?? String(err) }).catch(() => {});
}
break;
case Action.DETECT_PR:
if (!prUrl) { try { prUrl = await provider.getMergedMRUrl(issueId) ?? undefined; } catch (err) {
auditLog(workspaceDir, "pipeline_warning", { step: "detectPr", issue: issueId, role, error: (err as Error).message ?? String(err) }).catch(() => {});
} }
break;
case Action.MERGE_PR:
try { await provider.mergePr(issueId); } catch (err) {
auditLog(workspaceDir, "pipeline_warning", { step: "mergePr", issue: issueId, role, error: (err as Error).message ?? String(err) }).catch(() => {});
}
break;
}
}
// Get issue early (for URL in notification)
const issue = await provider.getIssue(issueId);
// Get next state description from workflow
const nextState = getNextStateDescription(workflow, role, result);
// Send notification early (before deactivation and label transition which can fail)
const notifyConfig = getNotificationConfig(pluginConfig);
notify(
{
type: "workerComplete",
project: projectName,
groupId,
issueId,
issueUrl: issue.web_url,
role,
result: result as "done" | "pass" | "fail" | "refine" | "blocked",
summary,
nextState,
},
{
workspaceDir,
config: notifyConfig,
groupId,
channel: channel ?? "telegram",
runtime,
},
).catch((err) => {
auditLog(workspaceDir, "pipeline_warning", { step: "notify", issue: issueId, role, error: (err as Error).message ?? String(err) }).catch(() => {});
});
// Deactivate worker + transition label
await deactivateWorker(workspaceDir, groupId, role);
await provider.transitionLabel(issueId, rule.from as StateLabel, rule.to as StateLabel);
// Execute post-transition actions
for (const action of rule.actions) {
switch (action) {
case Action.CLOSE_ISSUE:
await provider.closeIssue(issueId);
break;
case Action.REOPEN_ISSUE:
await provider.reopenIssue(issueId);
break;
}
}
// Send review routing notification when developer completes
if (role === "developer" && result === "done") {
// Re-fetch issue to get labels after transition
const updated = await provider.getIssue(issueId);
const routing = detectStepRouting(updated.labels, "review") as "human" | "agent" | null;
if (routing === "human" || routing === "agent") {
notify(
{
type: "reviewNeeded",
project: projectName,
groupId,
issueId,
issueUrl: updated.web_url,
issueTitle: updated.title,
routing,
prUrl,
},
{
workspaceDir,
config: notifyConfig,
groupId,
channel: channel ?? "telegram",
runtime,
},
).catch((err) => {
auditLog(workspaceDir, "pipeline_warning", { step: "reviewNotify", issue: issueId, role, error: (err as Error).message ?? String(err) }).catch(() => {});
});
}
}
// Build announcement using workflow-derived emoji
const emoji = getCompletionEmoji(role, result);
const label = key.replace(":", " ").toUpperCase();
let announcement = `${emoji} ${label} #${issueId}`;
if (summary) announcement += `${summary}`;
announcement += `\n📋 Issue: ${issue.web_url}`;
if (prUrl) announcement += `\n🔗 PR: ${prUrl}`;
announcement += `\n${nextState}.`;
return {
labelTransition: `${rule.from}${rule.to}`,
announcement,
nextState,
prUrl,
issueUrl: issue.web_url,
issueClosed: rule.actions.includes(Action.CLOSE_ISSUE),
issueReopened: rule.actions.includes(Action.REOPEN_ISSUE),
};
}