buildify-cli 1.0.0__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/PKG-INFO +2 -2
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/README.md +1 -1
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/pyproject.toml +1 -1
- buildify_cli-1.1.0/src/buildify_cli/__init__.py +1 -0
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/src/buildify_cli/cli.py +159 -3
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/src/buildify_cli/client.py +86 -0
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/src/buildify_cli/config.py +2 -2
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/src/buildify_cli/publish_view.py +71 -0
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/src/buildify_cli/usage.md +44 -3
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/src/buildify_cli.egg-info/PKG-INFO +2 -2
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/src/buildify_cli.egg-info/SOURCES.txt +2 -1
- buildify_cli-1.1.0/tests/test_history_view.py +57 -0
- buildify_cli-1.0.0/src/buildify_cli/__init__.py +0 -1
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/setup.cfg +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/src/buildify_cli/__main__.py +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/src/buildify_cli/credential_form.py +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/src/buildify_cli/exit_codes.py +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/src/buildify_cli/output.py +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/src/buildify_cli.egg-info/dependency_links.txt +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/src/buildify_cli.egg-info/entry_points.txt +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/src/buildify_cli.egg-info/requires.txt +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/src/buildify_cli.egg-info/top_level.txt +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/tests/test_auth_config.py +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.1.0}/tests/test_credential_form.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: buildify-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: CLI for the Buildify open API: discover bundle nodes, author and test workflows (machine Bearer auth)
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -66,7 +66,7 @@ interactive prompt. `config set-key api_key` still updates the current profile,
|
|
|
66
66
|
| `key` | `test` |
|
|
67
67
|
| `bundle` | `list`, `nodes`, `node-properties`, `node-relations`, `doc`, `cred-types`, `cred-properties` |
|
|
68
68
|
| `project` | `list`, `create`, `get`, `credentials`, `create-credential`, `workers` |
|
|
69
|
-
| `workflow` | `list`, `create`, `get-draft`, `save-draft`, `validate`, `test-run`, `cancel-test`, `deploy`, `published`, `deployment` |
|
|
69
|
+
| `workflow` | `list`, `create`, `get-draft`, `save-draft`, `validate`, `test-run`, `cancel-test`, `deploy`, `published`, `deployments`, `deployment`, `versions`, `get-version` |
|
|
70
70
|
| `config` | `init`, `set-key`, `add-profile`, `use`, `profiles`, `remove-profile`, `show` |
|
|
71
71
|
|
|
72
72
|
Every command accepts `--json` for a single-line envelope
|
|
@@ -53,7 +53,7 @@ interactive prompt. `config set-key api_key` still updates the current profile,
|
|
|
53
53
|
| `key` | `test` |
|
|
54
54
|
| `bundle` | `list`, `nodes`, `node-properties`, `node-relations`, `doc`, `cred-types`, `cred-properties` |
|
|
55
55
|
| `project` | `list`, `create`, `get`, `credentials`, `create-credential`, `workers` |
|
|
56
|
-
| `workflow` | `list`, `create`, `get-draft`, `save-draft`, `validate`, `test-run`, `cancel-test`, `deploy`, `published`, `deployment` |
|
|
56
|
+
| `workflow` | `list`, `create`, `get-draft`, `save-draft`, `validate`, `test-run`, `cancel-test`, `deploy`, `published`, `deployments`, `deployment`, `versions`, `get-version` |
|
|
57
57
|
| `config` | `init`, `set-key`, `add-profile`, `use`, `profiles`, `remove-profile`, `show` |
|
|
58
58
|
|
|
59
59
|
Every command accepts `--json` for a single-line envelope
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "buildify-cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.1.0"
|
|
8
8
|
description = "CLI for the Buildify open API: discover bundle nodes, author and test workflows (machine Bearer auth)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.1.0"
|
|
@@ -47,7 +47,12 @@ from buildify_cli.credential_form import (
|
|
|
47
47
|
prompt_payload,
|
|
48
48
|
)
|
|
49
49
|
from buildify_cli.output import human_err, is_tty, print_json
|
|
50
|
-
from buildify_cli.publish_view import
|
|
50
|
+
from buildify_cli.publish_view import (
|
|
51
|
+
format_deployment_records,
|
|
52
|
+
format_publish_report,
|
|
53
|
+
format_version_records,
|
|
54
|
+
is_publish_payload,
|
|
55
|
+
)
|
|
51
56
|
|
|
52
57
|
app = typer.Typer(
|
|
53
58
|
help="Buildify open API CLI: discover nodes, author workflows, validate, test run and deploy.",
|
|
@@ -62,7 +67,7 @@ app = typer.Typer(
|
|
|
62
67
|
" bundle Bundle / node discovery: list, nodes, node-properties, node-relations, doc, cred-types, cred-properties\n"
|
|
63
68
|
" project list, create, get, credentials, create-credential, workers\n"
|
|
64
69
|
" workflow list, create, get-draft, save-draft, validate, test-run, cancel-test,\n"
|
|
65
|
-
" deploy, published, deployment\n"
|
|
70
|
+
" deploy, published, deployments, deployment, versions, get-version\n"
|
|
66
71
|
" config Manage the [openapi] section of ~/.buildifyrc\n"
|
|
67
72
|
" version Print CLI version\n"
|
|
68
73
|
" help Markdown reference on stdout (for AI/agents)\n\n"
|
|
@@ -248,6 +253,15 @@ def _emit_network_error(ctx: typer.Context, exc: BaseException) -> int:
|
|
|
248
253
|
return ec.NETWORK
|
|
249
254
|
|
|
250
255
|
|
|
256
|
+
def _show_page(ctx: typer.Context, data: Any, formatter) -> None:
|
|
257
|
+
"""Pretty-print a page for humans; `--json` keeps the server payload."""
|
|
258
|
+
if _cfg(ctx).json_output:
|
|
259
|
+
print_json(ok=True, code=200, msg="ok", http_status=200, data=data)
|
|
260
|
+
else:
|
|
261
|
+
typer.echo(formatter(data))
|
|
262
|
+
raise typer.Exit(ec.OK)
|
|
263
|
+
|
|
264
|
+
|
|
251
265
|
def _call(ctx: typer.Context, fn, *args, **kwargs) -> None:
|
|
252
266
|
"""Run one client call and exit with the mapped code."""
|
|
253
267
|
try:
|
|
@@ -462,6 +476,25 @@ def cmd_project_get(ctx: typer.Context, project: str = _OPT_PROJECT) -> None:
|
|
|
462
476
|
_call(ctx, c.project_get, project)
|
|
463
477
|
|
|
464
478
|
|
|
479
|
+
@project_app.command("update")
|
|
480
|
+
def cmd_project_update(
|
|
481
|
+
ctx: typer.Context,
|
|
482
|
+
project: str = _OPT_PROJECT,
|
|
483
|
+
name: str = typer.Option(..., "--name", "-n", help="New project name."),
|
|
484
|
+
remark: Optional[str] = typer.Option(None, "--remark", help="New description. Omit to keep the current one."),
|
|
485
|
+
) -> None:
|
|
486
|
+
"""Rename a project or change its description."""
|
|
487
|
+
with _client(ctx) as c:
|
|
488
|
+
_call(ctx, c.project_update, project, name, remark)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@project_app.command("delete")
|
|
492
|
+
def cmd_project_delete(ctx: typer.Context, project: str = _OPT_PROJECT) -> None:
|
|
493
|
+
"""Delete a project that has no workflows. Confirm with the user first."""
|
|
494
|
+
with _client(ctx) as c:
|
|
495
|
+
_call(ctx, c.project_delete, project)
|
|
496
|
+
|
|
497
|
+
|
|
465
498
|
@project_app.command("credentials")
|
|
466
499
|
def cmd_project_credentials(
|
|
467
500
|
ctx: typer.Context,
|
|
@@ -688,6 +721,74 @@ def cmd_flow_create(
|
|
|
688
721
|
_call(ctx, c.flow_create, project, workflow_name=name, worker_ids=list(worker), remark=remark)
|
|
689
722
|
|
|
690
723
|
|
|
724
|
+
_OPT_WORKERS = typer.Option(
|
|
725
|
+
..., "--worker", help="Worker public id; repeat for multiple. See `buildify project workers`."
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
@flow_app.command("update")
|
|
730
|
+
def cmd_flow_update(
|
|
731
|
+
ctx: typer.Context,
|
|
732
|
+
project: str = _OPT_PROJECT,
|
|
733
|
+
flow: str = _OPT_FLOW,
|
|
734
|
+
name: str = typer.Option(..., "--name", "-n", help="New workflow name."),
|
|
735
|
+
remark: Optional[str] = typer.Option(None, "--remark", help="New description. Omit to keep the current one."),
|
|
736
|
+
) -> None:
|
|
737
|
+
"""Rename a workflow or change its description."""
|
|
738
|
+
with _client(ctx) as c:
|
|
739
|
+
_call(ctx, c.flow_update, project, flow, workflow_name=name, remark=remark)
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
@flow_app.command("delete")
|
|
743
|
+
def cmd_flow_delete(ctx: typer.Context, project: str = _OPT_PROJECT, flow: str = _OPT_FLOW) -> None:
|
|
744
|
+
"""Delete a workflow that has no bound servers. Confirm with the user first."""
|
|
745
|
+
with _client(ctx) as c:
|
|
746
|
+
_call(ctx, c.flow_delete, project, flow)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
@flow_app.command("workers")
|
|
750
|
+
def cmd_flow_workers(ctx: typer.Context, project: str = _OPT_PROJECT, flow: str = _OPT_FLOW) -> None:
|
|
751
|
+
"""Servers bound to a workflow, including ones waiting to be removed."""
|
|
752
|
+
with _client(ctx) as c:
|
|
753
|
+
_call(ctx, c.flow_workers, project, flow)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
@flow_app.command("assign-workers")
|
|
757
|
+
def cmd_flow_assign_workers(
|
|
758
|
+
ctx: typer.Context,
|
|
759
|
+
project: str = _OPT_PROJECT,
|
|
760
|
+
flow: str = _OPT_FLOW,
|
|
761
|
+
worker: list[str] = _OPT_WORKERS,
|
|
762
|
+
) -> None:
|
|
763
|
+
"""Bind extra servers. A published flow is deployed to the new servers."""
|
|
764
|
+
with _client(ctx) as c:
|
|
765
|
+
_call(ctx, c.flow_assign_workers, project, flow, list(worker))
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
@flow_app.command("unassign-workers")
|
|
769
|
+
def cmd_flow_unassign_workers(
|
|
770
|
+
ctx: typer.Context,
|
|
771
|
+
project: str = _OPT_PROJECT,
|
|
772
|
+
flow: str = _OPT_FLOW,
|
|
773
|
+
worker: list[str] = _OPT_WORKERS,
|
|
774
|
+
) -> None:
|
|
775
|
+
"""Unbind servers. A published flow is undeployed and removed after the server confirms."""
|
|
776
|
+
with _client(ctx) as c:
|
|
777
|
+
_call(ctx, c.flow_unassign_workers, project, flow, list(worker))
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
@flow_app.command("force-unassign-workers")
|
|
781
|
+
def cmd_flow_force_unassign_workers(
|
|
782
|
+
ctx: typer.Context,
|
|
783
|
+
project: str = _OPT_PROJECT,
|
|
784
|
+
flow: str = _OPT_FLOW,
|
|
785
|
+
worker: list[str] = _OPT_WORKERS,
|
|
786
|
+
) -> None:
|
|
787
|
+
"""Drop server bindings immediately. Use when the server is gone or a normal unassign is stuck."""
|
|
788
|
+
with _client(ctx) as c:
|
|
789
|
+
_call(ctx, c.flow_force_unassign_workers, project, flow, list(worker))
|
|
790
|
+
|
|
791
|
+
|
|
691
792
|
@flow_app.command("get-draft")
|
|
692
793
|
def cmd_flow_get_draft(ctx: typer.Context, project: str = _OPT_PROJECT, flow: str = _OPT_FLOW) -> None:
|
|
693
794
|
"""Draft canvas JSON plus the optimistic-lock `version` needed to save it back."""
|
|
@@ -901,6 +1002,35 @@ def cmd_flow_published(ctx: typer.Context, project: str = _OPT_PROJECT, flow: st
|
|
|
901
1002
|
_call(ctx, c.flow_published, project, flow)
|
|
902
1003
|
|
|
903
1004
|
|
|
1005
|
+
@flow_app.command("deployments")
|
|
1006
|
+
def cmd_flow_deployments(
|
|
1007
|
+
ctx: typer.Context,
|
|
1008
|
+
project: str = _OPT_PROJECT,
|
|
1009
|
+
flow: str = _OPT_FLOW,
|
|
1010
|
+
deployment: Optional[str] = typer.Option(
|
|
1011
|
+
None, "--deployment", "-d", help="Only this deployment public id (deploy_...)."
|
|
1012
|
+
),
|
|
1013
|
+
page: int = typer.Option(1, "--page", min=1, help="Page number, starting at 1."),
|
|
1014
|
+
page_size: int = typer.Option(10, "--page-size", min=1, max=100, help="Page size, at most 100."),
|
|
1015
|
+
begin: Optional[str] = typer.Option(None, "--begin", help="Created at or after, yyyy-MM-dd HH:mm:ss."),
|
|
1016
|
+
end: Optional[str] = typer.Option(None, "--end", help="Created at or before, yyyy-MM-dd HH:mm:ss."),
|
|
1017
|
+
) -> None:
|
|
1018
|
+
"""List deployment records for a flow (newest first). Requires view_deployment."""
|
|
1019
|
+
with _client(ctx) as c:
|
|
1020
|
+
data = _fetch(
|
|
1021
|
+
ctx,
|
|
1022
|
+
c.flow_deployments,
|
|
1023
|
+
project,
|
|
1024
|
+
flow,
|
|
1025
|
+
deployment_id=deployment,
|
|
1026
|
+
page=page,
|
|
1027
|
+
page_size=page_size,
|
|
1028
|
+
begin_time=begin,
|
|
1029
|
+
end_time=end,
|
|
1030
|
+
)
|
|
1031
|
+
_show_page(ctx, data, format_deployment_records)
|
|
1032
|
+
|
|
1033
|
+
|
|
904
1034
|
@flow_app.command("deployment")
|
|
905
1035
|
def cmd_flow_deployment(
|
|
906
1036
|
ctx: typer.Context,
|
|
@@ -908,11 +1038,37 @@ def cmd_flow_deployment(
|
|
|
908
1038
|
flow: str = _OPT_FLOW,
|
|
909
1039
|
deployment: str = typer.Option(..., "--deployment", "-d", help="Deployment public id (deploy_...)."),
|
|
910
1040
|
) -> None:
|
|
911
|
-
"""One deployment
|
|
1041
|
+
"""One deployment record: counts, per-server tasks, and the publish summary."""
|
|
912
1042
|
with _client(ctx) as c:
|
|
913
1043
|
_call(ctx, c.flow_deployment, project, flow, deployment)
|
|
914
1044
|
|
|
915
1045
|
|
|
1046
|
+
@flow_app.command("versions")
|
|
1047
|
+
def cmd_flow_versions(
|
|
1048
|
+
ctx: typer.Context,
|
|
1049
|
+
project: str = _OPT_PROJECT,
|
|
1050
|
+
flow: str = _OPT_FLOW,
|
|
1051
|
+
page: int = typer.Option(1, "--page", min=1, help="Page number, starting at 1."),
|
|
1052
|
+
page_size: int = typer.Option(10, "--page-size", min=1, max=100, help="Page size, at most 100."),
|
|
1053
|
+
) -> None:
|
|
1054
|
+
"""List historical versions (newest first), including the draft. Requires view_version."""
|
|
1055
|
+
with _client(ctx) as c:
|
|
1056
|
+
data = _fetch(ctx, c.flow_versions, project, flow, page=page, page_size=page_size)
|
|
1057
|
+
_show_page(ctx, data, format_version_records)
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
@flow_app.command("get-version")
|
|
1061
|
+
def cmd_flow_get_version(
|
|
1062
|
+
ctx: typer.Context,
|
|
1063
|
+
project: str = _OPT_PROJECT,
|
|
1064
|
+
flow: str = _OPT_FLOW,
|
|
1065
|
+
version_id: str = typer.Option(..., "--version-id", help="Version public id (v-...)."),
|
|
1066
|
+
) -> None:
|
|
1067
|
+
"""Canvas JSON of one historical version. Read-only; not a draft lock. Requires view_version."""
|
|
1068
|
+
with _client(ctx) as c:
|
|
1069
|
+
_call(ctx, c.flow_version, project, flow, version_id)
|
|
1070
|
+
|
|
1071
|
+
|
|
916
1072
|
# --- config ---
|
|
917
1073
|
config_app = typer.Typer(help="Write/read the openapi section of ~/.buildifyrc (overridden by env at runtime).")
|
|
918
1074
|
app.add_typer(config_app, name="config")
|
|
@@ -116,6 +116,15 @@ class OpenApiClient:
|
|
|
116
116
|
def project_get(self, project_id: str) -> tuple[int, Any]:
|
|
117
117
|
return self._get(f"{API_PREFIX}/projects/{project_id}")
|
|
118
118
|
|
|
119
|
+
def project_update(self, project_id: str, name: str, remark: Optional[str]) -> tuple[int, Any]:
|
|
120
|
+
payload: dict[str, Any] = {"projectName": name}
|
|
121
|
+
if remark is not None:
|
|
122
|
+
payload["remark"] = remark
|
|
123
|
+
return self._post(f"{API_PREFIX}/projects/{project_id}", payload)
|
|
124
|
+
|
|
125
|
+
def project_delete(self, project_id: str) -> tuple[int, Any]:
|
|
126
|
+
return self._delete(f"{API_PREFIX}/projects/{project_id}")
|
|
127
|
+
|
|
119
128
|
def project_credentials(self, project_id: str, bundle_name: Optional[str] = None) -> tuple[int, Any]:
|
|
120
129
|
return self._get(
|
|
121
130
|
f"{API_PREFIX}/projects/{project_id}/credentials",
|
|
@@ -171,6 +180,43 @@ class OpenApiClient:
|
|
|
171
180
|
{"workflowName": workflow_name, "workerIds": worker_ids, "remark": remark},
|
|
172
181
|
)
|
|
173
182
|
|
|
183
|
+
def flow_update(
|
|
184
|
+
self,
|
|
185
|
+
project_id: str,
|
|
186
|
+
workflow_id: str,
|
|
187
|
+
*,
|
|
188
|
+
workflow_name: str,
|
|
189
|
+
remark: Optional[str],
|
|
190
|
+
) -> tuple[int, Any]:
|
|
191
|
+
payload: dict[str, Any] = {"workflowName": workflow_name}
|
|
192
|
+
if remark is not None:
|
|
193
|
+
payload["remark"] = remark
|
|
194
|
+
return self._post(f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/settings", payload)
|
|
195
|
+
|
|
196
|
+
def flow_delete(self, project_id: str, workflow_id: str) -> tuple[int, Any]:
|
|
197
|
+
return self._delete(f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}")
|
|
198
|
+
|
|
199
|
+
def flow_workers(self, project_id: str, workflow_id: str) -> tuple[int, Any]:
|
|
200
|
+
return self._get(f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/workers")
|
|
201
|
+
|
|
202
|
+
def flow_assign_workers(self, project_id: str, workflow_id: str, worker_ids: list[str]) -> tuple[int, Any]:
|
|
203
|
+
return self._post(
|
|
204
|
+
f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/assign_workers",
|
|
205
|
+
{"workerIds": worker_ids},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def flow_unassign_workers(self, project_id: str, workflow_id: str, worker_ids: list[str]) -> tuple[int, Any]:
|
|
209
|
+
return self._post(
|
|
210
|
+
f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/unassign_workers",
|
|
211
|
+
{"workerIds": worker_ids},
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def flow_force_unassign_workers(self, project_id: str, workflow_id: str, worker_ids: list[str]) -> tuple[int, Any]:
|
|
215
|
+
return self._post(
|
|
216
|
+
f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/force_unassign_workers",
|
|
217
|
+
{"workerIds": worker_ids},
|
|
218
|
+
)
|
|
219
|
+
|
|
174
220
|
def flow_get_draft(self, project_id: str, workflow_id: str) -> tuple[int, Any]:
|
|
175
221
|
return self._get(f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/draft")
|
|
176
222
|
|
|
@@ -250,6 +296,46 @@ class OpenApiClient:
|
|
|
250
296
|
def flow_published(self, project_id: str, workflow_id: str) -> tuple[int, Any]:
|
|
251
297
|
return self._get(f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/published")
|
|
252
298
|
|
|
299
|
+
def flow_deployments(
|
|
300
|
+
self,
|
|
301
|
+
project_id: str,
|
|
302
|
+
workflow_id: str,
|
|
303
|
+
*,
|
|
304
|
+
deployment_id: Optional[str] = None,
|
|
305
|
+
page: Optional[int] = None,
|
|
306
|
+
page_size: Optional[int] = None,
|
|
307
|
+
begin_time: Optional[str] = None,
|
|
308
|
+
end_time: Optional[str] = None,
|
|
309
|
+
) -> tuple[int, Any]:
|
|
310
|
+
return self._get(
|
|
311
|
+
f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/deployments",
|
|
312
|
+
{
|
|
313
|
+
"deploymentId": deployment_id,
|
|
314
|
+
"page": page,
|
|
315
|
+
"pageSize": page_size,
|
|
316
|
+
"beginTime": begin_time,
|
|
317
|
+
"endTime": end_time,
|
|
318
|
+
},
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def flow_versions(
|
|
322
|
+
self,
|
|
323
|
+
project_id: str,
|
|
324
|
+
workflow_id: str,
|
|
325
|
+
*,
|
|
326
|
+
page: Optional[int] = None,
|
|
327
|
+
page_size: Optional[int] = None,
|
|
328
|
+
) -> tuple[int, Any]:
|
|
329
|
+
return self._get(
|
|
330
|
+
f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/versions",
|
|
331
|
+
{"page": page, "pageSize": page_size},
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def flow_version(self, project_id: str, workflow_id: str, version_id: str) -> tuple[int, Any]:
|
|
335
|
+
return self._get(
|
|
336
|
+
f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/versions/{version_id}"
|
|
337
|
+
)
|
|
338
|
+
|
|
253
339
|
def flow_deployment(
|
|
254
340
|
self,
|
|
255
341
|
project_id: str,
|
|
@@ -22,8 +22,8 @@ ENV_JSON = "BUILDIFY_OPENAPI_JSON"
|
|
|
22
22
|
# Set to 1/true/yes to never prompt for a missing key (CI / non-interactive).
|
|
23
23
|
ENV_NO_PROMPT = "BUILDIFY_OPENAPI_NO_PROMPT"
|
|
24
24
|
|
|
25
|
-
# ~/.buildifyrc 与 buildify-publish
|
|
26
|
-
#
|
|
25
|
+
# ~/.buildifyrc 与 buildify-publish 共用。开放 API 在 [openapi],发布配置在 [publish],
|
|
26
|
+
# 两套密钥不会互相覆盖。
|
|
27
27
|
RC_SECTION = "openapi"
|
|
28
28
|
|
|
29
29
|
RC_KEYS = frozenset({"base_url", "api_key"})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
from typing import Any, Optional
|
|
4
5
|
|
|
5
6
|
_STATUS_ICON = {
|
|
@@ -106,6 +107,76 @@ def format_publish_report(payload: dict[str, Any]) -> str:
|
|
|
106
107
|
return "\n".join(lines).rstrip() + "\n"
|
|
107
108
|
|
|
108
109
|
|
|
110
|
+
def format_deployment_records(page: Any) -> str:
|
|
111
|
+
"""Markdown table for a deployment-record page."""
|
|
112
|
+
if not isinstance(page, dict):
|
|
113
|
+
return json.dumps(page, ensure_ascii=False, indent=2) + "\n"
|
|
114
|
+
rows = page.get("data") if isinstance(page.get("data"), list) else []
|
|
115
|
+
lines = [
|
|
116
|
+
_page_heading(page, len(rows)),
|
|
117
|
+
"",
|
|
118
|
+
"| 部署 | 版本 | 状态 | 成功 | 失败 | 警告 | 时间 | 来源 |",
|
|
119
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- |",
|
|
120
|
+
]
|
|
121
|
+
if not rows:
|
|
122
|
+
lines.append("| — | — | — | — | — | — | — | — |")
|
|
123
|
+
for row in rows:
|
|
124
|
+
if not isinstance(row, dict):
|
|
125
|
+
continue
|
|
126
|
+
status = _status_key(row.get("deployStatus"))
|
|
127
|
+
icon = row.get("deployStatusIcon") or _STATUS_ICON.get(status, "⏳")
|
|
128
|
+
label = row.get("deployStatusLabel") or _STATUS_LABEL.get(status, "待部署")
|
|
129
|
+
lines.append(
|
|
130
|
+
f"| `{_cell(row.get('deploymentId'))}` | `{_cell(row.get('versionName'))}` | "
|
|
131
|
+
f"{icon} {label} | {row.get('successCount', 0)} | {row.get('failCount', 0)} | "
|
|
132
|
+
f"{row.get('warningCount', 0)} | {_cell(row.get('createdAt'))} | {_source(row)} |"
|
|
133
|
+
)
|
|
134
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def format_version_records(page: Any) -> str:
|
|
138
|
+
"""Markdown table for a workflow version page."""
|
|
139
|
+
if not isinstance(page, dict):
|
|
140
|
+
return json.dumps(page, ensure_ascii=False, indent=2) + "\n"
|
|
141
|
+
rows = page.get("data") if isinstance(page.get("data"), list) else []
|
|
142
|
+
lines = [
|
|
143
|
+
_page_heading(page, len(rows)),
|
|
144
|
+
"",
|
|
145
|
+
"| 版本 | 名称 | 草稿 | 运行中 | 说明 | 更新时间 | 来源 |",
|
|
146
|
+
"| --- | --- | --- | --- | --- | --- | --- |",
|
|
147
|
+
]
|
|
148
|
+
if not rows:
|
|
149
|
+
lines.append("| — | — | — | — | — | — | — |")
|
|
150
|
+
for row in rows:
|
|
151
|
+
if not isinstance(row, dict):
|
|
152
|
+
continue
|
|
153
|
+
lines.append(
|
|
154
|
+
f"| `{_cell(row.get('versionId'))}` | `{_cell(row.get('versionName'))}` | "
|
|
155
|
+
f"{_yes(row.get('isDraft'))} | {_yes(row.get('isRunning'))} | {_cell(row.get('remark'))} | "
|
|
156
|
+
f"{_cell(row.get('updatedAt') or row.get('createdAt'))} | {_source(row)} |"
|
|
157
|
+
)
|
|
158
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _page_heading(page: dict[str, Any], shown: int) -> str:
|
|
162
|
+
total = page.get("total", shown)
|
|
163
|
+
current = page.get("page", 1)
|
|
164
|
+
pages = page.get("totalPages", 1)
|
|
165
|
+
return f"共 {total} 条,第 {current} / {pages} 页"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _yes(value: Any) -> str:
|
|
169
|
+
return "是" if value is True else "否"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _source(row: dict[str, Any]) -> str:
|
|
173
|
+
source = row.get("source") or "—"
|
|
174
|
+
key_name = row.get("sourceKeyName")
|
|
175
|
+
if key_name:
|
|
176
|
+
return _cell(f"{source} · {key_name}")
|
|
177
|
+
return _cell(source)
|
|
178
|
+
|
|
179
|
+
|
|
109
180
|
def _status_key(value: Any) -> str:
|
|
110
181
|
if value is None:
|
|
111
182
|
return "pending"
|
|
@@ -8,6 +8,12 @@ and docs, reference existing credentials, then create / validate / test run / de
|
|
|
8
8
|
**Auth:** `Authorization: Bearer <keyId>.<secret>`. Keys are **issued and revoked in the console**
|
|
9
9
|
(workspace settings → Open API keys), not via this CLI. One key is bound to exactly one workspace;
|
|
10
10
|
every command below is implicitly scoped to it. The plaintext is shown **only once** at creation.
|
|
11
|
+
A key may also be limited to scopes and projects. Scope values are the console permission codes
|
|
12
|
+
(`project:workflow:view`, `project:workflow:view_deployment`, `project:workflow:view_version`,
|
|
13
|
+
`project:workflow:deploy`, …). `*` is every capability; `project:*` and
|
|
14
|
+
`project:workflow:*` follow the console matcher (`project.*` is the same as `project:*`). HTTP 403
|
|
15
|
+
(`缺少权限: …`) means the key is valid but lacks that capability; it is not an invalid key (that stays
|
|
16
|
+
401). A project outside the key's allowlist is 404 `项目不存在`.
|
|
11
17
|
|
|
12
18
|
If no key is configured:
|
|
13
19
|
|
|
@@ -55,7 +61,7 @@ the user confirms project, name/label and form values. Publish with `workflow de
|
|
|
55
61
|
|
|
56
62
|
**Config file:** `~/.buildifyrc`, section `[openapi]`: `base_url`, legacy `api_key`, `current`, and
|
|
57
63
|
`profiles.<name>` (`api_key`, cached `tenant_id` / `key_name` / `key_prefix`). File mode `600`.
|
|
58
|
-
Shared with `buildify-publish`, which
|
|
64
|
+
Shared with `buildify-publish`, which keeps its host and keys under `[publish]` — the two sections do not collide.
|
|
59
65
|
|
|
60
66
|
**Precedence:** `--api-key` / `BUILDIFY_OPENAPI_API_KEY` > `--profile` / `BUILDIFY_OPENAPI_PROFILE` >
|
|
61
67
|
`.buildify-workspace` > `[openapi].current` > the only saved key > `profile_required` or `missing_api_key`.
|
|
@@ -88,17 +94,28 @@ All paths are under `/openapi/v1`.
|
|
|
88
94
|
| `bundle cred-types` | GET | `/bundles/credentials/types` |
|
|
89
95
|
| `bundle cred-properties` | GET | `/bundles/credentials/properties` |
|
|
90
96
|
| `project list` / `create` / `get` | GET / POST / GET | `/projects[...]` |
|
|
97
|
+
| `project update` | POST | `/projects/{id}` |
|
|
98
|
+
| `project delete` | DELETE | `/projects/{id}` |
|
|
91
99
|
| `project credentials` | GET | `/projects/{id}/credentials` |
|
|
92
100
|
| `project create-credential` | POST | `/projects/{id}/credentials` |
|
|
93
101
|
| `project workers` | GET | `/projects/{id}/workers` |
|
|
94
102
|
| `workflow list` / `create` | GET / POST | `/projects/{id}/workflows` |
|
|
103
|
+
| `workflow update` | POST | `/projects/{id}/workflows/{wid}/settings` |
|
|
104
|
+
| `workflow workers` | GET | `/projects/{id}/workflows/{wid}/workers` |
|
|
105
|
+
| `workflow assign-workers` | POST | `/projects/{id}/workflows/{wid}/assign_workers` |
|
|
106
|
+
| `workflow unassign-workers` | POST | `/projects/{id}/workflows/{wid}/unassign_workers` |
|
|
107
|
+
| `workflow force-unassign-workers` | POST | `/projects/{id}/workflows/{wid}/force_unassign_workers` |
|
|
108
|
+
| `workflow delete` | DELETE | `/projects/{id}/workflows/{wid}` |
|
|
95
109
|
| `workflow get-draft` / `save-draft` | GET / POST | `/projects/{id}/workflows/{wid}/draft` |
|
|
96
110
|
| `workflow validate` | POST | `/projects/{id}/workflows/validate` |
|
|
97
111
|
| `workflow test-run` | POST | `/projects/{id}/workflows/test_runs` |
|
|
98
112
|
| `workflow cancel-test` | POST | `/projects/{id}/workflows/test_runs/{executionId}/cancel` |
|
|
99
113
|
| `workflow deploy` | POST | `/projects/{id}/workflows/{wid}/deploy` |
|
|
100
114
|
| `workflow published` | GET | `/projects/{id}/workflows/{wid}/published` |
|
|
115
|
+
| `workflow deployments` | GET | `/projects/{id}/workflows/{wid}/deployments` |
|
|
101
116
|
| `workflow deployment` | GET | `/projects/{id}/workflows/{wid}/deployments/{deployId}` |
|
|
117
|
+
| `workflow versions` | GET | `/projects/{id}/workflows/{wid}/versions` |
|
|
118
|
+
| `workflow get-version` | GET | `/projects/{id}/workflows/{wid}/versions/{versionId}` |
|
|
102
119
|
|
|
103
120
|
## Typical session
|
|
104
121
|
|
|
@@ -140,6 +157,19 @@ buildify --json workflow deploy -p proj_xxx -w wf_xxx --worker wk_xxx \
|
|
|
140
157
|
|
|
141
158
|
`--file -` reads JSON from stdin (flow canvas or credential `data`).
|
|
142
159
|
|
|
160
|
+
## Changing projects, servers, and deleting flows
|
|
161
|
+
|
|
162
|
+
Ask the user before any of these. They change where a flow runs or remove data.
|
|
163
|
+
|
|
164
|
+
- `project update -p ID -n "新名称" [--remark 简介]` renames a project. Omit `--remark` to keep the current description.
|
|
165
|
+
- `project delete -p ID` deletes a project only when it has no workflows (otherwise HTTP 409).
|
|
166
|
+
- `workflow update -p ID -w WF -n "新名称" [--remark 简介]` renames a workflow.
|
|
167
|
+
- `workflow workers -p ID -w WF` lists servers bound to the flow. `deleteStatus` `0` is bound, `1` is waiting for the server to finish removal.
|
|
168
|
+
- `workflow assign-workers -p ID -w WF --worker wk_…` adds servers. Repeat `--worker` for more than one. If the flow is already published, the current published version is deployed to the new servers (`needDeploy: true`, `deploymentId`). Servers already bound are skipped. A server still waiting to be removed returns HTTP 409.
|
|
169
|
+
- `workflow unassign-workers` starts removal. Unpublished flows drop the binding immediately. Published flows set `deleteStatus` to `1` and send `UNDEPLOY`; the binding row stays until the server confirms.
|
|
170
|
+
- `workflow force-unassign-workers` drops the binding immediately and writes a sync log so an offline server drops the flow on its next sync. Use it when the server is gone or a normal unassign is stuck.
|
|
171
|
+
- `workflow delete -p ID -w WF` deletes the flow only when no server is bound, including ones still waiting to be removed (otherwise HTTP 409).
|
|
172
|
+
|
|
143
173
|
## project create-credential
|
|
144
174
|
|
|
145
175
|
Requires `--project`. Also pass `--bundle` and `--type`. If `--name` / `--label` / credential
|
|
@@ -166,7 +196,7 @@ rather than retrying blindly.
|
|
|
166
196
|
Checks, in this order: canvas structure, node existence in the workspace, `data.parameters`
|
|
167
197
|
against each node's `properties_json`, `edges[].data.relations[].name` against the source node's
|
|
168
198
|
`relations_json` (or the source node's custom `data.relations` for MCP), `data.credentials`
|
|
169
|
-
against the project's credentials, graph shape (trigger present, orphan nodes
|
|
199
|
+
against the project's credentials, graph shape (trigger present, orphan nodes),
|
|
170
200
|
and WARNINGs if catalog `icon` is missing on a node or an edge relation is missing `label` /
|
|
171
201
|
custom `icon`/`_id`.
|
|
172
202
|
|
|
@@ -232,7 +262,18 @@ Without `--json`, `workflow deploy` / `published` / `deployment` print Markdown
|
|
|
232
262
|
(项目 / 服务器 / 凭证;凭证表含 `name` 与 `label`,不含节点表)。
|
|
233
263
|
With `--json`, agents should render the same tables (see buildify-flow-dev skill).
|
|
234
264
|
|
|
235
|
-
`workflow published` is the live snapshot after deploy. `workflow deployment -d deploy_…` is one attempt
|
|
265
|
+
`workflow published` is the live snapshot after deploy. `workflow deployment -d deploy_…` is one attempt:
|
|
266
|
+
counts (`successCount` includes warnings), per-server `items`, and the same `summary` tables.
|
|
267
|
+
|
|
268
|
+
`workflow deployments` lists records newest first. Needs scope `project:workflow:view_deployment`.
|
|
269
|
+
`--page` / `--page-size` (max 100), optional `--deployment deploy_…`, and `--begin` / `--end`
|
|
270
|
+
(`yyyy-MM-dd HH:mm:ss`, quote the space). Without `--json` it prints a table; `--json` returns the
|
|
271
|
+
page (`total`, `page`, `pageSize`, `data`, `hasNext`).
|
|
272
|
+
|
|
273
|
+
`workflow versions` lists historical versions newest first, including the current draft (`isDraft`)
|
|
274
|
+
and any version still bound to a server (`isRunning`). Needs `project:workflow:view_version`.
|
|
275
|
+
`workflow get-version --version-id v-…` returns that version's canvas (`workflowJson`). It is
|
|
276
|
+
read-only and does not return the draft optimistic-lock `version`.
|
|
236
277
|
|
|
237
278
|
## JSON output schema
|
|
238
279
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: buildify-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: CLI for the Buildify open API: discover bundle nodes, author and test workflows (machine Bearer auth)
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -66,7 +66,7 @@ interactive prompt. `config set-key api_key` still updates the current profile,
|
|
|
66
66
|
| `key` | `test` |
|
|
67
67
|
| `bundle` | `list`, `nodes`, `node-properties`, `node-relations`, `doc`, `cred-types`, `cred-properties` |
|
|
68
68
|
| `project` | `list`, `create`, `get`, `credentials`, `create-credential`, `workers` |
|
|
69
|
-
| `workflow` | `list`, `create`, `get-draft`, `save-draft`, `validate`, `test-run`, `cancel-test`, `deploy`, `published`, `deployment` |
|
|
69
|
+
| `workflow` | `list`, `create`, `get-draft`, `save-draft`, `validate`, `test-run`, `cancel-test`, `deploy`, `published`, `deployments`, `deployment`, `versions`, `get-version` |
|
|
70
70
|
| `config` | `init`, `set-key`, `add-profile`, `use`, `profiles`, `remove-profile`, `show` |
|
|
71
71
|
|
|
72
72
|
Every command accepts `--json` for a single-line envelope
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from buildify_cli.publish_view import format_deployment_records, format_version_records
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_deployment_records_table_includes_counts_and_empty_page():
|
|
5
|
+
text = format_deployment_records(
|
|
6
|
+
{
|
|
7
|
+
"total": 1,
|
|
8
|
+
"page": 1,
|
|
9
|
+
"totalPages": 1,
|
|
10
|
+
"data": [
|
|
11
|
+
{
|
|
12
|
+
"deploymentId": "deploy_abc",
|
|
13
|
+
"versionName": "v1",
|
|
14
|
+
"deployStatus": "failed",
|
|
15
|
+
"deployStatusIcon": "❌",
|
|
16
|
+
"deployStatusLabel": "失败",
|
|
17
|
+
"successCount": 1,
|
|
18
|
+
"failCount": 2,
|
|
19
|
+
"warningCount": 0,
|
|
20
|
+
"createdAt": "2026-11-01 10:00:00",
|
|
21
|
+
"source": "api",
|
|
22
|
+
"sourceKeyName": "ci",
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
assert "共 1 条" in text
|
|
28
|
+
assert "deploy_abc" in text
|
|
29
|
+
assert "❌ 失败" in text
|
|
30
|
+
assert "api · ci" in text
|
|
31
|
+
|
|
32
|
+
empty = format_deployment_records({"total": 0, "page": 1, "totalPages": 0, "data": []})
|
|
33
|
+
assert "共 0 条" in empty
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_version_records_mark_draft_and_running():
|
|
37
|
+
text = format_version_records(
|
|
38
|
+
{
|
|
39
|
+
"total": 1,
|
|
40
|
+
"page": 1,
|
|
41
|
+
"totalPages": 1,
|
|
42
|
+
"data": [
|
|
43
|
+
{
|
|
44
|
+
"versionId": "v-abc",
|
|
45
|
+
"versionName": "draft",
|
|
46
|
+
"isDraft": True,
|
|
47
|
+
"isRunning": False,
|
|
48
|
+
"remark": "note",
|
|
49
|
+
"updatedAt": "2026-11-01 10:00:00",
|
|
50
|
+
"source": "console",
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
assert "v-abc" in text
|
|
56
|
+
assert "| 是 | 否 |" in text
|
|
57
|
+
assert "console" in text
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.0.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|