Repository navigation
feat: add independent bench and site cloning #557
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
+4,911
−144
Open
Changes from all commits
Commits
Show all changes
51 commits
Select commit
Hold shift + click to select a range
a5bb6d8
feat: fork development benches into isolated agent workspaces
BreadGenie deea9fe
feat: add separate bench and site cloning with Admin flows
BreadGenie 4d47c36
feat: clone origin default branches with UI and CLI overrides
BreadGenie 08f2ea4
feat: add collapsible app branch selectors to clone dialog
BreadGenie a61857a
fix: remove site notice from clone bench dialog
BreadGenie dfe9ff0
fix: preserve isolation and compatibility when cloning benches and sites
BreadGenie e565bd6
test: remove redundant clone workflow checks
BreadGenie d94a702
fix: resolve clone workflow type errors caught by CI
BreadGenie 3cc85f4
fix: protect clone credentials and roll back failed site clones
BreadGenie 1389a3a
fix: remove failed clone host and nginx routes
BreadGenie 97ba12b
fix: keep site action menus on the sites page
BreadGenie 5f3956b
perf: reuse clone artifacts and overlap database imports
BreadGenie cf7726d
perf: parallelize clone schema and filesystem preparation
BreadGenie 233972d
fix: recover clone caches and report concurrent failures
BreadGenie 89ed686
perf: batch editable installs for cloned benches
BreadGenie 12d538e
perf: seed branch dependencies before yarn reconciliation
BreadGenie 7e99f68
perf: parallelize app cloning and reduce fingerprint IO
BreadGenie 7a9d739
chore: merge latest develop into agent workspaces
BreadGenie 09887fe
perf(git): Reflink independent repository objects
BreadGenie b9268a1
Merge branch 'develop' into feat/agent-workspaces
BreadGenie 1d9a4d4
perf(dependencies): Cache reconciled Node installs
BreadGenie 5d50916
ci(unit): Refresh package index before installation
BreadGenie 734ea1a
fix(cloning): Address review findings
BreadGenie bbd7f84
refactor(cloning): Follow validation and path conventions
BreadGenie 2002346
refactor(cloning): Keep functions within complexity limits
BreadGenie 18b901a
chore(branch): Merge latest develop
BreadGenie 790e90a
fix(cloning): Preserve builds and shared Redis lifetimes
BreadGenie 4c27357
fix(runtime): Release leases before shutdown locks
BreadGenie ce5544d
perf(cloning): Copy shallow Git objects directly
BreadGenie 1801e38
chore(branch): Merge latest develop
BreadGenie a3ca293
fix(cloning): Preserve concurrent bench settings
BreadGenie e8756c0
chore(branch): Merge latest develop
BreadGenie 96cfaf7
fix: tighten clone dialog typography and source spacing
BreadGenie 6d6ab23
fix: address clone review findings and refine dialog labels
BreadGenie 1a5fc23
refactor: separate bench and site cloning from deferred forks
BreadGenie 6bfe1b9
Merge latest develop into feat/agent-workspaces
BreadGenie 4367e69
fix: preserve site engines and address cloning review findings
BreadGenie 3190ae5
fix: preserve installation failures and clean up started site imports
BreadGenie 62dc16f
Merge latest develop and resolve cloud settings conflict
BreadGenie 93e586f
fix: address clone task arguments and cleanup review findings
BreadGenie 1da7989
refactor: separate CLI value argument registration
BreadGenie 2715879
fix: improve clone credentials and setup failure reporting
BreadGenie c7b2c63
fix: validate clone databases and preserve cache reuse across tasks
BreadGenie c20701b
Merge develop with pilot dev workspaces
BreadGenie 3eb5ead
feat: seed pilot dev setup from an existing site
BreadGenie d30913e
refactor: move dev site seeding into a follow-up PR
BreadGenie b3f8da4
fix: isolate clone cleanup failures and validate cached asset paths
BreadGenie 65fbdc9
refactor: follow boolean naming for clone cleanup
BreadGenie dfe33c0
Merge remote-tracking branch 'origin/develop' into feat/agent-workspaces
BreadGenie 1287414
docs: include cloning routes in generated API reference
BreadGenie 0301d7f
fix: harden cloning caches, repository copies and upload links
BreadGenie File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import secrets | ||
| from pathlib import Path | ||
|
|
||
| from flask import current_app, jsonify, request | ||
|
|
||
| from admin.backend.api.responses import accepted_task_response, error_response | ||
| from admin.backend.api.v1.benches.support import BENCH_NAME_RE, guard_bench_management, target_bench_dir | ||
| from admin.backend.api.v1.sites.shared import host_resource_key, task_failure | ||
| from admin.backend.providers.apps import AppProvider | ||
| from pilot.config import BenchConfig | ||
| from pilot.core.bench import Bench | ||
| from pilot.core.bench.cloning.branches import validate_branches | ||
| from pilot.core.site.clone import SiteClone | ||
| from pilot.exceptions import BenchError, TaskConflictError | ||
| from pilot.internal.validators import validate_site_name | ||
| from pilot.tasks.clone_bench import CloneBenchTask | ||
| from pilot.tasks.clone_site import CloneSiteTask | ||
|
|
||
|
|
||
| def clone_branch_options(name: str): | ||
| root = Path(current_app.config["BENCH_ROOT"]) | ||
| if not BENCH_NAME_RE.fullmatch(name): | ||
| return error_response("invalid_bench_name", "Invalid bench name.", 422) | ||
| try: | ||
| path = target_bench_dir(root, name) | ||
| if not (path / "bench.toml").is_file(): | ||
| return error_response("bench_not_found", "Source bench not found.", 404) | ||
| source = Bench(path) | ||
| apps = source.get_clone_branch_options() | ||
| provider = AppProvider(path) | ||
| for app in apps: | ||
| title, _description = provider.get_pyproject_meta(path / "apps" / app["name"], app["name"]) | ||
| app["title"] = title or app["name"] | ||
| return jsonify({"apps": apps}) | ||
| except BenchError as error: | ||
| return error_response("clone_branches_unavailable", str(error), 422) | ||
| except ValueError: | ||
| return error_response("bench_not_found", "Source bench not found.", 404) | ||
| except Exception: | ||
| return error_response("clone_branches_unavailable", "Could not load app branches.", 503) | ||
|
|
||
|
|
||
| def get_clone_bench_name(name: str, data: dict) -> str: | ||
| if not isinstance(data.get("name"), str): | ||
| raise BenchError("Enter a destination bench name.") | ||
| target = data["name"].strip() | ||
| if not BENCH_NAME_RE.fullmatch(name) or not BENCH_NAME_RE.fullmatch(target): | ||
| raise BenchError("Invalid bench name.") | ||
| return target | ||
|
|
||
|
|
||
| def clone_bench(name: str): | ||
| root = Path(current_app.config["BENCH_ROOT"]) | ||
| data = request.get_json(silent=True) | ||
| if not isinstance(data, dict): | ||
| return error_response("invalid_clone", "Enter a destination bench name.", 422) | ||
| try: | ||
| target = get_clone_bench_name(name, data) | ||
| branch = data.get("branch", "default") | ||
| app_branches = data.get("app_branches", {}) | ||
| validate_branches(branch, app_branches) | ||
| BenchConfig.default(target).validate() | ||
| source = Bench(target_bench_dir(root, name)) | ||
| args = dict(source_bench=source.path.name, name=target, branch=branch, app_branches=app_branches) | ||
| existing = Bench(root).tasks.find_idempotent_task( | ||
| "clone-bench", args, request.headers.get("Idempotency-Key") | ||
| ) | ||
| if existing: | ||
| return accepted_task_response(root, existing) | ||
| if target_bench_dir(root, target).exists(): | ||
| return error_response("bench_exists", "Destination bench already exists.", 409) | ||
| task_id = CloneBenchTask.queue( | ||
| Bench(root), | ||
| source_bench=source.path.name, | ||
| name=target, | ||
| branch=branch, | ||
| app_branches=app_branches, | ||
| idempotency_key=request.headers.get("Idempotency-Key"), | ||
| resource_key=[f"bench:{name}", f"bench:{target}"], | ||
| ) | ||
| except TaskConflictError as error: | ||
| return task_failure(error) | ||
| except BenchError as error: | ||
| return error_response("invalid_clone", str(error), 422) | ||
| except Exception as error: | ||
| return task_failure(error) | ||
| return accepted_task_response(root, task_id) | ||
|
|
||
|
|
||
| def get_clone_site_names(name: str, data) -> tuple[str, str]: | ||
| if not isinstance(data, dict) or not all( | ||
| isinstance(data.get(key), str) for key in ("name", "target_bench") | ||
| ): | ||
| raise BenchError("Enter a site name and destination bench.") | ||
| target = data["name"].strip() | ||
| bench_name = data["target_bench"].strip() | ||
| if validate_site_name(name) or validate_site_name(target) or not BENCH_NAME_RE.fullmatch(bench_name): | ||
| raise BenchError("Invalid site or bench name.") | ||
| return target, bench_name | ||
|
|
||
|
|
||
| def clone_site(name: str): | ||
| root = Path(current_app.config["BENCH_ROOT"]) | ||
| data = request.get_json(silent=True) | ||
| try: | ||
| target, bench_name = get_clone_site_names(name, data) | ||
| except BenchError as error: | ||
| return error_response("invalid_clone", str(error), 422) | ||
| if bench_name != root.name and (response := guard_bench_management()): | ||
|
BreadGenie marked this conversation as resolved.
Resolved
|
||
| return response | ||
|
BreadGenie marked this conversation as resolved.
Resolved
|
||
| try: | ||
| bench = Bench(root) | ||
| destination = Bench(target_bench_dir(root, bench_name)) | ||
| password = secrets.token_urlsafe(24) | ||
| args = dict(site=name, name=target, target_bench=bench_name, admin_password=password) | ||
| existing = bench.tasks.find_idempotent_task( | ||
| "clone-site", args, request.headers.get("Idempotency-Key") | ||
| ) | ||
| if existing: | ||
| return accepted_task_response(root, existing) | ||
| SiteClone(bench.site(name), destination, target, password).validate() | ||
|
BreadGenie marked this conversation as resolved.
Resolved
|
||
| task_id = CloneSiteTask.queue( | ||
| bench, | ||
| site=name, | ||
| name=target, | ||
| target_bench=bench_name, | ||
| admin_password=password, | ||
| idempotency_key=request.headers.get("Idempotency-Key"), | ||
| resource_key=[f"site:{name.lower()}", f"bench:{bench_name}", host_resource_key(target)], | ||
| ) | ||
|
BreadGenie marked this conversation as resolved.
Resolved
|
||
| except TaskConflictError as error: | ||
| return task_failure(error) | ||
| except BenchError as error: | ||
| return error_response("invalid_clone", str(error), 422) | ||
| except Exception as error: | ||
| return task_failure(error) | ||
| return accepted_task_response(root, task_id) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.