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
13 changes: 7 additions & 6 deletions src/commands/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,33 +100,34 @@ export async function runLoop(cwd: string, args: string[] = []): Promise<void> {
const parsed = parseLoopArgs(args);
let priorFinalBody = "";
let succeeded = false;
let loopWorkspace = cwd;
const previousNoInspect = process.env.CSTACK_NO_POSTRUN_INSPECT;
const previousAutomatedLoop = process.env.CSTACK_AUTOMATED_LOOP;
process.env.CSTACK_NO_POSTRUN_INSPECT = "1";
process.env.CSTACK_AUTOMATED_LOOP = "1";

try {
if (parsed.options.repo) {
loopWorkspace = await cloneIterationRepo(parsed.options.repo, parsed.options.branch, 1);
}
for (let iteration = 1; iteration <= parsed.options.iterations; iteration += 1) {
const iterationCwd = parsed.options.repo
? await cloneIterationRepo(parsed.options.repo, parsed.options.branch, iteration)
: cwd;
const intent =
iteration === 1 || !priorFinalBody ? parsed.intent : buildRetryIntent(parsed.intent, priorFinalBody);

process.stdout.write(
[
`Loop iteration: ${iteration}/${parsed.options.iterations}`,
`Workspace: ${iterationCwd}`,
`Workspace: ${loopWorkspace}`,
`Intent: ${intent.split("\n")[0]}`
].join("\n") + "\n"
);

const runId = await runIntent(iterationCwd, intent, {
const runId = await runIntent(loopWorkspace, intent, {
dryRun: false,
entrypoint: "run",
...(parsed.options.safe ? { safe: true } : {})
});
const run = await readRun(iterationCwd, runId);
const run = await readRun(loopWorkspace, runId);
priorFinalBody = await fs.readFile(run.finalPath, "utf8").catch(() => "");

process.stdout.write(
Expand Down
250 changes: 243 additions & 7 deletions src/deliver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { runDeliverValidationExecution, type DeliverValidationExecutionResult }
import { WorkflowController } from "./workflow-machine.js";
import type {
CstackConfig,
DeliverGitHubConfig,
DeliveryReadinessPolicyRecord,
DeploymentEvidenceRecord,
DeliverTargetMode,
Expand Down Expand Up @@ -219,6 +220,196 @@ function mergeUniqueLines(values: string[]): string[] {
return [...new Set(values.filter(Boolean))];
}

function createEmptyGitHubMutationRecord(initialBranch = ""): GitHubMutationRecord {
return {
enabled: false,
branch: {
initial: initialBranch,
current: initialBranch,
created: false,
pushed: false,
remote: null
},
commit: {
created: false,
changedFiles: []
},
pullRequest: {
created: false,
updated: false
},
checks: {
watched: false,
polls: 0,
completed: false,
summary: ""
},
release: {
requested: false,
created: false,
pushed: false,
uploadedFiles: [],
summary: ""
},
blockers: [],
summary: ""
};
}

function buildStageSyncPolicy(policy: DeliverGitHubConfig): DeliverGitHubConfig {
return {
...policy,
createRelease: false,
releasePushTag: false,
releaseFiles: [],
watchChecks: false
};
}

function createStageSyncReviewVerdict(stageName: "build" | "validation"): DeliverReviewVerdict {
return {
mode: "readiness",
status: "completed",
summary:
stageName === "build"
? "Stage sync after build: repository changes were prepared for review."
: "Stage sync after validation: repository changes were prepared for review.",
findings: [],
recommendedActions: [],
acceptedSpecialists: [],
reportMarkdown:
stageName === "build"
? "# Review Findings\n\nStage sync after build. Formal review has not run yet.\n"
: "# Review Findings\n\nStage sync after validation. Formal review has not run yet.\n"
};
}

function mergeGitHubMutationRecords(current: GitHubMutationRecord, next: GitHubMutationRecord): GitHubMutationRecord {
const currentRelease = current.release;
const nextRelease = next.release;
const mergedCommit: GitHubMutationRecord["commit"] = {
created: current.commit.created || next.commit.created,
changedFiles: mergeUniqueLines([...current.commit.changedFiles, ...next.commit.changedFiles])
};
const mergedCommitSha = next.commit.sha ?? current.commit.sha;
if (mergedCommitSha) {
mergedCommit.sha = mergedCommitSha;
}
const mergedCommitMessage = next.commit.message ?? current.commit.message;
if (mergedCommitMessage) {
mergedCommit.message = mergedCommitMessage;
}

const mergedPullRequest: GitHubMutationRecord["pullRequest"] = {
created: current.pullRequest.created || next.pullRequest.created,
updated: current.pullRequest.updated || next.pullRequest.updated
};
const mergedPullRequestNumber = next.pullRequest.number ?? current.pullRequest.number;
if (mergedPullRequestNumber !== undefined) {
mergedPullRequest.number = mergedPullRequestNumber;
}
const mergedPullRequestUrl = next.pullRequest.url ?? current.pullRequest.url;
if (mergedPullRequestUrl) {
mergedPullRequest.url = mergedPullRequestUrl;
}
const mergedPullRequestTitle = next.pullRequest.title ?? current.pullRequest.title;
if (mergedPullRequestTitle) {
mergedPullRequest.title = mergedPullRequestTitle;
}
const mergedBaseRefName = next.pullRequest.baseRefName ?? current.pullRequest.baseRefName;
if (mergedBaseRefName) {
mergedPullRequest.baseRefName = mergedBaseRefName;
}
const mergedHeadRefName = next.pullRequest.headRefName ?? current.pullRequest.headRefName;
if (mergedHeadRefName) {
mergedPullRequest.headRefName = mergedHeadRefName;
}
const mergedDraft = next.pullRequest.draft ?? current.pullRequest.draft;
if (mergedDraft !== undefined) {
mergedPullRequest.draft = mergedDraft;
}

let mergedRelease: GitHubMutationRecord["release"] | undefined;
if (currentRelease || nextRelease) {
mergedRelease = {
requested: Boolean(currentRelease?.requested || nextRelease?.requested),
created: Boolean(currentRelease?.created || nextRelease?.created),
pushed: Boolean(currentRelease?.pushed || nextRelease?.pushed),
uploadedFiles: mergeUniqueLines([...(currentRelease?.uploadedFiles ?? []), ...(nextRelease?.uploadedFiles ?? [])]),
summary: nextRelease?.summary || currentRelease?.summary || ""
};
const mergedTagName = nextRelease?.tagName ?? currentRelease?.tagName;
if (mergedTagName) {
mergedRelease.tagName = mergedTagName;
}
const mergedVersion = nextRelease?.version ?? currentRelease?.version;
if (mergedVersion !== undefined) {
mergedRelease.version = mergedVersion;
}
const mergedReleaseUrl = nextRelease?.url ?? currentRelease?.url;
if (mergedReleaseUrl) {
mergedRelease.url = mergedReleaseUrl;
}
const mergedReleaseName = nextRelease?.name ?? currentRelease?.name;
if (mergedReleaseName !== undefined) {
mergedRelease.name = mergedReleaseName;
}
}

return {
enabled: current.enabled || next.enabled,
branch: {
initial: next.branch.initial || current.branch.initial,
current: next.branch.current || current.branch.current,
created: current.branch.created || next.branch.created,
pushed: current.branch.pushed || next.branch.pushed,
remote: next.branch.remote ?? current.branch.remote ?? null
},
commit: mergedCommit,
pullRequest: mergedPullRequest,
checks: {
watched: current.checks.watched || next.checks.watched,
polls: Math.max(current.checks.polls, next.checks.polls),
completed: current.checks.completed || next.checks.completed,
summary: next.checks.summary || current.checks.summary
},
...(mergedRelease ? { release: mergedRelease } : {}),
blockers: mergeUniqueLines([...current.blockers, ...next.blockers]),
summary: next.summary || current.summary
};
}

async function syncStageChangesToGitHub(options: {
cwd: string;
gitBranch: string;
runId: string;
input: string;
issueNumbers: number[];
policy: DeliverGitHubConfig;
buildSummary: string;
verificationRecord: object;
stageName: "build" | "validation";
pullRequestBodyPath: string;
linkedRunId?: string;
}): Promise<PerformGitHubMutationResult | null> {
if (!options.policy.enabled) {
return null;
}
return performGitHubDeliverMutations({
cwd: options.cwd,
gitBranch: options.gitBranch,
runId: `${options.runId}-${options.stageName}`,
input: options.input,
issueNumbers: options.issueNumbers,
policy: buildStageSyncPolicy(options.policy),
buildSummary: options.buildSummary,
reviewVerdict: createStageSyncReviewVerdict(options.stageName),
verificationRecord: options.verificationRecord,
...(options.linkedRunId ? { linkedRunId: options.linkedRunId } : {}),
pullRequestBodyPath: options.pullRequestBodyPath
});
}

function summarizeBuildFailure(buildExecution: BuildExecutionResult): string {
if (buildExecution.failureDiagnosis?.summary) {
return buildExecution.failureDiagnosis.summary;
Expand Down Expand Up @@ -896,6 +1087,9 @@ function createBlockedReadinessPolicyRecord(options: {

export async function runDeliverExecution(options: DeliverExecutionOptions): Promise<DeliverExecutionResult> {
const selectedSpecialists = selectDeliverSpecialists(options.input);
const githubPolicy = options.config.workflows.deliver.github ?? {};
let currentGitBranch = options.gitBranch;
let cumulativeGitHubMutation = createEmptyGitHubMutationRecord(options.gitBranch);
let stageLineage = options.controller.currentStageLineage;
const { prompt, context } = await buildDeliverPrompt({
cwd: options.cwd,
Expand Down Expand Up @@ -1188,6 +1382,44 @@ export async function runDeliverExecution(options: DeliverExecutionOptions): Pro
validationStatus === "completed" ? "completed" : validationStatus === "deferred" ? "deferred" : "failed"
);
await writeValidationArtifacts({ validationStageDir, validationExecution });
if (buildExecution.result.code === 0) {
const buildStageSyncResult = await syncStageChangesToGitHub({
cwd: options.cwd,
gitBranch: currentGitBranch,
runId: options.runId,
input: options.input,
issueNumbers: options.issueNumbers,
policy: githubPolicy,
buildSummary: buildExecution.finalBody,
verificationRecord: buildExecution.verificationRecord,
stageName: "build",
...(options.linkedContext?.run.id ? { linkedRunId: options.linkedContext.run.id } : {}),
pullRequestBodyPath: path.join(buildStageDir, "artifacts", "pull-request-body.md")
});
if (buildStageSyncResult?.record.branch.current) {
currentGitBranch = buildStageSyncResult.record.branch.current;
cumulativeGitHubMutation = mergeGitHubMutationRecords(cumulativeGitHubMutation, buildStageSyncResult.record);
}
}
if (validationExecution.validationPlan.status !== "blocked") {
const validationStageSyncResult = await syncStageChangesToGitHub({
cwd: options.cwd,
gitBranch: currentGitBranch,
runId: options.runId,
input: options.input,
issueNumbers: options.issueNumbers,
policy: githubPolicy,
buildSummary: buildExecution.finalBody,
verificationRecord: buildExecution.verificationRecord,
stageName: "validation",
...(options.linkedContext?.run.id ? { linkedRunId: options.linkedContext.run.id } : {}),
pullRequestBodyPath: path.join(validationStageDir, "artifacts", "pull-request-body.md")
});
if (validationStageSyncResult?.record.branch.current) {
currentGitBranch = validationStageSyncResult.record.branch.current;
cumulativeGitHubMutation = mergeGitHubMutationRecords(cumulativeGitHubMutation, validationStageSyncResult.record);
}
}
await events.emit("activity", "Validation stage finished, starting review synthesis");
await options.controller.send({
type: "SET_STAGE_STATUS",
Expand Down Expand Up @@ -1310,13 +1542,13 @@ export async function runDeliverExecution(options: DeliverExecutionOptions): Pro
});
events.markStage("ship", "running");

let githubMutation = createBlockedGitHubMutationRecord();
let githubMutation = cumulativeGitHubMutation;
let githubMutationResult: PerformGitHubMutationResult = {
branch: options.gitBranch,
branch: currentGitBranch,
record: githubMutation
};
let githubDeliveryRecord = createBlockedGitHubDeliveryRecord({
gitBranch: options.gitBranch,
gitBranch: currentGitBranch,
deliveryMode: options.deliveryMode,
issueNumbers: options.issueNumbers,
config: options.config,
Expand All @@ -1337,24 +1569,28 @@ export async function runDeliverExecution(options: DeliverExecutionOptions): Pro
try {
githubMutationResult = await performGitHubDeliverMutations({
cwd: options.cwd,
gitBranch: options.gitBranch,
gitBranch: currentGitBranch,
runId: options.runId,
input: options.input,
issueNumbers: options.issueNumbers,
policy: options.config.workflows.deliver.github ?? {},
policy: githubPolicy,
buildSummary: buildExecution.finalBody,
reviewVerdict,
verificationRecord: buildExecution.verificationRecord,
...(options.linkedContext?.run.id ? { linkedRunId: options.linkedContext.run.id } : {}),
pullRequestBodyPath: path.join(shipStageDir, "artifacts", "pull-request-body.md")
});
githubMutationResult = {
...githubMutationResult,
record: mergeGitHubMutationRecords(cumulativeGitHubMutation, githubMutationResult.record)
};

const githubEvidence = await collectGitHubDeliveryEvidence({
cwd: options.cwd,
gitBranch: githubMutationResult.record.branch.current,
deliveryMode: options.deliveryMode,
issueNumbers: options.issueNumbers,
policy: options.config.workflows.deliver.github ?? {},
policy: githubPolicy,
input: options.input,
mutationRecord: githubMutationResult.record,
...(options.linkedContext?.artifactBody ? { linkedArtifactBody: options.linkedContext.artifactBody } : {})
Expand Down Expand Up @@ -1489,7 +1725,7 @@ export async function runDeliverExecution(options: DeliverExecutionOptions): Pro
record: githubMutation
};
githubDeliveryRecord = createBlockedGitHubDeliveryRecord({
gitBranch: options.gitBranch,
gitBranch: currentGitBranch,
deliveryMode: options.deliveryMode,
issueNumbers: options.issueNumbers,
config: options.config,
Expand Down
Loading
Loading