diff --git a/.github/workflows/reusable-code.yml b/.github/workflows/reusable-code.yml index fc504862ef..b851bccbe0 100644 --- a/.github/workflows/reusable-code.yml +++ b/.github/workflows/reusable-code.yml @@ -160,6 +160,7 @@ jobs: REPO_FULL_NAME: ${{ inputs.source_repo }} GITHUB_ISSUE_URL: ${{ fromJSON(inputs.event_payload).issue.html_url }} COMMENT_BODY: ${{ fromJSON(inputs.event_payload).comment.body }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} FULLSEND_DIR: ${{ inputs.install_mode == 'per-repo' && '.fullsend' || '' }} run: bash "${FULLSEND_DIR:+$FULLSEND_DIR/}scripts/pre-code.sh" diff --git a/.github/workflows/reusable-fix.yml b/.github/workflows/reusable-fix.yml index 6cee2a0a8f..9135b1c346 100644 --- a/.github/workflows/reusable-fix.yml +++ b/.github/workflows/reusable-fix.yml @@ -373,6 +373,7 @@ jobs: TRIGGER_SOURCE: ${{ inputs.trigger_source }} FIX_ITERATION: ${{ steps.context.outputs.iteration }} HUMAN_INSTRUCTION: ${{ steps.context.outputs.instruction }} + FIX_SKIP_TOOL_INSTALL: "true" FULLSEND_DIR: ${{ inputs.install_mode == 'per-repo' && '.fullsend' || '' }} run: bash "${FULLSEND_DIR:+$FULLSEND_DIR/}scripts/pre-fix.sh" diff --git a/Makefile b/Makefile index 949b330579..ead0df3add 100644 --- a/Makefile +++ b/Makefile @@ -138,6 +138,7 @@ script-test: $(call run-timed,bash internal/scaffold/fullsend-repo/scripts/reconcile-repos-test.sh) $(call run-timed,bash internal/scaffold/fullsend-repo/scripts/validate-output-schema-test.sh) $(call run-timed,bash internal/scaffold/fullsend-repo/scripts/pre-code-test.sh) + $(call run-timed,bash internal/scaffold/fullsend-repo/scripts/pre-fix-test.sh) $(call run-timed,bash internal/scaffold/fullsend-repo/scripts/pre-fetch-prior-review-test.sh) $(call run-timed,python3 internal/scaffold/fullsend-repo/scripts/process-fix-result-test.py) $(call run-timed,python3 skills/topissues/scripts/topissues_test.py) diff --git a/internal/scaffold/fullsend-repo/harness/code.yaml b/internal/scaffold/fullsend-repo/harness/code.yaml index 728cf919f5..f601681d6e 100644 --- a/internal/scaffold/fullsend-repo/harness/code.yaml +++ b/internal/scaffold/fullsend-repo/harness/code.yaml @@ -63,3 +63,5 @@ forge: REPO_FULL_NAME: "${REPO_FULL_NAME}" ISSUE_NUMBER: "${ISSUE_NUMBER}" REPO_DIR: "${GITHUB_WORKSPACE}/target-repo" + # Skips the already-performed existing-PR check (issue #4718) + CODE_SKIP_EXISTING_PR_CHECK: "true" diff --git a/internal/scaffold/fullsend-repo/scripts/pre-code-test.sh b/internal/scaffold/fullsend-repo/scripts/pre-code-test.sh index 57aecfe990..5141e2909f 100644 --- a/internal/scaffold/fullsend-repo/scripts/pre-code-test.sh +++ b/internal/scaffold/fullsend-repo/scripts/pre-code-test.sh @@ -461,6 +461,94 @@ run_test_github_output "skip-output-false-on-no-token" \ 0 \ "GH_TOKEN=" +# --- CODE_SKIP_EXISTING_PR_CHECK tests (issue #4718) --- +# +# harness/code.yaml sets this for the second (harness pre_script) invocation +# of pre-code.sh, so the existing-human-PR search/label/comment side effects +# — already performed by the reusable workflow's inline "Validate inputs" +# step — don't run a second time. + +# Human PR exists, but the flag is set → check must be bypassed entirely, +# not just short-circuited by the existing branches (no PR search, no +# "Skipping code agent", proceeds as if no PR was found). +run_test_stdout_excludes "gate-skip-flag-bypasses-check" \ + "${HUMAN_PR_JSON}" \ + "already performed by the workflow's inline gate" \ + "Checking for existing open PRs" \ + 0 \ + "CODE_SKIP_EXISTING_PR_CHECK=true" + +run_test_stdout_excludes "gate-skip-flag-does-not-skip-agent" \ + "${HUMAN_PR_JSON}" \ + "already performed by the workflow's inline gate" \ + "Skipping code agent" \ + 0 \ + "CODE_SKIP_EXISTING_PR_CHECK=true" + +# The flag skips the whole block, including the skipped= write — nothing +# downstream reads GITHUB_OUTPUT from this invocation, so it must stay empty. +run_test_github_output_excludes() { + local test_name="$1" + local pr_list_output="$2" + local excluded_output="$3" + local expect_exit="$4" + local extra_env="${5:-}" + + local mock_bin + mock_bin="$(build_mock "${pr_list_output}")" + local gh_output="${TMPDIR}/github-output.txt" + : > "${gh_output}" + + local env_cmd=( + env + PATH="${mock_bin}:${PATH}" + ISSUE_NUMBER="42" + REPO_FULL_NAME="test-org/test-repo" + GITHUB_ISSUE_URL="https://github.com/test-org/test-repo/issues/42" + GH_TOKEN="fake-token" + GITHUB_OUTPUT="${gh_output}" + ) + + if [[ -n "${extra_env}" ]]; then + while IFS= read -r kv; do + [[ -n "${kv}" ]] && env_cmd+=("${kv}") + done <<< "${extra_env}" + fi + + local exit_code=0 + "${env_cmd[@]}" bash "${PRE_SCRIPT}" > "${TMPDIR}/stdout.log" 2>&1 || exit_code=$? + + if [[ ${exit_code} -ne ${expect_exit} ]]; then + echo "FAIL: ${test_name} — expected exit ${expect_exit}, got ${exit_code}" + cat "${TMPDIR}/stdout.log" + FAILURES=$((FAILURES + 1)) + return + fi + + if grep -qF "${excluded_output}" "${gh_output}" 2>/dev/null; then + echo "FAIL: ${test_name} — GITHUB_OUTPUT unexpectedly contains '${excluded_output}'" + echo "Actual GITHUB_OUTPUT:" + cat "${gh_output}" + FAILURES=$((FAILURES + 1)) + return + fi + + echo "PASS: ${test_name}" +} + +run_test_github_output_excludes "gate-skip-flag-writes-no-skipped-output" \ + "${HUMAN_PR_JSON}" \ + "skipped=" \ + 0 \ + "CODE_SKIP_EXISTING_PR_CHECK=true" + +# Flag unset (default) → behavior is completely unchanged from before #4718: +# the existing-PR check still runs and still blocks on a human PR. +run_test_stdout "gate-skip-flag-unset-check-still-runs" \ + "${HUMAN_PR_JSON}" \ + "Checking for existing open PRs" \ + 0 + # --- Summary --- echo "" diff --git a/internal/scaffold/fullsend-repo/scripts/pre-code.sh b/internal/scaffold/fullsend-repo/scripts/pre-code.sh index d801e80796..e884933826 100755 --- a/internal/scaffold/fullsend-repo/scripts/pre-code.sh +++ b/internal/scaffold/fullsend-repo/scripts/pre-code.sh @@ -8,6 +8,20 @@ # ISSUE_NUMBER — must be a positive integer # REPO_FULL_NAME — must be owner/repo format # GITHUB_ISSUE_URL — must be a valid GitHub issue URL +# +# Optional environment variables: +# CODE_SKIP_EXISTING_PR_CHECK +# — "true" to skip the existing-human-PR search below. +# Set via harness/code.yaml's forge.github.runner_env +# so the *second* invocation of this script (as the +# harness pre_script, run by `fullsend run` right +# before sandbox creation) doesn't repeat the GH API +# search/label/comment that the reusable workflow's +# inline "Validate inputs" step already performed +# before the expensive GCP/bot-identity/agent-env +# setup steps (issue #4718). Leave unset for that +# inline invocation — it is the one that must produce +# the skipped= output those steps gate on. set -euo pipefail echo "::notice::🔗 Code target: ${GITHUB_ISSUE_URL:-}" @@ -54,73 +68,77 @@ echo " GITHUB_ISSUE_URL=${GITHUB_ISSUE_URL}" # --------------------------------------------------------------------------- # Check for existing human PRs linked to this issue # --------------------------------------------------------------------------- -# Skip if GH_TOKEN is not available (best-effort check). -if [[ -z "${GH_TOKEN:-}" ]]; then - echo "GH_TOKEN not set — skipping existing-PR check" - echo "skipped=false" >> "${GITHUB_OUTPUT:-/dev/null}" - exit 0 -fi +if [[ "${CODE_SKIP_EXISTING_PR_CHECK:-false}" == "true" ]]; then + echo "Existing-PR check already performed by the workflow's inline gate — skipping" +else + # Skip if GH_TOKEN is not available (best-effort check). + if [[ -z "${GH_TOKEN:-}" ]]; then + echo "GH_TOKEN not set — skipping existing-PR check" + echo "skipped=false" >> "${GITHUB_OUTPUT:-/dev/null}" + exit 0 + fi -# Allow override when --force is in the trigger comment or CODE_FORCE is set. -echo "Evaluating force override: CODE_FORCE='${CODE_FORCE:-}' COMMENT_BODY='${COMMENT_BODY:-}'" -if [[ "${CODE_FORCE:-}" == "true" ]] || [[ "${COMMENT_BODY:-}" == *--force* ]]; then - echo "Force override — skipping existing-PR check" - echo "skipped=false" >> "${GITHUB_OUTPUT:-/dev/null}" - exit 0 -fi + # Allow override when --force is in the trigger comment or CODE_FORCE is set. + echo "Evaluating force override: CODE_FORCE='${CODE_FORCE:-}' COMMENT_BODY='${COMMENT_BODY:-}'" + if [[ "${CODE_FORCE:-}" == "true" ]] || [[ "${COMMENT_BODY:-}" == *--force* ]]; then + echo "Force override — skipping existing-PR check" + echo "skipped=false" >> "${GITHUB_OUTPUT:-/dev/null}" + exit 0 + fi -BOT_LOGIN="fullsend-ai[bot]" -CODER_BOT_LOGIN="fullsend-ai-coder[bot]" - -echo "Checking for existing open PRs linked to issue #${ISSUE_NUMBER}..." - -# Search for open PRs in the repo that mention the issue number. -# This catches PRs with "Closes #N", "Fixes #N", or "#N" in the body/title. -# Use gh's built-in --jq to filter out bot-authored PRs in one call. -HUMAN_PR_LINES="$(gh pr list --repo "${REPO_FULL_NAME}" --state open \ - --search "${ISSUE_NUMBER} in:body,title" \ - --json number,url,author \ - --jq "[.[] | select(.author.login != \"${BOT_LOGIN}\" and .author.login != \"${CODER_BOT_LOGIN}\")] | .[] | \"\(.number)\t\(.author.login)\t\(.url)\"" \ - 2>/dev/null || true)" - -if [[ -n "${HUMAN_PR_LINES}" ]]; then - # Parse the first PR for the notice. - FIRST_PR_NUM="$(echo "${HUMAN_PR_LINES}" | head -1 | cut -f1)" - FIRST_PR_AUTHOR="$(echo "${HUMAN_PR_LINES}" | head -1 | cut -f2)" - - echo "::notice::Found existing human PR #${FIRST_PR_NUM} by @${FIRST_PR_AUTHOR}" - - # Apply pr-open label to signal work is already underway. - gh label create "pr-open" --repo "${REPO_FULL_NAME}" \ - --description "An open PR already addresses this issue" --color "D4C5F9" \ - --force 2>/dev/null || true - gh api "repos/${REPO_FULL_NAME}/issues/${ISSUE_NUMBER}/labels" \ - -f "labels[]=pr-open" --silent 2>/dev/null || true - - # Build a markdown list of existing PRs. - PR_LIST_MD="" - while IFS=$'\t' read -r pr_num pr_author _pr_url; do - PR_LIST_MD="${PR_LIST_MD} + BOT_LOGIN="fullsend-ai[bot]" + CODER_BOT_LOGIN="fullsend-ai-coder[bot]" + + echo "Checking for existing open PRs linked to issue #${ISSUE_NUMBER}..." + + # Search for open PRs in the repo that mention the issue number. + # This catches PRs with "Closes #N", "Fixes #N", or "#N" in the body/title. + # Use gh's built-in --jq to filter out bot-authored PRs in one call. + HUMAN_PR_LINES="$(gh pr list --repo "${REPO_FULL_NAME}" --state open \ + --search "${ISSUE_NUMBER} in:body,title" \ + --json number,url,author \ + --jq "[.[] | select(.author.login != \"${BOT_LOGIN}\" and .author.login != \"${CODER_BOT_LOGIN}\")] | .[] | \"\(.number)\t\(.author.login)\t\(.url)\"" \ + 2>/dev/null || true)" + + if [[ -n "${HUMAN_PR_LINES}" ]]; then + # Parse the first PR for the notice. + FIRST_PR_NUM="$(echo "${HUMAN_PR_LINES}" | head -1 | cut -f1)" + FIRST_PR_AUTHOR="$(echo "${HUMAN_PR_LINES}" | head -1 | cut -f2)" + + echo "::notice::Found existing human PR #${FIRST_PR_NUM} by @${FIRST_PR_AUTHOR}" + + # Apply pr-open label to signal work is already underway. + gh label create "pr-open" --repo "${REPO_FULL_NAME}" \ + --description "An open PR already addresses this issue" --color "D4C5F9" \ + --force 2>/dev/null || true + gh api "repos/${REPO_FULL_NAME}/issues/${ISSUE_NUMBER}/labels" \ + -f "labels[]=pr-open" --silent 2>/dev/null || true + + # Build a markdown list of existing PRs. + PR_LIST_MD="" + while IFS=$'\t' read -r pr_num pr_author _pr_url; do + PR_LIST_MD="${PR_LIST_MD} - #${pr_num} by @${pr_author}" - done <<< "${HUMAN_PR_LINES}" + done <<< "${HUMAN_PR_LINES}" - SKIP_COMMENT="An open PR already addresses this issue — skipping automated implementation. + SKIP_COMMENT="An open PR already addresses this issue — skipping automated implementation. ${PR_LIST_MD} To override, comment \`/fs-code --force\` on this issue. Posted by fullsend pre-code check" - printf '%s' "${SKIP_COMMENT}" | gh issue comment "${ISSUE_NUMBER}" \ - --repo "${REPO_FULL_NAME}" --body-file - 2>/dev/null || true + printf '%s' "${SKIP_COMMENT}" | gh issue comment "${ISSUE_NUMBER}" \ + --repo "${REPO_FULL_NAME}" --body-file - 2>/dev/null || true - echo "Skipping code agent — existing PR(s) found for issue #${ISSUE_NUMBER}" - echo "skipped=true" >> "${GITHUB_OUTPUT:-/dev/null}" - exit 0 -fi + echo "Skipping code agent — existing PR(s) found for issue #${ISSUE_NUMBER}" + echo "skipped=true" >> "${GITHUB_OUTPUT:-/dev/null}" + exit 0 + fi -echo "No existing human PRs found — proceeding with code agent" -echo "skipped=false" >> "${GITHUB_OUTPUT:-/dev/null}" + echo "No existing human PRs found — proceeding with code agent" + echo "skipped=false" >> "${GITHUB_OUTPUT:-/dev/null}" +fi # --------------------------------------------------------------------------- # Auto-detect and install pre-commit tool dependencies diff --git a/internal/scaffold/fullsend-repo/scripts/pre-fix-test.sh b/internal/scaffold/fullsend-repo/scripts/pre-fix-test.sh new file mode 100644 index 0000000000..e518f99018 --- /dev/null +++ b/internal/scaffold/fullsend-repo/scripts/pre-fix-test.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# pre-fix-test.sh — Test pre-fix.sh input validation, iteration cap, and +# the FIX_SKIP_TOOL_INSTALL flag (issue #4718). +# +# Run from the repo root: bash internal/scaffold/fullsend-repo/scripts/pre-fix-test.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PRE_SCRIPT="${SCRIPT_DIR}/pre-fix.sh" +FAILURES=0 + +TMPDIR="$(mktemp -d)" +trap 'rm -rf "${TMPDIR}"' EXIT + +# run_test invokes pre-fix.sh with a base set of valid env vars, overridden/ +# extended by extra_env, and checks the exit code plus an optional stdout +# substring. +run_test() { + local test_name="$1" + local expect_exit="$2" + local expected_stdout="${3:-}" + local extra_env="${4:-}" + + local env_cmd=( + env + PR_NUMBER="42" + REPO_FULL_NAME="test-org/test-repo" + TRIGGER_SOURCE="human-dev" + ) + + if [[ -n "${extra_env}" ]]; then + while IFS= read -r kv; do + [[ -n "${kv}" ]] && env_cmd+=("${kv}") + done <<< "${extra_env}" + fi + + local exit_code=0 + "${env_cmd[@]}" bash "${PRE_SCRIPT}" > "${TMPDIR}/stdout.log" 2>&1 || exit_code=$? + + if [[ ${exit_code} -ne ${expect_exit} ]]; then + echo "FAIL: ${test_name} — expected exit ${expect_exit}, got ${exit_code}" + cat "${TMPDIR}/stdout.log" + FAILURES=$((FAILURES + 1)) + return + fi + + if [[ -n "${expected_stdout}" ]] && ! grep -qF "${expected_stdout}" "${TMPDIR}/stdout.log" 2>/dev/null; then + echo "FAIL: ${test_name} — expected stdout '${expected_stdout}' not found" + echo "Actual stdout:" + cat "${TMPDIR}/stdout.log" + FAILURES=$((FAILURES + 1)) + return + fi + + echo "PASS: ${test_name}" +} + +run_test_excludes() { + local test_name="$1" + local expect_exit="$2" + local excluded_stdout="$3" + local extra_env="${4:-}" + + local env_cmd=( + env + PR_NUMBER="42" + REPO_FULL_NAME="test-org/test-repo" + TRIGGER_SOURCE="human-dev" + ) + + if [[ -n "${extra_env}" ]]; then + while IFS= read -r kv; do + [[ -n "${kv}" ]] && env_cmd+=("${kv}") + done <<< "${extra_env}" + fi + + local exit_code=0 + "${env_cmd[@]}" bash "${PRE_SCRIPT}" > "${TMPDIR}/stdout.log" 2>&1 || exit_code=$? + + if [[ ${exit_code} -ne ${expect_exit} ]]; then + echo "FAIL: ${test_name} — expected exit ${expect_exit}, got ${exit_code}" + cat "${TMPDIR}/stdout.log" + FAILURES=$((FAILURES + 1)) + return + fi + + if grep -qF "${excluded_stdout}" "${TMPDIR}/stdout.log" 2>/dev/null; then + echo "FAIL: ${test_name} — excluded stdout '${excluded_stdout}' was found" + echo "Actual stdout:" + cat "${TMPDIR}/stdout.log" + FAILURES=$((FAILURES + 1)) + return + fi + + echo "PASS: ${test_name}" +} + +# --- Input validation --- + +run_test "valid-input-passes" 0 "Input validation passed" +run_test "invalid-pr-number-fails" 1 "" "PR_NUMBER=abc" +run_test "invalid-repo-full-name-fails" 1 "" "REPO_FULL_NAME=not-a-repo" +run_test "missing-trigger-source-fails" 1 "" "TRIGGER_SOURCE=" + +# --- Instruction length cap --- + +run_test "instruction-within-cap-passes" 0 "Input validation passed" \ + "HUMAN_INSTRUCTION=short instruction" + +LONG_INSTRUCTION="$(printf 'a%.0s' {1..10001})" +run_test "instruction-over-cap-fails" 1 "" \ + "HUMAN_INSTRUCTION=${LONG_INSTRUCTION}" + +# --- Iteration cap --- + +run_test "human-iteration-within-cap-passes" 0 "Input validation passed" \ + "FIX_ITERATION=10" +run_test "human-iteration-over-cap-fails" 1 "" \ + "FIX_ITERATION=11" + +run_test "bot-iteration-within-cap-passes" 0 "Input validation passed" \ + "TRIGGER_SOURCE=fullsend-ai-coder[bot] +FIX_ITERATION=5" +run_test "bot-iteration-over-cap-fails" 1 "" \ + "TRIGGER_SOURCE=fullsend-ai-coder[bot] +FIX_ITERATION=6" + +# --- FIX_SKIP_TOOL_INSTALL (issue #4718) --- +# +# reusable-fix.yml's inline "Validate inputs" step sets this so it can fail +# fast on bad input/iteration cap without also running the tool auto-install +# — that only needs to happen once, in the harness pre_script invocation +# inside "Run fix agent". + +run_test "skip-tool-install-flag-defers" 0 \ + "Tool auto-install deferred to the harness pre_script invocation — skipping here" \ + "FIX_SKIP_TOOL_INSTALL=true" + +run_test_excludes "skip-tool-install-flag-unset-does-not-defer" 0 \ + "deferred to the harness pre_script invocation" + +# --- Summary --- + +echo "" +if [[ ${FAILURES} -gt 0 ]]; then + echo "${FAILURES} test(s) failed" + exit 1 +fi +echo "All tests passed" diff --git a/internal/scaffold/fullsend-repo/scripts/pre-fix.sh b/internal/scaffold/fullsend-repo/scripts/pre-fix.sh index 8264cedfba..9e2e6057cc 100644 --- a/internal/scaffold/fullsend-repo/scripts/pre-fix.sh +++ b/internal/scaffold/fullsend-repo/scripts/pre-fix.sh @@ -15,6 +15,16 @@ # ITERATION_CAP — max bot-triggered iterations (default: 5) # ITERATION_CAP_HUMAN — max human-triggered iterations (default: 10) # HUMAN_INSTRUCTION — instruction text (only when TRIGGER_SOURCE doesn't end in [bot]) +# FIX_SKIP_TOOL_INSTALL +# — "true" to skip the pre-commit tool auto-install below. +# Set on reusable-fix.yml's inline "Validate inputs" +# step, which runs this script before GCP/agent-env +# setup purely to fail fast on bad input or an +# exceeded iteration cap — it has no need to install +# tools onto the runner. Leave unset for the harness +# pre_script invocation (inside "Run fix agent"), +# which does need the tools installed before the +# post-script's pre-commit run (issue #4718). set -euo pipefail # --------------------------------------------------------------------------- @@ -103,64 +113,68 @@ fi # --------------------------------------------------------------------------- # Auto-detect and install pre-commit tool dependencies # --------------------------------------------------------------------------- -# Ensures tools required by the target repo's pre-commit hooks are -# available on the runner for the authoritative post-script check. -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -TARGET_REPO="${REPO_DIR:-${GITHUB_WORKSPACE:-}/target-repo}" -RESOLVE_SCRIPT="${SCRIPT_DIR}/resolve-precommit-tools.py" -INSTALL_SCRIPT="${SCRIPT_DIR}/install-precommit-tools.sh" - -# Fallback: when this script is fetched as a single blob from a URL base, -# BASH_SOURCE points to a temp dir with no siblings. The reusable workflow's -# "Prepare workspace" step always materializes the full scripts/ directory -# at ${GITHUB_WORKSPACE}/scripts/ (per-org) or ${GITHUB_WORKSPACE}/.fullsend/scripts/ -# (per-repo). Try those paths when the BASH_SOURCE-relative lookup misses. -if [ ! -f "${RESOLVE_SCRIPT}" ] || [ ! -f "${INSTALL_SCRIPT}" ]; then - for _ws_candidate in "${GITHUB_WORKSPACE:-}/scripts" "${GITHUB_WORKSPACE:-}/.fullsend/scripts"; do - if [ -f "${_ws_candidate}/resolve-precommit-tools.py" ] \ - && [ -f "${_ws_candidate}/install-precommit-tools.sh" ]; then - RESOLVE_SCRIPT="${_ws_candidate}/resolve-precommit-tools.py" - INSTALL_SCRIPT="${_ws_candidate}/install-precommit-tools.sh" - break - fi - done -fi - -# Warn instead of silently skipping when the repo needs the auto-install but -# the companions are missing everywhere (issue #3070) — a silent skip here -# surfaces later as a confusing "Executable X not found" pre-commit failure. -if [ -f "${TARGET_REPO}/.pre-commit-config.yaml" ] \ - && { [ ! -f "${RESOLVE_SCRIPT}" ] || [ ! -f "${INSTALL_SCRIPT}" ]; }; then - echo "::warning::Pre-commit tool auto-install skipped: companion scripts not found" - echo "::warning::Expected ${RESOLVE_SCRIPT} and ${INSTALL_SCRIPT}" - echo "::warning::Pre-commit hooks requiring system tools (e.g. lychee) may fail" -fi +if [[ "${FIX_SKIP_TOOL_INSTALL:-false}" == "true" ]]; then + echo "Tool auto-install deferred to the harness pre_script invocation — skipping here" +else + # Ensures tools required by the target repo's pre-commit hooks are + # available on the runner for the authoritative post-script check. + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + TARGET_REPO="${REPO_DIR:-${GITHUB_WORKSPACE:-}/target-repo}" + RESOLVE_SCRIPT="${SCRIPT_DIR}/resolve-precommit-tools.py" + INSTALL_SCRIPT="${SCRIPT_DIR}/install-precommit-tools.sh" -if [ -f "${TARGET_REPO}/.pre-commit-config.yaml" ] \ - && [ -f "${RESOLVE_SCRIPT}" ] \ - && [ -f "${INSTALL_SCRIPT}" ]; then - echo "Resolving pre-commit tool dependencies..." - MANIFEST="$(mktemp)" - LOCAL_REG="$(mktemp)" - RESOLVE_ARGS=("${TARGET_REPO}") - _BASE_BR="${TARGET_BRANCH:-}" - if [ -z "${_BASE_BR}" ]; then - _BASE_BR="$(git -C "${TARGET_REPO}" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||')" || _BASE_BR="" + # Fallback: when this script is fetched as a single blob from a URL base, + # BASH_SOURCE points to a temp dir with no siblings. The reusable workflow's + # "Prepare workspace" step always materializes the full scripts/ directory + # at ${GITHUB_WORKSPACE}/scripts/ (per-org) or ${GITHUB_WORKSPACE}/.fullsend/scripts/ + # (per-repo). Try those paths when the BASH_SOURCE-relative lookup misses. + if [ ! -f "${RESOLVE_SCRIPT}" ] || [ ! -f "${INSTALL_SCRIPT}" ]; then + for _ws_candidate in "${GITHUB_WORKSPACE:-}/scripts" "${GITHUB_WORKSPACE:-}/.fullsend/scripts"; do + if [ -f "${_ws_candidate}/resolve-precommit-tools.py" ] \ + && [ -f "${_ws_candidate}/install-precommit-tools.sh" ]; then + RESOLVE_SCRIPT="${_ws_candidate}/resolve-precommit-tools.py" + INSTALL_SCRIPT="${_ws_candidate}/install-precommit-tools.sh" + break + fi + done fi - if [ -n "${_BASE_BR}" ] \ - && git -C "${TARGET_REPO}" show "origin/${_BASE_BR}:.pre-commit-tools.yaml" > "${LOCAL_REG}" 2>/dev/null; then - RESOLVE_ARGS+=("--local-registry" "${LOCAL_REG}") + + # Warn instead of silently skipping when the repo needs the auto-install but + # the companions are missing everywhere (issue #3070) — a silent skip here + # surfaces later as a confusing "Executable X not found" pre-commit failure. + if [ -f "${TARGET_REPO}/.pre-commit-config.yaml" ] \ + && { [ ! -f "${RESOLVE_SCRIPT}" ] || [ ! -f "${INSTALL_SCRIPT}" ]; }; then + echo "::warning::Pre-commit tool auto-install skipped: companion scripts not found" + echo "::warning::Expected ${RESOLVE_SCRIPT} and ${INSTALL_SCRIPT}" + echo "::warning::Pre-commit hooks requiring system tools (e.g. lychee) may fail" fi - if python3 "${RESOLVE_SCRIPT}" "${RESOLVE_ARGS[@]}" > "${MANIFEST}"; then - if [ -s "${MANIFEST}" ] && jq -e '.tools | length > 0' "${MANIFEST}" >/dev/null 2>&1; then - bash "${INSTALL_SCRIPT}" "${MANIFEST}" + + if [ -f "${TARGET_REPO}/.pre-commit-config.yaml" ] \ + && [ -f "${RESOLVE_SCRIPT}" ] \ + && [ -f "${INSTALL_SCRIPT}" ]; then + echo "Resolving pre-commit tool dependencies..." + MANIFEST="$(mktemp)" + LOCAL_REG="$(mktemp)" + RESOLVE_ARGS=("${TARGET_REPO}") + _BASE_BR="${TARGET_BRANCH:-}" + if [ -z "${_BASE_BR}" ]; then + _BASE_BR="$(git -C "${TARGET_REPO}" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||')" || _BASE_BR="" + fi + if [ -n "${_BASE_BR}" ] \ + && git -C "${TARGET_REPO}" show "origin/${_BASE_BR}:.pre-commit-tools.yaml" > "${LOCAL_REG}" 2>/dev/null; then + RESOLVE_ARGS+=("--local-registry" "${LOCAL_REG}") + fi + if python3 "${RESOLVE_SCRIPT}" "${RESOLVE_ARGS[@]}" > "${MANIFEST}"; then + if [ -s "${MANIFEST}" ] && jq -e '.tools | length > 0' "${MANIFEST}" >/dev/null 2>&1; then + bash "${INSTALL_SCRIPT}" "${MANIFEST}" + else + echo "No additional pre-commit tools needed" + fi else - echo "No additional pre-commit tools needed" + echo "::warning::Pre-commit tool resolution failed — continuing without auto-install" fi - else - echo "::warning::Pre-commit tool resolution failed — continuing without auto-install" + rm -f "${MANIFEST}" "${LOCAL_REG}" fi - rm -f "${MANIFEST}" "${LOCAL_REG}" fi export PATH="${HOME}/.local/bin:${PATH}" echo "${HOME}/.local/bin" >> "${GITHUB_PATH:-/dev/null}"