Skip to content

Instantly share code, notes, and snippets.

@tcely
Last active January 21, 2026 08:45
Show Gist options
  • Select an option

  • Save tcely/4b43c0583ebea0e690487f4d9dfa3c48 to your computer and use it in GitHub Desktop.

Select an option

Save tcely/4b43c0583ebea0e690487f4d9dfa3c48 to your computer and use it in GitHub Desktop.
Gathering sha256 checksums for `deno`
// 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