avr-cli 0.4.0__tar.gz → 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {avr_cli-0.4.0 → avr_cli-0.4.1}/PKG-INFO +1 -1
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avr_cli.egg-info/PKG-INFO +1 -1
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/_build_info.py +1 -1
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/commands/sbom.py +111 -26
- {avr_cli-0.4.0 → avr_cli-0.4.1}/pyproject.toml +1 -1
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_sbom.py +192 -5
- {avr_cli-0.4.0 → avr_cli-0.4.1}/LICENSE +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/README.md +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avr_cli.egg-info/SOURCES.txt +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avr_cli.egg-info/dependency_links.txt +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avr_cli.egg-info/entry_points.txt +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avr_cli.egg-info/requires.txt +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avr_cli.egg-info/top_level.txt +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/__init__.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/api_client.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/assets/logo_mark.svg +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/audit.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/auth.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/billing.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/click_ext.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/commands/__init__.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/commands/auth_cmd.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/commands/cache.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/commands/config_cmd.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/commands/health.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/commands/internal.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/commands/job.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/commands/log.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/commands/org.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/commands/pr.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/commands/repo.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/commands/run.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/commands/settings.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/commands/status.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/commands/workflow.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/config.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/display.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/docs_gen.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/firewall.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/helpers.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/json_output.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/log_display.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/main.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/metrics_display.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/output.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/paths.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/repo_context.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/run_diagnostics.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/run_diagnostics_display.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/run_refs.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/transport.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/version.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/avrea_cli/vm.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/setup.cfg +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_api_client.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_auth.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_auth_login.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_auth_sso.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_cache.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_config.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_config_cmd.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_display.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_docs_gen.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_env_conventions.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_firewall.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_global_flags.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_helpers.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_job_list.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_job_metrics_cmd.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_job_view_logs.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_json_output.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_lazy_registry.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_log_display.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_metrics_display.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_org_email_domain.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_org_saml.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_output.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_piped_output.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_pr_list.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_public_mirrors.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_repo_context.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_repo_mirror.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_run_attempts.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_run_command_refs.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_run_diagnose.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_run_list_flags.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_run_logs.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_run_refs.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_run_view.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_settings.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_transport.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_vm.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_web_flag.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_workflow_actions.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_workflow_run.py +0 -0
- {avr_cli-0.4.0 → avr_cli-0.4.1}/tests/test_workflow_view.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# Auto-generated by .github/workflows/release.yml; do not edit.
|
|
2
|
-
GIT_COMMIT = "
|
|
2
|
+
GIT_COMMIT = "de175ab9edd3880dab9a4eb3614fff64c9015397"
|
|
@@ -30,6 +30,8 @@ import shlex
|
|
|
30
30
|
import time
|
|
31
31
|
|
|
32
32
|
_COMMIT_SHA_RE = re.compile(r"^(?:[0-9a-f]{40}|[0-9a-f]{64})$")
|
|
33
|
+
_COMMIT_PREFIX_RE = re.compile(r"^[0-9a-f]{7,64}$")
|
|
34
|
+
_HISTORY_PAGE_LIMIT = 1000
|
|
33
35
|
|
|
34
36
|
_ARTIFACT_FILENAMES = {
|
|
35
37
|
"cyclonedx": "bom.cyclonedx.json",
|
|
@@ -55,8 +57,8 @@ def _validate_commit(_ctx: click.Context, _param: click.Parameter, value: str |
|
|
|
55
57
|
if value is None:
|
|
56
58
|
return None
|
|
57
59
|
sha = value.strip().lower()
|
|
58
|
-
if not
|
|
59
|
-
raise click.BadParameter("must be a
|
|
60
|
+
if not _COMMIT_PREFIX_RE.match(sha):
|
|
61
|
+
raise click.BadParameter("must be a hex commit SHA or a prefix of at least 7 characters.")
|
|
60
62
|
return sha
|
|
61
63
|
|
|
62
64
|
|
|
@@ -70,14 +72,56 @@ def _strip_ref(_ctx: click.Context, _param: click.Parameter, value: str | None)
|
|
|
70
72
|
return ref
|
|
71
73
|
|
|
72
74
|
|
|
75
|
+
_commit_argument = click.argument("commit", required=False, callback=_validate_commit)
|
|
73
76
|
_commit_option = click.option(
|
|
74
77
|
"--commit",
|
|
75
|
-
"
|
|
78
|
+
"commit_option",
|
|
76
79
|
callback=_validate_commit,
|
|
77
|
-
help="
|
|
80
|
+
help="Same as the COMMIT argument.",
|
|
78
81
|
)
|
|
79
82
|
|
|
80
83
|
|
|
84
|
+
def _requested_commit(commit: str | None, commit_option: str | None) -> str | None:
|
|
85
|
+
if commit and commit_option and commit != commit_option:
|
|
86
|
+
raise click.UsageError("Pass the commit either as the COMMIT argument or with --commit, not both.")
|
|
87
|
+
return commit or commit_option
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _resolve_commit(client: ApiClient, org_id: str, repo_id: str, commit: str) -> str:
|
|
91
|
+
"""The full SHA for ``commit``, a full SHA or a prefix of one.
|
|
92
|
+
|
|
93
|
+
The API addresses SBOMs by full SHA only, so a prefix is matched against the
|
|
94
|
+
recorded history. Matching stops at a second hit, which already makes the
|
|
95
|
+
prefix ambiguous."""
|
|
96
|
+
if _COMMIT_SHA_RE.match(commit):
|
|
97
|
+
return commit
|
|
98
|
+
matches: list[str] = []
|
|
99
|
+
params: dict[str, Any] = {"limit": _HISTORY_PAGE_LIMIT}
|
|
100
|
+
while len(matches) < 2:
|
|
101
|
+
try:
|
|
102
|
+
response = client.public_get(_sbom_path(org_id, repo_id), params=params)
|
|
103
|
+
except httpx.HTTPStatusError as exc:
|
|
104
|
+
handle_http_error(exc, "list SBOMs")
|
|
105
|
+
for row in response.get("data", []):
|
|
106
|
+
sha = row.get("commit_sha") or ""
|
|
107
|
+
if sha.startswith(commit) and sha not in matches:
|
|
108
|
+
matches.append(sha)
|
|
109
|
+
next_cursor = (response.get("pagination") or {}).get("next_cursor")
|
|
110
|
+
if not next_cursor:
|
|
111
|
+
break
|
|
112
|
+
params = {"limit": _HISTORY_PAGE_LIMIT, "cursor": next_cursor}
|
|
113
|
+
|
|
114
|
+
if not matches:
|
|
115
|
+
raise click.ClickException(
|
|
116
|
+
f"No recorded SBOM matches commit {commit}. Run `avr sbom list` to see the recorded commits."
|
|
117
|
+
)
|
|
118
|
+
if len(matches) > 1:
|
|
119
|
+
raise click.ClickException(
|
|
120
|
+
f"Commit {commit} is ambiguous; it matches {matches[0]} and {matches[1]}. Use a longer prefix."
|
|
121
|
+
)
|
|
122
|
+
return matches[0]
|
|
123
|
+
|
|
124
|
+
|
|
81
125
|
def _resolve_scope(ctx: click.Context, org_id: str | None, repo_id: str | None) -> tuple[ApiClient, str, str]:
|
|
82
126
|
client: ApiClient = ctx.obj["client"]
|
|
83
127
|
config: CliConfig = ctx.obj["config"]
|
|
@@ -239,18 +283,23 @@ _SBOM_VIEW_FIELDS = make_schema(
|
|
|
239
283
|
|
|
240
284
|
|
|
241
285
|
@sbom.command("view")
|
|
286
|
+
@_commit_argument
|
|
242
287
|
@_repo_option
|
|
243
288
|
@_org_option
|
|
244
289
|
@_commit_option
|
|
245
290
|
@json_options
|
|
246
291
|
@click.pass_context
|
|
247
|
-
def sbom_view(ctx, repo_id, org_id,
|
|
292
|
+
def sbom_view(ctx, commit, repo_id, org_id, commit_option, json_fields, jq_expr):
|
|
248
293
|
"""Show an SBOM's summary: dependency counts, licences, and artifacts.
|
|
249
294
|
|
|
295
|
+
COMMIT is the commit of a recorded SBOM, as a full SHA or a unique prefix
|
|
296
|
+
of at least 7 characters such as the one `avr sbom list` shows. Defaults to
|
|
297
|
+
the latest SBOM.
|
|
298
|
+
|
|
250
299
|
\b
|
|
251
300
|
Examples:
|
|
252
301
|
avr sbom view --repo acme/api
|
|
253
|
-
avr sbom view --repo acme/api
|
|
302
|
+
avr sbom view 3f2c9a1b7d04 --repo acme/api
|
|
254
303
|
avr sbom view --repo acme/api --json summary --jq .summary.licenses
|
|
255
304
|
|
|
256
305
|
\b
|
|
@@ -260,7 +309,10 @@ def sbom_view(ctx, repo_id, org_id, commit_sha, json_fields, jq_expr):
|
|
|
260
309
|
"""
|
|
261
310
|
if handle_json_meta(json_fields, jq_expr, _SBOM_VIEW_FIELDS):
|
|
262
311
|
return
|
|
312
|
+
commit_sha = _requested_commit(commit, commit_option)
|
|
263
313
|
client, org_id, repo_id = _resolve_scope(ctx, org_id, repo_id)
|
|
314
|
+
if commit_sha is not None:
|
|
315
|
+
commit_sha = _resolve_commit(client, org_id, repo_id, commit_sha)
|
|
264
316
|
snapshot = _fetch_snapshot(client, org_id, repo_id, commit_sha)
|
|
265
317
|
|
|
266
318
|
if json_fields is not None:
|
|
@@ -276,12 +328,11 @@ def sbom_view(ctx, repo_id, org_id, commit_sha, json_fields, jq_expr):
|
|
|
276
328
|
"avr",
|
|
277
329
|
"sbom",
|
|
278
330
|
"download",
|
|
331
|
+
snapshot.get("commit_sha", ""),
|
|
279
332
|
"--org",
|
|
280
333
|
org_id,
|
|
281
334
|
"--repo",
|
|
282
335
|
repo_id,
|
|
283
|
-
"--commit",
|
|
284
|
-
snapshot.get("commit_sha", ""),
|
|
285
336
|
"--format",
|
|
286
337
|
"inventory",
|
|
287
338
|
]
|
|
@@ -342,6 +393,7 @@ def sbom_view(ctx, repo_id, org_id, commit_sha, json_fields, jq_expr):
|
|
|
342
393
|
|
|
343
394
|
|
|
344
395
|
@sbom.command("download")
|
|
396
|
+
@_commit_argument
|
|
345
397
|
@_repo_option
|
|
346
398
|
@_org_option
|
|
347
399
|
@_commit_option
|
|
@@ -360,18 +412,25 @@ def sbom_view(ctx, repo_id, org_id, commit_sha, json_fields, jq_expr):
|
|
|
360
412
|
help='Output file path, or "-" for stdout. Defaults to the artifact filename in the current directory.',
|
|
361
413
|
)
|
|
362
414
|
@click.pass_context
|
|
363
|
-
def sbom_download(ctx, repo_id, org_id,
|
|
415
|
+
def sbom_download(ctx, commit, repo_id, org_id, commit_option, artifact_format, out_path):
|
|
364
416
|
"""Download an SBOM artifact (CycloneDX, SPDX, or dependency inventory).
|
|
365
417
|
|
|
418
|
+
COMMIT is the commit of a recorded SBOM, as a full SHA or a unique prefix
|
|
419
|
+
of at least 7 characters such as the one `avr sbom list` shows. Defaults to
|
|
420
|
+
the latest SBOM.
|
|
421
|
+
|
|
366
422
|
\b
|
|
367
423
|
Examples:
|
|
368
424
|
avr sbom download --repo acme/api
|
|
369
425
|
avr sbom download --repo acme/api --format spdx --out api.spdx.json
|
|
370
|
-
avr sbom download --repo acme/api --
|
|
426
|
+
avr sbom download 3f2c9a1b7d04 --repo acme/api --out - | jq .components
|
|
371
427
|
"""
|
|
428
|
+
commit_sha = _requested_commit(commit, commit_option)
|
|
372
429
|
client, org_id, repo_id = _resolve_scope(ctx, org_id, repo_id)
|
|
373
430
|
if commit_sha is None:
|
|
374
431
|
commit_sha = _fetch_snapshot(client, org_id, repo_id, None)["commit_sha"]
|
|
432
|
+
else:
|
|
433
|
+
commit_sha = _resolve_commit(client, org_id, repo_id, commit_sha)
|
|
375
434
|
|
|
376
435
|
filename = _ARTIFACT_FILENAMES[artifact_format]
|
|
377
436
|
try:
|
|
@@ -508,14 +567,10 @@ class _TaskTracker:
|
|
|
508
567
|
|
|
509
568
|
|
|
510
569
|
@sbom.command("generate")
|
|
570
|
+
@click.argument("ref", required=False, callback=_strip_ref)
|
|
511
571
|
@_repo_option
|
|
512
572
|
@_org_option
|
|
513
|
-
@click.option(
|
|
514
|
-
"--ref",
|
|
515
|
-
default=None,
|
|
516
|
-
callback=_strip_ref,
|
|
517
|
-
help="Branch, tag, or commit SHA to analyse. Defaults to the default-branch tip.",
|
|
518
|
-
)
|
|
573
|
+
@click.option("--ref", "ref_option", default=None, callback=_strip_ref, help="Same as the REF argument.")
|
|
519
574
|
@click.option("--wait", is_flag=True, default=False, help="Wait until generation finishes before returning.")
|
|
520
575
|
@click.option(
|
|
521
576
|
"--wait-timeout",
|
|
@@ -526,17 +581,22 @@ class _TaskTracker:
|
|
|
526
581
|
)
|
|
527
582
|
@json_options
|
|
528
583
|
@click.pass_context
|
|
529
|
-
def sbom_generate(ctx, repo_id, org_id,
|
|
584
|
+
def sbom_generate(ctx, ref, repo_id, org_id, ref_option, wait, wait_timeout, json_fields, jq_expr):
|
|
530
585
|
"""Start SBOM generation for a repository.
|
|
531
586
|
|
|
587
|
+
REF is the branch, tag, or commit SHA to analyse; an abbreviated SHA such
|
|
588
|
+
as the one `avr sbom list` shows works too. Defaults to the default-branch
|
|
589
|
+
tip. Without --wait, commit_sha is the full SHA of the commit the run
|
|
590
|
+
analyses when REF is a full SHA or one the API expanded.
|
|
591
|
+
|
|
532
592
|
One analysis runs per repository at a time. A request for the same ref as
|
|
533
593
|
the analysis already running joins it; a request for a different ref is
|
|
534
594
|
rejected (HTTP 409) until that analysis finishes. Once an SBOM has been
|
|
535
595
|
recorded for the repository, a new run is refused (HTTP 429) for a
|
|
536
596
|
cooldown period, and the error says how many seconds remain.
|
|
537
597
|
|
|
538
|
-
With --wait and
|
|
539
|
-
|
|
598
|
+
With --wait and a REF that names a commit, success means a new SBOM for
|
|
599
|
+
that commit is downloadable, whatever the analysis task's own outcome
|
|
540
600
|
(task_status); commit_sha and recorded_at identify it. A failed analysis
|
|
541
601
|
can still deliver its SBOM later, so such a wait runs to --wait-timeout
|
|
542
602
|
before reporting failure. For a branch, tag, or the default branch the API
|
|
@@ -547,8 +607,8 @@ def sbom_generate(ctx, repo_id, org_id, ref, wait, wait_timeout, json_fields, jq
|
|
|
547
607
|
\b
|
|
548
608
|
Examples:
|
|
549
609
|
avr sbom generate --repo acme/api
|
|
550
|
-
avr sbom generate --repo acme/api
|
|
551
|
-
avr sbom generate --repo acme/api --
|
|
610
|
+
avr sbom generate v1.4.0 --repo acme/api
|
|
611
|
+
avr sbom generate 3f2c9a1b7d04 --repo acme/api --wait
|
|
552
612
|
|
|
553
613
|
\b
|
|
554
614
|
JSON FIELDS
|
|
@@ -556,6 +616,9 @@ def sbom_generate(ctx, repo_id, org_id, ref, wait, wait_timeout, json_fields, jq
|
|
|
556
616
|
"""
|
|
557
617
|
if handle_json_meta(json_fields, jq_expr, _SBOM_GENERATE_FIELDS):
|
|
558
618
|
return
|
|
619
|
+
if ref and ref_option and ref != ref_option:
|
|
620
|
+
raise click.UsageError("Pass the ref either as the REF argument or with --ref, not both.")
|
|
621
|
+
ref = ref or ref_option
|
|
559
622
|
output = _JsonOutput(
|
|
560
623
|
fields=split_fields(json_fields, _SBOM_GENERATE_FIELDS) if json_fields is not None else [],
|
|
561
624
|
jq_expr=jq_expr,
|
|
@@ -585,13 +648,20 @@ def sbom_generate(ctx, repo_id, org_id, ref, wait, wait_timeout, json_fields, jq
|
|
|
585
648
|
"recorded_at": None,
|
|
586
649
|
}
|
|
587
650
|
|
|
651
|
+
expanded_sha = response.get("commit_sha")
|
|
652
|
+
expanded = commit_sha is None and isinstance(expanded_sha, str) and _COMMIT_SHA_RE.match(expanded_sha)
|
|
653
|
+
if expanded:
|
|
654
|
+
commit_sha = expanded_sha
|
|
655
|
+
|
|
588
656
|
if not wait:
|
|
657
|
+
result["commit_sha"] = commit_sha
|
|
589
658
|
if output.emit(result):
|
|
590
659
|
return
|
|
660
|
+
target = f" for {commit_sha[:12]}" if commit_sha else ""
|
|
591
661
|
if result["status"] == "already_running":
|
|
592
|
-
click.echo(f"SBOM generation already running (task {task_id}); joined it.")
|
|
662
|
+
click.echo(f"SBOM generation already running{target} (task {task_id}); joined it.")
|
|
593
663
|
else:
|
|
594
|
-
click.echo(f"SBOM generation started (task {task_id}).")
|
|
664
|
+
click.echo(f"SBOM generation started{target} (task {task_id}).")
|
|
595
665
|
click.echo("Generation takes a few minutes; run `avr sbom view` afterwards to see the result.", err=True)
|
|
596
666
|
return
|
|
597
667
|
|
|
@@ -603,8 +673,13 @@ def sbom_generate(ctx, repo_id, org_id, ref, wait, wait_timeout, json_fields, jq
|
|
|
603
673
|
_wait_for_task_outcome(waiter)
|
|
604
674
|
return
|
|
605
675
|
snapshot_path = f"{_sbom_path(org_id, repo_id)}/commits/{commit_sha}"
|
|
676
|
+
if expanded:
|
|
677
|
+
# The API expanded an abbreviated SHA, so the baseline is read only now,
|
|
678
|
+
# after the run has started; retry it like the wait's own reads.
|
|
679
|
+
baseline = _snapshot_after_request(waiter, snapshot_path, commit_sha)
|
|
606
680
|
baseline_recorded_at = baseline.get("recorded_at") if baseline else None
|
|
607
|
-
|
|
681
|
+
view_command = shlex.join(["avr", "sbom", "view", commit_sha, "--org", org_id, "--repo", repo_id])
|
|
682
|
+
_wait_for_commit_delivery(waiter, snapshot_path, commit_sha, baseline_recorded_at, view_command)
|
|
608
683
|
|
|
609
684
|
|
|
610
685
|
@dataclass(frozen=True)
|
|
@@ -647,6 +722,16 @@ class _Wait:
|
|
|
647
722
|
raise SystemExit(1)
|
|
648
723
|
|
|
649
724
|
|
|
725
|
+
def _snapshot_after_request(waiter: _Wait, snapshot_path: str, commit_sha: str) -> dict[str, Any] | None:
|
|
726
|
+
"""The commit's current snapshot, or None; transient errors retry until the deadline."""
|
|
727
|
+
while True:
|
|
728
|
+
try:
|
|
729
|
+
return waiter.poller.get(snapshot_path, action="check for the commit's SBOM", missing_ok=True)
|
|
730
|
+
except _TransientPollError as exc:
|
|
731
|
+
if not waiter.poller.sleep(exc.backoff):
|
|
732
|
+
waiter.time_out(f"the SBOM for {commit_sha[:12]} could not be checked")
|
|
733
|
+
|
|
734
|
+
|
|
650
735
|
def _wait_for_task_outcome(waiter: _Wait) -> None:
|
|
651
736
|
"""Report the analysis task's own outcome; no SBOM can be attributed to it."""
|
|
652
737
|
tracker = waiter.tracker
|
|
@@ -690,7 +775,7 @@ def _echo_task_outcome(tracker: _TaskTracker) -> None:
|
|
|
690
775
|
|
|
691
776
|
|
|
692
777
|
def _wait_for_commit_delivery(
|
|
693
|
-
waiter: _Wait, snapshot_path: str, commit_sha: str, baseline_recorded_at: str | None
|
|
778
|
+
waiter: _Wait, snapshot_path: str, commit_sha: str, baseline_recorded_at: str | None, view_command: str
|
|
694
779
|
) -> None:
|
|
695
780
|
"""Wait for the commit's SBOM to be recorded after the baseline.
|
|
696
781
|
|
|
@@ -731,7 +816,7 @@ def _wait_for_commit_delivery(
|
|
|
731
816
|
"producing the SBOM.",
|
|
732
817
|
err=True,
|
|
733
818
|
)
|
|
734
|
-
click.echo(f"SBOM generated for {short_sha}. Run `
|
|
819
|
+
click.echo(f"SBOM generated for {short_sha}. Run `{view_command}` to see it.")
|
|
735
820
|
|
|
736
821
|
|
|
737
822
|
def _echo_undelivered(tracker: _TaskTracker, short_sha: str, wait_timeout: int) -> None:
|
|
@@ -67,6 +67,7 @@ HISTORY = {
|
|
|
67
67
|
],
|
|
68
68
|
"pagination": {"next_cursor": "Y3Vyc29y"},
|
|
69
69
|
}
|
|
70
|
+
HISTORY_LAST_PAGE = {**HISTORY, "pagination": {"next_cursor": None}}
|
|
70
71
|
|
|
71
72
|
|
|
72
73
|
def _http_error(status: int, detail: str = "", headers: dict[str, str] | None = None) -> httpx.HTTPStatusError:
|
|
@@ -201,12 +202,37 @@ class TestSbomView:
|
|
|
201
202
|
assert result.exit_code == 0, result.output
|
|
202
203
|
assert calls.log[-1][1] == f"{BASE}/commits/{SHA}"
|
|
203
204
|
|
|
204
|
-
def
|
|
205
|
-
|
|
205
|
+
def test_commit_as_argument(self, runner, calls):
|
|
206
|
+
calls.route("GET", f"{BASE}/commits/{SHA}", SNAPSHOT)
|
|
207
|
+
result = runner.invoke(cli, ["sbom", "view", SHA, "--repo", "rep-123"])
|
|
208
|
+
assert result.exit_code == 0, result.output
|
|
209
|
+
assert calls.log[-1][1] == f"{BASE}/commits/{SHA}"
|
|
210
|
+
|
|
211
|
+
def test_short_commit_resolves_through_history(self, runner, calls):
|
|
212
|
+
calls.route("GET", BASE, HISTORY_LAST_PAGE)
|
|
213
|
+
calls.route("GET", f"{BASE}/commits/{SHA}", SNAPSHOT)
|
|
214
|
+
result = runner.invoke(cli, ["sbom", "view", SHA[:12], "--repo", "rep-123"])
|
|
215
|
+
assert result.exit_code == 0, result.output
|
|
216
|
+
assert calls.log[-1][1] == f"{BASE}/commits/{SHA}"
|
|
217
|
+
|
|
218
|
+
@pytest.mark.parametrize("commit", ["abc123", "xyz1234567", "a" * 65, " "])
|
|
219
|
+
def test_rejects_invalid_commit(self, runner, calls, commit):
|
|
220
|
+
result = runner.invoke(cli, ["sbom", "view", "--repo", "rep-123", "--commit", commit])
|
|
221
|
+
assert result.exit_code == 2
|
|
222
|
+
assert "at least 7" in result.output
|
|
223
|
+
assert calls.log == []
|
|
224
|
+
|
|
225
|
+
def test_rejects_conflicting_commit_argument_and_option(self, runner, calls):
|
|
226
|
+
result = runner.invoke(cli, ["sbom", "view", SHA, "--repo", "rep-123", "--commit", OTHER_SHA])
|
|
206
227
|
assert result.exit_code == 2
|
|
207
|
-
assert "
|
|
228
|
+
assert "--commit" in result.output
|
|
208
229
|
assert calls.log == []
|
|
209
230
|
|
|
231
|
+
def test_accepts_same_commit_as_argument_and_option(self, runner, calls):
|
|
232
|
+
calls.route("GET", f"{BASE}/commits/{SHA}", SNAPSHOT)
|
|
233
|
+
result = runner.invoke(cli, ["sbom", "view", SHA, "--repo", "rep-123", "--commit", SHA.upper()])
|
|
234
|
+
assert result.exit_code == 0, result.output
|
|
235
|
+
|
|
210
236
|
def test_not_found_hints_generate(self, runner, calls):
|
|
211
237
|
calls.route("GET", f"{BASE}/latest", _http_error(404, "analysis result not found"))
|
|
212
238
|
result = runner.invoke(cli, ["sbom", "view", "--repo", "rep-123"])
|
|
@@ -251,14 +277,14 @@ class TestSbomView:
|
|
|
251
277
|
omitted = {"omitted": "oversized", "serialized_bytes": 143362}
|
|
252
278
|
calls.route("GET", f"{BASE}/commits/{SHA}", {**SNAPSHOT, "summary": omitted})
|
|
253
279
|
result = runner.invoke(cli, ["sbom", "view", "--repo", "rep-123", "--commit", SHA])
|
|
254
|
-
expected = f"avr sbom download --org org-default --repo rep-123 --
|
|
280
|
+
expected = f"avr sbom download {SHA} --org org-default --repo rep-123 --format inventory"
|
|
255
281
|
assert expected in result.output
|
|
256
282
|
|
|
257
283
|
def test_copyleft_overflow_hint_pins_the_viewed_snapshot(self, runner, calls):
|
|
258
284
|
summary = {**SUMMARY, "copyleft": ["a"], "copyleft_omitted": 3}
|
|
259
285
|
calls.route("GET", f"{BASE}/latest", {**SNAPSHOT, "summary": summary})
|
|
260
286
|
result = runner.invoke(cli, ["sbom", "view", "--repo", "rep-123"])
|
|
261
|
-
expected = f"avr sbom download --org org-default --repo rep-123 --
|
|
287
|
+
expected = f"avr sbom download {SHA} --org org-default --repo rep-123 --format inventory"
|
|
262
288
|
assert expected in result.output
|
|
263
289
|
|
|
264
290
|
def test_inventory_hint_quotes_values_for_the_shell(self, runner, calls):
|
|
@@ -353,6 +379,72 @@ class TestSbomDownload:
|
|
|
353
379
|
assert "no artifact" in result.output
|
|
354
380
|
assert not (tmp_path / "x").exists()
|
|
355
381
|
|
|
382
|
+
def test_short_commit_argument_downloads_the_resolved_commit(self, runner, calls):
|
|
383
|
+
calls.route("GET", BASE, HISTORY_LAST_PAGE)
|
|
384
|
+
calls.route("GET_BYTES", f"{BASE}/commits/{SHA}/artifacts/bom.cyclonedx.json", b'{"ok":true}')
|
|
385
|
+
result = runner.invoke(cli, ["sbom", "download", SHA[:12], "--repo", "rep-123", "--out", "-"])
|
|
386
|
+
assert result.exit_code == 0, result.output
|
|
387
|
+
assert result.stdout == '{"ok":true}'
|
|
388
|
+
assert calls.log[-1] == ("GET_BYTES", f"{BASE}/commits/{SHA}/artifacts/bom.cyclonedx.json", None)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
AMBIGUOUS_SHA = "a" * 12 + "f" * 28
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _history_page(*shas: str, next_cursor: str | None = None) -> dict[str, Any]:
|
|
395
|
+
return {"data": [{"commit_sha": sha} for sha in shas], "pagination": {"next_cursor": next_cursor}}
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class TestSbomCommitPrefix:
|
|
399
|
+
def test_full_commit_skips_history(self, runner, calls):
|
|
400
|
+
calls.route("GET_BYTES", f"{BASE}/commits/{SHA}/artifacts/bom.cyclonedx.json", b"{}")
|
|
401
|
+
result = runner.invoke(cli, ["sbom", "download", SHA, "--repo", "rep-123", "--out", "-"])
|
|
402
|
+
assert result.exit_code == 0, result.output
|
|
403
|
+
assert [c[0] for c in calls.log] == ["GET_BYTES"]
|
|
404
|
+
|
|
405
|
+
def test_prefix_is_case_insensitive(self, runner, calls):
|
|
406
|
+
calls.route("GET", BASE, HISTORY_LAST_PAGE)
|
|
407
|
+
calls.route("GET_BYTES", f"{BASE}/commits/{SHA}/artifacts/bom.cyclonedx.json", b"{}")
|
|
408
|
+
result = runner.invoke(cli, ["sbom", "download", SHA[:7].upper(), "--repo", "rep-123", "--out", "-"])
|
|
409
|
+
assert result.exit_code == 0, result.output
|
|
410
|
+
|
|
411
|
+
def test_resolution_pages_through_history(self, runner, calls):
|
|
412
|
+
calls.route("GET", BASE, [_history_page(OTHER_SHA, next_cursor="page2"), _history_page(SHA)])
|
|
413
|
+
calls.route("GET_BYTES", f"{BASE}/commits/{SHA}/artifacts/bom.cyclonedx.json", b"{}")
|
|
414
|
+
result = runner.invoke(cli, ["sbom", "download", SHA[:7], "--repo", "rep-123", "--out", "-"])
|
|
415
|
+
assert result.exit_code == 0, result.output
|
|
416
|
+
history_params = [c[2] for c in calls.log if c[:2] == ("GET", BASE)]
|
|
417
|
+
assert history_params == [{"limit": 1000}, {"limit": 1000, "cursor": "page2"}]
|
|
418
|
+
|
|
419
|
+
def test_unknown_prefix_names_the_list_command(self, runner, calls):
|
|
420
|
+
calls.route("GET", BASE, _history_page(OTHER_SHA))
|
|
421
|
+
result = runner.invoke(cli, ["sbom", "download", SHA[:7], "--repo", "rep-123"])
|
|
422
|
+
assert result.exit_code == 1
|
|
423
|
+
assert SHA[:7] in result.output
|
|
424
|
+
assert "avr sbom list" in result.output
|
|
425
|
+
assert [c[0] for c in calls.log] == ["GET"]
|
|
426
|
+
|
|
427
|
+
def test_ambiguous_prefix_lists_candidates(self, runner, calls):
|
|
428
|
+
calls.route("GET", BASE, [_history_page(SHA, next_cursor="page2"), _history_page(AMBIGUOUS_SHA)])
|
|
429
|
+
result = runner.invoke(cli, ["sbom", "view", SHA[:10], "--repo", "rep-123"])
|
|
430
|
+
assert result.exit_code == 1
|
|
431
|
+
assert "ambiguous" in result.output
|
|
432
|
+
assert SHA in result.output
|
|
433
|
+
assert AMBIGUOUS_SHA in result.output
|
|
434
|
+
|
|
435
|
+
def test_longer_prefix_disambiguates(self, runner, calls):
|
|
436
|
+
calls.route("GET", BASE, _history_page(SHA, AMBIGUOUS_SHA))
|
|
437
|
+
calls.route("GET", f"{BASE}/commits/{SHA}", SNAPSHOT)
|
|
438
|
+
result = runner.invoke(cli, ["sbom", "view", SHA[:13], "--repo", "rep-123"])
|
|
439
|
+
assert result.exit_code == 0, result.output
|
|
440
|
+
assert calls.log[-1][1] == f"{BASE}/commits/{SHA}"
|
|
441
|
+
|
|
442
|
+
def test_history_error_is_reported(self, runner, calls):
|
|
443
|
+
calls.route("GET", BASE, _http_error(403, "forbidden"))
|
|
444
|
+
result = runner.invoke(cli, ["sbom", "view", SHA[:7], "--repo", "rep-123"])
|
|
445
|
+
assert result.exit_code == 1
|
|
446
|
+
assert "forbidden" in result.output
|
|
447
|
+
|
|
356
448
|
|
|
357
449
|
STATE_PATH = "/orgs/org-default/repos/rep-123/ai-task/state"
|
|
358
450
|
LATEST = f"{BASE}/latest"
|
|
@@ -587,6 +679,7 @@ class TestSbomGenerateWaitForCommit:
|
|
|
587
679
|
result = runner.invoke(cli, ["sbom", "generate", "--repo", "rep-123", "--ref", NEW_SHA, "--wait"])
|
|
588
680
|
assert result.exit_code == 0, result.output
|
|
589
681
|
assert f"SBOM generated for {NEW_SHA[:12]}" in result.output
|
|
682
|
+
assert f"avr sbom view {NEW_SHA} --org org-default --repo rep-123" in result.output
|
|
590
683
|
|
|
591
684
|
def test_unrelated_snapshot_arrival_does_not_satisfy_wait(self, runner, calls):
|
|
592
685
|
calls.route("GET", COMMIT_PATH, PRIOR_FOR_COMMIT)
|
|
@@ -725,6 +818,100 @@ class TestSbomGenerateWaitForCommit:
|
|
|
725
818
|
assert calls.log == []
|
|
726
819
|
|
|
727
820
|
|
|
821
|
+
SHORT_NEW_SHA = NEW_SHA[:12]
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
class TestSbomGenerateRefArgument:
|
|
825
|
+
def test_usage_shows_ref_as_optional(self, runner):
|
|
826
|
+
result = runner.invoke(cli, ["sbom", "generate", "--help"])
|
|
827
|
+
assert "generate [OPTIONS] [REF]" in result.output
|
|
828
|
+
|
|
829
|
+
def test_ref_as_argument(self, runner, calls):
|
|
830
|
+
calls.route("POST", f"{BASE}/generate", {"ai_task_id": "task-1", "status": "generating"})
|
|
831
|
+
result = runner.invoke(cli, ["sbom", "generate", "v1.2.0", "--repo", "rep-123"])
|
|
832
|
+
assert result.exit_code == 0, result.output
|
|
833
|
+
assert calls.log[-1][2] == {"scope": "sbom", "ref": "v1.2.0"}
|
|
834
|
+
|
|
835
|
+
def test_rejects_conflicting_ref_argument_and_option(self, runner, calls):
|
|
836
|
+
result = runner.invoke(cli, ["sbom", "generate", "v1.2.0", "--repo", "rep-123", "--ref", "main"])
|
|
837
|
+
assert result.exit_code == 2
|
|
838
|
+
assert "REF argument or with --ref, not both" in result.output
|
|
839
|
+
assert calls.log == []
|
|
840
|
+
|
|
841
|
+
def test_short_sha_is_sent_for_the_api_to_expand(self, runner, calls):
|
|
842
|
+
calls.route("POST", f"{BASE}/generate", {"ai_task_id": "task-1", "status": "generating"})
|
|
843
|
+
result = runner.invoke(cli, ["sbom", "generate", SHORT_NEW_SHA, "--repo", "rep-123"])
|
|
844
|
+
assert result.exit_code == 0, result.output
|
|
845
|
+
assert calls.log == [("POST", f"{BASE}/generate", {"scope": "sbom", "ref": SHORT_NEW_SHA})]
|
|
846
|
+
|
|
847
|
+
def test_wait_follows_the_commit_the_api_expanded(self, runner, calls):
|
|
848
|
+
calls.route("POST", f"{BASE}/generate", {"ai_task_id": "task-1", "status": "generating", "commit_sha": NEW_SHA})
|
|
849
|
+
calls.route("GET", COMMIT_PATH, [_http_error(404, "analysis result not found"), NEW_SNAPSHOT])
|
|
850
|
+
calls.route("GET", STATE_PATH, {"ai_task_id": "task-1", "status": "RUNNING"})
|
|
851
|
+
result = runner.invoke(
|
|
852
|
+
cli, ["sbom", "generate", SHORT_NEW_SHA, "--repo", "rep-123", "--wait", "--json", "status,commit_sha"]
|
|
853
|
+
)
|
|
854
|
+
assert result.exit_code == 0, result.output
|
|
855
|
+
assert json.loads(result.stdout) == {"status": "completed", "commit_sha": NEW_SHA}
|
|
856
|
+
assert [c[1] for c in calls.log[:2]] == [f"{BASE}/generate", COMMIT_PATH]
|
|
857
|
+
|
|
858
|
+
def test_wait_for_an_expanded_commit_needs_a_new_delivery(self, runner, calls):
|
|
859
|
+
calls.route("POST", f"{BASE}/generate", {"ai_task_id": "task-1", "status": "generating", "commit_sha": NEW_SHA})
|
|
860
|
+
calls.route("GET", COMMIT_PATH, [PRIOR_FOR_COMMIT, PRIOR_FOR_COMMIT, NEW_SNAPSHOT])
|
|
861
|
+
calls.route("GET", STATE_PATH, {"ai_task_id": "task-1", "status": "RUNNING"})
|
|
862
|
+
result = runner.invoke(
|
|
863
|
+
cli, ["sbom", "generate", SHORT_NEW_SHA, "--repo", "rep-123", "--wait", "--json", "recorded_at"]
|
|
864
|
+
)
|
|
865
|
+
assert result.exit_code == 0, result.output
|
|
866
|
+
assert json.loads(result.stdout) == {"recorded_at": NEW_SNAPSHOT["recorded_at"]}
|
|
867
|
+
|
|
868
|
+
@pytest.mark.parametrize("expanded", [{}, {"commit_sha": None}, {"commit_sha": SHORT_NEW_SHA}])
|
|
869
|
+
def test_wait_follows_the_task_without_an_expanded_commit(self, runner, calls, expanded):
|
|
870
|
+
calls.route("POST", f"{BASE}/generate", {"ai_task_id": "task-1", "status": "generating", **expanded})
|
|
871
|
+
calls.route("GET", STATE_PATH, {"ai_task_id": "task-1", "status": "COMPLETED"})
|
|
872
|
+
result = runner.invoke(
|
|
873
|
+
cli, ["sbom", "generate", SHORT_NEW_SHA, "--repo", "rep-123", "--wait", "--json", "status,commit_sha"]
|
|
874
|
+
)
|
|
875
|
+
assert result.exit_code == 0, result.output
|
|
876
|
+
assert json.loads(result.stdout) == {"status": "completed", "commit_sha": None}
|
|
877
|
+
assert all("/commits/" not in c[1] for c in calls.log)
|
|
878
|
+
|
|
879
|
+
@pytest.mark.parametrize("failure", [_http_error(503), httpx.ConnectError("connection reset")])
|
|
880
|
+
def test_baseline_after_the_request_retries_transient_errors(self, runner, calls, failure):
|
|
881
|
+
calls.route("POST", f"{BASE}/generate", {"ai_task_id": "task-1", "status": "generating", "commit_sha": NEW_SHA})
|
|
882
|
+
calls.route("GET", COMMIT_PATH, [failure, PRIOR_FOR_COMMIT, PRIOR_FOR_COMMIT, NEW_SNAPSHOT])
|
|
883
|
+
calls.route("GET", STATE_PATH, {"ai_task_id": "task-1", "status": "RUNNING"})
|
|
884
|
+
result = runner.invoke(
|
|
885
|
+
cli, ["sbom", "generate", SHORT_NEW_SHA, "--repo", "rep-123", "--wait", "--json", "recorded_at"]
|
|
886
|
+
)
|
|
887
|
+
assert result.exit_code == 0, result.output
|
|
888
|
+
assert json.loads(result.stdout) == {"recorded_at": NEW_SNAPSHOT["recorded_at"]}
|
|
889
|
+
|
|
890
|
+
def test_baseline_after_the_request_that_never_succeeds_times_out(self, runner, calls):
|
|
891
|
+
calls.route("POST", f"{BASE}/generate", {"ai_task_id": "task-1", "status": "generating", "commit_sha": NEW_SHA})
|
|
892
|
+
calls.route("GET", COMMIT_PATH, _http_error(503))
|
|
893
|
+
args = ["sbom", "generate", SHORT_NEW_SHA, "--repo", "rep-123", "--wait", "--wait-timeout", "30"]
|
|
894
|
+
result = runner.invoke(cli, args)
|
|
895
|
+
assert result.exit_code == 1
|
|
896
|
+
assert f"the SBOM for {SHORT_NEW_SHA} could not be checked" in result.output
|
|
897
|
+
assert calls.now >= 30
|
|
898
|
+
assert all(c[1] != STATE_PATH for c in calls.log)
|
|
899
|
+
|
|
900
|
+
@pytest.mark.parametrize(("ref", "response"), [(SHORT_NEW_SHA, {"commit_sha": NEW_SHA}), (NEW_SHA, {})])
|
|
901
|
+
def test_reports_the_pinned_commit_without_wait(self, runner, calls, ref, response):
|
|
902
|
+
calls.route("POST", f"{BASE}/generate", {"ai_task_id": "task-1", "status": "generating", **response})
|
|
903
|
+
json_result = runner.invoke(cli, ["sbom", "generate", ref, "--repo", "rep-123", "--json", "commit_sha"])
|
|
904
|
+
human_result = runner.invoke(cli, ["sbom", "generate", ref, "--repo", "rep-123"])
|
|
905
|
+
assert json.loads(json_result.stdout) == {"commit_sha": NEW_SHA}
|
|
906
|
+
assert f"started for {NEW_SHA[:12]} (task task-1)" in human_result.output
|
|
907
|
+
|
|
908
|
+
@pytest.mark.parametrize("response", [{}, {"commit_sha": SHORT_NEW_SHA}])
|
|
909
|
+
def test_reports_no_commit_without_a_valid_expansion(self, runner, calls, response):
|
|
910
|
+
calls.route("POST", f"{BASE}/generate", {"ai_task_id": "task-1", "status": "generating", **response})
|
|
911
|
+
result = runner.invoke(cli, ["sbom", "generate", SHORT_NEW_SHA, "--repo", "rep-123", "--json", "commit_sha"])
|
|
912
|
+
assert json.loads(result.stdout) == {"commit_sha": None}
|
|
913
|
+
|
|
914
|
+
|
|
728
915
|
RUNNING = {"ai_task_id": "task-1", "status": "RUNNING"}
|
|
729
916
|
COMPLETED = {"ai_task_id": "task-1", "status": "COMPLETED"}
|
|
730
917
|
UNPINNED_WAIT = ["sbom", "generate", "--repo", "rep-123", "--wait"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|