fix(pipeline): track produced file paths accurately for deploy URL

- RunContext.producedFiles[] accumulates repo-relative paths of extracted
  code files as juniors write them
- implement selfTestReport now includes producedFiles[]
- runner extractStageText forwards producedFiles= in priorStages
- derivePreviewUrl uses producedFiles list first (exact match), then falls
  back to rawUrlBase/index.html heuristic
- Fixes bug where deployUrl pointed to files/index.html but actual repo
  path was implement/files/frontend/sprints/SPRINT-AUTO/index.html
This commit is contained in:
2026-04-10 20:48:37 +09:00
parent 17b2f2b232
commit be8715a7f0
2 changed files with 40 additions and 7 deletions

View File

@@ -31,6 +31,8 @@ interface RunContext {
rails: RailsClient;
agentName: string;
workspaceDir: string;
/** Aggregated code file paths (relative to pipeline repo root) across all juniors */
producedFiles: string[];
}
/**
@@ -49,7 +51,13 @@ export async function executeInvocation(
const workspaceDir = join(WORKSPACE_ROOT, req.pipelineId, req.stage);
await mkdir(workspaceDir, { recursive: true });
const ctx: RunContext = { req, rails, agentName, workspaceDir };
const ctx: RunContext = {
req,
rails,
agentName,
workspaceDir,
producedFiles: [],
};
// 1) Manager itself runs first (it is the single root). Its output is the
// strategic decision that feeds into children.
@@ -137,7 +145,13 @@ export async function executeInvocation(
.join("\n\n---\n\n")
.slice(0, 6000);
return buildSuccessResult(req.stage, req.task, aggregated, gitResult);
return buildSuccessResult(
req.stage,
req.task,
aggregated,
gitResult,
ctx.producedFiles,
);
} catch (err) {
const errorReason = err instanceof Error ? err.message : String(err);
await rails.recordEvent(managerId, "failed", { errorReason });
@@ -265,6 +279,12 @@ async function maybeExtractFiles(
if (blocks.length === 0) return [];
const saved = await saveExtractedFiles(ctx.workspaceDir, blocks);
// Track the repo-relative path (e.g., implement/files/frontend/index.html)
// so the deploy stage can build an accurate preview URL.
for (const f of saved) {
const repoRelPath = `${ctx.req.stage}/files/${f.path}`;
ctx.producedFiles.push(repoRelPath);
}
return saved.map((f) => {
const base: { path: string; lang: string; absPath?: string } = {
path: f.path,
@@ -389,12 +409,19 @@ function derivePreviewUrl(
const rawBase = rawBaseMatch?.[1];
if (!rawBase) return "";
// Heuristic: if there's an index.html path referenced anywhere in the
// priorStages text, link directly to it. Otherwise link to the repo root.
const htmlMatch = impl.text.match(/([a-zA-Z0-9_\-./]+\.html)/);
if (htmlMatch?.[1]) {
return `${rawBase}/files/${htmlMatch[1]}`;
// Prefer the exact producedFiles list emitted by implement stage.
const producedMatch = impl.text.match(/producedFiles=([^\n]+)/);
if (producedMatch?.[1]) {
const files = producedMatch[1]
.split(",")
.map((s) => s.trim())
.filter(Boolean);
const html = files.find((f) => f.toLowerCase().endsWith(".html"));
if (html) return `${rawBase}/${html}`;
if (files[0]) return `${rawBase}/${files[0]}`;
}
// Fallback: browse view
return rawBase.replace("/raw/branch/main", "");
}
@@ -409,6 +436,7 @@ function buildSuccessResult(
commit: string;
filesCount: number;
} | null,
producedFiles: string[] = [],
): HandoffMessage {
const summary = outputText?.slice(0, 2000) ?? "";
switch (stage) {
@@ -433,6 +461,7 @@ function buildSuccessResult(
workdir: task.workdir || "",
selfTestReport: {
summary,
producedFiles,
...(gitResult?.ok && {
repoUrl: gitResult.repoUrl,
rawUrlBase: gitResult.rawUrlBase,

View File

@@ -204,6 +204,7 @@ function extractStageText(h: HandoffMessage): string {
repoUrl?: string;
rawUrlBase?: string;
filesCount?: number;
producedFiles?: string[];
}
| undefined;
const parts: string[] = [];
@@ -213,6 +214,9 @@ function extractStageText(h: HandoffMessage): string {
if (typeof report?.filesCount === "number") {
parts.push(`[git] filesCount=${report.filesCount}`);
}
if (report?.producedFiles && report.producedFiles.length > 0) {
parts.push(`[git] producedFiles=${report.producedFiles.join(",")}`);
}
return parts.join("\n");
}
case "review": {