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 <noreply@anthropic.com>
This commit is contained in:
6 files changed
+29
-44
No files matched your search
+3
-20
@@ -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();
|
||||
}
|
||||
});
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { said, type Key } from "./text.ts";
|
||||
|
||||
const ICONS = {
|
||||
download: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v11M7 10l5 5 5-5M5 20h14"/></svg>`,
|
||||
gitea: `<svg viewBox="4 140 632 392" fill="currentColor"><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z"/></svg>`,
|
||||
github: `<svg viewBox="0 0 16 16" fill="currentColor"><path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"/></svg>`,
|
||||
};
|
||||
|
||||
/** A link with `icon` before its keyed text. */
|
||||
|
||||
+3
-3
@@ -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<HTMLElement>("#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";
|
||||
|
||||
+2
-2
@@ -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 };
|
||||
|
||||
|
||||
@@ -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: "源码",
|
||||
|
||||
+20
-16
@@ -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<T>(path: string): Promise<T> {
|
||||
async function read<T>(env: Env, path: string): Promise<T> {
|
||||
const headers: Record<string, string> = {
|
||||
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<string> {
|
||||
const { tag_name: tag } = await read<Release>("/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<string> {
|
||||
const { tag_name: tag } = await read<Release>(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<number> {
|
||||
async function downloads(env: Env): Promise<number> {
|
||||
let total = 0;
|
||||
for (let page = 1; ; page++) {
|
||||
const releases = await read<Release[]>(`/releases?limit=${PAGE}&page=${page}`);
|
||||
const releases = await read<Release[]>(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<number> {
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(): Promise<Response> {
|
||||
async fetch(_request: Request, env: Env): Promise<Response> {
|
||||
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 });
|
||||
|
||||
Reference in new issue
Block a user