Skip to content

ci: reserve the Supabase ports before supabase start (both lanes) - #588

Merged
AndresL230 merged 4 commits into
mainfrom
ci/integration-port-preflight
Aug 26, 2026
Merged

ci: reserve the Supabase ports before supabase start (both lanes)#588
AndresL230 merged 4 commits into
mainfrom
ci/integration-port-preflight

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

What failed

Run 32624263094 (push to main,
9f34454) died at Start Supabase:

failed to start docker container "supabase_db_sapling": … failed to bind host port for
0.0.0.0:54322:172.18.0.2:5432/tcp: address already in use

The identical commit, re-run 40 minutes later, passed. No code cause — a day of signal went to
chasing it.

The mechanism

The runner is a fresh VM per job, so nothing is left over. But Supabase's ports (54320–54329) sit
inside Linux's default ephemeral source-port range, 32768–60999. Any outbound TCP connection the
job makes can be handed 54322 as its source port, and Docker's later bind(0.0.0.0:54322) then
fails.

The attempt-1 log times it exactly: the last of dozens of ghcr.io image pulls completes at
06:58:38.6156, Starting database... is logged 0.03s later, and the bind fails. Those pulls are
hundreds of outbound connections. One of them took the port.

Note the pulls happen insidesupabase start — so a pre-flight check would have passed. The fix
has to be pre-emptive.

What the guard does

scripts/preflight-ports.sh, called right after checkout in both lanes that boot Supabase —
integration.yml and e2e.yml (which reaches supabase start through make e2e-up
scripts/e2e-up.sh). Right after checkout is the earliest point supabase/config.toml exists, and
it puts every later download — setup-python, setup-cli, npm ci, playwright install — behind
the reservation. Bash only; no new actions or dependencies.

  1. Reserves the ports via net.ipv4.ip_local_reserved_ports, which excludes them from
    automatic source-port assignment while leaving explicit bind() untouched — so Docker can still
    publish them. Merges with any existing value (idempotent), then reads the value back and
    verifies membership before claiming anything is reserved.
  2. Fails fast if a foreign process is LISTENing on a port supabase start will actually bind,
    printing its state, address, pid and program. It never kills what isn't ours.
  3. Waits out a non-listening holder (up to 75s — a TIME-WAIT entry lives a fixed 60s), printing
    what it's waiting on: the one case reservation can't fix retroactively, because it was allocated
    before the step ran. On timeout it warns and proceeds rather than failing on a guess.
  4. Prints one greppable line: preflight: ports 54321,54322,54323,54324 free (enabled services; reserved from the ephemeral range) — and the word free only ever appears when the ports were
    observed free.

Two different port sets, both derived from supabase/config.toml rather than hardcoded:

  • RESERVE — every *port key inside the live ephemeral range (read from /proc, so the
    premise is re-verified in every run's log). Enabled or not: reserving a port nothing binds costs
    nothing, and it keeps the protection in place the day someone flips enabled = true.
  • CHECK — the ports supabase start will actually bind: *port keys of enabled sections,
    minus shadow_port (only db diff binds that), and not range-filtered. Only this set can fail
    or stall the job, so a holder on the disabled pooler's 54329 can't red an unfiltered PR gate.

supabase start also picks up e2e-up.sh's stop-and-retry, which covers stale/exited projects and
the preflight's admitted give-up path.


Merge-gate review rework (2026-08-26) — commit d992c12f

A /code-review at the merge gate returned 15 findings against the original inline step. All of
them are addressed here; the whole body moved out of the workflow into scripts/preflight-ports.sh
so it is testable and shellcheckable, and so one implementation serves both lanes.

#FindingDisposition
F1[a-z_]*port can't match pop3_port (digit in the key)Fixed — matches port / [a-z0-9_]+_port, anchored so a key merely ending in "port" can't slip in
F2v = $2 + 0 silently coerced 54_322→54 and "54322"→0Fixed — bare integers only (TOML _ separators accepted); anything else is reported by line and key as unprotected
F3Drain: wrong SO_REUSEADDR claim, 30s < 60s TIME-WAIT, ESTABLISHED called transientFixed — budget 75s; the message says SO_REUSEADDR needs the option on both sockets (an outbound client socket never set it, so Docker's bind really does fail) and that ESTABLISHED is a live connection, not a transient one
F4busy=$(sudo ss … || true) was fail-openFixed — the probe and both awk filters branch on their own exit status; a probe that can't run is reported as unknown, never as free
F5Ephemeral range hardcoded 32768-60999Fixed — read from /proc/sys/net/ipv4/ip_local_port_range; it filters the RESERVE set only, the CHECK set stays unfiltered
F6Leftover-container sweep wrong five waysReplaced — sweep deleted; supabase start gets e2e-up.sh's stop-and-retry instead, which also covers the exited/stale projects docker ps couldn't see
F7Fail/wait set included ports this config never bindsFixed — the CHECK/RESERVE split above; enabled-ness is derived per config section, so enabled = true on the pooler moves 54329 into the fail set with no edit
F8Reserved-ports merge was an unverified read-modify-writeFixed — a failed read skips the write rather than clobbering an unseen reservation; the merge keeps existing ranges intact; the value is read back and checked by membership (the kernel normalises to ranges — the live run prints 54320-54324,54327,54329, so a string compare would mismatch on every success)
F9Zero-grace LISTEN exit right after docker rmFalls away with F6
F10Summary said "(reserved …)" even when the sysctl failedFixed — three states (reserved / not reserved / no reservation needed) interpolated into the summary
F11"no ports parsed" message named only one causeFixed — the exit-1 names both: the step is obsolete, or the parser broke
F12Extract to scripts/preflight-ports.sh, call from both lanesDone — the workflow steps are now one line each
F13Ports hardcoded at four other sitesFollow-up #594 (deliberately not fixed here: this PR is CI hardening, that touches the local dev scripts)
F14No supabase start failure recovery in this laneFixed — the stop-and-retry above, mirroring scripts/e2e-up.sh:219-226
F15Decorative || trues, duplicated regex build, repeated indent pipeline, waited -eq 0 sentinelFixed — one indent() helper, one regex build, a named announced flag, no unguarded || true

Test evidence

Branch-coverage harness — 20 cases, 69 assertions, all green, run twice: under the default awk
and under gawk --posix (stand-in for the runner's mawk). It builds a throwaway repo root per case,
copies the real script in with only its two /proc constants repointed at fixtures (the patch is
grep-verified — a silently-failed patch produced a false GREEN in the previous round), and drives it
with sudo/ss shims. The sudo shim emulates the kernel: it expands ranges, stores a set, and
prints it back collapsed into ranges.

Covered: clean path · digit-bearing pop3_port protected · 54_322 parsed as 54322 · non-integer
value diagnosed by line+key · probe failure → WARNING, never "free" · sysctl fails → summary says NOT
reserved · sysctl "succeeds" but readback lacks the ports → WARNING · foreign LISTENer on an enabled
port → exit 1 naming the pid · LISTENer on the disabled pooler's port → no fail · same holder after
flipping the pooler to enabled = true → now fails (the derivation is live) · shadow_port reserved
but never checked · TIME-WAIT drain waits, announces once, proceeds clean · drain timeout → WARNING +
continue · no port keys → exit 1 naming both branches · ports outside the ephemeral range → nothing
to reserve, checks still run and still fail · unreadable reservation → write skipped · pre-existing
1024-1030 range survives the merge · no passwordless sudo → unprivileged probe · no ss
"unverified", not "free" · non-numeric drain timeout → loud fallback · a sudo shim that fails any call arriving without -n.

shellcheck 0.11.0 -xclean, zero findings (the previous round had no shellcheck available).

Re-review residuals — commit 53997446

The re-review passed all 15 findings and raised three non-blocking residuals, closed in one commit:

  • Every sudo call now uses -n. The probe gated on sudo -n true, but the sysctl write did
    not — a hand-run on a box without passwordless sudo would have blocked on a password prompt
    instead of degrading to the diagnostic half the header promises. The probe command is sudo -n ss
    too, closing the window where the gate passes and the timestamp expires before the probe.
  • The remaining command substitutions branch on their status (record split, merged-list build,
    readback membership) — in-memory string ops that realistically cannot fail, but that is the
    reasoning which produced the fail-open probe F4 condemned: a failed check_csv would have printed
    "no ENABLED service binds a port" and exited 0 having checked nothing, and an empty merged list
    would have cleared the bitmap it extends.
  • PREFLIGHT_DRAIN_TIMEOUT is validated as a whole number — a non-numeric value made every
    -ge test fail and spun the drain loop to the job timeout. It warns and falls back to 75.

Lane runs on this branch @ 53997446 — both green

LaneRunResult
integration.yml (dispatch)32934241228success — 71 passed, 2231 deselected (real work, zero skipped)
e2e.yml (dispatch)32934243435success — every step green, 73 passed (3.8m) Playwright

Plus the PR-triggered CI and
integration runs, both green.
(The previous SHA d992c12f was green on the same four lanes:
32932741569 /
32932743439.)

Both lanes printed the same three lines on the runner:

preflight: ephemeral source-port range 32768-60999
preflight: reserved 54320-54324,54327,54329
preflight: ports 54321,54322,54323,54324 free (enabled services; reserved from the ephemeral range)

Read that middle line closely: the kernel handed the value back range-collapsed, which is
exactly why F8's readback is a membership check and not a string compare — and the absence of a
mismatch WARNING is the membership check passing on real hardware. 54322 is inside 32768-60999
on GitHub's own runner, so the diagnosis holds where it matters, and it is re-verified in every run's
log forever.

Cannot prove a negative

One green run per lane does not prove the collision is gone — the failure rate was already low. What
the change guarantees is (a) the kernel will not hand out these ports as source ports any more,
which removes the mechanism, and (b) if a lane ever goes red on ports again, the log names the
culprit instead of leaving a bare address already in use.

Refs #574

AndresL230and others added 2 commits August 23, 2026 04:02
Run 32624263094 (push to main, 9f34454) died at `Start Supabase` with
`failed to bind host port for 0.0.0.0:54322: address already in use`, and
the identical commit re-run 40 minutes later passed.
The runner is a fresh VM, so nothing is left over. The mechanism is that
Supabase's ports (54320-54329) sit inside Linux's default ephemeral
source-port range (32768-60999): the attempt-1 log shows dozens of image
pulls finishing and the bind error landing in the same second, so an
outbound connection had been handed 54322 as its source port.
Reserve the ports out of automatic assignment (explicit bind() is
unaffected, so Docker can still publish them), derived from
supabase/config.toml so moving a port cannot un-protect it. Then diagnose
what reservation cannot undo: remove leftover containers publishing them,
fail by name on a foreign LISTENer, and wait out a draining socket.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The leftover-container cleanup was the one pipeline in the step without a
guard. Under the runner's `bash -e` (confirmed in run 32627212767's log:
`shell: /usr/bin/bash -e {0}`), a `docker rm -f` failure — permission, or a
race with another remover — aborted the step on the spot: no `preflight:`
diagnostic, no LISTEN check, no drain wait, no summary line. That is exactly
the opaque-CI-failure class this step exists to remove.
Warn and continue instead, consistent with the sysctl and drain branches;
the LISTEN check immediately after already names anything that survived.
Reproduced against the pre-fix commit: exit 123 with no output after the
docker error. With the guard: WARNING, then the step runs to completion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 23, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging5399744Commit Preview URL

Branch Preview URL
Aug 26 2026, 05:31 AM

@coderabbitai

coderabbitaiBot commented Aug 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e658fcf-c533-4321-9785-a9f8d9224359

📥 Commits

Reviewing files that changed from the base of the PR and between 9f34454 and d992c12.

📒 Files selected for processing (3)
  • .github/workflows/e2e.yml
  • .github/workflows/integration.yml
  • scripts/preflight-ports.sh

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds a CI-only Supabase port preflight script. E2E and integration workflows run it after checkout. The integration workflow also retries supabase start once after cleanup.

Changes

Supabase port preflight

Layer / File(s)Summary
Parse Supabase port configuration
scripts/preflight-ports.sh
The script validates prerequisites and parses enabled Supabase port settings from supabase/config.toml.
Reserve and verify ports
scripts/preflight-ports.sh
The script updates kernel reservations and probes configured ports. It reports foreign listeners, probe failures, and timeout results.
Run preflight and retry startup
.github/workflows/e2e.yml, .github/workflows/integration.yml, scripts/preflight-ports.sh
Both workflows run the preflight after checkout. The integration workflow stops Supabase and retries startup once after failure.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk:⚪ Minimal · up to d992c

This PR adds preflight protection for Supabase port collisions in CI and reports successful validation; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
participant GitHub Actions
participant preflight-ports.sh
participant Linux kernel
participant ss
participant Supabase CLI
GitHub Actions->>preflight-ports.sh: Run port preflight after checkout
preflight-ports.sh->>Linux kernel: Reserve configured ports
preflight-ports.sh->>ss: Probe configured bind ports
ss-->>preflight-ports.sh: Return socket status
preflight-ports.sh-->>GitHub Actions: Return preflight status
GitHub Actions->>Supabase CLI: Run supabase start
Supabase CLI-->>GitHub Actions: Return startup status
GitHub Actions->>Supabase CLI: Run supabase stop after failure
GitHub Actions->>Supabase CLI: Retry supabase start
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (2 skipped: 2 …
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly and concisely describes the main change: reserving Supabase ports before startup in both CI lanes.
Description check✅ PassedThe description is detailed and on-topic. It explains the failure, mechanism, implementation, affected workflows, testing evidence, issue reference, and review considerations. It does not use the repo…
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (2 skipped: 2 unsupported.)

Full details: Description check

Explanation

The description is detailed and on-topic. It explains the failure, mechanism, implementation, affected workflows, testing evidence, issue reference, and review considerations. It does not use the repository template headings exactly, but it provides the required information.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/integration-port-preflight

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@supabase

supabaseBot commented Aug 23, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

…sh (#574)
Merge-gate review of #588 found 15 issues in the inline step. The whole body
moves to scripts/preflight-ports.sh — testable, shellcheckable, and called by
BOTH lanes (integration.yml, and e2e.yml, which reaches `supabase start`
through `make e2e-up` and had the identical exposure).
What changed beyond the move:
- Two port sets, not one. RESERVE = every config.toml *port inside the LIVE
ephemeral range (read from /proc, not hardcoded). CHECK = the ports
`supabase start` actually binds — ENABLED sections only, minus shadow_port,
unfiltered by range. Only CHECK can fail or stall the job, so a holder on
the disabled pooler's 54329 no longer fails an unfiltered PR gate; enabled-
ness is derived per section, so flipping `enabled = true` is picked up.
- Parser: `[a-z_]*port` could not match the digit-bearing `pop3_port`; values
are now matched as bare integers (TOML underscores accepted) and anything
else is reported by line and key instead of being coerced to 0 and dropped.
- No more fail-open probes: the `ss` call and the awk filters branch on their
own exit status, so a probe that could not run never reads as "free".
- The sysctl is verified. A failed read of the current value skips the write
rather than clobbering a reservation it cannot see; the merge keeps existing
RANGES intact; the value is read back and checked by membership (the kernel
normalises to ranges, so a string compare would always mismatch); and the
summary line says NOT reserved when any of that fails.
- Drain: budget 30s -> 75s, because a TIME-WAIT entry lives a fixed 60s and
SO_REUSEADDR does NOT let a bind step over it (Linux needs the option on
both sockets, and an outbound connection never set it). ESTABLISHED is no
longer described as transient.
- The docker-container sweep is deleted — it was wrong in five ways where it
could fire. `supabase start` gets e2e-up.sh's stop-and-retry instead, which
also covers exited/stale projects the sweep could not see.
Refs #574
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230AndresL230 changed the title ci(integration): reserve the Supabase ports before supabase startci: reserve the Supabase ports before supabase start (both lanes)Aug 26, 2026
…guarded substitutions, validated timeout
Three non-blocking findings from the merge-gate re-review of #588:
- Every sudo call now uses `-n`. The socket probe already gated on
`sudo -n true`, but the sysctl write did not, so a hand-run on a box without
passwordless sudo would have blocked on a password prompt instead of failing
into the diagnostic half the header promises. The probe command itself is
`sudo -n ss` too, closing the window where the gate passes and the sudo
timestamp expires before the probe.
- The record-splitting command substitutions and the readback membership check
branch on their status like the probes do. They are in-memory string ops that
realistically cannot fail, but "realistically cannot fail" is the reasoning
that produced the fail-open probe this rework had to fix: a failed
`check_csv` pipeline would have printed "no ENABLED service binds a port" and
exited 0 having checked nothing, and a failed membership check would have
claimed the ports were reserved. The merged-list build is guarded for a
sharper reason still — an empty value would CLEAR the bitmap it extends.
- PREFLIGHT_DRAIN_TIMEOUT is validated as a whole number. A non-numeric value
made every `-ge` test fail and spun the drain loop to the job timeout — a
worse failure than the one being guarded. It warns and falls back to 75
rather than failing a lane over a typo.
Harness: 20 cases / 69 assertions green under both awk and `gawk --posix`,
including the three new ones (bad timeout value, empty override, and a sudo
shim that fails any call arriving without `-n`). shellcheck 0.11.0 clean.
Refs #574
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 6225bba into mainAug 26, 2026
10 checks passed
AndresL230 added a commit that referenced this pull request Aug 26, 2026
Brings in the include_answer_key default flip (#590), the dead
effective_explanations deletion (#587) and the CI port preflight (#588).
Clean auto-merge: main's routes/quiz.py hunks are in the generate
handler's response projection, this branch's are in the shared
gather/hoist above it.
Sign up for freeto 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.

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
ci: reserve the Supabase ports before `supabase start` (both lanes) by AndresL230 · Pull Request #588 · SaplingLearn/Sapling · GitHub
Skip to content

ci: reserve the Supabase ports before supabase start (both lanes) - #588

Merged
AndresL230 merged 4 commits into
mainfrom
ci/integration-port-preflight
Aug 26, 2026
Merged

ci: reserve the Supabase ports before supabase start (both lanes)#588
AndresL230 merged 4 commits into
mainfrom
ci/integration-port-preflight

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

What failed

Run 32624263094 (push to main,
9f34454) died at Start Supabase:

failed to start docker container "supabase_db_sapling": … failed to bind host port for
0.0.0.0:54322:172.18.0.2:5432/tcp: address already in use

The identical commit, re-run 40 minutes later, passed. No code cause — a day of signal went to
chasing it.

The mechanism

The runner is a fresh VM per job, so nothing is left over. But Supabase's ports (54320–54329) sit
inside Linux's default ephemeral source-port range, 32768–60999. Any outbound TCP connection the
job makes can be handed 54322 as its source port, and Docker's later bind(0.0.0.0:54322) then
fails.

The attempt-1 log times it exactly: the last of dozens of ghcr.io image pulls completes at
06:58:38.6156, Starting database... is logged 0.03s later, and the bind fails. Those pulls are
hundreds of outbound connections. One of them took the port.

Note the pulls happen insidesupabase start — so a pre-flight check would have passed. The fix
has to be pre-emptive.

What the guard does

scripts/preflight-ports.sh, called right after checkout in both lanes that boot Supabase —
integration.yml and e2e.yml (which reaches supabase start through make e2e-up
scripts/e2e-up.sh). Right after checkout is the earliest point supabase/config.toml exists, and
it puts every later download — setup-python, setup-cli, npm ci, playwright install — behind
the reservation. Bash only; no new actions or dependencies.

  1. Reserves the ports via net.ipv4.ip_local_reserved_ports, which excludes them from
    automatic source-port assignment while leaving explicit bind() untouched — so Docker can still
    publish them. Merges with any existing value (idempotent), then reads the value back and
    verifies membership before claiming anything is reserved.
  2. Fails fast if a foreign process is LISTENing on a port supabase start will actually bind,
    printing its state, address, pid and program. It never kills what isn't ours.
  3. Waits out a non-listening holder (up to 75s — a TIME-WAIT entry lives a fixed 60s), printing
    what it's waiting on: the one case reservation can't fix retroactively, because it was allocated
    before the step ran. On timeout it warns and proceeds rather than failing on a guess.
  4. Prints one greppable line: preflight: ports 54321,54322,54323,54324 free (enabled services; reserved from the ephemeral range) — and the word free only ever appears when the ports were
    observed free.

Two different port sets, both derived from supabase/config.toml rather than hardcoded:

  • RESERVE — every *port key inside the live ephemeral range (read from /proc, so the
    premise is re-verified in every run's log). Enabled or not: reserving a port nothing binds costs
    nothing, and it keeps the protection in place the day someone flips enabled = true.
  • CHECK — the ports supabase start will actually bind: *port keys of enabled sections,
    minus shadow_port (only db diff binds that), and not range-filtered. Only this set can fail
    or stall the job, so a holder on the disabled pooler's 54329 can't red an unfiltered PR gate.

supabase start also picks up e2e-up.sh's stop-and-retry, which covers stale/exited projects and
the preflight's admitted give-up path.


Merge-gate review rework (2026-08-26) — commit d992c12f

A /code-review at the merge gate returned 15 findings against the original inline step. All of
them are addressed here; the whole body moved out of the workflow into scripts/preflight-ports.sh
so it is testable and shellcheckable, and so one implementation serves both lanes.

#FindingDisposition
F1[a-z_]*port can't match pop3_port (digit in the key)Fixed — matches port / [a-z0-9_]+_port, anchored so a key merely ending in "port" can't slip in
F2v = $2 + 0 silently coerced 54_322→54 and "54322"→0Fixed — bare integers only (TOML _ separators accepted); anything else is reported by line and key as unprotected
F3Drain: wrong SO_REUSEADDR claim, 30s < 60s TIME-WAIT, ESTABLISHED called transientFixed — budget 75s; the message says SO_REUSEADDR needs the option on both sockets (an outbound client socket never set it, so Docker's bind really does fail) and that ESTABLISHED is a live connection, not a transient one
F4busy=$(sudo ss … || true) was fail-openFixed — the probe and both awk filters branch on their own exit status; a probe that can't run is reported as unknown, never as free
F5Ephemeral range hardcoded 32768-60999Fixed — read from /proc/sys/net/ipv4/ip_local_port_range; it filters the RESERVE set only, the CHECK set stays unfiltered
F6Leftover-container sweep wrong five waysReplaced — sweep deleted; supabase start gets e2e-up.sh's stop-and-retry instead, which also covers the exited/stale projects docker ps couldn't see
F7Fail/wait set included ports this config never bindsFixed — the CHECK/RESERVE split above; enabled-ness is derived per config section, so enabled = true on the pooler moves 54329 into the fail set with no edit
F8Reserved-ports merge was an unverified read-modify-writeFixed — a failed read skips the write rather than clobbering an unseen reservation; the merge keeps existing ranges intact; the value is read back and checked by membership (the kernel normalises to ranges — the live run prints 54320-54324,54327,54329, so a string compare would mismatch on every success)
F9Zero-grace LISTEN exit right after docker rmFalls away with F6
F10Summary said "(reserved …)" even when the sysctl failedFixed — three states (reserved / not reserved / no reservation needed) interpolated into the summary
F11"no ports parsed" message named only one causeFixed — the exit-1 names both: the step is obsolete, or the parser broke
F12Extract to scripts/preflight-ports.sh, call from both lanesDone — the workflow steps are now one line each
F13Ports hardcoded at four other sitesFollow-up #594 (deliberately not fixed here: this PR is CI hardening, that touches the local dev scripts)
F14No supabase start failure recovery in this laneFixed — the stop-and-retry above, mirroring scripts/e2e-up.sh:219-226
F15Decorative || trues, duplicated regex build, repeated indent pipeline, waited -eq 0 sentinelFixed — one indent() helper, one regex build, a named announced flag, no unguarded || true

Test evidence

Branch-coverage harness — 20 cases, 69 assertions, all green, run twice: under the default awk
and under gawk --posix (stand-in for the runner's mawk). It builds a throwaway repo root per case,
copies the real script in with only its two /proc constants repointed at fixtures (the patch is
grep-verified — a silently-failed patch produced a false GREEN in the previous round), and drives it
with sudo/ss shims. The sudo shim emulates the kernel: it expands ranges, stores a set, and
prints it back collapsed into ranges.

Covered: clean path · digit-bearing pop3_port protected · 54_322 parsed as 54322 · non-integer
value diagnosed by line+key · probe failure → WARNING, never "free" · sysctl fails → summary says NOT
reserved · sysctl "succeeds" but readback lacks the ports → WARNING · foreign LISTENer on an enabled
port → exit 1 naming the pid · LISTENer on the disabled pooler's port → no fail · same holder after
flipping the pooler to enabled = true → now fails (the derivation is live) · shadow_port reserved
but never checked · TIME-WAIT drain waits, announces once, proceeds clean · drain timeout → WARNING +
continue · no port keys → exit 1 naming both branches · ports outside the ephemeral range → nothing
to reserve, checks still run and still fail · unreadable reservation → write skipped · pre-existing
1024-1030 range survives the merge · no passwordless sudo → unprivileged probe · no ss
"unverified", not "free" · non-numeric drain timeout → loud fallback · a sudo shim that fails any call arriving without -n.

shellcheck 0.11.0 -xclean, zero findings (the previous round had no shellcheck available).

Re-review residuals — commit 53997446

The re-review passed all 15 findings and raised three non-blocking residuals, closed in one commit:

  • Every sudo call now uses -n. The probe gated on sudo -n true, but the sysctl write did
    not — a hand-run on a box without passwordless sudo would have blocked on a password prompt
    instead of degrading to the diagnostic half the header promises. The probe command is sudo -n ss
    too, closing the window where the gate passes and the timestamp expires before the probe.
  • The remaining command substitutions branch on their status (record split, merged-list build,
    readback membership) — in-memory string ops that realistically cannot fail, but that is the
    reasoning which produced the fail-open probe F4 condemned: a failed check_csv would have printed
    "no ENABLED service binds a port" and exited 0 having checked nothing, and an empty merged list
    would have cleared the bitmap it extends.
  • PREFLIGHT_DRAIN_TIMEOUT is validated as a whole number — a non-numeric value made every
    -ge test fail and spun the drain loop to the job timeout. It warns and falls back to 75.

Lane runs on this branch @ 53997446 — both green

LaneRunResult
integration.yml (dispatch)32934241228success — 71 passed, 2231 deselected (real work, zero skipped)
e2e.yml (dispatch)32934243435success — every step green, 73 passed (3.8m) Playwright

Plus the PR-triggered CI and
integration runs, both green.
(The previous SHA d992c12f was green on the same four lanes:
32932741569 /
32932743439.)

Both lanes printed the same three lines on the runner:

preflight: ephemeral source-port range 32768-60999
preflight: reserved 54320-54324,54327,54329
preflight: ports 54321,54322,54323,54324 free (enabled services; reserved from the ephemeral range)

Read that middle line closely: the kernel handed the value back range-collapsed, which is
exactly why F8's readback is a membership check and not a string compare — and the absence of a
mismatch WARNING is the membership check passing on real hardware. 54322 is inside 32768-60999
on GitHub's own runner, so the diagnosis holds where it matters, and it is re-verified in every run's
log forever.

Cannot prove a negative

One green run per lane does not prove the collision is gone — the failure rate was already low. What
the change guarantees is (a) the kernel will not hand out these ports as source ports any more,
which removes the mechanism, and (b) if a lane ever goes red on ports again, the log names the
culprit instead of leaving a bare address already in use.

Refs #574

AndresL230and others added 2 commits August 23, 2026 04:02
Run 32624263094 (push to main, 9f34454) died at `Start Supabase` with
`failed to bind host port for 0.0.0.0:54322: address already in use`, and
the identical commit re-run 40 minutes later passed.
The runner is a fresh VM, so nothing is left over. The mechanism is that
Supabase's ports (54320-54329) sit inside Linux's default ephemeral
source-port range (32768-60999): the attempt-1 log shows dozens of image
pulls finishing and the bind error landing in the same second, so an
outbound connection had been handed 54322 as its source port.
Reserve the ports out of automatic assignment (explicit bind() is
unaffected, so Docker can still publish them), derived from
supabase/config.toml so moving a port cannot un-protect it. Then diagnose
what reservation cannot undo: remove leftover containers publishing them,
fail by name on a foreign LISTENer, and wait out a draining socket.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The leftover-container cleanup was the one pipeline in the step without a
guard. Under the runner's `bash -e` (confirmed in run 32627212767's log:
`shell: /usr/bin/bash -e {0}`), a `docker rm -f` failure — permission, or a
race with another remover — aborted the step on the spot: no `preflight:`
diagnostic, no LISTEN check, no drain wait, no summary line. That is exactly
the opaque-CI-failure class this step exists to remove.
Warn and continue instead, consistent with the sysctl and drain branches;
the LISTEN check immediately after already names anything that survived.
Reproduced against the pre-fix commit: exit 123 with no output after the
docker error. With the guard: WARNING, then the step runs to completion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 23, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging5399744Commit Preview URL

Branch Preview URL
Aug 26 2026, 05:31 AM

@coderabbitai

coderabbitaiBot commented Aug 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e658fcf-c533-4321-9785-a9f8d9224359

📥 Commits

Reviewing files that changed from the base of the PR and between 9f34454 and d992c12.

📒 Files selected for processing (3)
  • .github/workflows/e2e.yml
  • .github/workflows/integration.yml
  • scripts/preflight-ports.sh

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds a CI-only Supabase port preflight script. E2E and integration workflows run it after checkout. The integration workflow also retries supabase start once after cleanup.

Changes

Supabase port preflight

Layer / File(s)Summary
Parse Supabase port configuration
scripts/preflight-ports.sh
The script validates prerequisites and parses enabled Supabase port settings from supabase/config.toml.
Reserve and verify ports
scripts/preflight-ports.sh
The script updates kernel reservations and probes configured ports. It reports foreign listeners, probe failures, and timeout results.
Run preflight and retry startup
.github/workflows/e2e.yml, .github/workflows/integration.yml, scripts/preflight-ports.sh
Both workflows run the preflight after checkout. The integration workflow stops Supabase and retries startup once after failure.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk:⚪ Minimal · up to d992c

This PR adds preflight protection for Supabase port collisions in CI and reports successful validation; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
participant GitHub Actions
participant preflight-ports.sh
participant Linux kernel
participant ss
participant Supabase CLI
GitHub Actions->>preflight-ports.sh: Run port preflight after checkout
preflight-ports.sh->>Linux kernel: Reserve configured ports
preflight-ports.sh->>ss: Probe configured bind ports
ss-->>preflight-ports.sh: Return socket status
preflight-ports.sh-->>GitHub Actions: Return preflight status
GitHub Actions->>Supabase CLI: Run supabase start
Supabase CLI-->>GitHub Actions: Return startup status
GitHub Actions->>Supabase CLI: Run supabase stop after failure
GitHub Actions->>Supabase CLI: Retry supabase start
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (2 skipped: 2 …
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly and concisely describes the main change: reserving Supabase ports before startup in both CI lanes.
Description check✅ PassedThe description is detailed and on-topic. It explains the failure, mechanism, implementation, affected workflows, testing evidence, issue reference, and review considerations. It does not use the repo…
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (2 skipped: 2 unsupported.)

Full details: Description check

Explanation

The description is detailed and on-topic. It explains the failure, mechanism, implementation, affected workflows, testing evidence, issue reference, and review considerations. It does not use the repository template headings exactly, but it provides the required information.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/integration-port-preflight

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@supabase

supabaseBot commented Aug 23, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

…sh (#574)
Merge-gate review of #588 found 15 issues in the inline step. The whole body
moves to scripts/preflight-ports.sh — testable, shellcheckable, and called by
BOTH lanes (integration.yml, and e2e.yml, which reaches `supabase start`
through `make e2e-up` and had the identical exposure).
What changed beyond the move:
- Two port sets, not one. RESERVE = every config.toml *port inside the LIVE
ephemeral range (read from /proc, not hardcoded). CHECK = the ports
`supabase start` actually binds — ENABLED sections only, minus shadow_port,
unfiltered by range. Only CHECK can fail or stall the job, so a holder on
the disabled pooler's 54329 no longer fails an unfiltered PR gate; enabled-
ness is derived per section, so flipping `enabled = true` is picked up.
- Parser: `[a-z_]*port` could not match the digit-bearing `pop3_port`; values
are now matched as bare integers (TOML underscores accepted) and anything
else is reported by line and key instead of being coerced to 0 and dropped.
- No more fail-open probes: the `ss` call and the awk filters branch on their
own exit status, so a probe that could not run never reads as "free".
- The sysctl is verified. A failed read of the current value skips the write
rather than clobbering a reservation it cannot see; the merge keeps existing
RANGES intact; the value is read back and checked by membership (the kernel
normalises to ranges, so a string compare would always mismatch); and the
summary line says NOT reserved when any of that fails.
- Drain: budget 30s -> 75s, because a TIME-WAIT entry lives a fixed 60s and
SO_REUSEADDR does NOT let a bind step over it (Linux needs the option on
both sockets, and an outbound connection never set it). ESTABLISHED is no
longer described as transient.
- The docker-container sweep is deleted — it was wrong in five ways where it
could fire. `supabase start` gets e2e-up.sh's stop-and-retry instead, which
also covers exited/stale projects the sweep could not see.
Refs #574
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230AndresL230 changed the title ci(integration): reserve the Supabase ports before supabase startci: reserve the Supabase ports before supabase start (both lanes)Aug 26, 2026
…guarded substitutions, validated timeout
Three non-blocking findings from the merge-gate re-review of #588:
- Every sudo call now uses `-n`. The socket probe already gated on
`sudo -n true`, but the sysctl write did not, so a hand-run on a box without
passwordless sudo would have blocked on a password prompt instead of failing
into the diagnostic half the header promises. The probe command itself is
`sudo -n ss` too, closing the window where the gate passes and the sudo
timestamp expires before the probe.
- The record-splitting command substitutions and the readback membership check
branch on their status like the probes do. They are in-memory string ops that
realistically cannot fail, but "realistically cannot fail" is the reasoning
that produced the fail-open probe this rework had to fix: a failed
`check_csv` pipeline would have printed "no ENABLED service binds a port" and
exited 0 having checked nothing, and a failed membership check would have
claimed the ports were reserved. The merged-list build is guarded for a
sharper reason still — an empty value would CLEAR the bitmap it extends.
- PREFLIGHT_DRAIN_TIMEOUT is validated as a whole number. A non-numeric value
made every `-ge` test fail and spun the drain loop to the job timeout — a
worse failure than the one being guarded. It warns and falls back to 75
rather than failing a lane over a typo.
Harness: 20 cases / 69 assertions green under both awk and `gawk --posix`,
including the three new ones (bad timeout value, empty override, and a sudo
shim that fails any call arriving without `-n`). shellcheck 0.11.0 clean.
Refs #574
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 6225bba into mainAug 26, 2026
10 checks passed
AndresL230 added a commit that referenced this pull request Aug 26, 2026
Brings in the include_answer_key default flip (#590), the dead
effective_explanations deletion (#587) and the CI port preflight (#588).
Clean auto-merge: main's routes/quiz.py hunks are in the generate
handler's response projection, this branch's are in the shared
gather/hoist above it.
Sign up for freeto 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.

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' ci: reserve the Supabase ports before `supabase start` (both lanes) by AndresL230 · Pull Request #588 · SaplingLearn/Sapling · GitHub
Skip to content

ci: reserve the Supabase ports before supabase start (both lanes) - #588

Merged
AndresL230 merged 4 commits into
mainfrom
ci/integration-port-preflight
Aug 26, 2026
Merged

ci: reserve the Supabase ports before supabase start (both lanes)#588
AndresL230 merged 4 commits into
mainfrom
ci/integration-port-preflight

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

What failed

Run 32624263094 (push to main,
9f34454) died at Start Supabase:

failed to start docker container "supabase_db_sapling": … failed to bind host port for
0.0.0.0:54322:172.18.0.2:5432/tcp: address already in use

The identical commit, re-run 40 minutes later, passed. No code cause — a day of signal went to
chasing it.

The mechanism

The runner is a fresh VM per job, so nothing is left over. But Supabase's ports (54320–54329) sit
inside Linux's default ephemeral source-port range, 32768–60999. Any outbound TCP connection the
job makes can be handed 54322 as its source port, and Docker's later bind(0.0.0.0:54322) then
fails.

The attempt-1 log times it exactly: the last of dozens of ghcr.io image pulls completes at
06:58:38.6156, Starting database... is logged 0.03s later, and the bind fails. Those pulls are
hundreds of outbound connections. One of them took the port.

Note the pulls happen insidesupabase start — so a pre-flight check would have passed. The fix
has to be pre-emptive.

What the guard does

scripts/preflight-ports.sh, called right after checkout in both lanes that boot Supabase —
integration.yml and e2e.yml (which reaches supabase start through make e2e-up
scripts/e2e-up.sh). Right after checkout is the earliest point supabase/config.toml exists, and
it puts every later download — setup-python, setup-cli, npm ci, playwright install — behind
the reservation. Bash only; no new actions or dependencies.

  1. Reserves the ports via net.ipv4.ip_local_reserved_ports, which excludes them from
    automatic source-port assignment while leaving explicit bind() untouched — so Docker can still
    publish them. Merges with any existing value (idempotent), then reads the value back and
    verifies membership before claiming anything is reserved.
  2. Fails fast if a foreign process is LISTENing on a port supabase start will actually bind,
    printing its state, address, pid and program. It never kills what isn't ours.
  3. Waits out a non-listening holder (up to 75s — a TIME-WAIT entry lives a fixed 60s), printing
    what it's waiting on: the one case reservation can't fix retroactively, because it was allocated
    before the step ran. On timeout it warns and proceeds rather than failing on a guess.
  4. Prints one greppable line: preflight: ports 54321,54322,54323,54324 free (enabled services; reserved from the ephemeral range) — and the word free only ever appears when the ports were
    observed free.

Two different port sets, both derived from supabase/config.toml rather than hardcoded:

  • RESERVE — every *port key inside the live ephemeral range (read from /proc, so the
    premise is re-verified in every run's log). Enabled or not: reserving a port nothing binds costs
    nothing, and it keeps the protection in place the day someone flips enabled = true.
  • CHECK — the ports supabase start will actually bind: *port keys of enabled sections,
    minus shadow_port (only db diff binds that), and not range-filtered. Only this set can fail
    or stall the job, so a holder on the disabled pooler's 54329 can't red an unfiltered PR gate.

supabase start also picks up e2e-up.sh's stop-and-retry, which covers stale/exited projects and
the preflight's admitted give-up path.


Merge-gate review rework (2026-08-26) — commit d992c12f

A /code-review at the merge gate returned 15 findings against the original inline step. All of
them are addressed here; the whole body moved out of the workflow into scripts/preflight-ports.sh
so it is testable and shellcheckable, and so one implementation serves both lanes.

#FindingDisposition
F1[a-z_]*port can't match pop3_port (digit in the key)Fixed — matches port / [a-z0-9_]+_port, anchored so a key merely ending in "port" can't slip in
F2v = $2 + 0 silently coerced 54_322→54 and "54322"→0Fixed — bare integers only (TOML _ separators accepted); anything else is reported by line and key as unprotected
F3Drain: wrong SO_REUSEADDR claim, 30s < 60s TIME-WAIT, ESTABLISHED called transientFixed — budget 75s; the message says SO_REUSEADDR needs the option on both sockets (an outbound client socket never set it, so Docker's bind really does fail) and that ESTABLISHED is a live connection, not a transient one
F4busy=$(sudo ss … || true) was fail-openFixed — the probe and both awk filters branch on their own exit status; a probe that can't run is reported as unknown, never as free
F5Ephemeral range hardcoded 32768-60999Fixed — read from /proc/sys/net/ipv4/ip_local_port_range; it filters the RESERVE set only, the CHECK set stays unfiltered
F6Leftover-container sweep wrong five waysReplaced — sweep deleted; supabase start gets e2e-up.sh's stop-and-retry instead, which also covers the exited/stale projects docker ps couldn't see
F7Fail/wait set included ports this config never bindsFixed — the CHECK/RESERVE split above; enabled-ness is derived per config section, so enabled = true on the pooler moves 54329 into the fail set with no edit
F8Reserved-ports merge was an unverified read-modify-writeFixed — a failed read skips the write rather than clobbering an unseen reservation; the merge keeps existing ranges intact; the value is read back and checked by membership (the kernel normalises to ranges — the live run prints 54320-54324,54327,54329, so a string compare would mismatch on every success)
F9Zero-grace LISTEN exit right after docker rmFalls away with F6
F10Summary said "(reserved …)" even when the sysctl failedFixed — three states (reserved / not reserved / no reservation needed) interpolated into the summary
F11"no ports parsed" message named only one causeFixed — the exit-1 names both: the step is obsolete, or the parser broke
F12Extract to scripts/preflight-ports.sh, call from both lanesDone — the workflow steps are now one line each
F13Ports hardcoded at four other sitesFollow-up #594 (deliberately not fixed here: this PR is CI hardening, that touches the local dev scripts)
F14No supabase start failure recovery in this laneFixed — the stop-and-retry above, mirroring scripts/e2e-up.sh:219-226
F15Decorative || trues, duplicated regex build, repeated indent pipeline, waited -eq 0 sentinelFixed — one indent() helper, one regex build, a named announced flag, no unguarded || true

Test evidence

Branch-coverage harness — 20 cases, 69 assertions, all green, run twice: under the default awk
and under gawk --posix (stand-in for the runner's mawk). It builds a throwaway repo root per case,
copies the real script in with only its two /proc constants repointed at fixtures (the patch is
grep-verified — a silently-failed patch produced a false GREEN in the previous round), and drives it
with sudo/ss shims. The sudo shim emulates the kernel: it expands ranges, stores a set, and
prints it back collapsed into ranges.

Covered: clean path · digit-bearing pop3_port protected · 54_322 parsed as 54322 · non-integer
value diagnosed by line+key · probe failure → WARNING, never "free" · sysctl fails → summary says NOT
reserved · sysctl "succeeds" but readback lacks the ports → WARNING · foreign LISTENer on an enabled
port → exit 1 naming the pid · LISTENer on the disabled pooler's port → no fail · same holder after
flipping the pooler to enabled = true → now fails (the derivation is live) · shadow_port reserved
but never checked · TIME-WAIT drain waits, announces once, proceeds clean · drain timeout → WARNING +
continue · no port keys → exit 1 naming both branches · ports outside the ephemeral range → nothing
to reserve, checks still run and still fail · unreadable reservation → write skipped · pre-existing
1024-1030 range survives the merge · no passwordless sudo → unprivileged probe · no ss
"unverified", not "free" · non-numeric drain timeout → loud fallback · a sudo shim that fails any call arriving without -n.

shellcheck 0.11.0 -xclean, zero findings (the previous round had no shellcheck available).

Re-review residuals — commit 53997446

The re-review passed all 15 findings and raised three non-blocking residuals, closed in one commit:

  • Every sudo call now uses -n. The probe gated on sudo -n true, but the sysctl write did
    not — a hand-run on a box without passwordless sudo would have blocked on a password prompt
    instead of degrading to the diagnostic half the header promises. The probe command is sudo -n ss
    too, closing the window where the gate passes and the timestamp expires before the probe.
  • The remaining command substitutions branch on their status (record split, merged-list build,
    readback membership) — in-memory string ops that realistically cannot fail, but that is the
    reasoning which produced the fail-open probe F4 condemned: a failed check_csv would have printed
    "no ENABLED service binds a port" and exited 0 having checked nothing, and an empty merged list
    would have cleared the bitmap it extends.
  • PREFLIGHT_DRAIN_TIMEOUT is validated as a whole number — a non-numeric value made every
    -ge test fail and spun the drain loop to the job timeout. It warns and falls back to 75.

Lane runs on this branch @ 53997446 — both green

LaneRunResult
integration.yml (dispatch)32934241228success — 71 passed, 2231 deselected (real work, zero skipped)
e2e.yml (dispatch)32934243435success — every step green, 73 passed (3.8m) Playwright

Plus the PR-triggered CI and
integration runs, both green.
(The previous SHA d992c12f was green on the same four lanes:
32932741569 /
32932743439.)

Both lanes printed the same three lines on the runner:

preflight: ephemeral source-port range 32768-60999
preflight: reserved 54320-54324,54327,54329
preflight: ports 54321,54322,54323,54324 free (enabled services; reserved from the ephemeral range)

Read that middle line closely: the kernel handed the value back range-collapsed, which is
exactly why F8's readback is a membership check and not a string compare — and the absence of a
mismatch WARNING is the membership check passing on real hardware. 54322 is inside 32768-60999
on GitHub's own runner, so the diagnosis holds where it matters, and it is re-verified in every run's
log forever.

Cannot prove a negative

One green run per lane does not prove the collision is gone — the failure rate was already low. What
the change guarantees is (a) the kernel will not hand out these ports as source ports any more,
which removes the mechanism, and (b) if a lane ever goes red on ports again, the log names the
culprit instead of leaving a bare address already in use.

Refs #574

AndresL230and others added 2 commits August 23, 2026 04:02
Run 32624263094 (push to main, 9f34454) died at `Start Supabase` with
`failed to bind host port for 0.0.0.0:54322: address already in use`, and
the identical commit re-run 40 minutes later passed.
The runner is a fresh VM, so nothing is left over. The mechanism is that
Supabase's ports (54320-54329) sit inside Linux's default ephemeral
source-port range (32768-60999): the attempt-1 log shows dozens of image
pulls finishing and the bind error landing in the same second, so an
outbound connection had been handed 54322 as its source port.
Reserve the ports out of automatic assignment (explicit bind() is
unaffected, so Docker can still publish them), derived from
supabase/config.toml so moving a port cannot un-protect it. Then diagnose
what reservation cannot undo: remove leftover containers publishing them,
fail by name on a foreign LISTENer, and wait out a draining socket.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The leftover-container cleanup was the one pipeline in the step without a
guard. Under the runner's `bash -e` (confirmed in run 32627212767's log:
`shell: /usr/bin/bash -e {0}`), a `docker rm -f` failure — permission, or a
race with another remover — aborted the step on the spot: no `preflight:`
diagnostic, no LISTEN check, no drain wait, no summary line. That is exactly
the opaque-CI-failure class this step exists to remove.
Warn and continue instead, consistent with the sysctl and drain branches;
the LISTEN check immediately after already names anything that survived.
Reproduced against the pre-fix commit: exit 123 with no output after the
docker error. With the guard: WARNING, then the step runs to completion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 23, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging5399744Commit Preview URL

Branch Preview URL
Aug 26 2026, 05:31 AM

@coderabbitai

coderabbitaiBot commented Aug 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e658fcf-c533-4321-9785-a9f8d9224359

📥 Commits

Reviewing files that changed from the base of the PR and between 9f34454 and d992c12.

📒 Files selected for processing (3)
  • .github/workflows/e2e.yml
  • .github/workflows/integration.yml
  • scripts/preflight-ports.sh

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds a CI-only Supabase port preflight script. E2E and integration workflows run it after checkout. The integration workflow also retries supabase start once after cleanup.

Changes

Supabase port preflight

Layer / File(s)Summary
Parse Supabase port configuration
scripts/preflight-ports.sh
The script validates prerequisites and parses enabled Supabase port settings from supabase/config.toml.
Reserve and verify ports
scripts/preflight-ports.sh
The script updates kernel reservations and probes configured ports. It reports foreign listeners, probe failures, and timeout results.
Run preflight and retry startup
.github/workflows/e2e.yml, .github/workflows/integration.yml, scripts/preflight-ports.sh
Both workflows run the preflight after checkout. The integration workflow stops Supabase and retries startup once after failure.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk:⚪ Minimal · up to d992c

This PR adds preflight protection for Supabase port collisions in CI and reports successful validation; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
participant GitHub Actions
participant preflight-ports.sh
participant Linux kernel
participant ss
participant Supabase CLI
GitHub Actions->>preflight-ports.sh: Run port preflight after checkout
preflight-ports.sh->>Linux kernel: Reserve configured ports
preflight-ports.sh->>ss: Probe configured bind ports
ss-->>preflight-ports.sh: Return socket status
preflight-ports.sh-->>GitHub Actions: Return preflight status
GitHub Actions->>Supabase CLI: Run supabase start
Supabase CLI-->>GitHub Actions: Return startup status
GitHub Actions->>Supabase CLI: Run supabase stop after failure
GitHub Actions->>Supabase CLI: Retry supabase start
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (2 skipped: 2 …
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly and concisely describes the main change: reserving Supabase ports before startup in both CI lanes.
Description check✅ PassedThe description is detailed and on-topic. It explains the failure, mechanism, implementation, affected workflows, testing evidence, issue reference, and review considerations. It does not use the repo…
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (2 skipped: 2 unsupported.)

Full details: Description check

Explanation

The description is detailed and on-topic. It explains the failure, mechanism, implementation, affected workflows, testing evidence, issue reference, and review considerations. It does not use the repository template headings exactly, but it provides the required information.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/integration-port-preflight

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@supabase

supabaseBot commented Aug 23, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

…sh (#574)
Merge-gate review of #588 found 15 issues in the inline step. The whole body
moves to scripts/preflight-ports.sh — testable, shellcheckable, and called by
BOTH lanes (integration.yml, and e2e.yml, which reaches `supabase start`
through `make e2e-up` and had the identical exposure).
What changed beyond the move:
- Two port sets, not one. RESERVE = every config.toml *port inside the LIVE
ephemeral range (read from /proc, not hardcoded). CHECK = the ports
`supabase start` actually binds — ENABLED sections only, minus shadow_port,
unfiltered by range. Only CHECK can fail or stall the job, so a holder on
the disabled pooler's 54329 no longer fails an unfiltered PR gate; enabled-
ness is derived per section, so flipping `enabled = true` is picked up.
- Parser: `[a-z_]*port` could not match the digit-bearing `pop3_port`; values
are now matched as bare integers (TOML underscores accepted) and anything
else is reported by line and key instead of being coerced to 0 and dropped.
- No more fail-open probes: the `ss` call and the awk filters branch on their
own exit status, so a probe that could not run never reads as "free".
- The sysctl is verified. A failed read of the current value skips the write
rather than clobbering a reservation it cannot see; the merge keeps existing
RANGES intact; the value is read back and checked by membership (the kernel
normalises to ranges, so a string compare would always mismatch); and the
summary line says NOT reserved when any of that fails.
- Drain: budget 30s -> 75s, because a TIME-WAIT entry lives a fixed 60s and
SO_REUSEADDR does NOT let a bind step over it (Linux needs the option on
both sockets, and an outbound connection never set it). ESTABLISHED is no
longer described as transient.
- The docker-container sweep is deleted — it was wrong in five ways where it
could fire. `supabase start` gets e2e-up.sh's stop-and-retry instead, which
also covers exited/stale projects the sweep could not see.
Refs #574
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230AndresL230 changed the title ci(integration): reserve the Supabase ports before supabase startci: reserve the Supabase ports before supabase start (both lanes)Aug 26, 2026
…guarded substitutions, validated timeout
Three non-blocking findings from the merge-gate re-review of #588:
- Every sudo call now uses `-n`. The socket probe already gated on
`sudo -n true`, but the sysctl write did not, so a hand-run on a box without
passwordless sudo would have blocked on a password prompt instead of failing
into the diagnostic half the header promises. The probe command itself is
`sudo -n ss` too, closing the window where the gate passes and the sudo
timestamp expires before the probe.
- The record-splitting command substitutions and the readback membership check
branch on their status like the probes do. They are in-memory string ops that
realistically cannot fail, but "realistically cannot fail" is the reasoning
that produced the fail-open probe this rework had to fix: a failed
`check_csv` pipeline would have printed "no ENABLED service binds a port" and
exited 0 having checked nothing, and a failed membership check would have
claimed the ports were reserved. The merged-list build is guarded for a
sharper reason still — an empty value would CLEAR the bitmap it extends.
- PREFLIGHT_DRAIN_TIMEOUT is validated as a whole number. A non-numeric value
made every `-ge` test fail and spun the drain loop to the job timeout — a
worse failure than the one being guarded. It warns and falls back to 75
rather than failing a lane over a typo.
Harness: 20 cases / 69 assertions green under both awk and `gawk --posix`,
including the three new ones (bad timeout value, empty override, and a sudo
shim that fails any call arriving without `-n`). shellcheck 0.11.0 clean.
Refs #574
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 6225bba into mainAug 26, 2026
10 checks passed
AndresL230 added a commit that referenced this pull request Aug 26, 2026
Brings in the include_answer_key default flip (#590), the dead
effective_explanations deletion (#587) and the CI port preflight (#588).
Clean auto-merge: main's routes/quiz.py hunks are in the generate
handler's response projection, this branch's are in the shared
gather/hoist above it.
Sign up for freeto 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.

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' ci: reserve the Supabase ports before `supabase start` (both lanes) by AndresL230 · Pull Request #588 · SaplingLearn/Sapling · GitHub
Skip to content

ci: reserve the Supabase ports before supabase start (both lanes) - #588

Merged
AndresL230 merged 4 commits into
mainfrom
ci/integration-port-preflight
Aug 26, 2026
Merged

ci: reserve the Supabase ports before supabase start (both lanes)#588
AndresL230 merged 4 commits into
mainfrom
ci/integration-port-preflight

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

What failed

Run 32624263094 (push to main,
9f34454) died at Start Supabase:

failed to start docker container "supabase_db_sapling": … failed to bind host port for
0.0.0.0:54322:172.18.0.2:5432/tcp: address already in use

The identical commit, re-run 40 minutes later, passed. No code cause — a day of signal went to
chasing it.

The mechanism

The runner is a fresh VM per job, so nothing is left over. But Supabase's ports (54320–54329) sit
inside Linux's default ephemeral source-port range, 32768–60999. Any outbound TCP connection the
job makes can be handed 54322 as its source port, and Docker's later bind(0.0.0.0:54322) then
fails.

The attempt-1 log times it exactly: the last of dozens of ghcr.io image pulls completes at
06:58:38.6156, Starting database... is logged 0.03s later, and the bind fails. Those pulls are
hundreds of outbound connections. One of them took the port.

Note the pulls happen insidesupabase start — so a pre-flight check would have passed. The fix
has to be pre-emptive.

What the guard does

scripts/preflight-ports.sh, called right after checkout in both lanes that boot Supabase —
integration.yml and e2e.yml (which reaches supabase start through make e2e-up
scripts/e2e-up.sh). Right after checkout is the earliest point supabase/config.toml exists, and
it puts every later download — setup-python, setup-cli, npm ci, playwright install — behind
the reservation. Bash only; no new actions or dependencies.

  1. Reserves the ports via net.ipv4.ip_local_reserved_ports, which excludes them from
    automatic source-port assignment while leaving explicit bind() untouched — so Docker can still
    publish them. Merges with any existing value (idempotent), then reads the value back and
    verifies membership before claiming anything is reserved.
  2. Fails fast if a foreign process is LISTENing on a port supabase start will actually bind,
    printing its state, address, pid and program. It never kills what isn't ours.
  3. Waits out a non-listening holder (up to 75s — a TIME-WAIT entry lives a fixed 60s), printing
    what it's waiting on: the one case reservation can't fix retroactively, because it was allocated
    before the step ran. On timeout it warns and proceeds rather than failing on a guess.
  4. Prints one greppable line: preflight: ports 54321,54322,54323,54324 free (enabled services; reserved from the ephemeral range) — and the word free only ever appears when the ports were
    observed free.

Two different port sets, both derived from supabase/config.toml rather than hardcoded:

  • RESERVE — every *port key inside the live ephemeral range (read from /proc, so the
    premise is re-verified in every run's log). Enabled or not: reserving a port nothing binds costs
    nothing, and it keeps the protection in place the day someone flips enabled = true.
  • CHECK — the ports supabase start will actually bind: *port keys of enabled sections,
    minus shadow_port (only db diff binds that), and not range-filtered. Only this set can fail
    or stall the job, so a holder on the disabled pooler's 54329 can't red an unfiltered PR gate.

supabase start also picks up e2e-up.sh's stop-and-retry, which covers stale/exited projects and
the preflight's admitted give-up path.


Merge-gate review rework (2026-08-26) — commit d992c12f

A /code-review at the merge gate returned 15 findings against the original inline step. All of
them are addressed here; the whole body moved out of the workflow into scripts/preflight-ports.sh
so it is testable and shellcheckable, and so one implementation serves both lanes.

#FindingDisposition
F1[a-z_]*port can't match pop3_port (digit in the key)Fixed — matches port / [a-z0-9_]+_port, anchored so a key merely ending in "port" can't slip in
F2v = $2 + 0 silently coerced 54_322→54 and "54322"→0Fixed — bare integers only (TOML _ separators accepted); anything else is reported by line and key as unprotected
F3Drain: wrong SO_REUSEADDR claim, 30s < 60s TIME-WAIT, ESTABLISHED called transientFixed — budget 75s; the message says SO_REUSEADDR needs the option on both sockets (an outbound client socket never set it, so Docker's bind really does fail) and that ESTABLISHED is a live connection, not a transient one
F4busy=$(sudo ss … || true) was fail-openFixed — the probe and both awk filters branch on their own exit status; a probe that can't run is reported as unknown, never as free
F5Ephemeral range hardcoded 32768-60999Fixed — read from /proc/sys/net/ipv4/ip_local_port_range; it filters the RESERVE set only, the CHECK set stays unfiltered
F6Leftover-container sweep wrong five waysReplaced — sweep deleted; supabase start gets e2e-up.sh's stop-and-retry instead, which also covers the exited/stale projects docker ps couldn't see
F7Fail/wait set included ports this config never bindsFixed — the CHECK/RESERVE split above; enabled-ness is derived per config section, so enabled = true on the pooler moves 54329 into the fail set with no edit
F8Reserved-ports merge was an unverified read-modify-writeFixed — a failed read skips the write rather than clobbering an unseen reservation; the merge keeps existing ranges intact; the value is read back and checked by membership (the kernel normalises to ranges — the live run prints 54320-54324,54327,54329, so a string compare would mismatch on every success)
F9Zero-grace LISTEN exit right after docker rmFalls away with F6
F10Summary said "(reserved …)" even when the sysctl failedFixed — three states (reserved / not reserved / no reservation needed) interpolated into the summary
F11"no ports parsed" message named only one causeFixed — the exit-1 names both: the step is obsolete, or the parser broke
F12Extract to scripts/preflight-ports.sh, call from both lanesDone — the workflow steps are now one line each
F13Ports hardcoded at four other sitesFollow-up #594 (deliberately not fixed here: this PR is CI hardening, that touches the local dev scripts)
F14No supabase start failure recovery in this laneFixed — the stop-and-retry above, mirroring scripts/e2e-up.sh:219-226
F15Decorative || trues, duplicated regex build, repeated indent pipeline, waited -eq 0 sentinelFixed — one indent() helper, one regex build, a named announced flag, no unguarded || true

Test evidence

Branch-coverage harness — 20 cases, 69 assertions, all green, run twice: under the default awk
and under gawk --posix (stand-in for the runner's mawk). It builds a throwaway repo root per case,
copies the real script in with only its two /proc constants repointed at fixtures (the patch is
grep-verified — a silently-failed patch produced a false GREEN in the previous round), and drives it
with sudo/ss shims. The sudo shim emulates the kernel: it expands ranges, stores a set, and
prints it back collapsed into ranges.

Covered: clean path · digit-bearing pop3_port protected · 54_322 parsed as 54322 · non-integer
value diagnosed by line+key · probe failure → WARNING, never "free" · sysctl fails → summary says NOT
reserved · sysctl "succeeds" but readback lacks the ports → WARNING · foreign LISTENer on an enabled
port → exit 1 naming the pid · LISTENer on the disabled pooler's port → no fail · same holder after
flipping the pooler to enabled = true → now fails (the derivation is live) · shadow_port reserved
but never checked · TIME-WAIT drain waits, announces once, proceeds clean · drain timeout → WARNING +
continue · no port keys → exit 1 naming both branches · ports outside the ephemeral range → nothing
to reserve, checks still run and still fail · unreadable reservation → write skipped · pre-existing
1024-1030 range survives the merge · no passwordless sudo → unprivileged probe · no ss
"unverified", not "free" · non-numeric drain timeout → loud fallback · a sudo shim that fails any call arriving without -n.

shellcheck 0.11.0 -xclean, zero findings (the previous round had no shellcheck available).

Re-review residuals — commit 53997446

The re-review passed all 15 findings and raised three non-blocking residuals, closed in one commit:

  • Every sudo call now uses -n. The probe gated on sudo -n true, but the sysctl write did
    not — a hand-run on a box without passwordless sudo would have blocked on a password prompt
    instead of degrading to the diagnostic half the header promises. The probe command is sudo -n ss
    too, closing the window where the gate passes and the timestamp expires before the probe.
  • The remaining command substitutions branch on their status (record split, merged-list build,
    readback membership) — in-memory string ops that realistically cannot fail, but that is the
    reasoning which produced the fail-open probe F4 condemned: a failed check_csv would have printed
    "no ENABLED service binds a port" and exited 0 having checked nothing, and an empty merged list
    would have cleared the bitmap it extends.
  • PREFLIGHT_DRAIN_TIMEOUT is validated as a whole number — a non-numeric value made every
    -ge test fail and spun the drain loop to the job timeout. It warns and falls back to 75.

Lane runs on this branch @ 53997446 — both green

LaneRunResult
integration.yml (dispatch)32934241228success — 71 passed, 2231 deselected (real work, zero skipped)
e2e.yml (dispatch)32934243435success — every step green, 73 passed (3.8m) Playwright

Plus the PR-triggered CI and
integration runs, both green.
(The previous SHA d992c12f was green on the same four lanes:
32932741569 /
32932743439.)

Both lanes printed the same three lines on the runner:

preflight: ephemeral source-port range 32768-60999
preflight: reserved 54320-54324,54327,54329
preflight: ports 54321,54322,54323,54324 free (enabled services; reserved from the ephemeral range)

Read that middle line closely: the kernel handed the value back range-collapsed, which is
exactly why F8's readback is a membership check and not a string compare — and the absence of a
mismatch WARNING is the membership check passing on real hardware. 54322 is inside 32768-60999
on GitHub's own runner, so the diagnosis holds where it matters, and it is re-verified in every run's
log forever.

Cannot prove a negative

One green run per lane does not prove the collision is gone — the failure rate was already low. What
the change guarantees is (a) the kernel will not hand out these ports as source ports any more,
which removes the mechanism, and (b) if a lane ever goes red on ports again, the log names the
culprit instead of leaving a bare address already in use.

Refs #574

AndresL230and others added 2 commits August 23, 2026 04:02
Run 32624263094 (push to main, 9f34454) died at `Start Supabase` with
`failed to bind host port for 0.0.0.0:54322: address already in use`, and
the identical commit re-run 40 minutes later passed.
The runner is a fresh VM, so nothing is left over. The mechanism is that
Supabase's ports (54320-54329) sit inside Linux's default ephemeral
source-port range (32768-60999): the attempt-1 log shows dozens of image
pulls finishing and the bind error landing in the same second, so an
outbound connection had been handed 54322 as its source port.
Reserve the ports out of automatic assignment (explicit bind() is
unaffected, so Docker can still publish them), derived from
supabase/config.toml so moving a port cannot un-protect it. Then diagnose
what reservation cannot undo: remove leftover containers publishing them,
fail by name on a foreign LISTENer, and wait out a draining socket.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The leftover-container cleanup was the one pipeline in the step without a
guard. Under the runner's `bash -e` (confirmed in run 32627212767's log:
`shell: /usr/bin/bash -e {0}`), a `docker rm -f` failure — permission, or a
race with another remover — aborted the step on the spot: no `preflight:`
diagnostic, no LISTEN check, no drain wait, no summary line. That is exactly
the opaque-CI-failure class this step exists to remove.
Warn and continue instead, consistent with the sysctl and drain branches;
the LISTEN check immediately after already names anything that survived.
Reproduced against the pre-fix commit: exit 123 with no output after the
docker error. With the guard: WARNING, then the step runs to completion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 23, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging5399744Commit Preview URL

Branch Preview URL
Aug 26 2026, 05:31 AM

@coderabbitai

coderabbitaiBot commented Aug 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e658fcf-c533-4321-9785-a9f8d9224359

📥 Commits

Reviewing files that changed from the base of the PR and between 9f34454 and d992c12.

📒 Files selected for processing (3)
  • .github/workflows/e2e.yml
  • .github/workflows/integration.yml
  • scripts/preflight-ports.sh

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds a CI-only Supabase port preflight script. E2E and integration workflows run it after checkout. The integration workflow also retries supabase start once after cleanup.

Changes

Supabase port preflight

Layer / File(s)Summary
Parse Supabase port configuration
scripts/preflight-ports.sh
The script validates prerequisites and parses enabled Supabase port settings from supabase/config.toml.
Reserve and verify ports
scripts/preflight-ports.sh
The script updates kernel reservations and probes configured ports. It reports foreign listeners, probe failures, and timeout results.
Run preflight and retry startup
.github/workflows/e2e.yml, .github/workflows/integration.yml, scripts/preflight-ports.sh
Both workflows run the preflight after checkout. The integration workflow stops Supabase and retries startup once after failure.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk:⚪ Minimal · up to d992c

This PR adds preflight protection for Supabase port collisions in CI and reports successful validation; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
participant GitHub Actions
participant preflight-ports.sh
participant Linux kernel
participant ss
participant Supabase CLI
GitHub Actions->>preflight-ports.sh: Run port preflight after checkout
preflight-ports.sh->>Linux kernel: Reserve configured ports
preflight-ports.sh->>ss: Probe configured bind ports
ss-->>preflight-ports.sh: Return socket status
preflight-ports.sh-->>GitHub Actions: Return preflight status
GitHub Actions->>Supabase CLI: Run supabase start
Supabase CLI-->>GitHub Actions: Return startup status
GitHub Actions->>Supabase CLI: Run supabase stop after failure
GitHub Actions->>Supabase CLI: Retry supabase start
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (2 skipped: 2 …
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly and concisely describes the main change: reserving Supabase ports before startup in both CI lanes.
Description check✅ PassedThe description is detailed and on-topic. It explains the failure, mechanism, implementation, affected workflows, testing evidence, issue reference, and review considerations. It does not use the repo…
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (2 skipped: 2 unsupported.)

Full details: Description check

Explanation

The description is detailed and on-topic. It explains the failure, mechanism, implementation, affected workflows, testing evidence, issue reference, and review considerations. It does not use the repository template headings exactly, but it provides the required information.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/integration-port-preflight

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@supabase

supabaseBot commented Aug 23, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

…sh (#574)
Merge-gate review of #588 found 15 issues in the inline step. The whole body
moves to scripts/preflight-ports.sh — testable, shellcheckable, and called by
BOTH lanes (integration.yml, and e2e.yml, which reaches `supabase start`
through `make e2e-up` and had the identical exposure).
What changed beyond the move:
- Two port sets, not one. RESERVE = every config.toml *port inside the LIVE
ephemeral range (read from /proc, not hardcoded). CHECK = the ports
`supabase start` actually binds — ENABLED sections only, minus shadow_port,
unfiltered by range. Only CHECK can fail or stall the job, so a holder on
the disabled pooler's 54329 no longer fails an unfiltered PR gate; enabled-
ness is derived per section, so flipping `enabled = true` is picked up.
- Parser: `[a-z_]*port` could not match the digit-bearing `pop3_port`; values
are now matched as bare integers (TOML underscores accepted) and anything
else is reported by line and key instead of being coerced to 0 and dropped.
- No more fail-open probes: the `ss` call and the awk filters branch on their
own exit status, so a probe that could not run never reads as "free".
- The sysctl is verified. A failed read of the current value skips the write
rather than clobbering a reservation it cannot see; the merge keeps existing
RANGES intact; the value is read back and checked by membership (the kernel
normalises to ranges, so a string compare would always mismatch); and the
summary line says NOT reserved when any of that fails.
- Drain: budget 30s -> 75s, because a TIME-WAIT entry lives a fixed 60s and
SO_REUSEADDR does NOT let a bind step over it (Linux needs the option on
both sockets, and an outbound connection never set it). ESTABLISHED is no
longer described as transient.
- The docker-container sweep is deleted — it was wrong in five ways where it
could fire. `supabase start` gets e2e-up.sh's stop-and-retry instead, which
also covers exited/stale projects the sweep could not see.
Refs #574
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230AndresL230 changed the title ci(integration): reserve the Supabase ports before supabase startci: reserve the Supabase ports before supabase start (both lanes)Aug 26, 2026
…guarded substitutions, validated timeout
Three non-blocking findings from the merge-gate re-review of #588:
- Every sudo call now uses `-n`. The socket probe already gated on
`sudo -n true`, but the sysctl write did not, so a hand-run on a box without
passwordless sudo would have blocked on a password prompt instead of failing
into the diagnostic half the header promises. The probe command itself is
`sudo -n ss` too, closing the window where the gate passes and the sudo
timestamp expires before the probe.
- The record-splitting command substitutions and the readback membership check
branch on their status like the probes do. They are in-memory string ops that
realistically cannot fail, but "realistically cannot fail" is the reasoning
that produced the fail-open probe this rework had to fix: a failed
`check_csv` pipeline would have printed "no ENABLED service binds a port" and
exited 0 having checked nothing, and a failed membership check would have
claimed the ports were reserved. The merged-list build is guarded for a
sharper reason still — an empty value would CLEAR the bitmap it extends.
- PREFLIGHT_DRAIN_TIMEOUT is validated as a whole number. A non-numeric value
made every `-ge` test fail and spun the drain loop to the job timeout — a
worse failure than the one being guarded. It warns and falls back to 75
rather than failing a lane over a typo.
Harness: 20 cases / 69 assertions green under both awk and `gawk --posix`,
including the three new ones (bad timeout value, empty override, and a sudo
shim that fails any call arriving without `-n`). shellcheck 0.11.0 clean.
Refs #574
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 6225bba into mainAug 26, 2026
10 checks passed
AndresL230 added a commit that referenced this pull request Aug 26, 2026
Brings in the include_answer_key default flip (#590), the dead
effective_explanations deletion (#587) and the CI port preflight (#588).
Clean auto-merge: main's routes/quiz.py hunks are in the generate
handler's response projection, this branch's are in the shared
gather/hoist above it.
Sign up for freeto 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.

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' ci: reserve the Supabase ports before `supabase start` (both lanes) by AndresL230 · Pull Request #588 · SaplingLearn/Sapling · GitHub
Skip to content

ci: reserve the Supabase ports before supabase start (both lanes) - #588

Merged
AndresL230 merged 4 commits into
mainfrom
ci/integration-port-preflight
Aug 26, 2026
Merged

ci: reserve the Supabase ports before supabase start (both lanes)#588
AndresL230 merged 4 commits into
mainfrom
ci/integration-port-preflight

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

What failed

Run 32624263094 (push to main,
9f34454) died at Start Supabase:

failed to start docker container "supabase_db_sapling": … failed to bind host port for
0.0.0.0:54322:172.18.0.2:5432/tcp: address already in use

The identical commit, re-run 40 minutes later, passed. No code cause — a day of signal went to
chasing it.

The mechanism

The runner is a fresh VM per job, so nothing is left over. But Supabase's ports (54320–54329) sit
inside Linux's default ephemeral source-port range, 32768–60999. Any outbound TCP connection the
job makes can be handed 54322 as its source port, and Docker's later bind(0.0.0.0:54322) then
fails.

The attempt-1 log times it exactly: the last of dozens of ghcr.io image pulls completes at
06:58:38.6156, Starting database... is logged 0.03s later, and the bind fails. Those pulls are
hundreds of outbound connections. One of them took the port.

Note the pulls happen insidesupabase start — so a pre-flight check would have passed. The fix
has to be pre-emptive.

What the guard does

scripts/preflight-ports.sh, called right after checkout in both lanes that boot Supabase —
integration.yml and e2e.yml (which reaches supabase start through make e2e-up
scripts/e2e-up.sh). Right after checkout is the earliest point supabase/config.toml exists, and
it puts every later download — setup-python, setup-cli, npm ci, playwright install — behind
the reservation. Bash only; no new actions or dependencies.

  1. Reserves the ports via net.ipv4.ip_local_reserved_ports, which excludes them from
    automatic source-port assignment while leaving explicit bind() untouched — so Docker can still
    publish them. Merges with any existing value (idempotent), then reads the value back and
    verifies membership before claiming anything is reserved.
  2. Fails fast if a foreign process is LISTENing on a port supabase start will actually bind,
    printing its state, address, pid and program. It never kills what isn't ours.
  3. Waits out a non-listening holder (up to 75s — a TIME-WAIT entry lives a fixed 60s), printing
    what it's waiting on: the one case reservation can't fix retroactively, because it was allocated
    before the step ran. On timeout it warns and proceeds rather than failing on a guess.
  4. Prints one greppable line: preflight: ports 54321,54322,54323,54324 free (enabled services; reserved from the ephemeral range) — and the word free only ever appears when the ports were
    observed free.

Two different port sets, both derived from supabase/config.toml rather than hardcoded:

  • RESERVE — every *port key inside the live ephemeral range (read from /proc, so the
    premise is re-verified in every run's log). Enabled or not: reserving a port nothing binds costs
    nothing, and it keeps the protection in place the day someone flips enabled = true.
  • CHECK — the ports supabase start will actually bind: *port keys of enabled sections,
    minus shadow_port (only db diff binds that), and not range-filtered. Only this set can fail
    or stall the job, so a holder on the disabled pooler's 54329 can't red an unfiltered PR gate.

supabase start also picks up e2e-up.sh's stop-and-retry, which covers stale/exited projects and
the preflight's admitted give-up path.


Merge-gate review rework (2026-08-26) — commit d992c12f

A /code-review at the merge gate returned 15 findings against the original inline step. All of
them are addressed here; the whole body moved out of the workflow into scripts/preflight-ports.sh
so it is testable and shellcheckable, and so one implementation serves both lanes.

#FindingDisposition
F1[a-z_]*port can't match pop3_port (digit in the key)Fixed — matches port / [a-z0-9_]+_port, anchored so a key merely ending in "port" can't slip in
F2v = $2 + 0 silently coerced 54_322→54 and "54322"→0Fixed — bare integers only (TOML _ separators accepted); anything else is reported by line and key as unprotected
F3Drain: wrong SO_REUSEADDR claim, 30s < 60s TIME-WAIT, ESTABLISHED called transientFixed — budget 75s; the message says SO_REUSEADDR needs the option on both sockets (an outbound client socket never set it, so Docker's bind really does fail) and that ESTABLISHED is a live connection, not a transient one
F4busy=$(sudo ss … || true) was fail-openFixed — the probe and both awk filters branch on their own exit status; a probe that can't run is reported as unknown, never as free
F5Ephemeral range hardcoded 32768-60999Fixed — read from /proc/sys/net/ipv4/ip_local_port_range; it filters the RESERVE set only, the CHECK set stays unfiltered
F6Leftover-container sweep wrong five waysReplaced — sweep deleted; supabase start gets e2e-up.sh's stop-and-retry instead, which also covers the exited/stale projects docker ps couldn't see
F7Fail/wait set included ports this config never bindsFixed — the CHECK/RESERVE split above; enabled-ness is derived per config section, so enabled = true on the pooler moves 54329 into the fail set with no edit
F8Reserved-ports merge was an unverified read-modify-writeFixed — a failed read skips the write rather than clobbering an unseen reservation; the merge keeps existing ranges intact; the value is read back and checked by membership (the kernel normalises to ranges — the live run prints 54320-54324,54327,54329, so a string compare would mismatch on every success)
F9Zero-grace LISTEN exit right after docker rmFalls away with F6
F10Summary said "(reserved …)" even when the sysctl failedFixed — three states (reserved / not reserved / no reservation needed) interpolated into the summary
F11"no ports parsed" message named only one causeFixed — the exit-1 names both: the step is obsolete, or the parser broke
F12Extract to scripts/preflight-ports.sh, call from both lanesDone — the workflow steps are now one line each
F13Ports hardcoded at four other sitesFollow-up #594 (deliberately not fixed here: this PR is CI hardening, that touches the local dev scripts)
F14No supabase start failure recovery in this laneFixed — the stop-and-retry above, mirroring scripts/e2e-up.sh:219-226
F15Decorative || trues, duplicated regex build, repeated indent pipeline, waited -eq 0 sentinelFixed — one indent() helper, one regex build, a named announced flag, no unguarded || true

Test evidence

Branch-coverage harness — 20 cases, 69 assertions, all green, run twice: under the default awk
and under gawk --posix (stand-in for the runner's mawk). It builds a throwaway repo root per case,
copies the real script in with only its two /proc constants repointed at fixtures (the patch is
grep-verified — a silently-failed patch produced a false GREEN in the previous round), and drives it
with sudo/ss shims. The sudo shim emulates the kernel: it expands ranges, stores a set, and
prints it back collapsed into ranges.

Covered: clean path · digit-bearing pop3_port protected · 54_322 parsed as 54322 · non-integer
value diagnosed by line+key · probe failure → WARNING, never "free" · sysctl fails → summary says NOT
reserved · sysctl "succeeds" but readback lacks the ports → WARNING · foreign LISTENer on an enabled
port → exit 1 naming the pid · LISTENer on the disabled pooler's port → no fail · same holder after
flipping the pooler to enabled = true → now fails (the derivation is live) · shadow_port reserved
but never checked · TIME-WAIT drain waits, announces once, proceeds clean · drain timeout → WARNING +
continue · no port keys → exit 1 naming both branches · ports outside the ephemeral range → nothing
to reserve, checks still run and still fail · unreadable reservation → write skipped · pre-existing
1024-1030 range survives the merge · no passwordless sudo → unprivileged probe · no ss
"unverified", not "free" · non-numeric drain timeout → loud fallback · a sudo shim that fails any call arriving without -n.

shellcheck 0.11.0 -xclean, zero findings (the previous round had no shellcheck available).

Re-review residuals — commit 53997446

The re-review passed all 15 findings and raised three non-blocking residuals, closed in one commit:

  • Every sudo call now uses -n. The probe gated on sudo -n true, but the sysctl write did
    not — a hand-run on a box without passwordless sudo would have blocked on a password prompt
    instead of degrading to the diagnostic half the header promises. The probe command is sudo -n ss
    too, closing the window where the gate passes and the timestamp expires before the probe.
  • The remaining command substitutions branch on their status (record split, merged-list build,
    readback membership) — in-memory string ops that realistically cannot fail, but that is the
    reasoning which produced the fail-open probe F4 condemned: a failed check_csv would have printed
    "no ENABLED service binds a port" and exited 0 having checked nothing, and an empty merged list
    would have cleared the bitmap it extends.
  • PREFLIGHT_DRAIN_TIMEOUT is validated as a whole number — a non-numeric value made every
    -ge test fail and spun the drain loop to the job timeout. It warns and falls back to 75.

Lane runs on this branch @ 53997446 — both green

LaneRunResult
integration.yml (dispatch)32934241228success — 71 passed, 2231 deselected (real work, zero skipped)
e2e.yml (dispatch)32934243435success — every step green, 73 passed (3.8m) Playwright

Plus the PR-triggered CI and
integration runs, both green.
(The previous SHA d992c12f was green on the same four lanes:
32932741569 /
32932743439.)

Both lanes printed the same three lines on the runner:

preflight: ephemeral source-port range 32768-60999
preflight: reserved 54320-54324,54327,54329
preflight: ports 54321,54322,54323,54324 free (enabled services; reserved from the ephemeral range)

Read that middle line closely: the kernel handed the value back range-collapsed, which is
exactly why F8's readback is a membership check and not a string compare — and the absence of a
mismatch WARNING is the membership check passing on real hardware. 54322 is inside 32768-60999
on GitHub's own runner, so the diagnosis holds where it matters, and it is re-verified in every run's
log forever.

Cannot prove a negative

One green run per lane does not prove the collision is gone — the failure rate was already low. What
the change guarantees is (a) the kernel will not hand out these ports as source ports any more,
which removes the mechanism, and (b) if a lane ever goes red on ports again, the log names the
culprit instead of leaving a bare address already in use.

Refs #574

AndresL230and others added 2 commits August 23, 2026 04:02
Run 32624263094 (push to main, 9f34454) died at `Start Supabase` with
`failed to bind host port for 0.0.0.0:54322: address already in use`, and
the identical commit re-run 40 minutes later passed.
The runner is a fresh VM, so nothing is left over. The mechanism is that
Supabase's ports (54320-54329) sit inside Linux's default ephemeral
source-port range (32768-60999): the attempt-1 log shows dozens of image
pulls finishing and the bind error landing in the same second, so an
outbound connection had been handed 54322 as its source port.
Reserve the ports out of automatic assignment (explicit bind() is
unaffected, so Docker can still publish them), derived from
supabase/config.toml so moving a port cannot un-protect it. Then diagnose
what reservation cannot undo: remove leftover containers publishing them,
fail by name on a foreign LISTENer, and wait out a draining socket.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The leftover-container cleanup was the one pipeline in the step without a
guard. Under the runner's `bash -e` (confirmed in run 32627212767's log:
`shell: /usr/bin/bash -e {0}`), a `docker rm -f` failure — permission, or a
race with another remover — aborted the step on the spot: no `preflight:`
diagnostic, no LISTEN check, no drain wait, no summary line. That is exactly
the opaque-CI-failure class this step exists to remove.
Warn and continue instead, consistent with the sysctl and drain branches;
the LISTEN check immediately after already names anything that survived.
Reproduced against the pre-fix commit: exit 123 with no output after the
docker error. With the guard: WARNING, then the step runs to completion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 23, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging5399744Commit Preview URL

Branch Preview URL
Aug 26 2026, 05:31 AM

@coderabbitai

coderabbitaiBot commented Aug 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e658fcf-c533-4321-9785-a9f8d9224359

📥 Commits

Reviewing files that changed from the base of the PR and between 9f34454 and d992c12.

📒 Files selected for processing (3)
  • .github/workflows/e2e.yml
  • .github/workflows/integration.yml
  • scripts/preflight-ports.sh

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds a CI-only Supabase port preflight script. E2E and integration workflows run it after checkout. The integration workflow also retries supabase start once after cleanup.

Changes

Supabase port preflight

Layer / File(s)Summary
Parse Supabase port configuration
scripts/preflight-ports.sh
The script validates prerequisites and parses enabled Supabase port settings from supabase/config.toml.
Reserve and verify ports
scripts/preflight-ports.sh
The script updates kernel reservations and probes configured ports. It reports foreign listeners, probe failures, and timeout results.
Run preflight and retry startup
.github/workflows/e2e.yml, .github/workflows/integration.yml, scripts/preflight-ports.sh
Both workflows run the preflight after checkout. The integration workflow stops Supabase and retries startup once after failure.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk:⚪ Minimal · up to d992c

This PR adds preflight protection for Supabase port collisions in CI and reports successful validation; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
participant GitHub Actions
participant preflight-ports.sh
participant Linux kernel
participant ss
participant Supabase CLI
GitHub Actions->>preflight-ports.sh: Run port preflight after checkout
preflight-ports.sh->>Linux kernel: Reserve configured ports
preflight-ports.sh->>ss: Probe configured bind ports
ss-->>preflight-ports.sh: Return socket status
preflight-ports.sh-->>GitHub Actions: Return preflight status
GitHub Actions->>Supabase CLI: Run supabase start
Supabase CLI-->>GitHub Actions: Return startup status
GitHub Actions->>Supabase CLI: Run supabase stop after failure
GitHub Actions->>Supabase CLI: Retry supabase start
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (2 skipped: 2 …
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly and concisely describes the main change: reserving Supabase ports before startup in both CI lanes.
Description check✅ PassedThe description is detailed and on-topic. It explains the failure, mechanism, implementation, affected workflows, testing evidence, issue reference, and review considerations. It does not use the repo…
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (2 skipped: 2 unsupported.)

Full details: Description check

Explanation

The description is detailed and on-topic. It explains the failure, mechanism, implementation, affected workflows, testing evidence, issue reference, and review considerations. It does not use the repository template headings exactly, but it provides the required information.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/integration-port-preflight

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@supabase

supabaseBot commented Aug 23, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

…sh (#574)
Merge-gate review of #588 found 15 issues in the inline step. The whole body
moves to scripts/preflight-ports.sh — testable, shellcheckable, and called by
BOTH lanes (integration.yml, and e2e.yml, which reaches `supabase start`
through `make e2e-up` and had the identical exposure).
What changed beyond the move:
- Two port sets, not one. RESERVE = every config.toml *port inside the LIVE
ephemeral range (read from /proc, not hardcoded). CHECK = the ports
`supabase start` actually binds — ENABLED sections only, minus shadow_port,
unfiltered by range. Only CHECK can fail or stall the job, so a holder on
the disabled pooler's 54329 no longer fails an unfiltered PR gate; enabled-
ness is derived per section, so flipping `enabled = true` is picked up.
- Parser: `[a-z_]*port` could not match the digit-bearing `pop3_port`; values
are now matched as bare integers (TOML underscores accepted) and anything
else is reported by line and key instead of being coerced to 0 and dropped.
- No more fail-open probes: the `ss` call and the awk filters branch on their
own exit status, so a probe that could not run never reads as "free".
- The sysctl is verified. A failed read of the current value skips the write
rather than clobbering a reservation it cannot see; the merge keeps existing
RANGES intact; the value is read back and checked by membership (the kernel
normalises to ranges, so a string compare would always mismatch); and the
summary line says NOT reserved when any of that fails.
- Drain: budget 30s -> 75s, because a TIME-WAIT entry lives a fixed 60s and
SO_REUSEADDR does NOT let a bind step over it (Linux needs the option on
both sockets, and an outbound connection never set it). ESTABLISHED is no
longer described as transient.
- The docker-container sweep is deleted — it was wrong in five ways where it
could fire. `supabase start` gets e2e-up.sh's stop-and-retry instead, which
also covers exited/stale projects the sweep could not see.
Refs #574
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230AndresL230 changed the title ci(integration): reserve the Supabase ports before supabase startci: reserve the Supabase ports before supabase start (both lanes)Aug 26, 2026
…guarded substitutions, validated timeout
Three non-blocking findings from the merge-gate re-review of #588:
- Every sudo call now uses `-n`. The socket probe already gated on
`sudo -n true`, but the sysctl write did not, so a hand-run on a box without
passwordless sudo would have blocked on a password prompt instead of failing
into the diagnostic half the header promises. The probe command itself is
`sudo -n ss` too, closing the window where the gate passes and the sudo
timestamp expires before the probe.
- The record-splitting command substitutions and the readback membership check
branch on their status like the probes do. They are in-memory string ops that
realistically cannot fail, but "realistically cannot fail" is the reasoning
that produced the fail-open probe this rework had to fix: a failed
`check_csv` pipeline would have printed "no ENABLED service binds a port" and
exited 0 having checked nothing, and a failed membership check would have
claimed the ports were reserved. The merged-list build is guarded for a
sharper reason still — an empty value would CLEAR the bitmap it extends.
- PREFLIGHT_DRAIN_TIMEOUT is validated as a whole number. A non-numeric value
made every `-ge` test fail and spun the drain loop to the job timeout — a
worse failure than the one being guarded. It warns and falls back to 75
rather than failing a lane over a typo.
Harness: 20 cases / 69 assertions green under both awk and `gawk --posix`,
including the three new ones (bad timeout value, empty override, and a sudo
shim that fails any call arriving without `-n`). shellcheck 0.11.0 clean.
Refs #574
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 6225bba into mainAug 26, 2026
10 checks passed
AndresL230 added a commit that referenced this pull request Aug 26, 2026
Brings in the include_answer_key default flip (#590), the dead
effective_explanations deletion (#587) and the CI port preflight (#588).
Clean auto-merge: main's routes/quiz.py hunks are in the generate
handler's response projection, this branch's are in the shared
gather/hoist above it.
Sign up for freeto 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.

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' ci: reserve the Supabase ports before `supabase start` (both lanes) by AndresL230 · Pull Request #588 · SaplingLearn/Sapling · GitHub
Skip to content

ci: reserve the Supabase ports before supabase start (both lanes) - #588

Merged
AndresL230 merged 4 commits into
mainfrom
ci/integration-port-preflight
Aug 26, 2026
Merged

ci: reserve the Supabase ports before supabase start (both lanes)#588
AndresL230 merged 4 commits into
mainfrom
ci/integration-port-preflight

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

What failed

Run 32624263094 (push to main,
9f34454) died at Start Supabase:

failed to start docker container "supabase_db_sapling": … failed to bind host port for
0.0.0.0:54322:172.18.0.2:5432/tcp: address already in use

The identical commit, re-run 40 minutes later, passed. No code cause — a day of signal went to
chasing it.

The mechanism

The runner is a fresh VM per job, so nothing is left over. But Supabase's ports (54320–54329) sit
inside Linux's default ephemeral source-port range, 32768–60999. Any outbound TCP connection the
job makes can be handed 54322 as its source port, and Docker's later bind(0.0.0.0:54322) then
fails.

The attempt-1 log times it exactly: the last of dozens of ghcr.io image pulls completes at
06:58:38.6156, Starting database... is logged 0.03s later, and the bind fails. Those pulls are
hundreds of outbound connections. One of them took the port.

Note the pulls happen insidesupabase start — so a pre-flight check would have passed. The fix
has to be pre-emptive.

What the guard does

scripts/preflight-ports.sh, called right after checkout in both lanes that boot Supabase —
integration.yml and e2e.yml (which reaches supabase start through make e2e-up
scripts/e2e-up.sh). Right after checkout is the earliest point supabase/config.toml exists, and
it puts every later download — setup-python, setup-cli, npm ci, playwright install — behind
the reservation. Bash only; no new actions or dependencies.

  1. Reserves the ports via net.ipv4.ip_local_reserved_ports, which excludes them from
    automatic source-port assignment while leaving explicit bind() untouched — so Docker can still
    publish them. Merges with any existing value (idempotent), then reads the value back and
    verifies membership before claiming anything is reserved.
  2. Fails fast if a foreign process is LISTENing on a port supabase start will actually bind,
    printing its state, address, pid and program. It never kills what isn't ours.
  3. Waits out a non-listening holder (up to 75s — a TIME-WAIT entry lives a fixed 60s), printing
    what it's waiting on: the one case reservation can't fix retroactively, because it was allocated
    before the step ran. On timeout it warns and proceeds rather than failing on a guess.
  4. Prints one greppable line: preflight: ports 54321,54322,54323,54324 free (enabled services; reserved from the ephemeral range) — and the word free only ever appears when the ports were
    observed free.

Two different port sets, both derived from supabase/config.toml rather than hardcoded:

  • RESERVE — every *port key inside the live ephemeral range (read from /proc, so the
    premise is re-verified in every run's log). Enabled or not: reserving a port nothing binds costs
    nothing, and it keeps the protection in place the day someone flips enabled = true.
  • CHECK — the ports supabase start will actually bind: *port keys of enabled sections,
    minus shadow_port (only db diff binds that), and not range-filtered. Only this set can fail
    or stall the job, so a holder on the disabled pooler's 54329 can't red an unfiltered PR gate.

supabase start also picks up e2e-up.sh's stop-and-retry, which covers stale/exited projects and
the preflight's admitted give-up path.


Merge-gate review rework (2026-08-26) — commit d992c12f

A /code-review at the merge gate returned 15 findings against the original inline step. All of
them are addressed here; the whole body moved out of the workflow into scripts/preflight-ports.sh
so it is testable and shellcheckable, and so one implementation serves both lanes.

#FindingDisposition
F1[a-z_]*port can't match pop3_port (digit in the key)Fixed — matches port / [a-z0-9_]+_port, anchored so a key merely ending in "port" can't slip in
F2v = $2 + 0 silently coerced 54_322→54 and "54322"→0Fixed — bare integers only (TOML _ separators accepted); anything else is reported by line and key as unprotected
F3Drain: wrong SO_REUSEADDR claim, 30s < 60s TIME-WAIT, ESTABLISHED called transientFixed — budget 75s; the message says SO_REUSEADDR needs the option on both sockets (an outbound client socket never set it, so Docker's bind really does fail) and that ESTABLISHED is a live connection, not a transient one
F4busy=$(sudo ss … || true) was fail-openFixed — the probe and both awk filters branch on their own exit status; a probe that can't run is reported as unknown, never as free
F5Ephemeral range hardcoded 32768-60999Fixed — read from /proc/sys/net/ipv4/ip_local_port_range; it filters the RESERVE set only, the CHECK set stays unfiltered
F6Leftover-container sweep wrong five waysReplaced — sweep deleted; supabase start gets e2e-up.sh's stop-and-retry instead, which also covers the exited/stale projects docker ps couldn't see
F7Fail/wait set included ports this config never bindsFixed — the CHECK/RESERVE split above; enabled-ness is derived per config section, so enabled = true on the pooler moves 54329 into the fail set with no edit
F8Reserved-ports merge was an unverified read-modify-writeFixed — a failed read skips the write rather than clobbering an unseen reservation; the merge keeps existing ranges intact; the value is read back and checked by membership (the kernel normalises to ranges — the live run prints 54320-54324,54327,54329, so a string compare would mismatch on every success)
F9Zero-grace LISTEN exit right after docker rmFalls away with F6
F10Summary said "(reserved …)" even when the sysctl failedFixed — three states (reserved / not reserved / no reservation needed) interpolated into the summary
F11"no ports parsed" message named only one causeFixed — the exit-1 names both: the step is obsolete, or the parser broke
F12Extract to scripts/preflight-ports.sh, call from both lanesDone — the workflow steps are now one line each
F13Ports hardcoded at four other sitesFollow-up #594 (deliberately not fixed here: this PR is CI hardening, that touches the local dev scripts)
F14No supabase start failure recovery in this laneFixed — the stop-and-retry above, mirroring scripts/e2e-up.sh:219-226
F15Decorative || trues, duplicated regex build, repeated indent pipeline, waited -eq 0 sentinelFixed — one indent() helper, one regex build, a named announced flag, no unguarded || true

Test evidence

Branch-coverage harness — 20 cases, 69 assertions, all green, run twice: under the default awk
and under gawk --posix (stand-in for the runner's mawk). It builds a throwaway repo root per case,
copies the real script in with only its two /proc constants repointed at fixtures (the patch is
grep-verified — a silently-failed patch produced a false GREEN in the previous round), and drives it
with sudo/ss shims. The sudo shim emulates the kernel: it expands ranges, stores a set, and
prints it back collapsed into ranges.

Covered: clean path · digit-bearing pop3_port protected · 54_322 parsed as 54322 · non-integer
value diagnosed by line+key · probe failure → WARNING, never "free" · sysctl fails → summary says NOT
reserved · sysctl "succeeds" but readback lacks the ports → WARNING · foreign LISTENer on an enabled
port → exit 1 naming the pid · LISTENer on the disabled pooler's port → no fail · same holder after
flipping the pooler to enabled = true → now fails (the derivation is live) · shadow_port reserved
but never checked · TIME-WAIT drain waits, announces once, proceeds clean · drain timeout → WARNING +
continue · no port keys → exit 1 naming both branches · ports outside the ephemeral range → nothing
to reserve, checks still run and still fail · unreadable reservation → write skipped · pre-existing
1024-1030 range survives the merge · no passwordless sudo → unprivileged probe · no ss
"unverified", not "free" · non-numeric drain timeout → loud fallback · a sudo shim that fails any call arriving without -n.

shellcheck 0.11.0 -xclean, zero findings (the previous round had no shellcheck available).

Re-review residuals — commit 53997446

The re-review passed all 15 findings and raised three non-blocking residuals, closed in one commit:

  • Every sudo call now uses -n. The probe gated on sudo -n true, but the sysctl write did
    not — a hand-run on a box without passwordless sudo would have blocked on a password prompt
    instead of degrading to the diagnostic half the header promises. The probe command is sudo -n ss
    too, closing the window where the gate passes and the timestamp expires before the probe.
  • The remaining command substitutions branch on their status (record split, merged-list build,
    readback membership) — in-memory string ops that realistically cannot fail, but that is the
    reasoning which produced the fail-open probe F4 condemned: a failed check_csv would have printed
    "no ENABLED service binds a port" and exited 0 having checked nothing, and an empty merged list
    would have cleared the bitmap it extends.
  • PREFLIGHT_DRAIN_TIMEOUT is validated as a whole number — a non-numeric value made every
    -ge test fail and spun the drain loop to the job timeout. It warns and falls back to 75.

Lane runs on this branch @ 53997446 — both green

LaneRunResult
integration.yml (dispatch)32934241228success — 71 passed, 2231 deselected (real work, zero skipped)
e2e.yml (dispatch)32934243435success — every step green, 73 passed (3.8m) Playwright

Plus the PR-triggered CI and
integration runs, both green.
(The previous SHA d992c12f was green on the same four lanes:
32932741569 /
32932743439.)

Both lanes printed the same three lines on the runner:

preflight: ephemeral source-port range 32768-60999
preflight: reserved 54320-54324,54327,54329
preflight: ports 54321,54322,54323,54324 free (enabled services; reserved from the ephemeral range)

Read that middle line closely: the kernel handed the value back range-collapsed, which is
exactly why F8's readback is a membership check and not a string compare — and the absence of a
mismatch WARNING is the membership check passing on real hardware. 54322 is inside 32768-60999
on GitHub's own runner, so the diagnosis holds where it matters, and it is re-verified in every run's
log forever.

Cannot prove a negative

One green run per lane does not prove the collision is gone — the failure rate was already low. What
the change guarantees is (a) the kernel will not hand out these ports as source ports any more,
which removes the mechanism, and (b) if a lane ever goes red on ports again, the log names the
culprit instead of leaving a bare address already in use.

Refs #574

AndresL230and others added 2 commits August 23, 2026 04:02
Run 32624263094 (push to main, 9f34454) died at `Start Supabase` with
`failed to bind host port for 0.0.0.0:54322: address already in use`, and
the identical commit re-run 40 minutes later passed.
The runner is a fresh VM, so nothing is left over. The mechanism is that
Supabase's ports (54320-54329) sit inside Linux's default ephemeral
source-port range (32768-60999): the attempt-1 log shows dozens of image
pulls finishing and the bind error landing in the same second, so an
outbound connection had been handed 54322 as its source port.
Reserve the ports out of automatic assignment (explicit bind() is
unaffected, so Docker can still publish them), derived from
supabase/config.toml so moving a port cannot un-protect it. Then diagnose
what reservation cannot undo: remove leftover containers publishing them,
fail by name on a foreign LISTENer, and wait out a draining socket.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The leftover-container cleanup was the one pipeline in the step without a
guard. Under the runner's `bash -e` (confirmed in run 32627212767's log:
`shell: /usr/bin/bash -e {0}`), a `docker rm -f` failure — permission, or a
race with another remover — aborted the step on the spot: no `preflight:`
diagnostic, no LISTEN check, no drain wait, no summary line. That is exactly
the opaque-CI-failure class this step exists to remove.
Warn and continue instead, consistent with the sysctl and drain branches;
the LISTEN check immediately after already names anything that survived.
Reproduced against the pre-fix commit: exit 123 with no output after the
docker error. With the guard: WARNING, then the step runs to completion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 23, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging5399744Commit Preview URL

Branch Preview URL
Aug 26 2026, 05:31 AM

@coderabbitai

coderabbitaiBot commented Aug 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e658fcf-c533-4321-9785-a9f8d9224359

📥 Commits

Reviewing files that changed from the base of the PR and between 9f34454 and d992c12.

📒 Files selected for processing (3)
  • .github/workflows/e2e.yml
  • .github/workflows/integration.yml
  • scripts/preflight-ports.sh

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds a CI-only Supabase port preflight script. E2E and integration workflows run it after checkout. The integration workflow also retries supabase start once after cleanup.

Changes

Supabase port preflight

Layer / File(s)Summary
Parse Supabase port configuration
scripts/preflight-ports.sh
The script validates prerequisites and parses enabled Supabase port settings from supabase/config.toml.
Reserve and verify ports
scripts/preflight-ports.sh
The script updates kernel reservations and probes configured ports. It reports foreign listeners, probe failures, and timeout results.
Run preflight and retry startup
.github/workflows/e2e.yml, .github/workflows/integration.yml, scripts/preflight-ports.sh
Both workflows run the preflight after checkout. The integration workflow stops Supabase and retries startup once after failure.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk:⚪ Minimal · up to d992c

This PR adds preflight protection for Supabase port collisions in CI and reports successful validation; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
participant GitHub Actions
participant preflight-ports.sh
participant Linux kernel
participant ss
participant Supabase CLI
GitHub Actions->>preflight-ports.sh: Run port preflight after checkout
preflight-ports.sh->>Linux kernel: Reserve configured ports
preflight-ports.sh->>ss: Probe configured bind ports
ss-->>preflight-ports.sh: Return socket status
preflight-ports.sh-->>GitHub Actions: Return preflight status
GitHub Actions->>Supabase CLI: Run supabase start
Supabase CLI-->>GitHub Actions: Return startup status
GitHub Actions->>Supabase CLI: Run supabase stop after failure
GitHub Actions->>Supabase CLI: Retry supabase start
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (2 skipped: 2 …
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly and concisely describes the main change: reserving Supabase ports before startup in both CI lanes.
Description check✅ PassedThe description is detailed and on-topic. It explains the failure, mechanism, implementation, affected workflows, testing evidence, issue reference, and review considerations. It does not use the repo…
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (2 skipped: 2 unsupported.)

Full details: Description check

Explanation

The description is detailed and on-topic. It explains the failure, mechanism, implementation, affected workflows, testing evidence, issue reference, and review considerations. It does not use the repository template headings exactly, but it provides the required information.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/integration-port-preflight

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@supabase

supabaseBot commented Aug 23, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

…sh (#574)
Merge-gate review of #588 found 15 issues in the inline step. The whole body
moves to scripts/preflight-ports.sh — testable, shellcheckable, and called by
BOTH lanes (integration.yml, and e2e.yml, which reaches `supabase start`
through `make e2e-up` and had the identical exposure).
What changed beyond the move:
- Two port sets, not one. RESERVE = every config.toml *port inside the LIVE
ephemeral range (read from /proc, not hardcoded). CHECK = the ports
`supabase start` actually binds — ENABLED sections only, minus shadow_port,
unfiltered by range. Only CHECK can fail or stall the job, so a holder on
the disabled pooler's 54329 no longer fails an unfiltered PR gate; enabled-
ness is derived per section, so flipping `enabled = true` is picked up.
- Parser: `[a-z_]*port` could not match the digit-bearing `pop3_port`; values
are now matched as bare integers (TOML underscores accepted) and anything
else is reported by line and key instead of being coerced to 0 and dropped.
- No more fail-open probes: the `ss` call and the awk filters branch on their
own exit status, so a probe that could not run never reads as "free".
- The sysctl is verified. A failed read of the current value skips the write
rather than clobbering a reservation it cannot see; the merge keeps existing
RANGES intact; the value is read back and checked by membership (the kernel
normalises to ranges, so a string compare would always mismatch); and the
summary line says NOT reserved when any of that fails.
- Drain: budget 30s -> 75s, because a TIME-WAIT entry lives a fixed 60s and
SO_REUSEADDR does NOT let a bind step over it (Linux needs the option on
both sockets, and an outbound connection never set it). ESTABLISHED is no
longer described as transient.
- The docker-container sweep is deleted — it was wrong in five ways where it
could fire. `supabase start` gets e2e-up.sh's stop-and-retry instead, which
also covers exited/stale projects the sweep could not see.
Refs #574
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230AndresL230 changed the title ci(integration): reserve the Supabase ports before supabase startci: reserve the Supabase ports before supabase start (both lanes)Aug 26, 2026
…guarded substitutions, validated timeout
Three non-blocking findings from the merge-gate re-review of #588:
- Every sudo call now uses `-n`. The socket probe already gated on
`sudo -n true`, but the sysctl write did not, so a hand-run on a box without
passwordless sudo would have blocked on a password prompt instead of failing
into the diagnostic half the header promises. The probe command itself is
`sudo -n ss` too, closing the window where the gate passes and the sudo
timestamp expires before the probe.
- The record-splitting command substitutions and the readback membership check
branch on their status like the probes do. They are in-memory string ops that
realistically cannot fail, but "realistically cannot fail" is the reasoning
that produced the fail-open probe this rework had to fix: a failed
`check_csv` pipeline would have printed "no ENABLED service binds a port" and
exited 0 having checked nothing, and a failed membership check would have
claimed the ports were reserved. The merged-list build is guarded for a
sharper reason still — an empty value would CLEAR the bitmap it extends.
- PREFLIGHT_DRAIN_TIMEOUT is validated as a whole number. A non-numeric value
made every `-ge` test fail and spun the drain loop to the job timeout — a
worse failure than the one being guarded. It warns and falls back to 75
rather than failing a lane over a typo.
Harness: 20 cases / 69 assertions green under both awk and `gawk --posix`,
including the three new ones (bad timeout value, empty override, and a sudo
shim that fails any call arriving without `-n`). shellcheck 0.11.0 clean.
Refs #574
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 6225bba into mainAug 26, 2026
10 checks passed
AndresL230 added a commit that referenced this pull request Aug 26, 2026
Brings in the include_answer_key default flip (#590), the dead
effective_explanations deletion (#587) and the CI port preflight (#588).
Clean auto-merge: main's routes/quiz.py hunks are in the generate
handler's response projection, this branch's are in the shared
gather/hoist above it.
Sign up for freeto 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.

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' ci: reserve the Supabase ports before `supabase start` (both lanes) by AndresL230 · Pull Request #588 · SaplingLearn/Sapling · GitHub
Skip to content

ci: reserve the Supabase ports before supabase start (both lanes) - #588

Merged
AndresL230 merged 4 commits into
mainfrom
ci/integration-port-preflight
Aug 26, 2026
Merged

ci: reserve the Supabase ports before supabase start (both lanes)#588
AndresL230 merged 4 commits into
mainfrom
ci/integration-port-preflight

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

What failed

Run 32624263094 (push to main,
9f34454) died at Start Supabase:

failed to start docker container "supabase_db_sapling": … failed to bind host port for
0.0.0.0:54322:172.18.0.2:5432/tcp: address already in use

The identical commit, re-run 40 minutes later, passed. No code cause — a day of signal went to
chasing it.

The mechanism

The runner is a fresh VM per job, so nothing is left over. But Supabase's ports (54320–54329) sit
inside Linux's default ephemeral source-port range, 32768–60999. Any outbound TCP connection the
job makes can be handed 54322 as its source port, and Docker's later bind(0.0.0.0:54322) then
fails.

The attempt-1 log times it exactly: the last of dozens of ghcr.io image pulls completes at
06:58:38.6156, Starting database... is logged 0.03s later, and the bind fails. Those pulls are
hundreds of outbound connections. One of them took the port.

Note the pulls happen insidesupabase start — so a pre-flight check would have passed. The fix
has to be pre-emptive.

What the guard does

scripts/preflight-ports.sh, called right after checkout in both lanes that boot Supabase —
integration.yml and e2e.yml (which reaches supabase start through make e2e-up
scripts/e2e-up.sh). Right after checkout is the earliest point supabase/config.toml exists, and
it puts every later download — setup-python, setup-cli, npm ci, playwright install — behind
the reservation. Bash only; no new actions or dependencies.

  1. Reserves the ports via net.ipv4.ip_local_reserved_ports, which excludes them from
    automatic source-port assignment while leaving explicit bind() untouched — so Docker can still
    publish them. Merges with any existing value (idempotent), then reads the value back and
    verifies membership before claiming anything is reserved.
  2. Fails fast if a foreign process is LISTENing on a port supabase start will actually bind,
    printing its state, address, pid and program. It never kills what isn't ours.
  3. Waits out a non-listening holder (up to 75s — a TIME-WAIT entry lives a fixed 60s), printing
    what it's waiting on: the one case reservation can't fix retroactively, because it was allocated
    before the step ran. On timeout it warns and proceeds rather than failing on a guess.
  4. Prints one greppable line: preflight: ports 54321,54322,54323,54324 free (enabled services; reserved from the ephemeral range) — and the word free only ever appears when the ports were
    observed free.

Two different port sets, both derived from supabase/config.toml rather than hardcoded:

  • RESERVE — every *port key inside the live ephemeral range (read from /proc, so the
    premise is re-verified in every run's log). Enabled or not: reserving a port nothing binds costs
    nothing, and it keeps the protection in place the day someone flips enabled = true.
  • CHECK — the ports supabase start will actually bind: *port keys of enabled sections,
    minus shadow_port (only db diff binds that), and not range-filtered. Only this set can fail
    or stall the job, so a holder on the disabled pooler's 54329 can't red an unfiltered PR gate.

supabase start also picks up e2e-up.sh's stop-and-retry, which covers stale/exited projects and
the preflight's admitted give-up path.


Merge-gate review rework (2026-08-26) — commit d992c12f

A /code-review at the merge gate returned 15 findings against the original inline step. All of
them are addressed here; the whole body moved out of the workflow into scripts/preflight-ports.sh
so it is testable and shellcheckable, and so one implementation serves both lanes.

#FindingDisposition
F1[a-z_]*port can't match pop3_port (digit in the key)Fixed — matches port / [a-z0-9_]+_port, anchored so a key merely ending in "port" can't slip in
F2v = $2 + 0 silently coerced 54_322→54 and "54322"→0Fixed — bare integers only (TOML _ separators accepted); anything else is reported by line and key as unprotected
F3Drain: wrong SO_REUSEADDR claim, 30s < 60s TIME-WAIT, ESTABLISHED called transientFixed — budget 75s; the message says SO_REUSEADDR needs the option on both sockets (an outbound client socket never set it, so Docker's bind really does fail) and that ESTABLISHED is a live connection, not a transient one
F4busy=$(sudo ss … || true) was fail-openFixed — the probe and both awk filters branch on their own exit status; a probe that can't run is reported as unknown, never as free
F5Ephemeral range hardcoded 32768-60999Fixed — read from /proc/sys/net/ipv4/ip_local_port_range; it filters the RESERVE set only, the CHECK set stays unfiltered
F6Leftover-container sweep wrong five waysReplaced — sweep deleted; supabase start gets e2e-up.sh's stop-and-retry instead, which also covers the exited/stale projects docker ps couldn't see
F7Fail/wait set included ports this config never bindsFixed — the CHECK/RESERVE split above; enabled-ness is derived per config section, so enabled = true on the pooler moves 54329 into the fail set with no edit
F8Reserved-ports merge was an unverified read-modify-writeFixed — a failed read skips the write rather than clobbering an unseen reservation; the merge keeps existing ranges intact; the value is read back and checked by membership (the kernel normalises to ranges — the live run prints 54320-54324,54327,54329, so a string compare would mismatch on every success)
F9Zero-grace LISTEN exit right after docker rmFalls away with F6
F10Summary said "(reserved …)" even when the sysctl failedFixed — three states (reserved / not reserved / no reservation needed) interpolated into the summary
F11"no ports parsed" message named only one causeFixed — the exit-1 names both: the step is obsolete, or the parser broke
F12Extract to scripts/preflight-ports.sh, call from both lanesDone — the workflow steps are now one line each
F13Ports hardcoded at four other sitesFollow-up #594 (deliberately not fixed here: this PR is CI hardening, that touches the local dev scripts)
F14No supabase start failure recovery in this laneFixed — the stop-and-retry above, mirroring scripts/e2e-up.sh:219-226
F15Decorative || trues, duplicated regex build, repeated indent pipeline, waited -eq 0 sentinelFixed — one indent() helper, one regex build, a named announced flag, no unguarded || true

Test evidence

Branch-coverage harness — 20 cases, 69 assertions, all green, run twice: under the default awk
and under gawk --posix (stand-in for the runner's mawk). It builds a throwaway repo root per case,
copies the real script in with only its two /proc constants repointed at fixtures (the patch is
grep-verified — a silently-failed patch produced a false GREEN in the previous round), and drives it
with sudo/ss shims. The sudo shim emulates the kernel: it expands ranges, stores a set, and
prints it back collapsed into ranges.

Covered: clean path · digit-bearing pop3_port protected · 54_322 parsed as 54322 · non-integer
value diagnosed by line+key · probe failure → WARNING, never "free" · sysctl fails → summary says NOT
reserved · sysctl "succeeds" but readback lacks the ports → WARNING · foreign LISTENer on an enabled
port → exit 1 naming the pid · LISTENer on the disabled pooler's port → no fail · same holder after
flipping the pooler to enabled = true → now fails (the derivation is live) · shadow_port reserved
but never checked · TIME-WAIT drain waits, announces once, proceeds clean · drain timeout → WARNING +
continue · no port keys → exit 1 naming both branches · ports outside the ephemeral range → nothing
to reserve, checks still run and still fail · unreadable reservation → write skipped · pre-existing
1024-1030 range survives the merge · no passwordless sudo → unprivileged probe · no ss
"unverified", not "free" · non-numeric drain timeout → loud fallback · a sudo shim that fails any call arriving without -n.

shellcheck 0.11.0 -xclean, zero findings (the previous round had no shellcheck available).

Re-review residuals — commit 53997446

The re-review passed all 15 findings and raised three non-blocking residuals, closed in one commit:

  • Every sudo call now uses -n. The probe gated on sudo -n true, but the sysctl write did
    not — a hand-run on a box without passwordless sudo would have blocked on a password prompt
    instead of degrading to the diagnostic half the header promises. The probe command is sudo -n ss
    too, closing the window where the gate passes and the timestamp expires before the probe.
  • The remaining command substitutions branch on their status (record split, merged-list build,
    readback membership) — in-memory string ops that realistically cannot fail, but that is the
    reasoning which produced the fail-open probe F4 condemned: a failed check_csv would have printed
    "no ENABLED service binds a port" and exited 0 having checked nothing, and an empty merged list
    would have cleared the bitmap it extends.
  • PREFLIGHT_DRAIN_TIMEOUT is validated as a whole number — a non-numeric value made every
    -ge test fail and spun the drain loop to the job timeout. It warns and falls back to 75.

Lane runs on this branch @ 53997446 — both green

LaneRunResult
integration.yml (dispatch)32934241228success — 71 passed, 2231 deselected (real work, zero skipped)
e2e.yml (dispatch)32934243435success — every step green, 73 passed (3.8m) Playwright

Plus the PR-triggered CI and
integration runs, both green.
(The previous SHA d992c12f was green on the same four lanes:
32932741569 /
32932743439.)

Both lanes printed the same three lines on the runner:

preflight: ephemeral source-port range 32768-60999
preflight: reserved 54320-54324,54327,54329
preflight: ports 54321,54322,54323,54324 free (enabled services; reserved from the ephemeral range)

Read that middle line closely: the kernel handed the value back range-collapsed, which is
exactly why F8's readback is a membership check and not a string compare — and the absence of a
mismatch WARNING is the membership check passing on real hardware. 54322 is inside 32768-60999
on GitHub's own runner, so the diagnosis holds where it matters, and it is re-verified in every run's
log forever.

Cannot prove a negative

One green run per lane does not prove the collision is gone — the failure rate was already low. What
the change guarantees is (a) the kernel will not hand out these ports as source ports any more,
which removes the mechanism, and (b) if a lane ever goes red on ports again, the log names the
culprit instead of leaving a bare address already in use.

Refs #574

AndresL230and others added 2 commits August 23, 2026 04:02
Run 32624263094 (push to main, 9f34454) died at `Start Supabase` with
`failed to bind host port for 0.0.0.0:54322: address already in use`, and
the identical commit re-run 40 minutes later passed.
The runner is a fresh VM, so nothing is left over. The mechanism is that
Supabase's ports (54320-54329) sit inside Linux's default ephemeral
source-port range (32768-60999): the attempt-1 log shows dozens of image
pulls finishing and the bind error landing in the same second, so an
outbound connection had been handed 54322 as its source port.
Reserve the ports out of automatic assignment (explicit bind() is
unaffected, so Docker can still publish them), derived from
supabase/config.toml so moving a port cannot un-protect it. Then diagnose
what reservation cannot undo: remove leftover containers publishing them,
fail by name on a foreign LISTENer, and wait out a draining socket.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The leftover-container cleanup was the one pipeline in the step without a
guard. Under the runner's `bash -e` (confirmed in run 32627212767's log:
`shell: /usr/bin/bash -e {0}`), a `docker rm -f` failure — permission, or a
race with another remover — aborted the step on the spot: no `preflight:`
diagnostic, no LISTEN check, no drain wait, no summary line. That is exactly
the opaque-CI-failure class this step exists to remove.
Warn and continue instead, consistent with the sysctl and drain branches;
the LISTEN check immediately after already names anything that survived.
Reproduced against the pre-fix commit: exit 123 with no output after the
docker error. With the guard: WARNING, then the step runs to completion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 23, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging5399744Commit Preview URL

Branch Preview URL
Aug 26 2026, 05:31 AM

@coderabbitai

coderabbitaiBot commented Aug 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e658fcf-c533-4321-9785-a9f8d9224359

📥 Commits

Reviewing files that changed from the base of the PR and between 9f34454 and d992c12.

📒 Files selected for processing (3)
  • .github/workflows/e2e.yml
  • .github/workflows/integration.yml
  • scripts/preflight-ports.sh

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds a CI-only Supabase port preflight script. E2E and integration workflows run it after checkout. The integration workflow also retries supabase start once after cleanup.

Changes

Supabase port preflight

Layer / File(s)Summary
Parse Supabase port configuration
scripts/preflight-ports.sh
The script validates prerequisites and parses enabled Supabase port settings from supabase/config.toml.
Reserve and verify ports
scripts/preflight-ports.sh
The script updates kernel reservations and probes configured ports. It reports foreign listeners, probe failures, and timeout results.
Run preflight and retry startup
.github/workflows/e2e.yml, .github/workflows/integration.yml, scripts/preflight-ports.sh
Both workflows run the preflight after checkout. The integration workflow stops Supabase and retries startup once after failure.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk:⚪ Minimal · up to d992c

This PR adds preflight protection for Supabase port collisions in CI and reports successful validation; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
participant GitHub Actions
participant preflight-ports.sh
participant Linux kernel
participant ss
participant Supabase CLI
GitHub Actions->>preflight-ports.sh: Run port preflight after checkout
preflight-ports.sh->>Linux kernel: Reserve configured ports
preflight-ports.sh->>ss: Probe configured bind ports
ss-->>preflight-ports.sh: Return socket status
preflight-ports.sh-->>GitHub Actions: Return preflight status
GitHub Actions->>Supabase CLI: Run supabase start
Supabase CLI-->>GitHub Actions: Return startup status
GitHub Actions->>Supabase CLI: Run supabase stop after failure
GitHub Actions->>Supabase CLI: Retry supabase start
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (2 skipped: 2 …
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly and concisely describes the main change: reserving Supabase ports before startup in both CI lanes.
Description check✅ PassedThe description is detailed and on-topic. It explains the failure, mechanism, implementation, affected workflows, testing evidence, issue reference, and review considerations. It does not use the repo…
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (2 skipped: 2 unsupported.)

Full details: Description check

Explanation

The description is detailed and on-topic. It explains the failure, mechanism, implementation, affected workflows, testing evidence, issue reference, and review considerations. It does not use the repository template headings exactly, but it provides the required information.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/integration-port-preflight

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@supabase

supabaseBot commented Aug 23, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

…sh (#574)
Merge-gate review of #588 found 15 issues in the inline step. The whole body
moves to scripts/preflight-ports.sh — testable, shellcheckable, and called by
BOTH lanes (integration.yml, and e2e.yml, which reaches `supabase start`
through `make e2e-up` and had the identical exposure).
What changed beyond the move:
- Two port sets, not one. RESERVE = every config.toml *port inside the LIVE
ephemeral range (read from /proc, not hardcoded). CHECK = the ports
`supabase start` actually binds — ENABLED sections only, minus shadow_port,
unfiltered by range. Only CHECK can fail or stall the job, so a holder on
the disabled pooler's 54329 no longer fails an unfiltered PR gate; enabled-
ness is derived per section, so flipping `enabled = true` is picked up.
- Parser: `[a-z_]*port` could not match the digit-bearing `pop3_port`; values
are now matched as bare integers (TOML underscores accepted) and anything
else is reported by line and key instead of being coerced to 0 and dropped.
- No more fail-open probes: the `ss` call and the awk filters branch on their
own exit status, so a probe that could not run never reads as "free".
- The sysctl is verified. A failed read of the current value skips the write
rather than clobbering a reservation it cannot see; the merge keeps existing
RANGES intact; the value is read back and checked by membership (the kernel
normalises to ranges, so a string compare would always mismatch); and the
summary line says NOT reserved when any of that fails.
- Drain: budget 30s -> 75s, because a TIME-WAIT entry lives a fixed 60s and
SO_REUSEADDR does NOT let a bind step over it (Linux needs the option on
both sockets, and an outbound connection never set it). ESTABLISHED is no
longer described as transient.
- The docker-container sweep is deleted — it was wrong in five ways where it
could fire. `supabase start` gets e2e-up.sh's stop-and-retry instead, which
also covers exited/stale projects the sweep could not see.
Refs #574
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230AndresL230 changed the title ci(integration): reserve the Supabase ports before supabase startci: reserve the Supabase ports before supabase start (both lanes)Aug 26, 2026
…guarded substitutions, validated timeout
Three non-blocking findings from the merge-gate re-review of #588:
- Every sudo call now uses `-n`. The socket probe already gated on
`sudo -n true`, but the sysctl write did not, so a hand-run on a box without
passwordless sudo would have blocked on a password prompt instead of failing
into the diagnostic half the header promises. The probe command itself is
`sudo -n ss` too, closing the window where the gate passes and the sudo
timestamp expires before the probe.
- The record-splitting command substitutions and the readback membership check
branch on their status like the probes do. They are in-memory string ops that
realistically cannot fail, but "realistically cannot fail" is the reasoning
that produced the fail-open probe this rework had to fix: a failed
`check_csv` pipeline would have printed "no ENABLED service binds a port" and
exited 0 having checked nothing, and a failed membership check would have
claimed the ports were reserved. The merged-list build is guarded for a
sharper reason still — an empty value would CLEAR the bitmap it extends.
- PREFLIGHT_DRAIN_TIMEOUT is validated as a whole number. A non-numeric value
made every `-ge` test fail and spun the drain loop to the job timeout — a
worse failure than the one being guarded. It warns and falls back to 75
rather than failing a lane over a typo.
Harness: 20 cases / 69 assertions green under both awk and `gawk --posix`,
including the three new ones (bad timeout value, empty override, and a sudo
shim that fails any call arriving without `-n`). shellcheck 0.11.0 clean.
Refs #574
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 6225bba into mainAug 26, 2026
10 checks passed
AndresL230 added a commit that referenced this pull request Aug 26, 2026
Brings in the include_answer_key default flip (#590), the dead
effective_explanations deletion (#587) and the CI port preflight (#588).
Clean auto-merge: main's routes/quiz.py hunks are in the generate
handler's response projection, this branch's are in the shared
gather/hoist above it.
Sign up for freeto 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.

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); ci: reserve the Supabase ports before `supabase start` (both lanes) by AndresL230 · Pull Request #588 · SaplingLearn/Sapling · GitHub
Skip to content

ci: reserve the Supabase ports before supabase start (both lanes) - #588

Merged
AndresL230 merged 4 commits into
mainfrom
ci/integration-port-preflight
Aug 26, 2026
Merged

ci: reserve the Supabase ports before supabase start (both lanes)#588
AndresL230 merged 4 commits into
mainfrom
ci/integration-port-preflight

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

What failed

Run 32624263094 (push to main,
9f34454) died at Start Supabase:

failed to start docker container "supabase_db_sapling": … failed to bind host port for
0.0.0.0:54322:172.18.0.2:5432/tcp: address already in use

The identical commit, re-run 40 minutes later, passed. No code cause — a day of signal went to
chasing it.

The mechanism

The runner is a fresh VM per job, so nothing is left over. But Supabase's ports (54320–54329) sit
inside Linux's default ephemeral source-port range, 32768–60999. Any outbound TCP connection the
job makes can be handed 54322 as its source port, and Docker's later bind(0.0.0.0:54322) then
fails.

The attempt-1 log times it exactly: the last of dozens of ghcr.io image pulls completes at
06:58:38.6156, Starting database... is logged 0.03s later, and the bind fails. Those pulls are
hundreds of outbound connections. One of them took the port.

Note the pulls happen insidesupabase start — so a pre-flight check would have passed. The fix
has to be pre-emptive.

What the guard does

scripts/preflight-ports.sh, called right after checkout in both lanes that boot Supabase —
integration.yml and e2e.yml (which reaches supabase start through make e2e-up
scripts/e2e-up.sh). Right after checkout is the earliest point supabase/config.toml exists, and
it puts every later download — setup-python, setup-cli, npm ci, playwright install — behind
the reservation. Bash only; no new actions or dependencies.

  1. Reserves the ports via net.ipv4.ip_local_reserved_ports, which excludes them from
    automatic source-port assignment while leaving explicit bind() untouched — so Docker can still
    publish them. Merges with any existing value (idempotent), then reads the value back and
    verifies membership before claiming anything is reserved.
  2. Fails fast if a foreign process is LISTENing on a port supabase start will actually bind,
    printing its state, address, pid and program. It never kills what isn't ours.
  3. Waits out a non-listening holder (up to 75s — a TIME-WAIT entry lives a fixed 60s), printing
    what it's waiting on: the one case reservation can't fix retroactively, because it was allocated
    before the step ran. On timeout it warns and proceeds rather than failing on a guess.
  4. Prints one greppable line: preflight: ports 54321,54322,54323,54324 free (enabled services; reserved from the ephemeral range) — and the word free only ever appears when the ports were
    observed free.

Two different port sets, both derived from supabase/config.toml rather than hardcoded:

  • RESERVE — every *port key inside the live ephemeral range (read from /proc, so the
    premise is re-verified in every run's log). Enabled or not: reserving a port nothing binds costs
    nothing, and it keeps the protection in place the day someone flips enabled = true.
  • CHECK — the ports supabase start will actually bind: *port keys of enabled sections,
    minus shadow_port (only db diff binds that), and not range-filtered. Only this set can fail
    or stall the job, so a holder on the disabled pooler's 54329 can't red an unfiltered PR gate.

supabase start also picks up e2e-up.sh's stop-and-retry, which covers stale/exited projects and
the preflight's admitted give-up path.


Merge-gate review rework (2026-08-26) — commit d992c12f

A /code-review at the merge gate returned 15 findings against the original inline step. All of
them are addressed here; the whole body moved out of the workflow into scripts/preflight-ports.sh
so it is testable and shellcheckable, and so one implementation serves both lanes.

#FindingDisposition
F1[a-z_]*port can't match pop3_port (digit in the key)Fixed — matches port / [a-z0-9_]+_port, anchored so a key merely ending in "port" can't slip in
F2v = $2 + 0 silently coerced 54_322→54 and "54322"→0Fixed — bare integers only (TOML _ separators accepted); anything else is reported by line and key as unprotected
F3Drain: wrong SO_REUSEADDR claim, 30s < 60s TIME-WAIT, ESTABLISHED called transientFixed — budget 75s; the message says SO_REUSEADDR needs the option on both sockets (an outbound client socket never set it, so Docker's bind really does fail) and that ESTABLISHED is a live connection, not a transient one
F4busy=$(sudo ss … || true) was fail-openFixed — the probe and both awk filters branch on their own exit status; a probe that can't run is reported as unknown, never as free
F5Ephemeral range hardcoded 32768-60999Fixed — read from /proc/sys/net/ipv4/ip_local_port_range; it filters the RESERVE set only, the CHECK set stays unfiltered
F6Leftover-container sweep wrong five waysReplaced — sweep deleted; supabase start gets e2e-up.sh's stop-and-retry instead, which also covers the exited/stale projects docker ps couldn't see
F7Fail/wait set included ports this config never bindsFixed — the CHECK/RESERVE split above; enabled-ness is derived per config section, so enabled = true on the pooler moves 54329 into the fail set with no edit
F8Reserved-ports merge was an unverified read-modify-writeFixed — a failed read skips the write rather than clobbering an unseen reservation; the merge keeps existing ranges intact; the value is read back and checked by membership (the kernel normalises to ranges — the live run prints 54320-54324,54327,54329, so a string compare would mismatch on every success)
F9Zero-grace LISTEN exit right after docker rmFalls away with F6
F10Summary said "(reserved …)" even when the sysctl failedFixed — three states (reserved / not reserved / no reservation needed) interpolated into the summary
F11"no ports parsed" message named only one causeFixed — the exit-1 names both: the step is obsolete, or the parser broke
F12Extract to scripts/preflight-ports.sh, call from both lanesDone — the workflow steps are now one line each
F13Ports hardcoded at four other sitesFollow-up #594 (deliberately not fixed here: this PR is CI hardening, that touches the local dev scripts)
F14No supabase start failure recovery in this laneFixed — the stop-and-retry above, mirroring scripts/e2e-up.sh:219-226
F15Decorative || trues, duplicated regex build, repeated indent pipeline, waited -eq 0 sentinelFixed — one indent() helper, one regex build, a named announced flag, no unguarded || true

Test evidence

Branch-coverage harness — 20 cases, 69 assertions, all green, run twice: under the default awk
and under gawk --posix (stand-in for the runner's mawk). It builds a throwaway repo root per case,
copies the real script in with only its two /proc constants repointed at fixtures (the patch is
grep-verified — a silently-failed patch produced a false GREEN in the previous round), and drives it
with sudo/ss shims. The sudo shim emulates the kernel: it expands ranges, stores a set, and
prints it back collapsed into ranges.

Covered: clean path · digit-bearing pop3_port protected · 54_322 parsed as 54322 · non-integer
value diagnosed by line+key · probe failure → WARNING, never "free" · sysctl fails → summary says NOT
reserved · sysctl "succeeds" but readback lacks the ports → WARNING · foreign LISTENer on an enabled
port → exit 1 naming the pid · LISTENer on the disabled pooler's port → no fail · same holder after
flipping the pooler to enabled = true → now fails (the derivation is live) · shadow_port reserved
but never checked · TIME-WAIT drain waits, announces once, proceeds clean · drain timeout → WARNING +
continue · no port keys → exit 1 naming both branches · ports outside the ephemeral range → nothing
to reserve, checks still run and still fail · unreadable reservation → write skipped · pre-existing
1024-1030 range survives the merge · no passwordless sudo → unprivileged probe · no ss
"unverified", not "free" · non-numeric drain timeout → loud fallback · a sudo shim that fails any call arriving without -n.

shellcheck 0.11.0 -xclean, zero findings (the previous round had no shellcheck available).

Re-review residuals — commit 53997446

The re-review passed all 15 findings and raised three non-blocking residuals, closed in one commit:

  • Every sudo call now uses -n. The probe gated on sudo -n true, but the sysctl write did
    not — a hand-run on a box without passwordless sudo would have blocked on a password prompt
    instead of degrading to the diagnostic half the header promises. The probe command is sudo -n ss
    too, closing the window where the gate passes and the timestamp expires before the probe.
  • The remaining command substitutions branch on their status (record split, merged-list build,
    readback membership) — in-memory string ops that realistically cannot fail, but that is the
    reasoning which produced the fail-open probe F4 condemned: a failed check_csv would have printed
    "no ENABLED service binds a port" and exited 0 having checked nothing, and an empty merged list
    would have cleared the bitmap it extends.
  • PREFLIGHT_DRAIN_TIMEOUT is validated as a whole number — a non-numeric value made every
    -ge test fail and spun the drain loop to the job timeout. It warns and falls back to 75.

Lane runs on this branch @ 53997446 — both green

LaneRunResult
integration.yml (dispatch)32934241228success — 71 passed, 2231 deselected (real work, zero skipped)
e2e.yml (dispatch)32934243435success — every step green, 73 passed (3.8m) Playwright

Plus the PR-triggered CI and
integration runs, both green.
(The previous SHA d992c12f was green on the same four lanes:
32932741569 /
32932743439.)

Both lanes printed the same three lines on the runner:

preflight: ephemeral source-port range 32768-60999
preflight: reserved 54320-54324,54327,54329
preflight: ports 54321,54322,54323,54324 free (enabled services; reserved from the ephemeral range)

Read that middle line closely: the kernel handed the value back range-collapsed, which is
exactly why F8's readback is a membership check and not a string compare — and the absence of a
mismatch WARNING is the membership check passing on real hardware. 54322 is inside 32768-60999
on GitHub's own runner, so the diagnosis holds where it matters, and it is re-verified in every run's
log forever.

Cannot prove a negative

One green run per lane does not prove the collision is gone — the failure rate was already low. What
the change guarantees is (a) the kernel will not hand out these ports as source ports any more,
which removes the mechanism, and (b) if a lane ever goes red on ports again, the log names the
culprit instead of leaving a bare address already in use.

Refs #574

AndresL230and others added 2 commits August 23, 2026 04:02
Run 32624263094 (push to main, 9f34454) died at `Start Supabase` with
`failed to bind host port for 0.0.0.0:54322: address already in use`, and
the identical commit re-run 40 minutes later passed.
The runner is a fresh VM, so nothing is left over. The mechanism is that
Supabase's ports (54320-54329) sit inside Linux's default ephemeral
source-port range (32768-60999): the attempt-1 log shows dozens of image
pulls finishing and the bind error landing in the same second, so an
outbound connection had been handed 54322 as its source port.
Reserve the ports out of automatic assignment (explicit bind() is
unaffected, so Docker can still publish them), derived from
supabase/config.toml so moving a port cannot un-protect it. Then diagnose
what reservation cannot undo: remove leftover containers publishing them,
fail by name on a foreign LISTENer, and wait out a draining socket.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The leftover-container cleanup was the one pipeline in the step without a
guard. Under the runner's `bash -e` (confirmed in run 32627212767's log:
`shell: /usr/bin/bash -e {0}`), a `docker rm -f` failure — permission, or a
race with another remover — aborted the step on the spot: no `preflight:`
diagnostic, no LISTEN check, no drain wait, no summary line. That is exactly
the opaque-CI-failure class this step exists to remove.
Warn and continue instead, consistent with the sysctl and drain branches;
the LISTEN check immediately after already names anything that survived.
Reproduced against the pre-fix commit: exit 123 with no output after the
docker error. With the guard: WARNING, then the step runs to completion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 23, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging5399744Commit Preview URL

Branch Preview URL
Aug 26 2026, 05:31 AM

@coderabbitai

coderabbitaiBot commented Aug 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e658fcf-c533-4321-9785-a9f8d9224359

📥 Commits

Reviewing files that changed from the base of the PR and between 9f34454 and d992c12.

📒 Files selected for processing (3)
  • .github/workflows/e2e.yml
  • .github/workflows/integration.yml
  • scripts/preflight-ports.sh

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds a CI-only Supabase port preflight script. E2E and integration workflows run it after checkout. The integration workflow also retries supabase start once after cleanup.

Changes

Supabase port preflight

Layer / File(s)Summary
Parse Supabase port configuration
scripts/preflight-ports.sh
The script validates prerequisites and parses enabled Supabase port settings from supabase/config.toml.
Reserve and verify ports
scripts/preflight-ports.sh
The script updates kernel reservations and probes configured ports. It reports foreign listeners, probe failures, and timeout results.
Run preflight and retry startup
.github/workflows/e2e.yml, .github/workflows/integration.yml, scripts/preflight-ports.sh
Both workflows run the preflight after checkout. The integration workflow stops Supabase and retries startup once after failure.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk:⚪ Minimal · up to d992c

This PR adds preflight protection for Supabase port collisions in CI and reports successful validation; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
participant GitHub Actions
participant preflight-ports.sh
participant Linux kernel
participant ss
participant Supabase CLI
GitHub Actions->>preflight-ports.sh: Run port preflight after checkout
preflight-ports.sh->>Linux kernel: Reserve configured ports
preflight-ports.sh->>ss: Probe configured bind ports
ss-->>preflight-ports.sh: Return socket status
preflight-ports.sh-->>GitHub Actions: Return preflight status
GitHub Actions->>Supabase CLI: Run supabase start
Supabase CLI-->>GitHub Actions: Return startup status
GitHub Actions->>Supabase CLI: Run supabase stop after failure
GitHub Actions->>Supabase CLI: Retry supabase start
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (2 skipped: 2 …
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly and concisely describes the main change: reserving Supabase ports before startup in both CI lanes.
Description check✅ PassedThe description is detailed and on-topic. It explains the failure, mechanism, implementation, affected workflows, testing evidence, issue reference, and review considerations. It does not use the repo…
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (2 skipped: 2 unsupported.)

Full details: Description check

Explanation

The description is detailed and on-topic. It explains the failure, mechanism, implementation, affected workflows, testing evidence, issue reference, and review considerations. It does not use the repository template headings exactly, but it provides the required information.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/integration-port-preflight

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@supabase

supabaseBot commented Aug 23, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

…sh (#574)
Merge-gate review of #588 found 15 issues in the inline step. The whole body
moves to scripts/preflight-ports.sh — testable, shellcheckable, and called by
BOTH lanes (integration.yml, and e2e.yml, which reaches `supabase start`
through `make e2e-up` and had the identical exposure).
What changed beyond the move:
- Two port sets, not one. RESERVE = every config.toml *port inside the LIVE
ephemeral range (read from /proc, not hardcoded). CHECK = the ports
`supabase start` actually binds — ENABLED sections only, minus shadow_port,
unfiltered by range. Only CHECK can fail or stall the job, so a holder on
the disabled pooler's 54329 no longer fails an unfiltered PR gate; enabled-
ness is derived per section, so flipping `enabled = true` is picked up.
- Parser: `[a-z_]*port` could not match the digit-bearing `pop3_port`; values
are now matched as bare integers (TOML underscores accepted) and anything
else is reported by line and key instead of being coerced to 0 and dropped.
- No more fail-open probes: the `ss` call and the awk filters branch on their
own exit status, so a probe that could not run never reads as "free".
- The sysctl is verified. A failed read of the current value skips the write
rather than clobbering a reservation it cannot see; the merge keeps existing
RANGES intact; the value is read back and checked by membership (the kernel
normalises to ranges, so a string compare would always mismatch); and the
summary line says NOT reserved when any of that fails.
- Drain: budget 30s -> 75s, because a TIME-WAIT entry lives a fixed 60s and
SO_REUSEADDR does NOT let a bind step over it (Linux needs the option on
both sockets, and an outbound connection never set it). ESTABLISHED is no
longer described as transient.
- The docker-container sweep is deleted — it was wrong in five ways where it
could fire. `supabase start` gets e2e-up.sh's stop-and-retry instead, which
also covers exited/stale projects the sweep could not see.
Refs #574
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230AndresL230 changed the title ci(integration): reserve the Supabase ports before supabase startci: reserve the Supabase ports before supabase start (both lanes)Aug 26, 2026
…guarded substitutions, validated timeout
Three non-blocking findings from the merge-gate re-review of #588:
- Every sudo call now uses `-n`. The socket probe already gated on
`sudo -n true`, but the sysctl write did not, so a hand-run on a box without
passwordless sudo would have blocked on a password prompt instead of failing
into the diagnostic half the header promises. The probe command itself is
`sudo -n ss` too, closing the window where the gate passes and the sudo
timestamp expires before the probe.
- The record-splitting command substitutions and the readback membership check
branch on their status like the probes do. They are in-memory string ops that
realistically cannot fail, but "realistically cannot fail" is the reasoning
that produced the fail-open probe this rework had to fix: a failed
`check_csv` pipeline would have printed "no ENABLED service binds a port" and
exited 0 having checked nothing, and a failed membership check would have
claimed the ports were reserved. The merged-list build is guarded for a
sharper reason still — an empty value would CLEAR the bitmap it extends.
- PREFLIGHT_DRAIN_TIMEOUT is validated as a whole number. A non-numeric value
made every `-ge` test fail and spun the drain loop to the job timeout — a
worse failure than the one being guarded. It warns and falls back to 75
rather than failing a lane over a typo.
Harness: 20 cases / 69 assertions green under both awk and `gawk --posix`,
including the three new ones (bad timeout value, empty override, and a sudo
shim that fails any call arriving without `-n`). shellcheck 0.11.0 clean.
Refs #574
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 6225bba into mainAug 26, 2026
10 checks passed
AndresL230 added a commit that referenced this pull request Aug 26, 2026
Brings in the include_answer_key default flip (#590), the dead
effective_explanations deletion (#587) and the CI port preflight (#588).
Clean auto-merge: main's routes/quiz.py hunks are in the generate
handler's response projection, this branch's are in the shared
gather/hoist above it.
Sign up for freeto 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.

1 participant

@AndresL230