diff --git a/.agents/skills/nathan/SKILL.md b/.agents/skills/nathan/SKILL.md index c339123fcb8..c379ad77378 100644 --- a/.agents/skills/nathan/SKILL.md +++ b/.agents/skills/nathan/SKILL.md @@ -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-` 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 diff --git a/scripts/nathan.mjs b/scripts/nathan.mjs index eba857beb5f..ad56c817b47 100644 --- a/scripts/nathan.mjs +++ b/scripts/nathan.mjs @@ -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 (or: pnpm nathan -- ) // 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-.`); + 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 () => {