From c97e64286002a3d40486aa9ab3281a5caccec906 Mon Sep 17 00:00:00 2001 From: rrs <276464689+robotrocketscience@users.noreply.github.com> Date: Sun, 10 May 2026 18:11:25 -0700 Subject: [PATCH 1/2] fix(merge-train): verify signatures via GitHub API instead of %G? (#619) The previous check used `git log --format='%G?'`, which requires the runner to have `gpg.format=ssh` and `gpg.ssh.allowedSignersFile` configured for SSH-signed commits to register as 'G'. actions/checkout sets neither, so every SSH signature returned 'N' (no signature) and every PR labeled `ready-to-merge` since #602 shipped got unlabeled. Switch to `gh api repos/.../commits/` and read `.commit.verification.verified`. That is the same source of truth the `required_signatures` branch protection rule uses downstream, so any commit the workflow accepts will also pass the push gate. The fail-message wording shifts to 'not signed (per GitHub verification API)' so future debugging points at the right oracle. --- .github/workflows/merge-train.yml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/merge-train.yml b/.github/workflows/merge-train.yml index b6cbbd089..707077ef7 100644 --- a/.github/workflows/merge-train.yml +++ b/.github/workflows/merge-train.yml @@ -103,9 +103,25 @@ jobs: # be signed. The bot can't sign, but it isn't adding commits # here — these are the author's. Verify upfront so a quiet # branch-protection rejection downstream doesn't confuse. - unsigned=$(git log --format='%H %G?' "origin/main..origin/${HEAD_REF}" | awk '$2!="G" && $2!="U" {print $1}' || true) + # + # Use GitHub's verification API rather than `git log --format=%G?`: + # %G? requires local `gpg.ssh.allowedSignersFile` to recognise + # SSH signatures, which actions/checkout does not set. GitHub's + # API is the same source of truth `required_signatures` branch + # protection uses downstream, so any commit it reports as + # verified will also pass the push gate. + unsigned="" + while IFS= read -r sha; do + [ -z "${sha}" ] && continue + verified=$(gh api "repos/${REPO}/commits/${sha}" \ + --jq '.commit.verification.verified // false' 2>/dev/null || echo "false") + if [ "${verified}" != "true" ]; then + unsigned="${unsigned}${sha}"$'\n' + fi + done < <(git log --format='%H' "origin/main..origin/${HEAD_REF}") + unsigned=${unsigned%$'\n'} if [ -n "${unsigned}" ]; then - fail_and_unlabel "one or more commits between main and \`${HEAD_REF}\` are not GPG/SSH-signed:\n\n\`\`\`\n${unsigned}\n\`\`\`\n\nSign them locally and re-add the label. The bot cannot sign on your behalf." + fail_and_unlabel "one or more commits between main and \`${HEAD_REF}\` are not signed (per GitHub verification API):\n\n\`\`\`\n${unsigned}\n\`\`\`\n\nSign them locally and re-add the label. The bot cannot sign on your behalf." fi echo "[4/5] waiting for required checks to complete..." From b2657537cabe41afb61e02e809a05f5c5120b114 Mon Sep 17 00:00:00 2001 From: rrs <276464689+robotrocketscience@users.noreply.github.com> Date: Sun, 10 May 2026 18:11:52 -0700 Subject: [PATCH 2/2] docs(changelog): unreleased entry for #619 merge-train signature fix --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2b7fbf4f..f8de604db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ installable release; see the roadmap in [README.md](README.md). ## [Unreleased] +### Fixed + +- **`merge-train` signature check rejected every signed PR** ([#619](https://github.com/robotrocketscience/aelfrice/issues/619)). The check shipped in #602 used `git log --format='%G?'`, which only registers SSH signatures as `G` when the runner has `gpg.format=ssh` and `gpg.ssh.allowedSignersFile` configured — `actions/checkout` sets neither, so every SSH-signed commit fell through to GPG parsing and `%G?` returned `N`. Every PR labeled `ready-to-merge` since #602 shipped was unlabeled with a spurious "not signed" comment, and `main` only advanced via manual operator FF pushes. Replaced with `gh api repos/.../commits/ --jq '.commit.verification.verified'`, the same source of truth the `required_signatures` branch protection rule uses downstream. Any commit the workflow accepts will also pass the push gate. + ### Added - **HRR persistence — split-format save/load on `HRRStructIndex`** ([#553](https://github.com/robotrocketscience/aelfrice/issues/553)). `HRRStructIndex.save(path)` now writes a per-store directory containing `struct.npy` (the `(N, dim)` float64 matrix, mmap-able) plus `meta.npz` (the small metadata blob: belief ids, role/id vectors, dim, seed, layout version) instead of a single bundled `.npz`. Writes are atomic via temp-file + `os.replace` so a reader process never observes a partial write. `HRRStructIndex.load(path)` auto-detects the layout: a directory with the two split-format files loads the new layout; a file path falls through to the v1.7 bundled `.npz` reader and emits a one-shot module-logger deprecation warning (legacy stores still load — this is a substrate landing, not a breaking change). New `mmap=False` keyword on `load()` requests `np.load(mmap_mode='r')` on the struct matrix; default off keeps existing callers byte-stable. The split layout is the substrate that makes persistence-default-ON viable per `docs/feature-hrr-integration.md` — `np.load(.npz, mmap_mode='r')` is silently ignored per the numpy docs, so mmap requires this format change. Sub-pieces (`HRRStructIndexCache` mmap read path, ephemeral-path auto-disable, `aelf doctor` rows, `AELFRICE_HRR_PERSIST` opt-out flag) ship as separate atomic PRs per the spec's "Coordination" section.