buildify-cli 1.0.0__tar.gz → 1.2.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.2.0}/PKG-INFO +3 -3
- {buildify_cli-1.0.0 → buildify_cli-1.2.0}/README.md +2 -2
- {buildify_cli-1.0.0 → buildify_cli-1.2.0}/pyproject.toml +1 -1
- buildify_cli-1.2.0/src/buildify_cli/__init__.py +1 -0
- {buildify_cli-1.0.0 → buildify_cli-1.2.0}/src/buildify_cli/cli.py +257 -4
- {buildify_cli-1.0.0 → buildify_cli-1.2.0}/src/buildify_cli/client.py +123 -0
- {buildify_cli-1.0.0 → buildify_cli-1.2.0}/src/buildify_cli/config.py +2 -2
- {buildify_cli-1.0.0 → buildify_cli-1.2.0}/src/buildify_cli/publish_view.py +71 -0
- {buildify_cli-1.0.0 → buildify_cli-1.2.0}/src/buildify_cli/usage.md +57 -3
- {buildify_cli-1.0.0 → buildify_cli-1.2.0}/src/buildify_cli.egg-info/PKG-INFO +3 -3
- {buildify_cli-1.0.0 → buildify_cli-1.2.0}/src/buildify_cli.egg-info/SOURCES.txt +2 -1
- buildify_cli-1.2.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.2.0}/setup.cfg +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.2.0}/src/buildify_cli/__main__.py +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.2.0}/src/buildify_cli/credential_form.py +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.2.0}/src/buildify_cli/exit_codes.py +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.2.0}/src/buildify_cli/output.py +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.2.0}/src/buildify_cli.egg-info/dependency_links.txt +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.2.0}/src/buildify_cli.egg-info/entry_points.txt +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.2.0}/src/buildify_cli.egg-info/requires.txt +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.2.0}/src/buildify_cli.egg-info/top_level.txt +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.2.0}/tests/test_auth_config.py +0 -0
- {buildify_cli-1.0.0 → buildify_cli-1.2.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.2.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
|
|
@@ -65,8 +65,8 @@ interactive prompt. `config set-key api_key` still updates the current profile,
|
|
|
65
65
|
| — | `health`, `version`, `help` |
|
|
66
66
|
| `key` | `test` |
|
|
67
67
|
| `bundle` | `list`, `nodes`, `node-properties`, `node-relations`, `doc`, `cred-types`, `cred-properties` |
|
|
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` |
|
|
68
|
+
| `project` | `list`, `create`, `get`, `update`, `delete`, `credentials`, `create-credential`, `update-credential`, `delete-credential`, `credential-workflows`, `workers` |
|
|
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
|
|
@@ -52,8 +52,8 @@ interactive prompt. `config set-key api_key` still updates the current profile,
|
|
|
52
52
|
| — | `health`, `version`, `help` |
|
|
53
53
|
| `key` | `test` |
|
|
54
54
|
| `bundle` | `list`, `nodes`, `node-properties`, `node-relations`, `doc`, `cred-types`, `cred-properties` |
|
|
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` |
|
|
55
|
+
| `project` | `list`, `create`, `get`, `update`, `delete`, `credentials`, `create-credential`, `update-credential`, `delete-credential`, `credential-workflows`, `workers` |
|
|
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.2.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.2.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.",
|
|
@@ -60,9 +65,10 @@ app = typer.Typer(
|
|
|
60
65
|
" health GET /openapi/v1/health (no key)\n"
|
|
61
66
|
" key test Validate the configured key\n"
|
|
62
67
|
" bundle Bundle / node discovery: list, nodes, node-properties, node-relations, doc, cred-types, cred-properties\n"
|
|
63
|
-
" project list, create, get, credentials, create-credential
|
|
68
|
+
" project list, create, get, update, delete, credentials, create-credential,\n"
|
|
69
|
+
" update-credential, delete-credential, credential-workflows, workers\n"
|
|
64
70
|
" workflow list, create, get-draft, save-draft, validate, test-run, cancel-test,\n"
|
|
65
|
-
" deploy, published, deployment\n"
|
|
71
|
+
" deploy, published, deployments, deployment, versions, get-version\n"
|
|
66
72
|
" config Manage the [openapi] section of ~/.buildifyrc\n"
|
|
67
73
|
" version Print CLI version\n"
|
|
68
74
|
" help Markdown reference on stdout (for AI/agents)\n\n"
|
|
@@ -248,6 +254,15 @@ def _emit_network_error(ctx: typer.Context, exc: BaseException) -> int:
|
|
|
248
254
|
return ec.NETWORK
|
|
249
255
|
|
|
250
256
|
|
|
257
|
+
def _show_page(ctx: typer.Context, data: Any, formatter) -> None:
|
|
258
|
+
"""Pretty-print a page for humans; `--json` keeps the server payload."""
|
|
259
|
+
if _cfg(ctx).json_output:
|
|
260
|
+
print_json(ok=True, code=200, msg="ok", http_status=200, data=data)
|
|
261
|
+
else:
|
|
262
|
+
typer.echo(formatter(data))
|
|
263
|
+
raise typer.Exit(ec.OK)
|
|
264
|
+
|
|
265
|
+
|
|
251
266
|
def _call(ctx: typer.Context, fn, *args, **kwargs) -> None:
|
|
252
267
|
"""Run one client call and exit with the mapped code."""
|
|
253
268
|
try:
|
|
@@ -435,6 +450,7 @@ project_app = typer.Typer(help="Projects, credentials and workers.")
|
|
|
435
450
|
app.add_typer(project_app, name="project")
|
|
436
451
|
|
|
437
452
|
_OPT_PROJECT = typer.Option(..., "--project", "-p", help="Project public id (proj_...).")
|
|
453
|
+
_OPT_CREDENTIALS = typer.Option(..., "--credentials", help="Credential public id (cred_...).")
|
|
438
454
|
|
|
439
455
|
|
|
440
456
|
@project_app.command("list")
|
|
@@ -462,6 +478,25 @@ def cmd_project_get(ctx: typer.Context, project: str = _OPT_PROJECT) -> None:
|
|
|
462
478
|
_call(ctx, c.project_get, project)
|
|
463
479
|
|
|
464
480
|
|
|
481
|
+
@project_app.command("update")
|
|
482
|
+
def cmd_project_update(
|
|
483
|
+
ctx: typer.Context,
|
|
484
|
+
project: str = _OPT_PROJECT,
|
|
485
|
+
name: str = typer.Option(..., "--name", "-n", help="New project name."),
|
|
486
|
+
remark: Optional[str] = typer.Option(None, "--remark", help="New description. Omit to keep the current one."),
|
|
487
|
+
) -> None:
|
|
488
|
+
"""Rename a project or change its description."""
|
|
489
|
+
with _client(ctx) as c:
|
|
490
|
+
_call(ctx, c.project_update, project, name, remark)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@project_app.command("delete")
|
|
494
|
+
def cmd_project_delete(ctx: typer.Context, project: str = _OPT_PROJECT) -> None:
|
|
495
|
+
"""Delete a project that has no workflows. Confirm with the user first."""
|
|
496
|
+
with _client(ctx) as c:
|
|
497
|
+
_call(ctx, c.project_delete, project)
|
|
498
|
+
|
|
499
|
+
|
|
465
500
|
@project_app.command("credentials")
|
|
466
501
|
def cmd_project_credentials(
|
|
467
502
|
ctx: typer.Context,
|
|
@@ -596,6 +631,101 @@ def cmd_project_create_credential(
|
|
|
596
631
|
)
|
|
597
632
|
|
|
598
633
|
|
|
634
|
+
@project_app.command("update-credential")
|
|
635
|
+
def cmd_project_update_credential(
|
|
636
|
+
ctx: typer.Context,
|
|
637
|
+
project: str = _OPT_PROJECT,
|
|
638
|
+
credentials: str = _OPT_CREDENTIALS,
|
|
639
|
+
label: Optional[str] = typer.Option(None, "--label", help="New display name. Omit to keep the current one."),
|
|
640
|
+
description: Optional[str] = typer.Option(
|
|
641
|
+
None, "--description", help="New remark. Pass an empty string to clear it. Omit to keep the current one."
|
|
642
|
+
),
|
|
643
|
+
bundle_version: Optional[str] = typer.Option(
|
|
644
|
+
None, "--bundle-version", "-V", help="Bind another version of the same bundle. Omit to keep the current one."
|
|
645
|
+
),
|
|
646
|
+
data_inline: Optional[str] = typer.Option(
|
|
647
|
+
None,
|
|
648
|
+
"--data",
|
|
649
|
+
help="JSON object of form fields. Insecure in argv; prefer --file - / stdin.",
|
|
650
|
+
),
|
|
651
|
+
file: Optional[Path] = typer.Option(
|
|
652
|
+
None,
|
|
653
|
+
"--file",
|
|
654
|
+
"-f",
|
|
655
|
+
help="JSON file of form fields to merge, or '-' for stdin. Omit to keep stored secrets.",
|
|
656
|
+
),
|
|
657
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip the TTY confirmation prompt."),
|
|
658
|
+
) -> None:
|
|
659
|
+
"""Update a credential's label, remark, bundle version, or form data.
|
|
660
|
+
|
|
661
|
+
Omitted fields stay as they are. Secrets are never printed. Masked or blank secret fields
|
|
662
|
+
keep the stored value. Confirm with the user before running.
|
|
663
|
+
"""
|
|
664
|
+
if data_inline:
|
|
665
|
+
human_err("warning: --data is visible in process lists and shell history; prefer --file -")
|
|
666
|
+
data: Optional[dict[str, Any]] = None
|
|
667
|
+
if data_inline is not None or file is not None:
|
|
668
|
+
parsed = _read_json_arg(ctx, data_inline, file, what="credential data")
|
|
669
|
+
if not isinstance(parsed, dict):
|
|
670
|
+
raise _fail_local(ctx, "credential data must be a JSON object")
|
|
671
|
+
data = parsed
|
|
672
|
+
if label is None and description is None and bundle_version is None and data is None:
|
|
673
|
+
raise _fail_local(ctx, "至少提供 --label、--description、--bundle-version 或 --file 之一")
|
|
674
|
+
if label is not None and not label.strip():
|
|
675
|
+
raise _fail_local(ctx, "label 不能为空")
|
|
676
|
+
if _can_prompt_for_key(ctx) and not yes:
|
|
677
|
+
human_err("即将更新访问凭证,请确认:")
|
|
678
|
+
human_err(f" credentials:{credentials}")
|
|
679
|
+
if label is not None:
|
|
680
|
+
human_err(f" label:{label}")
|
|
681
|
+
if description is not None:
|
|
682
|
+
human_err(f" description:{description if description else '(清空)'}")
|
|
683
|
+
if bundle_version is not None:
|
|
684
|
+
human_err(f" bundleVersion:{bundle_version}")
|
|
685
|
+
if data is not None:
|
|
686
|
+
human_err(f" 参数键:{', '.join(data) or '(空)'}(值已隐藏)")
|
|
687
|
+
if not typer.confirm("确认更新?", default=False, err=True):
|
|
688
|
+
raise _fail_local(ctx, "已取消更新", ec.LOCAL_VALIDATION)
|
|
689
|
+
with _client(ctx) as c:
|
|
690
|
+
_call(
|
|
691
|
+
ctx,
|
|
692
|
+
c.project_update_credential,
|
|
693
|
+
project,
|
|
694
|
+
credentials,
|
|
695
|
+
label=label.strip() if label is not None else None,
|
|
696
|
+
description=description,
|
|
697
|
+
bundle_version=bundle_version,
|
|
698
|
+
data=data,
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
@project_app.command("delete-credential")
|
|
703
|
+
def cmd_project_delete_credential(
|
|
704
|
+
ctx: typer.Context,
|
|
705
|
+
project: str = _OPT_PROJECT,
|
|
706
|
+
credentials: str = _OPT_CREDENTIALS,
|
|
707
|
+
) -> None:
|
|
708
|
+
"""Soft-delete a credential that no workflow references. Confirm with the user first.
|
|
709
|
+
|
|
710
|
+
HTTP 409 when a flow still references it.
|
|
711
|
+
"""
|
|
712
|
+
with _client(ctx) as c:
|
|
713
|
+
_call(ctx, c.project_delete_credential, project, credentials)
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
@project_app.command("credential-workflows")
|
|
717
|
+
def cmd_project_credential_workflows(
|
|
718
|
+
ctx: typer.Context,
|
|
719
|
+
project: str = _OPT_PROJECT,
|
|
720
|
+
credentials: str = _OPT_CREDENTIALS,
|
|
721
|
+
page: int = typer.Option(1, "--page", min=1, help="Page number, starting at 1."),
|
|
722
|
+
page_size: int = typer.Option(10, "--page-size", min=1, max=100, help="Page size, at most 100."),
|
|
723
|
+
) -> None:
|
|
724
|
+
"""Workflows that reference a credential. No secrets. Requires credentials view."""
|
|
725
|
+
with _client(ctx) as c:
|
|
726
|
+
_call(ctx, c.project_credential_workflows, project, credentials, page=page, page_size=page_size)
|
|
727
|
+
|
|
728
|
+
|
|
599
729
|
def _prompt_credential_data(fields: list[dict[str, Any]]) -> dict[str, Any]:
|
|
600
730
|
values: dict[str, Any] = {}
|
|
601
731
|
if not fields:
|
|
@@ -688,6 +818,74 @@ def cmd_flow_create(
|
|
|
688
818
|
_call(ctx, c.flow_create, project, workflow_name=name, worker_ids=list(worker), remark=remark)
|
|
689
819
|
|
|
690
820
|
|
|
821
|
+
_OPT_WORKERS = typer.Option(
|
|
822
|
+
..., "--worker", help="Worker public id; repeat for multiple. See `buildify project workers`."
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
@flow_app.command("update")
|
|
827
|
+
def cmd_flow_update(
|
|
828
|
+
ctx: typer.Context,
|
|
829
|
+
project: str = _OPT_PROJECT,
|
|
830
|
+
flow: str = _OPT_FLOW,
|
|
831
|
+
name: str = typer.Option(..., "--name", "-n", help="New workflow name."),
|
|
832
|
+
remark: Optional[str] = typer.Option(None, "--remark", help="New description. Omit to keep the current one."),
|
|
833
|
+
) -> None:
|
|
834
|
+
"""Rename a workflow or change its description."""
|
|
835
|
+
with _client(ctx) as c:
|
|
836
|
+
_call(ctx, c.flow_update, project, flow, workflow_name=name, remark=remark)
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
@flow_app.command("delete")
|
|
840
|
+
def cmd_flow_delete(ctx: typer.Context, project: str = _OPT_PROJECT, flow: str = _OPT_FLOW) -> None:
|
|
841
|
+
"""Delete a workflow that has no bound servers. Confirm with the user first."""
|
|
842
|
+
with _client(ctx) as c:
|
|
843
|
+
_call(ctx, c.flow_delete, project, flow)
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
@flow_app.command("workers")
|
|
847
|
+
def cmd_flow_workers(ctx: typer.Context, project: str = _OPT_PROJECT, flow: str = _OPT_FLOW) -> None:
|
|
848
|
+
"""Servers bound to a workflow, including ones waiting to be removed."""
|
|
849
|
+
with _client(ctx) as c:
|
|
850
|
+
_call(ctx, c.flow_workers, project, flow)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
@flow_app.command("assign-workers")
|
|
854
|
+
def cmd_flow_assign_workers(
|
|
855
|
+
ctx: typer.Context,
|
|
856
|
+
project: str = _OPT_PROJECT,
|
|
857
|
+
flow: str = _OPT_FLOW,
|
|
858
|
+
worker: list[str] = _OPT_WORKERS,
|
|
859
|
+
) -> None:
|
|
860
|
+
"""Bind extra servers. A published flow is deployed to the new servers."""
|
|
861
|
+
with _client(ctx) as c:
|
|
862
|
+
_call(ctx, c.flow_assign_workers, project, flow, list(worker))
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
@flow_app.command("unassign-workers")
|
|
866
|
+
def cmd_flow_unassign_workers(
|
|
867
|
+
ctx: typer.Context,
|
|
868
|
+
project: str = _OPT_PROJECT,
|
|
869
|
+
flow: str = _OPT_FLOW,
|
|
870
|
+
worker: list[str] = _OPT_WORKERS,
|
|
871
|
+
) -> None:
|
|
872
|
+
"""Unbind servers. A published flow is undeployed and removed after the server confirms."""
|
|
873
|
+
with _client(ctx) as c:
|
|
874
|
+
_call(ctx, c.flow_unassign_workers, project, flow, list(worker))
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
@flow_app.command("force-unassign-workers")
|
|
878
|
+
def cmd_flow_force_unassign_workers(
|
|
879
|
+
ctx: typer.Context,
|
|
880
|
+
project: str = _OPT_PROJECT,
|
|
881
|
+
flow: str = _OPT_FLOW,
|
|
882
|
+
worker: list[str] = _OPT_WORKERS,
|
|
883
|
+
) -> None:
|
|
884
|
+
"""Drop server bindings immediately. Use when the server is gone or a normal unassign is stuck."""
|
|
885
|
+
with _client(ctx) as c:
|
|
886
|
+
_call(ctx, c.flow_force_unassign_workers, project, flow, list(worker))
|
|
887
|
+
|
|
888
|
+
|
|
691
889
|
@flow_app.command("get-draft")
|
|
692
890
|
def cmd_flow_get_draft(ctx: typer.Context, project: str = _OPT_PROJECT, flow: str = _OPT_FLOW) -> None:
|
|
693
891
|
"""Draft canvas JSON plus the optimistic-lock `version` needed to save it back."""
|
|
@@ -901,6 +1099,35 @@ def cmd_flow_published(ctx: typer.Context, project: str = _OPT_PROJECT, flow: st
|
|
|
901
1099
|
_call(ctx, c.flow_published, project, flow)
|
|
902
1100
|
|
|
903
1101
|
|
|
1102
|
+
@flow_app.command("deployments")
|
|
1103
|
+
def cmd_flow_deployments(
|
|
1104
|
+
ctx: typer.Context,
|
|
1105
|
+
project: str = _OPT_PROJECT,
|
|
1106
|
+
flow: str = _OPT_FLOW,
|
|
1107
|
+
deployment: Optional[str] = typer.Option(
|
|
1108
|
+
None, "--deployment", "-d", help="Only this deployment public id (deploy_...)."
|
|
1109
|
+
),
|
|
1110
|
+
page: int = typer.Option(1, "--page", min=1, help="Page number, starting at 1."),
|
|
1111
|
+
page_size: int = typer.Option(10, "--page-size", min=1, max=100, help="Page size, at most 100."),
|
|
1112
|
+
begin: Optional[str] = typer.Option(None, "--begin", help="Created at or after, yyyy-MM-dd HH:mm:ss."),
|
|
1113
|
+
end: Optional[str] = typer.Option(None, "--end", help="Created at or before, yyyy-MM-dd HH:mm:ss."),
|
|
1114
|
+
) -> None:
|
|
1115
|
+
"""List deployment records for a flow (newest first). Requires view_deployment."""
|
|
1116
|
+
with _client(ctx) as c:
|
|
1117
|
+
data = _fetch(
|
|
1118
|
+
ctx,
|
|
1119
|
+
c.flow_deployments,
|
|
1120
|
+
project,
|
|
1121
|
+
flow,
|
|
1122
|
+
deployment_id=deployment,
|
|
1123
|
+
page=page,
|
|
1124
|
+
page_size=page_size,
|
|
1125
|
+
begin_time=begin,
|
|
1126
|
+
end_time=end,
|
|
1127
|
+
)
|
|
1128
|
+
_show_page(ctx, data, format_deployment_records)
|
|
1129
|
+
|
|
1130
|
+
|
|
904
1131
|
@flow_app.command("deployment")
|
|
905
1132
|
def cmd_flow_deployment(
|
|
906
1133
|
ctx: typer.Context,
|
|
@@ -908,11 +1135,37 @@ def cmd_flow_deployment(
|
|
|
908
1135
|
flow: str = _OPT_FLOW,
|
|
909
1136
|
deployment: str = typer.Option(..., "--deployment", "-d", help="Deployment public id (deploy_...)."),
|
|
910
1137
|
) -> None:
|
|
911
|
-
"""One deployment
|
|
1138
|
+
"""One deployment record: counts, per-server tasks, and the publish summary."""
|
|
912
1139
|
with _client(ctx) as c:
|
|
913
1140
|
_call(ctx, c.flow_deployment, project, flow, deployment)
|
|
914
1141
|
|
|
915
1142
|
|
|
1143
|
+
@flow_app.command("versions")
|
|
1144
|
+
def cmd_flow_versions(
|
|
1145
|
+
ctx: typer.Context,
|
|
1146
|
+
project: str = _OPT_PROJECT,
|
|
1147
|
+
flow: str = _OPT_FLOW,
|
|
1148
|
+
page: int = typer.Option(1, "--page", min=1, help="Page number, starting at 1."),
|
|
1149
|
+
page_size: int = typer.Option(10, "--page-size", min=1, max=100, help="Page size, at most 100."),
|
|
1150
|
+
) -> None:
|
|
1151
|
+
"""List historical versions (newest first), including the draft. Requires view_version."""
|
|
1152
|
+
with _client(ctx) as c:
|
|
1153
|
+
data = _fetch(ctx, c.flow_versions, project, flow, page=page, page_size=page_size)
|
|
1154
|
+
_show_page(ctx, data, format_version_records)
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
@flow_app.command("get-version")
|
|
1158
|
+
def cmd_flow_get_version(
|
|
1159
|
+
ctx: typer.Context,
|
|
1160
|
+
project: str = _OPT_PROJECT,
|
|
1161
|
+
flow: str = _OPT_FLOW,
|
|
1162
|
+
version_id: str = typer.Option(..., "--version-id", help="Version public id (v-...)."),
|
|
1163
|
+
) -> None:
|
|
1164
|
+
"""Canvas JSON of one historical version. Read-only; not a draft lock. Requires view_version."""
|
|
1165
|
+
with _client(ctx) as c:
|
|
1166
|
+
_call(ctx, c.flow_version, project, flow, version_id)
|
|
1167
|
+
|
|
1168
|
+
|
|
916
1169
|
# --- config ---
|
|
917
1170
|
config_app = typer.Typer(help="Write/read the openapi section of ~/.buildifyrc (overridden by env at runtime).")
|
|
918
1171
|
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",
|
|
@@ -147,6 +156,43 @@ class OpenApiClient:
|
|
|
147
156
|
payload["description"] = description
|
|
148
157
|
return self._post(f"{API_PREFIX}/projects/{project_id}/credentials", payload)
|
|
149
158
|
|
|
159
|
+
def project_update_credential(
|
|
160
|
+
self,
|
|
161
|
+
project_id: str,
|
|
162
|
+
credentials_id: str,
|
|
163
|
+
*,
|
|
164
|
+
label: Optional[str] = None,
|
|
165
|
+
description: Optional[str] = None,
|
|
166
|
+
bundle_version: Optional[str] = None,
|
|
167
|
+
data: Optional[dict[str, Any]] = None,
|
|
168
|
+
) -> tuple[int, Any]:
|
|
169
|
+
payload: dict[str, Any] = {}
|
|
170
|
+
if label is not None:
|
|
171
|
+
payload["label"] = label
|
|
172
|
+
if description is not None:
|
|
173
|
+
payload["description"] = description
|
|
174
|
+
if bundle_version is not None:
|
|
175
|
+
payload["bundleVersion"] = bundle_version
|
|
176
|
+
if data is not None:
|
|
177
|
+
payload["data"] = data
|
|
178
|
+
return self._post(f"{API_PREFIX}/projects/{project_id}/credentials/{credentials_id}", payload)
|
|
179
|
+
|
|
180
|
+
def project_delete_credential(self, project_id: str, credentials_id: str) -> tuple[int, Any]:
|
|
181
|
+
return self._delete(f"{API_PREFIX}/projects/{project_id}/credentials/{credentials_id}")
|
|
182
|
+
|
|
183
|
+
def project_credential_workflows(
|
|
184
|
+
self,
|
|
185
|
+
project_id: str,
|
|
186
|
+
credentials_id: str,
|
|
187
|
+
*,
|
|
188
|
+
page: int = 1,
|
|
189
|
+
page_size: int = 10,
|
|
190
|
+
) -> tuple[int, Any]:
|
|
191
|
+
return self._get(
|
|
192
|
+
f"{API_PREFIX}/projects/{project_id}/credentials/{credentials_id}/workflows",
|
|
193
|
+
{"page": page, "pageSize": page_size},
|
|
194
|
+
)
|
|
195
|
+
|
|
150
196
|
def project_workers(self, project_id: str, online_only: bool = False) -> tuple[int, Any]:
|
|
151
197
|
return self._get(
|
|
152
198
|
f"{API_PREFIX}/projects/{project_id}/workers",
|
|
@@ -171,6 +217,43 @@ class OpenApiClient:
|
|
|
171
217
|
{"workflowName": workflow_name, "workerIds": worker_ids, "remark": remark},
|
|
172
218
|
)
|
|
173
219
|
|
|
220
|
+
def flow_update(
|
|
221
|
+
self,
|
|
222
|
+
project_id: str,
|
|
223
|
+
workflow_id: str,
|
|
224
|
+
*,
|
|
225
|
+
workflow_name: str,
|
|
226
|
+
remark: Optional[str],
|
|
227
|
+
) -> tuple[int, Any]:
|
|
228
|
+
payload: dict[str, Any] = {"workflowName": workflow_name}
|
|
229
|
+
if remark is not None:
|
|
230
|
+
payload["remark"] = remark
|
|
231
|
+
return self._post(f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/settings", payload)
|
|
232
|
+
|
|
233
|
+
def flow_delete(self, project_id: str, workflow_id: str) -> tuple[int, Any]:
|
|
234
|
+
return self._delete(f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}")
|
|
235
|
+
|
|
236
|
+
def flow_workers(self, project_id: str, workflow_id: str) -> tuple[int, Any]:
|
|
237
|
+
return self._get(f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/workers")
|
|
238
|
+
|
|
239
|
+
def flow_assign_workers(self, project_id: str, workflow_id: str, worker_ids: list[str]) -> tuple[int, Any]:
|
|
240
|
+
return self._post(
|
|
241
|
+
f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/assign_workers",
|
|
242
|
+
{"workerIds": worker_ids},
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def flow_unassign_workers(self, project_id: str, workflow_id: str, worker_ids: list[str]) -> tuple[int, Any]:
|
|
246
|
+
return self._post(
|
|
247
|
+
f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/unassign_workers",
|
|
248
|
+
{"workerIds": worker_ids},
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def flow_force_unassign_workers(self, project_id: str, workflow_id: str, worker_ids: list[str]) -> tuple[int, Any]:
|
|
252
|
+
return self._post(
|
|
253
|
+
f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/force_unassign_workers",
|
|
254
|
+
{"workerIds": worker_ids},
|
|
255
|
+
)
|
|
256
|
+
|
|
174
257
|
def flow_get_draft(self, project_id: str, workflow_id: str) -> tuple[int, Any]:
|
|
175
258
|
return self._get(f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/draft")
|
|
176
259
|
|
|
@@ -250,6 +333,46 @@ class OpenApiClient:
|
|
|
250
333
|
def flow_published(self, project_id: str, workflow_id: str) -> tuple[int, Any]:
|
|
251
334
|
return self._get(f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/published")
|
|
252
335
|
|
|
336
|
+
def flow_deployments(
|
|
337
|
+
self,
|
|
338
|
+
project_id: str,
|
|
339
|
+
workflow_id: str,
|
|
340
|
+
*,
|
|
341
|
+
deployment_id: Optional[str] = None,
|
|
342
|
+
page: Optional[int] = None,
|
|
343
|
+
page_size: Optional[int] = None,
|
|
344
|
+
begin_time: Optional[str] = None,
|
|
345
|
+
end_time: Optional[str] = None,
|
|
346
|
+
) -> tuple[int, Any]:
|
|
347
|
+
return self._get(
|
|
348
|
+
f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/deployments",
|
|
349
|
+
{
|
|
350
|
+
"deploymentId": deployment_id,
|
|
351
|
+
"page": page,
|
|
352
|
+
"pageSize": page_size,
|
|
353
|
+
"beginTime": begin_time,
|
|
354
|
+
"endTime": end_time,
|
|
355
|
+
},
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
def flow_versions(
|
|
359
|
+
self,
|
|
360
|
+
project_id: str,
|
|
361
|
+
workflow_id: str,
|
|
362
|
+
*,
|
|
363
|
+
page: Optional[int] = None,
|
|
364
|
+
page_size: Optional[int] = None,
|
|
365
|
+
) -> tuple[int, Any]:
|
|
366
|
+
return self._get(
|
|
367
|
+
f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/versions",
|
|
368
|
+
{"page": page, "pageSize": page_size},
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def flow_version(self, project_id: str, workflow_id: str, version_id: str) -> tuple[int, Any]:
|
|
372
|
+
return self._get(
|
|
373
|
+
f"{API_PREFIX}/projects/{project_id}/workflows/{workflow_id}/versions/{version_id}"
|
|
374
|
+
)
|
|
375
|
+
|
|
253
376
|
def flow_deployment(
|
|
254
377
|
self,
|
|
255
378
|
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,31 @@ 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` |
|
|
101
|
+
| `project update-credential` | POST | `/projects/{id}/credentials/{credentialsId}` |
|
|
102
|
+
| `project delete-credential` | DELETE | `/projects/{id}/credentials/{credentialsId}` |
|
|
103
|
+
| `project credential-workflows` | GET | `/projects/{id}/credentials/{credentialsId}/workflows` |
|
|
93
104
|
| `project workers` | GET | `/projects/{id}/workers` |
|
|
94
105
|
| `workflow list` / `create` | GET / POST | `/projects/{id}/workflows` |
|
|
106
|
+
| `workflow update` | POST | `/projects/{id}/workflows/{wid}/settings` |
|
|
107
|
+
| `workflow workers` | GET | `/projects/{id}/workflows/{wid}/workers` |
|
|
108
|
+
| `workflow assign-workers` | POST | `/projects/{id}/workflows/{wid}/assign_workers` |
|
|
109
|
+
| `workflow unassign-workers` | POST | `/projects/{id}/workflows/{wid}/unassign_workers` |
|
|
110
|
+
| `workflow force-unassign-workers` | POST | `/projects/{id}/workflows/{wid}/force_unassign_workers` |
|
|
111
|
+
| `workflow delete` | DELETE | `/projects/{id}/workflows/{wid}` |
|
|
95
112
|
| `workflow get-draft` / `save-draft` | GET / POST | `/projects/{id}/workflows/{wid}/draft` |
|
|
96
113
|
| `workflow validate` | POST | `/projects/{id}/workflows/validate` |
|
|
97
114
|
| `workflow test-run` | POST | `/projects/{id}/workflows/test_runs` |
|
|
98
115
|
| `workflow cancel-test` | POST | `/projects/{id}/workflows/test_runs/{executionId}/cancel` |
|
|
99
116
|
| `workflow deploy` | POST | `/projects/{id}/workflows/{wid}/deploy` |
|
|
100
117
|
| `workflow published` | GET | `/projects/{id}/workflows/{wid}/published` |
|
|
118
|
+
| `workflow deployments` | GET | `/projects/{id}/workflows/{wid}/deployments` |
|
|
101
119
|
| `workflow deployment` | GET | `/projects/{id}/workflows/{wid}/deployments/{deployId}` |
|
|
120
|
+
| `workflow versions` | GET | `/projects/{id}/workflows/{wid}/versions` |
|
|
121
|
+
| `workflow get-version` | GET | `/projects/{id}/workflows/{wid}/versions/{versionId}` |
|
|
102
122
|
|
|
103
123
|
## Typical session
|
|
104
124
|
|
|
@@ -140,6 +160,19 @@ buildify --json workflow deploy -p proj_xxx -w wf_xxx --worker wk_xxx \
|
|
|
140
160
|
|
|
141
161
|
`--file -` reads JSON from stdin (flow canvas or credential `data`).
|
|
142
162
|
|
|
163
|
+
## Changing projects, servers, and deleting flows
|
|
164
|
+
|
|
165
|
+
Ask the user before any of these. They change where a flow runs or remove data.
|
|
166
|
+
|
|
167
|
+
- `project update -p ID -n "新名称" [--remark 简介]` renames a project. Omit `--remark` to keep the current description.
|
|
168
|
+
- `project delete -p ID` deletes a project only when it has no workflows (otherwise HTTP 409).
|
|
169
|
+
- `workflow update -p ID -w WF -n "新名称" [--remark 简介]` renames a workflow.
|
|
170
|
+
- `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.
|
|
171
|
+
- `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.
|
|
172
|
+
- `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.
|
|
173
|
+
- `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.
|
|
174
|
+
- `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).
|
|
175
|
+
|
|
143
176
|
## project create-credential
|
|
144
177
|
|
|
145
178
|
Requires `--project`. Also pass `--bundle` and `--type`. If `--name` / `--label` / credential
|
|
@@ -154,6 +187,16 @@ Requires `--project`. Also pass `--bundle` and `--type`. If `--name` / `--label`
|
|
|
154
187
|
Response is metadata only (`name` / `label` / `type` / `credentialsId`). Duplicate
|
|
155
188
|
`bundle+type+name` in the same project returns HTTP 409.
|
|
156
189
|
|
|
190
|
+
`project credentials` includes `credentialsId` for the commands below. Secrets are never returned.
|
|
191
|
+
|
|
192
|
+
## project update-credential / delete-credential / credential-workflows
|
|
193
|
+
|
|
194
|
+
Ask the user before update or delete.
|
|
195
|
+
|
|
196
|
+
- `project update-credential -p ID --credentials cred_…` changes label, remark, bundle version, or form data. Pass at least one of `--label`, `--description`, `--bundle-version`, `--file`. Omitted fields stay unchanged. An empty `--description` clears the remark. `--file -` merges form fields; masked or blank secret fields keep the stored value. Secrets are never printed. A TTY asks for confirmation unless `--yes`; `--json` does not prompt.
|
|
197
|
+
- `project delete-credential -p ID --credentials cred_…` soft-deletes a credential. HTTP 409 when a workflow still references it.
|
|
198
|
+
- `project credential-workflows -p ID --credentials cred_… [--page 1] [--page-size 10]` lists workflows that use the credential. No secrets. Same view permission as `project credentials`.
|
|
199
|
+
|
|
157
200
|
## Version conflicts on save-draft
|
|
158
201
|
|
|
159
202
|
`workflow save-draft` reads the current draft to obtain the optimistic-lock `version` unless you pass
|
|
@@ -166,7 +209,7 @@ rather than retrying blindly.
|
|
|
166
209
|
Checks, in this order: canvas structure, node existence in the workspace, `data.parameters`
|
|
167
210
|
against each node's `properties_json`, `edges[].data.relations[].name` against the source node's
|
|
168
211
|
`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
|
|
212
|
+
against the project's credentials, graph shape (trigger present, orphan nodes),
|
|
170
213
|
and WARNINGs if catalog `icon` is missing on a node or an edge relation is missing `label` /
|
|
171
214
|
custom `icon`/`_id`.
|
|
172
215
|
|
|
@@ -232,7 +275,18 @@ Without `--json`, `workflow deploy` / `published` / `deployment` print Markdown
|
|
|
232
275
|
(项目 / 服务器 / 凭证;凭证表含 `name` 与 `label`,不含节点表)。
|
|
233
276
|
With `--json`, agents should render the same tables (see buildify-flow-dev skill).
|
|
234
277
|
|
|
235
|
-
`workflow published` is the live snapshot after deploy. `workflow deployment -d deploy_…` is one attempt
|
|
278
|
+
`workflow published` is the live snapshot after deploy. `workflow deployment -d deploy_…` is one attempt:
|
|
279
|
+
counts (`successCount` includes warnings), per-server `items`, and the same `summary` tables.
|
|
280
|
+
|
|
281
|
+
`workflow deployments` lists records newest first. Needs scope `project:workflow:view_deployment`.
|
|
282
|
+
`--page` / `--page-size` (max 100), optional `--deployment deploy_…`, and `--begin` / `--end`
|
|
283
|
+
(`yyyy-MM-dd HH:mm:ss`, quote the space). Without `--json` it prints a table; `--json` returns the
|
|
284
|
+
page (`total`, `page`, `pageSize`, `data`, `hasNext`).
|
|
285
|
+
|
|
286
|
+
`workflow versions` lists historical versions newest first, including the current draft (`isDraft`)
|
|
287
|
+
and any version still bound to a server (`isRunning`). Needs `project:workflow:view_version`.
|
|
288
|
+
`workflow get-version --version-id v-…` returns that version's canvas (`workflowJson`). It is
|
|
289
|
+
read-only and does not return the draft optimistic-lock `version`.
|
|
236
290
|
|
|
237
291
|
## JSON output schema
|
|
238
292
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: buildify-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.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
|
|
@@ -65,8 +65,8 @@ interactive prompt. `config set-key api_key` still updates the current profile,
|
|
|
65
65
|
| — | `health`, `version`, `help` |
|
|
66
66
|
| `key` | `test` |
|
|
67
67
|
| `bundle` | `list`, `nodes`, `node-properties`, `node-relations`, `doc`, `cred-types`, `cred-properties` |
|
|
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` |
|
|
68
|
+
| `project` | `list`, `create`, `get`, `update`, `delete`, `credentials`, `create-credential`, `update-credential`, `delete-credential`, `credential-workflows`, `workers` |
|
|
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
|