Last active
January 21, 2026 08:45
-
-
Save tcely/4b43c0583ebea0e690487f4d9dfa3c48 to your computer and use it in GitHub Desktop.
Gathering sha256 checksums for `deno`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // pull_digests.ts | |
| /* | |
| * $ version='2.6.5' | |
| * $ deno run \ | |
| * --allow-net=api.github.com,github.com,release-assets.githubusercontent.com \ | |
| * --allow-write=release_digests.sha256 \ | |
| * pull_digests.ts "v${version}" | |
| * $ file="$(deno eval 'console.log("deno-" + Deno.build.target + ".zip")')" | |
| * $ checksum="$(grep -e "${file}" release_digests.sha256 | awk '1 == NR {print $NF; exit;}')" | |
| * $ deno upgrade --checksum="${checksum}" "${version}" | |
| */ | |
| const repo = "denoland/deno"; | |
| const outputFile = "release_digests.sha256"; | |
| // GitHub rollout date for native asset.digest field | |
| const GITHUB_DIGEST_ROLLOUT_DATE = new Date("2025-06-03T00:00:00Z"); | |
| // Added `.sha256sum` files to releases | |
| const DENO_DIGEST_INCLUSION_DATE = new Date("2024-10-16T00:00:00Z"); | |
| // Capture tag from CLI argument or default to "latest" | |
| const [tagArg] = Deno.args; | |
| const isLatest = !tagArg || tagArg === "latest"; | |
| /** | |
| * Formats Date to YYYY-0MM-DD (e.g., 2026-001-20) | |
| */ | |
| function formatDate(date: Date): string { | |
| const z = "0"; | |
| const month = String(1 + date.getMonth()).padStart(3, z); | |
| const day = String(date.getDate()).padStart(2, z); | |
| return `${date.getFullYear()}-${month}-${day}`; | |
| } | |
| async function fetchReleaseDigests() { | |
| const host = "api.github.com"; | |
| // The '/repos/' segment is mandatory for the GitHub API path | |
| const baseUrl = `https://${host}/repos/${repo}/releases`; | |
| const endpoint = isLatest | |
| ? `${baseUrl}/latest` | |
| : `${baseUrl}/tags/${tagArg}`; | |
| console.log( | |
| `Fetching digests for: ${isLatest ? "latest release" : tagArg}...`, | |
| ); | |
| console.log(`Requesting URL: ${endpoint}`); | |
| const response = await fetch(endpoint, { | |
| headers: { | |
| "Accept": "application/vnd.github+json", | |
| "X-GitHub-Api-Version": "2022-11-28", | |
| "User-Agent": "Deno-Digest-Fetcher/1.0", | |
| }, | |
| }); | |
| if (!response.ok) { | |
| if (404 === response.status) { | |
| throw new Error( | |
| `HTTP 404: Release "${ | |
| tagArg || "latest" | |
| }" not found. Verify the tag includes 'v' (e.g., v2.0.1).`, | |
| ); | |
| } | |
| throw new Error(`API Error ${response.status}: ${response.statusText}`); | |
| } | |
| const releaseData = await response.json(); | |
| const publishDate = new Date(releaseData.published_at); | |
| const assets = releaseData.assets; | |
| const published = publishDate.toLocaleDateString(); | |
| const tag = releaseData.tag_name; | |
| const version = tag.replace(/^v/, ""); | |
| const finalDigests = new Map<string, string>(); // Filename -> Hex Hash | |
| console.log( | |
| `Processing ${tag} ([${formatDate(publishDate)}]: ${published})...`, | |
| ); | |
| // 1. Check if the release pre-dates the inclusion of digest files | |
| if (publishDate < DENO_DIGEST_INCLUSION_DATE) { | |
| const formatted = formatDate(DENO_DIGEST_INCLUSION_DATE); | |
| throw new Error( | |
| `Unsupported: ${tag} was published on: ` + | |
| `[${formatDate(publishDate)}]: ${published}. ` + | |
| `Deno only provides digests for releases created after ${formatted}.`, | |
| ); | |
| } | |
| // 2. Check if the release pre-dates the June 2025 digest feature | |
| if (publishDate < GITHUB_DIGEST_ROLLOUT_DATE) { | |
| const formatted = formatDate(GITHUB_DIGEST_ROLLOUT_DATE); | |
| /* | |
| * throw new Error( | |
| * "Unsupported: " + | |
| * `${tag} was published on: [${formatDate(publishDate)}]: ${published}. ` + | |
| */ | |
| console.warn( | |
| "[WARNING] " + | |
| `GitHub only provides native digests for releases created after ${formatted}.`, | |
| ); | |
| } | |
| // 3. Set any digests provided by GitHub | |
| for (const asset of assets) { | |
| if (asset.digest) { | |
| finalDigests.set( | |
| asset.name, | |
| asset.digest.replace(/^sha256:/, "").toLowerCase(), | |
| ); | |
| } | |
| } | |
| // 4. Set any digests provided by suffix files | |
| for (const asset of assets) { | |
| if (asset.name.endsWith(".sha256sum")) { | |
| const contentRes = await fetch(asset.browser_download_url); | |
| const text = await contentRes.text(); | |
| const parts = text.trim().split(/\s+[*]?/); | |
| const prefixFileName = asset.name.replace(/\.sha256sum$/, ""); | |
| let fileName = prefixFileName; | |
| let digest = "undefined"; | |
| if ("Algorithm" === parts[0]) { | |
| const winFormat = text.trim().split(/[\r\n]+/); | |
| // Line 2 has the digest | |
| let p = winFormat[1].trim().split(/\s+:\s+/); | |
| digest = p[1].trim().toLowerCase(); | |
| // Line 3 has the full file path | |
| p = winFormat[2].trim().split(/\s+:\s+/); | |
| p = p[1].trim().split(/\\+/); | |
| fileName = p[p.length - 1]; | |
| } else { | |
| fileName = parts[1]; | |
| digest = parts[0].trim().toLowerCase(); | |
| } | |
| if ("undefined" !== digest) { | |
| finalDigests.set(fileName, digest); | |
| } | |
| const prefixDigest = finalDigests.get(prefixFileName); | |
| const suffixFileDigest = finalDigests.get(fileName); | |
| if (fileName !== prefixFileName) { | |
| console.warn(`[MISMATCH] Asset: ${asset.name}`); | |
| console.warn(` File: ${fileName}`); | |
| console.warn(` Prefix: ${prefixFileName}`); | |
| } | |
| // Mismatch check (Only if both exist) | |
| if ( | |
| prefixDigest && suffixFileDigest && | |
| prefixDigest !== suffixFileDigest | |
| ) { | |
| console.warn(`[MISMATCH] Asset: ${asset.name}`); | |
| console.warn( | |
| ` GitHub API: SHA256 (${prefixFileName}) = ${prefixDigest}`, | |
| ); | |
| console.warn( | |
| ` Asset File: SHA256 (${fileName}) = ${suffixFileDigest}`, | |
| ); | |
| } | |
| } | |
| } | |
| // 5. Resolve / verify digests for `.zip` archives | |
| for (const asset of assets) { | |
| const githubDigest = asset.digest | |
| ? asset.digest.replace(/^sha256:/, "").toLowerCase() | |
| : null; | |
| const suffixFileDigest = finalDigests.get(asset.name); | |
| // Determine the most authoritative digest available | |
| const resolvedDigest = githubDigest || suffixFileDigest; | |
| if (resolvedDigest) { | |
| finalDigests.set(asset.name, resolvedDigest); | |
| if (`deno-${Deno.build.target}.zip` === asset.name) { | |
| console.log( | |
| `[CMD]: deno upgrade --checksum='${resolvedDigest}' '${version}'`, | |
| ); | |
| } | |
| } else if (asset.name.endsWith(".zip")) { | |
| // Require zip archives to have a provided digest | |
| throw new Error( | |
| `Security Error: Asset "${asset.name}" is missing a SHA-256 digest.`, | |
| ); | |
| } | |
| } | |
| // 6. Write BSD-style --tag format | |
| const lines = Array.from(finalDigests.entries()) | |
| .map(([file, digest]) => `SHA256 (${file}) = ${digest}`); | |
| if (lines.length === 0) { | |
| throw new Error("[ERROR] No digests found."); | |
| } | |
| await Deno.writeTextFile(outputFile, lines.join("\n")); | |
| console.log( | |
| `Successfully saved ${lines.length} digests for ${tag} to ${outputFile}`, | |
| ); | |
| } | |
| fetchReleaseDigests().catch((err) => { | |
| console.error(err.message); | |
| Deno.exit(1); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment