mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
v1.91.134.0 feat(evals): readiness-gated paid dispatch (R1), verdict reuse (R2a/R2), receipt store that restores (#3262)
* feat(evals): readiness-gated paid dispatch (R1), restamp-only gate reuse (R2a), and a receipt store that restores Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(evals): R2 verdict reuse across main moves; review fixes (newer-run classification, receipt parser, manual acceptance) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(evals): R2 own-diff signature covers release-output content and CHANGELOG deletions and preamble Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(evals): R2 normalises release outputs by stamp pattern only; compares stamp-file modes; unreadable blobs refuse Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: stamp v1.91.134.0 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(evals): autoplan-guard-pty actor continues through the phase boundary; name PHASE_BOUNDARY_STOP Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
fd13cf25e8
commit
a2a6cabfa6
21 files changed
+1758
-84
No files matched your search
+144
-38
@@ -120,47 +120,46 @@ jobs:
|
||||
docker manifest inspect "$EVAL_IMAGE" > /tmp/eval-runtime-manifest.json
|
||||
echo "id=$(sha256sum /tmp/eval-runtime-manifest.json | cut -d ' ' -f1)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ── Push-burst debounce (W1d: CEO-15, ENG-11, DX-7) ──────────────────────
|
||||
# Only on `synchronize` when another evals.yml run for this PR started in
|
||||
# the last 15 minutes (a push burst): wait 90 s before planning, so the next
|
||||
# push's cancel-in-progress lands before any paid slice starts. A first push
|
||||
# pays nothing. cancel-in-progress does the superseding; the head-SHA check
|
||||
# only reports "superseded by <sha>" when this run outlives the wait. The PR
|
||||
# label evals-no-debounce skips the wait.
|
||||
debounce:
|
||||
# ── Readiness-gated dispatch (R1, GSTA-48) ───────────────────────────────
|
||||
# Paid slices for a PR head start only after the PR has been quiet for the
|
||||
# window (vars.EVALS_QUIET_MINUTES, default 10; the evals-no-debounce label
|
||||
# sets it to 0) and Free Tests has passed on the same head SHA. A newer push
|
||||
# still cancels this run (cancel-in-progress), so the window is a sleep; the
|
||||
# same heads get the same selection and verdict rules, only the start moves.
|
||||
# Free Tests red on this head fails the run without paid work (re-run this
|
||||
# workflow after a green Free Tests rerun). Dispatches never wait: they are
|
||||
# explicit (eval:bg, census). Version Gate is not an input, so a stamp push
|
||||
# waits for its own Free Tests only and always gets its gate.
|
||||
readiness:
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.event_name == 'pull_request' && github.event.action == 'synchronize' && github.actor != 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository && !contains(github.event.pull_request.labels.*.name, 'evals-no-debounce')
|
||||
timeout-minutes: 5
|
||||
if: github.event_name == 'pull_request' && github.actor != 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
timeout-minutes: 90
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
superseded: ${{ steps.wait.outputs.superseded }}
|
||||
steps:
|
||||
- name: Wait out a push burst
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.4.2
|
||||
|
||||
- name: Wait for a quiet PR and green Free Tests on this head
|
||||
id: wait
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
HEAD_BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
QUIET_MINUTES: ${{ contains(github.event.pull_request.labels.*.name, 'evals-no-debounce') && '0' || vars.EVALS_QUIET_MINUTES || '10' }}
|
||||
run: |
|
||||
since=$(date -u -d '15 minutes ago' +%s)
|
||||
recent=$(gh api -X GET "repos/$GITHUB_REPOSITORY/actions/workflows/evals.yml/runs" -f event=pull_request \
|
||||
-f branch="$HEAD_BRANCH" -f per_page=20 \
|
||||
--jq "[.workflow_runs[] | select(.id < $GITHUB_RUN_ID and any(.pull_requests[]; .number == $PR_NUMBER) and ((.created_at | fromdateiso8601) >= $since))] | length" \
|
||||
2>/dev/null || echo 0)
|
||||
if [ "${recent:-0}" -eq 0 ]; then
|
||||
echo "- Debounce: no other run for this PR in the last 15 min; no wait" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
echo "- Debounce: ${recent} run(s) for this PR started in the last 15 min; waiting 90 s (label evals-no-debounce skips this)" >> "$GITHUB_STEP_SUMMARY"
|
||||
sleep 90
|
||||
head=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .head.sha 2>/dev/null || echo "$HEAD_SHA")
|
||||
if [ "$head" != "$HEAD_SHA" ]; then
|
||||
echo "superseded=true" >> "$GITHUB_OUTPUT"
|
||||
echo "- Debounce: superseded by ${head}; this run plans no slices" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
bun --no-install run scripts/eval-readiness.ts wait --repo "$GITHUB_REPOSITORY" --pr "$PR_NUMBER" --head-sha "$HEAD_SHA" \
|
||||
--run-id "$GITHUB_RUN_ID" --quiet-minutes "$QUIET_MINUTES" --deadline-minutes 80 \
|
||||
--github-output "$GITHUB_OUTPUT" --summary "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# ── Receipt recovery (W1c: CEO-14/34, ENG-4/5/7) ─────────────────────────
|
||||
# A run cancelled by the next push never reaches slices-report, so its
|
||||
@@ -175,10 +174,10 @@ jobs:
|
||||
# plan-slices turns reuse off, so cases execute unreduced.
|
||||
recover-receipts:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [debounce]
|
||||
# Runs after the debounce settles, so a superseded run's cancellation has usually completed.
|
||||
needs: [readiness]
|
||||
# Runs after readiness settles, so a superseded run's cancellation has usually completed; a head whose Free Tests failed recovers nothing.
|
||||
# A dispatch naming pr_receipts reads that PR's receipts from its runs' artifacts (cache entries are ref-scoped).
|
||||
if: ${{ !cancelled() && needs.debounce.outputs.superseded != 'true' && github.actor != 'dependabot[bot]' && ((github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && !contains(github.event.pull_request.labels.*.name, 'evals-fresh')) || (github.event_name == 'workflow_dispatch' && inputs.pr_receipts != '')) }}
|
||||
if: ${{ !cancelled() && (needs.readiness.result == 'success' || needs.readiness.result == 'skipped') && needs.readiness.outputs.superseded != 'true' && github.actor != 'dependabot[bot]' && ((github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && !contains(github.event.pull_request.labels.*.name, 'evals-fresh')) || (github.event_name == 'workflow_dispatch' && inputs.pr_receipts != '')) }}
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -221,6 +220,42 @@ jobs:
|
||||
--store /tmp/gstack-eval-input-cache --store-key "$STORE_KEY" --mode "$RECOVERY_MODE" --budget-seconds 60 >> "$GITHUB_STEP_SUMMARY" \
|
||||
|| echo "- Receipt recovery crashed; reuse stays off this run" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# Verdict reuse (GSTA-48), a gate edit: base-ref code proposes reusing
|
||||
# the newest executed GREEN gate of this PR when this head differs from
|
||||
# it only by stamp files on the same base (R2a: TREE same-modulo-stamps
|
||||
# from the tree receipt under the base's ship policy), or when main
|
||||
# moved and the PR's own diff from its merge-base, stamp files excluded,
|
||||
# is byte-identical (R2). plan-slices confirms the selection is
|
||||
# identical. Anything else (a red, partial or cancelled gate, a changed
|
||||
# own diff, a non-stamp byte, no binding or receipt, a gh or git error,
|
||||
# evals-fresh) leaves no candidate: the full gate runs.
|
||||
- name: Find a gate verdict to reuse (R2a/R2, base-ref code)
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
HEAD_BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
if [ ! -f scripts/gate-reuse.ts ]; then
|
||||
echo "- Gate verdict reuse unavailable: the base ref has no scripts/gate-reuse.ts; the full gate runs" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
bun --no-install run scripts/gate-reuse.ts find --repo "$GITHUB_REPOSITORY" --pr "$PR_NUMBER" --head-repo "$HEAD_REPO" \
|
||||
--branch "$HEAD_BRANCH" --run-id "$GITHUB_RUN_ID" --head-sha "$HEAD_SHA" --tested-sha "$GITHUB_SHA" \
|
||||
--out /tmp/gate-reuse --budget-seconds 90 >> "$GITHUB_STEP_SUMMARY" \
|
||||
|| echo "- Gate verdict reuse crashed; the full gate runs" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Hand a gate reuse candidate to plan-slices
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||
with:
|
||||
name: gate-reuse-candidate
|
||||
path: /tmp/gate-reuse
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
|
||||
- name: Hand the receipt store to plan-slices
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||
with:
|
||||
@@ -248,15 +283,16 @@ jobs:
|
||||
# branch (see CLAUDE.md's garrytan-agents workflow).
|
||||
plan-slices:
|
||||
runs-on: ubicloud-standard-8
|
||||
needs: [debounce, recover-receipts]
|
||||
# !cancelled(): plans even when the debounce or recovery was skipped (first push, dispatch, evals-fresh) or recovery failed (reuse is then off); a superseded run plans nothing.
|
||||
if: ${{ !cancelled() && needs.debounce.outputs.superseded != 'true' && github.actor != 'dependabot[bot]' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
needs: [readiness, recover-receipts]
|
||||
# !cancelled(): plans even when readiness or recovery was skipped (dispatch, evals-fresh) or recovery failed (reuse is then off); a superseded run, or a head whose readiness failed (Free Tests red), plans nothing.
|
||||
if: ${{ !cancelled() && (needs.readiness.result == 'success' || needs.readiness.result == 'skipped') && needs.readiness.outputs.superseded != 'true' && github.actor != 'dependabot[bot]' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
slices: ${{ steps.matrix.outputs.slices }}
|
||||
slice_timeouts: ${{ steps.matrix.outputs.slice_timeouts }}
|
||||
gate_reuse: ${{ steps.gate_reuse.outputs.gate_reuse }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
@@ -311,6 +347,24 @@ jobs:
|
||||
EVALS_BASE: ${{ inputs.base_sha || (inputs.base_ref != '' && format('origin/{0}', inputs.base_ref)) || '' }}
|
||||
run: EVALS_TIER=gate bun --no-install run scripts/test-paid-shards.ts --tier gate --emit-plan /tmp/paid-plan/manifest.json --slice-budget 420 --jobs 2 --max-parallel 20
|
||||
|
||||
# R2a/R2: reuse holds only when this head's own plan selects exactly the
|
||||
# cases and trial plans the reused gate ran (selection fingerprint).
|
||||
- name: Download a gate reuse candidate
|
||||
if: github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
||||
with:
|
||||
name: gate-reuse-candidate
|
||||
path: /tmp/gate-reuse
|
||||
|
||||
- name: Confirm gate verdict reuse (R2a/R2)
|
||||
id: gate_reuse
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
mkdir -p /tmp/gate-reuse
|
||||
bun --no-install run scripts/gate-reuse.ts confirm /tmp/gate-reuse /tmp/paid-plan/manifest.json --out /tmp/paid-plan \
|
||||
--github-output "$GITHUB_OUTPUT" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# CEO-13: the mode, case counts and, on fallback, every file that
|
||||
# restored the full gate with the edit that narrows it next time.
|
||||
- name: Summarize PR coverage
|
||||
@@ -336,6 +390,7 @@ jobs:
|
||||
/tmp/paid-plan/manifest.json
|
||||
/tmp/paid-plan/receipts
|
||||
/tmp/paid-plan/store
|
||||
/tmp/paid-plan/gate-reuse.json
|
||||
retention-days: 30
|
||||
|
||||
eval-slices:
|
||||
@@ -346,7 +401,8 @@ jobs:
|
||||
# !cancelled(), not always(): still runs when build-image was skipped
|
||||
# (image already published), but a newer push's cancel-in-progress stops
|
||||
# it instead of letting a superseded run finish its paid slices first.
|
||||
if: ${{ !cancelled() && needs.build-image.result == 'success' && needs.plan-slices.result == 'success' }}
|
||||
# A confirmed verdict reuse (R2a/R2) runs no paid slice.
|
||||
if: ${{ !cancelled() && needs.build-image.result == 'success' && needs.plan-slices.result == 'success' && needs.plan-slices.outputs.gate_reuse != 'true' }}
|
||||
# The planner packs ~7 minutes of recorded work per slice (EVALS_JOBS=2 x
|
||||
# EVALS_CONCURRENCY=2 per runner, never the old 40-way per-row fan-out
|
||||
# that queued claude session STARTUP behind 39 siblings). Each slice's job
|
||||
@@ -500,7 +556,11 @@ jobs:
|
||||
retention-days: 30
|
||||
|
||||
slices-report:
|
||||
runs-on: ubicloud-standard-2
|
||||
# GitHub-hosted, like recover-receipts: Ubicloud runners send actions/cache
|
||||
# to Ubicloud's own transparent cache, which a GitHub-hosted restore never
|
||||
# sees, so a store saved on Ubicloud was never restored (GSTA-48: every
|
||||
# restore missed, e.g. #3253's restamp gate 3 minutes after a saved store).
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [plan-slices, eval-slices]
|
||||
# !cancelled(): the report must run (and FAIL) when an executor died — a
|
||||
# missing slice artifact reading as green is the class this lane kills —
|
||||
@@ -518,7 +578,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
reconcile-exit: ${{ steps.reconcile.outputs.exit }}
|
||||
reconcile-exit: ${{ steps.reconcile.outputs.exit || steps.reused.outputs.exit }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
@@ -543,6 +603,7 @@ jobs:
|
||||
|
||||
- name: Reconcile slices against the manifest (fail-closed)
|
||||
id: reconcile
|
||||
if: needs.plan-slices.outputs.gate_reuse != 'true'
|
||||
run: |
|
||||
set +e
|
||||
EVALS_TIER=gate bun --no-install run scripts/test-paid-shards.ts --tier gate --report /tmp/paid-report | tee /tmp/report.txt
|
||||
@@ -552,6 +613,19 @@ jobs:
|
||||
# (caught by the ship review army; the wiring test now pins this).
|
||||
echo "exit=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# R2a/R2: the planner confirmed a reuse, so no slice ran; the verdict is
|
||||
# the reused gate's, printed with which run, which head and why. A
|
||||
# missing or malformed reuse record exits 1 (fail-closed).
|
||||
- name: Report the reused gate verdict (R2a/R2)
|
||||
id: reused
|
||||
if: needs.plan-slices.outputs.gate_reuse == 'true'
|
||||
run: |
|
||||
set +e
|
||||
bun --no-install run scripts/gate-reuse.ts report /tmp/paid-report/gate-reuse.json --repo "$GITHUB_REPOSITORY" > /tmp/report.txt
|
||||
code=$?
|
||||
cat /tmp/report.txt
|
||||
echo "exit=$code" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Summarize PR coverage and reuse
|
||||
if: always()
|
||||
run: bun --no-install run scripts/test-pr-profile.ts summary /tmp/paid-report/manifest.json /tmp/paid-report/collector-outcomes.json >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -594,6 +668,29 @@ jobs:
|
||||
if-no-files-found: ignore
|
||||
retention-days: 30
|
||||
|
||||
# R2a/R2: what this executed run proved, read by a later push. The base
|
||||
# is the tested merge commit's first parent. Reused runs bind nothing,
|
||||
# so reuse never chains off a reuse.
|
||||
- name: Bind this run's verdict for reuse (R2a/R2)
|
||||
if: always() && github.event_name == 'pull_request' && needs.plan-slices.outputs.gate_reuse != 'true'
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
RECONCILE_EXIT: ${{ steps.reconcile.outputs.exit }}
|
||||
run: |
|
||||
bun --no-install run scripts/gate-reuse.ts bind /tmp/paid-report --out /tmp/gate-binding --repo "$GITHUB_REPOSITORY" \
|
||||
--pr "$PR_NUMBER" --run-id "$GITHUB_RUN_ID" --run-attempt "$GITHUB_RUN_ATTEMPT" --head-sha "$HEAD_SHA" \
|
||||
--tested-sha "$GITHUB_SHA" --profile "$EVALS_PROFILE" --reconcile-exit "$RECONCILE_EXIT" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload the gate binding
|
||||
if: always() && github.event_name == 'pull_request' && needs.plan-slices.outputs.gate_reuse != 'true'
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||
with:
|
||||
name: gate-binding-a${{ github.run_attempt }}
|
||||
path: /tmp/gate-binding/gate-binding.json
|
||||
if-no-files-found: ignore
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload trial outcomes for pass-rate history
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||
@@ -604,7 +701,7 @@ jobs:
|
||||
retention-days: 90
|
||||
|
||||
- name: Fail the workflow when reconciliation failed
|
||||
if: steps.reconcile.outputs.exit != '0'
|
||||
if: (steps.reconcile.outputs.exit || steps.reused.outputs.exit) != '0'
|
||||
run: exit 1
|
||||
|
||||
# PR comment in its OWN job with the write token and ZERO repo code: no
|
||||
@@ -644,6 +741,13 @@ jobs:
|
||||
RECONCILE_EXIT: ${{ needs.slices-report.outputs.reconcile-exit }}
|
||||
run: |
|
||||
# shellcheck disable=SC2086,SC2059
|
||||
# R2a/R2: a confirmed verdict reuse ran no case; say which run,
|
||||
# which head and why, with the receipt or own-diff signature.
|
||||
REUSE=/tmp/paid-report/gate-reuse.json
|
||||
if [ "${RECONCILE_EXIT:-1}" = "0" ] && [ -f "$REUSE" ] \
|
||||
&& jq -e '.schema == 1 and (.kind == "restamp-only" or .kind == "main-moved")' "$REUSE" >/dev/null 2>&1; then
|
||||
BODY=$(jq -r --arg repo "$GITHUB_REPOSITORY" '"## E2E Evals: ✅ PASS (reused: \(if .kind == "restamp-only" then "restamp-only, R2a" else "main moved, R2" end))\n\nNo paid case ran on `\(.current_head)`. It carries the GREEN verdict of [run \(.reused_run_id)](https://github.com/\($repo)/actions/runs/\(.reused_run_id)) at `\(.gated_head)`, because \(.why).\n\n```\n\(if .kind == "restamp-only" then "TREE: same-modulo-stamps receipt=\(.receipt_sha256)" else "OWN DIFF: identical sha256=\(.own_diff_sha256) (merge-base \(.gated_merge_base) -> \(.merge_base))" end)\nbase: \(.gated_base) -> \(.base_sha)\nselection: \(.selection_sha256) (identical to the reused gate)\n```\n\n---\n*Verdict reuse (R2a restamp-only, R2 main moved): a changed own diff, a non-stamp byte, a changed selection, a newer red or unbound gate, or a missing binding or receipt runs the full gate; the evals-fresh label opts out. Free Tests and every other check still ran on this head.*"' "$REUSE")
|
||||
else
|
||||
TOTAL=0; PASSED=0; FAILED=0; MANUAL=0; EXECUTED=0; REUSED=0; COST="0"
|
||||
SUITE_LINES=""
|
||||
VERIFIED=/tmp/verdict/paid-report/collector-outcomes.json
|
||||
@@ -739,6 +843,8 @@ jobs:
|
||||
\`\`\`"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
COMMENT_ID=$(gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments \
|
||||
--jq '.[] | select(.body | startswith("## E2E Evals")) | .id' | tail -1)
|
||||
|
||||
|
||||
Reference in new issue
Block a user