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
60 changes: 60 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ import {
partitionProvisionedCodeFiles,
} from "./src/flutterFlowCodeFileProvisioning.js";
import { buildReviewPresentation } from "./src/reviewPresentation.js";
import {
generateDeployRunId,
queryRunStatusWithinBudget,
} from "./src/runStatus.js";
import {
expectedWidgetClassFromFileName,
findUnbalancedBracketError,
Expand Down Expand Up @@ -3254,6 +3258,15 @@ async function provisionMissingCodeFiles(
return { remoteFiles, syncFileMap: fileMap, unverified: [], approximate: [] };
}

// The client generates the run id so that, if this response is ever lost,
// a 404 on GET /runStatus/<runId> is proof the request never reached the
// runner (nothing written, retry safe) rather than an ambiguous "unknown".
const runId = generateDeployRunId();

// True once the run-status lookup has decided this attempt's real outcome,
// so the late-arriving provision stream cannot double-credit its writes.
let runStatusResolved = false;

// The runner compiles these against the project's own package versions
// before it writes anything, so an API the generated code invented - a named
// argument the package never declared - stops the deploy instead of landing
Expand Down Expand Up @@ -3310,6 +3323,7 @@ async function provisionMissingCodeFiles(
projectId: apiClient.projectId,
baseUrl: apiClient.baseUrl,
commitMessage,
runId,
customClasses: missingCodeFiles,
verification,
stream: true,
Expand Down Expand Up @@ -3413,6 +3427,9 @@ async function provisionMissingCodeFiles(
// retires them outright.
provisionWork?.then(
(late) => {
// When the run-status lookup already decided this attempt's real
// outcome, the late stream must not double-credit its writes.
if (runStatusResolved) return;
if (late?.finalResultReceived && late.success) {
adjustProjectSourceDirty(apiClient, missingCodeFiles, {
uncertain: -1,
Expand All @@ -3429,6 +3446,49 @@ async function provisionMissingCodeFiles(
},
() => {},
);
// The record is the authority here: ask the runner what actually
// happened instead of re-reading the project and guessing. Only when the
// answer is definitive does it replace the estimate; a 404, a 5xx or an
// unrecognised body (an older runner) falls through to the reconcile.
let statusOutcome = { definitive: null };
try {
statusOutcome = await queryRunStatusWithinBudget({
provisionEndpoint: FLUTTERFLOW_CLASS_PROVISION_ENDPOINT,
runId,
apiKey: apiClient.apiKey,
});
} catch {
// The status query must fail closed: any unexpected throw from it (a
// browser abort surfacing unusually, a half-updated bundle) falls back
// to the existing reconcile instead of fabricating a new failure.
statusOutcome = { definitive: null };
}
if (statusOutcome.definitive === "success") {
// The runner confirms every class it was asked to write landed, so the
// provision succeeds and step 3 (the pubspec push) runs as usual — this
// is a known success, not an estimate.
runStatusResolved = true;
adjustProjectSourceDirty(apiClient, missingCodeFiles, {
uncertain: -1,
verified: 1,
});
return {
remoteFiles,
syncFileMap: excludeProvisionedCodeFiles(fileMap, missingCodeFiles),
unverified,
approximate,
provisionSucceeded: true,
};
Comment on lines +3475 to +3481

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Recovered deploys lose compile-check warnings

When verification fails to run and the provision response is lost, statusOutcome reports success without the runner's verificationSkipped warning. The deploy finishes without telling the user their classes were not compiled.

Learn more

The runner returns verificationSkipped when the compile check cannot run. The normal provisioning result appends that warning to unverified before returning it. The recovered-success branch bypasses the result and returns only the pre-request warnings, while recordDone does not persist the skip reason.

Example: Flutter package resolution fails, then GaugeWidget deploys successfully but its stream drops. A done status recovers the deploy, yet the user receives no warning that GaugeWidget was never compiled.

Recommended fix: Persist the runner's verification outcome in its run record and return it through the status endpoint. Add any recovered skip reason to unverified just as the normal result path does.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

}
if (statusOutcome.definitive === "failed") {
// A provable pre-write refusal: nothing could have been written, so it
// is a real failure, not an unknown outcome.
runStatusResolved = true;
throw new Error(
statusOutcome.error || "The deploy failed before writing anything.",
);
Comment on lines +3483 to +3489

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Pre-write failures leave project exports uncached

When a lost response resolves to failed, its attempted classes remain marked dirty despite never being written. resolveProjectPubspec cannot clear absent classes, so later deploys repeatedly export the project.

Learn more

The unconfirmed-error handler calls markProjectSourceDirty before querying status. The failed status is definitive only when the runner stopped before any class write, so that attempt cannot create a later file. The new failure branch leaves its uncertain credits in projectSourceDirtyWrites. resolveProjectPubspec removes uncertain credits only after observing matching content, so a file that was never written keeps project exports uncached indefinitely.

Example: A compile check rejects GaugeWidget, but its stream disconnects. Status confirms a pre-write failure. Future deploys see no GaugeWidget in FlutterFlow and fetch a new full export every time, instead of reusing the cached project source.

Recommended fix: On a definitive pre-write failure, decrement that attempt's uncertain credits with adjustProjectSourceDirty before throwing. Preserve credits from other concurrent attempts at the same path and content.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

}

const reconciled = await reconcileUnconfirmedProvision(
apiClient,
missingCodeFiles,
Expand Down
Loading
Loading