Required Noema Review ContextualWisdomLab/fast-mlsirm#1780@70879c408f80e90a3c6e723922bbb4be682be0ea #6829
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Required Noema Review | |
| run-name: >- | |
| Required Noema Review ${{ github.event.client_payload.target_repository || | |
| github.event.pull_request.base.repo.full_name || github.repository }}#${{ | |
| github.event.client_payload.pr_number || github.event.pull_request.number || | |
| 'event' }}@${{ | |
| github.event.client_payload.pr_head_sha || github.event.pull_request.head.sha || | |
| github.sha }} | |
| on: | |
| pull_request_target: | |
| types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] | |
| # Default-branch-only retry entrypoint; no caller-selected workflow ref. | |
| repository_dispatch: | |
| types: [noema-review] | |
| concurrency: | |
| # Workflow-level admission is required: a queued run cannot reach a job-level | |
| # cancellation guard while the organization is at its Actions job ceiling. | |
| group: >- | |
| required-noema-review-${{ | |
| github.event.pull_request.base.repo.full_name || | |
| github.event.client_payload.target_repository || github.repository }}-${{ | |
| github.event.pull_request.number || | |
| github.event.client_payload.pr_number || github.run_id }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| checks: read | |
| id-token: write | |
| jobs: | |
| admit-current-head: | |
| if: >- | |
| github.event_name == 'repository_dispatch' | |
| || ( | |
| github.event_name == 'pull_request_target' | |
| && github.event.action != 'closed' | |
| && github.event.action != 'converted_to_draft' | |
| && github.event.pull_request.head.repo.full_name == github.repository | |
| ) | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| outputs: | |
| admitted: ${{ steps.live_head.outputs.admitted }} | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| env: | |
| GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || github.token }} | |
| TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.event.client_payload.target_repository || github.repository }} | |
| PR_NUMBER: ${{ github.event.pull_request.number || github.event.client_payload.pr_number || '' }} | |
| EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.client_payload.pr_head_sha || '' }} | |
| steps: | |
| - name: Admit only the exact live Noema head | |
| id: live_head | |
| run: | | |
| set -euo pipefail | |
| echo "admitted=false" >>"$GITHUB_OUTPUT" | |
| if ! [[ "$TARGET_REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]] || | |
| ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || | |
| ! [[ "$EXPECTED_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then | |
| echo "::error::Noema admission rejected malformed pull request metadata." | |
| exit 1 | |
| fi | |
| live_pr="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")" | |
| live_head="$(jq -r '.head.sha // empty' <<<"$live_pr")" | |
| live_state="$(jq -r '.state // empty' <<<"$live_pr")" | |
| if [ "${live_head,,}" != "${EXPECTED_HEAD_SHA,,}" ] || [ "$live_state" != "open" ]; then | |
| echo "::notice::Noema admission retired a stale trigger before review queue entry." | |
| exit 0 | |
| fi | |
| echo "admitted=true" >>"$GITHUB_OUTPUT" | |
| echo "Exact live Noema head admitted for ${TARGET_REPOSITORY}#${PR_NUMBER}." | |
| cancel-closed-pr-runs: | |
| if: >- | |
| github.event_name == 'pull_request_target' && | |
| (github.event.action == 'closed' || github.event.action == 'converted_to_draft') | |
| runs-on: ubuntu-24.04 | |
| # Bound this job well short of GitHub's 360-minute platform default. Its | |
| # only step is a single-repository, status-filtered gh api --paginate | |
| # list-and-cancel sweep (up to 3 passes x 5 statuses), no branch update | |
| # or merge -- lighter than pr-review-merge-scheduler.yml's scan-pr-queue | |
| # job (PR #1702), which got timeout-minutes: 30 for a comparable | |
| # single-repo scan that also dispatches a review and updates a branch. | |
| timeout-minutes: 20 | |
| permissions: | |
| actions: write | |
| contents: read | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }} | |
| INACTIVE_PR_NUMBER: ${{ github.event.pull_request.number }} | |
| INACTIVE_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| PR_ACTION: ${{ github.event.action }} | |
| CURRENT_RUN_ID: ${{ github.run_id }} | |
| steps: | |
| - name: Cancel queued and running Noema reviews for the inactive pull request | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| live_target_matches() { | |
| local live_pr_json live_state live_draft live_head | |
| if ! live_pr_json="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${INACTIVE_PR_NUMBER}" 2>/tmp/noema-inactive-gh-error)"; then | |
| echo "::warning::Noema inactive-PR cleanup could not verify the live pull request; leaving runs unchanged." >&2 | |
| return 1 | |
| fi | |
| live_state="$(jq -r '.state // ""' <<<"$live_pr_json")" | |
| live_draft="$(jq -r '.draft // false' <<<"$live_pr_json")" | |
| live_head="$(jq -r '.head.sha // ""' <<<"$live_pr_json")" | |
| [ "$live_head" = "$INACTIVE_PR_HEAD_SHA" ] && { | |
| { [ "$PR_ACTION" = "closed" ] && [ "$live_state" = "closed" ]; } || | |
| { [ "$PR_ACTION" = "converted_to_draft" ] && [ "$live_state" = "open" ] && [ "$live_draft" = "true" ]; } | |
| } | |
| } | |
| # cancel_runs prints the number of runs it matched for $1's status | |
| # on stdout (its only stdout output) so the multi-pass loop below | |
| # can tell whether a pass found anything; all human-facing log | |
| # lines go to stderr so they don't pollute that count. | |
| # | |
| # The runs list is scoped to this repository, not to a specific | |
| # workflow file: noema-review.yml runs against sibling | |
| # repositories only through the organization's required-workflow | |
| # ruleset (README.md's "또 같이" / "siblings call it" section) and | |
| # is never itself committed to those repositories, so | |
| # actions/workflows/noema-review.yml/runs is not guaranteed to | |
| # resolve there -- GitHub's List repository workflows family | |
| # enumerates workflow files that exist in that repository's own | |
| # tree. actions/runs plus the run object's own `.path` field is | |
| # this repo's own already-proven pattern for this exact cross-repo | |
| # cleanup (see strix.yml's identical job). | |
| # Status stays a server-side filter -- bounding each query to only | |
| # the currently active runs -- rather than an unfiltered | |
| # per-workflow fetch filtered client-side, since noema-review.yml | |
| # is this org's central, highest-volume review workflow and an | |
| # unbounded history walk on every PR close is a real rate-limit | |
| # and latency risk here. | |
| cancel_runs() { | |
| local status="$1" | |
| if ! live_target_matches; then | |
| echo "::notice::Noema inactive-PR cleanup target changed; leaving runs unchanged." >&2 | |
| echo 0 | |
| return 0 | |
| fi | |
| local runs_url="repos/${TARGET_REPOSITORY}/actions/runs?status=${status}&per_page=100" | |
| local runs_json | |
| if ! runs_json="$(gh api --paginate "$runs_url" 2>/tmp/noema-close-gh-error)"; then | |
| echo "::warning::Noema close cleanup could not inspect ${TARGET_REPOSITORY}; leaving runs unchanged." >&2 | |
| sed 's/^/ /' /tmp/noema-close-gh-error >&2 || true | |
| echo 0 | |
| return 0 | |
| fi | |
| local run_ids | |
| # PR-scoped by two independent, OR'd signals -- neither alone | |
| # covers every trigger this job serves. The rendered | |
| # display_title (this workflow's own run-name, embedding the | |
| # target repository/PR number/head SHA) is this workflow's | |
| # original signal, and stays reliable for repository_dispatch | |
| # and workflow_run triggers. But GitHub does not consistently | |
| # render run-name for an organization-required-workflow | |
| # pull_request_target run materialized in a sibling repository | |
| # (Devin Review, PR #1507: "Sibling Noema runs evade | |
| # cancellation") -- `name` and `display_title` can both collapse | |
| # to the bare workflow name and the plain PR title there, | |
| # matching neither the old `.name ==` filter nor the | |
| # display_title prefix below. GitHub's own `pull_requests[]` | |
| # array on the run object closes that gap: it is populated for | |
| # this workflow's pull_request_target runs because the | |
| # noema-review job itself only ever processes same-repository, | |
| # non-fork pull requests (its own `if:` requires | |
| # `head.repo.full_name == github.repository`), so the cross-fork | |
| # "empty pull_requests[]" caveat that rules this field out | |
| # elsewhere in this org's tooling does not apply here. Neither | |
| # signal alone is sufficient for every trigger type, so this | |
| # matches on either one -- never a bare head_sha, which two | |
| # different open PRs can share (e.g. a duplicate PR opened from | |
| # the same branch against another target) and which would let | |
| # closing one cancel the other's still-needed run. Matching by | |
| # PR number rather than by the closed PR's current head SHA also | |
| # means historical-head runs from earlier pushes to this same PR | |
| # are still caught. `.path` pins the workflow identity in place | |
| # of the old `.name ==` filter: unlike `.name` (which, like | |
| # display_title, only carries the bare workflow name for a | |
| # required-workflow-ruleset run), `.path` was independently | |
| # confirmed stable across both native and sibling contexts. | |
| if ! run_ids="$(jq -r --arg pr "$INACTIVE_PR_NUMBER" \ | |
| --arg current "$CURRENT_RUN_ID" --arg target "$TARGET_REPOSITORY" ' | |
| .workflow_runs[] | |
| | select((.id | tostring) != $current) | |
| | select(.path == ".github/workflows/noema-review.yml") | |
| | select((.name // "") | startswith("Required Noema Review")) | |
| | select( | |
| ((.display_title // "") | startswith("Required Noema Review " + $target + "#" + $pr + "@")) | |
| or ((.pull_requests // []) | any(.number == ($pr | tonumber))) | |
| ) | |
| | .id | |
| ' <<<"$runs_json")"; then | |
| echo "::warning::Noema close cleanup received invalid run data for ${TARGET_REPOSITORY}; leaving runs unchanged." >&2 | |
| echo 0 | |
| return 0 | |
| fi | |
| local matched=0 | |
| while IFS= read -r run_id; do | |
| [ -n "$run_id" ] || continue | |
| if ! live_target_matches; then | |
| echo "::notice::Noema inactive-PR cleanup target changed before cancellation; leaving runs unchanged." >&2 | |
| break | |
| fi | |
| matched=$((matched + 1)) | |
| if gh api --method POST "repos/${TARGET_REPOSITORY}/actions/runs/${run_id}/cancel" >/dev/null 2>/tmp/noema-close-cancel-error; then | |
| echo "Cancelled Noema run ${run_id} in ${TARGET_REPOSITORY} for inactive PR #${INACTIVE_PR_NUMBER}." >&2 | |
| else | |
| echo "::warning::Noema close cleanup could not cancel run ${run_id}; it may have finished or the token lacks Actions write access." >&2 | |
| sed 's/^/ /' /tmp/noema-close-cancel-error >&2 || true | |
| fi | |
| done <<<"$run_ids" | |
| echo "$matched" | |
| } | |
| # A run can transition between the five active statuses between | |
| # one status's fetch and the next (e.g. it is "requested" when the | |
| # already-fetched "queued" list was read, then becomes "queued" | |
| # moments later, after this pass has already moved past checking | |
| # "queued") -- a real GitHub Actions run lifecycle race, not a | |
| # hypothetical. A single sequential sweep can let such a run | |
| # escape cancellation entirely. Re-scan every active status for up | |
| # to three passes: always run at least two full passes (a run that | |
| # slips through every status query in pass 1 has, by definition, | |
| # settled into a checkable status by the time pass 2 queries it | |
| # again), and only skip the third when both prior passes matched | |
| # nothing, bounding the retries so API flakiness cannot loop this | |
| # forever. | |
| max_passes=3 | |
| pass=1 | |
| found_any=0 | |
| while [ "$pass" -le "$max_passes" ]; do | |
| pass_matches=0 | |
| for active_status in queued in_progress requested waiting pending; do | |
| matched="$(cancel_runs "$active_status")" | |
| pass_matches=$((pass_matches + matched)) | |
| done | |
| echo "Noema close cleanup pass ${pass}/${max_passes} matched ${pass_matches} run(s) across active statuses." >&2 | |
| if [ "$pass_matches" -gt 0 ]; then | |
| found_any=1 | |
| fi | |
| if [ "$pass" -ge 2 ] && [ "$pass_matches" -eq 0 ] && [ "$found_any" -eq 0 ]; then | |
| break | |
| fi | |
| pass=$((pass + 1)) | |
| done | |
| noema-review: | |
| name: noema-review | |
| needs: [admit-current-head] | |
| runs-on: ubuntu-24.04 | |
| # No job-level timeout-minutes here, deliberately. This job's "Prepare | |
| # Noema model verdict" step calls two_phase.py's call_llm synchronously | |
| # via the contextual-orchestrator gateway and blocks on the model's own | |
| # response -- a job-level wall-clock bound here would cap the model's | |
| # reasoning/tool-use time directly, which docs/product-goal-directive.md | |
| # #8 prohibits ("Model timeout은 application·Agent·Gateway 공통 상한 없이 | |
| # 기본 null이다"; "OpenCode·Strix·Noema의 모델당 2시간 이상을 수용한다"). An | |
| # earlier version of this job set timeout-minutes: 210, reasoning it gave | |
| # that step "the same ~180-minute allowance" PR #1707 gave an unrelated | |
| # step -- that reasoning was wrong: #1707's poll_deadline_epoch bounds a | |
| # step that polls GitHub for whether a *separately triggered* review | |
| # process has posted a verdict yet (an async external wait), not a step | |
| # that itself runs the model synchronously. Any fixed cap on a job whose | |
| # body IS the synchronous model call is exactly the fixed inference-time | |
| # cap the policy forbids. See | |
| # docs/doctoring/autofix-and-noema-review-model-job-timeout-removal.md. | |
| if: >- | |
| needs.admit-current-head.outputs.admitted == 'true' | |
| && ( | |
| github.event_name == 'repository_dispatch' | |
| || ( | |
| github.event_name == 'pull_request_target' | |
| && github.event.action != 'closed' | |
| && github.event.action != 'converted_to_draft' | |
| && github.event.pull_request.head.repo.full_name == github.repository | |
| ) | |
| ) | |
| permissions: | |
| actions: write | |
| checks: read | |
| contents: read | |
| id-token: write | |
| pull-requests: read | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.event.client_payload.target_repository || github.repository }} | |
| PR_NUMBER: ${{ github.event.pull_request.number || github.event.client_payload.pr_number || '' }} | |
| EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.client_payload.pr_head_sha || '' }} | |
| steps: | |
| - name: Skip events without pull request context | |
| if: env.PR_NUMBER == '' | |
| run: | | |
| echo "::notice::Noema review skipped: no pull request number is associated with this event." | |
| - name: Resolve trusted Noema review source ref | |
| if: env.PR_NUMBER != '' | |
| id: trusted_source | |
| env: | |
| JOB_CONTEXT_JSON: ${{ toJSON(job) }} | |
| GITHUB_CONTEXT_JSON: ${{ toJSON(github) }} | |
| run: | | |
| set -euo pipefail | |
| python3 <<'PY' >>"$GITHUB_OUTPUT" | |
| import json | |
| import os | |
| import re | |
| import sys | |
| try: | |
| job_context = json.loads(os.environ.get("JOB_CONTEXT_JSON") or "{}") | |
| github_context = json.loads(os.environ.get("GITHUB_CONTEXT_JSON") or "{}") | |
| except json.JSONDecodeError as exc: | |
| print(f"::error::Could not parse GitHub workflow context JSON: {exc}", file=sys.stderr) | |
| raise SystemExit(1) | |
| trusted_repository = str( | |
| job_context.get("workflow_repository") or "ContextualWisdomLab/.github" | |
| ).strip() | |
| trusted_ref = str( | |
| job_context.get("workflow_sha") or github_context.get("workflow_sha") or "" | |
| ).strip() | |
| workflow_ref = str( | |
| job_context.get("workflow_ref") or github_context.get("workflow_ref") or "" | |
| ).strip() | |
| if not trusted_ref: | |
| trusted_ref = "main" | |
| prefix = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@" | |
| if workflow_ref.startswith(prefix): | |
| trusted_ref = workflow_ref.split("@", 1)[1] | |
| if trusted_repository != "ContextualWisdomLab/.github": | |
| print("::error::Trusted Noema workflow repository resolved outside ContextualWisdomLab/.github.", file=sys.stderr) | |
| raise SystemExit(1) | |
| if not re.fullmatch(r"[0-9a-fA-F]{40}|refs/[^\s]+|[A-Za-z0-9._/-]+", trusted_ref): | |
| print("::error::Trusted Noema workflow ref resolved to an invalid value.", file=sys.stderr) | |
| raise SystemExit(1) | |
| print(f"repository={trusted_repository}") | |
| print(f"ref={trusted_ref}") | |
| PY | |
| - name: Materialize trusted Noema review gate | |
| if: env.PR_NUMBER != '' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TRUSTED_SOURCE_REF: ${{ steps.trusted_source.outputs.ref }} | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "$TRUSTED_SOURCE_REF" =~ ^[0-9a-fA-F]{40}$ ]]; then | |
| echo "::error::Trusted Noema source ref must resolve to the immutable workflow commit SHA before archive materialization." | |
| exit 1 | |
| fi | |
| trusted_archive="${RUNNER_TEMP}/trusted-noema-source.tar.gz" | |
| api_url="${GITHUB_API_URL:-https://api.github.com}" | |
| curl -fsSL \ | |
| -H "Authorization: Bearer ${GH_TOKEN}" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -o "$trusted_archive" \ | |
| "${api_url}/repos/ContextualWisdomLab/.github/tarball/${TRUSTED_SOURCE_REF}" | |
| tar -xzf "$trusted_archive" -C "$GITHUB_WORKSPACE" --strip-components=1 | |
| test -f scripts/ci/noema_review_gate.py | |
| - name: Reject a stale trigger before credential or model setup | |
| if: env.PR_NUMBER != '' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "$EXPECTED_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then | |
| echo "::error::Noema trigger did not provide a canonical lowercase exact head SHA." | |
| exit 1 | |
| fi | |
| live_head="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}" --jq '.head.sha')" | |
| if [ "${live_head,,}" != "${EXPECTED_HEAD_SHA,,}" ]; then | |
| echo "::error::Noema trigger is stale; expected ${EXPECTED_HEAD_SHA}, observed ${live_head}." | |
| exit 1 | |
| fi | |
| - name: Cancel superseded Noema runs after live-head validation | |
| if: github.event_name == 'pull_request_target' && env.PR_NUMBER != '' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| CURRENT_RUN_ID: ${{ github.run_id }} | |
| run: | | |
| set -euo pipefail | |
| declare -A seen=() | |
| cancelled=0 | |
| for pass in 1 2; do | |
| for active_status in queued in_progress requested waiting pending; do | |
| if ! runs_json="$(gh api --paginate "repos/${TARGET_REPOSITORY}/actions/runs?status=${active_status}&per_page=100")"; then | |
| echo "::warning::Could not inspect ${active_status} Noema runs for superseded heads." | |
| continue | |
| fi | |
| # See the close-cleanup job's matching comment above cancel_runs's | |
| # own selector for the full rationale: display_title only | |
| # renders this workflow's PR/head-bearing run-name reliably for | |
| # a native trigger, so a sibling-repository required-workflow | |
| # run is additionally matched via GitHub's own pull_requests[] | |
| # array (populated here because noema-review only ever | |
| # processes same-repository, non-fork pull requests), and | |
| # `.path` pins workflow identity where `.name` cannot. The | |
| # live-head exclusion below is independently reinforced with a | |
| # direct `.head_sha` comparison -- the run object's own | |
| # head_sha field, unlike display_title, is populated and | |
| # accurate regardless of run-name rendering, so it protects the | |
| # current run even when its display_title never rendered a | |
| # matching "@$head" suffix to exclude by. | |
| if ! run_ids="$(jq -r --arg pr "$PR_NUMBER" --argjson current "$CURRENT_RUN_ID" \ | |
| --arg target "$TARGET_REPOSITORY" --arg head "$EXPECTED_HEAD_SHA" ' | |
| .workflow_runs[] | |
| | select(.id < $current) | |
| | select(.path == ".github/workflows/noema-review.yml") | |
| | select((.name // "") | startswith("Required Noema Review")) | |
| | select( | |
| ((.display_title // "") | startswith("Required Noema Review " + $target + "#" + $pr + "@")) | |
| or ((.pull_requests // []) | any(.number == ($pr | tonumber))) | |
| ) | |
| | select(((.display_title // "") | endswith("@" + $head)) | not) | |
| | select(((.head_sha // "") | ascii_downcase) != ($head | ascii_downcase)) | |
| | .id | |
| ' <<<"$runs_json")"; then | |
| echo "::warning::Could not parse ${active_status} Noema runs for superseded heads." | |
| continue | |
| fi | |
| while IFS= read -r run_id; do | |
| [ -n "$run_id" ] || continue | |
| [ -z "${seen[$run_id]:-}" ] || continue | |
| seen[$run_id]=1 | |
| # A transient failure here (rate limit, network blip) must | |
| # never crash this step under set -e: this is a housekeeping | |
| # cleanup, and letting an ancillary API hiccup fail the whole | |
| # job would block a perfectly valid, live-head review over | |
| # something unrelated to it. Treat "cannot verify" the same | |
| # as "verified stale": stop cancelling rather than risk a | |
| # wrong cancellation, but let the job continue. | |
| if ! live_head="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}" --jq '.head.sha' 2>/tmp/noema-supersede-live-head-error)"; then | |
| echo "::warning::Noema cleanup could not re-verify the live PR head before cancelling run ${run_id}; stopping cleanup rather than risking a wrong cancellation." >&2 | |
| sed 's/^/ /' /tmp/noema-supersede-live-head-error >&2 || true | |
| exit 0 | |
| fi | |
| if [ "${live_head,,}" != "${EXPECTED_HEAD_SHA,,}" ]; then | |
| echo "::notice::Noema cleanup stopped because the PR head advanced." | |
| exit 0 | |
| fi | |
| if gh api --method POST "repos/${TARGET_REPOSITORY}/actions/runs/${run_id}/cancel" >/dev/null; then | |
| cancelled=$((cancelled + 1)) | |
| echo "Cancelled superseded Noema run ${run_id} for PR #${PR_NUMBER}." | |
| else | |
| echo "::warning::Could not cancel superseded Noema run ${run_id}; it may already be terminal." | |
| fi | |
| done <<<"$run_ids" | |
| done | |
| echo "Superseded Noema cleanup pass ${pass}/2 complete." | |
| done | |
| echo "Cancelled ${cancelled} superseded Noema run(s) after live-head validation." | |
| - name: Select fail-closed Noema reviewer credential | |
| if: env.PR_NUMBER != '' | |
| id: noema_credential | |
| env: | |
| NOEMA_GITHUB_APP_CLIENT_ID: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID || '' }} | |
| NOEMA_GITHUB_APP_PRIVATE_KEY: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY || '' }} | |
| TOKEN_EXCHANGE_URL: ${{ vars.NOEMA_TOKEN_EXCHANGE_URL || vars.NOEMA_EXCHANGE_URL || '' }} | |
| NOEMA_REVIEW_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "$TARGET_REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]]; then | |
| echo "::error::Noema target repository must belong to ContextualWisdomLab; observed ${TARGET_REPOSITORY:-<empty>}." | |
| exit 1 | |
| fi | |
| repository_name="${TARGET_REPOSITORY#*/}" | |
| echo "repository=$repository_name" >>"$GITHUB_OUTPUT" | |
| if [ -n "${NOEMA_REVIEW_TOKEN:-}" ]; then | |
| echo "source=pat" >>"$GITHUB_OUTPUT" | |
| echo "::notice::Noema reviewer using the NOEMA_REVIEW_TOKEN secret fallback identity." | |
| exit 0 | |
| fi | |
| if [ -n "${NOEMA_GITHUB_APP_CLIENT_ID:-}" ] && [ -n "${NOEMA_GITHUB_APP_PRIVATE_KEY:-}" ]; then | |
| echo "source=github-app" >>"$GITHUB_OUTPUT" | |
| echo "::notice::Noema reviewer will mint a repository-scoped cwl-noema-review installation token." | |
| exit 0 | |
| fi | |
| if [ -n "${TOKEN_EXCHANGE_URL:-}" ]; then | |
| echo "source=oidc" >>"$GITHUB_OUTPUT" | |
| echo "::notice::Noema reviewer will use the configured OIDC app-token exchange." | |
| exit 0 | |
| fi | |
| echo "::error::Noema reviewer credential is unconfigured: set NOEMA_GITHUB_APP_CLIENT_ID with NOEMA_GITHUB_APP_PRIVATE_KEY, NOEMA_REVIEW_TOKEN, or NOEMA_TOKEN_EXCHANGE_URL. Review cannot be skipped." | |
| exit 1 | |
| - name: Mint repository-scoped Noema GitHub App token | |
| if: env.PR_NUMBER != '' && steps.noema_credential.outputs.source == 'github-app' | |
| id: noema_github_app_token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }} | |
| owner: ContextualWisdomLab | |
| repositories: ${{ steps.noema_credential.outputs.repository }} | |
| permission-actions: read | |
| permission-checks: read | |
| permission-contents: read | |
| permission-metadata: read | |
| permission-pull-requests: write | |
| permission-security-events: read | |
| permission-statuses: read | |
| permission-vulnerability-alerts: read | |
| - name: Exchange Noema app token through OIDC | |
| if: env.PR_NUMBER != '' && steps.noema_credential.outputs.source == 'oidc' | |
| id: noema_oidc_token | |
| env: | |
| OIDC_AUDIENCE: ${{ vars.NOEMA_OIDC_AUDIENCE || 'cwl-noema-review' }} | |
| TOKEN_EXCHANGE_URL: ${{ vars.NOEMA_TOKEN_EXCHANGE_URL || vars.NOEMA_EXCHANGE_URL || '' }} | |
| run: | | |
| set -euo pipefail | |
| fail_unavailable() { | |
| local message="$1" | |
| echo "::error::$message" | |
| exit 1 | |
| } | |
| if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then | |
| fail_unavailable "Noema app token exchange unavailable: OIDC request environment is missing." | |
| fi | |
| request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}" | |
| separator="&" | |
| case "$request_url" in | |
| *\?*) ;; | |
| *) separator="?" ;; | |
| esac | |
| if ! oidc_response="$( | |
| curl -fsS \ | |
| -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ | |
| "${request_url}${separator}audience=${OIDC_AUDIENCE}" | |
| )"; then | |
| fail_unavailable "Noema app token exchange unavailable: OIDC token request did not complete." | |
| fi | |
| oidc_token="$(jq -r '.value // empty' <<<"$oidc_response")" | |
| if [ -z "$oidc_token" ]; then | |
| fail_unavailable "Noema app token exchange unavailable: OIDC token response was empty." | |
| fi | |
| if ! token_response="$( | |
| curl -fsS \ | |
| -X POST \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer ${oidc_token}" \ | |
| --data "$(jq -cn --arg target_repository "$TARGET_REPOSITORY" '{target_repository:$target_repository}')" \ | |
| "${TOKEN_EXCHANGE_URL}" | |
| )"; then | |
| fail_unavailable "Noema app token exchange unavailable: app token request did not complete." | |
| fi | |
| app_token="$(jq -r '.token // empty' <<<"$token_response")" | |
| if [ -z "$app_token" ]; then | |
| fail_unavailable "Noema app token exchange unavailable: app token response was empty." | |
| fi | |
| echo "::add-mask::$app_token" | |
| echo "token=$app_token" >>"$GITHUB_OUTPUT" | |
| - name: Validate current pull request head | |
| if: env.PR_NUMBER != '' | |
| env: | |
| GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }} | |
| run: | | |
| set -euo pipefail | |
| if ! [[ "$EXPECTED_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then | |
| echo "::error::Noema expected head must be a full commit SHA." | |
| exit 1 | |
| fi | |
| pull_request_json="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")" | |
| live_state="$(jq -r '.state // empty' <<<"$pull_request_json")" | |
| live_head_sha="$(jq -r '.head.sha // empty' <<<"$pull_request_json")" | |
| if [ "$live_state" != "open" ] || [ "${live_head_sha,,}" != "${EXPECTED_HEAD_SHA,,}" ]; then | |
| printf '::error::Noema review target is closed or stale. expected head=%s; live state=%s head=%s.\n' \ | |
| "$EXPECTED_HEAD_SHA" "${live_state:-missing}" "${live_head_sha:-missing}" | |
| exit 1 | |
| fi | |
| - name: Resolve Noema target repository visibility | |
| if: env.PR_NUMBER != '' | |
| id: target_visibility | |
| env: | |
| GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${GH_TOKEN:-}" ]; then | |
| echo "::error::Noema target visibility cannot be resolved without the selected repository-scoped reviewer token." | |
| exit 1 | |
| fi | |
| visibility="" | |
| for target_visibility_attempt in 1 2 3 4 5 6; do | |
| if visibility="$( | |
| gh api "/repos/${TARGET_REPOSITORY}" --jq '.visibility // (if .private then "private" else "public" end)' | |
| )"; then | |
| break | |
| fi | |
| visibility="" | |
| if [ "$target_visibility_attempt" -lt 6 ]; then | |
| echo "Repository visibility lookup failed (attempt ${target_visibility_attempt}/6), possibly a transient GitHub API rate limit; retrying after backoff." >&2 | |
| sleep "$(( target_visibility_attempt * 5 ))" | |
| fi | |
| done | |
| case "$visibility" in | |
| private|internal) | |
| echo "require_zdr=true" >>"$GITHUB_OUTPUT" | |
| echo "::notice::Private/internal target requires an attested ZDR-only review pool." | |
| ;; | |
| public) | |
| echo "require_zdr=false" >>"$GITHUB_OUTPUT" | |
| ;; | |
| *) | |
| echo "::error::Noema target repository visibility is missing or unsupported: ${visibility:-<empty>}." | |
| exit 1 | |
| ;; | |
| esac | |
| - name: Provision contextual-orchestrator review sidecar | |
| if: env.PR_NUMBER != '' | |
| env: | |
| BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} | |
| NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} | |
| NVIDIA_NIM_API_KEY_SUB: ${{ secrets.NVIDIA_NIM_API_KEY_SUB }} | |
| OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| CONTEXTUAL_ORCHESTRATOR_REQUIRE_ZDR: ${{ steps.target_visibility.outputs.require_zdr }} | |
| run: | | |
| set -euo pipefail | |
| bash "$GITHUB_WORKSPACE/scripts/ci/contextual_orchestrator_review_sidecar.sh" | |
| - name: Prepare Noema model verdict | |
| if: env.PR_NUMBER != '' | |
| id: noema_prepare | |
| env: | |
| GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }} | |
| NOEMA_REVIEW_TOKEN_SOURCE: ${{ steps.noema_credential.outputs.source == 'pat' && 'noema-review-pat' || steps.noema_credential.outputs.source == 'github-app' && 'noema-review-github-app' || 'noema-review-app-oidc' }} | |
| NOEMA_REVIEW_ACTOR: ${{ steps.noema_github_app_token.outputs['app-slug'] && format('{0}[bot]', steps.noema_github_app_token.outputs['app-slug']) || '' }} | |
| NOEMA_REVIEW_INSTALLATION_ID: ${{ steps.noema_github_app_token.outputs['installation-id'] }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${PR_NUMBER:-}" ]; then | |
| echo "No pull request number was available for this event; skipping." | |
| echo "prepared=false" >>"$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [ -z "${GH_TOKEN:-}" ]; then | |
| echo "::error::Noema reviewer credential selection succeeded but no token was minted; review cannot prepare a verdict." | |
| exit 1 | |
| fi | |
| if [ -z "${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}" ] || [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE:-}" ]; then | |
| echo "::error::contextual-orchestrator review sidecar must be provisioned before Noema LLM review." | |
| exit 1 | |
| fi | |
| source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" | |
| export NOEMA_LLM_API_URL="${CONTEXTUAL_ORCHESTRATOR_BASE_URL%/}/v1/chat/completions" | |
| export NOEMA_LLM_MODEL="orchestrator/free" | |
| export NOEMA_LLM_API_KEY="${CONTEXTUAL_ORCHESTRATOR_TOKEN}" | |
| export NOEMA_LLM_VIA_ORCHESTRATOR=1 | |
| verdict_file="${RUNNER_TEMP}/noema-verdict-envelope.json" | |
| rm -f "$verdict_file" | |
| python3 "$GITHUB_WORKSPACE/.github/actions/noema-review/two_phase.py" --repo "$TARGET_REPOSITORY" --pr-number "$PR_NUMBER" --expected-head "$EXPECTED_HEAD_SHA" --prepare-verdict-file "$verdict_file" | |
| if [ -f "$verdict_file" ]; then | |
| echo "prepared=true" >>"$GITHUB_OUTPUT" | |
| else | |
| echo "prepared=false" >>"$GITHUB_OUTPUT" | |
| echo "::notice::Noema model phase produced no publishable envelope; publication is skipped." | |
| fi | |
| - name: Upload contextual-orchestrator sidecar evidence on failure | |
| if: failure() && env.PR_NUMBER != '' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: noema-sidecar-evidence | |
| path: | | |
| strix_runs/contextual-orchestrator-sidecar.stderr.log | |
| strix_runs/contextual-orchestrator-preflight.json | |
| if-no-files-found: ignore | |
| retention-days: 5 | |
| - name: Refresh repository-scoped Noema GitHub App token for publication | |
| if: env.PR_NUMBER != '' && steps.noema_prepare.outputs.prepared == 'true' && steps.noema_credential.outputs.source == 'github-app' | |
| id: noema_github_app_publication_token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }} | |
| owner: ContextualWisdomLab | |
| repositories: ${{ steps.noema_credential.outputs.repository }} | |
| permission-actions: read | |
| permission-checks: read | |
| permission-contents: read | |
| permission-metadata: read | |
| permission-pull-requests: write | |
| permission-security-events: read | |
| permission-statuses: read | |
| permission-vulnerability-alerts: read | |
| - name: Publish prepared Noema verdict on the exact live head | |
| if: env.PR_NUMBER != '' && steps.noema_prepare.outputs.prepared == 'true' | |
| env: | |
| GH_TOKEN: ${{ steps.noema_credential.outputs.source == 'pat' && secrets.NOEMA_REVIEW_TOKEN || steps.noema_credential.outputs.source == 'github-app' && steps.noema_github_app_publication_token.outputs.token || steps.noema_credential.outputs.source == 'oidc' && steps.noema_oidc_token.outputs.token || '' }} | |
| NOEMA_REVIEW_TOKEN_SOURCE: ${{ steps.noema_credential.outputs.source == 'pat' && 'noema-review-pat' || steps.noema_credential.outputs.source == 'github-app' && 'noema-review-github-app-refresh' || steps.noema_credential.outputs.source == 'oidc' && 'noema-review-app-oidc' || '' }} | |
| NOEMA_REVIEW_ACTOR: ${{ steps.noema_github_app_publication_token.outputs['app-slug'] && format('{0}[bot]', steps.noema_github_app_publication_token.outputs['app-slug']) || '' }} | |
| NOEMA_REVIEW_INSTALLATION_ID: ${{ steps.noema_github_app_publication_token.outputs['installation-id'] }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${GH_TOKEN:-}" ]; then | |
| echo "::error::Noema publication has no credential for the explicitly selected reviewer source; refusing any GITHUB_TOKEN or author fallback." | |
| exit 1 | |
| fi | |
| verdict_file="${RUNNER_TEMP}/noema-verdict-envelope.json" | |
| if [ ! -f "$verdict_file" ]; then | |
| echo "::error::Noema prepared-verdict output claimed success but its private envelope is missing." | |
| exit 1 | |
| fi | |
| python3 "$GITHUB_WORKSPACE/.github/actions/noema-review/two_phase.py" --repo "$TARGET_REPOSITORY" --pr-number "$PR_NUMBER" --expected-head "$EXPECTED_HEAD_SHA" --publish-verdict-file "$verdict_file" |