Skip to content

feat(builds): Pull + Commit + Push Builds #6498

Open
Aradhya-Tripathi wants to merge 11 commits into
developfrom
quick-builds
Open

feat(builds): Pull + Commit + Push Builds #6498
Aradhya-Tripathi wants to merge 11 commits into
developfrom
quick-builds

Conversation

@Aradhya-Tripathi
Copy link
Copy Markdown
Contributor

No description provided.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 24, 2026

Greptile Summary

This PR introduces "patch builds" — a fast deploy path that pulls updated app commits into an existing Docker image, commits the container, and pushes the new tag instead of running a full Dockerfile rebuild. A new can_run_patch_build eligibility check is exposed through deploy_information, and the "Deploy as Patch" button routes through a new trigger_patch_deploy code path in both the old and new deploy flows.

  • New patch build lifecycle: DeployCandidateBuild.run_patch_build / send_patch_build_instructions sends pull+commit+push instructions to the agent; _process_patch_build_job handles callbacks, syncs step statuses, and fans out to a second-platform build if needed.
  • Eligibility guard: can_run_patch_build validates that app list, sources, dependencies, packages, and env vars are unchanged vs. the last active bench's candidate before enabling the button.
  • Suspension error not checked: trigger_patch_deploy() returns {\"error\": True, \"message\": \"...\"} when builds are suspended, but both bench_update.deploy() and release_pipeline.initiate_patch_deploy() access [\"name\"] on the result unconditionally, raising a KeyError.

Confidence Score: 3/5

Safe to merge once the suspension-error handling is fixed in bench_update.deploy() and initiate_patch_deploy; those two spots will throw a KeyError on any patch deploy attempt while builds are suspended.

Both bench_update.deploy() and release_pipeline.initiate_patch_deploy() call trigger_patch_deploy() and immediately subscript ["name"] on the result without checking the error key. When builds are suspended, trigger_patch_deploy() returns {error: True, message: ...} with no name key, so both callers raise an unhandled KeyError. In the old deploy flow this surfaces as a 500 to the user; in the new flow it produces an opaque task failure. All other patch-build logic looks correct.

press/press/doctype/bench_update/bench_update.py (line 122) and press/press/doctype/release_pipeline/release_pipeline.py (line 298) both need error-response handling after calling trigger_patch_deploy().

Important Files Changed

Filename Overview
press/press/doctype/bench_update/bench_update.py Adds trigger_patch_deploy parameter to deploy(); accessing deploy["name"] after trigger_patch_deploy() returns an error dict (no name key) when builds are suspended causes a KeyError. Also, the parameter is a dead letter in the new-deploy-flow path.
press/press/doctype/release_pipeline/release_pipeline.py Adds initiate_patch_deploy task and trigger_patch_deploy threading through prepare_deployment/create_deploy_candidate; initiate_patch_deploy accesses ["name"] on a dict that has no name key when builds are suspended.
dashboard/src/components/group/UpdateReleaseGroupDialog.vue Adds Deploy as Patch button and patchDeploy resource; deployAsPatch() method lacks the restrictMessage guard that updateBench() enforces, allowing bypass of the I understand confirmation UI.
press/press/doctype/deploy_candidate_build/deploy_candidate_build.py Adds patch build lifecycle methods; logic is well-structured with proper exception handling.
press/press/doctype/release_group/release_group.py Adds _get_previous_candidate, _has_active_benches, and can_run_patch_build helpers; checks are comprehensive across apps, sources, deps, packages, and envvars.
press/press/doctype/deploy_candidate/deploy_candidate.py Adds trigger_patch_deploy() whitelisted method; correctly checks is_suspended() but returns an error dict instead of throwing, which callers must handle explicitly.
press/api/bench.py Threads trigger_patch_deploy flag through deploy_and_update into both old and new deploy flows; the old-flow path is where the suspension-check KeyError would surface.

Sequence Diagram

sequenceDiagram
    participant UI as UpdateReleaseGroupDialog
    participant API as press.api.bench
    participant BU as BenchUpdate.deploy()
    participant DC as DeployCandidate
    participant DCB as DeployCandidateBuild
    participant Agent as Agent (builder)

    UI->>API: "deploy_and_update(trigger_patch_deploy=True)"
    API->>BU: "bench_update.deploy(trigger_patch_deploy=True)"
    BU->>DC: rg.create_deploy_candidate()
    DC-->>BU: candidate
    BU->>DC: candidate.trigger_patch_deploy()
    Note over DC: Returns error dict if suspended - deploy[name] raises KeyError
    DC-->>BU: "error=False, name=build_name"
    BU-->>API: build_name
    DCB->>DCB: after_insert - run_patch_build()
    DCB->>DCB: can_run_patch_build() check
    DCB->>DCB: _get_previous_candidate()
    DCB->>Agent: run_patch_build(base_image, app_instructions)
    Agent-->>DCB: job callback process_run_patch_build
    DCB->>DCB: _sync_patch_build_step_statuses()
    DCB->>DCB: patch_build_output_parser.parse_and_update()
    alt SUCCESS
        DCB->>DCB: update_deploy_candidate_with_build()
        DCB->>DCB: _create_platform_patch_build_if_required_and_deploy()
    else FAILURE
        DCB->>DCB: handle_build_failure()
    end
Loading
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
press/press/doctype/bench_update/bench_update.py:116-122
`KeyError` when builds are suspended and a patch deploy is requested. `trigger_patch_deploy()` returns `{"error": True, "message": "..."}` — which has no `"name"` key — when `is_suspended()` is `True`, so `deploy["name"]` on the next line raises an unhandled `KeyError` that bubbles up to the API caller as a 500 error.

```suggestion
		deploy = (
			candidate.schedule_build_and_deploy(ignore_permissions=ignore_permissions)
			if not trigger_patch_deploy
			else candidate.trigger_patch_deploy(ignore_permissions=ignore_permissions)
		)

		if deploy.get("error"):
			frappe.throw(deploy.get("message", "Patch build could not be initiated."))

		return deploy["name"]
```

### Issue 2 of 4
press/press/doctype/release_pipeline/release_pipeline.py:293-298
`trigger_patch_deploy()` returns `{"error": True, "message": "..."}` — with no `"name"` key — when builds are suspended at task execution time. If builds are suspended in the narrow window between task enqueuing and execution, this line raises a `KeyError`, causing an opaque task failure instead of a clear suspension message propagating through the pipeline.

```suggestion
	@task(queue=_get_task_execution_queue())
	def initiate_patch_deploy(self, deploy_candidate: str) -> str:
		"""Start the deploy candidate build process with patch deploy flag, skipping pre-build validations."""
		candidate: DeployCandidate = frappe.get_doc("Deploy Candidate", deploy_candidate)
		deploy_candidate_build = candidate.trigger_patch_deploy(ignore_permissions=True)
		if deploy_candidate_build.get("error"):
			raise ReleasePipelineFailure(
				deploy_candidate_build.get("message", "Patch build could not be initiated.")
			)
		return deploy_candidate_build["name"]
```

### Issue 3 of 4
dashboard/src/components/group/UpdateReleaseGroupDialog.vue:739-741
`deployAsPatch()` skips the same pre-submit checks that `updateBench()` enforces. When `canShowDeploy` is true while `step === 'restrict-build'`, both buttons are visible but only the regular deploy button guards against `restrictMessage && !ignoreWillFailCheck`. A user can click "Deploy as Patch" and bypass the UI confirmation; the backend re-runs the will-fail check but the asymmetry could allow patch deploys to slip through if the check passes on retry.

```suggestion
		deployAsPatch() {
			if (this.restrictMessage && !this.ignoreWillFailCheck) {
				this.errorMessage =
					'Please check the <b>I understand</b> box to proceed'
				return
			}
			this.$resources.patchDeploy.submit()
		},
```

### Issue 4 of 4
press/press/doctype/bench_update/bench_update.py:116-122
**Dead-letter `trigger_patch_deploy` in new-deploy-flow path**

`create_deploy_candidate()` in `release_pipeline.py` calls `bench_update.deploy(create_build=False, trigger_patch_deploy=trigger_patch_deploy)`. Because `create_build=False` causes an early return before the `trigger_patch_deploy` branch is reached, the parameter has no effect here — the pipeline triggers the patch deploy explicitly via `initiate_patch_deploy()` instead. The silently ignored parameter could mislead future readers into thinking the pipeline already handles dispatch here.

Reviews (4): Last reviewed commit: "Merge branch 'develop' into quick-builds" | Re-trigger Greptile

Comment thread press/press/doctype/deploy_candidate_build/deploy_candidate_build.py Outdated
Comment thread press/press/doctype/deploy_candidate_build/deploy_candidate_build.py Outdated
Comment thread press/press/doctype/bench_update/bench_update.py Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented May 24, 2026

Codecov Report

❌ Patch coverage is 37.15847% with 115 lines in your changes missing coverage. Please review.
✅ Project coverage is 50.03%. Comparing base (b16bb87) to head (afb6ee9).
⚠️ Report is 3 commits behind head on develop.

Files with missing lines Patch % Lines
...e/deploy_candidate_build/deploy_candidate_build.py 18.18% 63 Missing ⚠️
.../doctype/deploy_candidate/docker_output_parsers.py 20.00% 28 Missing ⚠️
press/press/doctype/release_group/release_group.py 80.43% 9 Missing ⚠️
...press/doctype/release_pipeline/release_pipeline.py 38.46% 8 Missing ⚠️
...press/doctype/deploy_candidate/deploy_candidate.py 28.57% 5 Missing ⚠️
press/agent.py 50.00% 1 Missing ⚠️
press/press/doctype/agent_job/agent_job.py 50.00% 1 Missing ⚠️

❌ Your patch check has failed because the patch coverage (37.15%) is below the target coverage (75.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #6498      +/-   ##
===========================================
- Coverage    50.05%   50.03%   -0.03%     
===========================================
  Files          954      954              
  Lines        78960    79147     +187     
  Branches       366      369       +3     
===========================================
+ Hits         39527    39603      +76     
- Misses       39408    39518     +110     
- Partials        25       26       +1     
Flag Coverage Δ
dashboard 60.76% <ø> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Aradhya-Tripathi
Copy link
Copy Markdown
Contributor Author

@greptileai rereview

@Aradhya-Tripathi
Copy link
Copy Markdown
Contributor Author

@greptileai rereview

@Aradhya-Tripathi
Copy link
Copy Markdown
Contributor Author

Greptile Summary

This PR introduces a "Patch Build" deploy path that bypasses the full Docker image rebuild by pulling only the changed app commits into the existing running container, committing the result, and pushing a new image tag — significantly reducing deploy time for pure app-code updates.

  • Adds can_run_patch_build eligibility gating (same apps/sources/deps/env-vars, active benches required), a trigger_patch_deploy flag threaded through the API → BenchUpdate.deployDeployCandidate.trigger_patch_deploy, and a new \"Run Patch Build\" agent job type with its fixture entry.
  • Adds _process_patch_build_job and PatchBuildOutputParser to handle the shortened step sequence (pull → commit → push) and sync step statuses back from agent job records.
  • Exposes a "Deploy as Patch" button in the dashboard dialog, visible only when the backend signals eligibility via deploy_information.can_run_patch_build.

Confidence Score: 3/5

The new patch-build path has two logic gaps that can cause silent or unexpected failures in production: one in error handling when a patch build fails inside the release pipeline, and one in the multi-arch secondary build flow.

When a patch build fails on the new deploy flow, _check_for_scheduled_build_retries queries for a Run Remote Builder agent job that will never exist for patch builds, throwing DoesNotExistError instead of the expected ReleasePipelineFailure, leaving the pipeline in an ambiguous state. Separately, the secondary-platform build creation updates the previous-candidate pointer before the second build runs its guard, causing it to fail mid-deploy.

release_pipeline.py (_check_for_scheduled_build_retries) and deploy_candidate_build.py (_create_platform_patch_build_if_required_and_deploy) need the most attention before merge.

Important Files Changed

Sequence Diagram

Prompt To Fix All With AI

Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
press/press/doctype/release_pipeline/release_pipeline.py:323-330
**Hardcoded job type breaks patch-build failure handling**

`_check_for_scheduled_build_retries` fetches an agent job filtering by `"job_type": "Run Remote Builder"`, but patch builds create an agent job of type `"Run Patch Build"`. When a patch build fails, `monitor_build_success` calls this method and `frappe.get_doc` raises `DoesNotExistError` because no `"Run Remote Builder"` job exists for the build. This exception is not a `ReleasePipelineFailure`, so it bypasses the `except ReleasePipelineFailure` block in `create_release`, leaving the pipeline in an undefined error state rather than cleanly transitioning to `"Failure"`.

The job type should be determined from the build record itself (or accepted as a parameter) rather than hardcoded to the regular-build value.

### Issue 2 of 3
press/press/doctype/deploy_candidate_build/deploy_candidate_build.py:1123-1142
**Secondary patch build skips the `can_run_patch_build` guard**

`_create_platform_patch_build_if_required_and_deploy` inserts a new `DeployCandidateBuild` with `run_build=0` and calls `new_dcb.run_patch_build()`. `run_patch_build()` re-checks `can_run_patch_build(self.group)`, but by the time the secondary platform build is triggered the first build has already committed image metadata, so the "previous candidate" returned by `_get_previous_candidate` is now the *new* candidate that was just pushed — not the original baseline. This can cause `can_run_patch_build` to return `False` for the secondary build, throwing `"Patch build cannot be run."` mid-deploy and leaving the first platform's image pushed but the second one never created, resulting in a partial multi-arch deploy with no recovery path.

### Issue 3 of 3
dashboard/src/components/group/UpdateReleaseGroupDialog.vue:679-681
**"Deploy as Patch" is reachable on the `restrict-build` step without acknowledgement**

`canShowDeploy` is `!canShowNext`, and `canShowNext` returns `false` on the `restrict-build` step, so the "Deploy as Patch" button is visible there. Clicking it submits with `run_will_fail_check: !this.ignoreWillFailCheck` — which evaluates to `true` when the checkbox hasn't been ticked — causing the backend to re-run the check and immediately return a `BuildValidationError`, then cycling the user back to the same step. The button should either be hidden on the `restrict-build` step, or `deployAsPatch` should mirror the `updateBench` guard that rejects submission when `this.restrictMessage` is set and `!this.ignoreWillFailCheck`.

Reviews (3): Last reviewed commit: "chore(build): Rename as patch build" | Re-trigger Greptile

The second issue is not correct, since the secondary platform build will not create a new deploy candidate doctype instead will associate the deploy_Candidate_build doctype to the older deploy_candidate doctype

@Aradhya-Tripathi
Copy link
Copy Markdown
Contributor Author

@greptileai rereview

Comment on lines +116 to 122
deploy = (
candidate.schedule_build_and_deploy(ignore_permissions=ignore_permissions)
if not trigger_patch_deploy
else candidate.trigger_patch_deploy(ignore_permissions=ignore_permissions)
)

return deploy["name"]
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.

P1 KeyError when builds are suspended and a patch deploy is requested. trigger_patch_deploy() returns {"error": True, "message": "..."} — which has no "name" key — when is_suspended() is True, so deploy["name"] on the next line raises an unhandled KeyError that bubbles up to the API caller as a 500 error.

Suggested change
deploy = (
candidate.schedule_build_and_deploy(ignore_permissions=ignore_permissions)
if not trigger_patch_deploy
else candidate.trigger_patch_deploy(ignore_permissions=ignore_permissions)
)
return deploy["name"]
deploy = (
candidate.schedule_build_and_deploy(ignore_permissions=ignore_permissions)
if not trigger_patch_deploy
else candidate.trigger_patch_deploy(ignore_permissions=ignore_permissions)
)
if deploy.get("error"):
frappe.throw(deploy.get("message", "Patch build could not be initiated."))
return deploy["name"]
Prompt To Fix With AI
This is a comment left during a code review.
Path: press/press/doctype/bench_update/bench_update.py
Line: 116-122

Comment:
`KeyError` when builds are suspended and a patch deploy is requested. `trigger_patch_deploy()` returns `{"error": True, "message": "..."}` — which has no `"name"` key — when `is_suspended()` is `True`, so `deploy["name"]` on the next line raises an unhandled `KeyError` that bubbles up to the API caller as a 500 error.

```suggestion
		deploy = (
			candidate.schedule_build_and_deploy(ignore_permissions=ignore_permissions)
			if not trigger_patch_deploy
			else candidate.trigger_patch_deploy(ignore_permissions=ignore_permissions)
		)

		if deploy.get("error"):
			frappe.throw(deploy.get("message", "Patch build could not be initiated."))

		return deploy["name"]
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +293 to +298
@task(queue=_get_task_execution_queue())
def initiate_patch_deploy(self, deploy_candidate: str) -> str:
"""Start the deploy candidate build process with patch deploy flag, skipping pre-build validations."""
candidate: DeployCandidate = frappe.get_doc("Deploy Candidate", deploy_candidate)
deploy_candidate_build = candidate.trigger_patch_deploy(ignore_permissions=True)
return deploy_candidate_build["name"]
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.

P1 trigger_patch_deploy() returns {"error": True, "message": "..."} — with no "name" key — when builds are suspended at task execution time. If builds are suspended in the narrow window between task enqueuing and execution, this line raises a KeyError, causing an opaque task failure instead of a clear suspension message propagating through the pipeline.

Suggested change
@task(queue=_get_task_execution_queue())
def initiate_patch_deploy(self, deploy_candidate: str) -> str:
"""Start the deploy candidate build process with patch deploy flag, skipping pre-build validations."""
candidate: DeployCandidate = frappe.get_doc("Deploy Candidate", deploy_candidate)
deploy_candidate_build = candidate.trigger_patch_deploy(ignore_permissions=True)
return deploy_candidate_build["name"]
@task(queue=_get_task_execution_queue())
def initiate_patch_deploy(self, deploy_candidate: str) -> str:
"""Start the deploy candidate build process with patch deploy flag, skipping pre-build validations."""
candidate: DeployCandidate = frappe.get_doc("Deploy Candidate", deploy_candidate)
deploy_candidate_build = candidate.trigger_patch_deploy(ignore_permissions=True)
if deploy_candidate_build.get("error"):
raise ReleasePipelineFailure(
deploy_candidate_build.get("message", "Patch build could not be initiated.")
)
return deploy_candidate_build["name"]
Prompt To Fix With AI
This is a comment left during a code review.
Path: press/press/doctype/release_pipeline/release_pipeline.py
Line: 293-298

Comment:
`trigger_patch_deploy()` returns `{"error": True, "message": "..."}` — with no `"name"` key — when builds are suspended at task execution time. If builds are suspended in the narrow window between task enqueuing and execution, this line raises a `KeyError`, causing an opaque task failure instead of a clear suspension message propagating through the pipeline.

```suggestion
	@task(queue=_get_task_execution_queue())
	def initiate_patch_deploy(self, deploy_candidate: str) -> str:
		"""Start the deploy candidate build process with patch deploy flag, skipping pre-build validations."""
		candidate: DeployCandidate = frappe.get_doc("Deploy Candidate", deploy_candidate)
		deploy_candidate_build = candidate.trigger_patch_deploy(ignore_permissions=True)
		if deploy_candidate_build.get("error"):
			raise ReleasePipelineFailure(
				deploy_candidate_build.get("message", "Patch build could not be initiated.")
			)
		return deploy_candidate_build["name"]
```

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant