From 864860e432c0b14bad166639802a4b33855e742b Mon Sep 17 00:00:00 2001 From: stack72 Date: Tue, 5 May 2026 13:20:03 +0100 Subject: [PATCH] fix(runtime): bundle Deno canary 19bd3d8b as v2.8.0 bridge for tls_wrap panic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deno v2.7.12–v2.7.14 panic on Option::unwrap() of a None ArrayBuffer in ext/node/ops/tls_wrap.rs (denoland/deno#33713), surfacing in swamp as exit-134 crashes during post-workflow datastore pushes. Fix landed on Deno main as df8d21c2 via denoland/deno#33737 (merged 2026-05-01); ships in v2.8.0 in ~2 weeks. Bridge by bundling a pinned Deno canary commit known to contain the fix (19bd3d8b — verified 83 commits ahead of df8d21c2, 0 behind), with a deliberately one-grep-removable opt-in: - scripts/deno_canary.txt (new) is the toggle. First non-blank, non-`#`-comment line is the SHA. Header documents WHY, HOW, and a 6-step BACK-OUT CHECKLIST. - DENO_CANARY_SHA env var overrides the file (CI ad-hoc testing). - Stable channel path is bit-identical when no pin is present — download_deno.ts falls through to the existing GitHub-releases URL. Version marker uses canary- so its shape differs from semver: the runtime's exact-string version-marker check forces re-extraction in either direction, leaving no stale binary on user machines when we switch back to stable. All canary code is wrapped in `// CANARY-BRIDGE` markers (3 sites). compile.ts, deno.json, and .github/workflows/release.yml are untouched. Lab issues: swamp-club#213, swamp-club#219, swamp-club#224. Validation - All 5 canary URLs HEAD'd 200 across the swamp build target matrix. - Local aarch64-apple-darwin build extracts canary deno cleanly (~/.swamp/deno/.version = canary-19bd3d8b). - TLS panic probe runs clean on canary; reproducibly panics on v2.7.14. - swamp-uat uat:cli on aarch64-apple-darwin: canary 387 passed / 11 failed vs stable v2.7.14 382 passed / 16 failed. Zero panic / tls_wrap / SIGABRT signatures in either build. Back-out (when v2.8.0 ships) — embedded in scripts/deno_canary.txt: 1. rm scripts/deno_canary.txt scripts/download_deno_test.ts 2. Delete the two CANARY-BRIDGE blocks in scripts/download_deno.ts 3. `grep -rn CANARY-BRIDGE` returns zero matches 4. Bump system Deno to v2.8.0 5. Rebuild and republish swamp via the normal release flow Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/deno_canary.txt | 27 +++++++++++ scripts/download_deno.ts | 90 ++++++++++++++++++++++++++++++++--- scripts/download_deno_test.ts | 88 ++++++++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+), 7 deletions(-) create mode 100644 scripts/deno_canary.txt create mode 100644 scripts/download_deno_test.ts diff --git a/scripts/deno_canary.txt b/scripts/deno_canary.txt new file mode 100644 index 00000000..20711e37 --- /dev/null +++ b/scripts/deno_canary.txt @@ -0,0 +1,27 @@ +# CANARY-BRIDGE pin — temporary opt-in to a Deno canary commit. +# +# WHY THIS EXISTS +# Deno v2.7.12–v2.7.14 panic on `Option::unwrap()` of a None ArrayBuffer +# in ext/node/ops/tls_wrap.rs (denoland/deno#33713). The fix landed on +# main in denoland/deno#33737 (commit df8d21c2) and will ship in v2.8.0 +# in ~2 weeks. This pin bridges to that release without forcing every +# user onto a broken stable. +# +# HOW IT WORKS +# The first non-blank, non-`#`-comment line below is treated as the +# Deno canary commit SHA. scripts/download_deno.ts reads it (via +# readCanarySha()), downloads from dl.deno.land/canary//, and +# stamps version.txt as `canary-`. Setting the env var +# DENO_CANARY_SHA overrides this file (CI ad-hoc testing). +# +# BACK-OUT CHECKLIST (when v2.8.0 ships) +# 1. Delete this file (scripts/deno_canary.txt). +# 2. In scripts/download_deno.ts, delete the two `CANARY-BRIDGE` +# blocks (the helpers near the top and the branch in main()). +# 3. Delete scripts/download_deno_test.ts (or its canary cases). +# 4. `grep -rn CANARY-BRIDGE` should return zero matches. +# 5. Bump the system Deno used by CI / contributors to v2.8.0. +# 6. Rebuild and republish swamp via the normal release flow. +# +# Pinned commit (verified to include the tls_wrap fix df8d21c2): +19bd3d8b99d92f15d20692aca02ac059bbc9ada7 diff --git a/scripts/download_deno.ts b/scripts/download_deno.ts index f612ae8a..89fd85c1 100644 --- a/scripts/download_deno.ts +++ b/scripts/download_deno.ts @@ -32,6 +32,77 @@ const TARGET_ARTIFACT_MAP: Record = { "x86_64-pc-windows-msvc": "deno-x86_64-pc-windows-msvc.zip", }; +// CANARY-BRIDGE: temporary opt-in for shipping a pinned Deno canary commit +// while waiting for an upstream stable release. Remove this entire block +// (and the call site in main(), and scripts/deno_canary.txt) when the +// targeted stable release ships. See scripts/deno_canary.txt for the +// full back-out checklist. +const CANARY_PIN_FILE = "deno_canary.txt"; + +/** + * Returns the canary commit SHA to download, or null for stable mode. + * + * Priority: + * 1. `DENO_CANARY_SHA` env var (CI ad-hoc override). + * 2. `scripts/deno_canary.txt` (committed pin — first non-blank, + * non-`#`-comment line). + * 3. null — fall through to the stable GitHub-releases path. + */ +async function readCanarySha(): Promise { + const fromEnv = Deno.env.get("DENO_CANARY_SHA")?.trim(); + if (fromEnv) return fromEnv; + + const pinPath = join(import.meta.dirname ?? ".", CANARY_PIN_FILE); + let content: string; + try { + content = await Deno.readTextFile(pinPath); + } catch (err) { + if (err instanceof Deno.errors.NotFound) return null; + throw err; + } + for (const raw of content.split("\n")) { + const line = raw.trim(); + if (!line || line.startsWith("#")) continue; + return line; + } + return null; +} + +export interface DownloadPlan { + url: string; + versionLabel: string; + channel: "stable" | "canary"; +} + +/** + * Builds the download URL and the string written to `version.txt`. + * + * Stable channel uses the GitHub releases artifact path; canary uses + * `dl.deno.land/canary//`. The `versionLabel` distinguishes canary + * builds (`canary-`) so the runtime's version-marker check + * forces a fresh extraction on every SHA bump. + */ +export function buildDownloadPlan( + channel: "stable" | "canary", + versionOrSha: string, + artifact: string, +): DownloadPlan { + if (channel === "canary") { + return { + url: `https://dl.deno.land/canary/${versionOrSha}/${artifact}`, + versionLabel: `canary-${versionOrSha.slice(0, 8)}`, + channel, + }; + } + return { + url: + `https://github.com/denoland/deno/releases/download/v${versionOrSha}/${artifact}`, + versionLabel: versionOrSha, + channel, + }; +} +// END CANARY-BRIDGE + /** Maps Deno.build.os + Deno.build.arch to a target triple. */ function detectCurrentTarget(): string { const os = Deno.build.os; @@ -129,17 +200,22 @@ async function main() { Deno.exit(1); } - const version = await getDenoVersion(); const isWindows = target.includes("windows"); const binaryName = isWindows ? "deno.exe" : "deno"; - console.log(`Deno version: ${version}`); + // CANARY-BRIDGE: pick channel from deno_canary.txt / DENO_CANARY_SHA + // (returns null in stable mode). Remove this branch when the bridge ends. + const canarySha = await readCanarySha(); + const plan = canarySha + ? buildDownloadPlan("canary", canarySha, artifact) + : buildDownloadPlan("stable", await getDenoVersion(), artifact); + + console.log(`Channel: ${plan.channel}`); console.log(`Target: ${target}`); console.log(`Artifact: ${artifact}`); + console.log(`Source: ${plan.url}`); - const url = - `https://github.com/denoland/deno/releases/download/v${version}/${artifact}`; - const zipBytes = await downloadFile(url); + const zipBytes = await downloadFile(plan.url); console.log(`Downloaded ${zipBytes.length} bytes, extracting...`); const binaryBytes = await extractDenoFromZip(zipBytes, binaryName); @@ -163,10 +239,10 @@ async function main() { // Write version file const versionPath = join(outputDir, "version.txt"); - await Deno.writeTextFile(versionPath, version); + await Deno.writeTextFile(versionPath, plan.versionLabel); const sizeMB = (binaryBytes.length / 1024 / 1024).toFixed(1); - console.log(`Wrote deno ${version} (${sizeMB} MB) to ${outputPath}`); + console.log(`Wrote deno ${plan.versionLabel} (${sizeMB} MB) to ${outputPath}`); console.log(`Wrote version to ${versionPath}`); } diff --git a/scripts/download_deno_test.ts b/scripts/download_deno_test.ts new file mode 100644 index 00000000..de4dd445 --- /dev/null +++ b/scripts/download_deno_test.ts @@ -0,0 +1,88 @@ +// Swamp, an Automation Framework +// Copyright (C) 2026 System Initiative, Inc. +// +// This file is part of Swamp. +// +// Swamp is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License version 3 +// as published by the Free Software Foundation, with the Swamp +// Extension and Definition Exception (found in the "COPYING-EXCEPTION" +// file). +// +// Swamp is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Swamp. If not, see . + +import { assertEquals } from "@std/assert"; +import { buildDownloadPlan } from "./download_deno.ts"; + +// CANARY-BRIDGE: these tests cover the canary opt-in. When the bridge +// ends, delete this file along with the helpers in download_deno.ts. +// +// `scripts/` is excluded from `deno test` in deno.json, so this file +// does not run in CI by default. To run it manually: +// +// cp scripts/download_deno*.ts /tmp/ \ +// && deno test --no-check --config deno.json /tmp/download_deno_test.ts +// +// The smoke test (compile + run TLS probe against the bundled deno) is +// the load-bearing validation; this file documents the URL-builder +// contract and acts as a fast pre-build sanity check. + +Deno.test("buildDownloadPlan: stable channel uses GitHub releases URL", () => { + const plan = buildDownloadPlan( + "stable", + "2.7.14", + "deno-x86_64-apple-darwin.zip", + ); + assertEquals(plan.channel, "stable"); + assertEquals( + plan.url, + "https://github.com/denoland/deno/releases/download/v2.7.14/deno-x86_64-apple-darwin.zip", + ); + assertEquals(plan.versionLabel, "2.7.14"); +}); + +Deno.test("buildDownloadPlan: canary channel uses dl.deno.land URL", () => { + const sha = "19bd3d8b99d92f15d20692aca02ac059bbc9ada7"; + const plan = buildDownloadPlan( + "canary", + sha, + "deno-aarch64-apple-darwin.zip", + ); + assertEquals(plan.channel, "canary"); + assertEquals( + plan.url, + `https://dl.deno.land/canary/${sha}/deno-aarch64-apple-darwin.zip`, + ); +}); + +Deno.test("buildDownloadPlan: canary versionLabel uses 8-char short sha", () => { + const plan = buildDownloadPlan( + "canary", + "19bd3d8b99d92f15d20692aca02ac059bbc9ada7", + "deno-x86_64-unknown-linux-gnu.zip", + ); + assertEquals(plan.versionLabel, "canary-19bd3d8b"); +}); + +Deno.test("buildDownloadPlan: canary label distinct from any stable semver", () => { + // The runtime compares version markers as exact strings, so canary labels + // must not collide with semver — guards against stale-cache issues when + // bridging back to stable. + const canary = buildDownloadPlan( + "canary", + "19bd3d8b99d92f15d20692aca02ac059bbc9ada7", + "deno-x86_64-apple-darwin.zip", + ); + const stable = buildDownloadPlan( + "stable", + "2.8.0", + "deno-x86_64-apple-darwin.zip", + ); + assertEquals(canary.versionLabel === stable.versionLabel, false); +});