Skip to content

test: stabilize flaky CI on main - #657

Merged
k9ert merged 13 commits into
mainfrom
kn/lnurl-dev-env
Jul 29, 2026
Merged

test: stabilize flaky CI on main#657
k9ert merged 13 commits into
mainfrom
kn/lnurl-dev-env

Conversation

@k9ert

@k9ert k9ert commented Jun 30, 2026

Copy link
Copy Markdown

Purpose

This PR stabilizes the flaky main-branch CI path we investigated on GitHub runners. We are using repeated CI probe commits to prove the branch stays green across multiple full runs.

What Changed

  1. LNURL dev server SQLite now runs in memory.
  • before: LNURL_DB_URL=sqlite:///data/lnurl.db?mode=rwc
  • after: LNURL_DB_URL=:memory:

This removes the file-backed SQLite database from the LNURL service in the dev/test compose stack. The existing LNURL volume mount and user setting are intentionally left in place so this change stays focused on database persistence/state rather than broad compose cleanup.

  1. Public price subscription tests are hardened.
  • always stop the GraphQL subscriber in teardown
  • aggregate errors across all matching subscription events instead of assuming exactly one event
  • fix the realtime price assertion path from data.brealtimePrice to data.realtimePrice

The previous test could receive multiple valid price events, produce multiple zero error counts, fail a string equality check, and then skip subscriber cleanup. That failure mode made the BATS suite time out after the actual assertion problem.

  1. Hedge-arbitrage integration tests get a local 300s Jest timeout.

The remaining red checks after the E2E fixes were core/api:test-integration timeouts in test/integration/scenarios/hedge-arbitrage.spec.ts. This file bootstraps LND nodes and runs repeated invoice probe/payment flows. CI first exceeded the default 60s timeout, then later exceeded a 120s file-local timeout in multiple cases, so the timeout is now scoped to 300s for this scenario file instead of raising the global integration timeout.

  1. On-chain receive balance checks now wait for metric convergence.

A later probe run passed E2E except for onchain-receive teardown after the test body itself succeeded. The failure was balance_for_check reading nonzero balance metrics immediately after address-only assertions. The onchain-receive teardown now retries the existing balance invariant before failing, preserving the check while tolerating short metric lag.

Why This Fixes Main

The devsync discussion pointed at the LNURL server SQLite setup as the likely source of the original GitHub-runner flake: CI could leave the SQLite file or its related state in a bad condition, and resetting the Docker host made the problem disappear. Main already had cleanup around dev/.data/lnurl.db files, which confirms persisted LNURL SQLite state was a known weak spot. Moving LNURL to in-memory SQLite removes that state from the CI path.

After that, the next failures were separate test robustness issues exposed by getting farther through CI: the public price subscription assertion/cleanup bug, the hedge-arbitrage scenario hitting too-small per-test timeouts, and an onchain-receive teardown reading eventually-consistent balance metrics too early. Each is handled directly and narrowly.

Validation

Green full runs so far:

  • 349ac50: passed E2E execute via bats and Integration build and test core
  • 0feaf00: passed E2E execute via bats and Integration build and test core

Probe run a0f969b exposed the onchain-receive balance convergence issue fixed by this PR. Probe run 509c5e0 passed E2E but exposed that 120s was still too low for hedge-arbitrage integration cases. The latest commit is now rerunning CI. The target remains at least five full green CI runs before calling this stable.

@dolcalmi dolcalmi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should not be removed, if the problem is in other PR this will hide the real issue

@k9ert
k9ert marked this pull request as draft June 30, 2026 17:08
@github-actions github-actions Bot added the core label Jun 30, 2026
@k9ert k9ert changed the title fix: disable lnurl server in dev core bundle debug: inspect lnurl server auth failure Jun 30, 2026
@k9ert
k9ert force-pushed the kn/lnurl-dev-env branch from bfb9ba2 to 8979362 Compare July 7, 2026 11:08
@k9ert

k9ert commented Jul 7, 2026

Copy link
Copy Markdown
Author

CI probe result update:

The current-main probe did not hit the earlier LNURL missing-user failure and did not fail in api-keys-limits setup. It timed out because an earlier bats test failed and left the suite in a bad state.

Evidence from run https://github.com/blinkbitcoin/blink/actions/runs/28861583452:

  • Last passing test: ok 73 ln-receive: error on cancel a settled ln invoice
  • Failing test: not ok 74 ln-receive: error on cancel a held ln invoice
  • The test created a BTC invoice, stopped api-trigger, started lnd_outside_cli payinvoice in the background, confirmed GraphQL still showed the invoice as PENDING, then called lnInvoiceCancel.
  • Actual GraphQL response: {"data":{"lnInvoiceCancel":{"errors":[],"success":true}}}
  • Expected: success == false and error message The invoice has already been processed.
  • After that failure, no further bats progress was printed for ~8 minutes until the workflow hit the 25-minute job timeout. The Tilt log continued to show service activity, including Bria wallet syncs and bitcoind RPCs, so this was not a total runner/service freeze.

Likely next investigation target: the held-invoice cancel behavior in bats/core/api/ln-receive.bats / lnInvoiceCancel. Also harden that test so api-trigger is restarted even when an assertion fails before the cleanup section.

@k9ert

k9ert commented Jul 7, 2026

Copy link
Copy Markdown
Author

Second probe: testing the timeout hypothesis.

I pushed 8058ca57d test: probe ln receive trigger cleanup, which adds teardown cleanup in bats/core/api/ln-receive.bats:

  • if $TRIGGER_STOP_FILE is still present after a test, remove it
  • wait for api-trigger to start before running the balance check

Hypothesis: the current-main probe timed out because ln-receive: error on cancel a held ln invoice failed before its normal cleanup, leaving api-trigger stopped and a background payment/test process hanging. If this probe turns the timeout into a normal test failure, that confirms the timeout mechanism even if the underlying cancel behavior is still broken.

@k9ert

k9ert commented Jul 7, 2026

Copy link
Copy Markdown
Author

Reviewed the probe assumptions against the actual CI logs. TLDR: probe method is sound, but the PR body's failure description is inaccurate, and the probe never answered its own question because the empty-commit run got cancelled.

What the logs actually show (#656, run 28857941573)

  • First failure is test 63 — first LN-settle test in ln-receive.bats: lnd_outside_cli payinvoice fails with FAILURE_REASON_NO_ROUTE. All LN payments after that fail the same (63–74).
  • Cascade: trigger-down tests (68–71) touch $TRIGGER_STOP_FILE, fail at the payment step before the rm, leaving the stop file behind → trigger stays stopped → later setup_file funding times out: ln-send (75, fund_user_lightning, INSUFFICIENT_BALANCE), lnurl-send (91, fund_user_onchain 'bob'), plus 101/108 and the public-ln-receive block.
  • The claim "fails funding Alice on-chain for api-keys-limits" appears nowhere in the latest run. No api-keys-limits setup failure, no Alice onchain-funding failure — description is stale (likely from an earlier force-pushed run).

Assessment of assumptions

  1. "fix(api): show LNURL memo on intraledger receives #656 can't be the cause" — accurate, provable without the probe. NO_ROUTE is emitted by lnd_outside routing before any blink code runs; fix(api): show LNURL memo on intraledger receives #656 only touches intraledger memo metadata. It cannot produce a routing failure between two LND nodes.
  2. Probe design reasonable (E2E only runs on PRs), but the pure-main probe run (8979362) was cancelled when the second commit was pushed — "does main fail unchanged?" was never answered.
  3. Second commit muddies the probe. The green E2E run (28865251675) ran main + the teardown() stop-file cleanup, not pure main. The cleanup is a no-op unless a trigger-down test already failed, so it could not have "fixed" the primary NO_ROUTE failure. Green run mostly indicates NO_ROUTE is a flake (channel lnd_outside→lnd1 inactive/unconfirmed at test time — this branch already has one red + one green E2E run from 2026-06-30).
  4. The teardown cleanup itself is a good fix — limits blast radius so one failed trigger-down test doesn't poison every subsequent file. Worth keeping regardless.

Suggested next steps

  • Fix stale PR title/body ("lnurl server auth failure" / api-keys-limits — both describe older failure modes).
  • Real root cause: why lnd_outside→lnd1 had no route at test start. A channel-active retry/assert before the first LN test would turn this flake into a clear setup failure.
  • Consider moving the teardown() cleanup into a shared helper — the stale stop file poisons other files (ln-send, lnurl-send), whose setups don't defend against it.

@k9ert k9ert changed the title debug: inspect lnurl server auth failure test: probe e2e trigger cleanup Jul 7, 2026
dolcalmi
dolcalmi previously approved these changes Jul 7, 2026
@k9ert k9ert changed the title test: probe e2e trigger cleanup test: probe in-memory LNURL sqlite db Jul 7, 2026
@k9ert

k9ert commented Jul 8, 2026

Copy link
Copy Markdown
Author

Commit: 349ac50 test: increase hedge arbitrage timeout

Reasoning:
The latest red check was no longer the LNURL/E2E failure. E2E passed, and the remaining failure was Integration test -> build and test core, specifically core/api:test-integration timing out at Jest's default 60000 ms in test/integration/scenarios/hedge-arbitrage.spec.ts.

This file bootstraps LND nodes and runs repeated invoice probe/payment flows. The failing case timed out rather than failing an assertion, so this commit scopes a larger 120000 ms Jest timeout to that hedge-arbitrage scenario file only, instead of raising the global integration timeout.

Follow-up:
This should unblock the slow CI case. If it still flakes, the next place to inspect is the LND hodl invoice cancellation path, because cancelHodlInvoice is not currently wrapped in the same application-level timeout pattern used by nearby LND probe/payment calls.

@k9ert
k9ert marked this pull request as ready for review July 8, 2026 08:57
@k9ert

k9ert commented Jul 8, 2026

Copy link
Copy Markdown
Author

Re-reviewed with the three new commits (in-memory LNURL sqlite, public.bats hardening, hedge-arbitrage timeout). Verified claims against run logs. Overall much stronger — title/body now accurate, 3 of 4 claims check out, one causal claim is shaky.

Verified accurate

  1. In-memory probe got through the LNURL areas — on bededae, E2E run 28875994598 passed everything through test 141 incl. lnurl-send and public-ln-receive (which failed the day before), then failed only at test 145 public: can subscribe to price.
  2. Brittle price assertion diagnosis correct — old jq ran per-line, so two valid price events produce "0\n0" which fails the == "0" compare. jq -s map|add is the right fix. brealtimePrice → realtimePrice is a genuine bug find: the typo path made the assertion vacuously pass (null.errors | length = 0).
  3. Integration timeout bump justified — run 28892636902 shows via no-amount fee probe (60001 ms) exceeding the 60s Jest default, file otherwise progressing (171s total). 120s file-wide is pragmatic.
  4. Latest commit 349ac50 fully green — E2E, Integration, everything.

Questionable

  • "Because the failed assertion skipped stop_subscriber, the suite later timed out" — logs don't support this chain. Test 146's subscribe_to calls stop_subscriber first (reaping 145's leaked process), and 146–148 all passed normally after the failure. The run then went quiet after test 148 (quiz) for ~90s and ended as cancelled, not timed-out — if that was a manual cancel, the body overstates it. The teardown addition is still good hygiene either way (and makes the inline stop_subscriber calls at public.bats:93,107 redundant — could drop them).

Watch-outs before this graduates from probe to fix

  • :memory: + connection pooling: in-memory SQLite is per-connection unless shared-cache. Tests pass, so the server apparently uses one connection today — but if blink-lnurl-server ever pools >1, each connection gets its own empty DB (intermittent "no such table"). If config supports it, sqlite::memory:?cache=shared or explicit pool-size-1 is safer.
  • If merged: bats/ci_run.sh's rm dev/.data/lnurl.db*, the /data volume mount, and user: "0:0" become dead weight (body already acknowledges keeping them for signal focus — fine for the probe).
  • One green run is weak evidence for a flake, as the body itself notes — and the earlier fix(api): show LNURL memo on intraledger receives #656 first failure (NO_ROUTE from lnd_outside at the first LN-settle test) is not addressed by any of these changes. If that was the real killer, it can recur independently of LNURL/price fixes. Worth re-running E2E on this head a few times.

openoms
openoms previously approved these changes Jul 8, 2026

@openoms openoms left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@k9ert k9ert changed the title test: probe in-memory LNURL sqlite db test: stabilize flaky CI on main Jul 8, 2026
@k9ert

k9ert commented Jul 9, 2026

Copy link
Copy Markdown
Author

Probe 7 (76cfaa8d6) failed for external setup/network reasons, not because the SDL diffs changed. Check SDLs passed the schema diff targets, then root//dev:supergraph failed when Rover tried to download the Apollo supergraph plugin (v2.3.2) from https://rover.apollo.dev/... and the request timed out.

To avoid this class of flake, make the supergraph plugin hermetic: prefetch/vendor it through Nix/Buck or cache it as an explicit CI dependency, with retry as a fallback.

@github-actions github-actions Bot added the ci label Jul 9, 2026
@k9ert

k9ert commented Jul 9, 2026

Copy link
Copy Markdown
Author

Probe 11 (e0b90ce2d) failed in Check SDLs for dependency setup, not because an SDL diff failed. The schema-diff targets that ran passed, but root//core/api:node_modules failed during pnpm install core/api: protoc-gen-js downloaded protobuf-javascript-3.21.4-linux-x86_64.zip from GitHub and then failed to unzip it with Invalid or unsupported zip format. No END header found, which is consistent with a truncated/corrupt download.

To avoid this class of flake, make the protobuf JS binary hermetic: prefetch it through Nix/Buck or vendor/cache it as an explicit CI input. A retry around package postinstall downloads would help, but removing runtime network downloads from CI is the stronger fix.

@k9ert
k9ert force-pushed the kn/lnurl-dev-env branch from e2fcb55 to 004d7e4 Compare July 11, 2026 12:42
@k9ert

k9ert commented Jul 11, 2026

Copy link
Copy Markdown
Author

Updated the PR with 004d7e4. It fixes two CI failure modes: Check SDLs was depending on Rover lazy-downloading supergraph-v2.3.2 during the action, so the commit pins that archive as a Buck remote_file and stages Rover plus the plugin from explicit Buck inputs; build and test core moved past Buck outer timeout but then hit Jest default 60s hook timeout in LND/bitcoind bootstrap, so the integration target now sets JEST_TIMEOUT=600000 to match the longer Buck timeout. A subagent reviewed the change and flagged one hermeticity risk in the first version; the amended commit addresses it by passing the Rover node_modules tree as an explicit rule input instead of parsing the wrapper. Local validation: python3 -B -m py_compile toolchains/rover/generate_supergraph.py, git diff --check, and buck2 test //dev:check-sdls.

@k9ert
k9ert force-pushed the kn/lnurl-dev-env branch from 004d7e4 to c343ff0 Compare July 11, 2026 14:22
@k9ert

k9ert commented Jul 11, 2026

Copy link
Copy Markdown
Author

Follow-up on c343ff0: the previous amended commit still failed Check SDLs on Linux because I passed the build_node_modules output root to the script but then looked for .pnpm directly under it. CI showed the standardized Buck output shape is /node_modules/.pnpm, so the helper now enforces exactly that contract and fails clearly if it changes. Local validation after the correction: python3 -B -m py_compile toolchains/rover/generate_supergraph.py, explicit helper resolution against the Buck output path, git diff --check, and buck2 test //dev:check-sdls.

@k9ert
k9ert force-pushed the kn/lnurl-dev-env branch from c343ff0 to b51258e Compare July 12, 2026 10:26
@k9ert

k9ert commented Jul 12, 2026

Copy link
Copy Markdown
Author

Follow-up on b51258e: execute via bats exposed a race in ln-receive: error on cancel a held ln invoice. The test saw the API invoice as PENDING and immediately tried to cancel, but CI showed LND still had the HTLC IN_FLIGHT; in that transition window cancel can succeed, so the test was not reliably testing a held invoice. The fix waits until the receiving LND invoice is ACCEPTED before asserting lnInvoiceCancel must fail. This should make the assertion match the test intent without broad timeout increases or app behavior changes. Note: build and test core reruns failed on crates.io 429/500 download errors, which is external dependency fetch noise rather than this code path.

@k9ert
k9ert force-pushed the kn/lnurl-dev-env branch from db886b5 to e6b5b05 Compare July 28, 2026 09:08
k9ert and others added 6 commits July 28, 2026 11:19
Persisted LNURL SQLite state was the suspected source of the GitHub-runner
flake: CI could leave the file or its WAL in a bad condition, and resetting
the Docker host made the problem disappear. Main already carried cleanup
around dev/.data/lnurl.db*, which confirms it was a known weak spot.

Moving the dev/test compose stack to an in-memory database removes that
state from the CI path. The /data volume mount, the user "0:0" override and
the ci_run.sh cleanup all existed only for the file-backed db, so they go
with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A BATS run died late in the suite with userLogin returning
UnknownKratosError because Kratos had exited. The Tilt log showed its
embedded courier failing to reach mailslurper:1025, which this compose file
does not provide, and taking the whole process down with it.

BATS does not need SMTP delivery here; it reads verification codes straight
from kratos-pg.courier_messages. Drop --watch-courier so the auth APIs
survive an unavailable mail sink. If local email delivery is wanted later,
the clean shape is a separate courier service plus an explicit sink.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Several BATS failures were assertion/timing bugs rather than app defects:

- public.bats aggregated errors with a per-line jq, so two valid price
  events produced "0\n0" and failed an == "0" compare; jq -s map|add fixes
  it. The realtime assertion also read .data.brealtimePrice, a typo that
  made the check vacuously pass. Subscriber cleanup moves to teardown so a
  failed assertion cannot leak the process.
- balance_for_check ran one-shot immediately after async Bria/LND/exporter
  work. Add a shared assert_balance_for_check helper that retries the same
  invariant, with a longer window for the onchain settlement tests.
- ln-receive's held-invoice cancel test saw the API report PENDING while
  LND still had the HTLC in flight; in that window cancel legitimately
  succeeds, so it was not testing a held invoice. Wait for ACCEPTED first.
- ln-receive teardown now clears a leaked trigger stop file and waits for
  api-trigger to restart, so one failed trigger-down test cannot poison
  every later file.
- The internal payout cancel test used the default FAST queue, where the
  payout can be batched and broadcast before the test finishes waiting for
  payout_submitted. SLOW keeps the payout cancellable so the test checks
  what it is named for. FAST remains covered by its own settle test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
core/api:test-integration timed out in hedge-arbitrage.spec.ts, which
bootstraps LND nodes, opens channels, and then runs repeated invoice
probe/payment flows. CI first exceeded Jest's 60s default, then a
file-local 120s, then 300s in the shared beforeAll.

Scope the larger timeout to that scenario file and set JEST_TIMEOUT on the
integration target rather than raising the global default, so unrelated
suites keep failing fast.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Check SDLs failed when Rover lazily downloaded supergraph-v2.3.2 from
rover.apollo.dev during the action and the request timed out. The schema
diff targets themselves passed; only the runtime fetch was flaky.

Pin the archive as a remote_file and stage Rover plus the plugin from
explicit Buck inputs, passing the Rover node_modules tree as a rule input
rather than parsing the wrapper. The helper resolves the build_node_modules
output root strictly as <output-root>/node_modules/.pnpm and fails clearly
if that contract changes. Staging is guarded to Linux x86_64.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fresh GitHub runners start with an empty Buck download cache, so the
generated Rust crate set is fetched at build time. Unbounded fanout hit
rate limiting and truncated archives; cap parallelism at 4 across the
workflows and ci_run.sh, and record in reindeer.toml why the checked-in
BUCK file must stay on static.crates.io archive URLs.

The bats suite takes ~24-25 min on a clean run and can spend several more
minutes resetting lightning state between files, especially after the cron
channel tests, so raise the e2e job cap to 60 minutes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@k9ert
k9ert force-pushed the kn/lnurl-dev-env branch from e6b5b05 to 072cbbd Compare July 28, 2026 10:33
k9ert and others added 7 commits July 28, 2026 12:33
The legacy onchain receive is booked by onchainBlockEventHandler, which
rescans only ONCHAIN_MIN_CONFIRMATIONS + 1 blocks deep — 3 blocks with the
dev config's minConfirmations: 2. The test mined six blocks back-to-back,
so the tx could leave that window before the handler processed it.

When that happened the three 0.01 BTC outputs stayed in lnd's onchain
wallet with no ledger entry and no user credit. Lightning.getTotalBalance()
counts lnd's onchain wallet, so galoy_lndBalanceSync went to exactly
3000000 and stayed there: a permanent gap, which is why retrying the
invariant in teardown could never converge and why every later test in the
run failed the same check.

Evidence from run 30351229783: at the transition, physical lnd rose 3000000
while liabilities did not move, and the scanner logged the tx once at
10:52:51 and then did not run again until 10:59:54.

Mine only what is needed to confirm, and never mine past the scan window
while waiting for the receipts to be recorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@blink-claw-bot blink-claw-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Thorough CI stabilization — each fix addresses a specific failure mode with clear reasoning.

HIGH

None.

MEDIUM

  • generate_supergraph.py binary path resolution (line ~55): glob.glob for binary-install@*/node_modules/binary-install/node_modules/.bin is the right approach but is coupled to pnpm's internal layout. The len(matches) != 1 guard makes this fail loudly if the layout changes, which is the correct behavior for CI. Just flagging it as a future maintenance point if pnpm or binary-install's packaging changes.

LOW

  • jest.setTimeout(600000) (10 min) in hedge-arbitrage.spec.ts is very generous. Fine for now since CI was hitting 5-minute walls, but consider whether individual test cases that time out at 600s are really testing what they should be. Not blocking.

What's working well

  • data.brealtimePricedata.realtimePrice — This typo fix in public.bats is probably the most impactful change in the PR. The old assertion path was silently evaluating to null, so the jq output was never a valid error count, causing string-equality failures that skipped subscriber cleanup and cascaded into suite timeouts.
  • assert_balance_for_check with retry — The previous balance_for_check was a snapshot that could catch transient metric states. Wrapping it in retry is the correct fix for asynchronous exporter convergence.
  • LNURL in-memory SQLite — Clean removal of state leakage. Leaving the volume mount/user config untouched keeps the change focused.
  • Smarter block mining in onchain-receive — Mining only what's needed and not exceeding the ONCHAIN_MIN_CONFIRMATIONS + 1 scan window fixes a subtle root cause where transactions could escape the handler's rescan depth.
  • check_lnd_invoice_accepted — Waiting for ACCEPTED before attempting cancellation eliminates the race between PENDING and hold-lock. Good defensive testing.
  • onchain-send SLOW speed — Ensures the payout stays in a cancellable state long enough for the test to exercise the cancel path.
  • Rover plugin pinning — Eliminating the runtime download from rover.apollo.dev removes a flaky network dependency. Linux-only staging with platform detection is the right call — developers on macOS keep normal behavior.
  • Kratos --watch-courier removal — The compose file doesn't provide mailslurper, so the courier would fail and crash Kratos. Clean fix.
  • BUCK2_JOBS=4 — Bounds parallelism during cold-cache crate downloads on fresh runners.

🤖 Review by claude-opus-4-6 via Blink-Claw-Bot

@k9ert
k9ert requested review from patternn and pretyflaco July 29, 2026 11:20
@k9ert
k9ert requested a review from littledino2112 July 29, 2026 12:51
@k9ert
k9ert enabled auto-merge (squash) July 29, 2026 13:04
@k9ert
k9ert merged commit ded5604 into main Jul 29, 2026
16 checks passed
@k9ert
k9ert deleted the kn/lnurl-dev-env branch July 29, 2026 13:29
littledino2112 pushed a commit that referenced this pull request Jul 31, 2026
quickstart/bin/re-render.sh feeds dev/docker-compose.deps.yml to ytt,
which rejects plain "#" comments. #657 added three, so re-render.sh
aborts and the Concourse prep-quickstart task fails before
check-quickstart ever runs. Quickstart image bumps stopped Jul 28.

Switch to "#!" — the syntax already used inline a few lines above.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants