diff --git a/dashboard/src/components/group/UpdateReleaseGroupDialog.vue b/dashboard/src/components/group/UpdateReleaseGroupDialog.vue
index 1a77d7327d1..2c5f8a98bc0 100644
--- a/dashboard/src/components/group/UpdateReleaseGroupDialog.vue
+++ b/dashboard/src/components/group/UpdateReleaseGroupDialog.vue
@@ -2,9 +2,9 @@
+ app.releases.some((release) => release.is_yanked),
+ )
+ "
class="mb-4"
title="A few commits have been yanked, click here to know more."
type="info"
/>
+ app.releases.some((release) => release.is_mandatory),
+ )
+ "
class="mb-4"
title="A mandatory update is available. Please select the update to proceed."
type="info"
@@ -74,6 +74,49 @@
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/press/agent.py b/press/agent.py
index a0b05ea2d49..1ea393abe82 100644
--- a/press/agent.py
+++ b/press/agent.py
@@ -1339,6 +1339,15 @@ def run_build(self, data: dict):
reference_name=reference_name,
)
+ def run_patch_build(self, data: dict):
+ return self.create_agent_job(
+ "Run Patch Build",
+ "builder/patch_build",
+ data=data,
+ reference_doctype="Deploy Candidate Build",
+ reference_name=data.get("deploy_candidate_build"),
+ )
+
def call_supervisorctl(self, bench: str, action: str, programs: list[str]):
return self.create_agent_job(
"Call Bench Supervisorctl",
diff --git a/press/api/bench.py b/press/api/bench.py
index d9fcc28d524..aa7ad1f2612 100644
--- a/press/api/bench.py
+++ b/press/api/bench.py
@@ -803,6 +803,7 @@ def deploy_and_update(
apps: list,
sites: list | None = None,
run_will_fail_check: bool = True,
+ trigger_patch_deploy: bool = False,
):
use_new_deploy_flow = frappe.db.get_single_value("Press Settings", "use_new_deploy_flow") or 0
@@ -815,7 +816,10 @@ def deploy_and_update(
apps,
sites,
False,
- ).deploy(run_will_fail_check)
+ ).deploy(
+ run_will_fail_check,
+ trigger_patch_deploy=trigger_patch_deploy,
+ )
# We check permissions early on and don't change permissions in the middle of the Workflow
current_team = get_current_team()
@@ -829,7 +833,10 @@ def deploy_and_update(
)
release_pipeline.insert()
release_pipeline.create_release.run_as_workflow(
- apps=apps, sites=sites, run_will_fail_check=run_will_fail_check
+ apps=apps,
+ sites=sites,
+ run_will_fail_check=run_will_fail_check,
+ trigger_patch_deploy=trigger_patch_deploy,
)
return release_pipeline.name
diff --git a/press/api/tests/test_bench.py b/press/api/tests/test_bench.py
index ee10aab41d3..26b7530b9e2 100644
--- a/press/api/tests/test_bench.py
+++ b/press/api/tests/test_bench.py
@@ -121,7 +121,9 @@ def test_deploy_fn_deploys_bench_container(self):
"press.press.doctype.deploy_candidate.deploy_candidate.frappe.enqueue_doc",
new=foreground_enqueue_doc,
)
- @patch.object(DeployCandidate, "schedule_build_and_deploy", new=MagicMock())
+ @patch.object(
+ DeployCandidate, "schedule_build_and_deploy", new=MagicMock(return_value={"name": "mock-deploy"})
+ )
@patch("press.press.doctype.deploy_candidate.deploy_candidate.frappe.db.commit", new=Mock())
def test_deploy_and_update_fn_creates_bench_update(self):
group = new(
diff --git a/press/fixtures/agent_job_type.json b/press/fixtures/agent_job_type.json
index beb2795786e..5db2e0b7976 100644
--- a/press/fixtures/agent_job_type.json
+++ b/press/fixtures/agent_job_type.json
@@ -1,4 +1,124 @@
[
+ {
+ "disabled_auto_retry": 1,
+ "docstatus": 0,
+ "doctype": "Agent Job Type",
+ "max_retry_count": 3,
+ "modified": "2025-10-03 18:02:49.847351",
+ "name": "Run Bench on Shared FS",
+ "request_method": "POST",
+ "request_path": "server/run-benches-on-shared-fs",
+ "steps": [
+ {
+ "parent": "Run Bench on Shared FS",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
+ "step_name": "Change Bench Directory"
+ },
+ {
+ "parent": "Run Bench on Shared FS",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
+ "step_name": "Update Agent Nginx Conf File"
+ },
+ {
+ "parent": "Run Bench on Shared FS",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
+ "step_name": "Update Bench Nginx Conf File"
+ },
+ {
+ "parent": "Run Bench on Shared FS",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
+ "step_name": "Restart Benches"
+ }
+ ]
+ },
+ {
+ "disabled_auto_retry": 1,
+ "docstatus": 0,
+ "doctype": "Agent Job Type",
+ "max_retry_count": 3,
+ "modified": "2025-10-03 18:02:49.847351",
+ "name": "Run Benches on Shared FS",
+ "request_method": "POST",
+ "request_path": "server/run-benches-on-shared-fs",
+ "steps": [
+ {
+ "parent": "Run Benches on Shared FS",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
+ "step_name": "Change Bench Directory"
+ },
+ {
+ "parent": "Run Benches on Shared FS",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
+ "step_name": "Update Agent Nginx Conf File"
+ },
+ {
+ "parent": "Run Benches on Shared FS",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
+ "step_name": "Update Bench Nginx Conf File"
+ },
+ {
+ "parent": "Run Benches on Shared FS",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
+ "step_name": "Configure Site with Redis Private IP"
+ },
+ {
+ "parent": "Run Benches on Shared FS",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
+ "step_name": "Restart Benches"
+ }
+ ]
+ },
+ {
+ "disabled_auto_retry": 0,
+ "docstatus": 0,
+ "doctype": "Agent Job Type",
+ "max_retry_count": 6,
+ "modified": "2023-11-06 07:28:19.023648",
+ "name": "Setup Bench",
+ "request_method": "POST",
+ "request_path": "/benches/setup",
+ "steps": [
+ {
+ "parent": "Setup Bench",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
+ "step_name": "Setup Bench"
+ }
+ ]
+ },
+ {
+ "disabled_auto_retry": 1,
+ "docstatus": 0,
+ "doctype": "Agent Job Type",
+ "max_retry_count": 3,
+ "modified": "2025-11-28 22:48:21.908330",
+ "name": "Remove Auto Scale Site to Upstream",
+ "request_method": "POST",
+ "request_path": "/proxy/upstreams/{primary_upstream}/remove-auto-scale-site",
+ "steps": [
+ {
+ "parent": "Remove Auto Scale Site to Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
+ "step_name": "Remove Auto Scale Site to Upstream"
+ },
+ {
+ "parent": "Remove Auto Scale Site to Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
+ "step_name": "Reload NGINX"
+ }
+ ]
+ },
{
"disabled_auto_retry": 1,
"docstatus": 0,
@@ -10,9 +130,15 @@
"request_path": "/database/binlogs/purge_by_size_limit",
"steps": [
{
+ "parent": "Purge Binlogs By Size Limit",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Find Binlogs To Purge By Size Limit"
},
{
+ "parent": "Purge Binlogs By Size Limit",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Purge Binlog"
}
]
@@ -28,6 +154,9 @@
"request_path": "/database/performance_report",
"steps": [
{
+ "parent": "Fetch Performance Report",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Fetch Performance Report"
}
]
@@ -61,18 +190,33 @@
"request_path": "/bench/{bench}/update_inplace",
"steps": [
{
+ "parent": "Update In Place",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Pull App Changes"
},
{
+ "parent": "Update In Place",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Migrate Sites"
},
{
+ "parent": "Update In Place",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Rebuild Bench Assets"
},
{
+ "parent": "Update In Place",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Commit Container Changes"
},
{
+ "parent": "Update In Place",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Restart"
}
]
@@ -88,9 +232,15 @@
"request_path": "/proxy/upstreams/{upstream}/domains",
"steps": [
{
+ "parent": "Add Domain to Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Add Site File to Upstream Directory"
},
{
+ "parent": "Add Domain to Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -106,9 +256,15 @@
"request_path": "/proxy/upstreams/{ip}/rename",
"steps": [
{
+ "parent": "Rename Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Rename Upstream Directory"
},
{
+ "parent": "Rename Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -124,30 +280,57 @@
"request_path": "benches/{site.bench}/sites/{site.name}/rename",
"steps": [
{
+ "parent": "Rename Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Enable Maintenance Mode"
},
{
+ "parent": "Rename Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Wait for Enqueued Jobs"
},
{
+ "parent": "Rename Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update Site Configuration"
},
{
+ "parent": "Rename Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Rename Site"
},
{
+ "parent": "Rename Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX"
},
{
+ "parent": "Rename Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
},
{
+ "parent": "Rename Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Disable Maintenance Mode"
},
{
+ "parent": "Rename Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Enable Scheduler"
},
{
+ "parent": "Rename Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Create User"
}
]
@@ -163,12 +346,21 @@
"request_path": null,
"steps": [
{
+ "parent": "Recover Failed Site Migration",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Move Site"
},
{
+ "parent": "Recover Failed Site Migration",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Restore Touched Tables"
},
{
+ "parent": "Recover Failed Site Migration",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Disable Maintenance Mode"
}
]
@@ -184,12 +376,21 @@
"request_path": "benches/{bench}/sites/{site}/domains",
"steps": [
{
+ "parent": "Add Domain",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update Site Configuration"
},
{
+ "parent": "Add Domain",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX"
},
{
+ "parent": "Add Domain",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -205,12 +406,21 @@
"request_path": "benches/{bench}/sites/{site}/domains/{domain}",
"steps": [
{
+ "parent": "Remove Domain",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update Site Configuration"
},
{
+ "parent": "Remove Domain",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX"
},
{
+ "parent": "Remove Domain",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -226,24 +436,45 @@
"request_path": "/benches/{bench}/sites/{site}/update/pull",
"steps": [
{
+ "parent": "Update Site Pull",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Enable Maintenance Mode"
},
{
+ "parent": "Update Site Pull",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Wait for Enqueued Jobs"
},
{
+ "parent": "Update Site Pull",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Move Site"
},
{
+ "parent": "Update Site Pull",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX"
},
{
+ "parent": "Update Site Pull",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX Target"
},
{
+ "parent": "Update Site Pull",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
},
{
+ "parent": "Update Site Pull",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Disable Maintenance Mode"
}
]
@@ -259,42 +490,81 @@
"request_path": "/benches/{bench}/sites/{site}/update/migrate",
"steps": [
{
+ "parent": "Update Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Enable Maintenance Mode"
},
{
+ "parent": "Update Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Wait for Enqueued Jobs"
},
{
+ "parent": "Update Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Clear Backup Directory"
},
{
+ "parent": "Update Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Backup Site Tables"
},
{
+ "parent": "Update Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Move Site"
},
{
+ "parent": "Update Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX"
},
{
+ "parent": "Update Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX Target"
},
{
+ "parent": "Update Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
},
{
+ "parent": "Update Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Run App Specific Scripts"
},
{
+ "parent": "Update Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Migrate Site"
},
{
+ "parent": "Update Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Log Touched Tables"
},
{
+ "parent": "Update Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Disable Maintenance Mode"
},
{
+ "parent": "Update Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Build Search Index"
}
]
@@ -310,9 +580,15 @@
"request_path": "/proxy/hosts",
"steps": [
{
+ "parent": "Add Host to Proxy",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Add Host to Proxy"
},
{
+ "parent": "Add Host to Proxy",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -328,9 +604,15 @@
"request_path": "/proxy/upstreams/{upstream}/sites",
"steps": [
{
+ "parent": "Add Code Server to Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Add Site File to Upstream Directory"
},
{
+ "parent": "Add Code Server to Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -346,9 +628,15 @@
"request_path": "/proxy/upstreams/{upstream}/sites",
"steps": [
{
+ "parent": "Remove Code Server from Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Remove Site File from Upstream Directory"
},
{
+ "parent": "Remove Code Server from Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -364,15 +652,27 @@
"request_path": "benches/{bench}/codeserver",
"steps": [
{
+ "parent": "Setup Code Server",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Create Code Server Config"
},
{
+ "parent": "Setup Code Server",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Start Code Server"
},
{
+ "parent": "Setup Code Server",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX"
},
{
+ "parent": "Setup Code Server",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -388,6 +688,9 @@
"request_path": "benches/{bench}/codeserver/start",
"steps": [
{
+ "parent": "Start Code Server",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Start Code Server"
}
]
@@ -403,6 +706,9 @@
"request_path": "benches/{bench}/codeserver/stop",
"steps": [
{
+ "parent": "Stop Code Server",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Stop Code Server"
}
]
@@ -418,12 +724,21 @@
"request_path": "benches/{bench}/codeserver/archive",
"steps": [
{
+ "parent": "Archive Code Server",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Remove Code Server"
},
{
+ "parent": "Archive Code Server",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Generate NGINX Configuration"
},
{
+ "parent": "Archive Code Server",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -439,9 +754,15 @@
"request_path": "/proxy/upstreams",
"steps": [
{
+ "parent": "Add Upstream to Proxy",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Add Upstream Directory"
},
{
+ "parent": "Add Upstream to Proxy",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -457,6 +778,9 @@
"request_path": "/benches/{bench}/sites/{site}/apps",
"steps": [
{
+ "parent": "Install App on Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Install App on Site"
}
]
@@ -472,36 +796,69 @@
"request_path": "/benches/{bench}/sites/restore",
"steps": [
{
+ "parent": "New Site from Backup",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Download Backup Files"
},
{
+ "parent": "New Site from Backup",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "New Site"
},
{
+ "parent": "New Site from Backup",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update Site Configuration"
},
{
+ "parent": "New Site from Backup",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Restore Site"
},
{
+ "parent": "New Site from Backup",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Delete Downloaded Backup Files"
},
{
+ "parent": "New Site from Backup",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Uninstall Unavailable Apps"
},
{
+ "parent": "New Site from Backup",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Migrate Site"
},
{
+ "parent": "New Site from Backup",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Set Administrator Password"
},
{
+ "parent": "New Site from Backup",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Enable Scheduler"
},
{
+ "parent": "New Site from Backup",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX"
},
{
+ "parent": "New Site from Backup",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -517,6 +874,9 @@
"request_path": "benches/{bench}/sites/{site}/reinstall",
"steps": [
{
+ "parent": "Reinstall Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reinstall Site"
}
]
@@ -532,9 +892,15 @@
"request_path": "/proxy/upstreams/{upstream}/sites/{site}/status",
"steps": [
{
+ "parent": "Update Site Status",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update Site File"
},
{
+ "parent": "Update Site Status",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -550,12 +916,21 @@
"request_path": "/benches/{bench}/sites/{site}/apps",
"steps": [
{
+ "parent": "Uninstall App from Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Backup Site"
},
{
+ "parent": "Uninstall App from Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Upload Site Backup to S3"
},
{
+ "parent": "Uninstall App from Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Uninstall App from Site"
}
]
@@ -571,18 +946,33 @@
"request_path": "/benches/{bench}/sites/{site}/update/pull/recover",
"steps": [
{
+ "parent": "Recover Failed Site Pull",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Move Site"
},
{
+ "parent": "Recover Failed Site Pull",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX"
},
{
+ "parent": "Recover Failed Site Pull",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX Target"
},
{
+ "parent": "Recover Failed Site Pull",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
},
{
+ "parent": "Recover Failed Site Pull",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Disable Maintenance Mode"
}
]
@@ -598,6 +988,9 @@
"request_path": "/benches/{bench}/sites/{site}/update/recover",
"steps": [
{
+ "parent": "Recover Failed Site Update",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Disable Maintenance Mode"
}
]
@@ -613,24 +1006,45 @@
"request_path": "/benches/{bench}/sites/{site}/update/migrate/recover",
"steps": [
{
+ "parent": "Recover Failed Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Move Site"
},
{
+ "parent": "Recover Failed Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX"
},
{
+ "parent": "Recover Failed Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX Target"
},
{
+ "parent": "Recover Failed Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
},
{
+ "parent": "Recover Failed Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Restore Touched Tables"
},
{
+ "parent": "Recover Failed Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Run App Specific Scripts"
},
{
+ "parent": "Recover Failed Site Migrate",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Disable Maintenance Mode"
}
]
@@ -646,6 +1060,9 @@
"request_path": "/benches/{bench}/sites/{site}/migrate",
"steps": [
{
+ "parent": "Migrate Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Migrate Site"
}
]
@@ -661,6 +1078,9 @@
"request_path": "/benches/{bench}/info",
"steps": [
{
+ "parent": "Fetch Sites Info",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Fetch Sites Info"
}
]
@@ -676,12 +1096,21 @@
"request_path": "/proxy/hosts/redirects",
"steps": [
{
+ "parent": "Setup Redirects on Hosts",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Remove Redirect on Host"
},
{
+ "parent": "Setup Redirects on Hosts",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Setup Redirect on Host"
},
{
+ "parent": "Setup Redirects on Hosts",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -697,6 +1126,9 @@
"request_path": "/proxy/hosts/redirects",
"steps": [
{
+ "parent": "Remove Redirects on Hosts",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Remove Redirect on Host"
}
]
@@ -712,15 +1144,27 @@
"request_path": "/proxy/upstreams/{upstream}/sites/{site}/rename",
"steps": [
{
+ "parent": "Rename Site on Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Rename Site File in Upstream Directory"
},
{
+ "parent": "Rename Site on Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Rename Host Directory"
},
{
+ "parent": "Rename Site on Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Rename Site in Host Directory"
},
{
+ "parent": "Rename Site on Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -736,9 +1180,15 @@
"request_path": "/proxy/wildcards",
"steps": [
{
+ "parent": "Add Wildcard Hosts to Proxy",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Add Wildcard Hosts to Proxy"
},
{
+ "parent": "Add Wildcard Hosts to Proxy",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -754,9 +1204,15 @@
"request_path": "/benches/{site.bench}/sites/{site.name}/erpnext",
"steps": [
{
+ "parent": "Setup ERPNext",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Create User"
},
{
+ "parent": "Setup ERPNext",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update ERPNext Configuration"
}
]
@@ -772,9 +1228,15 @@
"request_path": "/benches/{bench}/sites/{site}/cache",
"steps": [
{
+ "parent": "Clear Cache",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Clear Cache"
},
{
+ "parent": "Clear Cache",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Clear Website Cache"
}
]
@@ -790,9 +1252,15 @@
"request_path": "/benches/{bench}/sites/{site}/update/migrate/restore",
"steps": [
{
+ "parent": "Restore Site Tables",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Restore Site Tables"
},
{
+ "parent": "Restore Site Tables",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Disable Maintenance Mode"
}
]
@@ -808,12 +1276,21 @@
"request_path": "/ssh/users",
"steps": [
{
+ "parent": "Add User to Proxy",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Add User to Proxy"
},
{
+ "parent": "Add User to Proxy",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Add Certificate to User"
},
{
+ "parent": "Add User to Proxy",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Add Principal to User"
}
]
@@ -829,9 +1306,15 @@
"request_path": "/ssh/users/{user}",
"steps": [
{
+ "parent": "Remove User from Proxy",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Remove User from Proxy"
},
{
+ "parent": "Remove User from Proxy",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Remove Principal from User"
}
]
@@ -847,6 +1330,9 @@
"request_path": "/proxysql/users",
"steps": [
{
+ "parent": "Add User to ProxySQL",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Add User to ProxySQL"
}
]
@@ -862,6 +1348,9 @@
"request_path": "/proxysql/users/{username}",
"steps": [
{
+ "parent": "Remove User from ProxySQL",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Remove User from ProxySQL"
}
]
@@ -877,12 +1366,21 @@
"request_path": "/minio/create",
"steps": [
{
+ "parent": "Create Minio User",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Create Minio User"
},
{
+ "parent": "Create Minio User",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Create Minio Policy"
},
{
+ "parent": "Create Minio User",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Add Minio Policy"
}
]
@@ -898,6 +1396,9 @@
"request_path": "/minio/remove",
"steps": [
{
+ "parent": "Remove Minio User",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Remove Minio User"
}
]
@@ -913,6 +1414,9 @@
"request_path": "/minio/update",
"steps": [
{
+ "parent": "Enable Minio User",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Enable Minio User"
}
]
@@ -928,6 +1432,9 @@
"request_path": "/minio/update",
"steps": [
{
+ "parent": "Disable Minio User",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Disable Minio User"
}
]
@@ -943,12 +1450,21 @@
"request_path": "server/cleanup",
"steps": [
{
+ "parent": "Cleanup Unused Files",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Remove Archived Benches"
},
{
+ "parent": "Cleanup Unused Files",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Remove Temporary Files"
},
{
+ "parent": "Cleanup Unused Files",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Remove Unused Docker Artefacts"
}
]
@@ -964,6 +1480,9 @@
"request_path": "/proxysql/backends",
"steps": [
{
+ "parent": "Add Backend to ProxySQL",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Add Backend to ProxySQL"
}
]
@@ -979,6 +1498,9 @@
"request_path": "/benches/{bench}/sites/{site}/update/saas",
"steps": [
{
+ "parent": "Update Saas Plan",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update Saas Plan"
}
]
@@ -994,18 +1516,33 @@
"request_path": "benches/{bench}/sites/{site}/run_after_migrate_steps",
"steps": [
{
+ "parent": "Run After Migrate Steps",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Set Administrator Password"
},
{
+ "parent": "Run After Migrate Steps",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX"
},
{
+ "parent": "Run After Migrate Steps",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
},
{
+ "parent": "Run After Migrate Steps",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Disable Maintenance Mode"
},
{
+ "parent": "Run After Migrate Steps",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Enable Scheduler"
}
]
@@ -1021,27 +1558,51 @@
"request_path": "/benches/{bench}/sites/{site}/move_to_bench",
"steps": [
{
+ "parent": "Move Site to Bench",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Enable Maintenance Mode"
},
{
+ "parent": "Move Site to Bench",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Wait for Enqueued Jobs"
},
{
+ "parent": "Move Site to Bench",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Move Site"
},
{
+ "parent": "Move Site to Bench",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX"
},
{
+ "parent": "Move Site to Bench",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX Target"
},
{
+ "parent": "Move Site to Bench",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
},
{
+ "parent": "Move Site to Bench",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Migrate Site"
},
{
+ "parent": "Move Site to Bench",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Disable Maintenance Mode"
}
]
@@ -1057,6 +1618,9 @@
"request_path": "benches/{bench}/sites/{site}/usage",
"steps": [
{
+ "parent": "Reset Site Usage",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reset Site Usage"
}
]
@@ -1072,6 +1636,9 @@
"request_path": "/proxy/reload",
"steps": [
{
+ "parent": "Reload NGINX Job",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -1087,9 +1654,15 @@
"request_path": null,
"steps": [
{
+ "parent": "Backup Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Backup Site"
},
{
+ "parent": "Backup Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Upload Site Backup to S3"
}
]
@@ -1105,6 +1678,9 @@
"request_path": "/benches/{bench}/restart",
"steps": [
{
+ "parent": "Bench Restart",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Restart"
}
]
@@ -1120,9 +1696,15 @@
"request_path": "/benches/{bench}/archive",
"steps": [
{
+ "parent": "Archive Bench",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Disable Production"
},
{
+ "parent": "Archive Bench",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Move Bench to Archived Directory"
}
]
@@ -1138,15 +1720,27 @@
"request_path": "/benches",
"steps": [
{
+ "parent": "New Bench",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Initialize Bench"
},
{
+ "parent": "New Bench",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update Bench Configuration"
},
{
+ "parent": "New Bench",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Deploy Bench"
},
{
+ "parent": "New Bench",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX"
}
]
@@ -1162,12 +1756,21 @@
"request_path": "benches/{bench}/limit",
"steps": [
{
+ "parent": "Force Update Bench Limits",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Stop Bench"
},
{
+ "parent": "Force Update Bench Limits",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update Bench Memory Limits"
},
{
+ "parent": "Force Update Bench Limits",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Start Bench"
}
]
@@ -1183,18 +1786,33 @@
"request_path": "/benches/{bench}/config",
"steps": [
{
+ "parent": "Update Bench Configuration",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update Bench Configuration"
},
{
+ "parent": "Update Bench Configuration",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX"
},
{
+ "parent": "Update Bench Configuration",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Generate Docker Compose File"
},
{
+ "parent": "Update Bench Configuration",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update Bench Memory Limits"
},
{
+ "parent": "Update Bench Configuration",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Deploy Bench"
}
]
@@ -1210,6 +1828,9 @@
"request_path": "benches/{bench}/rebuild",
"steps": [
{
+ "parent": "Rebuild Bench Assets",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Rebuild Bench Assets"
}
]
@@ -1225,6 +1846,9 @@
"request_path": "benches/{bench}/sites/{site}/optimize",
"steps": [
{
+ "parent": "Optimize Tables",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Optimize Tables"
}
]
@@ -1240,6 +1864,9 @@
"request_path": "builder/build",
"steps": [
{
+ "parent": "Docker Image Build",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Docker Image Build"
}
]
@@ -1255,12 +1882,21 @@
"request_path": "/bench/{bench}/patch/{app}",
"steps": [
{
+ "parent": "Patch App",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Git Apply"
},
{
+ "parent": "Patch App",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Rebuild Bench Assets"
},
{
+ "parent": "Patch App",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Restart"
}
]
@@ -1276,6 +1912,9 @@
"request_path": "/benches/{bench}/supervisorctl",
"steps": [
{
+ "parent": "Call Bench Supervisorctl",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Run Supervisorctl Command"
}
]
@@ -1291,18 +1930,33 @@
"request_path": "/builder/build",
"steps": [
{
+ "parent": "Run Remote Builder",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Clone Repositories"
},
{
+ "parent": "Run Remote Builder",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Run Validations"
},
{
+ "parent": "Run Remote Builder",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Build Image"
},
{
+ "parent": "Run Remote Builder",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Push Docker Image"
},
{
+ "parent": "Run Remote Builder",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Cleanup Context"
}
]
@@ -1318,6 +1972,9 @@
"request_path": "/benches/{bench}/sites/{site}/add-database-index",
"steps": [
{
+ "parent": "Add Database Index",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Add Database Index With Bench Command"
}
]
@@ -1333,36 +1990,69 @@
"request_path": "/benches/{bench}/sites/{site}/restore",
"steps": [
{
+ "parent": "Restore Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Download Backup Files"
},
{
+ "parent": "Restore Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Restore Site"
},
{
+ "parent": "Restore Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Restore Files"
},
{
+ "parent": "Restore Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Checksum of Downloaded Backup Files"
},
{
+ "parent": "Restore Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Delete Downloaded Backup Files"
},
{
+ "parent": "Restore Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Uninstall Unavailable Apps"
},
{
+ "parent": "Restore Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Migrate Site"
},
{
+ "parent": "Restore Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Set Administrator Password"
},
{
+ "parent": "Restore Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Enable Scheduler"
},
{
+ "parent": "Restore Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX"
},
{
+ "parent": "Restore Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -1378,6 +2068,9 @@
"request_path": "/database/column-stats",
"steps": [
{
+ "parent": "Column Statistics",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Fetch Column Statistics"
}
]
@@ -1393,6 +2086,9 @@
"request_path": "/benches/{bench}/sites/{site}/complete-setup-wizard",
"steps": [
{
+ "parent": "Complete Setup Wizard",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Complete Setup Wizard"
}
]
@@ -1408,6 +2104,9 @@
"request_path": "/benches/{bench}/sites/{site}/create-user",
"steps": [
{
+ "parent": "Create User",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Create User"
}
]
@@ -1423,24 +2122,45 @@
"request_path": "/benches/{bench}/sites",
"steps": [
{
+ "parent": "New Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "New Site"
},
{
+ "parent": "New Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Install Apps"
},
{
+ "parent": "New Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update Site Configuration"
},
{
+ "parent": "New Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Enable Scheduler"
},
{
+ "parent": "New Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Create User"
},
{
+ "parent": "New Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX"
},
{
+ "parent": "New Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -1456,9 +2176,15 @@
"request_path": "/proxy/upstreams/{upstream}/sites",
"steps": [
{
+ "parent": "Add Site to Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Add Site File to Upstream Directory"
},
{
+ "parent": "Add Site to Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -1474,6 +2200,9 @@
"request_path": "/benches/{bench}/sites/{site}/config",
"steps": [
{
+ "parent": "Update Site Configuration",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update Site Configuration"
}
]
@@ -1489,6 +2218,9 @@
"request_path": "/proxy/upstreams/{upstream}/sites/{site}",
"steps": [
{
+ "parent": "Remove Site from Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Remove Site File from Upstream Directory"
}
]
@@ -1504,18 +2236,33 @@
"request_path": null,
"steps": [
{
+ "parent": "Archive Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Backup Site"
},
{
+ "parent": "Archive Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Upload Site Backup to S3"
},
{
+ "parent": "Archive Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Archive Site"
},
{
+ "parent": "Archive Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX"
},
{
+ "parent": "Archive Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -1531,15 +2278,27 @@
"request_path": "/bench/{bench}/recover_update_inplace",
"steps": [
{
+ "parent": "Recover Update In Place",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Enable Maintenance Mode"
},
{
+ "parent": "Recover Update In Place",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Deploy Bench"
},
{
+ "parent": "Recover Update In Place",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Setup NGINX"
},
{
+ "parent": "Recover Update In Place",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Recover Sites"
}
]
@@ -1555,6 +2314,9 @@
"request_path": "ping_job",
"steps": [
{
+ "parent": "Ping Job",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Ping Step"
}
]
@@ -1570,18 +2332,33 @@
"request_path": "/bench/{bench}/update_inplace",
"steps": [
{
+ "parent": "Update Bench In Place",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Pull App Changes"
},
{
+ "parent": "Update Bench In Place",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Migrate Sites"
},
{
+ "parent": "Update Bench In Place",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Rebuild Bench Assets"
},
{
+ "parent": "Update Bench In Place",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Commit Container Changes"
},
{
+ "parent": "Update Bench In Place",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Bench Restart"
}
]
@@ -1597,6 +2374,9 @@
"request_path": "/benches/{bench}/sites/{site}/database/users",
"steps": [
{
+ "parent": "Create Database User",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Create Database User"
}
]
@@ -1612,6 +2392,9 @@
"request_path": "/benches/{bench}/sites/{site}/database/users/{username}",
"steps": [
{
+ "parent": "Remove Database User",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Remove Database User"
}
]
@@ -1627,6 +2410,9 @@
"request_path": "/benches/{bench}/sites/{site}/database/users/{db_user}/permissions",
"steps": [
{
+ "parent": "Modify Database User Permissions",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Modify Database User Permissions"
}
]
@@ -1642,6 +2428,9 @@
"request_path": "/benches/{bench}/sites/{site}/database/schema",
"steps": [
{
+ "parent": "Fetch Database Table Schema",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Fetch Database Table Schema"
}
]
@@ -1657,6 +2446,9 @@
"request_path": "/benches/{bench}/sites/{site}/database/analyze-slow-queries",
"steps": [
{
+ "parent": "Analyze Slow Queries",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Analyze Slow Queries"
}
]
@@ -1672,27 +2464,51 @@
"request_path": "/database/physical-backup",
"steps": [
{
+ "parent": "Physical Backup Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Fetch Database Tables Information"
},
{
+ "parent": "Physical Backup Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Flush Database Tables"
},
{
+ "parent": "Physical Backup Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Flush Changes to Disk"
},
{
+ "parent": "Physical Backup Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Export Table Schema"
},
{
+ "parent": "Physical Backup Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Collect Files Metadata"
},
{
+ "parent": "Physical Backup Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Store Backup Metadata"
},
{
+ "parent": "Physical Backup Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Create Database Snapshot"
},
{
+ "parent": "Physical Backup Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Unlock Tables"
}
]
@@ -1708,51 +2524,99 @@
"request_path": "/database/physical-restore",
"steps": [
{
+ "parent": "Physical Restore Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Validate Backup Files"
},
{
+ "parent": "Physical Restore Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Validate Connection to Target Database"
},
{
+ "parent": "Physical Restore Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Warmup MyISAM Files"
},
{
+ "parent": "Physical Restore Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Check and Fix MyISAM Table Files"
},
{
+ "parent": "Physical Restore Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Warmup InnoDB Files"
},
{
+ "parent": "Physical Restore Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Collect InnoDB FTS Index Metadata"
},
{
+ "parent": "Physical Restore Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Prepare Database for Restoration"
},
{
+ "parent": "Physical Restore Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Create Tables from Table Schema"
},
{
+ "parent": "Physical Restore Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Sync FTS Indexes Prefix Length"
},
{
+ "parent": "Physical Restore Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Discard InnoDB Tablespaces"
},
{
+ "parent": "Physical Restore Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Copying InnoDB Table Files"
},
{
+ "parent": "Physical Restore Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Import InnoDB Tablespaces"
},
{
+ "parent": "Physical Restore Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Hold Write Lock on MyISAM Tables"
},
{
+ "parent": "Physical Restore Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Copying MyISAM Table Files"
},
{
+ "parent": "Physical Restore Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Unlock All Tables"
},
{
+ "parent": "Physical Restore Database",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Validate And Fix Tables"
}
]
@@ -1768,6 +2632,9 @@
"request_path": "/benches/{bench}/sites/{site}/activate",
"steps": [
{
+ "parent": "Activate Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Disable Maintenance Mode"
}
]
@@ -1783,9 +2650,15 @@
"request_path": "/benches/{bench}/sites/{site}/deactivate",
"steps": [
{
+ "parent": "Deactivate Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Enable Maintenance Mode"
},
{
+ "parent": "Deactivate Site",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Wait for Enqueued Jobs"
}
]
@@ -1801,6 +2674,9 @@
"request_path": "/database/binlogs/upload",
"steps": [
{
+ "parent": "Upload Binlogs To S3",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Upload Binlogs To S3"
}
]
@@ -1816,6 +2692,9 @@
"request_path": "/database/binlogs/indexer/add",
"steps": [
{
+ "parent": "Add Binlogs To Indexer",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Add Binlogs To Indexer"
}
]
@@ -1831,6 +2710,9 @@
"request_path": "/database/binlogs/indexer/remove",
"steps": [
{
+ "parent": "Remove Binlogs From Indexer",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Remove Binlogs From Indexer"
}
]
@@ -1846,9 +2728,15 @@
"request_path": "/snapshot_recovery/backup_files",
"steps": [
{
+ "parent": "Backup Files From Snapshot",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Backup Files"
},
{
+ "parent": "Backup Files From Snapshot",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Upload Backup Files to S3"
}
]
@@ -1864,9 +2752,15 @@
"request_path": "/snapshot_recovery/backup_database",
"steps": [
{
+ "parent": "Backup Database From Snapshot",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Backup Database"
},
{
+ "parent": "Backup Database From Snapshot",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Upload Backup Files to S3"
}
]
@@ -1882,6 +2776,9 @@
"request_path": "/snapshot_recovery/search_sites",
"steps": [
{
+ "parent": "Search Sites In Snapshot",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Search Sites"
}
]
@@ -1897,18 +2794,33 @@
"request_path": "server/change-bench-directory",
"steps": [
{
+ "parent": "Change Bench Directory",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Change Bench Directory"
},
{
+ "parent": "Change Bench Directory",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update Agent Nginx Conf File"
},
{
+ "parent": "Change Bench Directory",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update Bench Nginx Conf File"
},
{
+ "parent": "Change Bench Directory",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Configure Site with Redis Private IP"
},
{
+ "parent": "Change Bench Directory",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Restart Benches"
}
]
@@ -1924,6 +2836,9 @@
"request_path": "server/stop-bench-workers",
"steps": [
{
+ "parent": "Stop Bench Workers",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Stop Bench Workers"
}
]
@@ -1939,6 +2854,9 @@
"request_path": "nfs/add-to-acl",
"steps": [
{
+ "parent": "Add Servers to ACL",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Add Servers to ACL"
}
]
@@ -1954,6 +2872,9 @@
"request_path": "nfs/remove-from-acl",
"steps": [
{
+ "parent": "Remove Servers from ACL",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Remove Servers from ACL"
}
]
@@ -1969,6 +2890,9 @@
"request_path": "server/start-bench-workers",
"steps": [
{
+ "parent": "Start Bench Workers",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Start Bench Workers"
}
]
@@ -1984,6 +2908,9 @@
"request_path": "server/force-remove-all-benches",
"steps": [
{
+ "parent": "Force Remove All Benches",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Force Remove All Benches"
}
]
@@ -1999,6 +2926,9 @@
"request_path": "/proxy/hosts/{host}",
"steps": [
{
+ "parent": "Remove Host from Proxy",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Remove Host from Proxy"
}
]
@@ -2014,6 +2944,9 @@
"request_path": "/database/sql/execute",
"steps": [
{
+ "parent": "Run SQL Queries",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Run SQL Queries"
}
]
@@ -2029,6 +2962,9 @@
"request_path": "/benches/database_host",
"steps": [
{
+ "parent": "Update Database Host",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update Database Host"
}
]
@@ -2044,9 +2980,15 @@
"request_path": "/database/update-schema-sizes",
"steps": [
{
+ "parent": "Update Database Schema Sizes",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Setup Metadata Table"
},
{
+ "parent": "Update Database Schema Sizes",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update Database Schema Size in Metadata Table"
}
]
@@ -2062,9 +3004,15 @@
"request_path": "/proxy/upstreams/{primary_upstream}/auto-scale-site",
"steps": [
{
+ "parent": "Add Auto Scale Site to Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Add Auto Scale Site to Upstream"
},
{
+ "parent": "Add Auto Scale Site to Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -2080,9 +3028,15 @@
"request_path": "/proxy/upstreams/{primary_upstream}/remove-auto-scale-site",
"steps": [
{
+ "parent": "Remove Auto Scale Site from Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Remove Auto Scale Site from Upstream"
},
{
+ "parent": "Remove Auto Scale Site from Upstream",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -2098,6 +3052,9 @@
"request_path": "/server/push-images",
"steps": [
{
+ "parent": "Push Images to Registry",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Push Images to Registry"
}
]
@@ -2113,6 +3070,9 @@
"request_path": "/server/remove-localhost-redis-bind",
"steps": [
{
+ "parent": "Remove Redis Localhost Bind",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Remove Redis Localhost Bind"
}
]
@@ -2128,12 +3088,21 @@
"request_path": "/server/update-nginx-access",
"steps": [
{
+ "parent": "Update Nginx Access",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update config IP access"
},
{
+ "parent": "Update Nginx Access",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update Agent Nginx Conf File"
},
{
+ "parent": "Update Nginx Access",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Reload NGINX"
}
]
@@ -2149,6 +3118,9 @@
"request_path": "/database/flush-tables",
"steps": [
{
+ "parent": "Flush Tables",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Flush Tables"
}
]
@@ -2164,11 +3136,89 @@
"request_path": "/database/refresh-usage",
"steps": [
{
+ "parent": "Refresh Database Usage",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Analyze Tables of Database"
},
{
+ "parent": "Refresh Database Usage",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
"step_name": "Update Database Schema Size"
}
]
+ },
+ {
+ "disabled_auto_retry": 1,
+ "docstatus": 0,
+ "doctype": "Agent Job Type",
+ "max_retry_count": 3,
+ "modified": "2026-05-24 19:06:17.764873",
+ "name": "Run Patch Build",
+ "request_method": "POST",
+ "request_path": "/builder/instant_build",
+ "steps": [
+ {
+ "parent": "Run Patch Build",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
+ "step_name": "Start Base Container"
+ },
+ {
+ "parent": "Run Patch Build",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
+ "step_name": "Pull App Updates"
+ },
+ {
+ "parent": "Run Patch Build",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
+ "step_name": "Commit Image"
+ },
+ {
+ "parent": "Run Patch Build",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
+ "step_name": "Push Docker Image"
+ }
+ ]
+ },
+ {
+ "disabled_auto_retry": 1,
+ "docstatus": 0,
+ "doctype": "Agent Job Type",
+ "max_retry_count": 3,
+ "modified": "2025-10-27 16:18:50.088135",
+ "name": "Set Redis Password",
+ "request_method": "POST",
+ "request_path": "/benches/{bench}/set-redis-password",
+ "steps": [
+ {
+ "parent": "Set Redis Password",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
+ "step_name": "Set Redis Password"
+ }
+ ]
+ },
+ {
+ "disabled_auto_retry": 1,
+ "docstatus": 0,
+ "doctype": "Agent Job Type",
+ "max_retry_count": 3,
+ "modified": "2025-05-21 10:49:01.041121",
+ "name": "Pull Docker Images",
+ "request_method": "POST",
+ "request_path": "/server/pull-images",
+ "steps": [
+ {
+ "parent": "Pull Docker Images",
+ "parentfield": "steps",
+ "parenttype": "Agent Job Type",
+ "step_name": "Pull Docker Images"
+ }
+ ]
}
]
diff --git a/press/press/doctype/agent_job/agent_job.py b/press/press/doctype/agent_job/agent_job.py
index 5d25cdd5427..8311a29b653 100644
--- a/press/press/doctype/agent_job/agent_job.py
+++ b/press/press/doctype/agent_job/agent_job.py
@@ -1109,6 +1109,8 @@ def process_job_updates(job_name: str, response_data: dict | None = None): # no
AppPatch.process_patch_app(job)
elif job.job_type == "Run Remote Builder":
DeployCandidateBuild.process_run_build(job, response_data)
+ elif job.job_type == "Run Patch Build":
+ DeployCandidateBuild.process_run_patch_build(job, response_data)
elif job.job_type == "Create User":
process_create_user_job_update(job)
elif job.job_type == "Complete Setup Wizard":
diff --git a/press/press/doctype/bench_update/bench_update.py b/press/press/doctype/bench_update/bench_update.py
index 08537b15244..6ee66d7b67d 100644
--- a/press/press/doctype/bench_update/bench_update.py
+++ b/press/press/doctype/bench_update/bench_update.py
@@ -90,6 +90,7 @@ def deploy(
validate_pre_candidate_checks: bool = True,
create_build: bool = True,
ignore_permissions: bool = False,
+ trigger_patch_deploy: bool = False,
) -> str:
"""Creates and returns candidate name or build name depending on the point of invocation."""
rg: ReleaseGroup = frappe.get_doc("Release Group", self.group)
@@ -112,7 +113,17 @@ def deploy(
# In case we are not scheduling build from here (eg. new build flow) return candidate name here
return candidate.name
- deploy = candidate.schedule_build_and_deploy(ignore_permissions=ignore_permissions)
+ deploy = (
+ candidate.schedule_build_and_deploy(ignore_permissions=ignore_permissions)
+ if not trigger_patch_deploy
+ else candidate.trigger_patch_deploy(ignore_permissions=ignore_permissions)
+ )
+
+ # Trigger patch deploy can return error if the builds are suspended
+ if deploy.get("error"):
+ raise Exception(
+ deploy.get("message", "Build could not be initiated for the deploy candidate."),
+ )
return deploy["name"]
diff --git a/press/press/doctype/deploy_candidate/deploy_candidate.py b/press/press/doctype/deploy_candidate/deploy_candidate.py
index f58e93a91f2..715702a2849 100644
--- a/press/press/doctype/deploy_candidate/deploy_candidate.py
+++ b/press/press/doctype/deploy_candidate/deploy_candidate.py
@@ -238,6 +238,18 @@ def build(
deploy_candidate_build.insert()
return dict(error=False, message=deploy_candidate_build.name)
+ @frappe.whitelist()
+ def trigger_patch_deploy(self, ignore_permissions: bool = False):
+ if is_suspended():
+ return dict(
+ error=True,
+ message="Builds are currently suspended. Please try again later.",
+ )
+
+ deploy_candidate_build = self.create_build(no_cache=False, deploy_after_build=True, patch_build=True)
+ deploy_candidate_build.insert(ignore_permissions=ignore_permissions)
+ return {"error": False, "name": deploy_candidate_build.name}
+
@frappe.whitelist()
def schedule_build_and_deploy(
self,
diff --git a/press/press/doctype/deploy_candidate/docker_output_parsers.py b/press/press/doctype/deploy_candidate/docker_output_parsers.py
index 8f934cb65a2..204510db890 100644
--- a/press/press/doctype/deploy_candidate/docker_output_parsers.py
+++ b/press/press/doctype/deploy_candidate/docker_output_parsers.py
@@ -497,6 +497,64 @@ def parse_clone_output_and_update_step(self, job: AgentJob) -> bool:
return self._update_clone_steps(non_terminal_clone_steps, app_output_map)
+class PatchBuildOutputParser(StepMixin):
+ """Parses build/push output from patch build agent job steps."""
+
+ def __init__(self, dcb: "DeployCandidateBuild") -> None:
+ self.dcb = dcb
+
+ def _get_step_data(self, job: "AgentJob", step_name: str) -> "dict | None":
+ step = self._get_agent_step(job, step_name, ["name", "status"], as_dict=True)
+ if not isinstance(step, dict):
+ return None
+
+ raw = None
+ if step["status"] == "Running":
+ raw = frappe.cache.hget("agent_job_step_output", step["name"])
+
+ if not raw:
+ raw = frappe.db.get_value("Agent Job Step", step["name"], "data")
+
+ try:
+ return json.loads(raw) if raw else None
+ except (json.JSONDecodeError, AttributeError):
+ return raw
+
+ def parse_build_output(self, job: "AgentJob") -> None:
+ data = self._get_step_data(job, "Pull App Updates")
+ if not data:
+ return
+
+ build_output = ""
+
+ if isinstance(data, str):
+ # Case of cache
+ build_output = data
+ if isinstance(data, list):
+ # Case of DB stored output, which is a list of lines
+ build_output = "\n".join(str(line) for line in data)
+ if isinstance(data, dict):
+ # Case of error output which is a dict with error details
+ build_output = data.get("output", "")
+
+ if step := self.dcb.get_step("patch", "update-apps"):
+ step.output = build_output
+ step.save()
+
+ def parse_push_output(self, job: "AgentJob") -> None:
+ data = self._get_step_data(job, "Push Docker Image")
+ push_output = data.get("push") if isinstance(data, dict) else data
+ if not push_output:
+ return
+
+ self.dcb.upload_step_updater.process(push_output)
+ self.dcb.upload_step_updater.flush_output(commit=False)
+
+ def parse_and_update(self, job: "AgentJob") -> None:
+ self.parse_build_output(job)
+ self.parse_push_output(job)
+
+
class UploadStepUpdater:
"""
Processes the output of `client.images.push` and uses it to update
@@ -541,7 +599,7 @@ def process(self, output: "PushOutput"):
for line in output:
self._update_output(line)
- last_update = self.dc.last_updated
+ last_update = self.dc.last_updated if hasattr(self.dc, "last_updated") else now_datetime()
duration = (now_datetime() - last_update).total_seconds()
self.upload_step.duration = rounded(duration, 1)
self.flush_output()
diff --git a/press/press/doctype/deploy_candidate_build/deploy_candidate_build.json b/press/press/doctype/deploy_candidate_build/deploy_candidate_build.json
index baf1a713888..dd6eb365fc4 100644
--- a/press/press/doctype/deploy_candidate_build/deploy_candidate_build.json
+++ b/press/press/doctype/deploy_candidate_build/deploy_candidate_build.json
@@ -16,6 +16,7 @@
"no_cache",
"no_push",
"deploy_after_build",
+ "patch_build",
"run_build",
"scheduled_time",
"deploy_on_server",
@@ -103,15 +104,25 @@
"label": "Deploy After Build",
"read_only": 1
},
- { "fieldname": "column_break_9sga", "fieldtype": "Column Break" },
- { "fieldname": "section_break_bfxt", "fieldtype": "Section Break" },
+ {
+ "fieldname": "column_break_9sga",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_bfxt",
+ "fieldtype": "Section Break"
+ },
{
"fieldname": "build_directory",
"fieldtype": "Data",
"label": "Directory",
"read_only": 1
},
- { "fieldname": "output_tab", "fieldtype": "Tab Break", "label": "Output" },
+ {
+ "fieldname": "output_tab",
+ "fieldtype": "Tab Break",
+ "label": "Output"
+ },
{
"fieldname": "status",
"fieldtype": "Select",
@@ -131,7 +142,10 @@
"label": "Build Start",
"read_only": 1
},
- { "fieldname": "column_break_5m9f", "fieldtype": "Column Break" },
+ {
+ "fieldname": "column_break_5m9f",
+ "fieldtype": "Column Break"
+ },
{
"fieldname": "build_end",
"fieldtype": "Datetime",
@@ -176,7 +190,10 @@
"label": "Retry Count",
"read_only": 1
},
- { "fieldname": "column_break_3jie", "fieldtype": "Column Break" },
+ {
+ "fieldname": "column_break_3jie",
+ "fieldtype": "Column Break"
+ },
{
"default": "0",
"description": "Set if the build was manually failed or cancelled.",
@@ -246,7 +263,10 @@
"label": "Docker image",
"read_only": 1
},
- { "fieldname": "column_break_gi6p", "fieldtype": "Column Break" },
+ {
+ "fieldname": "column_break_gi6p",
+ "fieldtype": "Column Break"
+ },
{
"fetch_from": "group.team",
"fieldname": "team",
@@ -273,17 +293,35 @@
"label": "Deploy On Server",
"options": "Server",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "patch_build",
+ "fieldtype": "Check",
+ "label": "Patch Build"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [
- { "link_doctype": "Agent Job", "link_fieldname": "reference_name" },
- { "link_doctype": "Error Log", "link_fieldname": "reference_name" },
- { "link_doctype": "Press Notification", "link_fieldname": "document_name" },
- { "link_doctype": "Bench", "link_fieldname": "build" }
+ {
+ "link_doctype": "Agent Job",
+ "link_fieldname": "reference_name"
+ },
+ {
+ "link_doctype": "Error Log",
+ "link_fieldname": "reference_name"
+ },
+ {
+ "link_doctype": "Press Notification",
+ "link_fieldname": "document_name"
+ },
+ {
+ "link_doctype": "Bench",
+ "link_fieldname": "build"
+ }
],
- "modified": "2026-03-16 16:23:51.449383",
+ "modified": "2026-05-25 17:18:16.775165",
"modified_by": "Administrator",
"module": "Press",
"name": "Deploy Candidate Build",
diff --git a/press/press/doctype/deploy_candidate_build/deploy_candidate_build.py b/press/press/doctype/deploy_candidate_build/deploy_candidate_build.py
index b8d80b099e3..08122a505c3 100644
--- a/press/press/doctype/deploy_candidate_build/deploy_candidate_build.py
+++ b/press/press/doctype/deploy_candidate_build/deploy_candidate_build.py
@@ -36,6 +36,7 @@
from press.press.doctype.deploy_candidate.docker_output_parsers import (
CloneOutputParser,
DockerBuildOutputParser,
+ PatchBuildOutputParser,
UploadStepUpdater,
ValidationOutputParser,
)
@@ -93,6 +94,7 @@ def intermediate(cls):
"pull": "Pull Updates",
"mounts": "Setup Mounts",
"upload": "Upload",
+ "patch": "Patch Build",
}
# Key: (stage_slug, step_slug)
@@ -112,6 +114,17 @@ def intermediate(cls):
("validate", "dependencies"): "Validate Dependencies",
("mounts", "create"): "Prepare Mounts",
("upload", "image"): "Docker Image",
+ ("patch", "pull-image"): "Pull Base Image",
+ ("patch", "update-apps"): "Update Apps",
+ ("patch", "commit"): "Commit Image",
+}
+
+# Maps agent @step names → (stage_slug, step_slug) for instant builds
+PATCH_BUILD_STEP_MAP = {
+ "Start Base Container": ("patch", "pull-image"),
+ "Pull App Updates": ("patch", "update-apps"),
+ "Commit Image": ("patch", "commit"),
+ "Push Docker Image": ("upload", "image"),
}
@@ -219,6 +232,7 @@ class DeployCandidateBuild(Document):
no_build: DF.Check
no_cache: DF.Check
no_push: DF.Check
+ patch_build: DF.Check
pending_duration: DF.Time | None
pending_end: DF.Datetime | None
pending_start: DF.Datetime | None
@@ -334,6 +348,29 @@ def _generate_dockerfile(self) -> str:
is_path=True,
)
+ def add_patch_build_steps(self):
+ slugs = [
+ ("patch", "pull-image"),
+ ("patch", "update-apps"),
+ ("patch", "commit"),
+ ]
+ if not self.no_push:
+ slugs.append(("upload", "image"))
+
+ for stage_slug, step_slug in slugs:
+ stage, step = get_build_stage_and_step(stage_slug, step_slug, {})
+ self.append(
+ "build_steps",
+ dict(
+ status="Pending",
+ stage_slug=stage_slug,
+ step_slug=step_slug,
+ stage=stage,
+ step=step,
+ ),
+ )
+ self.save()
+
def add_post_build_steps(self):
slugs = []
@@ -419,6 +456,7 @@ def _set_output_parsers(self):
self.validation_output_parser = ValidationOutputParser(self)
self.build_output_parser = DockerBuildOutputParser(self)
self.upload_step_updater = UploadStepUpdater(self)
+ self.patch_build_output_parser = PatchBuildOutputParser(self)
def correct_upload_step_status(self):
if not (usu := self.upload_step_updater) or not usu.upload_step:
@@ -991,8 +1029,144 @@ def run_scheduled_build_and_deploy(self):
self.set_status(Status.DRAFT)
self.pre_build()
+ def send_patch_build_instructions(self, previous_candidate: "DeployCandidate"):
+ """Send patch build instructions to the agent for current deploy candidate build"""
+ self.set_build_server()
+ self._update_docker_image_metadata()
+ self.add_patch_build_steps()
+
+ settings = self._fetch_registry_settings()
+ Agent(self.build_server).run_patch_build(
+ {
+ "base_image": self._get_base_image_for_platform(previous_candidate),
+ "patch_build_app_instructions": self._get_patch_app_updates(previous_candidate),
+ "image_repository": self.docker_image_repository,
+ "image_tag": self.docker_image_tag,
+ "registry": {
+ "url": settings.docker_registry_url,
+ "username": settings.docker_registry_username,
+ "password": settings.docker_registry_password,
+ },
+ "deploy_candidate_build": self.name,
+ "deploy_after_build": self.deploy_after_build,
+ "no_push": self.no_push,
+ }
+ )
+ self.set_status(Status.RUNNING, commit=True)
+
+ def _get_base_image_for_platform(self, previous_candidate: "DeployCandidate") -> str:
+ build_name = (
+ previous_candidate.arm_build
+ if self.platform == "arm64"
+ else (previous_candidate.intel_build or previous_candidate.arm_build)
+ )
+ return frappe.db.get_value("Deploy Candidate Build", build_name, "docker_image")
+
+ def _get_patch_app_updates(self, previous_candidate: DeployCandidate) -> list:
+ apps = []
+ previous_releases = {app.app: app.release for app in previous_candidate.apps}
+ for app in self.candidate.apps:
+ if app.release == previous_releases[app.app]:
+ continue
+ source: AppSource = frappe.get_doc("App Source", app.source)
+ apps.append(
+ {
+ "app": app.app,
+ "url": source.get_repo_url(),
+ "hash": frappe.db.get_value("App Release", app.release, "hash"),
+ }
+ )
+ return apps
+
+ @staticmethod
+ def process_run_patch_build(job: "AgentJob", response_data: "dict | None"):
+ request_data = json.loads(job.request_data)
+ build: DeployCandidateBuild = frappe.get_doc(
+ "Deploy Candidate Build", request_data["deploy_candidate_build"]
+ )
+ build._process_patch_build_job(job, request_data, response_data)
+
+ def _process_patch_build_job(self, job: "AgentJob", request_data: dict, response_data: "dict | None"):
+ """
+ Processes patch build job updates. Unlike `_process_run_build`, patch builds don't
+ stream docker build output — step statuses are synced directly from agent job step records
+ and also update the step output based on the output received
+ """
+ self._set_output_parsers()
+ self._sync_patch_build_step_statuses(job)
+ self.patch_build_output_parser.parse_and_update(job)
+
+ if self.has_remote_build_failed(job, {}):
+ self.handle_build_failure(exc=None, job=job)
+ else:
+ self._update_status_from_remote_build_job(job)
+ if self.status == Status.SUCCESS.value:
+ self.update_deploy_candidate_with_build()
+ self._create_platform_patch_build_if_required_and_deploy(
+ request_data.get("deploy_after_build", True)
+ )
+
+ self.correct_upload_step_status()
+
+ def _sync_patch_build_step_statuses(self, job: "AgentJob"):
+ """Update each patch build step's status from the corresponding agent job step."""
+ for agent_step_name, (stage_slug, step_slug) in PATCH_BUILD_STEP_MAP.items():
+ status = job.get_step_status(agent_step_name)
+ if status and (step := self.get_step(stage_slug, step_slug)):
+ step.status = status
+ # All other steps will have "Skipped" if one failed therefore break here
+ if status == "Failure":
+ break
+
+ self.save(ignore_version=True)
+
+ def _create_platform_patch_build_if_required_and_deploy(self, deploy_after_build: bool):
+ """Create a platform specific patch build if requirement enforced by the deploy candidate and deploy after build if required"""
+ requires_arm = self.candidate.requires_arm_build and not self.candidate.arm_build
+ requires_intel = self.candidate.requires_intel_build and not self.candidate.intel_build
+
+ if requires_arm or requires_intel:
+ platform = "arm64" if requires_arm else "x86_64"
+ frappe.get_doc(
+ {
+ "doctype": "Deploy Candidate Build",
+ "deploy_candidate": self.deploy_candidate,
+ "no_push": self.no_push,
+ "deploy_after_build": deploy_after_build,
+ "platform": platform,
+ "patch_build": True,
+ }
+ ).insert()
+ elif deploy_after_build:
+ self.create_deploy()
+
+ def run_patch_build(self):
+ """Ensure this is called when `run_build` in insert is set to False since that will use the older flow"""
+ # In case after some bypass or error this is triggered without patch build being possible
+ # We need to run a check here.
+ from press.press.doctype.release_group.release_group import (
+ _get_previous_candidate,
+ can_run_patch_build,
+ )
+
+ if not can_run_patch_build(self.group):
+ frappe.throw("Patch build cannot be run.")
+
+ previous_candidate = _get_previous_candidate(self.group)
+ self.set_status(Status.PREPARING, timestamp_field="build_start", commit=True)
+ self._set_output_parsers()
+ try:
+ self.send_patch_build_instructions(previous_candidate)
+ except Exception as e:
+ self.handle_build_failure(e)
+ return
+
def after_insert(self):
- if self.run_build and self.status != Status.SCHEDULED.value:
+ if self.patch_build:
+ self.set_status(Status.DRAFT)
+ self.run_patch_build()
+
+ elif self.run_build and self.status != Status.SCHEDULED.value:
self.set_status(Status.DRAFT)
self.pre_build()
diff --git a/press/press/doctype/press_settings/press_settings.json b/press/press/doctype/press_settings/press_settings.json
index 563e21c1c9f..5d009a238d1 100644
--- a/press/press/doctype/press_settings/press_settings.json
+++ b/press/press/doctype/press_settings/press_settings.json
@@ -116,6 +116,7 @@
"compress_app_cache",
"use_delta_builds",
"use_agent_job_callbacks",
+ "allow_patch_builds",
"auto_update_section",
"auto_update_queue_size",
"remote_files_section",
@@ -1771,8 +1772,8 @@
"fieldname": "enable_mcp",
"fieldtype": "Check",
"label": "Enable MCP"
- },
- {
+ },
+ {
"fieldname": "pulse_section",
"fieldtype": "Section Break",
"label": "Pulse"
@@ -1813,11 +1814,16 @@
"fieldname": "default_dedicated_server_site_warranty_quota",
"fieldtype": "Int",
"label": "Default Dedicated Server Supported Site Quota"
+ },
+ {
+ "fieldname": "allow_patch_builds",
+ "fieldtype": "Check",
+ "label": "Allow Patch Builds"
}
],
"issingle": 1,
"links": [],
- "modified": "2026-06-02 14:15:34.170170",
+ "modified": "2026-06-02 22:46:34.584687",
"modified_by": "Administrator",
"module": "Press",
"name": "Press Settings",
diff --git a/press/press/doctype/press_settings/press_settings.py b/press/press/doctype/press_settings/press_settings.py
index 517d3e18eeb..c89e486cc36 100644
--- a/press/press/doctype/press_settings/press_settings.py
+++ b/press/press/doctype/press_settings/press_settings.py
@@ -29,6 +29,7 @@ class PressSettings(Document):
agent_github_access_token: DF.Data | None
agent_repository_owner: DF.Data | None
agent_sentry_dsn: DF.Data | None
+ allow_patch_builds: DF.Check
app_include_script: DF.Data | None
asset_store_access_key: DF.Data | None
asset_store_bucket_name: DF.Data | None
diff --git a/press/press/doctype/release_group/release_group.py b/press/press/doctype/release_group/release_group.py
index 0e7cabe8750..2b517a2da22 100644
--- a/press/press/doctype/release_group/release_group.py
+++ b/press/press/doctype/release_group/release_group.py
@@ -897,6 +897,9 @@ def deploy_information(self):
out.last_deploy = self.last_dc_info
out.deploy_in_progress = self.deploy_in_progress
out.has_running_release_pipeline = self.has_running_release_pipeline
+ out.can_run_patch_build = can_run_patch_build(
+ self.name
+ ) # Don't show the button if the user can't run an instant build
if not out.deploy_in_progress and out.has_running_release_pipeline:
# Check if the deploy has finished and bench creation is underway.
out.bench_creation_underway = bool(
@@ -2105,3 +2108,73 @@ def get_flattened_app_sources(app_sources: list[str | list[str]]) -> list[str]:
else:
flattened_sources.append(source)
return flattened_sources
+
+
+def _get_previous_candidate(release_group: str) -> "DeployCandidate | None":
+ """Get previous candidate from the release group"""
+ last_active_build = frappe.db.get_value(
+ "Bench", {"group": release_group, "status": "Active"}, "build", order_by="creation desc"
+ )
+ if not last_active_build:
+ return None
+
+ deploy_candidate = frappe.db.get_value("Deploy Candidate Build", last_active_build, "deploy_candidate")
+ if not deploy_candidate:
+ return None
+
+ return frappe.get_doc("Deploy Candidate", deploy_candidate)
+
+
+def _has_active_benches(previous_candidate: "DeployCandidate") -> bool:
+ """Check if active benches are present in case intel and arm both
+ are present in previous candidate check for both benches"""
+ intel_bench = arm_bench = None
+ if previous_candidate.intel_build:
+ intel_bench = frappe.db.get_value(
+ "Bench", {"build": previous_candidate.intel_build, "status": "Active"}, "name"
+ )
+ if previous_candidate.arm_build:
+ arm_bench = frappe.db.get_value(
+ "Bench", {"build": previous_candidate.arm_build, "status": "Active"}, "name"
+ )
+
+ if not intel_bench and not arm_bench:
+ return False
+
+ if previous_candidate.intel_build and previous_candidate.arm_build and (not intel_bench or not arm_bench):
+ return False
+
+ return True
+
+
+def can_run_patch_build(release_group: str) -> bool:
+ if not frappe.db.get_single_value("Press Settings", "allow_patch_builds"):
+ return False
+
+ previous_candidate = _get_previous_candidate(release_group)
+ if not previous_candidate:
+ return False
+
+ if frappe.db.get_value("Release Group", release_group, "public"):
+ return False
+
+ rg: ReleaseGroup = frappe.get_doc("Release Group", release_group)
+ pc = previous_candidate
+
+ state_unchanged = (
+ # same apps in same order
+ [app.app for app in pc.apps] == [app.app for app in rg.apps]
+ # same source/branch per app
+ and {app.app: app.source for app in pc.apps} == {app.app: app.source for app in rg.apps}
+ # same system dependencies (e.g. Python, Node versions)
+ and {d.dependency: d.version for d in pc.dependencies}
+ == {d.dependency: d.version for d in rg.dependencies}
+ # same apt/pip packages
+ and {p.package_manager: p.package for p in pc.packages}
+ == {p.package_manager: p.package for p in rg.packages}
+ # same environment variables
+ and {ev.key: ev.value for ev in pc.environment_variables}
+ == {ev.key: ev.value for ev in rg.environment_variables}
+ )
+
+ return state_unchanged and _has_active_benches(pc)
diff --git a/press/press/doctype/release_group/test_can_run_patch_build.py b/press/press/doctype/release_group/test_can_run_patch_build.py
new file mode 100644
index 00000000000..43d5747c851
--- /dev/null
+++ b/press/press/doctype/release_group/test_can_run_patch_build.py
@@ -0,0 +1,173 @@
+# Copyright (c) 2026, Frappe and Contributors
+# See license.txt
+from __future__ import annotations
+
+from unittest.mock import Mock, patch
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+
+from press.press.doctype.agent_job.agent_job import AgentJob
+from press.press.doctype.app.test_app import create_test_app
+from press.press.doctype.app_source.test_app_source import create_test_app_source
+from press.press.doctype.release_group.release_group import can_run_patch_build
+from press.press.doctype.release_group.test_release_group import create_test_release_group
+from press.press.doctype.site.test_site import create_test_bench
+
+
+def _setup(apps=None, public=False):
+ """Create release group + active bench, set intel_build on the candidate."""
+ if apps is None:
+ apps = [create_test_app()]
+ rg = create_test_release_group(apps, public=public)
+ bench = create_test_bench(group=rg)
+ frappe.db.set_value("Deploy Candidate", bench.candidate, "intel_build", bench.build)
+ frappe.db.set_single_value("Press Settings", "allow_patch_builds", 1)
+ return rg, bench
+
+
+def _add_arm_build(rg, bench, create_bench=True):
+ """Attach an arm DCB to the candidate and optionally create an active arm bench."""
+ arm_dcb = frappe.get_doc(
+ {
+ "doctype": "Deploy Candidate Build",
+ "deploy_candidate": bench.candidate,
+ "group": rg.name,
+ "run_build": 0,
+ "status": "Success",
+ "platform": "arm64",
+ }
+ ).insert()
+ frappe.db.set_value("Deploy Candidate", bench.candidate, "arm_build", arm_dcb.name)
+ if not create_bench:
+ return arm_dcb
+ return frappe.get_doc(
+ {
+ "doctype": "Bench",
+ "name": f"Test ARM Bench {frappe.generate_hash(length=8)}",
+ "status": "Active",
+ "background_workers": 1,
+ "gunicorn_workers": 2,
+ "group": rg.name,
+ "candidate": bench.candidate,
+ "build": arm_dcb.name,
+ "server": bench.server,
+ "docker_image": frappe.mock("url"),
+ }
+ ).insert()
+
+
+@patch.object(AgentJob, "enqueue_http_request", new=Mock())
+class TestCanRunPatchBuild(FrappeTestCase):
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def test_returns_true_when_state_matches(self):
+ rg, _ = _setup()
+ self.assertTrue(can_run_patch_build(rg.name))
+
+ def test_returns_false_when_no_active_bench(self):
+ rg = create_test_release_group([create_test_app()])
+ self.assertFalse(can_run_patch_build(rg.name))
+
+ def test_returns_false_when_bench_archived(self):
+ rg, bench = _setup()
+ bench.db_set("status", "Archived")
+ self.assertFalse(can_run_patch_build(rg.name))
+
+ def test_returns_false_when_app_added(self):
+ rg, _ = _setup()
+ new_app = create_test_app("erpnext", "ERPNext")
+ new_source = create_test_app_source(rg.version, new_app)
+ rg.append("apps", {"app": new_app.name, "source": new_source.name})
+ rg.save()
+ self.assertFalse(can_run_patch_build(rg.name))
+
+ def test_returns_false_when_app_removed(self):
+ app1 = create_test_app()
+ app2 = create_test_app("erpnext", "ERPNext")
+ rg, _ = _setup(apps=[app1, app2])
+ rg.apps = [row for row in rg.apps if row.app != app2.name]
+ rg.save()
+ self.assertFalse(can_run_patch_build(rg.name))
+
+ def test_returns_false_when_app_source_changed(self):
+ app = create_test_app()
+ rg, _ = _setup(apps=[app])
+ new_source = create_test_app_source(rg.version, app, branch="develop")
+ rg.apps[0].source = new_source.name
+ rg.save()
+ self.assertFalse(can_run_patch_build(rg.name))
+
+ def test_returns_false_for_public_group(self):
+ rg, _ = _setup()
+ rg.db_set("public", 1)
+ self.assertFalse(can_run_patch_build(rg.name))
+
+ def test_returns_false_when_dependency_version_changed(self):
+ rg, _ = _setup()
+ rg.dependencies[0].version = "99.99"
+ rg.save()
+ self.assertFalse(can_run_patch_build(rg.name))
+
+ def test_returns_false_when_dependency_added(self):
+ rg, _ = _setup()
+ rg.append("dependencies", {"dependency": "WKHTMLTOPDF_VERSION", "version": "0.12.6"})
+ rg.save()
+ self.assertFalse(can_run_patch_build(rg.name))
+
+ def test_returns_false_when_package_added(self):
+ rg, _ = _setup()
+ rg.append("packages", {"package_manager": "apt", "package": "libmagic1"})
+ rg.save()
+ self.assertFalse(can_run_patch_build(rg.name))
+
+ def test_returns_false_when_package_removed(self):
+ app = create_test_app()
+ rg = create_test_release_group([app])
+ rg.append("packages", {"package_manager": "apt", "package": "libmagic1"})
+ rg.save()
+ bench = create_test_bench(group=rg)
+ frappe.db.set_value("Deploy Candidate", bench.candidate, "intel_build", bench.build)
+ rg.packages = []
+ rg.save()
+ self.assertFalse(can_run_patch_build(rg.name))
+
+ def test_returns_false_when_env_var_added(self):
+ rg, _ = _setup()
+ rg.append("environment_variables", {"key": "MY_VAR", "value": "foo", "internal": 0})
+ rg.save()
+ self.assertFalse(can_run_patch_build(rg.name))
+
+ def test_returns_false_when_env_var_removed(self):
+ app = create_test_app()
+ rg = create_test_release_group([app])
+ rg.append("environment_variables", {"key": "MY_VAR", "value": "foo", "internal": 0})
+ rg.save()
+ bench = create_test_bench(group=rg)
+ frappe.db.set_value("Deploy Candidate", bench.candidate, "intel_build", bench.build)
+ rg.environment_variables = []
+ rg.save()
+ self.assertFalse(can_run_patch_build(rg.name))
+
+ def test_returns_false_when_env_var_value_changed(self):
+ app = create_test_app()
+ rg = create_test_release_group([app])
+ rg.append("environment_variables", {"key": "MY_VAR", "value": "foo", "internal": 0})
+ rg.save()
+ bench = create_test_bench(group=rg)
+ frappe.db.set_value("Deploy Candidate", bench.candidate, "intel_build", bench.build)
+ rg.environment_variables[0].value = "bar"
+ rg.save()
+ self.assertFalse(can_run_patch_build(rg.name))
+
+ def test_returns_false_when_arm_bench_missing_for_dual_platform_candidate(self):
+ rg, bench = _setup()
+ _add_arm_build(rg, bench, create_bench=False)
+ # candidate has both intel_build + arm_build but no active arm bench
+ self.assertFalse(can_run_patch_build(rg.name))
+
+ def test_returns_true_when_both_platform_benches_active(self):
+ rg, bench = _setup()
+ _add_arm_build(rg, bench, create_bench=True)
+ self.assertTrue(can_run_patch_build(rg.name))
diff --git a/press/press/doctype/release_pipeline/release_pipeline.py b/press/press/doctype/release_pipeline/release_pipeline.py
index ab819f79728..3610c1afcf7 100644
--- a/press/press/doctype/release_pipeline/release_pipeline.py
+++ b/press/press/doctype/release_pipeline/release_pipeline.py
@@ -267,19 +267,24 @@ def create_deploy_candidate(
apps: list[dict[str, str]],
sites: list[dict[str, Any]],
run_will_fail_check: bool = False,
- create_deploy: bool = False,
+ trigger_patch_deploy: bool = False,
) -> str:
"""Create a Deploy Candidate for the release group."""
assert isinstance(self.release_group, str)
bench_update: BenchUpdate = get_bench_update(
self.release_group, apps, sites, is_inplace_update=False, ignore_permissions=True
)
- return bench_update.deploy(
- run_will_fail_check=run_will_fail_check,
- validate_pre_candidate_checks=False,
- create_build=create_deploy,
- ignore_permissions=True,
- )
+ # This is only to handle the suspended builds + patch build case.
+ try:
+ return bench_update.deploy(
+ run_will_fail_check=run_will_fail_check,
+ validate_pre_candidate_checks=False,
+ create_build=False,
+ ignore_permissions=True,
+ trigger_patch_deploy=trigger_patch_deploy,
+ )
+ except Exception as e:
+ raise ReleasePipelineFailure(f"Failed to create deploy candidate: {e!s}") from e
@task(queue=_get_task_execution_queue())
def initiate_pre_build_validations(self, deploy_candidate: str) -> str:
@@ -290,6 +295,17 @@ def initiate_pre_build_validations(self, deploy_candidate: str) -> str:
)
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"]
+
def _get_required_build_count(self, deploy_candidate: str) -> int:
"""Get the number of builds required for this deploy, as we can have arm & intel build for the same deploy candidate"""
intel_build, arm_build = frappe.db.get_value(
@@ -357,16 +373,19 @@ def _get_latest_retried_build(self, deploy_candidate_build: str) -> str:
def monitor_build_success(self, deploy_candidate_build: str):
"""Monitor build till terminal state."""
deploy_candidate_build = self._get_latest_retried_build(deploy_candidate_build)
- deploy_candidate_build_status = frappe.db.get_value(
- "Deploy Candidate Build", deploy_candidate_build, "status"
+ deploy_candidate_build_doc: DeployCandidateBuild = frappe.get_doc(
+ "Deploy Candidate Build", deploy_candidate_build
)
- if deploy_candidate_build_status == "Success":
+ if deploy_candidate_build_doc.status == "Success":
return # Remote Build succeeded can mark as success and proceed
- if deploy_candidate_build_status == "Failure":
+ if deploy_candidate_build_doc.status == "Failure":
# This will raise and enqueue the function again in case there are scheduled retries for the build
- self._check_for_scheduled_build_retries(deploy_candidate_build)
+ # In case of patch builds we don't retry anyways therefore ignore that check here
+ if not deploy_candidate_build_doc.patch_build:
+ self._check_for_scheduled_build_retries(deploy_candidate_build)
+
raise ReleasePipelineFailure(
f"Remote build failed for Deploy Candidate Build {deploy_candidate_build}. Please check the build logs for more details."
)
@@ -591,7 +610,7 @@ def auto_update_bench_dependency_versions(self, deploy_candidate: str):
)
@task(queue=_get_task_execution_queue())
- def prepare_deployment(self, apps, sites, run_will_fail_check) -> tuple[str, str]:
+ def prepare_deployment(self, apps, sites, run_will_fail_check, trigger_patch_deploy) -> tuple[str, str]:
"""Creates the candidate and returns the primary build name."""
auto_upgrade_dependencies = frappe.db.get_single_value(
"Press Settings",
@@ -603,14 +622,18 @@ def prepare_deployment(self, apps, sites, run_will_fail_check) -> tuple[str, str
apps=apps,
sites=sites,
run_will_fail_check=run_will_fail_check,
- create_deploy=False,
+ trigger_patch_deploy=trigger_patch_deploy,
)
self.add_implicit_app_dependencies(deploy_candidate)
if auto_upgrade_dependencies:
self.auto_update_bench_dependency_versions(deploy_candidate)
- primary_build = self.initiate_pre_build_validations(deploy_candidate)
+ primary_build = (
+ self.initiate_pre_build_validations(deploy_candidate)
+ if not trigger_patch_deploy
+ else self.initiate_patch_deploy(deploy_candidate)
+ )
return deploy_candidate, primary_build
except (frappe.ValidationError, GithubFetchError) as e:
@@ -692,6 +715,7 @@ def create_release(
apps: list[dict[str, str]],
sites: list[dict[str, Any]],
run_will_fail_check: bool = False,
+ trigger_patch_deploy: bool = False,
):
"""Orchestrates the release process from validation to bench creation with recursive monitoring and retry handling"""
if not self.workflow:
@@ -703,7 +727,9 @@ def create_release(
self.run_pre_release_checks(apps)
# 2. Initialization Phase
- deploy_candidate, primary_build = self.prepare_deployment(apps, sites, run_will_fail_check)
+ deploy_candidate, primary_build = self.prepare_deployment(
+ apps, sites, run_will_fail_check, trigger_patch_deploy
+ )
# 3. Monitoring Phase (Handles 1 or 2 builds)
self.orchestrate_build_monitoring(deploy_candidate, primary_build)