feat(checksum): add checksum support to download components and update workflow

- Enhanced ButtonCard component to accept and display SHA-256 checksums.
- Updated PlatformDownload and release-data to include checksum information for each release.
- Modified DownloadScript to apply twilight mode to new checksum buttons.
- Adjusted download page to retrieve and display checksums dynamically.
This commit is contained in:
taroj1205 2025-05-09 12:13:27 +12:00
parent 59974742e9
commit 3816206f6b
No known key found for this signature in database
GPG key ID: 0FCB6CFFE0981AB7
7 changed files with 234 additions and 76 deletions

View file

@ -23,3 +23,5 @@ jobs:
- name: Build project
run: npm run build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -3,44 +3,151 @@ interface Props {
label: string
href: string
variant?: string
checksum?: string
}
const { label, href } = Astro.props
const { label, href, checksum } = Astro.props
---
<a
href={href}
class="download-link group relative flex items-center justify-between overflow-hidden rounded-2xl border border-subtle p-4 transition-all duration-200 hover:border-coral hover:shadow-sm data-[twilight='true']:hover:border-zen-blue dark:hover:shadow-md"
rel="noopener noreferrer"
>
<div>
<p class="text-lg font-medium">{label}</p>
</div>
<div class="ml-4 flex flex-col items-end">
<div class="flex items-center">
<span
class="release-type-tag mr-2 rounded-full bg-coral/10 px-2 py-1 text-xs font-medium text-coral transition-colors duration-200 group-hover:bg-coral/20 data-[twilight='true']:bg-zen-blue/10 data-[twilight='true']:text-zen-blue data-[twilight='true']:group-hover:bg-zen-blue/20"
>
Beta
</span>
<div
class="download-arrow-icon text-muted-foreground rounded-xl border border-subtle p-2 transition-colors duration-200 group-hover:border-coral group-hover:text-coral data-[twilight='true']:group-hover:border-zen-blue data-[twilight='true']:group-hover:text-zen-blue"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-arrow-up-right"
<div class="relative flex flex-col">
<a
href={href}
class="download-link group flex flex-1 items-center justify-between rounded-2xl border border-subtle p-4 transition-all duration-200 hover:border-coral hover:shadow-sm data-[twilight='true']:hover:border-zen-blue dark:hover:shadow-md"
rel="noopener noreferrer"
tabindex="0"
>
<div class="flex items-center gap-2">
<p class="text-lg font-medium">{label}</p>
{
checksum && (
<span class="group/checksum relative hidden items-center md:flex">
<button
type="button"
class="checksum-icon-btn text-muted-foreground flex items-center justify-center rounded-full p-1 hover:text-coral focus:outline-none focus:ring-2 focus:ring-coral data-[twilight='true']:hover:text-zen-blue data-[twilight='true']:focus:ring-zen-blue"
aria-label="Show SHA-256 checksum"
tabindex="0"
>
<svg
width="18"
height="18"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
viewBox="0 0 24 24"
>
<path d="M12 3l8 4v5c0 5.25-3.5 9.74-8 11-4.5-1.26-8-5.75-8-11V7l8-4z" />
<path d="M9 12l2 2 4-4" />
</svg>
</button>
<span class="absolute -top-10 left-1/2 z-50 hidden min-w-[120px] -translate-x-1/2 select-none whitespace-nowrap rounded-md border border-subtle bg-[rgba(255,255,255,0.98)] px-3 py-2 text-xs text-gray-700 shadow group-focus-within/checksum:hidden group-hover/checksum:flex group-focus-within/checksum:group-hover/checksum:hidden dark:bg-[rgba(24,24,27,0.98)] dark:text-gray-100">
Show SHA-256
</span>
<span class="checksum-tooltip popover absolute -left-14 -top-12 z-50 hidden min-w-[220px] items-center gap-2 whitespace-nowrap rounded-md border border-subtle bg-[rgba(255,255,255,0.98)] px-3 py-2 text-xs text-gray-700 shadow group-focus-within/checksum:flex dark:bg-[rgba(24,24,27,0.98)] dark:text-gray-100">
<span class="flex-1 truncate font-mono text-xs">{checksum}</span>
<button
type="button"
class="copy-btn rounded bg-coral px-2 py-1 text-xs text-white hover:bg-coral/80 data-[twilight='true']:bg-zen-blue data-[twilight='true']:hover:bg-zen-blue/80"
>
Copy
</button>
</span>
</span>
)
}
</div>
<div class="flex flex-col items-end gap-2">
<div class="flex items-center gap-2">
<span
class="release-type-tag rounded-full bg-coral/10 px-2 py-1 text-xs font-medium text-coral transition-colors duration-200 group-hover:bg-coral/20 data-[twilight='true']:bg-zen-blue/10 data-[twilight='true']:text-zen-blue data-[twilight='true']:group-hover:bg-zen-blue/20"
>
<path d="M7 17 17 7"></path>
<path d="M7 7h10v10"></path>
</svg>
Beta
</span>
<div
class="download-arrow-icon text-muted-foreground rounded-xl border border-subtle p-2 transition-colors duration-200 group-hover:border-coral group-hover:text-coral data-[twilight='true']:group-hover:border-zen-blue data-[twilight='true']:group-hover:text-zen-blue"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-arrow-up-right"
>
<path d="M7 17 17 7"></path>
<path d="M7 7h10v10"></path>
</svg>
</div>
</div>
</div>
</div>
</a>
</a>
</div>
<style>
.checksum-expand {
transition: all 0.2s;
z-index: 100;
}
.checksum-icon-btn {
transition:
color 0.15s,
background 0.15s;
}
.checksum-tooltip {
transition: opacity 0.15s;
opacity: 1;
}
</style>
<script>
const checksumButtons = document.querySelectorAll(
'.checksum-icon-btn',
) as NodeListOf<HTMLButtonElement>
const checksumTooltips = document.querySelectorAll(
'.checksum-tooltip',
) as NodeListOf<HTMLDivElement>
const copyButtons = document.querySelectorAll(
'.copy-btn',
) as NodeListOf<HTMLButtonElement>
function stopEvent(e: Event) {
e.preventDefault?.()
e.stopPropagation()
}
function copyChecksum(e: Event, checksum: string) {
e.preventDefault()
e.stopPropagation()
navigator.clipboard.writeText(checksum)
const btn = e.currentTarget as HTMLButtonElement
const original = btn.innerText
btn.innerText = 'Copied!'
setTimeout(() => (btn.innerText = original), 1200)
}
// Attach listeners after DOM is ready
checksumButtons.forEach((btn) => {
btn.addEventListener('click', stopEvent)
})
checksumTooltips.forEach((tooltip) => {
tooltip.addEventListener('mousedown', stopEvent)
tooltip.addEventListener('click', stopEvent)
})
copyButtons.forEach((btn) => {
btn.addEventListener('click', (e) =>
copyChecksum(
e,
(
btn
.closest('.checksum-tooltip')
?.querySelector('.font-mono') as HTMLSpanElement
)?.innerText,
),
)
btn.addEventListener('mousedown', stopEvent)
})
</script>

View file

@ -76,7 +76,7 @@
// Apply twilight mode to all relevant elements
const coralElements = document.querySelectorAll(
'.download-browser-logo, .release-type-tag, .decorative-gradient, .download-link, .download-arrow-icon, .download-card__icon',
'.download-browser-logo, .release-type-tag, .decorative-gradient, .download-link, .download-arrow-icon, .download-card__icon, .checksum-icon-btn, .copy-btn',
)
coralElements.forEach((element) => {
element.setAttribute('data-twilight', 'true')

View file

@ -49,11 +49,13 @@ import DownloadCard from './ButtonCard.astro'
label="x86_64"
href={releases.x86_64.tarball.link}
variant="x86_64"
checksum={releases.x86_64.tarball.checksum}
/>
<DownloadCard
label="ARM64"
href={releases.aarch64.tarball.link}
variant="aarch64"
checksum={releases.aarch64.tarball.checksum}
/>
</div>
</div>
@ -65,6 +67,7 @@ import DownloadCard from './ButtonCard.astro'
label={releaseNote.label}
href={releaseNote.link}
variant={variant}
checksum={releaseNote.checksum}
/>
))}
</div>

View file

@ -1,50 +1,61 @@
---
import { releaseNotes, releaseNotesTwilight } from '../../release-notes'
export const releases = {
macos: {
universal: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.macos-universal.dmg',
label: `Universal`,
},
},
windows: {
x86_64: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer.exe',
label: `64-bit (Recommended)`,
},
arm64: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer-arm64.exe',
label: `ARM64`,
},
},
linux: {
x86_64: {
tarball: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-x86_64.tar.xz',
label: `Tarball x86_64`,
},
appImage: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen-x86_64.AppImage',
label: `AppImage x86_64`,
/**
* Returns the releases object, injecting checksums dynamically.
* @param checksums Record<string, string> mapping filenames to SHA-256 hashes
*/
export function getReleasesWithChecksums(checksums: Record<string, string>) {
return {
macos: {
universal: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.macos-universal.dmg',
label: `Universal`,
checksum: checksums['zen.macos-universal.dmg'],
},
},
aarch64: {
tarball: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-aarch64.tar.xz',
label: `Tarball aarch64`,
windows: {
x86_64: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer.exe',
label: `64-bit (Recommended)`,
checksum: checksums['zen.installer.exe'],
},
appImage: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen-aarch64.AppImage',
label: `AppImage aarch64`,
arm64: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer-arm64.exe',
label: `ARM64`,
checksum: checksums['zen.installer-arm64.exe'],
},
},
flathub: {
all: {
link: 'https://flathub.org/apps/app.zen_browser.zen',
label: `Flathub`,
linux: {
x86_64: {
tarball: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-x86_64.tar.xz',
label: `Tarball x86_64`,
checksum: checksums['zen.linux-x86_64.tar.xz'],
},
appImage: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen-x86_64.AppImage',
label: `AppImage x86_64`,
checksum: checksums['zen-x86_64.AppImage'],
},
},
aarch64: {
tarball: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-aarch64.tar.xz',
label: `Tarball aarch64`,
checksum: checksums['zen.linux-aarch64.tar.xz'],
},
appImage: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen-aarch64.AppImage',
label: `AppImage aarch64`,
checksum: checksums['zen-aarch64.AppImage'],
},
},
flathub: {
all: {
link: 'https://flathub.org/apps/app.zen_browser.zen',
label: `Flathub`,
},
},
},
},
}
}
---

View file

@ -2,9 +2,10 @@
import Description from '../components/Description.astro'
import Title from '../components/Title.astro'
import Layout from '../layouts/Layout.astro'
import { releases } from '../components/download/release-data.astro'
import { getReleasesWithChecksums } from '../components/download/release-data.astro'
import PlatformDownload from '../components/download/PlatformDownload.astro'
import DownloadScript from '../components/download/DownloadScript.astro'
import { getChecksums } from '../utils/githubChecksums'
import { library, icon } from '@fortawesome/fontawesome-svg-core'
import {
@ -20,6 +21,9 @@ const windowsIcon = icon({ prefix: 'fab', iconName: 'windows' })
const linuxIcon = icon({ prefix: 'fab', iconName: 'linux' })
const appleIcon = icon({ prefix: 'fab', iconName: 'apple' })
const githubIcon = icon({ prefix: 'fab', iconName: 'github' })
const checksums = await getChecksums()
const releases = getReleasesWithChecksums(checksums)
---
<DownloadScript />

View file

@ -0,0 +1,31 @@
/**
* Fetches the latest release notes from GitHub and parses the SHA-256 checksums.
* Returns a mapping from filename to checksum.
*/
export async function getChecksums() {
const token = import.meta.env.GITHUB_TOKEN;
if (!token) throw new Error('GITHUB_TOKEN is not set in environment variables');
const res = await fetch('https://api.github.com/repos/zen-browser/desktop/releases/latest', {
headers: {
'Accept': 'application/vnd.github+json',
'Authorization': `Bearer ${token}`,
'X-GitHub-Api-Version': '2022-11-28',
'User-Agent': 'zen-browser-checksum-fetcher',
},
});
if (!res.ok) throw new Error('Failed to fetch GitHub release: ' + res.statusText);
const data = await res.json();
const body = data.body as string;
// Extract the checksum block
const match = body.match(/File Checksums \(SHA-256\)[\s\S]*?```([\s\S]*?)```/);
const checksums: Record<string, string> = {};
if (match && match[1]) {
match[1].split('\n').forEach(line => {
const [hash, filename] = line.trim().split(/\s+/, 2);
if (hash && filename) checksums[filename] = hash;
});
}
return checksums;
}