@toloka-platform/sdk 0.15.0 → 0.16.0
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/dist/cli/toloka.js +137 -134
- package/dist/cli/toloka.js.jsc +0 -0
- package/dist/cli/toloka.js.jsc.sha256 +1 -1
- package/dist/cli/toloka.js.sha256 +1 -1
- package/docs/cli.md +13 -6
- package/docs/troubleshooting.md +5 -1
- package/openapi/gen/api.schemas.ts +12 -0
- package/openapi/gen/api.ts +22 -0
- package/package.json +2 -2
- package/skills/catalog.json +6 -6
- package/skills/toloka-build/SKILL.md +5 -5
- package/skills/toloka-build/references/node-semantics.md +3 -1
- package/skills/toloka-build/references/workflow-and-cli.md +1 -0
- package/src/cli/commands/pipeline.ts +26 -1
- package/src/cli/runs.ts +148 -2
- package/src/client.ts +2 -0
- package/src/pipeline-graph-v1.contract.json +4 -2
package/dist/cli/toloka.js.jsc
CHANGED
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
d657cb43672cd6deb574d75ed2a1c7b4a14d36d5b02a833d118bb0cd8d6634e7 toloka.js.jsc
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
59d0e496f8f496fc0fe031360beb5ee033d0eff186c7004914a659d904483cf6 toloka.js
|
package/docs/cli.md
CHANGED
|
@@ -286,7 +286,7 @@ toloka validate [--wait]
|
|
|
286
286
|
|
|
287
287
|
It prints one line per pipeline with its status, then its issues: `valid`; `invalid`; `checks-pending` when only check state is open, such as a stored test case or a lint that has not run on this graph; `checks-running` when `--wait` gave up on running checks; or `skipped` when the pipeline or its dataset is not on the platform yet. A `note:` line explains what to do next when `--wait` could not finish. Exits `1` when any submitted graph is invalid or its checks were still running at the `--wait` deadline.
|
|
288
288
|
|
|
289
|
-
The platform lints a pipeline a few seconds after every apply. While a lint or a check is running, or a lint is about to start after an apply, validate lists it as `running running_node_check`; a lint that has not run on this graph and is no longer expected to start is `pending not_ran_node_check
|
|
289
|
+
The platform lints a pipeline a few seconds after every apply. While a lint or a check is running, or a lint is about to start after an apply, validate lists it as `running running_node_check`; a lint that has not run on this graph and is no longer expected to start is `pending not_ran_node_check`; start it with `toloka pipeline lint`. `--wait` validates again every 5 seconds until nothing is running, for up to 5 minutes, then prints the final report; a pipeline whose source differs from the applied graph is not waited on, because its lints start only after `toloka apply`. Right after an apply, plain `toloka validate` can print `valid` and exit `0` while lints are still running, so run `toloka validate --wait` before treating an applied pipeline as valid.
|
|
290
290
|
|
|
291
291
|
## check
|
|
292
292
|
|
|
@@ -338,6 +338,7 @@ Operate on a live pipeline in the selected project. Name or id is required unles
|
|
|
338
338
|
|
|
339
339
|
```sh
|
|
340
340
|
toloka pipeline list [--limit <n>]
|
|
341
|
+
toloka pipeline lint [name] [--wait] [--timeout <ms>]
|
|
341
342
|
toloka pipeline run [name] [--wait] [--timeout <ms>]
|
|
342
343
|
toloka pipeline status [name]
|
|
343
344
|
toloka pipeline runs [name] [--limit <n>]
|
|
@@ -350,6 +351,10 @@ toloka pipeline results <runId> [--limit <n>] [--failed]
|
|
|
350
351
|
|
|
351
352
|
`pipeline run` without `--wait` prints `Started run <id> is <status> for <name>.` and a follow-up that uses the pipeline id. Exit code `0` means only that the run was accepted. With `--wait`, it prints `Run <id> succeeded.` or `Run <id> failed.` and sets the exit code accordingly; see [Exit codes](#exit-codes).
|
|
352
353
|
|
|
354
|
+
The platform rejects a run until the built-in lints of every node on the path from Start to End have finished. Lints start a few seconds after every `apply`, but not when dataset items or their values change, and a lint that runs for more than 5 minutes or waits more than 15 minutes to start fails as timed out. `pipeline lint` starts every lint without a current result: one that has not run on the current graph and dataset items, or whose last run failed. It always runs the lints that check dataset contents again, because edits to item values do not invalidate their results. It leaves running lints and the other finished lints alone and prints one line per lint. With `--wait` it waits for them, 5 minutes by default; exit code `0` means every lint finished, `1` that one failed or could not start, and `2` that one was still running when the wait ended. Read the findings with `toloka validate`.
|
|
355
|
+
|
|
356
|
+
`pipeline run` does the same before it starts a run, then waits only for the lints of nodes on the path from Start to End, up to 5 minutes or the `--timeout` you pass, which bounds the whole command including the wait for the run. When a lint is still running when that time is up, it prints the lints, starts nothing, and exits `2`. Otherwise it asks the platform to start the run, and the platform rejects it if a lint on a connected node failed.
|
|
357
|
+
|
|
353
358
|
A run needs at least one dataset item that matches the root node's dataset and filter. Otherwise `pipeline run` fails with `No dataset items match, so a run would have nothing to process.` and starts nothing. `apply` and `validate` still succeed over an empty dataset, so upload items or widen the filter, then run again.
|
|
354
359
|
|
|
355
360
|
`pipeline status` reports the latest run of that pipeline. `pipeline runs` lists recent runs (default 10). `pipeline cost` forecasts what a run would bill. `pipeline cancel` and `pipeline results` take a run id.
|
|
@@ -508,11 +513,13 @@ The catalog is bound to the installed CLI version, so a skill that a newer relea
|
|
|
508
513
|
|
|
509
514
|
`toloka pipeline run --wait` maps the run outcome to the process exit code.
|
|
510
515
|
|
|
511
|
-
| Code | `pipeline run --wait`
|
|
512
|
-
| ---- |
|
|
513
|
-
| 0 | Run succeeded, or was accepted without `--wait`
|
|
514
|
-
| 1 | Run failed
|
|
515
|
-
| 2 | Run remained active when the wait window closed | A required checker could not run |
|
|
516
|
+
| Code | `pipeline run --wait` | `check` / apply local gate |
|
|
517
|
+
| ---- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
|
|
518
|
+
| 0 | Run succeeded, or was accepted without `--wait` | All required local checks passed |
|
|
519
|
+
| 1 | Run failed, or the platform refused to start it | Source, types, tests, or required coverage failed |
|
|
520
|
+
| 2 | Run remained active when the wait window closed, or lints were still running and no run was started | A required checker could not run |
|
|
521
|
+
|
|
522
|
+
The output tells the two exit-`2` cases apart: `Did not start a run of <name>: its lints are still running` means no run exists yet, so run `pipeline run` again later instead of polling `pipeline status`.
|
|
516
523
|
|
|
517
524
|
Every other failure exits non-zero with a message on stderr: missing project identity, a rejected login, an unauthenticated `auth-status`, or a failed `secret set`.
|
|
518
525
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -79,7 +79,11 @@ If it lists no new run, retry. The same call has succeeded minutes later with no
|
|
|
79
79
|
|
|
80
80
|
## Exit code 2 from toloka pipeline run
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
Read the output first: exit `2` has two causes.
|
|
83
|
+
|
|
84
|
+
If it says `Did not start a run of <name>: its lints are still running`, no run was created. The platform rejects a run until the lints of the nodes on the path from Start to End have finished. Do not look for a run with `toloka pipeline status`: wait for the lints with `toloka pipeline lint <name> --wait`, then run `toloka pipeline run` again.
|
|
85
|
+
|
|
86
|
+
Otherwise the timeout closed while the run was still in progress. That is not a failure of the run and not a failure of your pipeline. A queue with no one working it stays in progress indefinitely, so report what you observed — item counts and current states — rather than waiting unbounded:
|
|
83
87
|
|
|
84
88
|
```sh
|
|
85
89
|
toloka pipeline status <name>
|
|
@@ -2323,6 +2323,18 @@ export interface RunItem {
|
|
|
2323
2323
|
state: RunItemState;
|
|
2324
2324
|
}
|
|
2325
2325
|
|
|
2326
|
+
export type RunLintsResultRejectedItem = {
|
|
2327
|
+
nodeId: string;
|
|
2328
|
+
checkId: string;
|
|
2329
|
+
message: string;
|
|
2330
|
+
};
|
|
2331
|
+
|
|
2332
|
+
export interface RunLintsResult {
|
|
2333
|
+
checks: CheckJob[];
|
|
2334
|
+
rejected: RunLintsResultRejectedItem[];
|
|
2335
|
+
nodesInUse: string[];
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2326
2338
|
export type BatchInferenceModel = {
|
|
2327
2339
|
type: 'base_model';
|
|
2328
2340
|
baseModelId: 'qwen3-4b' | 'qwen3-8b' | 'qwen3-32b' | 'qwen3-235b';
|
package/openapi/gen/api.ts
CHANGED
|
@@ -102,6 +102,7 @@ import type {
|
|
|
102
102
|
RunItem,
|
|
103
103
|
RunItemId,
|
|
104
104
|
RunJob,
|
|
105
|
+
RunLintsResult,
|
|
105
106
|
SampleDatasetItemsBody,
|
|
106
107
|
SampleJob,
|
|
107
108
|
SampleResult,
|
|
@@ -2521,6 +2522,27 @@ return potatoFetch<CheckJob>(getCreatePipelineCheckUrl(pipelineId),
|
|
|
2521
2522
|
|
|
2522
2523
|
|
|
2523
2524
|
|
|
2525
|
+
export const getRunPipelineLintsUrl = (pipelineId: PipelineId,) => {
|
|
2526
|
+
|
|
2527
|
+
|
|
2528
|
+
|
|
2529
|
+
|
|
2530
|
+
return `/api/v2-beta/pipelines/${pipelineId}/lints`
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
export const runPipelineLints = async (pipelineId: PipelineId, options?: Parameters<typeof potatoFetch>[1]): Promise<RunLintsResult> => {
|
|
2534
|
+
|
|
2535
|
+
return potatoFetch<RunLintsResult>(getRunPipelineLintsUrl(pipelineId),
|
|
2536
|
+
{
|
|
2537
|
+
...options,
|
|
2538
|
+
method: 'POST'
|
|
2539
|
+
|
|
2540
|
+
|
|
2541
|
+
}
|
|
2542
|
+
);}
|
|
2543
|
+
|
|
2544
|
+
|
|
2545
|
+
|
|
2524
2546
|
export const getGetPipelineCheckUrl = (checkRunId: CheckRunId,) => {
|
|
2525
2547
|
|
|
2526
2548
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toloka-platform/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Build, inspect, and operate Toloka pipeline projects as TypeScript source. The public API may change between minor versions before 1.0.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -77,6 +77,6 @@
|
|
|
77
77
|
"bun": ">=1.4.0"
|
|
78
78
|
},
|
|
79
79
|
"tolokaBuild": {
|
|
80
|
-
"commit": "
|
|
80
|
+
"commit": "1b8cbf8c1fd6ce6a32b5c8de98a7772fdcc3afe3"
|
|
81
81
|
}
|
|
82
82
|
}
|
package/skills/catalog.json
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"testing",
|
|
12
12
|
"checks"
|
|
13
13
|
],
|
|
14
|
-
"version": "0.
|
|
15
|
-
"digest": "
|
|
14
|
+
"version": "0.16.0",
|
|
15
|
+
"digest": "d029c12c76c569ad6ed9f12713890344e82c35445ba0f310d3aa2adf6f0610a5",
|
|
16
16
|
"files": [
|
|
17
17
|
"SKILL.md",
|
|
18
18
|
"references/human-work.md",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"bug",
|
|
34
34
|
"report"
|
|
35
35
|
],
|
|
36
|
-
"version": "0.
|
|
36
|
+
"version": "0.16.0",
|
|
37
37
|
"digest": "70f0bd2c80b32f87896aeb27c363b8a2d37f40e22424f57fe1aad142e125ad67",
|
|
38
38
|
"files": [
|
|
39
39
|
"SKILL.md",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"getting-started",
|
|
51
51
|
"agent"
|
|
52
52
|
],
|
|
53
|
-
"version": "0.
|
|
53
|
+
"version": "0.16.0",
|
|
54
54
|
"digest": "2278adeb46a792356579395fcf73c42322cb23bcd0c61e20856ccabdad6762d1",
|
|
55
55
|
"files": [
|
|
56
56
|
"SKILL.md"
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"sft",
|
|
68
68
|
"models"
|
|
69
69
|
],
|
|
70
|
-
"version": "0.
|
|
70
|
+
"version": "0.16.0",
|
|
71
71
|
"digest": "3ce10dee5d3c283d8b5fdea15134a58fc738c7af2c1bb19eb5a0bf70ea0bca13",
|
|
72
72
|
"files": [
|
|
73
73
|
"SKILL.md"
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"prompt",
|
|
85
85
|
"models"
|
|
86
86
|
],
|
|
87
|
-
"version": "0.
|
|
87
|
+
"version": "0.16.0",
|
|
88
88
|
"digest": "be8bce8d9cf7241ccb60d2b2a9b7cc07d7755a76e8ff8ec1b0847c49efbb3469",
|
|
89
89
|
"files": [
|
|
90
90
|
"SKILL.md"
|
|
@@ -87,12 +87,12 @@ Let constructors mint IDs for new declarations. Preserve `id` options generated
|
|
|
87
87
|
| Status | Meaning |
|
|
88
88
|
| --- | --- |
|
|
89
89
|
| `valid` | No blocking error. Warnings are informational. |
|
|
90
|
-
| `checks-pending` | The only remaining issues are stored test cases or lints that have not run on this graph (`not_ran_node_check`).
|
|
90
|
+
| `checks-pending` | The only remaining issues are stored test cases or lints that have not run on this graph (`not_ran_node_check`). Run the lints with `toloka pipeline lint <name> --wait`; the user runs stored test cases from the node's Checks tab. Neither replaces a passing local gate. |
|
|
91
91
|
| `invalid` | A source/spec error, a failed check (`failed_node_check`), or a lint error. Fix source/spec errors before plan or apply. Failed checks and lint errors do not block apply, so you can apply a fix for them and validate again, but a run cannot start until they are gone. |
|
|
92
92
|
| `checks-running` | Only with `--wait`: a lint was still running when the wait ended. |
|
|
93
93
|
| `skipped` | The pipeline or its dataset is not on the platform yet; `toloka apply` creates it. |
|
|
94
94
|
|
|
95
|
-
The platform's lints, such as the Task UI interface lint and the quality-criteria lint, start by themselves a few seconds after every apply and take about a minute. Until they finish, plain `toloka validate` lists each as a `running_node_check` warning and can still print `valid` with exit 0, so it does not prove the lints passed. After applying a change, run `toloka validate --wait`: it validates again every 5 seconds, for up to 5 minutes, until no lint is running, and does not wait for a pipeline whose source has unapplied changes. Report the pipeline as valid only after that. When a reviewed plan adds a dataset field already bound by an existing live pipeline, pre-apply validation can report that exact field as missing; apply only the reviewed additive plan, then require post-apply validation to clear source/spec errors. Never rewrite valid source because a stored test case on the Checks tab has not run.
|
|
95
|
+
The platform's lints, such as the Task UI interface lint and the quality-criteria lint, start by themselves a few seconds after every apply and take about a minute. Until they finish, plain `toloka validate` lists each as a `running_node_check` warning and can still print `valid` with exit 0, so it does not prove the lints passed. After applying a change, run `toloka validate --wait`: it validates again every 5 seconds, for up to 5 minutes, until no lint is running, and does not wait for a pipeline whose source has unapplied changes. Report the pipeline as valid only after that. A lint does not restart by itself when dataset items or their values change, or after it fails or times out (after 5 minutes running or 15 minutes waiting to start): run `toloka pipeline lint <name> --wait`, which starts every lint without a current result and waits for it, then validate again. When a reviewed plan adds a dataset field already bound by an existing live pipeline, pre-apply validation can report that exact field as missing; apply only the reviewed additive plan, then require post-apply validation to clear source/spec errors. Never rewrite valid source because a stored test case on the Checks tab has not run.
|
|
96
96
|
|
|
97
97
|
Register every code-node behavior with `test.code(pipeline, node, name, callback)`; its callback receives a bound `run(inputs, options?)` helper with the node's Python execution contract, and the test counts only after `run` executes, an assertion runs, and the callback passes. Register required human-work behavior with `test.ui(pipeline, node, name, callback)`; its callback receives a bound `render(options?)`, and the test counts only after a successful `submit`, at least one assertion, and a passing callback. Add at least one `test.pipeline(pipeline, name, callback)` scenario: treat the fixture as `root.started` item state, execute connected nodes in order with the callback's bound `runCode` and `renderUi` helpers, merge only each node's declared outputs, and assert the final `root.completed` fields. Cover meaningful match branches, optional paths, and failures as well as the happy path.
|
|
98
98
|
|
|
@@ -109,11 +109,11 @@ When plan, apply, or validate fails, follow [Recover safely](references/workflow
|
|
|
109
109
|
|
|
110
110
|
## Run an applied pipeline
|
|
111
111
|
|
|
112
|
-
Before starting a run, require a passing `toloka check` and no blocking error from `toloka validate`. A `
|
|
112
|
+
Before starting a run, require a passing `toloka check` and no blocking error from `toloka validate`. A run cannot start until every lint of the pipeline has finished: `toloka pipeline run` first starts the lints without a current result and waits for them, up to 5 minutes or its `--timeout`, which bounds the whole command. It exits `2` without starting the run when a lint is still running at the end of that wait, and the platform rejects the run when a lint on a connected node failed. Run `toloka pipeline cost <name>` and present the forecast for user approval before starting the run. If it skips a billed node, report the authored rate and a labeled labor floor instead of presenting `$0.00` as the price; make the missing pricing evidence clear when seeking approval.
|
|
113
113
|
|
|
114
|
-
Start a run with `toloka pipeline run <name> --wait --timeout <ms>`, then read its exit code: `0` succeeded, `1` reached a failed terminal state, `2` still running when the window closed. Without `--wait`, exit `0` only means the run was accepted. Nothing reports back later, so never describe a started run as a finished one. Name the live pipeline; there is no implicit only-pipeline.
|
|
114
|
+
Start a run with `toloka pipeline run <name> --wait --timeout <ms>`, then read its exit code: `0` succeeded, `1` reached a failed terminal state or the platform refused to start the run, `2` still running when the window closed. Exit `2` with `Did not start a run of <name>: its lints are still running` means no run was created: do not poll `toloka pipeline status` for it, run `toloka pipeline lint <name> --wait`, then start the run again. Without `--wait`, exit `0` only means the run was accepted. Nothing reports back later, so never describe a started run as a finished one. Name the live pipeline; there is no implicit only-pipeline.
|
|
115
115
|
|
|
116
|
-
Keep monitoring bounded so the user receives a useful update even when expert work or queues take time. On exit `2
|
|
116
|
+
Keep monitoring bounded so the user receives a useful update even when expert work or queues take time. On exit `2` after the command printed a run id, report the observed item counts and states and say the run is still in progress; choose a further bounded check only when it serves the request. Use `toloka pipeline status <name>` and `toloka pipeline results <runId> --failed` for counts, states, and errors. `toloka status` (no prefix) is project health, not the latest run.
|
|
117
117
|
|
|
118
118
|
## Report with evidence
|
|
119
119
|
|
|
@@ -82,7 +82,9 @@ The platform generates `main_types.py` from the bound fields when it runs the no
|
|
|
82
82
|
|
|
83
83
|
The platform runs code nodes on Python 3.12 with the standard library only, and a node cannot install packages. Call HTTP and LLM APIs with `urllib.request` or another standard module, not a vendor SDK. `test.code` runs your local Python, so an import of a package that happens to be installed locally passes `toloka check` and still fails on the platform.
|
|
84
84
|
|
|
85
|
-
Bind every environment variable the code reads in `envVars`. Reference project secrets by name and set their values separately with `toloka secret set <name>`; use `os.environ['NAME']` in Python. The node's runtime has no ambient credentials.
|
|
85
|
+
Bind every environment variable the code reads in `envVars`. Reference project secrets by name and set their values separately with `toloka secret set <name>`; use `os.environ['NAME']` in Python. The node's runtime has no ambient credentials: a key that your own environment happens to hold, such as a coding agent's model credentials, is not available on the platform, so never use one in node code, test cases or scripts.
|
|
86
|
+
|
|
87
|
+
Claude Opus 4.7 and later and Claude Sonnet 5 and later reject sampling parameters: when a code node calls one of them, omit `temperature`, `top_p` and `top_k`. Claude 5.5 models also reject forced tool use, so do not set `tool_choice` to `any` or to a specific tool.
|
|
86
88
|
|
|
87
89
|
### Traps
|
|
88
90
|
|
|
@@ -70,6 +70,7 @@ Run the complete local gate first, then validate every bound pipeline remotely:
|
|
|
70
70
|
toloka check
|
|
71
71
|
toloka validate # before apply
|
|
72
72
|
toloka validate --wait # after apply: waits for the platform's lints
|
|
73
|
+
toloka pipeline lint <name> --wait # after dataset changes or a failed lint: reruns the lints
|
|
73
74
|
```
|
|
74
75
|
|
|
75
76
|
`toloka check` requires no credentials or live pipeline; SKILL.md describes what it runs and what its statuses mean. Use `toloka check <pipeline> --node <name-or-id>` for a focused edit loop, `--watch` for serialized reruns, and `--json` for automation.
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
listPipelineRuns,
|
|
9
9
|
pipelineRunResults,
|
|
10
10
|
pipelineRunStatus,
|
|
11
|
+
runPipelineLints,
|
|
11
12
|
startPipelineRun,
|
|
12
13
|
} from "../runs";
|
|
13
14
|
|
|
@@ -37,7 +38,11 @@ export const registerPipelineCommands = (program: Command, host: CommandHost): v
|
|
|
37
38
|
.command("run")
|
|
38
39
|
.argument("[name]", pipelineNameHelp)
|
|
39
40
|
.option("--wait", "wait for the run to reach a terminal state", false)
|
|
40
|
-
.option(
|
|
41
|
+
.option(
|
|
42
|
+
"--timeout <ms>",
|
|
43
|
+
"how long the command may wait in total, for the lints and, with --wait, for the run",
|
|
44
|
+
positiveInteger,
|
|
45
|
+
)
|
|
41
46
|
.description("start a run on an applied pipeline")
|
|
42
47
|
.action(async (name: string | undefined, options: { wait: boolean; timeout?: number }) => {
|
|
43
48
|
const { client, state, projectId } = await resolveContext(globals(), context);
|
|
@@ -53,6 +58,26 @@ export const registerPipelineCommands = (program: Command, host: CommandHost): v
|
|
|
53
58
|
context.io.stdout(result.output);
|
|
54
59
|
exitCode.value = exitCodeForOutcome(result.outcome);
|
|
55
60
|
});
|
|
61
|
+
pipeline
|
|
62
|
+
.command("lint")
|
|
63
|
+
.argument("[name]", pipelineNameHelp)
|
|
64
|
+
.option("--wait", "wait for the lints to finish", false)
|
|
65
|
+
.option("--timeout <ms>", "how long to wait", positiveInteger)
|
|
66
|
+
.description("run the pipeline's lints that have no current result")
|
|
67
|
+
.action(async (name: string | undefined, options: { wait: boolean; timeout?: number }) => {
|
|
68
|
+
const { client, state, projectId } = await resolveContext(globals(), context);
|
|
69
|
+
assertProjectSelected(projectId);
|
|
70
|
+
const result = await runPipelineLints(client, {
|
|
71
|
+
projectId,
|
|
72
|
+
state,
|
|
73
|
+
...(name === undefined ? {} : { pipeline: name }),
|
|
74
|
+
wait: options.wait,
|
|
75
|
+
...(options.timeout === undefined ? {} : { timeoutMs: options.timeout }),
|
|
76
|
+
...(context.io.select === undefined ? {} : { select: context.io.select }),
|
|
77
|
+
});
|
|
78
|
+
context.io.stdout(result.output);
|
|
79
|
+
exitCode.value = exitCodeForOutcome(result.outcome);
|
|
80
|
+
});
|
|
56
81
|
pipeline
|
|
57
82
|
.command("status")
|
|
58
83
|
.argument("[name]", pipelineNameHelp)
|
package/src/cli/runs.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { PotatoApiError } from "../errors";
|
|
|
4
4
|
import type { TolokaState } from "../state";
|
|
5
5
|
import type { RunItem, RunJob } from "../index";
|
|
6
6
|
import { assertCli } from "./cli-error";
|
|
7
|
+
import { apiErrorDetails } from "./format-error";
|
|
7
8
|
|
|
8
9
|
export type PipelineRef = {
|
|
9
10
|
readonly pipelineId: string;
|
|
@@ -249,6 +250,144 @@ type PipelineCommandOptions = {
|
|
|
249
250
|
readonly select?: (choices: readonly { name: string; value: string }[]) => Promise<string>;
|
|
250
251
|
};
|
|
251
252
|
|
|
253
|
+
const LINT_WAIT_TIMEOUT_MS = 5 * 60_000;
|
|
254
|
+
const LINT_POLL_INTERVAL_MS = 5_000;
|
|
255
|
+
|
|
256
|
+
type LintJob = Awaited<ReturnType<PotatoClient["checks"]["runLints"]>>["checks"][number];
|
|
257
|
+
|
|
258
|
+
const isLintFinished = (job: LintJob) => job.status === "succeeded" || job.status === "failed";
|
|
259
|
+
|
|
260
|
+
const waitForLints = async (
|
|
261
|
+
client: PotatoClient,
|
|
262
|
+
jobs: readonly LintJob[],
|
|
263
|
+
timeoutMs: number,
|
|
264
|
+
intervalMs: number,
|
|
265
|
+
): Promise<readonly LintJob[]> => {
|
|
266
|
+
const deadline = Date.now() + timeoutMs;
|
|
267
|
+
let current = jobs;
|
|
268
|
+
while (!current.every(isLintFinished) && Date.now() < deadline) {
|
|
269
|
+
await new Promise<void>((resolve) => {
|
|
270
|
+
setTimeout(resolve, Math.min(intervalMs, deadline - Date.now()));
|
|
271
|
+
});
|
|
272
|
+
current = await Promise.all(
|
|
273
|
+
current.map(async (job) => (isLintFinished(job) ? job : client.checks.get({ checkRunId: job.id }))),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return current;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const runLints = async (
|
|
280
|
+
client: PotatoClient,
|
|
281
|
+
pipelineId: string,
|
|
282
|
+
options: {
|
|
283
|
+
readonly wait: boolean;
|
|
284
|
+
readonly nodesInUseOnly: boolean;
|
|
285
|
+
readonly timeoutMs?: number;
|
|
286
|
+
readonly intervalMs?: number;
|
|
287
|
+
},
|
|
288
|
+
): Promise<{ readonly outcome: RunOutcome; readonly lines: readonly string[] }> => {
|
|
289
|
+
const result = await client.checks.runLints({ pipelineId });
|
|
290
|
+
const inScope = (nodeId: string) => !options.nodesInUseOnly || result.nodesInUse.includes(nodeId);
|
|
291
|
+
const checks = result.checks.filter((job) => inScope(job.nodeId));
|
|
292
|
+
const rejected = result.rejected.filter((entry) => inScope(entry.nodeId));
|
|
293
|
+
const jobs = options.wait
|
|
294
|
+
? await waitForLints(
|
|
295
|
+
client,
|
|
296
|
+
checks,
|
|
297
|
+
options.timeoutMs ?? LINT_WAIT_TIMEOUT_MS,
|
|
298
|
+
options.intervalMs ?? LINT_POLL_INTERVAL_MS,
|
|
299
|
+
)
|
|
300
|
+
: checks;
|
|
301
|
+
const outcome: RunOutcome =
|
|
302
|
+
options.wait && !jobs.every(isLintFinished)
|
|
303
|
+
? "timedOut"
|
|
304
|
+
: rejected.length > 0 || jobs.some((job) => job.status === "failed")
|
|
305
|
+
? "failed"
|
|
306
|
+
: jobs.every(isLintFinished)
|
|
307
|
+
? "succeeded"
|
|
308
|
+
: "started";
|
|
309
|
+
return {
|
|
310
|
+
outcome,
|
|
311
|
+
lines: [
|
|
312
|
+
...jobs.map(
|
|
313
|
+
(job) =>
|
|
314
|
+
` ${job.status} ${job.nodeId}/${job.checkId}${job.error === undefined || job.error === null ? "" : `: ${job.error}`}`,
|
|
315
|
+
),
|
|
316
|
+
...rejected.map((entry) => ` rejected ${entry.nodeId}/${entry.checkId}: ${entry.message}`),
|
|
317
|
+
],
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
export async function runPipelineLints(
|
|
322
|
+
client: PotatoClient,
|
|
323
|
+
options: PipelineCommandOptions & {
|
|
324
|
+
readonly wait?: boolean;
|
|
325
|
+
readonly timeoutMs?: number;
|
|
326
|
+
readonly intervalMs?: number;
|
|
327
|
+
},
|
|
328
|
+
): Promise<RunCommandResult> {
|
|
329
|
+
const ref = await resolvePipelineRef({ client, ...options });
|
|
330
|
+
const { outcome, lines } = await runLints(client, ref.pipelineId, {
|
|
331
|
+
wait: options.wait === true,
|
|
332
|
+
nodesInUseOnly: false,
|
|
333
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
334
|
+
...(options.intervalMs === undefined ? {} : { intervalMs: options.intervalMs }),
|
|
335
|
+
});
|
|
336
|
+
const summary =
|
|
337
|
+
outcome === "succeeded"
|
|
338
|
+
? `All lints of ${ref.name} have finished. Run toloka validate to read their findings.`
|
|
339
|
+
: outcome === "failed"
|
|
340
|
+
? `Some lints of ${ref.name} failed or could not start. Fix the reported problems, then run toloka pipeline lint again.`
|
|
341
|
+
: outcome === "timedOut"
|
|
342
|
+
? `Some lints of ${ref.name} are still running. Run toloka pipeline lint --wait again.`
|
|
343
|
+
: `Started the lints of ${ref.name}. Follow them with toloka pipeline lint --wait.`;
|
|
344
|
+
return { outcome, output: [summary, ...lines].join("\n") };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const startRunAfterLints = async (
|
|
348
|
+
client: PotatoClient,
|
|
349
|
+
ref: PipelineRef,
|
|
350
|
+
options: { readonly timeoutMs?: number; readonly intervalMs?: number },
|
|
351
|
+
): Promise<{ readonly run: RunJob } | { readonly result: RunCommandResult }> => {
|
|
352
|
+
const lints = await runLints(client, ref.pipelineId, {
|
|
353
|
+
wait: true,
|
|
354
|
+
nodesInUseOnly: true,
|
|
355
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
356
|
+
...(options.intervalMs === undefined ? {} : { intervalMs: options.intervalMs }),
|
|
357
|
+
}).catch((error: unknown) => {
|
|
358
|
+
if (error instanceof PotatoApiError && error.status === 403) return undefined;
|
|
359
|
+
throw error;
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
if (lints?.outcome === "timedOut") {
|
|
363
|
+
return {
|
|
364
|
+
result: {
|
|
365
|
+
outcome: "timedOut",
|
|
366
|
+
output: [
|
|
367
|
+
`Did not start a run of ${ref.name}: its lints are still running. Run toloka pipeline run again once toloka pipeline lint --wait reports them finished.`,
|
|
368
|
+
...lints.lines,
|
|
369
|
+
].join("\n"),
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
return { run: await client.runs.start({ pipelineId: ref.pipelineId }) };
|
|
376
|
+
} catch (error) {
|
|
377
|
+
if (lints?.outcome !== "failed" || !(error instanceof PotatoApiError) || error.status === undefined) throw error;
|
|
378
|
+
return {
|
|
379
|
+
result: {
|
|
380
|
+
outcome: "failed",
|
|
381
|
+
output: [
|
|
382
|
+
`Could not start a run of ${ref.name}: ${apiErrorDetails(error) ?? error.message}`,
|
|
383
|
+
"Its lints:",
|
|
384
|
+
...lints.lines,
|
|
385
|
+
].join("\n"),
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
252
391
|
export async function startPipelineRun(
|
|
253
392
|
client: PotatoClient,
|
|
254
393
|
options: PipelineCommandOptions & {
|
|
@@ -258,7 +397,12 @@ export async function startPipelineRun(
|
|
|
258
397
|
},
|
|
259
398
|
): Promise<RunCommandResult> {
|
|
260
399
|
const ref = await resolvePipelineRef({ client, ...options });
|
|
261
|
-
const
|
|
400
|
+
const startedAt = Date.now();
|
|
401
|
+
const preparation = await startRunAfterLints(client, ref, options);
|
|
402
|
+
|
|
403
|
+
if ("result" in preparation) return preparation.result;
|
|
404
|
+
|
|
405
|
+
const started = preparation.run;
|
|
262
406
|
|
|
263
407
|
if (options.wait !== true) {
|
|
264
408
|
return {
|
|
@@ -274,7 +418,9 @@ export async function startPipelineRun(
|
|
|
274
418
|
const finished = await client.waitFor(
|
|
275
419
|
{ type: "pipelineRun", runId: started.id },
|
|
276
420
|
{
|
|
277
|
-
...(options.timeoutMs === undefined
|
|
421
|
+
...(options.timeoutMs === undefined
|
|
422
|
+
? {}
|
|
423
|
+
: { timeoutMs: Math.max(0, options.timeoutMs - (Date.now() - startedAt)) }),
|
|
278
424
|
...(options.intervalMs === undefined ? {} : { intervalMs: options.intervalMs }),
|
|
279
425
|
},
|
|
280
426
|
);
|
package/src/client.ts
CHANGED
|
@@ -790,6 +790,8 @@ const createClientImplementation = (config: ClientConfig) => {
|
|
|
790
790
|
wire.createPipelineCheck(pipelineId, body, requestOptions),
|
|
791
791
|
get: ({ checkRunId }: { checkRunId: CheckRunId }) =>
|
|
792
792
|
wire.getPipelineCheck(checkRunId, requestOptions),
|
|
793
|
+
runLints: ({ pipelineId }: { pipelineId: PipelineId }) =>
|
|
794
|
+
wire.runPipelineLints(pipelineId, requestOptions),
|
|
793
795
|
};
|
|
794
796
|
|
|
795
797
|
const deliveryOrders = {
|
|
@@ -1478,7 +1478,8 @@
|
|
|
1478
1478
|
},
|
|
1479
1479
|
"required": [
|
|
1480
1480
|
"qaMode"
|
|
1481
|
-
]
|
|
1481
|
+
],
|
|
1482
|
+
"additionalProperties": false
|
|
1482
1483
|
},
|
|
1483
1484
|
{
|
|
1484
1485
|
"type": "object",
|
|
@@ -1513,7 +1514,8 @@
|
|
|
1513
1514
|
},
|
|
1514
1515
|
"required": [
|
|
1515
1516
|
"qaMode"
|
|
1516
|
-
]
|
|
1517
|
+
],
|
|
1518
|
+
"additionalProperties": false
|
|
1517
1519
|
}
|
|
1518
1520
|
]
|
|
1519
1521
|
},
|