Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 27 additions & 27 deletions .github/workflows/gate-topology.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ on:
# out. `tri gates unmeasured` reports the gap; this is what closes it.
workflow_dispatch:
inputs: {prs: {description: "claw-batch only: N:headsha pairs of specs/port pull requests to fold into one batch", required: false, default: "", type: string}}
# Only the claw-t27f job below runs on this schedule (#10268).
# Only claw-t27f (#10268) and claw-merge (#10973) run on this schedule.
schedule: [{cron: "*/10 * * * *"}]

# A run for a commit that has since been superseded measures nothing anyone
Expand Down Expand Up @@ -171,45 +171,45 @@ jobs:
cat /tmp/stats >> "$GITHUB_STEP_SUMMARY"
if [ -s /tmp/review.json ]; then gh api -X POST "repos/$R/pulls/$N/reviews" --input /tmp/review.json -q .html_url >> "$GITHUB_STEP_SUMMARY"; fi

# #10397: after a push to master, bring every conflict-free open PR that fell behind up to date.
# The decision (which PRs, which order, the cap of 5, the queue limit of 150, every skip) is specs/claw/refresh.t27 lowered
# to gen/c/claw/refresh.c; this job lists, feeds the C, and calls the endpoint for each `R` line.
# The token is the update-branch row of specs/claw/github_surface.t27: pull_requests write and
# nothing else. If GitHub answers 403 here, the App also needs contents:write for update-branch
# (REST docs); the failure is a warning with GitHub's own text, never a red job.
claw-refresh:
if: github.event_name == 'push'
# The lane's tick (#10973): merge_judge.t27 (merge) and refresh.t27 (update-branch) decide, this job lists, feeds gen/c/claw/merge_judge.c and applies; plan-only unless CLAW_MERGE_LIVE is 1.
claw-merge:
if: github.event_name != 'pull_request'
runs-on: ubuntu-24.04
permissions: {contents: read, pull-requests: read, actions: read}
concurrency: {group: claw-refresh, cancel-in-progress: true}
permissions: {contents: write, pull-requests: write, actions: read}
concurrency: {group: claw-merge, cancel-in-progress: false}
steps:
- id: have
env: {K: "${{ secrets.CLAW_APP_PRIVATE_KEY }}", I: "${{ vars.CLAW_APP_ID || secrets.CLAW_APP_ID }}"}
run: if [ -n "$K" ] && [ -n "$I" ]; then echo ok=1 >> "$GITHUB_OUTPUT"; else echo "claw/refresh updated nothing - secret CLAW_APP_PRIVATE_KEY or CLAW_APP_ID (variable or secret) is not set (docs/claw/README.md)." >> "$GITHUB_STEP_SUMMARY"; fi
- {id: tok, if: steps.have.outputs.ok, continue-on-error: true, uses: actions/create-github-app-token@v2, with: {app-id: "${{ vars.CLAW_APP_ID || secrets.CLAW_APP_ID }}", private-key: "${{ secrets.CLAW_APP_PRIVATE_KEY }}", owner: gHashTag, repositories: t27, permission-pull-requests: write}}
- {if: steps.tok.outcome == 'failure', run: echo "claw/refresh updated nothing - the TRI CLAW App could not mint a token with pull_requests write; grant it and accept it on the installation - docs/claw/README.md steps 1-2." >> "$GITHUB_STEP_SUMMARY"}
- {if: steps.tok.outputs.token, uses: actions/checkout@v6, with: {persist-credentials: false}}
- if: steps.tok.outputs.token
env: {GH_TOKEN: "${{ github.token }}", CLAW_TOKEN: "${{ steps.tok.outputs.token }}", R: "${{ github.repository }}"}
- {id: tok, if: steps.have.outputs.ok, continue-on-error: true, uses: actions/create-github-app-token@v2, with: {app-id: "${{ vars.CLAW_APP_ID || secrets.CLAW_APP_ID }}", private-key: "${{ secrets.CLAW_APP_PRIVATE_KEY }}", owner: gHashTag, repositories: t27, permission-pull-requests: write, permission-contents: write}}
- {if: steps.tok.outcome == 'failure', run: echo "claw/refresh updated nothing - the TRI CLAW App could not mint a token with pull_requests and contents write; grant it and accept it on the installation - docs/claw/README.md steps 1-2." >> "$GITHUB_STEP_SUMMARY"}
- {uses: actions/checkout@v6, with: {persist-credentials: false}}
- env: {GH_TOKEN: "${{ github.token }}", CLAW_TOKEN: "${{ steps.tok.outputs.token }}", R: "${{ github.repository }}", E: "${{ github.event_name }}", LIVE: "${{ vars.CLAW_MERGE_LIVE }}"}
run: |
printf '#include "gen/c/claw/refresh.c"\n#include <stdio.h>\n#include <stdlib.h>\nint main(int c,char**v){size_t k=1<<22,n;uint8_t*b=malloc(k),*o=malloc(k);n=fread(b,1,k,stdin);if(n==k)return 3;size_t m=refresh_plan(b,n,o,k,atoi(v[1]));if(m>k)return 4;fwrite(o,1,m,stdout);return 0;}\n' | cc -O1 -w -I . -x c -o /tmp/feed -
skip() { echo "::warning::claw/refresh $1"; echo "claw/refresh updated nothing: $1" >> "$GITHUB_STEP_SUMMARY"; exit 0; }
# Pages stay small (a page of 100 PRs with 50 labels each came back as HTML). A failed page fails the jq step: try three times, keep nothing of a failed try.
list() { for t in 1 2 3; do gh api graphql --paginate -F owner="${R%/*}" -F name="${R#*/}" -f query='query($owner:String!,$name:String!,$endCursor:String){repository(owner:$owner,name:$name){pullRequests(states:OPEN,first:30,after:$endCursor,orderBy:{field:CREATED_AT,direction:ASC}){pageInfo{hasNextPage endCursor} nodes{number headRefOid createdAt isDraft isCrossRepository maintainerCanModify mergeable mergeStateStatus baseRefName headRepository{nameWithOwner} autoMergeRequest{enabledAt} labels(first:10){nodes{name}}}}}}' --jq '.data.repository.pullRequests.nodes[]|[.number,.headRefOid,.createdAt,.isDraft,([.labels.nodes[].name]|join(",")),.isCrossRepository,(.headRepository.nameWithOwner//""),.maintainerCanModify,.mergeable,.mergeStateStatus,.baseRefName,(.autoMergeRequest!=null)]|@tsv' > /tmp/page 2>/dev/null </dev/null && cat /tmp/page && return 0; sleep $((t * 10)); done; return 1; }
# GitHub computes mergeability lazily after a push: ask once, wait, ask again.
printf '#include "gen/c/claw/merge_judge.c"\n#include <stdio.h>\n#include <stdlib.h>\nint main(int c,char**v){size_t k=1<<24,n;uint8_t*b=malloc(k),*o=malloc(k);n=fread(b,1,k,stdin);if(n==k)return 3;size_t m=lane_plan(atoi(v[1]),b,n,o,k,atoi(v[2]));if(m>k)return 4;fwrite(o,1,m,stdout);return 0;}\n' | cc -O1 -w -I . -x c -o /tmp/feed -
skip() { echo "::warning::claw-merge $1"; echo "claw-merge stopped: $1" >> "$GITHUB_STEP_SUMMARY"; exit 0; }
row='def row: if .__typename=="CheckRun" then [((.checkSuite.workflowRun.workflow.name // "")|if .=="" then "" else .+"/" end)+.name,.databaseId,.status,(.conclusion // "-")] else [.context,"-","CONTEXT",.state] end;'
list() { for t in 1 2 3; do gh api graphql --paginate -F owner="${R%/*}" -F name="${R#*/}" -f query='query($owner:String!,$name:String!,$endCursor:String){repository(owner:$owner,name:$name){pullRequests(states:OPEN,first:15,after:$endCursor,orderBy:{field:CREATED_AT,direction:ASC}){pageInfo{hasNextPage endCursor} nodes{number headRefOid headRefName createdAt isDraft isCrossRepository maintainerCanModify mergeable mergeStateStatus baseRefName changedFiles headRepository{nameWithOwner} autoMergeRequest{enabledAt} labels(first:10){nodes{name}} closingIssuesReferences(first:1){totalCount} commits(last:1){nodes{commit{oid committedDate statusCheckRollup{contexts(first:100){totalCount nodes{__typename ... on CheckRun{name status conclusion databaseId checkSuite{workflowRun{workflow{name}}}} ... on StatusContext{context state}}}}}}}}}}}' --jq "$row"'.data.repository.pullRequests.nodes[]|. as $p|$p.commits.nodes[0].commit as $c|($c.statusCheckRollup.contexts // {totalCount:0,nodes:[]}) as $x|([$p.labels.nodes[].name]|join(",")) as $l|(["R",$p.number,$p.headRefOid,$p.createdAt,$p.isDraft,$l,$p.isCrossRepository,($p.headRepository.nameWithOwner//""),$p.maintainerCanModify,$p.mergeable,$p.mergeStateStatus,$p.baseRefName,($p.autoMergeRequest!=null)]|@tsv),(["P",$p.number,$p.headRefOid,$p.isDraft,$l,$p.headRefName,$p.baseRefName,$p.mergeable,$p.mergeStateStatus,$p.closingIssuesReferences.totalCount,$c.oid,((now-($c.committedDate|fromdate))|floor|if .<0 then "-" else . end),$x.totalCount,$p.changedFiles]|@tsv),($x.nodes[]|row|["K"]+.|@tsv)' > /tmp/page 2>/dev/null </dev/null && cat /tmp/page && return 0; sleep $((t * 10)); done; return 1; }
list > /dev/null || :; sleep 30
list > /tmp/prs.tsv || skip "the pull request listing failed three times, so no plan was made."
# Every update-branch starts about 26 workflows: refresh.t27 decides from the queued-run count, and an unread count is -1.
list > /tmp/in || skip "the pull request listing failed three times, so no plan was made."
gh api graphql -F owner="${R%/*}" -F name="${R#*/}" -f query='query($owner:String!,$name:String!){repository(owner:$owner,name:$name){ref(qualifiedName:"refs/heads/master"){target{... on Commit{history(first:20){nodes{statusCheckRollup{contexts(first:100){nodes{__typename ... on CheckRun{name status conclusion databaseId checkSuite{workflowRun{workflow{name}}}} ... on StatusContext{context state}}}}}}}}}}}' --jq "$row"'.data.repository.ref.target.history.nodes[]|(.statusCheckRollup.contexts.nodes // [])[]|row|["B"]+.|@tsv' > /tmp/b 2>/dev/null </dev/null || echo "::warning::claw-merge could not read master's last checks: no red check is inherited"
plan() { cat /tmp/b /tmp/in "$@" | /tmp/feed 0 0 > /tmp/mplan || skip "could not build a merge plan."; }; plan
while IFS=$'\t' read -r k a b c; do [ "$k" = L ] || continue
[ "$a" = "$p" ] || { p=$a; rm -f /tmp/f; gh api --paginate "repos/$R/pulls/$a/files" -q '.[].filename' > /tmp/f </dev/null && awk -v a="$a" -v OFS='\t' '{print "F",a,$0}' /tmp/f; } || continue
gh api "repos/$R/actions/jobs/$c/logs" > /tmp/log 2>/dev/null </dev/null && { grep -oFf /tmp/f /tmp/log | sort -u || :; } | awk -v a="$a" -v c="$b" -v OFS='\t' '{print "O",a,c,$0; n++} END{if(!n) print "O",a,c,"-"}'
done < /tmp/mplan > /tmp/x; plan /tmp/x
while IFS=$'\t' read -r k a b; do case "$k" in G) if [ "$LIVE" != 1 ]; then echo "- would merge #$a at $b (plan only: CLAW_MERGE_LIVE is not 1)" >> "$GITHUB_STEP_SUMMARY"; elif err=$(GH_TOKEN="${CLAW_TOKEN:-$GH_TOKEN}" gh api -X PUT "repos/$R/pulls/$a/merge" -f merge_method=squash -f sha="$b" 2>&1 </dev/null); then echo "- merged #$a at $b" >> "$GITHUB_STEP_SUMMARY"; echo "$a" >> /tmp/merged; else echo "::warning::claw-merge could not merge #$a: $err"; fi;;
M) printf '%s\n' "$a" >> "$GITHUB_STEP_SUMMARY";; W|H|N) printf -- '- %s #%s: %s\n' "$k" "$a" "$b" >> "$GITHUB_STEP_SUMMARY";; esac; done < /tmp/mplan
[ "$E" = push ] && [ -n "$CLAW_TOKEN" ] || exit 0
q=$(gh api "repos/$R/actions/runs?status=queued&per_page=1" -q .total_count 2>/dev/null </dev/null) || q=-1; case "$q" in ''|*[!0-9]*) q=-1;; esac
# Copy the facts; behind_by is the one thing the listing lacks (the ruleset is not strict, so no PR is ever reported BEHIND).
while IFS= read -r line; do
grep '^R' /tmp/in | cut -f2- | while IFS= read -r line; do
sha=$(printf '%s' "$line" | cut -f2); base=$(printf '%s' "$line" | cut -f11)
bh=$(gh api "repos/$R/compare/$base...$sha" -q .behind_by 2>/dev/null </dev/null) || bh=-
printf '%s\t%s\t%s\n' "$(printf '%s' "$line" | cut -f1-10)" "${bh:--}" "$(printf '%s' "$line" | cut -f12)"
done < /tmp/prs.tsv | /tmp/feed "$q" > /tmp/plan || skip "could not build a plan."
done | /tmp/feed 1 "$q" > /tmp/plan || skip "could not build a plan."
while IFS=$'\t' read -r k a b; do
case "$k" in
R) if err=$(GH_TOKEN="$CLAW_TOKEN" gh api -X PUT "repos/$R/pulls/$a/update-branch" -f expected_head_sha="$b" 2>&1 </dev/null); then echo "claw/refresh: #$a updated"; else echo "::warning::claw/refresh could not update #$a: $err"; fi; sleep 1;;
R) grep -sqx "$a" /tmp/merged && continue; if err=$(GH_TOKEN="$CLAW_TOKEN" gh api -X PUT "repos/$R/pulls/$a/update-branch" -f expected_head_sha="$b" 2>&1 </dev/null); then echo "claw/refresh: #$a updated"; else echo "::warning::claw/refresh could not update #$a: $err"; fi; sleep 1;;
M) printf '%s\n' "$a" >> "$GITHUB_STEP_SUMMARY";;
esac
done < /tmp/plan
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/spec-guards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,11 @@ jobs:
./target/release/t27c gen-c specs/claw/sarif_feed.t27 > gen/c/claw/sarif_feed.c
./target/release/t27c gen-c specs/claw/suggest_feed.t27 > gen/c/claw/suggest_feed.c
./target/release/t27c gen-c specs/claw/refresh.t27 > gen/c/claw/refresh.c
./target/release/t27c gen-c specs/claw/ledger_union.t27 > gen/c/claw/ledger_union.c
./target/release/t27c gen-c specs/claw/ledger_union.t27 > gen/c/claw/ledger_union.c; ./target/release/t27c gen-c specs/claw/merge_judge.t27 > gen/c/claw/merge_judge.c
./target/release/t27c gen-c specs/policy/secret_gate.t27 > gen/c/policy/secret_gate.c
./target/release/t27c gen-rust specs/tri/fmt_source.t27 > bootstrap/gen/rust/tri/fmt_source.rs
./target/release/t27c gen-json specs/igla/own_model_board.t27 > gen/json/igla/own_model_board.json
git diff --exit-code -- gen/c/claw/gates.c gen/c/claw/t27f_gate.c gen/c/claw/sarif_feed.c gen/c/claw/suggest_feed.c gen/c/claw/refresh.c gen/c/claw/ledger_union.c gen/c/policy/secret_gate.c bootstrap/gen/rust/tri/fmt_source.rs gen/json/igla/own_model_board.json
git diff --exit-code -- gen/c/claw/gates.c gen/c/claw/t27f_gate.c gen/c/claw/sarif_feed.c gen/c/claw/suggest_feed.c gen/c/claw/refresh.c gen/c/claw/ledger_union.c gen/c/claw/merge_judge.c gen/c/policy/secret_gate.c bootstrap/gen/rust/tri/fmt_source.rs gen/json/igla/own_model_board.json

# #7565: test only the specs this change can affect. specs/ci/affected.t27 picks them (the
# changed specs and every spec that imports one through `use`, transitively); the graph is the
Expand Down
61 changes: 61 additions & 0 deletions .trinity/seals/claw_ClawMergeJudge.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"built_by": "t27c-bootstrap@0.5.2+b4bf811ac",
"closure": [
{
"spec": "specs/claw/check_latest.t27",
"spec_hash": "sha256:2de14a373379d1a5acc19fffeb4123c951dc6f36caffb883fb5a8a406be65d58"
},
{
"spec": "specs/claw/check_state.t27",
"spec_hash": "sha256:bd3aa36047755e7b95144ca4bbdb760aa971ceeb94b7e0728dd4db49b842ead6"
},
{
"spec": "specs/claw/queue_cost.t27",
"spec_hash": "sha256:9e5cd5e46f28ac23cf36a555a4f416bcabb8858d48fdd79afaa4743aa33e4106"
},
{
"spec": "specs/claw/red_origin.t27",
"spec_hash": "sha256:ca93d2b89d539b2f811f8d42465df012dc2c664791fe2121f0809214b576bcbe"
},
{
"spec": "specs/claw/refresh.t27",
"spec_hash": "sha256:6813191699e5c1bb152bb28126a34e87cf317fcfaf3c60c33c387ecd63af1df7"
},
{
"spec": "specs/claw/required_red.t27",
"spec_hash": "sha256:585ddbdea9f98d3b4a45700e47cf6706528339874d1adfce796828b6804b5866"
},
{
"spec": "specs/claw/sarif.t27",
"spec_hash": "sha256:f9c08d09fafe8c0e5665c3b5b803949b8c7f63432a66c3f1846f6731a15ffbdc"
},
{
"spec": "specs/claw/text.t27",
"spec_hash": "sha256:968755ec1917f766eb2cdd0ab4c0c48aef028a0e7d69e191911695e925b286ad"
}
],
"config": "gen=c,rust,verilog,zig;tests=zig",
"gen_hash_c": "sha256:960c99e70c02fd96bf64da3b77bba42f062efaa7e0310144aef81874ac8f9946",
"gen_hash_rust": "sha256:35f509afeb83e8548fc4839ffe8594f018f8cbb1d8ac81e53f114f63883685d6",
"gen_hash_verilog": "sha256:167cfe9d28bb76ac70218c180038e5a99a6caddbdb909b0d341155cbe4ad822e",
"gen_hash_zig": "sha256:cc5b29b0767ea02d0b469c849a98a8fd35162e78c32a49ec9e23b563fb2ebeab",
"module": "ClawMergeJudge",
"ring": 12,
"seal_schema": 2,
"sealed_at": "2026-10-19T10:23:45Z",
"sealed_by": "t27c-bootstrap@0.5.2",
"spec_hash": "sha256:614a28f7501e4eafa175b3250bf4c28ce3476dfeaf14a8c51a5a94087f3d39c7",
"spec_path": "specs/claw/merge_judge.t27",
"tests": {
"failed": 0,
"failing": [],
"forced": false,
"passed": 67,
"total": 67,
"vacuous": 0
},
"toolchain": {
"test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10",
"zig": "0.16.0"
}
}
Loading
Loading