diff --git a/scripts/pr-state b/scripts/pr-state index a416f2af21..1d2c701bd0 100755 --- a/scripts/pr-state +++ b/scripts/pr-state @@ -193,7 +193,7 @@ fi extract_checks_json() { local status_json=$1 - jq -cn --slurpfile checks_in <(printf '%s\n' "$status_json") ' + printf '%s\n' "$status_json" | jq -c ' def lower_or_null: if . == null or . == "" then null else ascii_downcase end; def extract($re; $field): @@ -205,8 +205,7 @@ extract_checks_json() { ((.state // "") | lower_or_null) as $state | if $state == "pending" or $state == "expected" then null else $state end end; - $checks_in[0] - | to_entries + to_entries | map(.value + {_index: .key} | { type: (.["__typename"] // null), name: (.name // .context // "unknown"), @@ -242,9 +241,8 @@ extract_checks_json() { extract_issue_comments_json() { local comments_json=$1 local since=$2 - jq -cn --slurpfile comments <(printf '%s\n' "$comments_json") --arg since "$since" ' - $comments[0] - | map(select($since == "" or ((.createdAt // .created_at // "") > $since))) + printf '%s\n' "$comments_json" | jq -c --arg since "$since" ' + map(select($since == "" or ((.createdAt // .created_at // "") > $since))) | map({ author: (.author.login // "unknown"), body: (.body // ""), @@ -265,9 +263,8 @@ extract_issue_comments_json() { extract_reviews_json() { local reviews_api_json=$1 local since=$2 - jq -cn --slurpfile reviews <(printf '%s\n' "$reviews_api_json") --arg since "$since" ' - $reviews[0] - | map(select($since == "" or ((.submitted_at // "") > $since))) + printf '%s\n' "$reviews_api_json" | jq -c --arg since "$since" ' + map(select($since == "" or ((.submitted_at // "") > $since))) | map({ author: (.user.login // "unknown"), state: ((.state // "unknown") | ascii_downcase), @@ -287,10 +284,10 @@ review_evidence_json() { local reviews_api_json=$5 local is_cross_repository=$6 local trusted_workflow_json=$7 - jq -cn --arg opening_head_oid "$opening_head_oid" --arg closing_head_oid "$closing_head_oid" --arg checks_head_oid "$checks_head_oid" --arg trusted_reviewer "$TRUSTED_REVIEWER" --argjson is_cross_repository "$is_cross_repository" --argjson reviews_known "$reviews_known" --argjson trusted_workflow "$trusted_workflow_json" \ - --slurpfile reviews <(printf '%s\n' "$reviews_api_json") ' + printf '%s\n' "$reviews_api_json" | + jq -c --arg opening_head_oid "$opening_head_oid" --arg closing_head_oid "$closing_head_oid" --arg checks_head_oid "$checks_head_oid" --arg trusted_reviewer "$TRUSTED_REVIEWER" --argjson is_cross_repository "$is_cross_repository" --argjson reviews_known "$reviews_known" --argjson trusted_workflow "$trusted_workflow_json" ' def normalized_reviews: - $reviews[0] | map({ + map({ author: (.user.login // "unknown"), state: ((.state // "unknown") | ascii_downcase), commit_id: (.commit_id // null), @@ -438,15 +435,14 @@ fetch_trusted_workflow_json() { extract_review_threads_json() { local threads_nodes_json=$1 local since=$2 - jq -cn --slurpfile threads <(printf '%s\n' "$threads_nodes_json") --arg since "$since" ' + printf '%s\n' "$threads_nodes_json" | jq -c --arg since "$since" ' def relevant_comments: if $since == "" then (.comments.nodes // []) else [(.comments.nodes // [])[] | select((.createdAt // "") > $since)] end; - $threads[0] - | map(. as $thread | (relevant_comments) as $comments | select($comments | length > 0) | { + map(. as $thread | (relevant_comments) as $comments | select($comments | length > 0) | { thread_id: .id, is_resolved: (.isResolved // false), path: (.path // ""), @@ -622,10 +618,9 @@ fetch_review_threads_nodes_json() { page_nodes="$(jq -c '.data.repository.pullRequest.reviewThreads.nodes // []' <<<"$page_json")" local next_nodes_file next_nodes_file="$(mktemp)" - if ! jq -cn \ + if ! printf '%s\n' "$page_nodes" | jq -cn \ --slurpfile prev "$nodes_file" \ - --slurpfile page <(printf '%s\n' "$page_nodes") \ - '$prev[0] + $page[0]' >"$next_nodes_file"; then + '$prev[0] + input' >"$next_nodes_file"; then add_error "review_threads" "failed to merge paginated review threads" rm -f "$next_nodes_file" "$nodes_file" return 1 @@ -717,12 +712,11 @@ if [[ -n "$repo_owner" && -n "$repo_name" && -n "$resolved_pr_number" ]]; then if threads_nodes_json="$(fetch_review_threads_nodes_json "$repo_owner" "$repo_name" "$resolved_pr_number")"; then review_threads_json="$(extract_review_threads_json "$threads_nodes_json" "$since")" all_review_threads_json="$(extract_review_threads_json "$threads_nodes_json" "")" - hidden_unresolved_threads_json="$(jq -cn \ - --slurpfile all <(printf '%s\n' "$all_review_threads_json") \ - --slurpfile filtered <(printf '%s\n' "$review_threads_json") \ - ' - ($filtered[0] | map(.thread_id)) as $filtered_ids - | $all[0] + hidden_unresolved_threads_json="$(printf '%s\n%s\n' "$all_review_threads_json" "$review_threads_json" | + jq -cn ' + [inputs] as [$all, $filtered] + | ($filtered | map(.thread_id)) as $filtered_ids + | $all | map(select(.is_resolved == false and (.thread_id as $id | $filtered_ids | index($id) | not))) | map({thread_id, comment_id, author, path, body_preview}) ')" diff --git a/scripts/pr-state.test.sh b/scripts/pr-state.test.sh index 257684e209..f634044a20 100644 --- a/scripts/pr-state.test.sh +++ b/scripts/pr-state.test.sh @@ -1263,6 +1263,47 @@ test_comment_mode_rejects_incompatible_flags() { pass "--comment rejects incompatible flags" } +join_line_continuations() { + local file=$1 + local line buffer='' lineno=0 start=1 + while IFS= read -r line || [[ -n "$line" ]]; do + lineno=$((lineno + 1)) + [[ -n "$buffer" ]] || start=$lineno + if [[ "$line" == *\\ ]]; then + buffer+="${line%\\} " + continue + fi + printf '%s:%s\n' "$start" "$buffer$line" + buffer='' + done <"$file" + [[ -z "$buffer" ]] || printf '%s:%s\n' "$start" "$buffer" +} + +test_jq_file_args_avoid_process_substitution() { + local pattern='(--slurpfile|--rawfile|--argfile)[[:space:]]+[^[:space:]]+[[:space:]]+<\(' + local tmp + make_tmp_dir tmp + + printf '%s\n' "jq -cn --slurpfile page <(printf x) '.'" >"$tmp/same_line.sh" + if ! join_line_continuations "$tmp/same_line.sh" | grep -qE "$pattern"; then + fail "guard misses process substitution on a single line" + fi + + printf '%s\n' 'jq -cn \' ' --slurpfile page \' ' <(printf x)' >"$tmp/continued.sh" + if ! join_line_continuations "$tmp/continued.sh" | grep -qE "$pattern"; then + fail "guard misses process substitution split across a line continuation" + fi + + local offenders + offenders="$(join_line_continuations "$SCRIPT" | grep -E "$pattern" || true)" + if [[ -n "$offenders" ]]; then + printf '%s\n' "$offenders" >&2 + fail "jq file arguments must not come from process substitution (native jq cannot open /proc//fd/)" + fi + pass "jq file arguments avoid process substitution" +} + +test_jq_file_args_avoid_process_substitution test_snapshot_happy_path test_old_head_review_does_not_qualify test_exact_head_selected_review_qualifies