From 6b28c54e33d9352491c25834e5be11957ca76cff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 19 Oct 2026 19:21:30 -0400 Subject: [PATCH] Site: releases and source on GitHub; the Mac build is notarized The worker reads the newest release and download counts from GitHub's API, with an optional GITHUB_TOKEN secret for the rate limit. Download links go to GitHub's latest release, and the source link wears the GitHub mark. The "not notarized" notice goes: macOS builds are signed and notarized now. Co-Authored-By: Claude --- site/e2e/text.spec.ts | 23 +++-------------------- site/src/icons.ts | 2 +- site/src/main.ts | 6 +++--- site/src/release.ts | 4 ++-- site/src/text.ts | 2 -- site/worker/index.ts | 36 ++++++++++++++++++++---------------- 6 files changed, 29 insertions(+), 44 deletions(-) diff --git a/site/e2e/text.spec.ts b/site/e2e/text.spec.ts index 8df5c61..90d1671 100644 --- a/site/e2e/text.spec.ts +++ b/site/e2e/text.spec.ts @@ -1,7 +1,7 @@ import { DOWNLOADS, expect, test, VERSION, type Page } from "./fixtures.ts"; import { TEXT, word, type Key, type Lang } from "../src/text.ts"; -const REPO = "https://git.zacharyzhang.com/ZacharyZhang-NY/ElyOffice"; +const REPO = "https://github.com/ELY-Open-Source/ElyOffice"; const APPS = ["ElyDoc", "ElyTable", "ElyShow", "ElyPDF", "ElyTree", "ElyMark"]; const RELEASES = `${REPO}/releases/latest`; @@ -114,10 +114,10 @@ test.describe("English locale", () => { }); test("a failed version read shows as an error", async ({ page }) => { - await page.route("**/release.json", (r) => r.fulfill({ status: 502, body: "Gitea answered 500" })); + await page.route("**/release.json", (r) => r.fulfill({ status: 502, body: "GitHub answered 500" })); await page.goto("/"); await expect.poll(() => errors.length).toBe(1); - expect(errors[0]).toContain("Gitea answered 500"); + expect(errors[0]).toContain("GitHub answered 500"); await expect(page.locator("#download h2")).toHaveText("Ely Office"); for (const c of await page.locator("[data-t=count]").all()) await expect(c).toBeHidden(); errors = []; @@ -174,20 +174,3 @@ test("the hero links share one baseline in either language", async ({ browser }) await page.close(); } }); - -test("the download area tells how to open an unnotarized app", async ({ browser }) => { - for (const [locale, words] of [ - ["en-US", ["System Settings", "Privacy & Security", "Open Anyway"]], - ["zh-CN", ["系统设置", "隐私与安全性", "仍要打开"]], - ] as const) { - const page = await browser.newPage({ locale, reducedMotion: "reduce" }); - await page.goto("/"); - const steps = page.locator("#download [data-t=unsigned]"); - await expect(steps).toBeVisible(); - for (const word of words) await expect(steps).toContainText(word); - await page.locator(".lang").click(); - await expect(steps).toBeVisible(); - await expect(steps).not.toContainText(words[0]); - await page.close(); - } -}); diff --git a/site/src/icons.ts b/site/src/icons.ts index 7af811f..25efa4b 100644 --- a/site/src/icons.ts +++ b/site/src/icons.ts @@ -3,7 +3,7 @@ import { said, type Key } from "./text.ts"; const ICONS = { download: ``, - gitea: ``, + github: ``, }; /** A link with `icon` before its keyed text. */ diff --git a/site/src/main.ts b/site/src/main.ts index 1fd28fa..6898bd4 100644 --- a/site/src/main.ts +++ b/site/src/main.ts @@ -11,7 +11,7 @@ import { said, speak, word, type Lang } from "./text.ts"; import { RELEASES, latest } from "./release.ts"; import { iconLink } from "./icons.ts"; -const REPO = "https://git.zacharyzhang.com/ZacharyZhang-NY/ElyOffice"; +const REPO = "https://github.com/ELY-Open-Source/ElyOffice"; const app = document.querySelector("#app"); if (!app) throw new Error("index.html has no #app"); @@ -19,7 +19,7 @@ if (!app) throw new Error("index.html has no #app"); const hero = document.createElement("header"); const title = document.createElement("h1"); title.textContent = "Ely Office"; -const source = iconLink("gitea", "source", REPO); +const source = iconLink("github", "source", REPO); const download = iconLink("download", "get", RELEASES); const links = document.createElement("div"); links.className = "links"; @@ -40,7 +40,7 @@ const count = said("p", "count"); count.hidden = true; const get = said("a", "get"); get.href = RELEASES; -footer.append(version, count, said("p", "platforms"), said("p", "needs"), said("p", "unsigned"), get); +footer.append(version, count, said("p", "platforms"), said("p", "needs"), get); const toggle = said("button", "other"); toggle.className = "lang"; diff --git a/site/src/release.ts b/site/src/release.ts index 23dbc44..7277b73 100644 --- a/site/src/release.ts +++ b/site/src/release.ts @@ -1,5 +1,5 @@ -// Gitea redirects this to the newest release. -export const RELEASES = "https://git.zacharyzhang.com/ZacharyZhang-NY/ElyOffice/releases/latest"; +// GitHub redirects this to the newest release. +export const RELEASES = "https://github.com/ELY-Open-Source/ElyOffice/releases/latest"; export type Release = { version: string; downloads: number }; diff --git a/site/src/text.ts b/site/src/text.ts index 3796a0f..4339b7c 100644 --- a/site/src/text.ts +++ b/site/src/text.ts @@ -15,7 +15,6 @@ const en = { download: "Download", platforms: "Out now for macOS, Windows and Linux.", needs: "macOS 14 or later, Apple silicon.", - unsigned: "Not notarized. If macOS blocks the app, open System Settings › Privacy & Security and click Open Anyway.", get: "Download", count: "{n} downloads so far.", source: "Source", @@ -53,7 +52,6 @@ const zh: typeof en = { download: "下载", platforms: "macOS、Windows 与 Linux 版已发布。", needs: "macOS 14 或更新,Apple 芯片。", - unsigned: "未经公证。macOS 拦下应用时,打开“系统设置 › 隐私与安全性”,点“仍要打开”。", get: "前往下载", count: "已下载 {n} 次。", source: "源码", diff --git a/site/worker/index.ts b/site/worker/index.ts index 081630d..4c8fbc7 100644 --- a/site/worker/index.ts +++ b/site/worker/index.ts @@ -1,34 +1,38 @@ // Answers /release.json with the newest release and downloads. -const REPO = "https://git.zacharyzhang.com/api/v1/repos/ZacharyZhang-NY/ElyOffice"; -const PAGE = 50; +const REPO = "https://api.github.com/repos/ELY-Open-Source/ElyOffice"; +const PAGE = 100; type Cached = RequestInit & { cf: { cacheTtl: number; cacheEverything: boolean } }; type Asset = { name: string; download_count: number }; type Release = { tag_name: string; assets: Asset[] }; +// A token lifts GitHub's limit for the shared egress addresses: `wrangler secret put GITHUB_TOKEN`. +type Env = { GITHUB_TOKEN?: string }; -const init: Cached = { - headers: { accept: "application/json", "user-agent": "ely-office-site" }, - cf: { cacheTtl: 300, cacheEverything: true }, -}; - -async function read(path: string): Promise { +async function read(env: Env, path: string): Promise { + const headers: Record = { + accept: "application/vnd.github+json", + "user-agent": "ely-office-site", + "x-github-api-version": "2022-11-28", + }; + if (env.GITHUB_TOKEN) headers.authorization = `Bearer ${env.GITHUB_TOKEN}`; + const init: Cached = { headers, cf: { cacheTtl: 300, cacheEverything: true } }; const res = await fetch(`${REPO}${path}`, init); - if (!res.ok) throw new Error(`Gitea answered ${res.status} for ${path}`); + if (!res.ok) throw new Error(`GitHub answered ${res.status} for ${path}`); return (await res.json()) as T; } /** The newest release's version, without the v. */ -async function version(): Promise { - const { tag_name: tag } = await read("/releases/latest"); - if (!/^v\d+\.\d+\.\d+$/.test(tag)) throw new Error(`Gitea's latest tag is ${JSON.stringify(tag)}`); +async function version(env: Env): Promise { + const { tag_name: tag } = await read(env, "/releases/latest"); + if (!/^v\d+\.\d+\.\d+$/.test(tag)) throw new Error(`GitHub's latest tag is ${JSON.stringify(tag)}`); return tag.slice(1); } /** Downloads of every release; update manifests are checks, not downloads. */ -async function downloads(): Promise { +async function downloads(env: Env): Promise { let total = 0; for (let page = 1; ; page++) { - const releases = await read(`/releases?limit=${PAGE}&page=${page}`); + const releases = await read(env, `/releases?per_page=${PAGE}&page=${page}`); for (const { assets } of releases) { for (const a of assets) { if (!Number.isInteger(a.download_count)) throw new Error(`${a.name} has no download count`); @@ -40,9 +44,9 @@ async function downloads(): Promise { } export default { - async fetch(): Promise { + async fetch(_request: Request, env: Env): Promise { try { - const [v, n] = await Promise.all([version(), downloads()]); + const [v, n] = await Promise.all([version(env), downloads(env)]); return Response.json({ version: v, downloads: n }, { headers: { "cache-control": "public, max-age=300" } }); } catch (e) { return new Response(String(e), { status: 502 });