Skip to content

Commit 44eef73

Browse files
committed
Merge branch 'add/juicefs-mounting' of https://github.com/transformerlab/transformerlab-app into add/juicefs-mounting
2 parents 63ea25b + 4c22dcc commit 44eef73

11 files changed

Lines changed: 121 additions & 19 deletions

File tree

.agents/skills/transformerlab-cli/SKILL.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,19 +162,26 @@ github_repo_branch: main
162162

163163
To validate without creating, use `lab task add ./my-task --dry-run`.
164164

165-
### Editing task.yaml for an existing task
165+
### Editing an existing task
166166

167-
Use `lab task edit` to update the `task.yaml` for a task that already exists on the server:
167+
Use `lab task edit` to update an existing task on the server. Three modes:
168168

169169
```bash
170-
# Interactive editor flow
170+
# Interactive editor flow (opens $EDITOR with current task.yaml)
171171
lab task edit TASK_ID
172172
173-
# Apply a local task.yaml directly
173+
# Replace ONLY task.yaml (leaves main.py and other attachments untouched)
174174
lab task edit TASK_ID --from-file ./task.yaml --no-interactive
175+
176+
# Replace task.yaml AND attachments (main.py, configs, etc.) from a directory
177+
lab task edit TASK_ID --from-dir ./my-task --no-interactive
175178
```
176179

177-
`lab task edit` expects a `TASK_ID` and supports `--from-file`, `--no-interactive`, and `--timeout`.
180+
**Choosing between `--from-file` and `--from-dir`:**
181+
- `--from-file <task.yaml>` only updates the YAML. Use it when you're tweaking parameters or the `run` command and the rest of the task directory is unchanged.
182+
- `--from-dir <directory>` zips and uploads the whole directory (must contain `task.yaml`), replacing the task's files server-side. Use it when you've also modified `main.py` or any sibling files. **If the task's `run` references a script (e.g. `python main.py`), reuploading just the YAML will leave the old script in place — use `--from-dir` to keep them in sync.**
183+
184+
`--from-file` and `--from-dir` are mutually exclusive. `lab task edit` also supports `--no-interactive`, `--dry-run` (with `--from-dir`), and `--timeout`.
178185

179186
### Uploading extra files to an existing task
180187

@@ -690,7 +697,7 @@ This applies to launching jobs, fetching logs, checking cluster status, and ever
690697
| `lab task info <id>` | Get task details | Yes |
691698
| `lab task init` | Scaffold `task.yaml` + `main.py` in the current directory (`--interactive` to prompt) | No |
692699
| `lab task add [dir]` | Add task from directory or `--from-git` URL (`--no-interactive`, `--dry-run`) | Yes |
693-
| `lab task edit <id>` | Edit an existing task's `task.yaml` (`--from-file`, `--no-interactive`, `--timeout`) | Yes |
700+
| `lab task edit <id>` | Edit an existing task. `--from-file <task.yaml>` for YAML-only; `--from-dir <dir>` to also replace attachments like `main.py` (`--no-interactive`, `--dry-run`, `--timeout`) | Yes |
694701
| `lab task upload <id> <path>` | Upload files/directories into an existing task (`--no-interactive`) | Yes |
695702
| `lab task delete <id>` | Delete a task (`--no-interactive` to skip confirmation) | Yes |
696703
| `lab task queue <id>` | Queue task on compute provider (`-m/--description` for a markdown run note; `-p/--param key=value` to override task parameters per run; required for agents, see "Always write a run description") | Yes |

.agents/skills/transformerlab-cli/references/commands.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,25 @@ Add a new task from a local directory containing `task.yaml`, or from a Git repo
120120

121121
**JSON output:** Returns the created task object.
122122

123+
### `task edit <task_id>`
124+
125+
Update an existing task on the server. Three modes:
126+
127+
| Option | Description |
128+
|---|---|
129+
| (no flag) | Interactive — fetches current `task.yaml`, opens `$EDITOR`, validates and PUTs it back. YAML-only. |
130+
| `--from-file <path>` | Replace **only** `task.yaml` from the given path. Leaves `main.py` and other attachments untouched on the server. |
131+
| `--from-dir <path>` | Replace `task.yaml` **and** sibling files (e.g. `main.py`, configs). Zips the directory, uploads, and applies. **Use this whenever the task's `run` references a script file you've also modified — `--from-file` alone will desync the YAML from the script.** |
132+
| `--no-interactive` | Skip confirmation prompt (required in automated contexts) |
133+
| `--dry-run` | With `--from-dir`, preview the upload without submitting |
134+
| `--timeout <seconds>` | Request timeout for fetch/validate/save (default: 300) |
135+
136+
`--from-file` and `--from-dir` are mutually exclusive.
137+
138+
### `task upload <task_id> <path>`
139+
140+
Upload additional files (or a whole directory) into an existing task without touching `task.yaml`. Useful when you want to add an attachment to a task whose YAML is already correct.
141+
123142
### `task delete <task_id>`
124143

125144
Delete a task by ID.

api/transformerlab/services/compute_provider/launch_template.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,13 @@ async def launch_template_on_provider(
598598
if await storage.isdir(task_src):
599599
workspace_job_dir = await get_job_dir(job_id, request.experiment_id)
600600
await copy_task_files_to_dir(task_src, workspace_job_dir)
601+
# The local provider runs the user command with cwd=local_provider_job_dir,
602+
# which is a separate directory from workspace_job_dir. Copy task files
603+
# there too so commands like `python main.py` resolve relative to cwd.
604+
if provider.type == ProviderType.LOCAL.value:
605+
local_job_dir = provider_config_dict.get("workspace_dir")
606+
if local_job_dir:
607+
await copy_task_files_to_dir(task_src, local_job_dir)
601608

602609
job_data = {
603610
"task_name": request.task_name,

cli/src/transformerlab_cli/commands/task.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -724,11 +724,42 @@ def command_task_validate(
724724
def command_task_edit(
725725
task_id: str = typer.Argument(..., help="Task ID to update"),
726726
from_file: str | None = typer.Option(None, "--from-file", help="Path to task.yaml to apply directly"),
727+
from_dir: str | None = typer.Option(
728+
None,
729+
"--from-dir",
730+
help="Path to a directory containing task.yaml (and any attachments) to fully replace task contents",
731+
),
732+
dry_run: bool = typer.Option(
733+
False,
734+
"--dry-run",
735+
help="Preview the task update without submitting it (only applies with --from-dir)",
736+
),
727737
no_interactive: bool = typer.Option(False, "--no-interactive", help="Skip confirmation prompt"),
728738
timeout: int = typer.Option(300, "--timeout", help="Request timeout in seconds for fetch/validate/save"),
729739
):
730-
"""Edit an existing task's task.yaml (interactive by default)."""
740+
"""Edit an existing task's task.yaml (interactive by default).
741+
742+
Use --from-file to replace only task.yaml, or --from-dir to replace task.yaml
743+
plus any sibling files (e.g. main.py) in the task directory.
744+
"""
745+
if from_file and from_dir:
746+
console.print("[error]Error:[/error] --from-file and --from-dir are mutually exclusive")
747+
raise typer.Exit(1)
748+
if dry_run and not from_dir:
749+
console.print("[warning]Warning:[/warning] --dry-run only applies with --from-dir; ignoring.")
750+
731751
current_experiment = require_current_experiment()
752+
753+
if from_dir:
754+
edit_task_from_directory(
755+
task_id=task_id,
756+
task_directory_path=from_dir,
757+
experiment_id=current_experiment,
758+
dry_run=dry_run,
759+
interactive=not no_interactive,
760+
)
761+
return
762+
732763
edit_task_yaml(
733764
task_id=task_id,
734765
experiment_id=current_experiment,

cli/tests/commands/test_task.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,45 @@ def test_task_edit_updates_yaml_from_file(_mock_exp, _mock_get, mock_put, _mock_
299299
assert submit_path == "/experiment/exp1/task/t1/yaml"
300300

301301

302+
@patch("transformerlab_cli.commands.task.api.post_text", return_value=_mock_resp({"valid": True}))
303+
@patch("transformerlab_cli.commands.task.api.put", return_value=_mock_resp({"received": [0]}))
304+
@patch("transformerlab_cli.commands.task.api.get", return_value=_mock_resp({"received": []}))
305+
@patch("transformerlab_cli.commands.task.api.post_json")
306+
@patch("transformerlab_cli.commands.task.require_current_experiment", return_value="exp1")
307+
def test_task_edit_from_dir_uploads_directory_zip(_mock_exp, mock_post_json, _mock_get, _mock_put, _mock_post_text):
308+
"""`lab task edit --from-dir` zips the directory and POSTs to the /edit endpoint."""
309+
mock_post_json.side_effect = [
310+
_mock_resp({"upload_id": "up-1", "chunk_size": 64 * 1024 * 1024}),
311+
_mock_resp({"status": "ok"}),
312+
_mock_resp({"id": "t1"}),
313+
]
314+
with runner.isolated_filesystem():
315+
task_dir = Path("task")
316+
task_dir.mkdir()
317+
(task_dir / "task.yaml").write_text("name: demo\nrun: python main.py\n", encoding="utf-8")
318+
(task_dir / "main.py").write_text("print('hi')\n", encoding="utf-8")
319+
result = runner.invoke(app, ["task", "edit", "t1", "--from-dir", str(task_dir), "--no-interactive"])
320+
assert result.exit_code == 0, result.output
321+
submit_path = mock_post_json.call_args_list[-1].args[0]
322+
assert submit_path == "/experiment/exp1/task/t1/edit?upload_id=up-1"
323+
324+
325+
def test_task_edit_rejects_from_file_and_from_dir_together():
326+
"""`lab task edit --from-file ... --from-dir ...` is rejected as mutually exclusive."""
327+
with runner.isolated_filesystem():
328+
task_yaml = Path("task.yaml")
329+
task_yaml.write_text("name: demo\n", encoding="utf-8")
330+
task_dir = Path("task")
331+
task_dir.mkdir()
332+
(task_dir / "task.yaml").write_text("name: demo\n", encoding="utf-8")
333+
result = runner.invoke(
334+
app,
335+
["task", "edit", "t1", "--from-file", str(task_yaml), "--from-dir", str(task_dir)],
336+
)
337+
assert result.exit_code != 0
338+
assert "mutually exclusive" in strip_ansi(result.output)
339+
340+
302341
@patch("transformerlab_cli.commands.task.api.put", return_value=_mock_resp({"received": [0]}))
303342
@patch("transformerlab_cli.commands.task.api.get", return_value=_mock_resp({"received": []}))
304343
@patch("transformerlab_cli.commands.task.api.post_json")

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"url": "https://github.com/dadmobile"
2626
}
2727
],
28-
"version": "0.37.0",
28+
"version": "0.37.2",
2929
"scripts": {
3030
"build": "dotenv -e .env -- cross-env NODE_ENV=production tsx node_modules/webpack/bin/webpack.js --config ./.erb/configs/webpack.config.cloud.prod.ts",
3131
"build:multiuser": "cross-env NODE_ENV=production tsx node_modules/webpack/bin/webpack.js --config ./.erb/configs/webpack.config.cloud.prod.ts",

test/playwright/dataset-generate-task-github.spec.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { test, expect, Page } from '@playwright/test';
22
import { login, selectFirstExperiment } from './helpers';
33

4-
const GITHUB_REPO_URL =
5-
'https://github.com/transformerlab/transformerlab-examples';
4+
const GITHUB_REPO_URL = 'https://github.com/transformerlab/transformerlab-app';
65
const GITHUB_SUBDIR =
76
'api/transformerlab/galleries/examples/demo-generate-task';
87

@@ -32,7 +31,7 @@ async function replaceTaskNameInMonaco(page: Page, taskName: string) {
3231
}
3332

3433
test.describe('Dataset Generation Task From GitHub', () => {
35-
test.setTimeout(180_000);
34+
test.setTimeout(420_000);
3635

3736
test('create task from GitHub, run local job, and require COMPLETE - 100%', async ({
3837
page,
@@ -112,7 +111,7 @@ test.describe('Dataset Generation Task From GitHub', () => {
112111
{
113112
message:
114113
'Expected job to reach COMPLETE - 100% (and never FAILED/COMPLETE - 0%)',
115-
timeout: 120000,
114+
timeout: 300000,
116115
intervals: [2000],
117116
},
118117
)

test/playwright/dataset-generate-task.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ async function replaceMonacoContents(page: Page, contents: string) {
3131
}
3232

3333
test.describe('Dataset Generation Task', () => {
34-
test.setTimeout(180_000);
34+
test.setTimeout(420_000);
3535

3636
test('create blank task, edit task.yaml, run local job, save dataset to registry, verify in Datasets page', async ({
3737
page,
@@ -118,7 +118,7 @@ run: "python demo-generate-task/fake_generate.py"
118118
{
119119
message:
120120
'Expected dataset job to reach COMPLETE - 100% (and never FAILED/COMPLETE - 0%)',
121-
timeout: 120000,
121+
timeout: 300000,
122122
intervals: [2000],
123123
},
124124
)

test/playwright/hello-world-task.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ async function replaceTaskNameInMonaco(page: Page, taskName: string) {
3232
*/
3333

3434
test.describe('Hello World Task', () => {
35-
test.setTimeout(120_000);
35+
test.setTimeout(300_000);
3636

3737
test('create blank task, run on local provider, verify output in Machine Logs', async ({
3838
page,
@@ -116,7 +116,7 @@ test.describe('Hello World Task', () => {
116116

117117
// ── Step 5: Wait for the job to complete ──
118118
await expect(queuedJobRow.getByText('COMPLETE')).toBeVisible({
119-
timeout: 60000,
119+
timeout: 180000,
120120
});
121121

122122
// ── Step 6: Open the Output modal and verify "hello" in Machine Logs ──

0 commit comments

Comments
 (0)