Public: anyone can read this; the crew can change it.
Sentinel: release was degraded 50 minutes today: Release 0.7.479 is unproven: https://hello--forge-trevato-apps.open-platform.sh/ as desktop TimeoutError: The operation… #10190
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/10189-sentinel-release-was-degraded-50-minutes → main 2 commits fork
2 files
changed +36
−6
src/release/smoke.ts +13 −6
| @@ -313,12 +313,19 @@ export async function smoke(base: string, token = "", options: Options = {}): Pr | ||
| 313 | 313 | } |
| 314 | 314 | if (options.appHost) { |
| 315 | 315 | const url = new URL("/", options.appHost).href; |
| 316 | try { | |
| 317 | const res = await fetch(url, { redirect: "manual", signal: AbortSignal.timeout(10_000), headers: { "user-agent": AGENTS["desktop"]! } }); | |
| 318 | const body = await res.text(); | |
| 319 | if (/<title>\s*forge\s*<\/title>/i.test(body)) fail(url, "desktop", res, "an app host answered with the forge's page"); | |
| 320 | } catch (error) { | |
| 321 | failures.push({ url, agent: "desktop", status: "error", contentType: "", why: String(error) }); | |
| 316 | // This check asks one thing: does an app host answer as the forge (#6726)? An app that is | |
| 317 | // asleep, stopped or broken and gives no answer at all says nothing about that: it is the | |
| 318 | // app's health, not the release's, so it is asked once more and then left out of the verdict | |
| 319 | // (#10189: hello's host timing out held release 0.7.479 unproven for hours). | |
| 320 | for (let attempt = 1; attempt <= 2; attempt++) { | |
| 321 | try { | |
| 322 | const res = await fetch(url, { redirect: "manual", signal: AbortSignal.timeout(10_000), headers: { "user-agent": AGENTS["desktop"]! } }); | |
| 323 | const body = await res.text(); | |
| 324 | if (/<title>\s*forge\s*<\/title>/i.test(body)) fail(url, "desktop", res, "an app host answered with the forge's page"); | |
| 325 | break; | |
| 326 | } catch (error) { | |
| 327 | console.warn(`smoke: app host ${url} gave no answer (${error}); ${attempt === 1 ? "asking again" : "the app's health, not the release's"}`); | |
| 328 | } | |
| 322 | 329 | } |
| 323 | 330 | } |
| 324 | 331 | if (options.heavy && failures.length === 0) { |
test/smoke.test.ts +23
| @@ -199,3 +199,26 @@ test("--app-host fails when an app host answers with the forge's page", async () | ||
| 199 | 199 | appPage.stop(true); |
| 200 | 200 | } |
| 201 | 201 | }); |
| 202 | ||
| 203 | test("an app host that gives no answer is asked again, then left out of the release verdict (#10189)", async () => { | |
| 204 | const base = stub("text/css"); | |
| 205 | const timeout = () => Object.assign(new Error("The operation timed out."), { name: "TimeoutError" }); | |
| 206 | const make = (fails: number, body = "<title>notes</title>") => { | |
| 207 | let hostCalls = 0; | |
| 208 | const f = (async (input: string | URL, init?: RequestInit) => { | |
| 209 | if (String(input).startsWith("http://app.invalid")) { | |
| 210 | if (++hostCalls <= fails) throw timeout(); | |
| 211 | return new Response(body); | |
| 212 | } | |
| 213 | return fetch(input, init); | |
| 214 | }) as unknown as typeof fetch; | |
| 215 | return { f, calls: () => hostCalls }; | |
| 216 | }; | |
| 217 | const once = make(1, "<title>forge</title>"); | |
| 218 | // A wake-up timeout is asked again, and the second answer is still judged. | |
| 219 | expect((await smoke(base, "t", { appHost: "http://app.invalid/", fetch: once.f })).map((f) => f.why)).toEqual(["an app host answered with the forge's page"]); | |
| 220 | expect(once.calls()).toBe(2); | |
| 221 | const silent = make(99); | |
| 222 | expect(await smoke(base, "t", { appHost: "http://app.invalid/", fetch: silent.f })).toEqual([]); | |
| 223 | expect(silent.calls()).toBe(2); | |
| 224 | }); |