@zhuoyuezs/ml-platform 0.1.16 → 0.1.17
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.
- package/DEVELOPMENT.md +8 -6
- package/README.md +6 -4
- package/checksums.json +35 -10
- package/package.json +1 -1
- package/release.json +15 -9
- package/runtime/business-client/README.md +22 -1
- package/runtime/business-client/package-lock.json +2 -2
- package/runtime/business-client/package.json +1 -1
- package/runtime/business-client/src/cli.js +127 -1
- package/skills/model-deployment-management/SKILL.md +47 -0
- package/skills/model-deployment-management/agents/openai.yaml +4 -0
- package/skills/model-deployment-management/references/contracts.md +55 -0
- package/skills/model-deployment-management/references/operations.md +78 -0
- package/skills/model-deployment-management/references/troubleshooting.md +31 -0
- package/skills/model-lifecycle-management/SKILL.md +4 -0
- package/skills/model-lifecycle-management/references/packaging.md +47 -0
package/DEVELOPMENT.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
- Business CLI 与 Skill 是并列组件;CLI 不属于 Skill。
|
|
10
10
|
- Skill 直接调用正式命令 `ml-platform`,不嵌入 CLI runtime。
|
|
11
11
|
- 正式 npm 包只有 `@zhuoyuezs/ml-platform`,正式 bin 只有 `ml-platform`。
|
|
12
|
-
- `business-client-js/` 是 CLI
|
|
12
|
+
- `business-client-js/` 是 CLI 唯一源码,仓库 `skills/` 下三个受支持 Skill 是各自唯一源码。
|
|
13
13
|
- `npx @zhuoyuezs/ml-platform@<version> install` 是面向用户的统一安装入口;默认目标是 Codex
|
|
14
14
|
用户级 Skill,不要求额外安装参数。
|
|
15
15
|
- 管理命令默认输出简洁结果;Agent 和 CI 显式使用 `--json` 获取稳定结构化输出。
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
```text
|
|
28
28
|
business-client-js/
|
|
29
29
|
skills/feature-management/
|
|
30
|
+
skills/model-lifecycle-management/
|
|
31
|
+
skills/model-deployment-management/
|
|
30
32
|
npm/ml-platform/
|
|
31
33
|
npm/scripts/build_ml_platform_release.py
|
|
32
34
|
agent_poc/codex-host/
|
|
@@ -41,7 +43,7 @@ package/
|
|
|
41
43
|
│ ├── main.js
|
|
42
44
|
│ └── lib.js
|
|
43
45
|
├── runtime/business-client/
|
|
44
|
-
├── skills
|
|
46
|
+
├── skills/<skill-name>/
|
|
45
47
|
├── release-policy.json
|
|
46
48
|
├── release.json
|
|
47
49
|
├── checksums.json
|
|
@@ -76,7 +78,7 @@ Host 副本、日志、会话数据、attic、凭证和实验文件。
|
|
|
76
78
|
|
|
77
79
|
完成标准:正式 CLI 不需要 Python、`uv` 或第三方 npm runtime 依赖,旧 Python client 可删除。
|
|
78
80
|
|
|
79
|
-
### Step 2:重建纯
|
|
81
|
+
### Step 2:重建纯 Skills(已完成)
|
|
80
82
|
|
|
81
83
|
- 保留 `SKILL.md`、`agents/`、`references/` 和 `assets/catalog-template/`;
|
|
82
84
|
- npm staging 时排除 `assets/wheels/`、Skill installer 和 CLI launcher;这些旧 Python
|
|
@@ -94,7 +96,7 @@ Host 副本、日志、会话数据、attic、凭证和实验文件。
|
|
|
94
96
|
只有 Step 1 和 Step 2 都完成后,才开始 npm dispatcher/installer:
|
|
95
97
|
|
|
96
98
|
- 定义 `release.json` v2 和 `checksums.json` 覆盖范围;
|
|
97
|
-
- 确定 `runtime/business-client` 与 `skills
|
|
99
|
+
- 确定 `runtime/business-client` 与 `skills/<skill-name>` 并列目录;
|
|
98
100
|
- 定义 `ml-platform` 安装管理命令和业务命令的无歧义分发;
|
|
99
101
|
- 定义 `npx ... install` 如何建立持久 CLI,禁止引用一次性 npx cache;
|
|
100
102
|
- 定义 CLI home、shim、PATH 检查和跨平台失败行为;
|
|
@@ -106,7 +108,7 @@ Host 副本、日志、会话数据、attic、凭证和实验文件。
|
|
|
106
108
|
### Step 4:重构 release builder(已完成)
|
|
107
109
|
|
|
108
110
|
- 从 `business-client-js` 复制唯一 client tree 到 `runtime/business-client`;
|
|
109
|
-
-
|
|
111
|
+
- 从仓库 canonical `skills/<skill-name>/` 复制全部纯 Skill;
|
|
110
112
|
- 生成 release v2 和 checksums;
|
|
111
113
|
- 拒绝多份 client、Skill runtime、旧 wheel 和未知文件;
|
|
112
114
|
- 执行 npm package allowlist 和可重现 digest 检查;
|
|
@@ -134,7 +136,7 @@ Host 副本、日志、会话数据、attic、凭证和实验文件。
|
|
|
134
136
|
|
|
135
137
|
### Step 7:Skill + CLI 端到端发布验收(已完成)
|
|
136
138
|
|
|
137
|
-
- `git diff --check -- npm business-client-js skills
|
|
139
|
+
- `git diff --check -- npm business-client-js skills agent_poc/codex-host`;
|
|
138
140
|
- JS business-client tests;
|
|
139
141
|
- JavaScript OpenAPI contract 与 Catalog tests;
|
|
140
142
|
- Skill validator 和 forward tests;
|
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# ML Platform
|
|
2
2
|
|
|
3
|
-
ML Platform 的 JavaScript CLI、`feature-management
|
|
3
|
+
ML Platform 的 JavaScript CLI、`feature-management`、`model-lifecycle-management` 与
|
|
4
|
+
`model-deployment-management` Agent Skill。CLI 与 Skill 由同一个 npm
|
|
4
5
|
release 安装和升级,运行时不需要 Python 或 `uv`。
|
|
5
6
|
|
|
6
7
|
## 环境要求
|
|
@@ -17,7 +18,7 @@ release 安装和升级,运行时不需要 Python 或 `uv`。
|
|
|
17
18
|
npx @zhuoyuezs/ml-platform@latest install
|
|
18
19
|
```
|
|
19
20
|
|
|
20
|
-
这条命令不需要额外参数。默认安装持久化的 `ml-platform`
|
|
21
|
+
这条命令不需要额外参数。默认安装持久化的 `ml-platform` 命令,并将三个 Skill 安装到
|
|
21
22
|
`~/.codex/skills/`,供当前用户的 Codex 使用。安装后重启终端和 Agent 会话。
|
|
22
23
|
|
|
23
24
|
只在当前项目使用 Skill 时,在项目根目录运行:
|
|
@@ -26,7 +27,7 @@ npx @zhuoyuezs/ml-platform@latest install
|
|
|
26
27
|
npx @zhuoyuezs/ml-platform@latest install --scope project
|
|
27
28
|
```
|
|
28
29
|
|
|
29
|
-
此时 Skill 安装到当前目录的 `.agents/skills
|
|
30
|
+
此时 Skill 安装到当前目录的 `.agents/skills/<skill-name>/`。如果命令不在项目根目录运行,
|
|
30
31
|
再使用 `--project-dir /absolute/path/to/project` 显式指定项目。
|
|
31
32
|
|
|
32
33
|
### 安装参数
|
|
@@ -124,7 +125,8 @@ npx --yes --registry=https://registry.npmjs.org/ \
|
|
|
124
125
|
## 包内容
|
|
125
126
|
|
|
126
127
|
- 一份 JavaScript business CLI;
|
|
127
|
-
-
|
|
128
|
+
- 三份纯 Skill:`feature-management`、`model-lifecycle-management` 与
|
|
129
|
+
`model-deployment-management`;
|
|
128
130
|
- 显式安装、升级、状态、校验和回滚逻辑。
|
|
129
131
|
|
|
130
132
|
不包含 Codex CLI、Python business-client wheel、Agent Host、平台服务端、模型训练或 Kubernetes
|
package/checksums.json
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
"files": [
|
|
3
3
|
{
|
|
4
4
|
"path": "runtime/business-client/README.md",
|
|
5
|
-
"sha256": "sha256:
|
|
6
|
-
"size_bytes":
|
|
5
|
+
"sha256": "sha256:168e376d3eec34da4cb7966aac44b5c87239ca432d9921588f499713c6aeb24b",
|
|
6
|
+
"size_bytes": 6122
|
|
7
7
|
},
|
|
8
8
|
{
|
|
9
9
|
"path": "runtime/business-client/package-lock.json",
|
|
10
|
-
"sha256": "sha256:
|
|
10
|
+
"sha256": "sha256:b234d20b44cbfd166efef2a9ec6d91e00488952c9a6c7ccb1fdfa50374c4b986",
|
|
11
11
|
"size_bytes": 381
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
"path": "runtime/business-client/package.json",
|
|
15
|
-
"sha256": "sha256:
|
|
15
|
+
"sha256": "sha256:95551c271e726f4d69bf81b9f234b11781dc3b9a2e4b9f86db90ba8ca827ce42",
|
|
16
16
|
"size_bytes": 501
|
|
17
17
|
},
|
|
18
18
|
{
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"path": "runtime/business-client/src/cli.js",
|
|
25
|
-
"sha256": "sha256:
|
|
26
|
-
"size_bytes":
|
|
25
|
+
"sha256": "sha256:3739b93b0bf42bf8c0521005cfe73b080e14ec88e0fe46feb0f375575a98b7ec",
|
|
26
|
+
"size_bytes": 53174
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"path": "runtime/business-client/src/config.js",
|
|
@@ -115,10 +115,35 @@
|
|
|
115
115
|
"sha256": "sha256:2c7a894330a649248e7f52c944422bcd7a3aba4558f7984d129dfd21c8736cf3",
|
|
116
116
|
"size_bytes": 4540
|
|
117
117
|
},
|
|
118
|
+
{
|
|
119
|
+
"path": "skills/model-deployment-management/SKILL.md",
|
|
120
|
+
"sha256": "sha256:5dfeab163f2c11bd4554da23e898d388a7883a2501e6a9655ee45fcada7659bb",
|
|
121
|
+
"size_bytes": 2608
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"path": "skills/model-deployment-management/agents/openai.yaml",
|
|
125
|
+
"sha256": "sha256:55c1a33d59cf08e27e7c806157576a9f8cec079a0fc6775f299794304ae8b6e1",
|
|
126
|
+
"size_bytes": 238
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"path": "skills/model-deployment-management/references/contracts.md",
|
|
130
|
+
"sha256": "sha256:3b040a97dcbf3b22b194bef2bb8c1af744d434ee88a6f47deed9ee4b83cf3147",
|
|
131
|
+
"size_bytes": 1926
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"path": "skills/model-deployment-management/references/operations.md",
|
|
135
|
+
"sha256": "sha256:410111f01b972832bd48290d280f5364cacf2e63e7d586d3b670fad9b997f695",
|
|
136
|
+
"size_bytes": 2333
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"path": "skills/model-deployment-management/references/troubleshooting.md",
|
|
140
|
+
"sha256": "sha256:2354dfad1493c361f534a7d02dbd8ccafa4662a6ebc8e6d1b0bf8dce447dd527",
|
|
141
|
+
"size_bytes": 1410
|
|
142
|
+
},
|
|
118
143
|
{
|
|
119
144
|
"path": "skills/model-lifecycle-management/SKILL.md",
|
|
120
|
-
"sha256": "sha256:
|
|
121
|
-
"size_bytes":
|
|
145
|
+
"sha256": "sha256:74ca6bad54ddb1dc30d3307335d045a70d2d383279011657cc49c1e549eda5a6",
|
|
146
|
+
"size_bytes": 3801
|
|
122
147
|
},
|
|
123
148
|
{
|
|
124
149
|
"path": "skills/model-lifecycle-management/agents/openai.yaml",
|
|
@@ -137,8 +162,8 @@
|
|
|
137
162
|
},
|
|
138
163
|
{
|
|
139
164
|
"path": "skills/model-lifecycle-management/references/packaging.md",
|
|
140
|
-
"sha256": "sha256:
|
|
141
|
-
"size_bytes":
|
|
165
|
+
"sha256": "sha256:f46a5f9c326049a0c2411fdb9d392bea2f4d667062289ee0322a5d9d328c8363",
|
|
166
|
+
"size_bytes": 4888
|
|
142
167
|
},
|
|
143
168
|
{
|
|
144
169
|
"path": "skills/model-lifecycle-management/references/training-contracts.md",
|
package/package.json
CHANGED
package/release.json
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"entrypoint": "src/cli.js",
|
|
14
14
|
"name": "ml-platform",
|
|
15
15
|
"path": "runtime/business-client",
|
|
16
|
-
"sha256": "sha256:
|
|
17
|
-
"version": "0.7.
|
|
16
|
+
"sha256": "sha256:864a3c32efc728bda13b4e2364174e63b9054368be9062ce4f67fe1aac034720",
|
|
17
|
+
"version": "0.7.7"
|
|
18
18
|
},
|
|
19
19
|
"policy_sha256": "sha256:02fbde0134696b78c0c365d6e0b76c9814227208da5b2b3d4e5dbd82dc4a3606",
|
|
20
20
|
"public_schema_versions": [
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"ml_data_platform.feature_set/v1",
|
|
26
26
|
"ml_data_platform.dataset_manifest/v1"
|
|
27
27
|
],
|
|
28
|
-
"release_version": "0.1.
|
|
28
|
+
"release_version": "0.1.17",
|
|
29
29
|
"runtime_requirements": {
|
|
30
30
|
"node": ">=18",
|
|
31
31
|
"os": [
|
|
@@ -37,16 +37,22 @@
|
|
|
37
37
|
"skills": {
|
|
38
38
|
"feature-management": {
|
|
39
39
|
"path": "skills/feature-management",
|
|
40
|
-
"requires_cli": ">=0.7.
|
|
41
|
-
"revision": "0.1.
|
|
40
|
+
"requires_cli": ">=0.7.7 <0.8.0",
|
|
41
|
+
"revision": "0.1.17",
|
|
42
42
|
"sha256": "sha256:182b6f2df4fe58c946e482dfaf416c7d5b78c6fde9c8c8d7293103246618cb4e"
|
|
43
43
|
},
|
|
44
|
+
"model-deployment-management": {
|
|
45
|
+
"path": "skills/model-deployment-management",
|
|
46
|
+
"requires_cli": ">=0.7.7 <0.8.0",
|
|
47
|
+
"revision": "0.1.17",
|
|
48
|
+
"sha256": "sha256:8a4c71ec8bde6c8b37c038f327797286ffa5a3ec10501278bd3d0abbab1b7509"
|
|
49
|
+
},
|
|
44
50
|
"model-lifecycle-management": {
|
|
45
51
|
"path": "skills/model-lifecycle-management",
|
|
46
|
-
"requires_cli": ">=0.7.
|
|
47
|
-
"revision": "0.1.
|
|
48
|
-
"sha256": "sha256:
|
|
52
|
+
"requires_cli": ">=0.7.7 <0.8.0",
|
|
53
|
+
"revision": "0.1.17",
|
|
54
|
+
"sha256": "sha256:a00a50de7b7a5244a08d9865db10c812a1c6291c94ca01a8562f1b2d1ff72f2b"
|
|
49
55
|
}
|
|
50
56
|
},
|
|
51
|
-
"source_commit": "
|
|
57
|
+
"source_commit": "2f547af208a94ebaf2a4c61ff6ca93f11bdbf43e"
|
|
52
58
|
}
|
|
@@ -48,7 +48,28 @@ ml-platform get-parameter-lifecycle <name> <version> --project <project>
|
|
|
48
48
|
ml-platform fetch-inference-context /absolute/path/to/request.json
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
受治理部署通过同一业务 CLI 完成预检、创建、状态等待、晋级、回滚和下线。正常业务操作
|
|
52
|
+
不使用 `kubectl`,也不创建、修改或删除 NetworkPolicy:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
ml-platform validate-deployment /absolute/path/to/deployment.json \
|
|
56
|
+
--evaluation-id <pass-evaluation-id>
|
|
57
|
+
ml-platform create-deployment /absolute/path/to/deployment.json \
|
|
58
|
+
--evaluation-id <pass-evaluation-id>
|
|
59
|
+
ml-platform wait-deployment <deployment-id> --wait-timeout-seconds 1800
|
|
60
|
+
ml-platform get-deployment <deployment-id>
|
|
61
|
+
ml-platform get-deployment-events <deployment-id> --limit 100 --offset 0
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`READY` 必须同时包含 `ReplicasReady=true`、`ArtifactsVerified=true` 和
|
|
65
|
+
`SmokePredictionPassed=true`。`get-deployment` 返回控制器实际观察到的 endpoint 元数据;
|
|
66
|
+
本地 CLI 不推测 Kubernetes Service 名称。部署后验证优先使用控制面代理:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
ml-platform predict-deployment <deployment-id> /absolute/path/to/request.json
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
直接访问已知外部 Serving URL 时仍可使用 `predict-model`。Chronos-2 v2 的
|
|
52
73
|
`known_future` 协变量放在 inline 请求的 `inputs.future_covariates` 中;每个预测步一行,
|
|
53
74
|
行数必须等于模型的 `prediction_length`。下例为 15 分钟频率、预测 30 分钟;预测明天时
|
|
54
75
|
同样提供整天的所有未来步:
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhuoyuezs/ml-platform-business-client",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@zhuoyuezs/ml-platform-business-client",
|
|
9
|
-
"version": "0.7.
|
|
9
|
+
"version": "0.7.7",
|
|
10
10
|
"license": "UNLICENSED",
|
|
11
11
|
"bin": {
|
|
12
12
|
"ml-platform": "src/cli.js"
|
|
@@ -35,6 +35,10 @@ const BUSINESS_COMMANDS = new Set([
|
|
|
35
35
|
"validate-evaluation-run", "submit-evaluation", "get-evaluation-run", "get-evaluation-job", "retry-evaluation-job", "cancel-evaluation-job", "list-evaluation-attempts",
|
|
36
36
|
"get-evaluation-result", "get-evaluation-result-metrics", "get-evaluation-result-artifacts", "summarize-evaluation-run", "get-evaluation-summary",
|
|
37
37
|
"create-model-package", "get-model-package", "cancel-model-package", "validate-model-artifact",
|
|
38
|
+
"list-deployment-targets", "validate-deployment", "create-deployment", "list-deployments",
|
|
39
|
+
"get-deployment", "wait-deployment", "reconcile-deployment", "approve-deployment",
|
|
40
|
+
"promote-deployment", "rollback-deployment", "suspend-deployment", "resume-deployment",
|
|
41
|
+
"undeploy-deployment", "get-deployment-events", "predict-deployment",
|
|
38
42
|
]);
|
|
39
43
|
|
|
40
44
|
const COMMAND_USAGE = {
|
|
@@ -138,6 +142,21 @@ const COMMAND_USAGE = {
|
|
|
138
142
|
"get-model-package": "get-model-package PACKAGE_ID [--package-version VERSION]",
|
|
139
143
|
"cancel-model-package": "cancel-model-package PACKAGE_ID [--package-version VERSION]",
|
|
140
144
|
"validate-model-artifact": "validate-model-artifact ARTIFACT_ID",
|
|
145
|
+
"list-deployment-targets": "list-deployment-targets",
|
|
146
|
+
"validate-deployment": "validate-deployment SPEC_JSON --evaluation-id ID",
|
|
147
|
+
"create-deployment": "create-deployment SPEC_JSON --evaluation-id ID",
|
|
148
|
+
"list-deployments": "list-deployments [--service SERVICE] [--environment ENV] [--state STATE] [-q QUERY] [--limit N] [--offset N]",
|
|
149
|
+
"get-deployment": "get-deployment DEPLOYMENT_ID",
|
|
150
|
+
"wait-deployment": "wait-deployment DEPLOYMENT_ID [--poll-interval SECONDS] [--wait-timeout-seconds SECONDS]",
|
|
151
|
+
"reconcile-deployment": "reconcile-deployment DEPLOYMENT_ID",
|
|
152
|
+
"approve-deployment": "approve-deployment DEPLOYMENT_ID --revision N --approver NAME --decision approved|rejected [--reason TEXT]",
|
|
153
|
+
"promote-deployment": "promote-deployment DEPLOYMENT_ID --stage canary|production --actor NAME --reason TEXT [--approval-id ID]",
|
|
154
|
+
"rollback-deployment": "rollback-deployment DEPLOYMENT_ID --actor NAME --reason TEXT [--to-revision N] [--to-deployment-id ID]",
|
|
155
|
+
"suspend-deployment": "suspend-deployment DEPLOYMENT_ID --actor NAME --reason TEXT",
|
|
156
|
+
"resume-deployment": "resume-deployment DEPLOYMENT_ID --actor NAME --reason TEXT",
|
|
157
|
+
"undeploy-deployment": "undeploy-deployment DEPLOYMENT_ID --actor NAME --reason TEXT",
|
|
158
|
+
"get-deployment-events": "get-deployment-events DEPLOYMENT_ID [--limit N] [--offset N]",
|
|
159
|
+
"predict-deployment": "predict-deployment DEPLOYMENT_ID REQUEST_JSON",
|
|
141
160
|
apply: "apply CATALOG_DIRECTORY [--dry-run] [--project PROJECT]",
|
|
142
161
|
};
|
|
143
162
|
|
|
@@ -291,6 +310,28 @@ async function waitForJob(api, jobId, pollInterval, timeoutSeconds) {
|
|
|
291
310
|
}
|
|
292
311
|
}
|
|
293
312
|
|
|
313
|
+
function observedDeploymentState(payload) {
|
|
314
|
+
const state = payload?.observed_state?.observed_state ?? payload?.observed_state;
|
|
315
|
+
if (typeof state !== "string") throw new Error("deployment status response has no observed state");
|
|
316
|
+
return state;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function waitForDeployment(api, deploymentId, pollInterval, timeoutSeconds) {
|
|
320
|
+
if (!(pollInterval > 0)) throw new Error("deployment poll interval must be positive");
|
|
321
|
+
if (timeoutSeconds !== undefined && !(timeoutSeconds > 0)) throw new Error("deployment wait timeout must be positive");
|
|
322
|
+
const deadline = timeoutSeconds === undefined ? Infinity : Date.now() + timeoutSeconds * 1000;
|
|
323
|
+
let previous;
|
|
324
|
+
while (true) {
|
|
325
|
+
const payload = await api.get(`/deployments/${encodeURIComponent(deploymentId)}`);
|
|
326
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("deployment status response must be a JSON object");
|
|
327
|
+
const state = observedDeploymentState(payload);
|
|
328
|
+
if (state !== previous) { process.stderr.write(`deployment ${deploymentId}: ${state}\n`); previous = state; }
|
|
329
|
+
if (["READY", "FAILED", "DEGRADED", "SUPERSEDED", "SUSPENDED", "UNDEPLOYED", "ROLLED_BACK"].includes(state)) return payload;
|
|
330
|
+
if (Date.now() >= deadline) throw new Error(`timed out waiting for deployment ${deploymentId}`);
|
|
331
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval * 1000));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
294
335
|
async function runBusinessCli(argv) {
|
|
295
336
|
if (argv.length === 0) throw new Error("a business command is required");
|
|
296
337
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
@@ -457,6 +498,91 @@ async function runBusinessCli(argv) {
|
|
|
457
498
|
const request = readSpec(positional(rest, "request JSON")); const servingUrl = take(rest, "--serving-url");
|
|
458
499
|
if (!servingUrl) throw new Error("--serving-url is required");
|
|
459
500
|
result = await new PlatformApiClient(servingUrl, options.timeout).post("/v1/inference", request);
|
|
501
|
+
} else if (options.command === "list-deployment-targets") {
|
|
502
|
+
result = await client(options).get("/deployment-targets");
|
|
503
|
+
} else if (["validate-deployment", "create-deployment"].includes(options.command)) {
|
|
504
|
+
const deployment = readSpec(positional(rest, "deployment spec JSON"));
|
|
505
|
+
const evaluationId = take(rest, "--evaluation-id");
|
|
506
|
+
if (!evaluationId) throw new Error("--evaluation-id is required");
|
|
507
|
+
const endpoint = options.command === "validate-deployment" ? "/deployments/validate" : "/deployments";
|
|
508
|
+
result = await client(options).post(endpoint, { deployment, evaluation_id: evaluationId });
|
|
509
|
+
} else if (options.command === "list-deployments") {
|
|
510
|
+
const params = {
|
|
511
|
+
service: take(rest, "--service"),
|
|
512
|
+
environment: take(rest, "--environment"),
|
|
513
|
+
state: take(rest, "--state"),
|
|
514
|
+
q: take(rest, "--q", take(rest, "-q")),
|
|
515
|
+
};
|
|
516
|
+
boolean(rest, "--all");
|
|
517
|
+
result = await listCollection(client(options), "/deployments", params, rest);
|
|
518
|
+
} else if (options.command === "get-deployment") {
|
|
519
|
+
const deploymentId = positional(rest, "deployment id");
|
|
520
|
+
result = await client(options).get(`/deployments/${encodeURIComponent(deploymentId)}`);
|
|
521
|
+
} else if (options.command === "wait-deployment") {
|
|
522
|
+
const deploymentId = positional(rest, "deployment id");
|
|
523
|
+
const poll = finiteNumber(take(rest, "--poll-interval", 2), "--poll-interval");
|
|
524
|
+
const rawTimeout = take(rest, "--wait-timeout-seconds");
|
|
525
|
+
result = await waitForDeployment(
|
|
526
|
+
client(options),
|
|
527
|
+
deploymentId,
|
|
528
|
+
poll,
|
|
529
|
+
rawTimeout === undefined ? undefined : finiteNumber(rawTimeout, "--wait-timeout-seconds"),
|
|
530
|
+
);
|
|
531
|
+
if (observedDeploymentState(result) !== "READY") exitCode = 1;
|
|
532
|
+
} else if (options.command === "reconcile-deployment") {
|
|
533
|
+
const deploymentId = positional(rest, "deployment id");
|
|
534
|
+
result = await client(options).post(`/deployments/${encodeURIComponent(deploymentId)}/reconcile`);
|
|
535
|
+
} else if (options.command === "approve-deployment") {
|
|
536
|
+
const deploymentId = positional(rest, "deployment id");
|
|
537
|
+
const rawRevision = take(rest, "--revision");
|
|
538
|
+
const approver = take(rest, "--approver");
|
|
539
|
+
if (rawRevision === undefined) throw new Error("--revision is required");
|
|
540
|
+
if (!approver) throw new Error("--approver is required");
|
|
541
|
+
const decision = take(rest, "--decision", "approved");
|
|
542
|
+
if (!["approved", "rejected"].includes(decision)) throw new Error("--decision must be approved or rejected");
|
|
543
|
+
result = await client(options).post(`/deployments/${encodeURIComponent(deploymentId)}/approvals`, {
|
|
544
|
+
revision: integer(rawRevision, "--revision"), stage: "production", decision, approver,
|
|
545
|
+
reason: take(rest, "--reason", ""),
|
|
546
|
+
});
|
|
547
|
+
} else if (options.command === "promote-deployment") {
|
|
548
|
+
const deploymentId = positional(rest, "deployment id");
|
|
549
|
+
const stage = take(rest, "--stage"); const actor = take(rest, "--actor"); const reason = take(rest, "--reason");
|
|
550
|
+
if (!["canary", "production"].includes(stage)) throw new Error("--stage must be canary or production");
|
|
551
|
+
if (!actor) throw new Error("--actor is required");
|
|
552
|
+
if (!reason) throw new Error("--reason is required");
|
|
553
|
+
result = await client(options).post(`/deployments/${encodeURIComponent(deploymentId)}/promote`, {
|
|
554
|
+
stage, actor, reason, approval_id: take(rest, "--approval-id"),
|
|
555
|
+
});
|
|
556
|
+
} else if (options.command === "rollback-deployment") {
|
|
557
|
+
const deploymentId = positional(rest, "deployment id");
|
|
558
|
+
const actor = take(rest, "--actor"); const reason = take(rest, "--reason");
|
|
559
|
+
if (!actor) throw new Error("--actor is required");
|
|
560
|
+
if (!reason) throw new Error("--reason is required");
|
|
561
|
+
const rawRevision = take(rest, "--to-revision");
|
|
562
|
+
result = await client(options).post(`/deployments/${encodeURIComponent(deploymentId)}/rollback`, {
|
|
563
|
+
actor, reason,
|
|
564
|
+
to_revision: rawRevision === undefined ? undefined : integer(rawRevision, "--to-revision"),
|
|
565
|
+
to_deployment_id: take(rest, "--to-deployment-id"),
|
|
566
|
+
});
|
|
567
|
+
} else if (["suspend-deployment", "resume-deployment", "undeploy-deployment"].includes(options.command)) {
|
|
568
|
+
const deploymentId = positional(rest, "deployment id");
|
|
569
|
+
const actor = take(rest, "--actor"); const reason = take(rest, "--reason");
|
|
570
|
+
if (!actor) throw new Error("--actor is required");
|
|
571
|
+
if (!reason) throw new Error("--reason is required");
|
|
572
|
+
const action = options.command.split("-")[0];
|
|
573
|
+
result = await client(options).post(`/deployments/${encodeURIComponent(deploymentId)}/${action}`, { actor, reason });
|
|
574
|
+
} else if (options.command === "get-deployment-events") {
|
|
575
|
+
const deploymentId = positional(rest, "deployment id");
|
|
576
|
+
result = await listCollection(
|
|
577
|
+
client(options),
|
|
578
|
+
`/deployments/${encodeURIComponent(deploymentId)}/events`,
|
|
579
|
+
{},
|
|
580
|
+
rest,
|
|
581
|
+
);
|
|
582
|
+
} else if (options.command === "predict-deployment") {
|
|
583
|
+
const deploymentId = positional(rest, "deployment id");
|
|
584
|
+
const request = readSpec(positional(rest, "request JSON"));
|
|
585
|
+
result = await client(options).post(`/deployments/${encodeURIComponent(deploymentId)}/predict`, request);
|
|
460
586
|
} else if (options.command === "get-model-runtime-release") {
|
|
461
587
|
result = await client(options).get("/model-runtime-release");
|
|
462
588
|
} else if (options.command.startsWith("list-") && LIFECYCLE_COLLECTIONS.has(options.command.slice(5))) {
|
|
@@ -529,7 +655,7 @@ async function listCollection(api, endpoint, params, rest) {
|
|
|
529
655
|
return api.get(endpoint, { ...params, limit, offset });
|
|
530
656
|
}
|
|
531
657
|
|
|
532
|
-
module.exports = { BUSINESS_COMMANDS, fetchAll, listCollection, parse, runBusinessCli, usage, waitForJob };
|
|
658
|
+
module.exports = { BUSINESS_COMMANDS, fetchAll, listCollection, parse, runBusinessCli, usage, waitForDeployment, waitForJob };
|
|
533
659
|
|
|
534
660
|
if (require.main === module) {
|
|
535
661
|
runBusinessCli(process.argv.slice(2)).then((code) => { process.exitCode = code; }).catch((error) => {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: model-deployment-management
|
|
3
|
+
description: Manage governed ITSMP model deployments and post-deployment predictions through the ml-platform business CLI. Use when a user asks to validate or create a DeploymentSpec, inspect targets or rollout state, wait for readiness, record approval, promote, roll back, suspend, resume, undeploy, inspect deployment events, or invoke a deployed model. Do not use for training, evaluation, packaging, image release engineering, or Kubernetes administration.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Model Deployment Management
|
|
7
|
+
|
|
8
|
+
Use the `ml-platform` executable installed with the same npm release as this
|
|
9
|
+
Skill. Normal deployment operations go through the ITSMP control plane; do not
|
|
10
|
+
use `kubectl` to create, promote, roll back, suspend, resume, or undeploy a
|
|
11
|
+
business model.
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
REGISTERED ModelArtifact + PASS EvaluationResult + READY ModelPackage
|
|
15
|
+
-> validated DeploymentSpec -> DeploymentRevision -> READY -> prediction
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Read [references/contracts.md](references/contracts.md) before authoring or
|
|
19
|
+
validating a DeploymentSpec. Read
|
|
20
|
+
[references/operations.md](references/operations.md) immediately before a
|
|
21
|
+
deployment mutation or prediction. Read
|
|
22
|
+
[references/troubleshooting.md](references/troubleshooting.md) when a
|
|
23
|
+
deployment does not reach `READY` or an operation is rejected.
|
|
24
|
+
|
|
25
|
+
## Required Gates
|
|
26
|
+
|
|
27
|
+
- Verify `ml-platform version`, `ml-platform show-config`, and
|
|
28
|
+
`ml-platform --profile server health` before control-plane work.
|
|
29
|
+
- Use only a `READY` ModelPackage image with an immutable `@sha256:` digest and
|
|
30
|
+
the package-bound FeatureRetrievalSpec and smoke request.
|
|
31
|
+
- Run `validate-deployment` before `create-deployment`. Validation is read-only
|
|
32
|
+
and does not authorize creation.
|
|
33
|
+
- Obtain explicit authorization for the exact deployment and operation before
|
|
34
|
+
create, reconcile, approval, promotion, rollback, suspend, resume, or
|
|
35
|
+
undeploy. Retry requires renewed authorization when the previous mutation
|
|
36
|
+
returned an ambiguous result.
|
|
37
|
+
- Treat `READY` as valid only when `ReplicasReady`, `ArtifactsVerified`, and
|
|
38
|
+
`SmokePredictionPassed` are all true for the current revision.
|
|
39
|
+
- Use `predict-deployment`; it routes through the control plane to the recorded
|
|
40
|
+
deployment endpoint. Do not infer Kubernetes Service names or require the
|
|
41
|
+
user to reach a cluster-local URL.
|
|
42
|
+
- Never create, modify, or delete NetworkPolicy. Cluster operators own that
|
|
43
|
+
boundary.
|
|
44
|
+
|
|
45
|
+
Report the deployment ID, revision, immutable image digest, desired and
|
|
46
|
+
observed state, failed conditions, endpoint reachability, and event evidence.
|
|
47
|
+
Do not claim a deployment is usable merely because its Pod is running.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Deployment Contracts
|
|
2
|
+
|
|
3
|
+
## DeploymentSpec
|
|
4
|
+
|
|
5
|
+
Use a JSON object accepted by `ml-platform validate-deployment` with these
|
|
6
|
+
load-bearing fields:
|
|
7
|
+
|
|
8
|
+
```json
|
|
9
|
+
{
|
|
10
|
+
"schema_version": "ml_data_platform.deployment/v1",
|
|
11
|
+
"deployment_id": "deployment_forecast_staging_01",
|
|
12
|
+
"service": "forecast-service",
|
|
13
|
+
"environment": "staging",
|
|
14
|
+
"model_artifact_id": "artifact_01",
|
|
15
|
+
"package_id": "package_01",
|
|
16
|
+
"package_version": "1",
|
|
17
|
+
"image": "registry.example/model-serving@sha256:<64-hex>",
|
|
18
|
+
"target": {"type": "kubernetes", "namespace": "itsmp-model-serving"},
|
|
19
|
+
"feature_retrieval": {},
|
|
20
|
+
"replicas": 1,
|
|
21
|
+
"resources": {},
|
|
22
|
+
"rollout": {"strategy": "canary", "traffic_percent": 10},
|
|
23
|
+
"stage": "staging"
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Do not reconstruct `feature_retrieval` or `feature_lookup_test` from memory.
|
|
28
|
+
They must match the registered artifact lineage and the tested ModelPackage.
|
|
29
|
+
The server normalizes an omitted lookup test from the package during
|
|
30
|
+
validation and creation.
|
|
31
|
+
|
|
32
|
+
The first deployment starts at `staging`. A replacement for an existing
|
|
33
|
+
service/environment declares the active leaf in `previous_deployment_id`.
|
|
34
|
+
Production is reached only by staging to canary to production promotion.
|
|
35
|
+
|
|
36
|
+
## Revision State
|
|
37
|
+
|
|
38
|
+
`DeploymentSpec` is immutable. Promotion, rollback, suspension, resumption,
|
|
39
|
+
and undeploy append immutable revisions. Reconcile writes mutable observed
|
|
40
|
+
state separately.
|
|
41
|
+
|
|
42
|
+
`READY` requires all three current-revision conditions:
|
|
43
|
+
|
|
44
|
+
- `ReplicasReady=true`
|
|
45
|
+
- `ArtifactsVerified=true`
|
|
46
|
+
- `SmokePredictionPassed=true`
|
|
47
|
+
|
|
48
|
+
The detail response includes `endpoints`. Cluster-local endpoints have
|
|
49
|
+
`externally_reachable=false`; a Gateway endpoint is externally reachable only
|
|
50
|
+
when rollout hostname metadata exists. `available=true` means the current
|
|
51
|
+
revision passed all READY gates.
|
|
52
|
+
|
|
53
|
+
Successful smoke verification is cached durably by revision, image digest,
|
|
54
|
+
contract hash, and frozen lookup-test hash. A new revision or changed identity
|
|
55
|
+
must run smoke again.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Deployment Operations
|
|
2
|
+
|
|
3
|
+
## Initialize
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
command -v ml-platform
|
|
7
|
+
ml-platform version
|
|
8
|
+
ml-platform show-config
|
|
9
|
+
ml-platform --profile server health
|
|
10
|
+
ml-platform --profile server list-deployment-targets
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Validate And Create
|
|
14
|
+
|
|
15
|
+
Validation and creation are separate authorization points:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
ml-platform --profile server validate-deployment /absolute/path/deployment.json \
|
|
19
|
+
--evaluation-id <pass-evaluation-id>
|
|
20
|
+
|
|
21
|
+
ml-platform --profile server create-deployment /absolute/path/deployment.json \
|
|
22
|
+
--evaluation-id <pass-evaluation-id>
|
|
23
|
+
|
|
24
|
+
ml-platform --profile server wait-deployment <deployment-id> \
|
|
25
|
+
--poll-interval 5 --wait-timeout-seconds 1800
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Use `get-deployment` to inspect the full revision history and endpoints. Use
|
|
29
|
+
`list-deployments` for discovery and `get-deployment-events` for paginated
|
|
30
|
+
audit evidence.
|
|
31
|
+
|
|
32
|
+
## Promotion And Approval
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
ml-platform --profile server promote-deployment <deployment-id> \
|
|
36
|
+
--stage canary --actor <actor> --reason <reason>
|
|
37
|
+
|
|
38
|
+
ml-platform --profile server approve-deployment <deployment-id> \
|
|
39
|
+
--revision <canary-revision> --approver <approver> \
|
|
40
|
+
--decision approved --reason <reason>
|
|
41
|
+
|
|
42
|
+
ml-platform --profile server promote-deployment <deployment-id> \
|
|
43
|
+
--stage production --actor <actor> --reason <reason> \
|
|
44
|
+
--approval-id <approval-id>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Do not approve production before the canary revision is `READY` and the user
|
|
48
|
+
has authorized that exact revision.
|
|
49
|
+
|
|
50
|
+
## Rollback And Offline Operations
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
ml-platform --profile server rollback-deployment <deployment-id> \
|
|
54
|
+
--to-revision <revision> --actor <actor> --reason <reason>
|
|
55
|
+
|
|
56
|
+
ml-platform --profile server suspend-deployment <deployment-id> \
|
|
57
|
+
--actor <actor> --reason <reason>
|
|
58
|
+
|
|
59
|
+
ml-platform --profile server resume-deployment <deployment-id> \
|
|
60
|
+
--actor <actor> --reason <reason>
|
|
61
|
+
|
|
62
|
+
ml-platform --profile server undeploy-deployment <deployment-id> \
|
|
63
|
+
--actor <actor> --reason <reason>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Suspend is recoverable. Undeploy removes runtime resources but retains revision
|
|
67
|
+
history; it is not equivalent to resume.
|
|
68
|
+
|
|
69
|
+
## Prediction
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
ml-platform --profile server predict-deployment <deployment-id> \
|
|
73
|
+
/absolute/path/request.json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Prediction is allowed only for a current `READY` revision with all readiness
|
|
77
|
+
conditions true. This command uses the control-plane proxy and does not require
|
|
78
|
+
a cluster-local Serving URL.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Deployment Troubleshooting
|
|
2
|
+
|
|
3
|
+
Start with read-only control-plane evidence:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
ml-platform --profile server get-deployment <deployment-id>
|
|
7
|
+
ml-platform --profile server get-deployment-events <deployment-id> \
|
|
8
|
+
--limit 100 --offset 0
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Interpret conditions before considering a reconcile:
|
|
12
|
+
|
|
13
|
+
- `ReplicasReady=false`: inspect scheduler, image pull, resource quota, and Pod
|
|
14
|
+
readiness through the platform/cluster diagnostic boundary.
|
|
15
|
+
- `ArtifactsVerified=false`: the Serving `/model` identity does not match the
|
|
16
|
+
revision, or the endpoint is unavailable.
|
|
17
|
+
- `SmokePredictionPassed=false`: preserve the frozen package smoke request and
|
|
18
|
+
error. Do not replace it with a current-time request to force success.
|
|
19
|
+
- `SUPERSEDED`: operate on the active deployment leaf.
|
|
20
|
+
- `SUSPENDED`: resume only with explicit authorization and a reason.
|
|
21
|
+
- `UNDEPLOYED`: create a governed deployment or rollback as supported; resume
|
|
22
|
+
is invalid.
|
|
23
|
+
|
|
24
|
+
`reconcile-deployment` is a mutation because it may update Kubernetes
|
|
25
|
+
resources and execute verification. Obtain authorization for the exact
|
|
26
|
+
deployment before invoking it. Repeated successful reconcile should not rerun
|
|
27
|
+
the same expensive smoke or append identical events.
|
|
28
|
+
|
|
29
|
+
Use Kubernetes commands only when the user explicitly requests platform
|
|
30
|
+
diagnostics or administration. Do not use them as an alternate deployment
|
|
31
|
+
workflow, and never create, modify, or delete NetworkPolicy.
|
|
@@ -36,3 +36,7 @@ to discover fields. Preserve release/error evidence when deployment differs.
|
|
|
36
36
|
- Keep credentials, registry secrets, Kubernetes details, and model binaries out of request JSON and reports.
|
|
37
37
|
|
|
38
38
|
Report immutable input identities, returned run/job/package IDs, status, validation or gate failures, and the next authorized action. Never claim deployment; it is outside this Skill.
|
|
39
|
+
|
|
40
|
+
Route deployment validation, creation, rollout state, approval, promotion,
|
|
41
|
+
rollback, offline operations, and deployment-bound prediction to the separate
|
|
42
|
+
`model-deployment-management` Skill.
|
|
@@ -31,6 +31,53 @@ runtime. `feature_lookup` still requires artifact lineage
|
|
|
31
31
|
signature. Do not infer either capability from training success.
|
|
32
32
|
Omit rather than use an empty list to request default inline behavior.
|
|
33
33
|
|
|
34
|
+
For `feature_lookup`, the package request must bind both the exact retrieval
|
|
35
|
+
contract and a sanitized real lookup test. They are immutable package identity,
|
|
36
|
+
not namespace Secret/ConfigMap settings:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"model_artifact_id": "replace_registered_model_artifact_id",
|
|
41
|
+
"package_version": "1.0.0",
|
|
42
|
+
"input_modes": ["inline", "feature_lookup"],
|
|
43
|
+
"feature_retrieval": {
|
|
44
|
+
"type": "data_platform",
|
|
45
|
+
"manifest": {
|
|
46
|
+
"project": "replace_project",
|
|
47
|
+
"dataset_id": "replace_inference_manifest",
|
|
48
|
+
"dataset_version": "v1",
|
|
49
|
+
"manifest_hash": "sha256:<registered-manifest-hash>"
|
|
50
|
+
},
|
|
51
|
+
"feature_set": {
|
|
52
|
+
"project": "replace_project",
|
|
53
|
+
"name": "replace_feature_set",
|
|
54
|
+
"version": "v1"
|
|
55
|
+
},
|
|
56
|
+
"contract_signature": "sha256:<artifact-contract-signature>",
|
|
57
|
+
"request": {
|
|
58
|
+
"mode": "row",
|
|
59
|
+
"input_adapter_hash": "sha256:<artifact-input-adapter-hash>",
|
|
60
|
+
"temporal_binding_hash": "sha256:<training-temporal-binding-hash>",
|
|
61
|
+
"entity_keys": {}
|
|
62
|
+
},
|
|
63
|
+
"freshness_policy": "strict"
|
|
64
|
+
},
|
|
65
|
+
"feature_lookup_test": {
|
|
66
|
+
"input_mode": "feature_lookup",
|
|
67
|
+
"cutoff_time": "2026-09-11T00:00:00+08:00",
|
|
68
|
+
"horizon": [{"duration": "PT10M"}],
|
|
69
|
+
"entity": {}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The inference DatasetManifest must exist with the supplied hash and use
|
|
75
|
+
`mode=inference`. Its FeatureSet, stable contract signature, adapter/temporal
|
|
76
|
+
hashes, entity keys, and row/context parameters must match the ModelArtifact.
|
|
77
|
+
The test cutoff must include a timezone. A legacy feature-lookup package missing
|
|
78
|
+
either package-bound field remains readable but cannot be rerun or deployed;
|
|
79
|
+
create a new package version.
|
|
80
|
+
|
|
34
81
|
Always query the exact package ID and version; states are `PACKAGING`, `READY`,
|
|
35
82
|
`FAILED`, `CANCELLED`. A client timeout does not prove packaging failed. Retain the
|
|
36
83
|
known identity and query it before attempting another creation.
|