Skip to content

ci: harden CI against silent hangs — timeouts, asn1c caching, failures - #61

Merged
Thecave3 merged 7 commits into
mainfrom
60-ci-hardening
Aug 19, 2026
Merged

ci: harden CI against silent hangs — timeouts, asn1c caching, failures#61
Thecave3 merged 7 commits into
mainfrom
60-ci-hardening

Conversation

@Thecave3

@Thecave3 Thecave3 commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Type of change

  • Bug fix
  • New feature / enhancement
  • New Service Model
  • Refactor (no behavior change)
  • Documentation
  • Test / CI / packaging
  • Other (explain):

Linked issue

Refs #60 (partial). Deliberately not "Closes #60": this issue covers more than this PR alone resolves — #62 fixes the actual E2E/Topologies deadlock, and latrec-portability.yml's missing timeout is being added directly on PR #59's branch by its author. Closing #60 is left to a maintainer once everything lands.

Mandatory test checklist

  • ./build_libe3 -c -d build -j $(nproc) -r -t passes (Release build + tests) — verified locally, 17/17 tests passed
  • ./build_libe3 -c -d build -j $(nproc) -g -t passes (Debug build + tests) — verified locally, 17/17 tests passed
  • cd build && ctest --output-on-failure is clean
  • MPMC queue benchmark (./build/test_bench_mpmc_queue) shows no regression vs main — ran locally (correctness stress tests all pass); neither commit touches include/libe3/mpmc_queue.hpp so no regression is possible
  • VERSION bumped per SemVer — N/A, no public API/ABI change
  • ./build_libe3 --docs — N/A, no include/ changes
  • New build dependencies installed by ./build_libe3 -I — N/A, no new dependencies added; install_asn1c_from_source()'s error-handling/caching behavior changed, not its dependency list
  • libe3.pc interface — N/A, unchanged

CI checklist

  • Unit Tests workflow is green — will confirm once this PR's own run completes (see "Observed vs. reasoned" below)
  • Commit policy workflow is green — will confirm once this PR's own run completes
  • MPMC Queue Benchmark — this PR does touch mpmc-bench.yml itself (adding a timeout/cache step), so its paths: allowlist means it will run live on this PR; will confirm it stays green

Twin-repo coordination

  • This PR does not change the E3 wire protocol or public ABI, OR a paired PR exists in each affected twin repo (link below).

Paired PR(s): none needed — CI-only change.

Workflow confirmation

  • My branch is a linear, fast-forward-able descendant of main (rebased if main moved), with no merge commits.
  • Every commit builds and passes tests on its own (atomic, git bisect-safe) with a descriptive message.
  • I have read and followed CONTRIBUTING.md.

Observed vs. reasoned

  • Observed: the build_libe3 commit (1st commit) — verified against isolated fixtures (a throwaway local git repo standing in for the real asn1c upstream, no network/sudo needed): a failing checkout now aborts immediately with the real error surfaced via tail instead of a swallowed || true; a simulated successful build writes the binary and a matching stamp; a matching stamp+binary short-circuits with zero rebuild attempted; a present binary with a mismatched stamp still triggers a full rebuild (protects commit-trailers.yml's per-commit-build job, which checks out several commits in the same container). Also ran a full local Release and Debug build + ctest (17/17 both) with asn1c already installed on the dev machine, exercising the short-circuit path, plus the MPMC benchmark's correctness suite.
  • Reasoned, not yet observed: the 2nd commit (workflow YAML) is CI-only — the repo's own "Detect code changes" gate classifies .github/workflows/*.yml as non-code, so it doesn't exercise the real build/test jobs on its own. Its mechanics (timeout values, the needs:-sequenced asn1c-cache warm-up jobs in the three matrix-heavy files, the cache key/permissions handling, the branch-filter/concurrency changes) are reasoned from the diff and cross-checked with a YAML parser plus a job-by-job timeout-minutes audit script, not yet watched in a live run. Will report back once this PR's own CI has actually run.

…rrors

install_asn1c_from_source() unconditionally rebuilt asn1c from source on
every job and wrapped the whole sequence in `(...) > log 2>&1 || true`,
with every individual command inside also guarded by its own `|| true`.
On PR #52 this meant a hung dependency install ran silently for six
hours (GitHub's hard job ceiling) with zero diagnostic output, and a
failed checkout of the pinned commit would have gone unnoticed too,
silently building an unpinned tree.

Add a short-circuit, keyed on a stamp file recording which commit is
currently installed (not just binary presence, so a stale /opt/asn1c
from a different pin is rebuilt rather than trusted), so an
already-populated /opt/asn1c matching the pinned commit -- e.g.
restored from an actions/cache entry, wired up in the next commit --
skips the rebuild entirely. The stamp also protects
commit-trailers.yml's per-commit-build job, which checks out several
commits in the same container: without it, an earlier commit pinning a
different asn1c revision would silently reuse whatever happened to
already be on disk.

Remove the per-command `|| true` on the steps that must be able to
fail the build (checkout, autoreconf, configure, make, make install),
and capture the subshell's real exit status via `set +e` / inner
`set -e` around it, printing the last 200 lines of the install log
before propagating the failure. `sudo ldconfig` is dropped too:
confirmed from the pinned fork's Makefile.am that `make install` only
ever populates /opt/asn1c/bin and /opt/asn1c/share, never a shared
library, so it was a no-op.

Verified against isolated fixtures (a throwaway local git repo
standing in for the real asn1c upstream, so no network/sudo needed):
a failing checkout aborts immediately with the real error surfaced via
tail instead of a swallowed `|| true`; a simulated successful build
writes the binary and a matching stamp; a matching stamp+binary
short-circuits with no rebuild attempted; and a present binary with a
mismatched stamp still triggers a full rebuild. Separately, a full
Release build + `ctest` (17/17) passes unchanged on this machine's
already-installed asn1c.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Andrea Lacava <a.lacava@northeastern.edu>
…ures

Sized from measured passing job durations (GitHub Actions API,
successful runs only) with ~2-3x headroom: add timeout-minutes to
every job across pr-tests.yml, commit-trailers.yml,
full-loop-bench.yml, mpmc-bench.yml, and tag-and-release.yml that had
none at all. On PR #52 "Build and Test (ubuntu-latest, Debug)" burned
GitHub's full 6-hour job ceiling inside dependency install; the values
here would have caught it within minutes. e2e-dapp-test.yml and
e2e-topologies.yml already had timeouts on their main jobs, but their
"Post PR comment" aggregation jobs did not - fixed too.

Cache the from-source asn1c build (actions/cache, keyed on
`hashFiles('build_libe3')` plus runner os/arch so the pinned commit
bump in the previous commit auto-invalidates every workflow's cache
with no YAML edits, and amd64/arm64 never share an entry) instead of
switching to the distro package: Ubuntu's asn1c is 0.9.28+dfsg-5 from
upstream vlm/asn1c, a different fork and version than the pinned
mouse07410/asn1c commit this codebase's 0.9.29-era assumptions rely
on. `/opt` is root:root on GitHub-hosted runners, so a `sudo mkdir -p
&& chown` step has to run before the cache action can create/restore
into /opt/asn1c on a fresh VM. In pr-tests.yml, e2e-dapp-test.yml, and
e2e-topologies.yml - the three files where several matrix legs would
otherwise race to rebuild asn1c in parallel on a cold cache - add an
upfront `asn1c-cache` job the matrix jobs `needs:`, so the cache is
always warm by the time they start instead of every leg paying the
cost independently.

Remove the workflow-level `./build_libe3 -I || true` in the 6 places
that had one (pr-tests.yml's 4 build jobs, commit-trailers.yml's
per-commit-build, mpmc-bench.yml's benchmark), so the previous
commit's now-honest script failures actually redden the CI step
instead of being masked a second time at the workflow level.

Drop the `branches: [main]` filter from `pull_request:` triggers in
pr-tests.yml, full-loop-bench.yml, mpmc-bench.yml, e2e-dapp-test.yml,
and e2e-topologies.yml. That filter matches the PR's *base* branch, so
today's open stack (#51->#52->#53->#58, each based on the previous
PR's branch instead of main) gets no build signal at all - only
commit-trailers.yml's checks, since that workflow never had the
filter. Each file's own cost gate (the `changes` job, `paths-ignore`,
or `paths` allowlist) already controls when the expensive jobs run,
independent of base branch, so this is a narrow, safe change.

Add `concurrency`/`cancel-in-progress` to pr-tests.yml,
commit-trailers.yml, and mpmc-bench.yml, matching the pattern already
used by full-loop-bench.yml and both E2E workflows, so a superseded
push to the same PR doesn't leave a stale run burning CI minutes
alongside the new one.

`latrec-portability.yml` isn't on main yet (lands with the still-open
PR #59) so it isn't touched here; its owner has already added a
timeout-minutes to it directly on that branch.

This commit is CI-only (.github/workflows/*.yml), which the repo's own
"Detect code changes" gate classifies as non-code, so it does not
exercise the real build/test jobs on this PR - its mechanics are
reasoned from the diff and cross-checked with `python3 -c
"import yaml"` for every file plus a job-by-job timeout-minutes audit,
not observed passing in a live run the way the previous commit's
build_libe3 change was.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Andrea Lacava <a.lacava@northeastern.edu>
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

⏱️ Full-loop Latency Benchmark (commit 3b09e95)

Full-loop latency benchmark (N=1028 after 50 warmup)

All values in microseconds (us). Link: zmq, transport: ipc, encoding: ASN.1 APER.

Phase mean p50 p99 max
1. Collect indication data 0.20 0.18 0.36 1.72
2. Create & encode indication 1.03 0.95 1.88 15.06
3. Deliver indication (RAN -> dApp) 111.89 107.28 190.32 220.22
4. Decode indication 0.76 0.69 1.41 15.71
5. Process data 0.03 0.03 0.04 0.14
6. Create & encode control 0.44 0.39 0.80 15.20
7. Deliver control (dApp -> RAN) 127.08 120.24 214.95 249.71
8. Decode & handle control 0.70 0.67 1.02 16.21
Total round-trip 242.13 208.62 351.41 393.46

Benchmarked on ubuntu-latest, Release build, ZMQ + IPC, ASN.1 APER.

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

📊 MPMC Queue Benchmark Results (commit 3b09e95)

MpmcQueue End-to-End Latency (SPSC, 30000 items per queue size)

Queue Capacity P50 (ns) P95 (ns) P99 (ns) P99.9 (ns)
16 1137 1188 1247 20213
64 1327 3332 4264 30712
256 1068 7590 30612 31975
1024 36203 40982 41432 41583
4096 102867 134727 137963 138664

LockFreeQueue End-to-End Latency (SPSC, 30000 items per queue size)

Queue Capacity P50 (ns) P95 (ns) P99 (ns)
16 1328 2330 2471
64 21219 25567 37029
256 24544 32007 47867
1024 110293 130511 139819
4096 468979 504386 509103

Throughput (400000 items per configuration)

Configuration Queue Cap Throughput (Mops/s)
SPSC (1P×1C) 256 15.57
MPSC (4P×1C) 1024 12.34
MPMC (4P×4C) 4096 11.60
MPMC (8P×4C) 4096 10.49

Stress / Correctness (100000 items each)

Test Items Result
SPSC (1P×1C) 100000 ✅ PASS
MPSC (4P×1C) 100000 ✅ PASS
MPMC (4P×4C) 100000 ✅ PASS
MPMC (8P×8C) 100000 ✅ PASS

Benchmarked on ubuntu-latest, Release build

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

🔀 E2E Topologies — multi-dApp / multi-RAN (commit 3b09e95)

zmq/ipc

  • 1 RAN - 1 dApp: indications=5
    • dapp peer=t11 ran=ran-solo sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0.6 max=1 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]
  • 1 RAN - 2 dApps: dApp#1 ind=5 sub=1, dApp#2 ind=6 sub=2, RAN saw 2 dApps
    • dapp1 peer=t12 ran=ran-shared sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0.8 max=1 @seq=1) hist[<=1:5 2-5:0 6-10:0 >10:0]
    • dapp2 peer=t12 ran=ran-shared sub=2 indications=6 seq=[0..5] dropped=0 (0%) age_ms(avg=0.833333 max=1 @seq=1) hist[<=1:6 2-5:0 6-10:0 >10:0]
  • 2 RANs - 1 dApp: from ran-a ind=5, from ran-b ind=5
    • dapp peer=t2a ran=ran-a sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0.2 max=1 @seq=2) hist[<=1:5 2-5:0 6-10:0 >10:0]
    • dapp peer=t2b ran=ran-b sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0 max=0 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]

zmq/tcp

  • 1 RAN - 1 dApp: indications=5
    • dapp peer=default ran=ran-solo sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0 max=0 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]
  • 1 RAN - 2 dApps: dApp#1 ind=5 sub=1, dApp#2 ind=6 sub=2, RAN saw 2 dApps
    • dapp1 peer=default ran=ran-shared sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0 max=0 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]
    • dapp2 peer=default ran=ran-shared sub=2 indications=6 seq=[0..5] dropped=0 (0%) age_ms(avg=0 max=0 @seq=0) hist[<=1:6 2-5:0 6-10:0 >10:0]
  • 2 RANs - 1 dApp: from ran-a ind=5, from ran-b ind=5
    • dapp peer=default ran=ran-a sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0 max=0 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]
    • dapp peer=off100 ran=ran-b sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0 max=0 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]

example_simple_agent + example_simple_dapp on ubuntu-latest, Release. Indication age is report-only.

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

🔄 E2E dApp Integration Results (commit 3b09e95)

posix/ipc

  • dApp exit: 0
  • Indications received: 7

posix/tcp

  • dApp exit: 0
  • Indications received: 7

zmq/ipc

  • dApp exit: 0
  • Indications received: 7

zmq/tcp

  • dApp exit: 0
  • Indications received: 7

example_simple_agent + example_simple_dapp on ubuntu-latest, Release build

The branch ruleset on main requires 15 status-check contexts, several
of which are gated on "did this PR touch code" -- either a runtime
job-level `if:` (pr-tests.yml, commit-trailers.yml) or a trigger-level
`paths-ignore` that skips the whole workflow (e2e-dapp-test.yml,
e2e-topologies.yml, full-loop-bench.yml). Required-status-checks does
not treat a "skipped" conclusion, or a context that never reports at
all, as satisfied -- confirmed against this repo's own history: PR #43
("docs: adopt OAI-style fast-forward merge...") merged with "Each
commit builds and tests", "SWIG Python Bindings", "All Encodings",
"Integration Tests", and "Build and Test" all showing
conclusion=skipped, and several E2E/Topologies/Full-loop-latency
contexts never reporting at all. That merge only went through because
of the repo's all-or-nothing admin bypass, which is also the thing
that lets a merge through without its build actually having been
checked -- see the "protect main" ruleset-split conversation this PR
enables. Fix that at the source instead: every required-check job now
runs unconditionally and reports a real `success`, moving the
docs-only skip down to individual steps.

- pr-tests.yml: removed the job-level `if:` on asn1c-cache/test, and
  added `needs: changes` to integration/all-encodings/swig (they only
  had `needs: test` before, which isn't enough to reference
  needs.changes.outputs.code); every expensive step in all five jobs
  is now step-gated instead.
- commit-trailers.yml: per-commit-build keeps its job-level
  `if: github.event_name == 'pull_request'` (an event-type check,
  unrelated to this fix, and this context is only meaningful for that
  event anyway) but the code-change check moved to its steps.
- e2e-dapp-test.yml, e2e-topologies.yml, full-loop-bench.yml: replaced
  `paths-ignore` with an unconditional trigger plus the same runtime
  `changes` job pattern already used in pr-tests.yml (robust to
  workflow_dispatch, which has no PR base/head to diff against).
  full-loop-bench.yml's "Post results as PR comment" step reads
  bench_results.md unconditionally with no graceful missing-file
  handling (unlike the E2E jobs' upload-artifact steps, which already
  have `if-no-files-found: warn`), so it needed its own gate to avoid
  crashing the job outright on a docs-only PR. Also gated the (already
  non-required) `comment` jobs in both E2E files the same way, so they
  don't spend a run trying to aggregate artifacts that were never
  produced.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Andrea Lacava <a.lacava@northeastern.edu>
Observed live on this PR's own CI, twice in a row and in two different
call sites: a plain `sudo apt-get update` hung silently for 12-18
minutes against azure.archive.ubuntu.com (Ign:/retry lines, then total
silence) before the job's own timeout-minutes finally killed it. One
occurrence was in pr-tests.yml's asn1c-cache job, inside build_libe3's
own install_dependencies(); the other was in e2e-topologies.yml's
topologies job, in the workflow-level `sudo apt-get update` step that
ran before calling `./build_libe3 -I` at all. Same mirror, same
symptom, two different commands -- the timeout-minutes added earlier
in this PR caught both correctly (that's the point), but a job dying
after 15-20 minutes on a transient mirror hiccup is still a bad
outcome worth avoiding when the hiccup usually clears on retry.

Add retry_apt(): wraps a package-manager command in `timeout 120`,
retrying up to 3 times. 120s comfortably covers an actually-successful
`apt-get update` (observed ~45s) or the `install` package list, while
still failing fast enough that 3 attempts fit well inside the
existing job timeouts. Wire it into install_dependencies()'s apt-get
branch (both the `update` and `install` calls).

Also remove the workflow-level `sudo apt-get update` step that
preceded `./build_libe3 -I` in every job across all 7 workflow files
(~15 call sites): it was calling apt-get update a second time only
seconds before install_dependencies() calls it again internally --
pure redundant work, and one more independent place the same mirror
flakiness could (and did) strike. The SWIG job's separate `sudo
apt-get install -y swig python3-dev` is similarly redundant (swig and
python3-dev are already in install_dependencies()'s own package list)
and was removed too, along with its now-orphaned update call.

Verified retry_apt() directly: succeeds immediately on a working
command; exhausts all attempts and returns non-zero on a command that
always fails; and, with a shortened timeout substituted in for a fast
test, confirmed a hanging command is killed at the timeout boundary on
every attempt rather than blocking indefinitely. Full local Release
build + `ctest` (17/17) still passes.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Andrea Lacava <a.lacava@northeastern.edu>
Pushed the previous commit's flat 120s retry_apt budget and watched it
fail for real, twice, on this PR's own CI: every attempt of `apt-get
install` hit the full 120s wall while still visibly downloading real
packages (up through an 11MB cmake package) rather than sitting idle
-- a mirror that's genuinely slow right now, not hung, and 120s isn't
enough headroom for a full ~18-package install under that condition.
`apt-get update` (metadata only) has consistently succeeded within
90s across every run observed so far, including this session's own
slower ones (45-89s).

Give retry_apt() an explicit per-attempt timeout parameter instead of
a hardcoded one, and size each call site from what was actually
observed: 90s for `apt-get update`, 180s for the `apt-get install`
package list. Also bump the four job timeouts that were at exactly
15 minutes and call install_dependencies (asn1c-cache in pr-tests.yml/
e2e-dapp-test.yml/e2e-topologies.yml, Build and Test, E2E, and
Full-loop latency) to 20, so 3 retries at the new, more realistic
per-attempt budgets can't crowd out the job's own real work under
today's observed mirror conditions.

Full local Release build + `ctest` (17/17) still passes.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Andrea Lacava <a.lacava@northeastern.edu>
@Thecave3 Thecave3 changed the title ci: harden CI against silent hangs — timeouts, asn1c caching, honest failures ci: harden CI against silent hangs — timeouts, asn1c caching, failures Aug 19, 2026
Pushed the previous commit's timing fix and watched a different,
sharper failure on this PR's own CI: E2E posix/ipc's install step hit
"E: Could not get lock /var/lib/dpkg/lock. It is held by process 2750
(dpkg)" on attempts 2 and 3, both firing within milliseconds of attempt
1 rather than after their own 180s budget. `timeout 180 sudo apt-get
install ...` sends its kill signal to the direct child, which is
`sudo`, not `apt-get`; sudo does not reliably forward that signal on to
its own child. So when attempt 1's wall-clock ran out mid-install
(observed at "Reading database ... 90%"), dpkg kept running orphaned
underneath the killed sudo/apt-get, still holding the lock -- and every
retry failed instantly against that stale lock instead of getting a
fresh timeout budget at all.

Reorder to `sudo timeout N apt-get ...`: timeout is now the direct
parent of apt-get, so its signal reaches the right process. Also run
`dpkg --configure -a` between attempts (best-effort) so a dpkg run that
still gets interrupted has a chance to finish or clean up before the
next retry.

Verified with a stubbed sudo (this dev machine has no passwordless
sudo, so a real one can't be exercised locally): success returns
immediately; a command that always fails exhausts all 3 attempts in
milliseconds (each one genuinely failing fast, not waiting out its
budget); a hanging command is killed at each attempt's timeout
boundary. Full local Release build + `ctest` (17/17) still passes.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Andrea Lacava <a.lacava@northeastern.edu>
"Post fast-forward merge command" only ran on an approved
pull_request_review, which never fires for a solo-authored PR --
GitHub doesn't allow self-approval and there's no second reviewer, so
this context sat permanently "skipped". Harmless to mergeability
(it isn't itself a required check), but it also meant the one job
meant to hand the maintainer the exact merge command never actually
ran for the PRs in this repo's current stack.

Decouple it from review state entirely: what actually makes the
comment trustworthy is that the required checks for this exact commit
are green, not who clicked approve. The job now polls the branch's
*effective* required status checks (via `gh api
repos/{repo}/rules/branches/main`, so it can't drift out of sync with
whatever the ruleset currently requires -- no hardcoded context list
or ruleset ID) until they've all completed, then posts the comment
only if every one succeeded. Bumped its timeout-minutes from 5 to 20
to fit the poll (up to 18 minutes, generous for a same-repo PR's own
checks) plus the post itself.

Verified the script directly with a stubbed `gh`, since a real
end-to-end run needs live checks in progress: waits through pending
polls before posting; declines to post when a required check failed;
updates an existing ready-to-merge comment instead of duplicating it;
and gives up quietly past its deadline without posting. Will confirm
it fires for real on this PR's own next push, now that PR #61's
checks are already green.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Andrea Lacava <a.lacava@northeastern.edu>
@github-actions

Copy link
Copy Markdown
Contributor

Ready to merge (fast-forward only)

All required checks are green for 3b09e95. A maintainer can land the reviewed commits with:

git fetch origin
git checkout main && git merge --ff-only 3b09e95dcb68c0bd1e9617c38248727f829b5964 && git push origin main

Head: 3b09e95dcb68c0bd1e9617c38248727f829b5964 (branch 60-ci-hardening). If --ff-only fails as non-fast-forward, the branch must be rebased on the latest main.

@Thecave3
Thecave3 merged commit 3b09e95 into main Aug 19, 2026
27 checks passed
@Thecave3
Thecave3 deleted the 60-ci-hardening branch August 19, 2026 23:03
Thecave3 added a commit that referenced this pull request Aug 19, 2026
Discovered live on this branch's own rebased CI run, right after
merging #61: the "ready to merge" comment posted while "Build and Test
(ubuntu-latest, Release)" was still in_progress. The polling loop
iterated over whatever check-runs the API happened to return and only
flipped `all_done` to false for entries it found with status !=
completed -- a required context that hasn't been *created* yet at all
(a matrix leg still queued behind another job) never appears in that
response, so the loop silently never touched it and `all_done` stayed
true.

Doesn't affect actual merge safety: the branch ruleset independently
re-checks every required context at push time regardless of what this
comment says, so a premature comment couldn't have caused an unsafe
merge, only a premature or misleading informational one. Still worth
fixing since it undermines the point of gating the comment on real
completion at all, and it was already merged into main via #61.

Fix: iterate over the required list, not over the found check-runs,
and look up each one's current status individually. A required
context missing from the response now correctly counts as "not done"
instead of being silently skipped.

Verified with a stubbed gh covering the exact bug scenario (a required
context absent from two consecutive polls, appearing only on the
third) plus the four scenarios from the original commit (waits then
posts, declines on a real failure, updates an existing comment,
gives up quietly past its deadline) -- all five now correct. Full
local Release build + `ctest` (17/17) still passes.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Andrea Lacava <a.lacava@northeastern.edu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: no CI job timeouts + swallowed asn1c install errors cause multi-hour hangs; stacked PRs get no build signal

1 participant