chore: Let nathan deploys proceed without a reply tunnel

When the localtunnel service is unreachable, a deploy with an explicit
fresh instance name is still confirmable by polling the instance URL, so
send the webhook anyway instead of aborting. Commands whose result only
arrives via the tunnel still fail fast.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Charlie Kolb 2026-08-05 14:08:41 +02:00
parent d5845df4fc
commit dcb7eb3fd3
No known key found for this signature in database
2 changed files with 31 additions and 15 deletions

View File

@ -15,8 +15,10 @@ the same commands from the repo via `pnpm nathan`.
- **No tunnel setup needed.** Nathan replies asynchronously through a short-lived
public tunnel that the script opens for you (`npx localtunnel`). A `deploy` to a
**new** `test-<name>` also polls that instance URL directly, so it still reports
success even if the tunnel drops. (Redeploying a name that's *already up* skips
the poll — it can't tell the old instance from the new — so it falls back to the
success even if the tunnel drops — or if the tunnel service (loca.lt) is down
entirely, in which case the deploy proceeds tunnel-less and only the poll
reports completion. (Redeploying a name that's *already up* skips
the poll — it can't tell the old instance from the new — so it needs the
tunnel; prefer a fresh name when you need the reliable report.)
- **A token in `~/.n8n/dev/nathan-token`.** If a command reports no token, **ask the
user for one** — point them at the form

View File

@ -5,6 +5,8 @@
// `response_url` we hand it — the same mechanism Slack uses. Since the internal
// instance can't reach localhost, we open a throwaway public tunnel to a local
// server, use that as the response_url, print whatever Nathan sends, and clean up.
// If the tunnel service is unreachable, a deploy with an explicit fresh name still
// works: the webhook is sent anyway and the instance URL is polled for completion.
//
// Usage: node scripts/nathan.mjs <nathan args> (or: pnpm nathan -- <args>)
// node scripts/nathan.mjs help
@ -224,17 +226,30 @@ async function waitReachable(url) {
return false;
}
let tunnel;
// Reliable backstop: if the instance URL is predictable, poll it directly so a
// dropped tunnel doesn't cost us the result — and if the tunnel service is down
// entirely (loca.lt outages happen), a pollable deploy can proceed without it.
const pollUrl = predictDeployUrl(text);
const up = (u) => fetch(`${u}/healthz`, { signal: AbortSignal.timeout(8000) }).then((r) => r.ok).catch(() => false);
let tunnel = null;
try {
console.error(`Opening tunnel + sending: /nathan ${text}`);
tunnel = await startTunnel();
if (!(await waitReachable(tunnel.url))) throw new Error(`${tunnel.url} did not become reachable in time`);
} catch (e) {
console.error('Could not open a public tunnel:', e.message);
await cleanup(1);
}
if (!(await waitReachable(tunnel.url))) {
console.error(`Tunnel ${tunnel.url} did not become reachable in time.`);
await cleanup(1);
try { tunnel?.proc.kill(); } catch {}
tunnel = null;
if (!pollUrl) {
console.error("This command's result only arrives via the tunnel — cannot continue without one.");
await cleanup(1);
}
if (await up(pollUrl)) {
console.error(`${pollUrl} already responds, so a redeploy can't be confirmed by polling. Retry, or deploy to a fresh test-<name>.`);
await cleanup(1);
}
console.error(`Continuing without the tunnel: Nathan's replies will be lost; polling ${pollUrl} to confirm the deploy instead.\n`);
}
// --- fire the command --------------------------------------------------------
@ -248,7 +263,8 @@ try {
token,
text,
command: '/nathan',
response_url: tunnel.url + CALLBACK_PATH,
// .invalid never resolves (RFC 2606) — replies go nowhere in tunnel-less mode
response_url: tunnel ? tunnel.url + CALLBACK_PATH : 'https://nathan-reply.invalid/',
user_id: user,
user_name: user,
channel_id: process.env.NATHAN_SLACK_CHANNEL || DEFAULT_SLACK_CHANNEL,
@ -265,13 +281,11 @@ if (!res.ok) {
}
console.error('Sent. Waiting for Nathan to reply (Ctrl-C to stop)…\n');
// Reliable backstop: if the instance URL is predictable, poll it directly so a
// dropped tunnel doesn't cost us the result. Skip if it already responds — that's
// the previous deployment still up mid-redeploy, indistinguishable from the new one.
const pollUrl = predictDeployUrl(text);
// Skip polling if the instance already responds — that's the previous deployment
// still up mid-redeploy, indistinguishable from the new one (tunnel-less mode
// already ruled this out above, before sending the webhook).
if (pollUrl) {
const up = (u) => fetch(`${u}/healthz`, { signal: AbortSignal.timeout(8000) }).then((r) => r.ok).catch(() => false);
if (await up(pollUrl)) {
if (tunnel && (await up(pollUrl))) {
console.error(`(${pollUrl} already responds — redeploy in progress; relying on the tunnel for completion.)`);
} else {
(async () => {