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 @@ 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)