Загрузка данных


#!/usr/bin/env bash
#
# Example:
#   ./scripts/runners/noir_runner.sh \
#     --config-file-path ./apa_analyzer_config.yaml \
#     --code-root-git-dir-path "$PWD/build_src/auth-proxy" \
#     --code-root-git-dir-path "$PWD/build_src/auth-proxy-extra" \
#     --repo-snaps-json ./repo-snaps.json \
#     --oakb-tool-report-dir-path-no-ts ./.oakb_reports/noir \
#     --component-code AUTH \
#     --component-version D-3.3.3-b333

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
REPO_ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd -P)"
SHARED_DIR="$REPO_ROOT_DIR/shared"
# shellcheck source=shared/lib/common.sh
source "$SHARED_DIR/lib/common.sh"
# shellcheck source=shared/use_config.sh
source "$SHARED_DIR/use_config.sh"

usage() {
  cat <<'EOF'
Usage:
  ./scripts/runners/noir_runner.sh [options]

Required:
  --config-file-path PATH
  --code-root-git-dir-path PATH
  --repo-snaps-json PATH
  --oakb-tool-report-dir-path-no-ts PATH

Optional:
  --component-code CODE
  --component-version VERSION
  --swagger-file-path PATH
  --jenkins-job-name NAME
  --jenkins-job-invoke-id ID
  --analyzer-repo-url URL
  --analyzer-repo-branch BRANCH
  --analyzer-repo-commit HASH
  --publish-manifest-path PATH
  --tools-root-dir-path PATH
  --debug
  --help
EOF
}

# Read one KEY=value entry from simple env-like files without sourcing them.
# This is used for two safe scalar lookups:
#
#   1. /etc/os-release, to detect the current Jenkins agent OS.
#   2. .oakb_bundle_source.env, to decide whether an already-unzipped Noir
#      bundle matches the selected bundle URL/platform and can be reused.
#
# The function intentionally supports only plain one-line KEY=value records with
# optional single or double quotes around the value; it is not a full shell
# parser.
key_value_file_value() {
  local file_path="$1"
  local key="$2"

  [[ -f "$file_path" ]] || return 0

  local line
  local value
  while IFS= read -r line || [[ -n "$line" ]]; do
    [[ "$line" == "$key="* ]] || continue
    value="${line#*=}"
    case "$value" in
      \"*\")
        value="${value#\"}"
        value="${value%\"}"
        ;;
      \'*\')
        value="${value#\'}"
        value="${value%\'}"
        ;;
    esac
    printf '%s\n' "$value"
    return 0
  done < "$file_path"
}

os_version_major() {
  local version_id="$1"

  if [[ "$version_id" =~ ^([0-9]+) ]]; then
    printf '%s\n' "${BASH_REMATCH[1]}"
  fi
}

noir_bundle_variant_key() {
  local os_id="$1"
  local os_version_major_value="$2"

  if [[ "$os_id" == "altlinux" && "$os_version_major_value" == "10" ]]; then
    printf 'altlinux_10\n'
  fi
}

normalize_bundle_variant_label() {
  local value="$1"

  case "$value" in
    default_rocky9|default_rocky|rocky9)
      printf 'default_el9\n'
      ;;
    *)
      printf '%s\n' "$value"
      ;;
  esac
}

resolve_bundle_zip_url() {
  local default_bundle_zip_url="$1"
  local variant_key="$2"
  local os_id="$3"
  local os_version_id="$4"
  local os_version_major_value="$5"
  local os_release_file="$6"

  common::log INFO "Noir bundle OS detection: os_release_file=$os_release_file id=${os_id:-unknown} version_id=${os_version_id:-unknown} version_major=${os_version_major_value:-unknown} variant_key=${variant_key:-default}"

  if [[ -n "${OAKB_NOIR_BUNDLE_ZIP_URL:-}" ]]; then
    common::log INFO "Using Noir bundle URL from OAKB_NOIR_BUNDLE_ZIP_URL"
    printf '%s\n' "$OAKB_NOIR_BUNDLE_ZIP_URL"
    return 0
  fi

  if [[ -n "$variant_key" ]]; then
    local variant_bundle_zip_url
    variant_bundle_zip_url="$(use_config::value ".data.oakb_tools.noir.bundle_zip_url_by_os.$variant_key")"
    if [[ -n "$variant_bundle_zip_url" ]]; then
      common::log INFO "Using Noir bundle URL for OS variant $variant_key: $variant_bundle_zip_url"
      printf '%s\n' "$variant_bundle_zip_url"
      return 0
    fi
    common::log INFO "No Noir bundle URL configured for OS variant $variant_key; using default bundle URL"
  fi

  common::log INFO "Using default Noir bundle URL: $default_bundle_zip_url"
  printf '%s\n' "$default_bundle_zip_url"
}

write_bundle_stamp() {
  local stamp_path="$1"
  local bundle_zip_url="$2"
  local bundle_platform_id="$3"
  local prepared_at_utc

  prepared_at_utc="$(common::timestamp_utc_rfc3339)"
  {
    printf 'bundle_zip_url=%s\n' "$bundle_zip_url"
    printf 'bundle_platform_id=%s\n' "$bundle_platform_id"
    printf 'prepared_at_utc=%s\n' "$prepared_at_utc"
  } > "$stamp_path"
}

prepare_bundle() {
  local tool_dir="$1"
  local bundle_zip_url="$2"
  local activation_script_rel="$3"
  local run_executable_rel="$4"
  local bundle_platform_id="$5"

  local activation_script_abs
  local run_executable_abs
  local stamp_path="$tool_dir/noir-bundle/.oakb_bundle_source.env"
  activation_script_abs="$(common::join_path "$tool_dir" "$activation_script_rel")"
  run_executable_abs="$(common::join_path "$tool_dir" "$run_executable_rel")"

  # Noir is shipped as a bundle declared in apa_analyzer_config.yaml. Reuse the
  # prepared bundle between builds unless OAKB_FORCE_TOOL_REFRESH=1 is set or
  # the selected bundle URL/platform differs from the prepared bundle stamp.
  if [[ "${OAKB_FORCE_TOOL_REFRESH:-0}" != "1" && -f "$activation_script_abs" && -f "$run_executable_abs" ]]; then
    local stamped_bundle_zip_url
    local stamped_bundle_platform_id
    stamped_bundle_zip_url="$(key_value_file_value "$stamp_path" "bundle_zip_url")"
    stamped_bundle_platform_id="$(key_value_file_value "$stamp_path" "bundle_platform_id")"

    if [[ "$stamped_bundle_zip_url" == "$bundle_zip_url" && "$stamped_bundle_platform_id" == "$bundle_platform_id" ]]; then
      common::log INFO "Reusing prepared noir bundle in $tool_dir"
      return 0
    fi

    common::log INFO "Refreshing prepared noir bundle in $tool_dir because selected bundle changed or stamp is missing"
  fi

  common::ensure_dir "$tool_dir"
  rm -rf "$tool_dir/noir-bundle"
  
  common::log INFO "Downloading noir bundle from $bundle_zip_url"
  common::run curl -ks --user "$TUZ_NAME_FOR_ACCESS_ANALYZER_BINS:$TUZ_PWD_FOR_ACCESS_ANALYZER_BINS" --http1.1  --fail --location --retry 3 --output "$tool_dir/noir-bundle.zip" "$bundle_zip_url"
  # --retry-connrefused <- unsupported by curl on some agents
  common::run unzip -oq "$tool_dir/noir-bundle.zip" -d "$tool_dir"

  [[ -f "$activation_script_abs" ]] || common::die "Activation script missing after unzip: $activation_script_abs"
  [[ -f "$run_executable_abs" ]] || common::die "Noir executable missing after unzip: $run_executable_abs"
  write_bundle_stamp "$stamp_path" "$bundle_zip_url" "$bundle_platform_id"
}

detect_tool_version() {
  local run_executable_abs="$1"
  local version_output
  version_output="$("$run_executable_abs" --version 2>/dev/null | sed -n '1p' || true)"
  if [[ -n "$version_output" ]]; then
    printf '%s\n' "$version_output"
  else
    printf 'unknown\n'
  fi
}

write_meta_file() {
  local meta_path="$1"
  local report_datetime_utc="$2"
  local tool_version="$3"
  local repo_snaps_json_path="$4"
  local component_code="$5"
  local component_version="$6"
  local report_dir_name="${7}"
  local report_main_file="${8}"
  local jenkins_job_name="${9}"
  local jenkins_job_invoke_id="${10}"
  local analyzer_repo_url="${11}"
  local analyzer_repo_branch="${12}"
  local analyzer_repo_commit="${13}"
  local bundle_variant="${14}"
  local bundle_zip_url="${15}"
  local bundle_platform_id="${16}"

  bundle_variant="$(normalize_bundle_variant_label "$bundle_variant")"

  # noir_meta.yaml is the contract consumed by TTR. Important matching fields:
  #
  #   data.report_info.repo_snaps[] with every repo snapshot included in this Noir run
  #   data.component.component_code / component_version
  #   data.report_info.report_main_file: noir-report.json
  #   data.report_generation.analyzer.* for analyzer repo provenance
  #   data.tool.bundle.* for selected Noir bundle provenance
  #
  # TTR later uses those values to decide whether a report satisfies a ticket's
  # Virtual Requested Tuple.
  jq -n \
    --slurpfile repo_snaps "$repo_snaps_json_path" \
    --arg report_datetime_utc "$report_datetime_utc" \
    --arg tool_version "$tool_version" \
    --arg component_code "$component_code" \
    --arg component_version "$component_version" \
    --arg report_dir_name "$report_dir_name" \
    --arg report_main_file "$report_main_file" \
    --arg jenkins_job_name "$jenkins_job_name" \
    --arg jenkins_job_invoke_id "$jenkins_job_invoke_id" \
    --arg analyzer_repo_url "$analyzer_repo_url" \
    --arg analyzer_repo_branch "$analyzer_repo_branch" \
    --arg analyzer_repo_commit "$analyzer_repo_commit" \
    --arg bundle_variant "$bundle_variant" \
    --arg bundle_zip_url "$bundle_zip_url" \
    --arg bundle_platform_id "$bundle_platform_id" \
    '
      def empty_to_null: if . == "" then null else . end;
      {
        schema: 1,
        kind: "noir_report_meta",
        data: {
          report_schema_version: 1,
          tool: {
            name: "noir",
            version: $tool_version,
            bundle: {
              variant: $bundle_variant,
              zip_url: $bundle_zip_url,
              platform_id: $bundle_platform_id
            }
          },
          report_info: {
            report_directory_name: $report_dir_name,
            report_datetime_utc: $report_datetime_utc,
            report_main_file: $report_main_file,
            repo_snaps: $repo_snaps[0]
          },
          report_generation: {
            jenkins_job: {
              job_name: $jenkins_job_name,
              job_invoke_id: $jenkins_job_invoke_id
            },
            analyzer: {
              repo_url: ($analyzer_repo_url | empty_to_null),
              repo_branch: ($analyzer_repo_branch | empty_to_null),
              repo_commit: ($analyzer_repo_commit | empty_to_null)
            }
          },
          component: {
            component_code: ($component_code | empty_to_null),
            component_version: ($component_version | empty_to_null)
          }
        }
      }
    ' | yq eval -P -o=yaml '.' - > "$meta_path"
}

pretty_print_json_file() {
  local json_path="$1"
  local pretty_path
  pretty_path="$(common::mktemp_file "oakb-noir-json.XXXXXX.json")"

  jq '.' "$json_path" > "$pretty_path"
  mv "$pretty_path" "$json_path"
}

validate_main_json_report_file() {
  local json_path="$1"

  [[ -s "$json_path" ]] || common::die "Noir did not produce main JSON report: $json_path"
  jq -e type "$json_path" >/dev/null || common::die "Noir produced invalid main JSON report: $json_path"
}

validate_swagger_file_path() {
  local swagger_file_path="$1"

  [[ -n "$swagger_file_path" ]] || return 0
  [[ -e "$swagger_file_path" ]] || common::die "SWAGGER_FILE_PATH points to a missing file: $swagger_file_path"
  [[ -f "$swagger_file_path" ]] || common::die "SWAGGER_FILE_PATH is not a regular file: $swagger_file_path"
  [[ -r "$swagger_file_path" ]] || common::die "SWAGGER_FILE_PATH is not readable: $swagger_file_path"
}

write_swagger_candidate_meta_file() {
  local meta_path="$1"
  local component_code="$2"
  local component_version="$3"
  local source_report_dir_path="$4"
  local jenkins_job_name="$5"
  local jenkins_job_invoke_id="$6"
  local created_at_utc="$7"

  jq -n \
    --arg component_code "$component_code" \
    --arg component_version "$component_version" \
    --arg source_report_dir_path "$source_report_dir_path" \
    --arg jenkins_job_name "$jenkins_job_name" \
    --arg jenkins_job_invoke_id "$jenkins_job_invoke_id" \
    --arg created_at_utc "$created_at_utc" \
    '
      def empty_to_null: if . == "" then null else . end;
      {
        schema: 1,
        kind: "tuple_artifact_candidate_meta",
        data: {
          artifact: {
            artifact_type: "swagger",
            file_name: "swagger.json"
          },
          tuple: {
            component_code: $component_code,
            component_version: $component_version
          },
          source: {
            producer_family: "noir",
            source_report_dir_path: $source_report_dir_path,
            producer_job_name: ($jenkins_job_name | empty_to_null),
            producer_job_invoke_id: ($jenkins_job_invoke_id | empty_to_null),
            created_at_utc: $created_at_utc
          }
        }
      }
    ' | yq eval -P -o=yaml '.' - > "$meta_path"
}

publish_swagger_candidate() {
  local swagger_file_path="$1"
  local candidate_dir_path="$2"
  local component_code="$3"
  local component_version="$4"
  local source_report_dir_path="$5"
  local jenkins_job_name="$6"
  local jenkins_job_invoke_id="$7"
  local created_at_utc="$8"

  [[ -n "$swagger_file_path" ]] || return 0
  [[ -n "$component_code" ]] || common::die "SWAGGER_FILE_PATH requires --component-code so the Swagger tuple artifact can be identified"
  [[ -n "$component_version" ]] || common::die "SWAGGER_FILE_PATH requires --component-version so the Swagger tuple artifact can be identified"

  common::ensure_dir "$candidate_dir_path"
  common::log INFO "Publishing Swagger tuple artifact candidate: $swagger_file_path -> $candidate_dir_path/swagger.json"
  common::run cp "$swagger_file_path" "$candidate_dir_path/swagger.json"
  write_swagger_candidate_meta_file \
    "$candidate_dir_path/artifact_meta.yaml" \
    "$component_code" \
    "$component_version" \
    "$source_report_dir_path" \
    "$jenkins_job_name" \
    "$jenkins_job_invoke_id" \
    "$created_at_utc"
}

run_noir_json_report_with_stdout_fallback() {
  local run_executable_abs="$1"
  local output_path="$2"
  local fallback_json_path="$3"
  shift 3

  local stdout_path
  stdout_path="$(common::mktemp_file "oakb-noir-stdout.XXXXXX.json")"
  rm -f "$output_path"

  # Noir bundle versions do not all handle "-o" consistently for the secondary
  # JSON report. Some write the report file, while v0.29.1 can print the JSON
  # to stderr/stdout even when "-o" is present. For no-tech fixture repos, it can
  # also produce no secondary output at all. Capture the no-log command output,
  # then fall back to the main JSON report so the published report shape remains
  # stable for downstream tools and operators.
  if ! common::run "$run_executable_abs" "$@" -o "$output_path" > "$stdout_path" 2>&1; then
    cat "$stdout_path" >&2 || true
    common::die "Noir command failed while producing JSON report: $output_path"
  fi
  if [[ ! -s "$output_path" && -s "$stdout_path" ]]; then
    cp "$stdout_path" "$output_path"
  fi
  if [[ ! -s "$output_path" && -s "$fallback_json_path" ]]; then
    cp "$fallback_json_path" "$output_path"
  fi
  rm -f "$stdout_path"
  [[ -s "$output_path" ]] || common::die "Noir did not produce JSON report: $output_path"
}

push_report_to_repo() {
  local report_dir="$1"
  local report_rel_path="$2"
  local reports_repo_url="$3"
  local reports_repo_branch="$4"
  local code_repo_refs="$5"
  local swagger_candidate_dir="${6:-}"
  local swagger_candidate_rel_path="${7:-}"

  local report_dir_name
  report_dir_name="$(basename "$report_rel_path")"

  local push_temp_dir
  push_temp_dir="$(common::mktemp_dir "oakb-noir-push.XXXXXX")"

  local worktree="$push_temp_dir/worktree"

  stage_noir_single_report_commit() {
    [[ -n "$report_dir_name" ]] || common::die "Report directory name is empty"
    rm -rf -- "$worktree/${report_rel_path:?}" || return $?
    common::run mkdir -p "$(dirname "$worktree/$report_rel_path")" || return $?
    common::run mkdir -p "$worktree/$report_rel_path" || return $?
    common::run cp -a "$report_dir"/. "$worktree/$report_rel_path"/ || return $?
    common::run git -C "$worktree" add "$report_rel_path" || return $?
    if [[ -n "$swagger_candidate_dir" || -n "$swagger_candidate_rel_path" ]]; then
      [[ -n "$swagger_candidate_dir" ]] || common::die "Swagger candidate manifest entry is missing local directory"
      [[ -n "$swagger_candidate_rel_path" ]] || common::die "Swagger candidate manifest entry is missing repo path"
      [[ -d "$swagger_candidate_dir" ]] || common::die "Swagger candidate directory not found: $swagger_candidate_dir"
      rm -rf -- "$worktree/${swagger_candidate_rel_path:?}" || return $?
      common::run mkdir -p "$(dirname "$worktree/$swagger_candidate_rel_path")" || return $?
      common::run mkdir -p "$worktree/$swagger_candidate_rel_path" || return $?
      common::run cp -a "$swagger_candidate_dir"/. "$worktree/$swagger_candidate_rel_path"/ || return $?
      common::run git -C "$worktree" add "$swagger_candidate_rel_path" || return $?
    fi

    if git -C "$worktree" diff --cached --quiet; then
      return 2
    fi

    common::run git -C "$worktree" commit -m "Add noir report $report_dir_name" -m "$code_repo_refs" >/dev/null || return $?
  }

  common::checkout_branch_for_push "$worktree" "$reports_repo_url" "$reports_repo_branch"
  local stage_status=0
  if stage_noir_single_report_commit; then
    :
  else
    stage_status=$?
    if [[ "$stage_status" -eq 2 ]]; then
      common::log INFO "No report changes to push for $report_dir_name"
      rm -rf -- "$push_temp_dir"
      return 0
    fi
    rm -rf -- "$push_temp_dir"
    common::die "Failed to stage report commit for $report_dir_name"
  fi

  local attempt
  local push_output=""
  for attempt in 1 2 3 4 5; do
    if push_output="$(git -C "$worktree" push origin HEAD:"$reports_repo_branch" 2>&1)"; then
      common::log INFO "Pushed $report_dir_name to $reports_repo_url"
      rm -rf -- "$push_temp_dir"
      return 0
    fi

    common::log INFO "Push attempt $attempt failed for $report_dir_name"
    common::log INFO "$push_output"
    if [[ "$attempt" -eq 5 ]]; then
      break
    fi

    common::log INFO "Refreshing $reports_repo_branch and rebasing report commit before retry"
    if ! common::rebase_branch_for_push_retry "$worktree" "$reports_repo_branch"; then
      common::log INFO "Rebase failed; rebuilding report commit on the current remote branch"
      git -C "$worktree" rebase --abort >/dev/null 2>&1 || true
      common::checkout_branch_for_push "$worktree" "$reports_repo_url" "$reports_repo_branch"
      if stage_noir_single_report_commit; then
        :
      else
        stage_status=$?
        if [[ "$stage_status" -eq 2 ]]; then
          common::log INFO "No report changes to push after refresh for $report_dir_name"
          rm -rf -- "$push_temp_dir"
          return 0
        fi
        rm -rf -- "$push_temp_dir"
        common::die "Failed to restage report commit for $report_dir_name"
      fi
    fi
    common::sleep_for_push_retry "$attempt"
  done

  rm -rf -- "$push_temp_dir"
  common::die "Failed to push $report_dir_name after 5 attempts"
}

append_publish_manifest() {
  local manifest_path="$1"
  local report_dir="$2"
  local report_rel_path="$3"
  local reports_repo_url="$4"
  local reports_repo_branch="$5"
  local code_repo_refs="$6"
  local swagger_candidate_dir="${7:-}"
  local swagger_candidate_rel_path="${8:-}"

  [[ -n "$manifest_path" ]] || return 0
  common::ensure_dir "$(dirname "$manifest_path")"
  printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
    "$report_dir" \
    "$report_rel_path" \
    "$reports_repo_url" \
    "$reports_repo_branch" \
    "$code_repo_refs" \
    "$swagger_candidate_dir" \
    "$swagger_candidate_rel_path" >> "$manifest_path"
}

main() {
  local config_file_path=""
  local repo_snaps_json_path=""
  local -a code_root_git_dir_paths=()
  local oakb_tool_report_dir_path_no_ts=""
  local component_code=""
  local component_version=""
  local swagger_file_path=""
  local jenkins_job_name=""
  local jenkins_job_invoke_id=""
  local analyzer_repo_url="${APA_ANALYZER_REPO_URL:-}"
  local analyzer_repo_branch="${APA_ANALYZER_REPO_BRANCH:-}"
  local analyzer_repo_commit="${APA_ANALYZER_REPO_COMMIT:-}"
  local publish_manifest_path=""
  local tools_root_dir_path="./.oakb_tools"

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --config-file-path)
        [[ $# -ge 2 ]] || common::die "--config-file-path requires a value"
        config_file_path="$2"
        shift 2
        ;;
      --repo-snaps-json)
        [[ $# -ge 2 ]] || common::die "--repo-snaps-json requires a value"
        repo_snaps_json_path="$2"
        shift 2
        ;;
      --code-root-git-dir-path)
        [[ $# -ge 2 ]] || common::die "--code-root-git-dir-path requires a value"
        code_root_git_dir_paths+=("$2")
        shift 2
        ;;
      --oakb-tool-report-dir-path-no-ts)
        [[ $# -ge 2 ]] || common::die "--oakb-tool-report-dir-path-no-ts requires a value"
        oakb_tool_report_dir_path_no_ts="$2"
        shift 2
        ;;
      --component-code)
        [[ $# -ge 2 ]] || common::die "--component-code requires a value"
        component_code="$2"
        shift 2
        ;;
      --component-version)
        [[ $# -ge 2 ]] || common::die "--component-version requires a value"
        component_version="$2"
        shift 2
        ;;
      --swagger-file-path)
        [[ $# -ge 2 ]] || common::die "--swagger-file-path requires a value"
        swagger_file_path="$2"
        shift 2
        ;;
      --jenkins-job-name)
        [[ $# -ge 2 ]] || common::die "--jenkins-job-name requires a value"
        jenkins_job_name="$2"
        shift 2
        ;;
      --jenkins-job-invoke-id)
        [[ $# -ge 2 ]] || common::die "--jenkins-job-invoke-id requires a value"
        jenkins_job_invoke_id="$2"
        shift 2
        ;;
      --analyzer-repo-url)
        [[ $# -ge 2 ]] || common::die "--analyzer-repo-url requires a value"
        analyzer_repo_url="$2"
        shift 2
        ;;
      --analyzer-repo-branch)
        [[ $# -ge 2 ]] || common::die "--analyzer-repo-branch requires a value"
        analyzer_repo_branch="$2"
        shift 2
        ;;
      --analyzer-repo-commit)
        [[ $# -ge 2 ]] || common::die "--analyzer-repo-commit requires a value"
        analyzer_repo_commit="$2"
        shift 2
        ;;
      --publish-manifest-path)
        [[ $# -ge 2 ]] || common::die "--publish-manifest-path requires a value"
        publish_manifest_path="$2"
        shift 2
        ;;
      --tools-root-dir-path)
        [[ $# -ge 2 ]] || common::die "--tools-root-dir-path requires a value"
        tools_root_dir_path="$2"
        shift 2
        ;;
      --debug)
        export DEBUG=1
        shift
        ;;
      --help|-h)
        usage
        exit 0
        ;;
      *)
        common::die "Unknown argument: $1"
        ;;
    esac
  done

  common::require_commands curl git jq yq unzip
  common::enable_debug_if_requested
  common::export_tmp_env

  [[ -n "$config_file_path" ]] || common::die "--config-file-path is required"
  [[ -n "$repo_snaps_json_path" ]] || common::die "--repo-snaps-json is required"
  ((${#code_root_git_dir_paths[@]} > 0)) || common::die "At least one --code-root-git-dir-path is required"
  [[ -n "$oakb_tool_report_dir_path_no_ts" ]] || common::die "--oakb-tool-report-dir-path-no-ts is required"
  [[ -f "$repo_snaps_json_path" ]] || common::die "Repo snaps JSON file does not exist: $repo_snaps_json_path"
  jq -e 'type == "array" and length > 0 and all(.[]; ((.repo_url // "") != "") and ((.repo_commit // "") != ""))' "$repo_snaps_json_path" >/dev/null || common::die "Repo snaps JSON must be a non-empty array with repo_url and repo_commit"
  local code_root_git_dir_path
  for code_root_git_dir_path in "${code_root_git_dir_paths[@]}"; do
    [[ -d "$code_root_git_dir_path" ]] || common::die "Code root Git directory does not exist: $code_root_git_dir_path"
  done
  validate_swagger_file_path "$swagger_file_path"

  use_config::init --config-file-path "$config_file_path"

  local default_bundle_zip_url
  local bundle_zip_url
  local activation_script_rel
  local run_executable_rel
  local reports_repo_url
  local reports_repo_branch
  default_bundle_zip_url="$(use_config::must_value '.data.oakb_tools.noir.bundle_zip_url' 'data.oakb_tools.noir.bundle_zip_url')"
  activation_script_rel="$(use_config::must_value '.data.oakb_tools.noir.how2run.activation_script_path' 'data.oakb_tools.noir.how2run.activation_script_path')"
  run_executable_rel="$(use_config::must_value '.data.oakb_tools.noir.how2run.run_executable_path' 'data.oakb_tools.noir.how2run.run_executable_path')"
  reports_repo_url="$(use_config::must_value '.data.oakb_tools.noir.send_reports_to_repo.repo_url' 'data.oakb_tools.noir.send_reports_to_repo.repo_url')"
  reports_repo_branch="$(use_config::must_value '.data.oakb_tools.noir.send_reports_to_repo.repo_branch' 'data.oakb_tools.noir.send_reports_to_repo.repo_branch')"

  local os_release_file="${OAKB_OS_RELEASE_FILE:-/etc/os-release}"
  local os_id
  local os_version_id
  local os_version_major_value
  local bundle_variant_key
  os_id="$(key_value_file_value "$os_release_file" "ID")"
  os_version_id="$(key_value_file_value "$os_release_file" "VERSION_ID")"
  os_version_major_value="$(os_version_major "$os_version_id")"
  bundle_variant_key="$(noir_bundle_variant_key "$os_id" "$os_version_major_value")"
  bundle_zip_url="$(resolve_bundle_zip_url "$default_bundle_zip_url" "$bundle_variant_key" "$os_id" "$os_version_id" "$os_version_major_value" "$os_release_file")"

  local bundle_platform_id
  local bundle_variant_label
  bundle_platform_id="os_id=${os_id:-unknown};os_version_id=${os_version_id:-unknown};os_version_major=${os_version_major_value:-unknown};variant_key=${bundle_variant_key:-default}"
  if [[ -n "${OAKB_NOIR_BUNDLE_ZIP_URL:-}" ]]; then
    bundle_variant_label="override"
  elif [[ "$bundle_zip_url" == "$default_bundle_zip_url" ]]; then
    bundle_variant_label="default_el9"
  else
    bundle_variant_label="$bundle_variant_key"
  fi
  bundle_variant_label="$(normalize_bundle_variant_label "$bundle_variant_label")"

  local tools_root_abs
  tools_root_abs="$(common::target_path_abs "$tools_root_dir_path")"
  local tool_dir="$tools_root_abs/noir"
  prepare_bundle "$tool_dir" "$bundle_zip_url" "$activation_script_rel" "$run_executable_rel" "$bundle_platform_id"

  local activation_script_abs
  local run_executable_abs
  activation_script_abs="$(common::join_path "$tool_dir" "$activation_script_rel")"
  run_executable_abs="$(common::join_path "$tool_dir" "$run_executable_rel")"

  local report_dir_base_abs
  report_dir_base_abs="$(common::target_path_abs "$oakb_tool_report_dir_path_no_ts")"
  common::ensure_dir "$report_dir_base_abs"

  local report_timestamp_compact
  local report_timestamp_rfc3339
  local report_dir_name
  local report_rel_path
  local report_dir_path
  report_timestamp_compact="$(common::timestamp_utc_compact)"
  report_timestamp_rfc3339="$(common::timestamp_utc_rfc3339)"
  report_dir_name="noir_${report_timestamp_compact}_$$"
  report_rel_path="$(common::report_rel_path_from_dir_name "$report_timestamp_compact" "$report_dir_name")"
  report_dir_path="$report_dir_base_abs/$report_rel_path"

  local report_data_dir="$report_dir_path/report_data"
  local meta_dir="$report_dir_path/report_meta"
  common::ensure_dir "$report_dir_path"
  common::ensure_dir "$report_data_dir"
  common::ensure_dir "$meta_dir"

  common::log INFO "Running noir against ${#code_root_git_dir_paths[@]} root(s)"
  # shellcheck disable=SC1090
  source "$activation_script_abs"

  local -a noir_base_args=()
  for code_root_git_dir_path in "${code_root_git_dir_paths[@]}"; do
    noir_base_args+=(-b "$code_root_git_dir_path")
  done
  if ! common::run "$run_executable_abs" "${noir_base_args[@]}" -u https://example.com -f json --include-path -o "$report_data_dir/noir-report.json" --exclude-techs oas_2_0,oas_3_0; then
    common::die "Noir command failed while producing main JSON report: $report_data_dir/noir-report.json"
  fi
  validate_main_json_report_file "$report_data_dir/noir-report.json"
  run_noir_json_report_with_stdout_fallback "$run_executable_abs" "$report_data_dir/interfaces.json" "$report_data_dir/noir-report.json" "${noir_base_args[@]}" -f json --include-path --include-techs --no-log
  pretty_print_json_file "$report_data_dir/interfaces.json"
  common::run "$run_executable_abs" "${noir_base_args[@]}" --include-path --include-techs --no-log --no-color > "$report_data_dir/interfaces.plain.txt"

  local swagger_candidate_dir_path=""
  local swagger_candidate_rel_path=""
  if [[ -n "$swagger_file_path" ]]; then
    swagger_candidate_rel_path="tuple_artifact_candidates/swagger/$report_rel_path"
    swagger_candidate_dir_path="$report_dir_base_abs/$swagger_candidate_rel_path"
    publish_swagger_candidate \
      "$swagger_file_path" \
      "$swagger_candidate_dir_path" \
      "$component_code" \
      "$component_version" \
      "$report_rel_path" \
      "$jenkins_job_name" \
      "$jenkins_job_invoke_id" \
      "$report_timestamp_rfc3339"
  fi

  local tool_version
  tool_version="$(detect_tool_version "$run_executable_abs")"

  write_meta_file \
    "$meta_dir/noir_meta.yaml" \
    "$report_timestamp_rfc3339" \
    "$tool_version" \
    "$repo_snaps_json_path" \
    "$component_code" \
    "$component_version" \
    "$report_dir_name" \
    "noir-report.json" \
    "$jenkins_job_name" \
    "$jenkins_job_invoke_id" \
    "$analyzer_repo_url" \
    "$analyzer_repo_branch" \
    "$analyzer_repo_commit" \
    "$bundle_variant_label" \
    "$bundle_zip_url" \
    "$bundle_platform_id"

  if [[ -n "$publish_manifest_path" ]]; then
    local code_repo_refs
    code_repo_refs="$(jq -r 'map((.repo_url // "") + "@" + (.repo_commit // "")) | join(", ")' "$repo_snaps_json_path")"
    append_publish_manifest \
      "$publish_manifest_path" \
      "$report_dir_path" \
      "$report_rel_path" \
      "$reports_repo_url" \
      "$reports_repo_branch" \
      "$code_repo_refs" \
      "$swagger_candidate_dir_path" \
      "$swagger_candidate_rel_path"
  else
    local code_repo_refs
    code_repo_refs="$(jq -r 'map((.repo_url // "") + "@" + (.repo_commit // "")) | join(", ")' "$repo_snaps_json_path")"
    push_report_to_repo \
      "$report_dir_path" \
      "$report_rel_path" \
      "$reports_repo_url" \
      "$reports_repo_branch" \
      "$code_repo_refs" \
      "$swagger_candidate_dir_path" \
      "$swagger_candidate_rel_path"
  fi
}

main "$@"