mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-12 03:30:28 +01:00
feat(evals): merge-train workflow side, inert behind the train policy block (GSTA-49 T1b)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
a844821583
commit
8128af38ec
16 files changed
+2265
-87
No files matched your search
@@ -0,0 +1,65 @@
|
||||
name: Main backstop gate
|
||||
# GSTA-49 T1b (docs/ship-policy.md#run-a-merge-train). Every push to main is
|
||||
# covered by a full gate. Under merge trains a train's merges are covered by
|
||||
# the gate the train already ran on the same trees, so this workflow gates
|
||||
# main only when no green-train record lists main's tree modulo stamps (a
|
||||
# merge outside the queue), after any train has finished merging. A red
|
||||
# backstop writes the freeze record gstack-ship-queue reads. Inert (no paid
|
||||
# work, nothing written) unless main's .gstack/ship-policy.json declares a
|
||||
# `train` block; without one the per-PR gates cover main as before.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
# One backstop at a time; a newer push waits and the script hands a superseded
|
||||
# push to it (only the newest pending run is kept).
|
||||
concurrency:
|
||||
group: evals-main-backstop
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
backstop:
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
# Settle window (60) + the full gate (270) + margin.
|
||||
timeout-minutes: 345
|
||||
permissions:
|
||||
# Pushes the freeze record to refs/notes/gstack-trains.
|
||||
contents: write
|
||||
# Dispatches the gate and reads train runs and their artifacts.
|
||||
actions: write
|
||||
checks: read
|
||||
# Opens the issue a red backstop names.
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Read with git and jq only: without a train block nothing below runs.
|
||||
- name: Read the train policy (main)
|
||||
id: policy
|
||||
run: |
|
||||
train=$(git show "${GITHUB_SHA}:.gstack/ship-policy.json" 2>/dev/null | jq -c 'if (.train | type) == "object" then .train else null end' 2>/dev/null || true)
|
||||
if [ -z "$train" ] || [ "$train" = null ]; then
|
||||
echo "train=false" >> "$GITHUB_OUTPUT"
|
||||
echo "- Backstop inert: main's .gstack/ship-policy.json declares no train block; per-PR gates cover main" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "train=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
if: steps.policy.outputs.train == 'true'
|
||||
with:
|
||||
bun-version: 1.4.2
|
||||
|
||||
- name: Gate main unless a green train covers its tree
|
||||
if: steps.policy.outputs.train == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GSTACK_STATE_ROOT: ${{ runner.temp }}/gstack-state
|
||||
run: |
|
||||
bun --no-install run scripts/train-backstop.ts run --repo "$GITHUB_REPOSITORY" --branch "$GITHUB_REF_NAME" --sha "$GITHUB_SHA" \
|
||||
--summary "$GITHUB_STEP_SUMMARY"
|
||||
+480
-27
@@ -1,6 +1,8 @@
|
||||
name: E2E Evals
|
||||
# eval-bg (scripts/eval-bg.ts) finds its dispatch by the nonce in the run name (ENG-12).
|
||||
run-name: ${{ inputs.nonce != '' && format('E2E Evals [eval-bg {0}]', inputs.nonce) || '' }}
|
||||
# A dispatched screen's name also ends ` screen #<pr>`, which recover-receipts and
|
||||
# gate-reuse filter on to list a PR's screens beside its pull_request runs.
|
||||
run-name: "${{ inputs.nonce != '' && format('E2E Evals [eval-bg {0}]{1}', inputs.nonce, inputs.screen_pr != '' && format(' screen #{0}', inputs.screen_pr) || '') || '' }}"
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
@@ -30,9 +32,33 @@ on:
|
||||
description: 'Same-repo PR number whose receipt store this run reads read-only (never saves); empty = fresh'
|
||||
type: string
|
||||
default: ''
|
||||
# Merge trains (docs/ship-policy.md#run-a-merge-train): only under a train
|
||||
# block in the default branch's .gstack/ship-policy.json. The route job
|
||||
# validates them before any paid work (scripts/train-gate.ts route).
|
||||
cases:
|
||||
description: 'Train bisect: comma-separated case ids from the full gate plan (gstack-ship-queue train bisect sets it)'
|
||||
type: string
|
||||
default: ''
|
||||
prefix_shas:
|
||||
description: 'Train bisect: comma-separated prefix commits, each an ancestor of the train head on train/<nonce>'
|
||||
type: string
|
||||
default: ''
|
||||
screen_pr:
|
||||
description: 'Dispatched screen: the same-repo PR number (gstack-ship-queue ready --screen sets it)'
|
||||
type: string
|
||||
default: ''
|
||||
screen_merge_sha:
|
||||
description: "Dispatched screen: the PR's refs/pull/<n>/merge commit to check out and test"
|
||||
type: string
|
||||
default: ''
|
||||
screen_policy:
|
||||
description: 'Dispatched screen: narrow or broad (a broad screen never adopts a narrow run)'
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
# A dispatched screen is keyed on its PR, so a newer head's screen cancels the older one.
|
||||
concurrency:
|
||||
group: evals-${{ github.event.pull_request.number || github.run_id }}
|
||||
group: evals-${{ github.event.pull_request.number || (inputs.screen_pr != '' && format('screen-{0}', inputs.screen_pr)) || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
@@ -120,6 +146,129 @@ jobs:
|
||||
docker manifest inspect "$EVAL_IMAGE" > /tmp/eval-runtime-manifest.json
|
||||
echo "id=$(sha256sum /tmp/eval-runtime-manifest.json | cut -d ' ' -f1)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ── Route (merge trains, GSTA-49 T1b; docs/ship-policy.md#run-a-merge-train)
|
||||
# Names the run's mode before any paid work, from the train policy of the
|
||||
# base revision (the PR's base commit, or the default branch for a
|
||||
# dispatch). Without a `train` block every run routes as before: a
|
||||
# pull_request run is `pr`, a dispatch is `dispatch`, and no other job
|
||||
# changes. Under a train block a PR push is `pr-summary` (screen-summary
|
||||
# prints the screening selection and its estimated cost; no paid slice
|
||||
# starts), and gstack-ship-queue's dispatches are `screen`, `train` or
|
||||
# `bisect` once scripts/train-gate.ts has validated their inputs. A refused
|
||||
# dispatch fails here, before any paid work.
|
||||
route:
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
mode: ${{ steps.route.outputs.mode }}
|
||||
record_artifact: ${{ steps.policy.outputs.record_artifact }}
|
||||
full_gate_check: ${{ steps.policy.outputs.full_gate_check }}
|
||||
screen_head: ${{ steps.screen.outputs.head_sha }}
|
||||
screen_base: ${{ steps.screen.outputs.base_sha }}
|
||||
screen_branch: ${{ steps.screen.outputs.head_branch }}
|
||||
superseded: ${{ steps.screen.outputs.superseded }}
|
||||
bisect_slices: ${{ steps.bisect.outputs.slices }}
|
||||
bisect_timeouts: ${{ steps.bisect.outputs.slice_timeouts }}
|
||||
bisect_refs: ${{ steps.bisect.outputs.prefix_refs }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.4.2
|
||||
|
||||
# Read with git and jq only: no repository code decides whether PR pushes start paid work.
|
||||
- name: Read the train policy (base revision)
|
||||
id: policy
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
run: |
|
||||
base="${BASE_SHA:-origin/${DEFAULT_BRANCH}}"
|
||||
train=$(git show "${base}:.gstack/ship-policy.json" 2>/dev/null | jq -c 'if (.train | type) == "object" then .train else null end' 2>/dev/null || true)
|
||||
artifact=$(jq -r '.record_artifact // ""' <<<"${train:-null}" 2>/dev/null || true)
|
||||
check=$(jq -r '.full_gate_check // ""' <<<"${train:-null}" 2>/dev/null || true)
|
||||
if [ -z "$train" ] || [ "$train" = null ]; then
|
||||
echo "train=false" >> "$GITHUB_OUTPUT"
|
||||
echo "- Train policy: none at ${base}; this run routes as before" >> "$GITHUB_STEP_SUMMARY"
|
||||
elif [[ "$artifact" =~ ^[A-Za-z0-9_.-]{1,64}$ ]] && [[ "$check" =~ ^[A-Za-z0-9_./-]{1,64}$ ]]; then
|
||||
{ echo "train=true"; echo "record_artifact=${artifact}"; echo "full_gate_check=${check}"; } >> "$GITHUB_OUTPUT"
|
||||
echo "- Train policy: declared at ${base} (record artifact ${artifact}, check ${check})" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "train=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning::the train block at ${base} names no valid record_artifact and full_gate_check; this run routes as before"
|
||||
fi
|
||||
|
||||
- name: Route this run
|
||||
id: route
|
||||
env:
|
||||
TRAIN_POLICY: ${{ steps.policy.outputs.train }}
|
||||
NONCE: ${{ inputs.nonce }}
|
||||
EXPECTED_SHA: ${{ inputs.expected_sha }}
|
||||
EVALS_ALL: ${{ inputs.evals_all }}
|
||||
CASES: ${{ inputs.cases }}
|
||||
PREFIX_SHAS: ${{ inputs.prefix_shas }}
|
||||
SCREEN_PR: ${{ inputs.screen_pr }}
|
||||
SCREEN_MERGE_SHA: ${{ inputs.screen_merge_sha }}
|
||||
SCREEN_POLICY: ${{ inputs.screen_policy }}
|
||||
run: |
|
||||
if [ "$GITHUB_EVENT_NAME" = pull_request ]; then
|
||||
mode='pr'
|
||||
if [ "$TRAIN_POLICY" = true ]; then mode=pr-summary; fi
|
||||
echo "mode=${mode}" >> "$GITHUB_OUTPUT"
|
||||
echo "- Run mode: ${mode}" >> "$GITHUB_STEP_SUMMARY"
|
||||
elif [ -f scripts/train-gate.ts ]; then
|
||||
bun --no-install run scripts/train-gate.ts route --github-output "$GITHUB_OUTPUT" >> "$GITHUB_STEP_SUMMARY"
|
||||
elif [ -n "${CASES}${PREFIX_SHAS}${SCREEN_PR}${SCREEN_MERGE_SHA}${SCREEN_POLICY}" ]; then
|
||||
echo "::error::${GITHUB_REF_NAME} has no scripts/train-gate.ts to validate the train or screen inputs; dispatch from a ref that has it"
|
||||
exit 1
|
||||
else
|
||||
echo "mode=dispatch" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# The screen's merge commit must merge the PR's current head into its
|
||||
# base, and Free Tests must be green on that head (base-branch code: a
|
||||
# screen dispatches on the default branch). No quiet window: a screen is
|
||||
# an explicit request. A moved PR head reports superseded and plans nothing.
|
||||
- name: Check the screen's PR head (Free Tests)
|
||||
id: screen
|
||||
if: steps.route.outputs.mode == 'screen'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
SCREEN_PR: ${{ inputs.screen_pr }}
|
||||
SCREEN_MERGE_SHA: ${{ inputs.screen_merge_sha }}
|
||||
run: |
|
||||
bun --no-install run scripts/eval-readiness.ts wait --repo "$GITHUB_REPOSITORY" --pr "$SCREEN_PR" --merge-sha "$SCREEN_MERGE_SHA" \
|
||||
--run-id "$GITHUB_RUN_ID" --quiet-minutes 0 --deadline-minutes 20 \
|
||||
--github-output "$GITHUB_OUTPUT" --summary "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Plan the bisect (one executor per prefix)
|
||||
id: bisect
|
||||
if: steps.route.outputs.mode == 'bisect'
|
||||
env:
|
||||
CASES: ${{ steps.route.outputs.cases }}
|
||||
PREFIXES: ${{ steps.route.outputs.prefixes }}
|
||||
NONCE: ${{ inputs.nonce }}
|
||||
run: |
|
||||
bun --no-install run scripts/train-gate.ts bisect-plan --cases "$CASES" --prefixes "$PREFIXES" --nonce "$NONCE" \
|
||||
--out /tmp/train-bisect/bisect.json --github-output "$GITHUB_OUTPUT" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Hand the bisect plan to its executors
|
||||
if: steps.route.outputs.mode == 'bisect'
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||
with:
|
||||
name: train-bisect-plan
|
||||
path: /tmp/train-bisect/bisect.json
|
||||
retention-days: 30
|
||||
|
||||
# ── 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
|
||||
@@ -161,6 +310,35 @@ jobs:
|
||||
--run-id "$GITHUB_RUN_ID" --quiet-minutes "$QUIET_MINUTES" --deadline-minutes 80 \
|
||||
--github-output "$GITHUB_OUTPUT" --summary "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# GSTA-48 P3-1: a head the readiness job blocked runs no gate, so the PR's
|
||||
# last "## E2E Evals" comment would still show an older head's verdict.
|
||||
# Replace it (never post a new one) with what happened to this head. Runs no
|
||||
# repository code.
|
||||
readiness-comment:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [readiness]
|
||||
if: ${{ !cancelled() && needs.readiness.result == 'failure' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
steps:
|
||||
- name: Replace a stale verdict comment
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
COMMENT_ID=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" --paginate \
|
||||
--jq '.[] | select(.body | startswith("## E2E Evals")) | .id' | tail -1)
|
||||
if [ -z "$COMMENT_ID" ]; then exit 0; fi
|
||||
BODY="## E2E Evals: ⛔ NOT RUN on \`${HEAD_SHA}\`
|
||||
|
||||
The readiness job blocked this head (Free Tests did not pass on it, or did not finish within the readiness deadline), so no paid gate ran and no verdict covers it. An earlier verdict in this comment belonged to an older head.
|
||||
|
||||
Details: [run ${GITHUB_RUN_ID}](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}). Fix and push, or re-run Free Tests and then re-run this workflow."
|
||||
gh api "repos/${GITHUB_REPOSITORY}/issues/comments/${COMMENT_ID}" -X PATCH -f body="$BODY"
|
||||
|
||||
# ── Receipt recovery (W1c: CEO-14/34, ENG-4/5/7) ─────────────────────────
|
||||
# A run cancelled by the next push never reaches slices-report, so its
|
||||
# finished receipts (passes AND the negatives executors write at the
|
||||
@@ -174,10 +352,11 @@ jobs:
|
||||
# plan-slices turns reuse off, so cases execute unreduced.
|
||||
recover-receipts:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [readiness]
|
||||
needs: [readiness, route]
|
||||
# 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.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 != '')) }}
|
||||
# A dispatched screen only looks for a gate verdict to reuse (R2a/R2); a PR push under a train policy (pr-summary) skips this job.
|
||||
if: ${{ !cancelled() && (needs.readiness.result == 'success' || needs.readiness.result == 'skipped') && needs.readiness.outputs.superseded != 'true' && needs.route.result == 'success' && needs.route.outputs.superseded != 'true' && github.actor != 'dependabot[bot]' && ((github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && needs.route.outputs.mode == 'pr' && !contains(github.event.pull_request.labels.*.name, 'evals-fresh')) || (github.event_name == 'workflow_dispatch' && inputs.pr_receipts != '') || needs.route.outputs.mode == 'screen') }}
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -185,7 +364,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha || inputs.base_sha || github.event.repository.default_branch }}
|
||||
ref: ${{ github.event.pull_request.base.sha || needs.route.outputs.screen_base || inputs.base_sha || github.event.repository.default_branch }}
|
||||
persist-credentials: false
|
||||
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
@@ -202,6 +381,7 @@ jobs:
|
||||
restore-keys: eval-input-v1-${{ github.repository_id }}-pr-${{ github.event.pull_request.number }}-
|
||||
|
||||
- name: Recover receipts of cancelled runs (base-ref code)
|
||||
if: needs.route.outputs.mode != 'screen'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
STORE_KEY: ${{ steps.store.outputs.cache-matched-key }}
|
||||
@@ -229,26 +409,32 @@ jobs:
|
||||
# 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.
|
||||
# A dispatched screen (GSTA-49 T1b) finds the same way, authenticated by
|
||||
# PR number, head, repository and tested base (its merge commit's first
|
||||
# parent), listing the PR's pull_request runs and earlier screens; a
|
||||
# broad screen never adopts a narrow run.
|
||||
- name: Find a gate verdict to reuse (R2a/R2, base-ref code)
|
||||
if: github.event_name == 'pull_request'
|
||||
if: github.event_name == 'pull_request' || needs.route.outputs.mode == 'screen'
|
||||
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 }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || inputs.screen_pr }}
|
||||
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
HEAD_BRANCH: ${{ github.event.pull_request.head.ref || needs.route.outputs.screen_branch }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha || needs.route.outputs.screen_head }}
|
||||
TESTED_SHA: ${{ inputs.screen_merge_sha || github.sha }}
|
||||
SCREEN_POLICY: ${{ inputs.screen_policy }}
|
||||
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" \
|
||||
--branch "$HEAD_BRANCH" --run-id "$GITHUB_RUN_ID" --head-sha "$HEAD_SHA" --tested-sha "$TESTED_SHA" \
|
||||
${SCREEN_POLICY:+--screen-policy "$SCREEN_POLICY"} --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'
|
||||
if: github.event_name == 'pull_request' || needs.route.outputs.mode == 'screen'
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||
with:
|
||||
name: gate-reuse-candidate
|
||||
@@ -264,6 +450,36 @@ jobs:
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
|
||||
# ── PR push under a train policy (GSTA-49 T1b, a gate edit) ─────────────
|
||||
# Only when the base revision declares a `train` block: the push starts no
|
||||
# paid slice. This job prints how to request the paid screen, the
|
||||
# screening selection and its estimated cost, for free; the train's full
|
||||
# gate covers every merge. Without a train block it never runs.
|
||||
screen-summary:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [route]
|
||||
if: needs.route.outputs.mode == 'pr-summary' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.4.2
|
||||
|
||||
- name: Print the screening selection and its estimated cost (no paid case runs)
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
EVALS_TIER=gate bun --no-install run scripts/test-paid-shards.ts --tier gate --emit-plan /tmp/paid-plan/manifest.json --slices 1
|
||||
bun --no-install run scripts/train-gate.ts screen-summary /tmp/paid-plan/manifest.json --pr "$PR_NUMBER" >> "$GITHUB_STEP_SUMMARY"
|
||||
bun --no-install run scripts/test-pr-profile.ts summary /tmp/paid-plan/manifest.json >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# ── Sliced lane (the ONLY paid lane; legacy 17-row matrix deleted) ──────────
|
||||
# One PLANNER computes diff selection + the slice plan ONCE (killing
|
||||
# per-slice selector divergence); K executors consume the manifest; the
|
||||
@@ -283,9 +499,10 @@ jobs:
|
||||
# branch (see CLAUDE.md's garrytan-agents workflow).
|
||||
plan-slices:
|
||||
runs-on: ubicloud-standard-8
|
||||
needs: [readiness, recover-receipts]
|
||||
needs: [readiness, recover-receipts, route]
|
||||
# !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) }}
|
||||
# The route job must have named a planned mode: pr-summary (a PR push under a train policy) and bisect plan nothing here, and neither does a refused dispatch.
|
||||
if: ${{ !cancelled() && (needs.readiness.result == 'success' || needs.readiness.result == 'skipped') && needs.readiness.outputs.superseded != 'true' && needs.route.result == 'success' && needs.route.outputs.superseded != 'true' && contains(fromJSON('["pr","screen","train","dispatch"]'), needs.route.outputs.mode) && 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
|
||||
@@ -293,9 +510,16 @@ jobs:
|
||||
slices: ${{ steps.matrix.outputs.slices }}
|
||||
slice_timeouts: ${{ steps.matrix.outputs.slice_timeouts }}
|
||||
gate_reuse: ${{ steps.gate_reuse.outputs.gate_reuse }}
|
||||
# The route, forwarded: the executors and the report read it from here.
|
||||
mode: ${{ needs.route.outputs.mode }}
|
||||
record_artifact: ${{ needs.route.outputs.record_artifact }}
|
||||
full_gate_check: ${{ needs.route.outputs.full_gate_check }}
|
||||
screen_head: ${{ needs.route.outputs.screen_head }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
# A dispatched screen tests the PR's pinned refs/pull/<n>/merge commit; empty for every other run (its own ref).
|
||||
ref: ${{ inputs.screen_merge_sha }}
|
||||
# Preserve full history for merge-base diff selection. Moving the
|
||||
# planner off the eval image must not change its selection inputs.
|
||||
fetch-depth: 0
|
||||
@@ -343,14 +567,14 @@ jobs:
|
||||
env:
|
||||
EVALS_ALL: ${{ (github.event_name == 'workflow_dispatch' && inputs.evals_all) && '1' || '' }}
|
||||
EVALS_CACHE_DIR: ${{ steps.reuse.outputs.reuse == 'on' && '/tmp/gstack-eval-input-cache' || '' }}
|
||||
# Dispatch diff base (CEO-17): the exact merge-base, else the named base branch, else detection.
|
||||
EVALS_BASE: ${{ inputs.base_sha || (inputs.base_ref != '' && format('origin/{0}', inputs.base_ref)) || '' }}
|
||||
# Dispatch diff base (CEO-17): a screen's merge-commit first parent, else the exact merge-base, else the named base branch, else detection.
|
||||
EVALS_BASE: ${{ needs.route.outputs.screen_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'
|
||||
if: github.event_name == 'pull_request' || needs.route.outputs.mode == 'screen'
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
||||
with:
|
||||
@@ -359,7 +583,7 @@ jobs:
|
||||
|
||||
- name: Confirm gate verdict reuse (R2a/R2)
|
||||
id: gate_reuse
|
||||
if: github.event_name == 'pull_request'
|
||||
if: github.event_name == 'pull_request' || needs.route.outputs.mode == 'screen'
|
||||
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 \
|
||||
@@ -430,6 +654,8 @@ jobs:
|
||||
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
# A dispatched screen's pinned merge commit (the planner verified it); empty otherwise.
|
||||
ref: ${{ inputs.screen_merge_sha }}
|
||||
# Full history: files with SELF-derived selection (the LLM-judge
|
||||
# map, routing) walk git at module load, and selection is
|
||||
# fail-closed on git errors — a shallow checkout crashed those
|
||||
@@ -555,6 +781,151 @@ jobs:
|
||||
if-no-files-found: ignore
|
||||
retention-days: 30
|
||||
|
||||
# ── Train bisect (GSTA-49 T1b) ─────────────────────────────────────────
|
||||
# One executor per prefix of the train (route's validated prefix_shas, each
|
||||
# an ancestor of the train head): every red case once, one trial, on that
|
||||
# prefix's own checkout. Each writes the per-prefix record that
|
||||
# `gstack-ship-queue train bisect` reads, as <record_artifact>-bisect-<sha12>.
|
||||
# A bisect never reuses a result and never binds a verdict.
|
||||
train-bisect:
|
||||
runs-on: ubicloud-standard-8
|
||||
needs: [build-image, route]
|
||||
if: ${{ !cancelled() && needs.build-image.result == 'success' && needs.route.result == 'success' && needs.route.outputs.mode == 'bisect' }}
|
||||
env:
|
||||
EVALS_RUN_ID: ci-${{ github.run_id }}-${{ github.run_attempt }}-train-bisect-${{ matrix.slice }}
|
||||
timeout-minutes: ${{ fromJSON(needs.route.outputs.bisect_timeouts)[matrix.slice] }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
container:
|
||||
image: ${{ needs.build-image.outputs.image-tag }}
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
options: --user runner
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 7
|
||||
matrix:
|
||||
slice: ${{ fromJSON(needs.route.outputs.bisect_slices) }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ fromJSON(needs.route.outputs.bisect_refs)[matrix.slice] }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Verify the prefix checkout
|
||||
id: prefix
|
||||
env:
|
||||
PREFIX_SHA: ${{ fromJSON(needs.route.outputs.bisect_refs)[matrix.slice] }}
|
||||
run: |
|
||||
actual=$(git rev-parse HEAD)
|
||||
if [ "$actual" != "$PREFIX_SHA" ]; then
|
||||
echo "::error::train-bisect checked out ${actual}, not its prefix ${PREFIX_SHA}"
|
||||
exit 1
|
||||
fi
|
||||
echo "short=${PREFIX_SHA:0:12}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Fix bun temp
|
||||
uses: ./.github/actions/fix-bun-temp
|
||||
|
||||
- name: Restore deps
|
||||
uses: ./.github/actions/restore-deps
|
||||
|
||||
- run: bun run build
|
||||
|
||||
# Any slice can host a PTY smoke, so the seed/registration steps run
|
||||
# UNCONDITIONALLY (both are idempotent) — the old matrix keyed them on
|
||||
# matrix.suite.name, which a sliced lane cannot do. The register
|
||||
# composite carries the fail-fast dangling-symlink/frontmatter
|
||||
# verification loop, so a moved skill target fails HERE in seconds,
|
||||
# not as a wedged PTY session at the shard wall.
|
||||
- name: Seed claude interactive config
|
||||
uses: ./.github/actions/seed-claude-config
|
||||
with:
|
||||
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
- name: Register gstack skills for PTY smokes
|
||||
uses: ./.github/actions/register-gstack-skills
|
||||
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
||||
with:
|
||||
name: train-bisect-plan
|
||||
path: /tmp/train-bisect
|
||||
|
||||
- name: Authenticate the Codex CLI
|
||||
shell: bash
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
codex="${GSTACK_CI_CODEX_BIN_DIR:?}/codex"
|
||||
export CODEX_HOME="$HOME/.gstack-ci-codex"
|
||||
mkdir -p "$CODEX_HOME"
|
||||
echo "GSTACK_CI_CODEX_HOME=$CODEX_HOME" >> "$GITHUB_ENV"
|
||||
if [ -z "${OPENAI_API_KEY:-}" ]; then
|
||||
echo "::warning::OPENAI_API_KEY is unavailable; Codex eval cases cannot authenticate and will fail."
|
||||
exit 0
|
||||
fi
|
||||
printenv OPENAI_API_KEY | "$codex" login --with-api-key
|
||||
"$codex" login status >/dev/null 2>&1
|
||||
|
||||
- name: Run the red cases on prefix ${{ matrix.slice }}
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
PLAYWRIGHT_BROWSERS_PATH: /opt/playwright-browsers
|
||||
EVALS_JOBS: "1"
|
||||
EVALS_CONCURRENCY: "1"
|
||||
SLICE: ${{ matrix.slice }}
|
||||
run: EVALS_TIER=gate bun run scripts/train-gate.ts bisect-prefix --plan /tmp/train-bisect/bisect.json --index "$SLICE" --out /tmp/train-bisect/out
|
||||
|
||||
- name: Upload the prefix record
|
||||
if: always() && steps.prefix.outputs.short != ''
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||
with:
|
||||
name: ${{ needs.route.outputs.record_artifact }}-bisect-${{ steps.prefix.outputs.short }}
|
||||
path: /tmp/train-bisect/out/bisect-prefix.json
|
||||
if-no-files-found: ignore
|
||||
overwrite: true
|
||||
retention-days: 90
|
||||
|
||||
- name: Upload native capture evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||
with:
|
||||
name: native-captures-${{ env.EVALS_RUN_ID }}
|
||||
include-hidden-files: true
|
||||
path: |
|
||||
~/.gstack/projects/*/e2e-runs
|
||||
~/.gstack/projects/*/evals/qa-callers
|
||||
~/.gstack-dev/e2e-runs
|
||||
~/.gstack-dev/evals/qa-callers
|
||||
if-no-files-found: ignore
|
||||
retention-days: 90
|
||||
|
||||
# The spooled per-shard full logs — a red weekly/PR lane three weeks
|
||||
# later needs more than a summary line.
|
||||
# always(), not failure(): a failed behavior trial is a verdict and no
|
||||
# longer reds its runner, but its full log is the diagnosis evidence.
|
||||
- name: Upload shard logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||
with:
|
||||
name: train-bisect-logs-${{ matrix.slice }}-a${{ github.run_attempt }}
|
||||
include-hidden-files: true
|
||||
# The Fix-bun-temp step points TMPDIR at /home/runner/.cache, so the
|
||||
# runner's spool lands THERE, not /tmp — the original /tmp glob
|
||||
# uploaded nothing and a red slice's diagnostics were unreachable.
|
||||
path: |
|
||||
/home/runner/.cache/gstack-paid-shard-*.log
|
||||
/tmp/gstack-paid-shard-*.log
|
||||
if-no-files-found: ignore
|
||||
retention-days: 30
|
||||
|
||||
slices-report:
|
||||
# GitHub-hosted, like recover-receipts: Ubicloud runners send actions/cache
|
||||
# to Ubicloud's own transparent cache, which a GitHub-hosted restore never
|
||||
@@ -582,6 +953,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
# A screen's pinned merge commit (empty otherwise); a train run needs the
|
||||
# history of its prefix commits T1..Tk for the train record.
|
||||
ref: ${{ inputs.screen_merge_sha }}
|
||||
fetch-depth: ${{ needs.plan-slices.outputs.mode == 'train' && '0' || '1' }}
|
||||
persist-credentials: false
|
||||
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
@@ -671,19 +1046,24 @@ jobs:
|
||||
# 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.
|
||||
# A dispatched screen binds too (purpose screen, its tested merge commit
|
||||
# and policy). Train, bisect and other dispatches never bind for 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'
|
||||
if: always() && (github.event_name == 'pull_request' || needs.plan-slices.outputs.mode == 'screen') && needs.plan-slices.outputs.gate_reuse != 'true'
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || inputs.screen_pr }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha || needs.plan-slices.outputs.screen_head }}
|
||||
TESTED_SHA: ${{ inputs.screen_merge_sha || github.sha }}
|
||||
SCREEN_POLICY: ${{ inputs.screen_policy }}
|
||||
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"
|
||||
--tested-sha "$TESTED_SHA" --profile "$EVALS_PROFILE" --reconcile-exit "$RECONCILE_EXIT" \
|
||||
${SCREEN_POLICY:+--purpose screen --screen-policy "$SCREEN_POLICY"} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload the gate binding
|
||||
if: always() && github.event_name == 'pull_request' && needs.plan-slices.outputs.gate_reuse != 'true'
|
||||
if: always() && (github.event_name == 'pull_request' || needs.plan-slices.outputs.mode == 'screen') && needs.plan-slices.outputs.gate_reuse != 'true'
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||
with:
|
||||
name: gate-binding-a${{ github.run_attempt }}
|
||||
@@ -700,17 +1080,81 @@ jobs:
|
||||
if-no-files-found: ignore
|
||||
retention-days: 90
|
||||
|
||||
# GSTA-49 T1b: a train's own gate run publishes its record, read by
|
||||
# gstack-ship-queue's verdict and verify and by the main backstop: the
|
||||
# census from this run's plan manifest, one panel per planned case, and
|
||||
# the prefix trees T1..Tk modulo stamps. GREEN only for a complete census.
|
||||
- name: Write the train record
|
||||
if: always() && needs.plan-slices.outputs.mode == 'train'
|
||||
env:
|
||||
NONCE: ${{ inputs.nonce }}
|
||||
HEAD_SHA: ${{ inputs.expected_sha }}
|
||||
RECORD: ${{ needs.plan-slices.outputs.record_artifact }}
|
||||
RECONCILE_EXIT: ${{ steps.reconcile.outputs.exit }}
|
||||
run: |
|
||||
bun --no-install run scripts/train-gate.ts train-record /tmp/paid-report --nonce "$NONCE" --head "$HEAD_SHA" \
|
||||
--reconcile-exit "$RECONCILE_EXIT" --run-id "$GITHUB_RUN_ID" --run-attempt "$GITHUB_RUN_ATTEMPT" --repo "$GITHUB_REPOSITORY" \
|
||||
--out "/tmp/train-record/${RECORD}.json" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload the train record
|
||||
if: always() && needs.plan-slices.outputs.mode == 'train'
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||
with:
|
||||
name: ${{ needs.plan-slices.outputs.record_artifact }}
|
||||
path: /tmp/train-record
|
||||
if-no-files-found: ignore
|
||||
overwrite: true
|
||||
retention-days: 90
|
||||
|
||||
- name: Fail the workflow when reconciliation failed
|
||||
if: (steps.reconcile.outputs.exit || steps.reused.outputs.exit) != '0'
|
||||
run: exit 1
|
||||
|
||||
# GSTA-49 T1b: the check run gstack-ship-queue requires on the train head,
|
||||
# published only for a complete GREEN census. Its own job with checks:
|
||||
# write and no repository code (the same split as slices-comment).
|
||||
train-check:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [plan-slices, slices-report]
|
||||
if: ${{ !cancelled() && needs.plan-slices.outputs.mode == 'train' && needs.slices-report.result == 'success' }}
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
checks: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
||||
with:
|
||||
name: ${{ needs.plan-slices.outputs.record_artifact }}
|
||||
path: /tmp/train-record
|
||||
|
||||
- name: Publish the full-gate check on the train head
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CHECK: ${{ needs.plan-slices.outputs.full_gate_check }}
|
||||
RECORD: ${{ needs.plan-slices.outputs.record_artifact }}
|
||||
NONCE: ${{ inputs.nonce }}
|
||||
HEAD_SHA: ${{ inputs.expected_sha }}
|
||||
run: |
|
||||
record="/tmp/train-record/${RECORD}.json"
|
||||
if ! jq -e --arg nonce "$NONCE" --arg head "$HEAD_SHA" \
|
||||
'.schema == 1 and .kind == "train-gate" and .verdict == "GREEN" and .census.complete == true and .nonce == $nonce and .head_sha == $head' \
|
||||
"$record" >/dev/null 2>&1; then
|
||||
echo "::error::the train record is not a complete GREEN census of ${HEAD_SHA} (nonce ${NONCE}); no ${CHECK} check run"
|
||||
exit 1
|
||||
fi
|
||||
summary=$(jq -r '"rule \(.census.executed.rule)/\(.census.planned.rule), judge \(.census.executed.judge)/\(.census.planned.judge); prefixes T1..T\(.prefixes | length)"' "$record")
|
||||
gh api "repos/${GITHUB_REPOSITORY}/check-runs" -X POST -f name="$CHECK" -f head_sha="$HEAD_SHA" -f status=completed -f conclusion=success \
|
||||
-f details_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
-f "output[title]=Train ${NONCE}: complete GREEN census" -f "output[summary]=${summary}" >/dev/null
|
||||
echo "- ${CHECK}: success on ${HEAD_SHA} (${summary})" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# PR comment in its OWN job with the write token and ZERO repo code: no
|
||||
# checkout, no bun install — only downloaded artifacts, jq, and gh. See the
|
||||
# trust-boundary note on slices-report.
|
||||
slices-comment:
|
||||
runs-on: ubicloud-standard-2
|
||||
needs: slices-report
|
||||
if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && needs.slices-report.result != 'skipped' }}
|
||||
# A dispatched screen comments on its PR too (screen_pr was validated by the route job, or nothing ran).
|
||||
if: ${{ !cancelled() && ((github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || inputs.screen_pr != '') && needs.slices-report.result != 'skipped' }}
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
pull-requests: write
|
||||
@@ -739,6 +1183,9 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RECONCILE_EXIT: ${{ needs.slices-report.outputs.reconcile-exit }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || inputs.screen_pr }}
|
||||
SCREEN_POLICY: ${{ inputs.screen_policy }}
|
||||
SCREEN_MERGE_SHA: ${{ inputs.screen_merge_sha }}
|
||||
run: |
|
||||
# shellcheck disable=SC2086,SC2059
|
||||
# R2a/R2: a confirmed verdict reuse ran no case; say which run,
|
||||
@@ -845,7 +1292,13 @@ jobs:
|
||||
|
||||
fi
|
||||
|
||||
COMMENT_ID=$(gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments \
|
||||
if [ -n "$SCREEN_POLICY" ]; then
|
||||
BODY="${BODY}
|
||||
|
||||
*Dispatched ${SCREEN_POLICY} screen (gstack-ship-queue ready --screen) of merge commit \`${SCREEN_MERGE_SHA}\`.*"
|
||||
fi
|
||||
|
||||
COMMENT_ID=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
|
||||
--jq '.[] | select(.body | startswith("## E2E Evals")) | .id' | tail -1)
|
||||
|
||||
if [ -n "$COMMENT_ID" ]; then
|
||||
@@ -855,6 +1308,6 @@ jobs:
|
||||
# REST, not gh's pr-comment subcommand: this job runs with NO
|
||||
# checkout (the token/exec split), and that subcommand resolves
|
||||
# the repo FROM git — it dies with "not a git repository" here.
|
||||
gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \
|
||||
gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
|
||||
-X POST -f body="$BODY"
|
||||
fi
|
||||
Reference in new issue
Block a user