From d1efed14994b2d422897abd9c80cd72a7b74f65f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 18:25:09 +0900 Subject: [PATCH 1/2] ci: run hourly PR repair through NVIDIA NIM OpenCode --- .../workflows/nvidia-nim-pr-maintenance.yml | 190 +++++++++ .../nvidia-nim-pr-review-autofix.yml | 403 ++++++++++++++++++ CHANGELOG.md | 15 +- .../nvidia-nim-hourly-pr-maintenance.md | 98 +++++ scripts/ci/pr_review_fix_scheduler_nim.py | 42 ++ tests/test_nvidia_nim_pr_maintenance.py | 168 ++++++++ tests/test_nvidia_nim_pr_maintenance_token.py | 36 ++ 7 files changed, 943 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/nvidia-nim-pr-maintenance.yml create mode 100644 .github/workflows/nvidia-nim-pr-review-autofix.yml create mode 100644 docs/automation/nvidia-nim-hourly-pr-maintenance.md create mode 100644 scripts/ci/pr_review_fix_scheduler_nim.py create mode 100644 tests/test_nvidia_nim_pr_maintenance.py create mode 100644 tests/test_nvidia_nim_pr_maintenance_token.py diff --git a/.github/workflows/nvidia-nim-pr-maintenance.yml b/.github/workflows/nvidia-nim-pr-maintenance.yml new file mode 100644 index 000000000..482e53790 --- /dev/null +++ b/.github/workflows/nvidia-nim-pr-maintenance.yml @@ -0,0 +1,190 @@ +name: NVIDIA NIM PR Maintenance + +on: + workflow_call: + inputs: + target_repository: + description: Repository to scan in owner/name form + required: false + default: "" + type: string + base_branch: + description: Base branch to scan; defaults to the caller repository default branch + required: false + default: "" + type: string + dry_run: + description: Print actions without dispatching an autofix + required: false + default: false + type: boolean + max_prs: + description: Maximum open pull requests to inspect + required: false + default: "50" + type: string + max_dispatches: + description: Maximum NVIDIA NIM autofix runs to dispatch + required: false + default: "1" + type: string + retry_hours: + description: Minimum hours before redispatching the same head + required: false + default: "1" + type: string + secrets: + PR_REVIEW_MERGE_TOKEN: + description: Optional cross-repository GitHub write token + required: false + OPENCODE_APPROVE_TOKEN: + description: Optional existing OpenCode GitHub write token fallback + required: false + repository_dispatch: + types: [nvidia-nim-pr-maintenance] + schedule: + - cron: "23 * * * *" + +concurrency: + group: >- + nvidia-nim-pr-maintenance-${{ + github.event.client_payload.target_repository || inputs.target_repository || + vars.PR_REVIEW_FIX_TARGET_REPOSITORY || github.repository }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + dispatch-review-fixes: + runs-on: ubuntu-24.04 + permissions: + actions: write + contents: read + id-token: write + issues: write + pull-requests: read + statuses: read + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + TARGET_REPOSITORY: >- + ${{ github.event.client_payload.target_repository || inputs.target_repository || + vars.PR_REVIEW_FIX_TARGET_REPOSITORY || github.repository }} + DEFAULT_BRANCH: >- + ${{ github.event.client_payload.base_branch || inputs.base_branch || + vars.PR_REVIEW_FIX_BASE_BRANCH || github.event.repository.default_branch }} + DRY_RUN: >- + ${{ github.event.client_payload.dry_run == true || + github.event.client_payload.dry_run == 'true' || inputs.dry_run == true }} + MAX_PRS: ${{ github.event.client_payload.max_prs || inputs.max_prs || '50' }} + MAX_DISPATCHES: >- + ${{ github.event.client_payload.max_dispatches || inputs.max_dispatches || '1' }} + RETRY_HOURS: ${{ github.event.client_payload.retry_hours || inputs.retry_hours || '1' }} + AUTOFIX_REPOSITORY: ContextualWisdomLab/.github + AUTOFIX_WORKFLOW: nvidia-nim-pr-review-autofix.yml + steps: + - name: Resolve immutable called-workflow source + id: trusted_source + env: + OIDC_AUDIENCE: cwl-nvidia-nim-pr-maintenance-source + run: | + set -euo pipefail + request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}" + separator="&" + case "$request_url" in + *\?*) ;; + *) separator="?" ;; + esac + response="$( + curl -fsS \ + -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ + "${request_url}${separator}audience=${OIDC_AUDIENCE}" + )" + token="$(jq -r '.value // empty' <<<"$response")" + [ -n "$token" ] || { echo "::error::OIDC token response was empty."; exit 1; } + payload_segment="$(cut -d. -f2 <<<"$token")" + case $((${#payload_segment} % 4)) in + 2) payload_segment="${payload_segment}==" ;; + 3) payload_segment="${payload_segment}=" ;; + esac + payload="$(printf '%s' "$payload_segment" | tr '_-' '/+' | base64 -d)" + source_sha="$(jq -r '.job_workflow_sha // .workflow_sha // empty' <<<"$payload")" + source_ref="$(jq -r '.job_workflow_ref // .workflow_ref // empty' <<<"$payload")" + if ! [[ "$source_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "::error::Called workflow source did not resolve to an immutable SHA." + exit 1 + fi + case "$source_ref" in + ContextualWisdomLab/.github/.github/workflows/nvidia-nim-pr-maintenance.yml@*) ;; + *) echo "::error::Unexpected called workflow source: ${source_ref:-missing}."; exit 1 ;; + esac + echo "sha=$source_sha" >>"$GITHUB_OUTPUT" + + - name: Checkout immutable scheduler source + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: ContextualWisdomLab/.github + ref: ${{ steps.trusted_source.outputs.sha }} + fetch-depth: 1 + persist-credentials: false + + - name: Exchange OpenCode app token for scheduler writes + id: scheduler_app_token + env: + OIDC_AUDIENCE: opencode-github-action + OPENCODE_API_BASE_URL: https://api.opencode.ai + run: | + set -euo pipefail + mark_unavailable() { + echo "available=false" >>"$GITHUB_OUTPUT" + } + request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}" + separator="&" + case "$request_url" in + *\?*) ;; + *) separator="?" ;; + esac + oidc_response="$( + curl -fsS \ + -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ + "${request_url}${separator}audience=${OIDC_AUDIENCE}" + )" || { mark_unavailable; exit 0; } + oidc_token="$(jq -r '.value // empty' <<<"$oidc_response")" + [ -n "$oidc_token" ] || { mark_unavailable; exit 0; } + token_response="$( + curl -fsS \ + -X POST \ + -H "Authorization: Bearer ${oidc_token}" \ + "${OPENCODE_API_BASE_URL}/exchange_github_app_token" + )" || { mark_unavailable; exit 0; } + app_token="$(jq -r '.token // empty' <<<"$token_response")" + [ -n "$app_token" ] || { mark_unavailable; exit 0; } + echo "::add-mask::$app_token" + { + echo "available=true" + echo "token=$app_token" + } >>"$GITHUB_OUTPUT" + + - name: Self-test NVIDIA NIM scheduler contract + run: python3 scripts/ci/pr_review_fix_scheduler_nim.py --self-test + + - name: Dispatch bounded NVIDIA NIM review repair + env: + GH_TOKEN: >- + ${{ steps.scheduler_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || + secrets.OPENCODE_APPROVE_TOKEN || github.token }} + run: | + set -euo pipefail + args=( + --repo "$TARGET_REPOSITORY" + --base-branch "$DEFAULT_BRANCH" + --max-prs "$MAX_PRS" + --max-dispatches "$MAX_DISPATCHES" + --retry-hours "$RETRY_HOURS" + --autofix-repository "$AUTOFIX_REPOSITORY" + --autofix-workflow "$AUTOFIX_WORKFLOW" + ) + if [ "$DRY_RUN" = "true" ]; then + args+=(--dry-run) + fi + python3 scripts/ci/pr_review_fix_scheduler_nim.py "${args[@]}" diff --git a/.github/workflows/nvidia-nim-pr-review-autofix.yml b/.github/workflows/nvidia-nim-pr-review-autofix.yml new file mode 100644 index 000000000..b769ad69b --- /dev/null +++ b/.github/workflows/nvidia-nim-pr-review-autofix.yml @@ -0,0 +1,403 @@ +name: NVIDIA NIM PR Review Autofix +run-name: >- + NVIDIA NIM Autofix ${{ github.event.client_payload.target_repository || github.repository }}#${{ + github.event.client_payload.pr_number || 'event' }}@${{ + github.event.client_payload.pr_head_sha || github.sha }} + +on: + repository_dispatch: + types: [nvidia-nim-pr-review-autofix] + +concurrency: + group: >- + nvidia-nim-pr-review-autofix-${{ github.event.client_payload.target_repository }}-${{ + github.event.client_payload.pr_number }} + cancel-in-progress: false + +permissions: + contents: read + id-token: write + +jobs: + autofix: + runs-on: ubuntu-24.04 + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + TARGET_REPOSITORY: ${{ github.event.client_payload.target_repository }} + PR_NUMBER: ${{ github.event.client_payload.pr_number }} + PR_BASE_REF: ${{ github.event.client_payload.pr_base_ref }} + PR_BASE_SHA: ${{ github.event.client_payload.pr_base_sha }} + PR_HEAD_REF: ${{ github.event.client_payload.pr_head_ref }} + PR_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha }} + RESOLVE_CONFLICT: ${{ github.event.client_payload.resolve_conflict || 'false' }} + NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Resolve immutable worker source + id: trusted_source + env: + OIDC_AUDIENCE: cwl-nvidia-nim-pr-autofix-source + run: | + set -euo pipefail + request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}" + separator="&" + case "$request_url" in + *\?*) ;; + *) separator="?" ;; + esac + response="$( + curl -fsS \ + -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ + "${request_url}${separator}audience=${OIDC_AUDIENCE}" + )" + token="$(jq -r '.value // empty' <<<"$response")" + [ -n "$token" ] || { echo "::error::OIDC token response was empty."; exit 1; } + payload_segment="$(cut -d. -f2 <<<"$token")" + case $((${#payload_segment} % 4)) in + 2) payload_segment="${payload_segment}==" ;; + 3) payload_segment="${payload_segment}=" ;; + esac + payload="$(printf '%s' "$payload_segment" | tr '_-' '/+' | base64 -d)" + source_sha="$(jq -r '.workflow_sha // empty' <<<"$payload")" + source_ref="$(jq -r '.workflow_ref // empty' <<<"$payload")" + if ! [[ "$source_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "::error::Worker source did not resolve to an immutable SHA." + exit 1 + fi + case "$source_ref" in + ContextualWisdomLab/.github/.github/workflows/nvidia-nim-pr-review-autofix.yml@*) ;; + *) echo "::error::Unexpected worker source: ${source_ref:-missing}."; exit 1 ;; + esac + echo "sha=$source_sha" >>"$GITHUB_OUTPUT" + + - name: Checkout immutable autofix source + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: ContextualWisdomLab/.github + ref: ${{ steps.trusted_source.outputs.sha }} + fetch-depth: 1 + persist-credentials: false + path: trusted-autofix-source + + - name: Require NVIDIA NIM credential + run: | + set -euo pipefail + if [ -z "${NVIDIA_API_KEY:-}" ]; then + echo "::error::NVIDIA_NIM_API_KEY is required; no alternate model provider is permitted for this scheduler." + exit 1 + fi + echo "::add-mask::$NVIDIA_API_KEY" + + - name: Exchange OpenCode app token for target repository writes + id: target_app_token + env: + OIDC_AUDIENCE: opencode-github-action + OPENCODE_API_BASE_URL: https://api.opencode.ai + run: | + set -euo pipefail + request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}" + separator="&" + case "$request_url" in + *\?*) ;; + *) separator="?" ;; + esac + oidc_response="$( + curl -fsS \ + -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ + "${request_url}${separator}audience=${OIDC_AUDIENCE}" + )" || true + oidc_token="$(jq -r '.value // empty' <<<"${oidc_response:-{}}")" + if [ -z "$oidc_token" ]; then + echo "available=false" >>"$GITHUB_OUTPUT" + exit 0 + fi + token_response="$( + curl -fsS \ + -X POST \ + -H "Authorization: Bearer ${oidc_token}" \ + "${OPENCODE_API_BASE_URL}/exchange_github_app_token" + )" || true + app_token="$(jq -r '.token // empty' <<<"${token_response:-{}}")" + if [ -z "$app_token" ]; then + echo "available=false" >>"$GITHUB_OUTPUT" + exit 0 + fi + echo "::add-mask::$app_token" + { + echo "available=true" + echo "token=$app_token" + } >>"$GITHUB_OUTPUT" + + - name: Fetch and checkout exact pull request head + env: + GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.target_app_token.outputs.token || github.token }} + run: | + set -euo pipefail + [[ "$TARGET_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || { + echo "::error::target_repository must be in owner/name form."; exit 1; + } + [[ "$PR_NUMBER" =~ ^[0-9]+$ ]] || { echo "::error::PR number must be numeric."; exit 1; } + [[ "$PR_BASE_SHA" =~ ^[0-9a-fA-F]{40}$ ]] || { echo "::error::Invalid PR base SHA."; exit 1; } + [[ "$PR_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]] || { echo "::error::Invalid PR head SHA."; exit 1; } + [ "$RESOLVE_CONFLICT" = "true" ] || [ "$RESOLVE_CONFLICT" = "false" ] || { + echo "::error::resolve_conflict must be true or false."; exit 1; + } + + live_pr_json="$(gh api -X GET "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")" + live_state="$(jq -r '.state // empty' <<<"$live_pr_json")" + live_base_ref="$(jq -r '.base.ref // empty' <<<"$live_pr_json")" + live_base_sha="$(jq -r '.base.sha // empty' <<<"$live_pr_json")" + live_head_ref="$(jq -r '.head.ref // empty' <<<"$live_pr_json")" + live_head_sha="$(jq -r '.head.sha // empty' <<<"$live_pr_json")" + live_head_repo="$(jq -r '.head.repo.full_name // empty' <<<"$live_pr_json")" + [ "$live_state" = "open" ] || { echo "::error::Autofix requires an open PR."; exit 1; } + [ "$live_head_repo" = "$TARGET_REPOSITORY" ] || { + echo "::error::Autofix supports only same-repository heads."; exit 1; + } + [ "$live_base_ref" = "$PR_BASE_REF" ] && [ "$live_base_sha" = "$PR_BASE_SHA" ] || { + echo "::error::PR base metadata moved before autofix."; exit 1; + } + [ "$live_head_ref" = "$PR_HEAD_REF" ] && [ "$live_head_sha" = "$PR_HEAD_SHA" ] || { + echo "::error::PR head moved before autofix."; exit 1; + } + + target_workspace="$RUNNER_TEMP/autofix-target" + mkdir -p "$target_workspace" + git init -q "$target_workspace" + gh auth setup-git + git -C "$target_workspace" remote add origin "${GITHUB_SERVER_URL}/${TARGET_REPOSITORY}.git" + git -C "$target_workspace" fetch --no-tags origin \ + "+refs/heads/${PR_BASE_REF}:refs/remotes/origin/${PR_BASE_REF}" \ + "+refs/heads/${PR_HEAD_REF}:refs/remotes/origin/${PR_HEAD_REF}" + [ "$(git -C "$target_workspace" rev-parse "refs/remotes/origin/${PR_HEAD_REF}")" = "$PR_HEAD_SHA" ] || { + echo "::error::Fetched head differs from the validated head."; exit 1; + } + git -C "$target_workspace" switch --detach "$PR_HEAD_SHA" + git -C "$target_workspace" config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git -C "$target_workspace" config user.name "github-actions[bot]" + echo "TARGET_WORKSPACE=$target_workspace" >>"$GITHUB_ENV" + + - name: Install pinned OpenCode CLI + env: + OPENCODE_VERSION: "1.17.13" + OPENCODE_SHA256: 157afa289d1a8d9372de0ce19ac726119b937a1f6b201808d46f06e4e59bb348 + run: | + set -euo pipefail + archive="$RUNNER_TEMP/opencode-linux-x64.tar.gz" + install_dir="$HOME/.opencode/bin" + mkdir -p "$install_dir" + curl -fsSL \ + -o "$archive" \ + "https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/opencode-linux-x64.tar.gz" + printf '%s %s\n' "$OPENCODE_SHA256" "$archive" | sha256sum -c - + tar -xzf "$archive" -C "$RUNNER_TEMP" + install -m 0755 "$RUNNER_TEMP/opencode" "$install_dir/opencode" + echo "$install_dir" >>"$GITHUB_PATH" + + - name: Prepare scoped repair context + env: + GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.target_app_token.outputs.token || github.token }} + run: | + set -euo pipefail + allowed_paths_file="$RUNNER_TEMP/autofix-allowed-paths.txt" + prompt_file="$RUNNER_TEMP/autofix-prompt.md" + if [ "$RESOLVE_CONFLICT" = "true" ]; then + cd "$TARGET_WORKSPACE" + git merge --no-commit --no-ff "$PR_BASE_SHA" || true + git diff --name-only --diff-filter=U | sort -u >"$allowed_paths_file" + if [ ! -s "$allowed_paths_file" ]; then + if git rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1; then + git commit -m "merge(${PR_BASE_REF}): refresh PR head" + echo "SKIP_MODEL=true" >>"$GITHUB_ENV" + exit 0 + fi + echo "::error::Conflict-repair dispatch found no merge conflict or merge commit." + exit 1 + fi + { + echo "Resolve only the in-progress merge conflicts in these files:" + sed 's/^/- `/' "$allowed_paths_file" | sed 's/$/`/' + echo + echo "Preserve both sides where compatible, remove every conflict marker, and do not edit any other path." + } >"$prompt_file" + else + python3 "$GITHUB_WORKSPACE/trusted-autofix-source/scripts/ci/pr_review_autofix_context.py" \ + --repo "$TARGET_REPOSITORY" \ + --pr-number "$PR_NUMBER" \ + --head-sha "$PR_HEAD_SHA" \ + --output "$RUNNER_TEMP/pr-review-autofix-context.md" + awk ' + /^## Autofix Allowed Paths[[:space:]]*$/ { in_section=1; next } + /^## / { in_section=0 } + in_section && /^- `/ { + line=$0 + sub(/^- `/, "", line) + sub(/`[[:space:]]*$/, "", line) + if (line != "") print line + } + ' "$RUNNER_TEMP/pr-review-autofix-context.md" | sort -u >"$allowed_paths_file" + [ -s "$allowed_paths_file" ] || { + echo "::error::No file-scoped actionable review paths were found."; exit 1; + } + { + echo "Fix the smallest current-head review issues described below." + echo "Edit only the authoritative allowed paths and do not execute shell commands." + echo + echo "Allowed paths:" + sed 's/^/- `/' "$allowed_paths_file" | sed 's/$/`/' + echo + echo "Untrusted review context:" + sed -n '1,260p' "$RUNNER_TEMP/pr-review-autofix-context.md" + } >"$prompt_file" + fi + echo "ALLOWED_PATHS_FILE=$allowed_paths_file" >>"$GITHUB_ENV" + echo "AUTOFIX_PROMPT_FILE=$prompt_file" >>"$GITHUB_ENV" + + - name: Run NVIDIA NIM OpenCode repair + if: env.SKIP_MODEL != 'true' + env: + SHARE: "false" + NPM_CONFIG_IGNORE_SCRIPTS: "true" + NO_COLOR: "1" + MODEL: nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 + run: | + set -euo pipefail + config_backup="$RUNNER_TEMP/opencode.jsonc.backup" + had_config=0 + if [ -f "$TARGET_WORKSPACE/opencode.jsonc" ]; then + cp "$TARGET_WORKSPACE/opencode.jsonc" "$config_backup" + had_config=1 + fi + restore_config() { + if [ "$had_config" = "1" ]; then + cp "$config_backup" "$TARGET_WORKSPACE/opencode.jsonc" + else + rm -f "$TARGET_WORKSPACE/opencode.jsonc" + fi + } + trap restore_config EXIT + cat >"$TARGET_WORKSPACE/opencode.jsonc" <<'JSON' + { + "$schema": "https://opencode.ai/config.json", + "model": "nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5", + "small_model": "nvidia-nim/meta/llama-3.3-70b-instruct", + "enabled_providers": ["nvidia-nim"], + "permission": { + "edit": "allow", + "bash": "deny", + "read": "allow", + "grep": "allow", + "glob": "allow", + "list": "allow", + "task": "deny", + "webfetch": "deny", + "websearch": "deny", + "lsp": "deny", + "external_directory": "deny" + }, + "agent": { + "ci-autofix": { + "description": "Conservative NVIDIA NIM PR review repair agent", + "mode": "primary", + "steps": 16, + "permission": { + "edit": "allow", + "bash": "deny", + "read": "allow", + "grep": "allow", + "glob": "allow", + "list": "allow", + "task": "deny", + "webfetch": "deny", + "websearch": "deny", + "lsp": "deny", + "external_directory": "deny" + } + } + }, + "provider": { + "nvidia-nim": { + "npm": "@ai-sdk/openai-compatible", + "name": "NVIDIA NIM", + "options": { + "baseURL": "https://integrate.api.nvidia.com/v1", + "apiKey": "{env:NVIDIA_API_KEY}" + }, + "models": { + "nvidia/llama-3.3-nemotron-super-49b-v1.5": { + "name": "Llama 3.3 Nemotron Super 49B v1.5", + "tool_call": true, + "reasoning": true, + "limit": {"context": 131072, "output": 16384} + }, + "meta/llama-3.3-70b-instruct": { + "name": "Llama 3.3 70B Instruct", + "tool_call": true, + "limit": {"context": 131072, "output": 8192} + } + } + } + } + } + JSON + cd "$TARGET_WORKSPACE" + timeout 18000 opencode run "$(cat "$AUTOFIX_PROMPT_FILE")" \ + --pure \ + --agent ci-autofix \ + --model "$MODEL" \ + --title "PR #${PR_NUMBER} NVIDIA NIM review repair" + restore_config + trap - EXIT + + - name: Validate repair scope and syntax + if: env.SKIP_MODEL != 'true' + run: | + set -euo pipefail + cd "$TARGET_WORKSPACE" + git diff --check + mapfile -t changed_files < <({ git diff --name-only; git ls-files --others --exclude-standard; } | sort -u) + [ "${#changed_files[@]}" -gt 0 ] || { echo "No safe repair change was produced."; exit 0; } + for changed_file in "${changed_files[@]}"; do + grep -Fxq -- "$changed_file" "$ALLOWED_PATHS_FILE" || { + echo "::error::OpenCode modified disallowed path ${changed_file}."; exit 1; + } + done + if [ "$RESOLVE_CONFLICT" = "true" ]; then + [ -z "$(git diff --name-only --diff-filter=U)" ] || { + echo "::error::Merge conflicts remain unresolved."; exit 1; + } + if grep -RInE '^(<<<<<<<|=======|>>>>>>>)' -- $(cat "$ALLOWED_PATHS_FILE"); then + echo "::error::Conflict markers remain in repaired files." + exit 1 + fi + fi + mapfile -t changed_python < <(printf '%s\n' "${changed_files[@]}" | grep -E '\.py$' || true) + if [ "${#changed_python[@]}" -gt 0 ]; then + python3 -m py_compile "${changed_python[@]}" + fi + + - name: Commit and push exact-head repair + env: + GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.target_app_token.outputs.token || github.token }} + run: | + set -euo pipefail + cd "$TARGET_WORKSPACE" + if git diff --quiet && git diff --cached --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then + echo "No repair commit required." + exit 0 + fi + live_head_sha="$(gh api -X GET "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}" --jq '.head.sha')" + [ "$live_head_sha" = "$PR_HEAD_SHA" ] || { + echo "::error::PR head moved during repair; refusing to push."; exit 1; + } + git add -A + if git rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1; then + git commit -m "merge(${PR_BASE_REF}): resolve PR #${PR_NUMBER} conflicts" + else + git commit -m "fix(pr-${PR_NUMBER}): address review feedback with NVIDIA NIM" + fi + git push origin "HEAD:${PR_HEAD_REF}" diff --git a/CHANGELOG.md b/CHANGELOG.md index e601de81b..6ec5ebdc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,13 @@ # Changelog -All notable changes to the organization automation repository are documented in -this file. The format follows Keep a Changelog, and versioned releases follow -Semantic Versioning where the repository publishes a release. +All notable changes to the ContextualWisdomLab organization automation control plane are documented in this file. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versioned releases follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html) when the control plane is tagged. ## [Unreleased] ### Added -- Added exact-base `uv.lock` materialization that reconstructs standalone nested projects with a checksum-pinned official `uv` exporter, isolated frozen/offline execution, strict exact-pin and SHA-256 output validation, and complete Python 3.10/3.14 quality evidence. - -### Fixed - -- Bound both trusted-uv quality jobs to `github.event.pull_request.head.sha` and added a permanent two-checkout regression contract so exact-head compatibility, coverage, docstring, and compilation claims cannot silently measure GitHub's generated pull-request merge revision. -- Made Strix treat only a single LiteLLM provider-error line containing NVIDIA NIM context and model-catalog 404 evidence as cross-model fallback evidence, rejecting cross-line signal assembly and provider-like target source literals; moved the public default to Nemotron 3 Super 120B and added a second NVIDIA hosted candidate before GitHub Models without neutralizing reported vulnerabilities. +- Added an hourly, single-flight pull-request maintenance workflow that reuses the central scheduler and dispatches a file-scoped OpenCode repair worker. +- Added a dedicated NVIDIA NIM repair worker that requires `NVIDIA_NIM_API_KEY`, uses no Copilot credential or GitHub Models inference, and leaves independent review and merge gates unchanged. +- Added immutable reusable-workflow source resolution through GitHub OIDC workflow-SHA claims, static contract tests, and APA 7th doctoring for the scheduler architecture. diff --git a/docs/automation/nvidia-nim-hourly-pr-maintenance.md b/docs/automation/nvidia-nim-hourly-pr-maintenance.md new file mode 100644 index 000000000..7b63c8645 --- /dev/null +++ b/docs/automation/nvidia-nim-hourly-pr-maintenance.md @@ -0,0 +1,98 @@ +# Hourly NVIDIA NIM PR maintenance + +## Decision + +The organization-owned pull-request repair loop runs once per hour and dispatches at most one file-scoped repair for a repository in each execution. The repair worker uses OpenCode with the organization secret `NVIDIA_NIM_API_KEY`; it does not use `COPILOT_GITHUB_TOKEN`, GitHub Models, or the credentials of the existing read-only review agents. + +The review and repair responsibilities remain separate: + +- the existing OpenCode/Noema review path continues to publish independent current-head review evidence; +- the new NVIDIA NIM worker may edit only paths named by current actionable review threads; +- all edits produce a new head that must pass the normal review, coverage, security, and branch-protection gates; +- the repair worker never approves, merges, publishes, or releases its own change. + +This is an organization-central MSA component. Leaf repositories call the reusable workflow by immutable commit SHA and retain only repository-specific cadence and target metadata. + +## Execution contract + +1. The scheduler runs at minute 23 of every hour. +2. It inspects a bounded number of open pull requests and dispatches at most one repair. +3. A head-specific marker prevents repeated work for one hour. +4. Only same-repository heads are writable. +5. Review text and PR text are untrusted; the authoritative edit scope is the validated path list produced by the central context collector. +6. The worker re-reads live base/head metadata before checkout and again before push. +7. Missing `NVIDIA_NIM_API_KEY`, missing file-scoped review evidence, moved heads, out-of-scope edits, unresolved merge markers, or malformed metadata fail closed. +8. OpenCode has file-edit permission but no shell, task, web, LSP, or external-directory permission. +9. The NVIDIA endpoint is configured as an OpenAI-compatible provider at `https://integrate.api.nvidia.com/v1`. +10. GitHub write credentials remain separate from inference credentials. The existing OpenCode App OIDC exchange and review-token fallback are used only for GitHub API and Git transport operations. + +## Immutable source boundary + +GitHub documents that the ordinary `github` context in a reusable workflow describes the caller. The scheduler therefore requests a GitHub OIDC token and extracts `job_workflow_sha` for reusable calls, falling back to `workflow_sha` for direct executions. The corresponding workflow reference must identify the expected central workflow path, and the resulting 40-character SHA is used for checkout. A caller cannot redirect privileged scheduler code to a mutable branch. + +The worker applies the same rule to its own `workflow_sha`. All third-party Actions and the OpenCode binary are pinned by immutable digest or SHA. + +## Model-compute allocation + +The current repair operation is intentionally a single bounded editing agent because the task already has a narrow review finding and an authoritative file allowlist. Multi-agent recursion would add coordination surface without evidence of benefit for this low-entropy operation. + +The broader commercial-development loop should route compute by task structure rather than always using one topology: + +- straightforward review repair: one editing agent plus independent reviewers; +- ambiguous cross-module defect: planner, implementation worker, and verifier; +- research-heavy architectural change: parallel evidence specialists followed by an independent synthesizer; +- high-risk release decision: independent verification paths with no shared intermediate verdict. + +This direction is consistent with Conductor and TRINITY, which treat communication topology, worker selection, role assignment, and recursive test-time delegation as adaptable decisions. Fugu operationalizes those ideas through a swappable model pool. The current workflow fixes agent count and recursion depth at one; future contextual-orchestrator integration must add an ablation proving that any deeper topology improves correctness or risk detection enough to justify its additional compute. + +## Secret and rollback operations + +Required organization secret: + +```text +NVIDIA_NIM_API_KEY +``` + +The workflow binds it only to the process variable expected by OpenCode: + +```text +NVIDIA_API_KEY +``` + +Rollback is deterministic: + +1. disable leaf callers of `nvidia-nim-pr-maintenance.yml`; +2. remove the hourly schedule or set `dry_run: true`; +3. retain the existing review/merge scheduler and independent review workflows; +4. revert the two NVIDIA NIM workflow files and wrapper script; +5. confirm no active `nvidia-nim-pr-review-autofix` run remains. + +No rollback step weakens required checks or review protection. + +## Verification evidence + +The merge gate requires: + +- static workflow-contract tests; +- wrapper unit tests and 100% production coverage/docstrings; +- actionlint and changed-file syntax checks; +- security, CodeQL, Semgrep, secret-scan, dependency, and Scorecard checks; +- an exact-head dry run that dispatches no worker; +- an exact-head controlled fixture proving one allowed edit can be committed while an out-of-scope edit is rejected; +- independent approval by a reviewer other than the last pusher. + +## References + +GitHub. (2026). *OpenID Connect reference*. GitHub Docs. https://docs.github.com/en/actions/reference/security/oidc + +GitHub. (2026). *Reusing workflow configurations*. GitHub Docs. https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations + +Nielsen, S., Cetin, E., Schwendeman, P., Sun, Q., Xu, J., & Tang, Y. (2025). *Learning to orchestrate agents in natural language with the Conductor* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04388 + +NVIDIA. (2026). *API reference for NVIDIA NIM for large language models*. NVIDIA Documentation. https://docs.nvidia.com/nim/large-language-models/latest/api-reference.html + +OpenCode. (2026). *Providers*. https://opencode.ai/docs/providers + +Sakana AI. (2026). *Sakana Fugu: Multi-agent system as a model*. https://sakana.ai/fugu/ + +Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025). *TRINITY: An evolved LLM coordinator* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04695 diff --git a/scripts/ci/pr_review_fix_scheduler_nim.py b/scripts/ci/pr_review_fix_scheduler_nim.py new file mode 100644 index 000000000..4cd075378 --- /dev/null +++ b/scripts/ci/pr_review_fix_scheduler_nim.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Run the central PR repair scheduler with the NVIDIA NIM autofix worker.""" + +from __future__ import annotations + +from pathlib import Path +import sys + +_REPOSITORY_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(_REPOSITORY_ROOT)) + +from scripts.ci import pr_review_fix_scheduler as scheduler # noqa: E402 + +NIM_AUTOFIX_WORKFLOW = "nvidia-nim-pr-review-autofix.yml" +NIM_AUTOFIX_EVENT_TYPE = "nvidia-nim-pr-review-autofix" + + +def _normalized_argv(argv: list[str]) -> list[str]: + """Return scheduler arguments pinned to the NVIDIA NIM worker contract.""" + + normalized = list(argv) + if "--autofix-workflow" not in normalized: + normalized.extend(["--autofix-workflow", NIM_AUTOFIX_WORKFLOW]) + return normalized + + +def main(argv: list[str]) -> int: + """Run the shared scheduler with temporary NVIDIA NIM dispatch constants.""" + + original_workflow = scheduler.DEFAULT_AUTOFIX_WORKFLOW + original_event_type = scheduler.AUTOFIX_REPOSITORY_DISPATCH_TYPE + try: + scheduler.DEFAULT_AUTOFIX_WORKFLOW = NIM_AUTOFIX_WORKFLOW + scheduler.AUTOFIX_REPOSITORY_DISPATCH_TYPE = NIM_AUTOFIX_EVENT_TYPE + return scheduler.main(_normalized_argv(argv)) + finally: + scheduler.DEFAULT_AUTOFIX_WORKFLOW = original_workflow + scheduler.AUTOFIX_REPOSITORY_DISPATCH_TYPE = original_event_type + + +if __name__ == "__main__": # pragma: no cover - exercised through the workflow + raise SystemExit(main(sys.argv[1:])) diff --git a/tests/test_nvidia_nim_pr_maintenance.py b/tests/test_nvidia_nim_pr_maintenance.py new file mode 100644 index 000000000..fd326410f --- /dev/null +++ b/tests/test_nvidia_nim_pr_maintenance.py @@ -0,0 +1,168 @@ +"""Contract tests for hourly NVIDIA NIM review repair automation.""" + +from __future__ import annotations + +from pathlib import Path + +from scripts.ci import pr_review_fix_scheduler_nim as nim_scheduler + + +SCHEDULER_WORKFLOW = Path(".github/workflows/nvidia-nim-pr-maintenance.yml") +AUTOFIX_WORKFLOW = Path(".github/workflows/nvidia-nim-pr-review-autofix.yml") + + +def test_wrapper_adds_nim_worker_when_caller_does_not_override() -> None: + """The wrapper must route the shared scheduler to the NIM worker by default.""" + + assert nim_scheduler._normalized_argv(["--self-test"]) == [ + "--self-test", + "--autofix-workflow", + nim_scheduler.NIM_AUTOFIX_WORKFLOW, + ] + + +def test_wrapper_preserves_explicit_worker_argument() -> None: + """An explicit worker argument must not be duplicated during normalization.""" + + argv = ["--autofix-workflow", nim_scheduler.NIM_AUTOFIX_WORKFLOW, "--self-test"] + assert nim_scheduler._normalized_argv(argv) == argv + + +def test_wrapper_applies_nim_dispatch_contract_and_restores_globals(monkeypatch) -> None: + """The wrapper must apply NIM routing only for the duration of one invocation.""" + + captured: dict[str, object] = {} + original_workflow = nim_scheduler.scheduler.DEFAULT_AUTOFIX_WORKFLOW + original_event = nim_scheduler.scheduler.AUTOFIX_REPOSITORY_DISPATCH_TYPE + + def fake_main(argv: list[str]) -> int: + captured["argv"] = argv + captured["workflow"] = nim_scheduler.scheduler.DEFAULT_AUTOFIX_WORKFLOW + captured["event"] = nim_scheduler.scheduler.AUTOFIX_REPOSITORY_DISPATCH_TYPE + return 17 + + monkeypatch.setattr(nim_scheduler.scheduler, "main", fake_main) + + assert nim_scheduler.main(["--self-test"]) == 17 + assert captured == { + "argv": [ + "--self-test", + "--autofix-workflow", + "nvidia-nim-pr-review-autofix.yml", + ], + "workflow": "nvidia-nim-pr-review-autofix.yml", + "event": "nvidia-nim-pr-review-autofix", + } + assert nim_scheduler.scheduler.DEFAULT_AUTOFIX_WORKFLOW == original_workflow + assert nim_scheduler.scheduler.AUTOFIX_REPOSITORY_DISPATCH_TYPE == original_event + + +def test_scheduler_runs_hourly_and_dispatches_one_bounded_repair() -> None: + """The scheduler cadence and single-flight bounds must remain explicit.""" + + workflow = SCHEDULER_WORKFLOW.read_text(encoding="utf-8") + + assert 'cron: "23 * * * *"' in workflow + assert "max_dispatches:" in workflow + assert "retry_hours:" in workflow + assert workflow.count('default: "1"') >= 2 + assert "MAX_DISPATCHES" in workflow + assert "cancel-in-progress: true" in workflow + assert "nvidia-nim-pr-review-autofix.yml" in workflow + assert "pr_review_fix_scheduler_nim.py --self-test" in workflow + assert "--retry-hours \"$RETRY_HOURS\"" in workflow + + +def test_scheduler_declares_only_named_reusable_write_secrets() -> None: + """Leaf callers need explicit GitHub write-token inputs, never blanket inheritance.""" + + workflow = SCHEDULER_WORKFLOW.read_text(encoding="utf-8") + + assert "PR_REVIEW_MERGE_TOKEN:" in workflow + assert "OPENCODE_APPROVE_TOKEN:" in workflow + assert workflow.count("required: false") >= 8 + assert "secrets: inherit" not in workflow + assert "COPILOT_GITHUB_TOKEN" not in workflow + assert "NVIDIA_NIM_API_KEY" not in workflow + + +def test_scheduler_materializes_immutable_called_workflow_source() -> None: + """Reusable callers must not redirect privileged scheduler code to a mutable ref.""" + + workflow = SCHEDULER_WORKFLOW.read_text(encoding="utf-8") + + assert "id-token: write" in workflow + assert ".job_workflow_sha // .workflow_sha // empty" in workflow + assert "Called workflow source did not resolve to an immutable SHA" in workflow + assert ( + "ContextualWisdomLab/.github/.github/workflows/" + "nvidia-nim-pr-maintenance.yml@*" + ) in workflow + assert "ref: ${{ steps.trusted_source.outputs.sha }}" in workflow + assert "ref: main" not in workflow + assert ( + "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" + in workflow + ) + + +def test_autofix_uses_only_nvidia_nim_model_credentials() -> None: + """Commercial repair inference must use NVIDIA NIM and never Copilot credentials.""" + + workflow = AUTOFIX_WORKFLOW.read_text(encoding="utf-8") + + assert "NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}" in workflow + assert "NVIDIA_NIM_API_KEY is required" in workflow + assert '"enabled_providers": ["nvidia-nim"]' in workflow + assert '"baseURL": "https://integrate.api.nvidia.com/v1"' in workflow + assert '"apiKey": "{env:NVIDIA_API_KEY}"' in workflow + assert '"npm": "@ai-sdk/openai-compatible"' in workflow + assert "nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5" in workflow + assert "COPILOT_GITHUB_TOKEN" not in workflow + assert "STRIX_GITHUB_MODELS_TOKEN" not in workflow + assert "models.github.ai" not in workflow + assert "github-models/" not in workflow + + +def test_autofix_remains_fail_closed_and_file_scoped() -> None: + """The repair worker must reject missing credentials, moved heads, and broad edits.""" + + workflow = AUTOFIX_WORKFLOW.read_text(encoding="utf-8") + + required_contracts = ( + "No file-scoped actionable review paths were found", + "OpenCode modified disallowed path", + "PR head moved during repair; refusing to push", + '"bash": "deny"', + '"edit": "allow"', + "git diff --check", + "python3 -m py_compile", + "Merge conflicts remain unresolved", + "Conflict markers remain in repaired files", + "same-repository heads", + ) + for contract in required_contracts: + assert contract in workflow + + +def test_autofix_source_and_toolchain_are_immutable() -> None: + """The worker source, actions, and OpenCode binary must be integrity bounded.""" + + workflow = AUTOFIX_WORKFLOW.read_text(encoding="utf-8") + + assert ".workflow_sha // empty" in workflow + assert "ref: ${{ steps.trusted_source.outputs.sha }}" in workflow + assert ( + "step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920" + in workflow + ) + assert ( + "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" + in workflow + ) + assert 'OPENCODE_VERSION: "1.17.13"' in workflow + assert ( + "OPENCODE_SHA256: " + "157afa289d1a8d9372de0ce19ac726119b937a1f6b201808d46f06e4e59bb348" + in workflow + ) diff --git a/tests/test_nvidia_nim_pr_maintenance_token.py b/tests/test_nvidia_nim_pr_maintenance_token.py new file mode 100644 index 000000000..3d43c15c3 --- /dev/null +++ b/tests/test_nvidia_nim_pr_maintenance_token.py @@ -0,0 +1,36 @@ +"""Cross-repository credential contracts for NVIDIA NIM PR maintenance.""" + +from __future__ import annotations + +from pathlib import Path + + +SCHEDULER_WORKFLOW = Path(".github/workflows/nvidia-nim-pr-maintenance.yml") + + +def test_scheduler_exchanges_app_token_before_cross_repository_dispatch() -> None: + """The scheduler should not require a leaf repository's token to write centrally.""" + + workflow = SCHEDULER_WORKFLOW.read_text(encoding="utf-8") + + assert "Exchange OpenCode app token for scheduler writes" in workflow + assert "OIDC_AUDIENCE: opencode-github-action" in workflow + assert "https://api.opencode.ai/exchange_github_app_token" not in workflow + assert '"${OPENCODE_API_BASE_URL}/exchange_github_app_token"' in workflow + assert "steps.scheduler_app_token.outputs.token" in workflow + assert ( + "steps.scheduler_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN" + in workflow + ) + assert "secrets.OPENCODE_APPROVE_TOKEN || github.token" in workflow + + +def test_scheduler_keeps_inference_and_github_credentials_separate() -> None: + """The scheduler may obtain GitHub transport credentials but no model API key.""" + + workflow = SCHEDULER_WORKFLOW.read_text(encoding="utf-8") + + assert "NVIDIA_NIM_API_KEY" not in workflow + assert "NVIDIA_API_KEY" not in workflow + assert "COPILOT_GITHUB_TOKEN" not in workflow + assert "STRIX_GITHUB_MODELS_TOKEN" not in workflow From 7fbd38f35050410ea3d74f3236cc1bcdb8211749 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 18:27:07 +0900 Subject: [PATCH 2/2] docs(changelog): preserve trusted-uv prerequisite history --- CHANGELOG.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ec5ebdc3..5e60a480c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,19 @@ # Changelog -All notable changes to the ContextualWisdomLab organization automation control plane are documented in this file. - -The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versioned releases follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html) when the control plane is tagged. +All notable changes to the organization automation repository are documented in +this file. The format follows Keep a Changelog, and versioned releases follow +Semantic Versioning where the repository publishes a release. ## [Unreleased] ### Added +- Added exact-base `uv.lock` materialization that reconstructs standalone nested projects with a checksum-pinned official `uv` exporter, isolated frozen/offline execution, strict exact-pin and SHA-256 output validation, and complete Python 3.10/3.14 quality evidence. - Added an hourly, single-flight pull-request maintenance workflow that reuses the central scheduler and dispatches a file-scoped OpenCode repair worker. - Added a dedicated NVIDIA NIM repair worker that requires `NVIDIA_NIM_API_KEY`, uses no Copilot credential or GitHub Models inference, and leaves independent review and merge gates unchanged. - Added immutable reusable-workflow source resolution through GitHub OIDC workflow-SHA claims, static contract tests, and APA 7th doctoring for the scheduler architecture. + +### Fixed + +- Bound both trusted-uv quality jobs to `github.event.pull_request.head.sha` and added a permanent two-checkout regression contract so exact-head compatibility, coverage, docstring, and compilation claims cannot silently measure GitHub's generated pull-request merge revision. +- Made Strix treat only a single LiteLLM provider-error line containing NVIDIA NIM context and model-catalog 404 evidence as cross-model fallback evidence, rejecting cross-line signal assembly and provider-like target source literals; moved the public default to Nemotron 3 Super 120B and added a second NVIDIA hosted candidate before GitHub Models without neutralizing reported vulnerabilities.