Repository navigation
fix(deploy): finish an interrupted deploy whose classes landed with a pubspec-only push #115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,8 +31,10 @@ import { | |
| import { applyFlutterFlowHeader } from "./src/flutterFlowHeader.js"; | ||
| import { buildBundleDeployPlan } from "./src/bundleDeployPlanner.js"; | ||
| import { | ||
| buildFinishPushFileMap, | ||
| excludeProvisionedCodeFiles, | ||
| findMissingCodeFiles, | ||
| isDeployFinishable, | ||
| partitionProvisionedCodeFiles, | ||
| } from "./src/flutterFlowCodeFileProvisioning.js"; | ||
| import { buildReviewPresentation } from "./src/reviewPresentation.js"; | ||
|
|
@@ -3219,7 +3221,8 @@ async function reconcileUnconfirmedProvision(apiClient, attempted, original) { | |
| message = | ||
| `Checked FlutterFlow just now: all ${landed.length} custom class(es) are in the project (${names(landed)}). ` + | ||
| "The final step, pushing your pubspec.yaml dependencies, did not run. " + | ||
| "Deploy again to finish it - classes already there are skipped, not duplicated."; | ||
| "Finish deploy to push the dependencies without re-writing the classes, " + | ||
| "or deploy again - classes already there are skipped, not duplicated."; | ||
| } else if (landed.length === 0) { | ||
| message = | ||
| `Checked FlutterFlow just now: none of the ${notLanded.length} custom class(es) are in the project yet (${names(notLanded)}). ` + | ||
|
|
@@ -3231,7 +3234,11 @@ async function reconcileUnconfirmedProvision(apiClient, attempted, original) { | |
| `not there yet: ${names(notLanded)}. Wait a minute, then deploy again - ` + | ||
| "classes already there are skipped, and the rest are written."; | ||
| } | ||
| return new UnconfirmedDeployError(message); | ||
| const error = new UnconfirmedDeployError(message); | ||
| // The reconcile outcome decides whether the terminal can offer "Finish | ||
| // deploy": when every class landed, only the pubspec push remains. | ||
| error.reconcilePartition = { landed, notLanded }; | ||
| return error; | ||
| } | ||
|
|
||
| async function provisionMissingCodeFiles( | ||
|
|
@@ -3422,11 +3429,28 @@ async function provisionMissingCodeFiles( | |
| }, | ||
| () => {}, | ||
| ); | ||
| throw await reconcileUnconfirmedProvision( | ||
| const reconciled = await reconcileUnconfirmedProvision( | ||
| apiClient, | ||
| missingCodeFiles, | ||
| error, | ||
| ); | ||
| // When every attempted class landed, offer "Finish deploy": completing | ||
| // the deploy only needs step 3 (the pubspec push). Carry what that push | ||
| // needs — the deploy-time credentials, the full file map, the already | ||
| // merged yaml, and the landed entry list — so the terminal action can | ||
| // push without re-provisioning the classes. | ||
| if (isDeployFinishable(reconciled.reconcilePartition)) { | ||
| reconciled.finishDeploy = { | ||
| apiKey: apiClient.apiKey, | ||
| projectId: apiClient.projectId, | ||
| endpoint: apiClient.baseUrl, | ||
| branchName: apiClient.branchName, | ||
| fileMap, | ||
| serializedYaml: pubspecYaml, | ||
| landed: reconciled.reconcilePartition.landed, | ||
| }; | ||
| } | ||
| throw reconciled; | ||
| } | ||
| throw error; | ||
| } | ||
|
|
@@ -4542,6 +4566,9 @@ async function executeCommit(code, options = {}) { | |
| targetIdentity, | ||
| state: commitState.currentState, | ||
| elapsedTime: commitState.getElapsedTime(), | ||
| ...(error.finishDeploy | ||
| ? { finishDeploy: mergeFinishDeployContext(error.finishDeploy, targetIdentity, commitState) } | ||
| : {}), | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -4749,6 +4776,9 @@ async function executeBundleCommit(bundlePlan, options = {}) { | |
| targetIdentity, | ||
| state: commitState.currentState, | ||
| elapsedTime: commitState.getElapsedTime(), | ||
| ...(error.finishDeploy | ||
| ? { finishDeploy: mergeFinishDeployContext(error.finishDeploy, targetIdentity, commitState) } | ||
| : {}), | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -7804,17 +7834,147 @@ function showCommitPartialModal(result) { | |
| * in-flight write is never cancelled. | ||
| */ | ||
| function showCommitUnconfirmedModal(result) { | ||
| let heading = "Deploy outcome not yet known"; | ||
| let title = | ||
| "This browser stopped waiting before the FlutterFlow server reported a result."; | ||
| let guidance = | ||
| "The deploy may still be finishing on the server — it was <strong>not cancelled</strong>. " + | ||
| "Open your FlutterFlow project and confirm whether the class landed before retrying, so you do not " + | ||
| "push a duplicate or build on top of an unknown state."; | ||
|
|
||
| // When a reconcile proved every class this deploy was writing landed, only | ||
| // the pubspec push is missing. Offer it here instead of a blind full | ||
| // re-deploy, which would re-run the slower class write for nothing. | ||
| if (result.finishDeploy) { | ||
| pendingFinishDeploy = result.finishDeploy; | ||
| heading = "Deploy almost complete"; | ||
| title = | ||
| "Your custom class(es) are in FlutterFlow — only the dependency push remains."; | ||
| guidance = | ||
| "Every class this deploy was writing is already in your project. The one step that did not run " + | ||
| "was pushing your pubspec.yaml dependencies, which the project needs to build. " + | ||
| "<button id=\"terminal-finish-deploy\" onclick=\"window.finishProvisionedDeploy()\" " + | ||
| "class=\"mt-3 px-4 py-2 text-sm font-medium text-white bg-amber-500 hover:bg-amber-600 rounded-lg transition-colors\">" + | ||
| "Finish deploy</button>"; | ||
| } | ||
|
|
||
| populateCommitTerminalModal(result, { | ||
| heading: "Deploy outcome not yet known", | ||
| title: | ||
| "This browser stopped waiting before the FlutterFlow server reported a result.", | ||
| guidance: | ||
| "The deploy may still be finishing on the server — it was <strong>not cancelled</strong>. " + | ||
| "Open your FlutterFlow project and confirm whether the class landed before retrying, so you do not " + | ||
| "push a duplicate or build on top of an unknown state.", | ||
| heading, | ||
| title, | ||
| guidance, | ||
| }); | ||
| } | ||
|
|
||
| // The "Finish deploy" action completes an interrupted deploy whose classes | ||
| // already landed. Holds the push context (credentials, file map, merged yaml, | ||
| // landed entries) plus the terminal template so the push renders like any | ||
| // deploy. Set by showCommitUnconfirmedModal, read by finishProvisionedDeploy. | ||
| let pendingFinishDeploy = null; | ||
|
|
||
| /** | ||
| * Merges the reconcile's finish-push context — built in | ||
| * `provisionMissingCodeFiles` where the api client, file map, and merged yaml | ||
| * are in scope — with the current deploy's terminal identity, so a finished | ||
| * deploy renders the same truthful terminal a normal deploy would. | ||
| * @param {Object} context - The finishDeploy push context on the reconcile error | ||
| * @param {Object} targetIdentity - The interrupted deploy's target identity | ||
| * @param {Object} commitState - Holds the elapsed-time for the terminal render | ||
| * @returns {Object} The merged finishDeploy payload | ||
| */ | ||
| function mergeFinishDeployContext(context, targetIdentity, commitState) { | ||
| return { | ||
| ...context, | ||
| result: { | ||
| targetIdentity, | ||
| metadata: { | ||
| artifactType: targetIdentity?.artifactType, | ||
| artifactName: targetIdentity?.artifactName, | ||
| fileName: targetIdentity?.fileName, | ||
| projectId: targetIdentity?.projectId, | ||
| }, | ||
| message: | ||
| `Deploy finished: ${targetIdentity?.artifactName || "custom classes"} and ` + | ||
| "their dependencies are now in FlutterFlow.", | ||
| addedDependencies: [], | ||
| unverified: [], | ||
| approximate: [], | ||
| warnings: [], | ||
| elapsedTime: commitState.getElapsedTime(), | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Completes an interrupted deploy whose classes already landed by running only | ||
| * step 3 — the pubspec push. The landed classes are excluded from the sync map | ||
| * (they are already in the project), so this never re-provisions them, and the | ||
| * push reuses the same file-map/zip/push machinery as a normal deploy. | ||
| */ | ||
| async function finishProvisionedDeploy() { | ||
| const finish = pendingFinishDeploy; | ||
| if (!finish) return; | ||
| if (deployInFlight) return; | ||
| pendingFinishDeploy = null; | ||
|
|
||
| const apiClient = new FlutterFlowApiClient( | ||
| finish.apiKey, | ||
| finish.projectId, | ||
| finish.branchName, | ||
| finish.endpoint, | ||
| ); | ||
|
|
||
| closeCommitTerminalModal(); | ||
| setDeployBusy(true); | ||
| showCommitProgress(); | ||
|
|
||
| try { | ||
| commitProgress.set("package"); | ||
| const finishFileMap = buildFinishPushFileMap( | ||
| finish.fileMap, | ||
| finish.landed, | ||
| finish.serializedYaml, | ||
| ); | ||
| const syncMetadata = await buildApiSyncMetadata(finishFileMap, new Map()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Existing bundle artifacts lose update metadata For bundles with existing actions, widgets, or functions, Learn moreThe finish map removes only classes that the runner attempted to create. Other bundle entries can already exist in FlutterFlow. buildFlutterFlowSyncMetadata consults remote files to attach Example: A bundle has a new Recommended fix: Fetch fresh remote files at finish time and pass them to Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| const zippedCustomCode = await createZipFromFileMap(finishFileMap); | ||
|
|
||
| const pushRequest = { | ||
| project_id: finish.projectId, | ||
| zipped_custom_code: zippedCustomCode, | ||
| uid: `web_${Date.now()}`, | ||
| branch_name: apiClient.branchName, | ||
| serialized_yaml: finish.serializedYaml, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Delayed finish overwrites newer project dependencies If dependencies change after reconciliation, Learn moreThe normal deploy merges generated dependencies with the current project manifest in resolveProjectPubspec. The finish action instead retains that earlier merged manifest and sends it as the complete Example: Deploy A prepares a manifest containing Recommended fix: Before constructing the finish request, fetch the current project manifest and merge only the interrupted deploy's required dependencies into it. Also reconcile remaining bundle artifacts against the fresh project export before writing them. Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| file_map: syncMetadata.fileMapContents, | ||
| functions_map: syncMetadata.functionsMapContents, | ||
| }; | ||
|
|
||
| invalidateProjectSourceCache(apiClient); | ||
| commitProgress.set("push"); | ||
| const result = await pushCodeWithUiTimeout(apiClient, pushRequest); | ||
|
|
||
| if (result.success) { | ||
| renderCommitTerminal({ | ||
| ...finish.result, | ||
| success: true, | ||
| warnings: result.errorMap ? Array.from(result.errorMap.entries()) : [], | ||
| }); | ||
| return; | ||
| } | ||
| const error = new Error( | ||
| result.errorMessage || getFlutterFlowErrorMessage(result.responseCode), | ||
| ); | ||
| error.remoteRefusal = true; | ||
| throw error; | ||
| } catch (error) { | ||
| renderCommitTerminal({ | ||
| ...finish.result, | ||
| success: false, | ||
| error: error.message, | ||
| unconfirmed: error instanceof UnconfirmedDeployError, | ||
| remoteRefusal: error.remoteRefusal === true, | ||
| }); | ||
|
Comment on lines
+7967
to
+7974
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Closes the shared terminal modal (partial / unconfirmed outcomes). | ||
| * @param {Event} [event] - Optional click event | ||
|
|
@@ -8229,6 +8389,7 @@ window.openCommitConfirmModal = openCommitConfirmModal; | |
| window.closeCommitConfirmModal = closeCommitConfirmModal; | ||
| window.closeCommitSuccessModal = closeCommitSuccessModal | ||
| window.closeCommitTerminalModal = closeCommitTerminalModal | ||
| window.finishProvisionedDeploy = finishProvisionedDeploy | ||
| window.showCommitPartialModal = showCommitPartialModal | ||
| window.showCommitUnconfirmedModal = showCommitUnconfirmedModal | ||
| window.showCommitSuccessModal = showCommitSuccessModal | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Failed reconciliation falsely marks deploy failed
When the project re-read fails,
isDeployFinishablereceives no partition and throws. The original unconfirmed outcome becomes a failed deploy, although classes can still land.Was this helpful? React with 👍 or 👎 to provide feedback.