From 76e4009c6ea9cdad725389d7f21508af56f1fd99 Mon Sep 17 00:00:00 2001 From: Andy Choquette <78888816+andychoquette@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:56:00 -0700 Subject: [PATCH 1/2] docs(cli): add scripted submit/wait/download workflow to CLI help Improves the `deadline` CLI help so an AI agent (or a human scripting the CLI) can discover the end-to-end job workflow without trial and error: - Top-level overview + `bundle`/`job` group help now show the submit -> wait -> download-output sequence with copy-pasteable, non-interactive (--yes) examples. - `job wait` is documented as the recommended completion signal (blocks; exit 0 = success) instead of polling `job get`. - `bundle submit` explains job-attachment storage-profile prompts and how to suppress them (--known-asset-path / --yes); `job download-output` documents --ignore-storage-profiles for same-machine submit+download. - `queue paramdefs` is called out as the way to discover queue environments (e.g. Conda), since there is no dedicated list subcommand. Additive (existing help preserved). Generated and A/B-proven by an eval harness: against a real Blender render task at k=5, the revised help cut the agent's median cost ~11% (fewer exploratory commands / turns) with success held at 100%. Signed-off-by: Andy Choquette <78888816+andychoquette@users.noreply.github.com> --- .../client/cli/_groups/bundle_group.py | 32 ++++++++++++++++++- src/deadline/client/cli/_groups/job_group.py | 27 ++++++++++++++-- .../client/cli/_groups/queue_group.py | 21 +++++++++++- src/deadline/client/cli/_main.py | 20 +++++++++--- 4 files changed, 92 insertions(+), 8 deletions(-) diff --git a/src/deadline/client/cli/_groups/bundle_group.py b/src/deadline/client/cli/_groups/bundle_group.py index 619fc603e..809e4ea0f 100644 --- a/src/deadline/client/cli/_groups/bundle_group.py +++ b/src/deadline/client/cli/_groups/bundle_group.py @@ -57,6 +57,16 @@ def cli_bundle(): Use `submit` for headless/scripted submission, or `gui-submit` to review and edit parameters in a GUI before submitting. + \b + A job bundle directory must contain template.yaml (or template.json). + It may also include parameter_values.yaml and asset_references.yaml. + + \b + Scripted workflow (no prompts): + deadline bundle submit ./bundle --yes + deadline job wait --job-id + deadline job download-output --job-id --yes + \b Learn more about [job bundles](https://docs.aws.amazon.com/deadline-cloud/latest/developerguide/build-job-bundle.html) """ @@ -218,7 +228,10 @@ def _interactive_confirmation_prompt(message: str, default_response: bool) -> bo "--known-asset-path", multiple=True, help="Path that should not generate warnings when outside storage profile locations. " - "Can be specified multiple times for different paths.", + "Use this when submitting from a temporary or non-standard directory to suppress " + "the 'unknown asset paths' confirmation prompt. " + "Can be specified multiple times for different paths. " + "Equivalent to adding paths to config setting 'settings.known_asset_paths'.", ) @click.option( "--save-debug-snapshot", @@ -269,6 +282,23 @@ def bundle_submit( to see its current taskRunStatus, or `deadline job wait --job-id ` to block until the job reaches a terminal state (SUCCEEDED / FAILED / CANCELED). + \b + JOB_BUNDLE_DIR is the path to the directory containing template.yaml (or + template.json), and optionally parameter_values.yaml and + asset_references.yaml. + + \b + If asset files reference paths outside the configured storage profile + locations (settings.storage_profile_id), submission will warn about + "unknown asset paths" and prompt for confirmation. To suppress this in + scripts/agents, either pass --known-asset-path for each additional + root, or pass --yes to auto-confirm all prompts. + + \b + After submission, use `deadline job wait --job-id ` to block until + the job completes (exit code 0 = success), then `deadline job + download-output --job-id ` to retrieve results. + \b Learn more about [job bundles](https://docs.aws.amazon.com/deadline-cloud/latest/developerguide/build-job-bundle.html) """ diff --git a/src/deadline/client/cli/_groups/job_group.py b/src/deadline/client/cli/_groups/job_group.py index 21519cef9..51b9b3a08 100644 --- a/src/deadline/client/cli/_groups/job_group.py +++ b/src/deadline/client/cli/_groups/job_group.py @@ -140,6 +140,11 @@ def cli_job(): to check status, read logs, wait for completion, download output, cancel, or requeue failed tasks. + \b + For scripted/agent workflows, prefer `wait` over polling `get`: + deadline job wait --job-id # blocks, exit 0 = success + deadline job download-output --job-id --yes + \b Learn more about [Deadline Cloud jobs](https://docs.aws.amazon.com/deadline-cloud/latest/userguide/deadline-cloud-jobs.html) """ @@ -239,6 +244,11 @@ def job_get(search_term: Optional[str], **args): If exactly one job matches, shows full details. If multiple match, shows a summary list. If no arguments provided, shows the default job from config. + \b + Output includes lifecycleStatus, taskRunStatus, and taskRunStatusCounts. + To block until a job finishes, use `deadline job wait --job-id ` + instead of polling this command. + \b Learn more about [Deadline Cloud jobs](https://docs.aws.amazon.com/deadline-cloud/latest/userguide/deadline-cloud-jobs.html) """ @@ -1105,7 +1115,14 @@ def job_download_output( ): """ Download the output of a Deadline Cloud job that was saved as job - attachments. + attachments. Files are downloaded to the paths specified at submission + time (mapped via storage profiles, or unmapped with + --ignore-storage-profiles). + + \b + Pass --yes to skip confirmation prompts (useful in scripts/agents). + Pass --ignore-storage-profiles when submitting and downloading on the + same machine to skip storage profile path mapping. Scope is controlled by which ids you pass: @@ -1442,7 +1459,8 @@ def job_wait_for_completion(max_poll_interval, timeout, output, **args): Blocks until the job reaches a terminal state (SUCCEEDED, FAILED, CANCELED, SUSPENDED, or NOT_COMPATIBLE), then prints any failed - step-task combinations. + step-task combinations. This is the recommended way to poll for job + completion in scripts and automation (instead of looping on `job get`). Uses exponential backoff for polling, starting at 0.5s and doubling until reaching --max-poll-interval. @@ -1457,6 +1475,11 @@ def job_wait_for_completion(max_poll_interval, timeout, output, **args): 4 - Job was suspended 5 - Job is not compatible + \b + Example (submit then wait): + deadline bundle submit ./bundle --yes + deadline job wait --job-id job-abc123 && deadline job download-output --job-id job-abc123 --yes + \b Learn more about [Deadline Cloud jobs](https://docs.aws.amazon.com/deadline-cloud/latest/userguide/deadline-cloud-jobs.html) """ diff --git a/src/deadline/client/cli/_groups/queue_group.py b/src/deadline/client/cli/_groups/queue_group.py index 1e3f13b59..3ead7a97c 100644 --- a/src/deadline/client/cli/_groups/queue_group.py +++ b/src/deadline/client/cli/_groups/queue_group.py @@ -46,6 +46,19 @@ def cli_queue(): export queue credentials for scripting, inspect queue parameter definitions, or sync job output for all jobs in a queue. + \b + Available subcommands: + list List queues in the farm + get Get details of a queue (incl. job attachment settings) + paramdefs List parameters from queue environments (e.g. conda) + export-credentials Export temporary queue role credentials + sync-output Incrementally download output for all jobs in queue + + \b + Note: There is no subcommand for listing queue environments directly. + Use `deadline queue paramdefs` to see what parameters (and therefore + which queue environments such as Conda) are configured on a queue. + \b Learn more about [queues](https://docs.aws.amazon.com/deadline-cloud/latest/userguide/queues.html) """ @@ -200,7 +213,13 @@ def queue_paramdefs(**args): Lists the parameter definitions for a Deadline Cloud queue in the farm. The parameter definitions include all the parameters defined by the - queue environments configured for the queue. + queue environments configured for the queue. This is the way to + discover which queue environments (e.g. Conda, service-managed fleet + software) are attached to a queue and what parameters they expose. + + \b + For example, a Conda queue environment defines parameters like + CondaPackages and CondaChannels that job templates can reference. \b Learn more about [queue environments](https://docs.aws.amazon.com/deadline-cloud/latest/userguide/create-queue-environment.html) diff --git a/src/deadline/client/cli/_main.py b/src/deadline/client/cli/_main.py index 74718c5c1..8df4be5c3 100644 --- a/src/deadline/client/cli/_main.py +++ b/src/deadline/client/cli/_main.py @@ -120,12 +120,24 @@ def deadline( Common workflows: \b - Submit a job: deadline bundle submit - Monitor a job: deadline job get --job-id - Wait for a job: deadline job wait - Download output: deadline job download-output + Submit a job: deadline bundle submit [--yes] + Wait for completion: deadline job wait --job-id (exit 0=ok) + Download output: deadline job download-output --job-id [--yes] + Monitor a job: deadline job get --job-id | deadline job logs Sync all output: deadline queue sync-output + \b + Scripted end-to-end example (no interactive prompts): + deadline bundle submit ./bundle --yes + deadline job wait --job-id job-abc123 + deadline job download-output --job-id job-abc123 --yes + + \b + Configuration: + deadline config show Show current farm/queue/profile + deadline config set Set a config value + deadline auth status Check authentication state + Works with any configured AWS credentials, or with Deadline Cloud monitor for identity-provider-based login (see `deadline auth login`). From 83e050e97004fae2c0ca069e95af4236476b8bf6 Mon Sep 17 00:00:00 2001 From: Andy Choquette <78888816+andychoquette@users.noreply.github.com> Date: Tue, 14 Jul 2026 07:49:57 -0700 Subject: [PATCH 2/2] feat(cli): add --wait and --download-on-success to bundle submit Collapses the common submit -> wait -> download-output sequence into a single `deadline bundle submit` invocation, so scripts and agents don't have to chain three commands and thread the job id between them: - --wait: after submit, block until the job reaches a terminal state (reusing api.wait_for_job_completion, the same path `deadline job wait` uses) and exit non-zero if it does not succeed. - --download-on-success: implies --wait; on SUCCEEDED, download the job's output (reusing job_group._download_job_output, the same path `deadline job download-output` uses). Both are additive and default off -- existing behavior is unchanged. They no-op when there is no real job (e.g. --save-debug-snapshot yields no job id). Adds unit tests covering the non-success exit and the download-on-success path. Signed-off-by: Andy Choquette <78888816+andychoquette@users.noreply.github.com> --- .../client/cli/_groups/bundle_group.py | 50 ++++++++++++-- src/deadline/client/cli/_groups/job_group.py | 4 +- .../cli/test_cli_bundle_submit.py | 65 +++++++++++++++++++ 3 files changed, 113 insertions(+), 6 deletions(-) diff --git a/src/deadline/client/cli/_groups/bundle_group.py b/src/deadline/client/cli/_groups/bundle_group.py index 809e4ea0f..a75e87431 100644 --- a/src/deadline/client/cli/_groups/bundle_group.py +++ b/src/deadline/client/cli/_groups/bundle_group.py @@ -246,6 +246,20 @@ def _interactive_confirmation_prompt(message: str, default_response: bool) -> bo "Use when S3 bucket contents may be out of sync with local caches. " "Overrides the 'settings.force_s3_check' config setting.", ) +@click.option( + "--wait", + is_flag=True, + help="After submitting, block until the job reaches a terminal state (the " + "equivalent of running `deadline job wait` on the new job). Exits non-zero if " + "the job does not succeed.", +) +@click.option( + "--download-on-success", + is_flag=True, + help="Implies --wait: after the job SUCCEEDS, download its output to the paths " + "recorded at submission time (the equivalent of `deadline job download-output`). " + "No-op if the job does not succeed.", +) @click.argument("job_bundle_dir") @_handle_error def bundle_submit( @@ -263,6 +277,8 @@ def bundle_submit( submitter_name, save_debug_snapshot, force_s3_check, + wait, + download_on_success, **args, ): """ @@ -291,13 +307,14 @@ def bundle_submit( If asset files reference paths outside the configured storage profile locations (settings.storage_profile_id), submission will warn about "unknown asset paths" and prompt for confirmation. To suppress this in - scripts/agents, either pass --known-asset-path for each additional + non-interactive use, either pass --known-asset-path for each additional root, or pass --yes to auto-confirm all prompts. \b - After submission, use `deadline job wait --job-id ` to block until - the job completes (exit code 0 = success), then `deadline job - download-output --job-id ` to retrieve results. + To do the whole workflow in one command, add --wait to block until the job + finishes (exit code 0 = success), and --download-on-success to also download + its output once it succeeds. Otherwise you can run the steps separately: + `deadline job wait --job-id ` then `deadline job download-output --job-id `. \b Learn more about [job bundles](https://docs.aws.amazon.com/deadline-cloud/latest/developerguide/build-job-bundle.html) @@ -381,6 +398,31 @@ def _check_create_job_wait_canceled() -> bool: ): config_file.set_setting("defaults.job_id", job_id) + # --download-on-success implies --wait. Skipped when there is no real job + # (e.g. --save-debug-snapshot yields job_id=None). + if job_id and (wait or download_on_success): + farm_id = config_file.get_setting("defaults.farm_id", config=config) + queue_id = config_file.get_setting("defaults.queue_id", config=config) + click.echo(f"Waiting for job {job_id} to complete...") + result = api.wait_for_job_completion( + farm_id=farm_id, queue_id=queue_id, job_id=job_id, config=config + ) + click.echo(f"Job completed with status: {result.status}") + if result.status != "SUCCEEDED": + sys.exit(1) + if download_on_success: + # Imported lazily to avoid a circular import with job_group. + from .job_group import _download_job_output + + _download_job_output( + config=config, + farm_id=farm_id, + queue_id=queue_id, + job_id=job_id, + step_id=None, + task_id=None, + ) + except AssetSyncCancelledError as exc: if sigint_handler.continue_operation: raise DeadlineOperationError(f"Job submission unexpectedly canceled:\n{exc}") from exc diff --git a/src/deadline/client/cli/_groups/job_group.py b/src/deadline/client/cli/_groups/job_group.py index 51b9b3a08..46d5bc83f 100644 --- a/src/deadline/client/cli/_groups/job_group.py +++ b/src/deadline/client/cli/_groups/job_group.py @@ -141,7 +141,7 @@ def cli_job(): cancel, or requeue failed tasks. \b - For scripted/agent workflows, prefer `wait` over polling `get`: + For scripted workflows, prefer `wait` over polling `get`: deadline job wait --job-id # blocks, exit 0 = success deadline job download-output --job-id --yes @@ -1120,7 +1120,7 @@ def job_download_output( --ignore-storage-profiles). \b - Pass --yes to skip confirmation prompts (useful in scripts/agents). + Pass --yes to skip confirmation prompts (useful when scripting). Pass --ignore-storage-profiles when submitting and downloading on the same machine to skip storage profile path mapping. diff --git a/test/unit/deadline_client/cli/test_cli_bundle_submit.py b/test/unit/deadline_client/cli/test_cli_bundle_submit.py index 8ef0e79f3..6cfff21e1 100644 --- a/test/unit/deadline_client/cli/test_cli_bundle_submit.py +++ b/test/unit/deadline_client/cli/test_cli_bundle_submit.py @@ -1437,3 +1437,68 @@ def test_bundle_gui_submit_submitter_info_file_missing_submitter_name( assert result.exit_code != 0 assert "submitter_name is required" in result.output + + +def test_cli_bundle_submit_wait_exits_nonzero_on_failure( + fresh_deadline_config, deadline_mock, temp_job_bundle_dir +): + """--wait blocks on the submitted job and exits non-zero when it does not succeed.""" + with open(os.path.join(temp_job_bundle_dir, "template.json"), "w", encoding="utf8") as f: + f.write(MOCK_JOB_TEMPLATE_CASES["MINIMAL_JSON"][1]) + deadline_mock.create_job.return_value = MOCK_CREATE_JOB_RESPONSE + deadline_mock.get_job.return_value = MOCK_GET_JOB_RESPONSE + + with patch.object(api_module, "wait_for_job_completion") as mock_wait: + mock_wait.return_value = MagicMock(status="FAILED") + runner = CliRunner() + result = runner.invoke( + main, + [ + "bundle", + "submit", + temp_job_bundle_dir, + "--farm-id", + MOCK_FARM_ID, + "--queue-id", + MOCK_QUEUE_ID, + "--wait", + ], + ) + + mock_wait.assert_called_once() + assert "Job completed with status: FAILED" in result.output + assert result.exit_code != 0 + + +def test_cli_bundle_submit_download_on_success( + fresh_deadline_config, deadline_mock, temp_job_bundle_dir +): + """--download-on-success waits, then downloads output when the job SUCCEEDS.""" + with open(os.path.join(temp_job_bundle_dir, "template.json"), "w", encoding="utf8") as f: + f.write(MOCK_JOB_TEMPLATE_CASES["MINIMAL_JSON"][1]) + deadline_mock.create_job.return_value = MOCK_CREATE_JOB_RESPONSE + deadline_mock.get_job.return_value = MOCK_GET_JOB_RESPONSE + + with ( + patch.object(api_module, "wait_for_job_completion") as mock_wait, + patch("deadline.client.cli._groups.job_group._download_job_output") as mock_download, + ): + mock_wait.return_value = MagicMock(status="SUCCEEDED") + runner = CliRunner() + result = runner.invoke( + main, + [ + "bundle", + "submit", + temp_job_bundle_dir, + "--farm-id", + MOCK_FARM_ID, + "--queue-id", + MOCK_QUEUE_ID, + "--download-on-success", + ], + ) + + mock_wait.assert_called_once() + mock_download.assert_called_once() + assert result.exit_code == 0