From d839802e24fcb2b8840a30cf90c7bea134378f51 Mon Sep 17 00:00:00 2001 From: "lightning-it-shared-assets-sync[bot]" <307342877+lightning-it-shared-assets-sync[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 22:48:51 +0000 Subject: [PATCH] chore: sync repository quality assets Shared-Assets-Source-SHA: 714bf6d266cddbd908b0502e5a035337c66dbe42 Shared-Assets-Source-Run: 32534477748 Shared-Assets-Source-Attempt: 1 Shared-Assets-Sync-App-ID: 4351516 --- .github/codex/prompts/review-exact-head.md | 2 +- .../release-bot-exact-head-review.yml | 76 +++-- scripts/materialize-exact-revision-review.py | 282 ++++++++++++++---- 3 files changed, 290 insertions(+), 70 deletions(-) diff --git a/.github/codex/prompts/review-exact-head.md b/.github/codex/prompts/review-exact-head.md index 7d2aaf0..787de23 100644 --- a/.github/codex/prompts/review-exact-head.md +++ b/.github/codex/prompts/review-exact-head.md @@ -1,4 +1,4 @@ -# Exact-head AI review +# Protected Exact-Revision Codex review Review only the change represented by `change.patch` and the immutable metadata in `review-metadata.json`. The directory intentionally contains no Git history diff --git a/.github/workflows/release-bot-exact-head-review.yml b/.github/workflows/release-bot-exact-head-review.yml index b4b85a6..4af0635 100644 --- a/.github/workflows/release-bot-exact-head-review.yml +++ b/.github/workflows/release-bot-exact-head-review.yml @@ -1,6 +1,6 @@ # Managed by lightning-it/shared-assets-lit. # Do not edit downstream copies directly. -# Protected per-repository MLX-90 §7.2 Exact-Revision Codex controller. +# REP-60 pipeline governance: protected MLX-90 §7.2 Exact-Revision Codex controller. # yamllint disable rule:truthy rule:line-length --- name: Protected Exact-Revision Codex review @@ -130,8 +130,50 @@ jobs: external_prefix="mlx90-exact-revision:v4:${input_sha256}:" external_id="${external_prefix}${GITHUB_RUN_ID}" expected_title="Exact-Revision Codex PR #${PR_NUMBER} ${EXPECTED_BASE}..${EXPECTED_HEAD}" - checks="$(gh api --paginate --slurp \ - "repos/${REPOSITORY}/commits/${EXPECTED_HEAD}/check-runs?check_name=Protected%20Exact-Revision%20Codex%20result&filter=all&per_page=100")" + read_reservation_checks() { + gh api --paginate --slurp \ + "repos/${REPOSITORY}/commits/${EXPECTED_HEAD}/check-runs?check_name=Protected%20Exact-Revision%20Codex%20result&filter=all&per_page=100" + } + create_reservation_once() { + local title="$1" summary="$2" created recovered recovery_attempt + if ! created="$(gh api --method POST "repos/${REPOSITORY}/check-runs" \ + -f name='Protected Exact-Revision Codex result' \ + -f head_sha="${EXPECTED_HEAD}" \ + -f status=in_progress \ + -f external_id="${external_id}" \ + -f "output[title]=${title}" \ + -f "output[summary]=${summary}")"; then + created='' + for recovery_attempt in $(seq 1 5); do + echo "Recovering immutable reservation creation outcome (attempt ${recovery_attempt}/5)." >&2 + sleep 5 + if ! recovered="$(read_reservation_checks | jq -c \ + --arg external_id "${external_id}" \ + --arg head "${EXPECTED_HEAD}" ' + [.[].check_runs[]? | + select(.name == "Protected Exact-Revision Codex result") | + select(.app.id == 15368 and .app.slug == "github-actions") | + select(.head_sha == $head and .external_id == $external_id)]')"; then + echo "Reservation recovery read failed; retrying read-only reconciliation." >&2 + continue + fi + if [ "$(jq 'length' <<<"${recovered}")" -gt 1 ]; then + echo "Ambiguous immutable reservation creation outcome." >&2 + return 1 + fi + if [ "$(jq 'length' <<<"${recovered}")" -eq 1 ]; then + created="$(jq -c '.[0]' <<<"${recovered}")" + break + fi + done + if [ -z "${created}" ]; then + echo "Reservation creation failed without one materialized exact result." >&2 + return 1 + fi + fi + printf '%s' "${created}" + } + checks="$(read_reservation_checks)" matching="$(jq -c \ --arg external_prefix "${external_prefix}" \ '[.[].check_runs[]? | @@ -204,7 +246,9 @@ jobs: exit 1 fi if [ "${status}" = completed ] && [ "${conclusion}" = success ]; then - jq -e '.status == "completed" and .conclusion == "success"' <<<"${prior_run}" >/dev/null + # Retry only downstream deterministic publication after a + # protected PASS; never invoke AI again for this input. + jq -e '.status == "completed"' <<<"${prior_run}" >/dev/null jq -e \ --arg input_sha256 "${input_sha256}" \ --arg run_url "${producer_run_url}" ' @@ -227,6 +271,10 @@ jobs: echo "A prior protected attempt for this immutable input is ${status}/${conclusion:-none}; automatic retry is forbidden." >&2 exit 1 fi + if [ "${GITHUB_RUN_ATTEMPT}" -ne 1 ]; then + echo "The current workflow run was rerun without a visible immutable reservation; automatic AI retry is forbidden." >&2 + exit 1 + fi # GitHub can remove an older custom check from a commit's visible # check-run inventory when a pull request is closed and reopened. # The protected workflow run and its job/step ledger remain the @@ -307,13 +355,9 @@ jobs: prior_run_id="$(jq -er '.[0].run_id | select(type == "number" and . > 0)' \ <<<"${successful_attempts}")" external_id="${external_prefix}${prior_run_id}" - reservation="$(gh api --method POST "repos/${REPOSITORY}/check-runs" \ - -f name='Protected Exact-Revision Codex result' \ - -f head_sha="${EXPECTED_HEAD}" \ - -f status=in_progress \ - -f external_id="${external_id}" \ - -f 'output[title]=Recovering durable protected Exact-Revision Codex PASS' \ - -f "output[summary]=Immutable input SHA-256: ${input_sha256}. Durable producer run: ${prior_run_id}.")" + reservation="$(create_reservation_once \ + 'Recovering durable protected Exact-Revision Codex PASS' \ + "Immutable input SHA-256: ${input_sha256}. Durable producer run: ${prior_run_id}.")" check_id="$(jq -er '.id | select(type == "number" and . > 0)' <<<"${reservation}")" { echo "reuse=true" @@ -342,13 +386,9 @@ jobs: echo "Recovering the durable protected PASS from producer run ${prior_run_id}; Codex will not run again." exit 0 fi - reservation="$(gh api --method POST "repos/${REPOSITORY}/check-runs" \ - -f name='Protected Exact-Revision Codex result' \ - -f head_sha="${EXPECTED_HEAD}" \ - -f status=in_progress \ - -f external_id="${external_id}" \ - -f 'output[title]=Protected Exact-Revision Codex review in progress' \ - -f "output[summary]=Immutable input SHA-256: ${input_sha256}.")" + reservation="$(create_reservation_once \ + 'Protected Exact-Revision Codex review in progress' \ + "Immutable input SHA-256: ${input_sha256}.")" check_id="$(jq -er '.id | select(type == "number" and . > 0)' <<<"${reservation}")" { echo "reuse=false" diff --git a/scripts/materialize-exact-revision-review.py b/scripts/materialize-exact-revision-review.py index 2f3bc89..b69db33 100644 --- a/scripts/materialize-exact-revision-review.py +++ b/scripts/materialize-exact-revision-review.py @@ -1,6 +1,6 @@ -"""Materialize and re-verify the bounded MLX-90 exact-revision review input.""" +"""Materialize the bounded REP-60 / MLX-90 section 7.2 review input.""" -# Format contract: Ruff 0.15.21 with line length 120 (Supplementary consumer policy). +# Canonical formatting contract: Ruff-compatible Python with line length 120. from __future__ import annotations @@ -10,11 +10,13 @@ import os import re import secrets +import selectors import shutil import stat import subprocess import sys import tempfile +import time from collections.abc import Sequence from pathlib import Path from typing import Any, NoReturn @@ -107,7 +109,11 @@ def run( command = " ".join(arguments) or "" fail(f"Command timed out after {COMMAND_TIMEOUT_SECONDS} seconds: {command}") if result.returncode != 0: - stderr = result.stderr if isinstance(result.stderr, str) else result.stderr.decode(errors="replace") + stderr = ( + result.stderr + if isinstance(result.stderr, str) + else result.stderr.decode(errors="replace") + ) command = " ".join(arguments) or "" fail(f"Command failed closed: {command}: {stderr.strip()}") return result @@ -153,7 +159,9 @@ def add_error_notes(error: BaseException, notes: Sequence[str]) -> None: add_note(note) -def fail_after_descriptor_cleanup(message: str, descriptor: int, label: str) -> NoReturn: +def fail_after_descriptor_cleanup( + message: str, descriptor: int, label: str +) -> NoReturn: """Raise one proof error after deterministically cleaning up its descriptor.""" cleanup_errors = close_descriptor_after_error(descriptor, label) failure = MaterializationError(message) @@ -161,7 +169,9 @@ def fail_after_descriptor_cleanup(message: str, descriptor: int, label: str) -> raise failure -def open_owned_parent_directory(path: Path, name: str, requirement: str) -> tuple[int, int, int]: +def open_owned_parent_directory( + path: Path, name: str, requirement: str +) -> tuple[int, int, int]: """Return the final parent fd plus O_NOFOLLOW and O_CLOEXEC flag values.""" no_follow = getattr(os, "O_NOFOLLOW", None) if not isinstance(no_follow, int) or no_follow == 0: @@ -202,7 +212,9 @@ def open_owned_parent_directory(path: Path, name: str, requirement: str) -> tupl ) ) directory = -1 - failure = MaterializationError(f"Protected {name} parent cannot be opened safely: {close_error}") + failure = MaterializationError( + f"Protected {name} parent cannot be opened safely: {close_error}" + ) add_error_notes(failure, cleanup_errors) raise failure from close_error directory = next_directory @@ -214,7 +226,9 @@ def open_owned_parent_directory(path: Path, name: str, requirement: str) -> tupl "Current parent directory", ) directory = -1 - failure = MaterializationError(f"Protected {name} parent cannot be opened safely: {error}") + failure = MaterializationError( + f"Protected {name} parent cannot be opened safely: {error}" + ) add_error_notes(failure, cleanup_errors) raise failure from error except BaseException as error: @@ -232,7 +246,9 @@ def open_owned_parent_directory(path: Path, name: str, requirement: str) -> tupl directory, "Validated parent directory", ) - failure = MaterializationError(f"Protected {name} parent cannot be inspected safely: {error}") + failure = MaterializationError( + f"Protected {name} parent cannot be inspected safely: {error}" + ) add_error_notes(failure, cleanup_errors) raise failure from error if not stat.S_ISDIR(parent_details.st_mode): @@ -297,7 +313,9 @@ def protected_asset_bytes(path: Path, name: str) -> bytes: ) if cleanup_errors: if active_error is None: - failure = MaterializationError(f"Protected {name} descriptors could not be closed safely.") + failure = MaterializationError( + f"Protected {name} descriptors could not be closed safely." + ) add_error_notes(failure, cleanup_errors) raise failure add_error_notes(active_error, cleanup_errors) @@ -336,7 +354,21 @@ def write_owned_regular_file(path: Path, payload: bytes, name: str) -> None: fail(f"Protected {name} must be owned by the current user.") finally: if existing_descriptor >= 0: - os.close(existing_descriptor) + descriptor_to_close = existing_descriptor + existing_descriptor = -1 + active_error = sys.exc_info()[1] + cleanup_errors = close_descriptor_after_error( + descriptor_to_close, + f"Protected {name} existing descriptor", + ) + if active_error is None and cleanup_errors: + failure = MaterializationError( + f"Protected {name} existing descriptor could not be closed safely." + ) + add_error_notes(failure, cleanup_errors) + raise failure + if active_error is not None: + add_error_notes(active_error, cleanup_errors) temporary_descriptor = os.open( temporary_name, @@ -369,8 +401,18 @@ def write_owned_regular_file(path: Path, payload: bytes, name: str) -> None: with os.fdopen(temporary_descriptor, "rb", closefd=False) as protected_file: if protected_file.read(len(payload) + 1) != payload: fail(f"Protected {name} temporary content changed while writing.") - os.close(temporary_descriptor) + descriptor_to_close = temporary_descriptor temporary_descriptor = -1 + cleanup_errors = close_descriptor_after_error( + descriptor_to_close, + f"Protected {name} temporary descriptor", + ) + if cleanup_errors: + failure = MaterializationError( + f"Protected {name} temporary descriptor could not be closed safely." + ) + add_error_notes(failure, cleanup_errors) + raise failure os.replace( temporary_name, @@ -387,50 +429,60 @@ def write_owned_regular_file(path: Path, payload: bytes, name: str) -> None: except OSError: pass except OSError as error: - fail(f"Protected {name} cannot be written atomically: {error}") + failure = MaterializationError( + f"Protected {name} cannot be written atomically: {error}" + ) + add_error_notes(failure, getattr(error, "__notes__", ())) + raise failure from error finally: active_error = sys.exc_info()[1] + final_cleanup_errors: list[str] = [] if temporary_descriptor >= 0: - try: - os.close(temporary_descriptor) - except OSError as cleanup_error: - cleanup_message = f"Protected {name} temporary close also failed: {cleanup_error}" - if active_error is None: - fail(cleanup_message) - add_note = getattr(active_error, "add_note", None) - if callable(add_note): - add_note(cleanup_message) + descriptor_to_close = temporary_descriptor + temporary_descriptor = -1 + final_cleanup_errors.extend( + close_descriptor_after_error( + descriptor_to_close, + f"Protected {name} temporary descriptor", + ) + ) if not replaced: try: os.unlink(temporary_name, dir_fd=directory) except FileNotFoundError: pass except OSError as cleanup_error: - cleanup_message = f"Protected {name} temporary cleanup also failed: {cleanup_error}" - if active_error is None: - fail(cleanup_message) - add_note = getattr(active_error, "add_note", None) - if callable(add_note): - add_note(cleanup_message) - try: - os.close(directory) - except OSError as cleanup_error: - cleanup_message = f"Protected {name} parent directory close also failed: {cleanup_error}" + final_cleanup_errors.append( + f"Protected {name} temporary cleanup also failed: {cleanup_error}" + ) + final_cleanup_errors.extend( + close_descriptor_after_error( + directory, + f"Protected {name} parent directory", + ) + ) + if final_cleanup_errors: if active_error is None: - fail(cleanup_message) - add_note = getattr(active_error, "add_note", None) - if callable(add_note): - add_note(cleanup_message) + failure = MaterializationError( + f"Protected {name} cleanup failed closed." + ) + add_error_notes(failure, final_cleanup_errors) + raise failure + add_error_notes(active_error, final_cleanup_errors) -def bind_protected_assets(metadata: dict[str, Any], asset_paths: dict[str, Path]) -> dict[str, Any]: +def bind_protected_assets( + metadata: dict[str, Any], asset_paths: dict[str, Path] +) -> dict[str, Any]: """Bind every base-controlled review asset into one canonical input hash.""" if set(asset_paths) != set(ASSET_ARGUMENTS): fail("The complete protected review-asset set is required.") bound = dict(metadata) for metadata_key, path in asset_paths.items(): asset_name = metadata_key.removesuffix("_sha256").replace("_", " ") - bound[metadata_key] = hashlib.sha256(protected_asset_bytes(path, asset_name)).hexdigest() + bound[metadata_key] = hashlib.sha256( + protected_asset_bytes(path, asset_name) + ).hexdigest() canonical = json.dumps(bound, sort_keys=True, separators=(",", ":")).encode("utf-8") bound["input_sha256"] = hashlib.sha256(canonical).hexdigest() return bound @@ -460,11 +512,16 @@ def validate_inputs(arguments: argparse.Namespace) -> None: fail("The protected workflow SHA must equal the live pull-request base SHA.") if arguments.trigger not in {"ready_for_review", "app_dispatch"}: fail("Unsupported exact-review trigger.") - if arguments.trigger == "app_dispatch" and arguments.dispatch_ref != f"refs/heads/{arguments.base_ref}": + if ( + arguments.trigger == "app_dispatch" + and arguments.dispatch_ref != f"refs/heads/{arguments.base_ref}" + ): fail("App dispatch must execute from the protected pull-request base ref.") -def read_live_pull_request(arguments: argparse.Namespace, *, home: Path) -> dict[str, Any]: +def read_live_pull_request( + arguments: argparse.Namespace, *, home: Path +) -> dict[str, Any]: gh = executable("gh") result = run( [ @@ -506,7 +563,9 @@ def read_live_pull_request(arguments: argparse.Namespace, *, home: Path) -> dict "head_repository": head_repository.get("full_name"), } if observed != expected: - fail(f"Live pull-request binding changed or is unauthorized: {json.dumps(observed, sort_keys=True)}") + fail( + f"Live pull-request binding changed or is unauthorized: {json.dumps(observed, sort_keys=True)}" + ) return pull_request @@ -517,16 +576,115 @@ def git_output( *, environment: dict[str, str], binary: bool = False, + max_bytes: int | None = None, ) -> bytes | str: + command = [git, f"--git-dir={git_dir}", *arguments] + if max_bytes is not None: + if not binary or max_bytes <= 0: + fail("Bounded Git output requires a positive binary byte limit.") + try: + process = subprocess.Popen( # noqa: S603 + command, + env=environment, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + except OSError as error: + fail(f"Command failed to start: {' '.join(command)}: {error}") + if process.stdout is None or process.stderr is None: + process.kill() + process.wait() + fail("Bounded Git output pipes could not be created.") + selector = selectors.DefaultSelector() + stdout = bytearray() + stderr = bytearray() + limit_exceeded = False + deadline = time.monotonic() + COMMAND_TIMEOUT_SECONDS + try: + for stream, label in ( + (process.stdout, "stdout"), + (process.stderr, "stderr"), + ): + os.set_blocking(stream.fileno(), False) + selector.register(stream, selectors.EVENT_READ, label) + while selector.get_map(): + remaining = deadline - time.monotonic() + if remaining <= 0: + process.kill() + process.wait() + fail( + f"Command timed out after {COMMAND_TIMEOUT_SECONDS} seconds: " + f"{' '.join(command)}" + ) + for key, _events in selector.select(remaining): + if key.data == "stdout": + remaining_bytes = max_bytes - len(stdout) + read_size = min(65_536, remaining_bytes + 1) + else: + read_size = 65_536 + try: + chunk = os.read(key.fd, read_size) + except BlockingIOError: + continue + if not chunk: + selector.unregister(key.fileobj) + continue + if key.data == "stdout": + remaining_bytes = max_bytes - len(stdout) + if remaining_bytes > 0: + stdout.extend(chunk[:remaining_bytes]) + if len(chunk) >= remaining_bytes: + limit_exceeded = True + if process.poll() is None: + process.kill() + elif len(stderr) < 65_536: + stderr.extend(chunk[: 65_536 - len(stderr)]) + remaining = deadline - time.monotonic() + if remaining <= 0: + process.kill() + process.wait() + fail( + f"Command timed out after {COMMAND_TIMEOUT_SECONDS} seconds: " + f"{' '.join(command)}" + ) + try: + return_code = process.wait(timeout=remaining) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + fail( + f"Command timed out after {COMMAND_TIMEOUT_SECONDS} seconds: " + f"{' '.join(command)}" + ) + finally: + selector.close() + process.stdout.close() + process.stderr.close() + if process.poll() is None: + process.kill() + process.wait() + if limit_exceeded: + fail( + "Exact-revision review input exceeds the protected byte limit " + f"of {max_bytes - 1} bytes." + ) + if return_code != 0: + fail( + f"Command failed closed: {' '.join(command)}: " + f"{stderr.decode(errors='replace').strip()}" + ) + return bytes(stdout) result = run( - [git, f"--git-dir={git_dir}", *arguments], + command, environment=environment, binary=binary, ) return result.stdout -def materialize(arguments: argparse.Namespace, output_directory: Path) -> dict[str, Any]: +def materialize( + arguments: argparse.Namespace, output_directory: Path +) -> dict[str, Any]: validate_inputs(arguments) if output_directory.exists(): fail(f"Review workspace already exists: {output_directory}") @@ -538,7 +696,9 @@ def materialize(arguments: argparse.Namespace, output_directory: Path) -> dict[s runner_temp = Path(os.environ.get("RUNNER_TEMP", tempfile.gettempdir())).resolve() if not runner_temp.is_dir(): fail("RUNNER_TEMP must identify an existing directory.") - with tempfile.TemporaryDirectory(prefix="exact-revision-materializer.", dir=runner_temp) as temporary: + with tempfile.TemporaryDirectory( + prefix="exact-revision-materializer.", dir=runner_temp + ) as temporary: temporary_root = Path(temporary) home = temporary_root / "home" home.mkdir(mode=0o700) @@ -653,12 +813,15 @@ def materialize(arguments: argparse.Namespace, output_directory: Path) -> dict[s ], environment=git_environment, binary=True, + max_bytes=MAX_REVIEW_BYTES, ) if not isinstance(diff, bytes): fail("Git returned an invalid diff representation.") review_bytes = len(diff) if review_bytes <= 0 or review_bytes >= MAX_REVIEW_BYTES: - fail(f"Exact-revision review input must contain 1..{MAX_REVIEW_BYTES - 1} bytes; observed {review_bytes}.") + fail( + f"Exact-revision review input must contain 1..{MAX_REVIEW_BYTES - 1} bytes; observed {review_bytes}." + ) diff_sha256 = hashlib.sha256(diff).hexdigest() read_live_pull_request(arguments, home=home) @@ -690,7 +853,9 @@ def materialize(arguments: argparse.Namespace, output_directory: Path) -> dict[s def bind_assets(review_directory: Path, asset_paths: dict[str, Path]) -> dict[str, Any]: metadata_path = review_directory / "review-metadata.json" try: - metadata = json.loads(protected_asset_bytes(metadata_path, "review metadata").decode("utf-8")) + metadata = json.loads( + protected_asset_bytes(metadata_path, "review metadata").decode("utf-8") + ) except (json.JSONDecodeError, UnicodeDecodeError) as error: fail(f"Review metadata is malformed: {error}") if not isinstance(metadata, dict): @@ -714,13 +879,20 @@ def verify( validate_inputs(arguments) patch = review_directory / "change.patch" metadata_path = review_directory / "review-metadata.json" - if not patch.is_file() or patch.is_symlink() or not metadata_path.is_file() or metadata_path.is_symlink(): + if ( + not patch.is_file() + or patch.is_symlink() + or not metadata_path.is_file() + or metadata_path.is_symlink() + ): fail("The review diff and metadata must be regular, non-symlink files.") patch_size = patch.stat().st_size if patch_size <= 0 or patch_size >= MAX_REVIEW_BYTES: fail(f"The review diff must be between 1 and {MAX_REVIEW_BYTES - 1} bytes.") try: - expected_metadata = json.loads(protected_asset_bytes(metadata_path, "review metadata").decode("utf-8")) + expected_metadata = json.loads( + protected_asset_bytes(metadata_path, "review metadata").decode("utf-8") + ) except (json.JSONDecodeError, UnicodeDecodeError) as error: fail(f"Review metadata is malformed: {error}") if not isinstance(expected_metadata, dict): @@ -738,9 +910,13 @@ def verify( runner_temp = Path(os.environ.get("RUNNER_TEMP", tempfile.gettempdir())).resolve() if not runner_temp.is_dir(): fail("RUNNER_TEMP must identify an existing directory.") - with tempfile.TemporaryDirectory(prefix="exact-revision-recheck.", dir=runner_temp) as temporary: + with tempfile.TemporaryDirectory( + prefix="exact-revision-recheck.", dir=runner_temp + ) as temporary: regenerated = Path(temporary) / "review" - actual_metadata = bind_protected_assets(materialize(arguments, regenerated), asset_paths) + actual_metadata = bind_protected_assets( + materialize(arguments, regenerated), asset_paths + ) if protected_asset_bytes(patch, "review diff") != protected_asset_bytes( regenerated / "change.patch", "regenerated diff" ): @@ -760,7 +936,9 @@ def parse_arguments() -> argparse.Namespace: parser.add_argument("--expected-base", required=True) parser.add_argument("--expected-head", required=True) parser.add_argument("--trusted-workflow-sha", required=True) - parser.add_argument("--trigger", required=True, choices=("ready_for_review", "app_dispatch")) + parser.add_argument( + "--trigger", required=True, choices=("ready_for_review", "app_dispatch") + ) parser.add_argument("--dispatch-ref", default="") parser.add_argument("--review-directory", required=True, type=Path) parser.add_argument("--materializer-path", type=Path) @@ -776,7 +954,9 @@ def main() -> int: if arguments.mode == "materialize": metadata = materialize(arguments, arguments.review_directory) elif arguments.mode == "bind-assets": - metadata = bind_assets(arguments.review_directory, asset_paths_from_arguments(arguments)) + metadata = bind_assets( + arguments.review_directory, asset_paths_from_arguments(arguments) + ) else: metadata = verify( arguments,