diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 1c7ecc8..cb3df4d 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -1,7 +1,7 @@ name: Website on: - # Wait for CI/packaging to settle so new downloads reach the same site build. + # Publish the website after main's checks complete. workflow_run: workflows: [CI] types: [completed] @@ -39,35 +39,7 @@ jobs: node-version-file: .node-version cache: npm - run: npm ci - - name: Read published downloads - env: - GH_TOKEN: ${{ github.token }} - PIIK_WEBSITE_RELEASE_DATA: ${{ runner.temp }}/website-release.json - run: | - node --input-type=module <<'JS' - import { execFileSync } from 'node:child_process'; - import { writeFileSync } from 'node:fs'; - const repository = 'TNTcraftHIM/Piik'; - const github = JSON.parse(execFileSync('gh', ['api', `repos/${repository}/releases/latest`], { encoding: 'utf8' })); - let mirror = null; - try { - const api = `https://gitee.com/api/v5/repos/${repository}`; - const read = async path => { - const response = await fetch(api + path, { signal: AbortSignal.timeout(20000) }); - if (!response.ok) throw new Error(`Mirror metadata: HTTP ${response.status}`); - return response.json(); - }; - const release = await read(`/releases/tags/${encodeURIComponent(github.tag_name)}`); - if (!Number.isSafeInteger(release.id) || release.id <= 0) throw new Error('Invalid mirror release ID'); - mirror = { ...release, assets: await read(`/releases/${release.id}/attach_files?per_page=100`) }; - } catch { - console.log('Mirror metadata unavailable; keeping the mirror release-page link.'); - } - writeFileSync(process.env.PIIK_WEBSITE_RELEASE_DATA, JSON.stringify({ github, mirror })); - JS - run: npm run build:website - env: - PIIK_WEBSITE_RELEASE_DATA: ${{ runner.temp }}/website-release.json - uses: actions/configure-pages@v5 - uses: actions/upload-pages-artifact@v4 with: diff --git a/docs/design/public-introduction.md b/docs/design/public-introduction.md index b71b261..ff9ac2b 100644 --- a/docs/design/public-introduction.md +++ b/docs/design/public-introduction.md @@ -133,12 +133,11 @@ are not automatically included in the website artifact. The [website operations guide](../operations/website.md) owns Pages publication, public destinations and the separate P2P-only demo. The primary action leads -to the platform download section. Each supported platform has a direct ZIP link -using GitHub's native latest-release asset route. Build verified Gitee links from -the matching release and source revision. Keep build-time versions out of -download captions. -Keep release notes separate; when a mirror package is unverified, label its -fallback as a release-page link. Verify destinations as part of publication. +to the platform download section. Each supported platform uses the native latest +download routes of GitHub and Gitee, as owned by the website operations guide. +Use “Download” / “下载” for the primary action and name Gitee on the alternative. +Keep archive formats and build-time versions out of button labels, and link +release notes separately. Verify destinations as part of publication. Demo, App and existing-site walkthroughs use real action names and explain the first sharing and viewing steps. Keep platform requirements and connection limitations beside the relevant steps. diff --git a/docs/operations/website.md b/docs/operations/website.md index ca8ec19..eeec260 100644 --- a/docs/operations/website.md +++ b/docs/operations/website.md @@ -73,18 +73,21 @@ film's real product components into an isolated demonstration frame; publishing ranges for seeking the soundtrack. Keep asset links relative so both a custom domain and GitHub's `/Piik/` project path work. -The Windows x64, macOS Apple silicon and Linux x64 ZIP cards use GitHub's native -[`releases/latest/download/asset-name.zip` links](https://docs.github.com/en/repositories/releasing-projects-on-github/linking-to-releases). -Fixed App filenames follow [versioning](../reference/versioning.md#release-and-recovery), -so these primary links also work in local builds and follow new releases without -a website rebuild. `PIIK_WEBSITE_RELEASE_DATA` points the build at temporary JSON -containing the published GitHub release and same-tag Gitee attachment metadata. -CI supplies this input; the builder validates source identity, uploaded ZIPs, -sizes and platform URLs. A missing/incomplete GitHub release fails deployment, -preserving the current site. Verified Gitee links target the matched release -and refresh after publication. Without matching metadata, Gitee retains its -labeled release-page link. Download captions omit build-time version snapshots; -the page makes no visitor API request. +The Windows x64, macOS Apple silicon and Linux x64 cards use fixed filenames and +each provider's native latest-attachment route: + +- GitHub: `releases/latest/download/asset-name.zip` + ([documentation](https://docs.github.com/en/repositories/releasing-projects-on-github/linking-to-releases)). +- Gitee: `releases/download/latest/asset-name.zip` + ([Windows download](https://gitee.com/TNTcraftHIM/Piik/releases/download/latest/piik-app-windows-amd64.zip)). + +Both links follow provider publication without a website rebuild or a release +API request from the build or visitor. Buttons say “Download” / “下载”; the +secondary link names Gitee. Keep versions and archive formats out of these labels. +[Versioning](../reference/versioning.md#release-sources) owns package verification +and completion of mirror publication. GitHub remains the primary source; the +Gitee download can temporarily lag while a mirror publication completes. +Verify the latest redirects and package checksums after releases. ## Publish @@ -97,11 +100,9 @@ the page makes no visitor API request. DNS using GitHub's [current domain instructions](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site). Actions publishing does not need a `CNAME` file in the source tree. 4. Accepted main pushes run CI first; its completion triggers **Website**, which - checks out current main and reads the latest published download metadata. - Waiting for CI includes its package publication and mirror attempt, so the - page picks up the resulting release. A CI failure leaves downloads pointing - to the latest successful public release. Manual **Website** dispatch from - `main` can refresh links after an operator publication or mirror retry. + checks out current main. Manual **Website** dispatch from `main` republishes + the site when needed. Download links resolve through each provider, so + publishing a package or retrying its mirror needs no link refresh. Only `build/site/` is uploaded; website work does not package App/Server. Verify HTTPS, both languages, relative assets and per-platform download links. diff --git a/scripts/build-website.mjs b/scripts/build-website.mjs index 3cc0fa9..22aa818 100644 --- a/scripts/build-website.mjs +++ b/scripts/build-website.mjs @@ -34,22 +34,8 @@ const welcomeLines = WELCOME_LINES.map(({ key }) => renderToStaticMarkup( createElement("span", { lang: "zh-CN" }, `“${locales.zh.copy[key]}”`), ), )); -let homepage = (await readFile(new URL("index.html", source), "utf8")) +const homepage = (await readFile(new URL("index.html", source), "utf8")) .replace(/

.*?<\/p>/, `

${welcomeLines[0]}

`); -if (process.env.PIIK_WEBSITE_RELEASE_DATA) { - const { websiteDownloads } = await tsImport("../site/downloads.ts", { parentURL: import.meta.url }); - const release = websiteDownloads(JSON.parse(await readFile(process.env.PIIK_WEBSITE_RELEASE_DATA, "utf8"))); - homepage = homepage.replace(/]*data-download="([^"]+)"[^>]*data-provider="([^"]+)"[^>]*)>([\s\S]*?)<\/a\s*>/g, - (original, attributes, target, provider, content) => { - const item = release.packages.find(item => item.target === target); - const href = provider === 'github' ? item?.url : item?.mirrorURL; - if (!href) return original; - if (provider === 'gitee') content = content - .replace(/[\s\S]*?<\/span\s*>/, 'Gitee alternative · ZIP') - .replace(/[\s\S]*?<\/span\s*>/, 'Gitee 备用 · ZIP'); - return `${content}`; - }); -} await writeFile(new URL("index.html", output), homepage.replace( '', ``, )); diff --git a/site/downloads.ts b/site/downloads.ts deleted file mode 100644 index ad28e9f..0000000 --- a/site/downloads.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Build-time download links. GitHub owns release identity; Gitee mirrors it. -import { parseMirrorReleaseMetadata, parseReleaseMetadata } from "../src/client/lib/release-update"; - -const targets = ["windows-amd64", "darwin-arm64", "linux-amd64"] as const; - -function assets(value: unknown): Record[] { - const list = value && typeof value === "object" ? (value as Record).assets : null; - return Array.isArray(list) - ? list.filter((asset): asset is Record => asset !== null && typeof asset === "object" && !Array.isArray(asset)) - : []; -} - -export function websiteDownloads(data: { github: unknown; mirror?: unknown }) { - const release = parseReleaseMetadata(data.github); - if (!release?.revision) throw new Error("Website downloads require a published GitHub release with a full source SHA"); - const primaryAssets = assets(data.github); - const mirror = parseMirrorReleaseMetadata(data.mirror); - const mirrorAssets = mirror?.version === release.version && mirror.revision === release.revision - ? assets(data.mirror) : []; - return { - packages: targets.map(target => { - const name = `piik-app-${target}.zip`; - const publishedURL = `https://github.com/TNTcraftHIM/Piik/releases/download/${release.version}/${name}`; - const matches = primaryAssets.filter(asset => asset.name === name); - const asset = matches[0]; - if (matches.length !== 1 || asset?.state !== "uploaded" || asset.browser_download_url !== publishedURL || - !Number.isSafeInteger(asset.size) || Number(asset.size) <= 0) { - throw new Error(`Website download is missing or invalid: ${target}`); - } - const mirrorURL = `https://gitee.com/TNTcraftHIM/Piik/releases/download/${release.version}/${name}`; - const mirrored = mirrorAssets.filter(candidate => candidate.name === name); - return { target, url: `https://github.com/TNTcraftHIM/Piik/releases/latest/download/${name}`, - mirrorURL: mirrored.length === 1 && mirrored[0]!.size === asset.size && - mirrored[0]!.browser_download_url === mirrorURL ? mirrorURL : null }; - }), - }; -} diff --git a/site/index.html b/site/index.html index 351eaec..aa6543d 100644 --- a/site/index.html +++ b/site/index.html @@ -721,8 +721,8 @@

- Download the ZIP for your system, then extract it. - 选择对应系统,下载 ZIP 后解压。 + Choose your system, then download and extract. + 选择对应系统,下载后解压。 Release notes版本说明

@@ -741,10 +741,10 @@ data-download="windows-amd64" data-provider="github" href="https://github.com/TNTcraftHIM/Piik/releases/latest/download/piik-app-windows-amd64.zip" aria-describedby="download-status" - >Download ZIP · GitHub下载 ZIP · GitHubView Gitee mirror查看 Gitee 镜像Download下载Download via GiteeGitee 备用下载
diff --git a/tests/website-downloads.test.ts b/tests/website-downloads.test.ts deleted file mode 100644 index 5ef36b7..0000000 --- a/tests/website-downloads.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { readFileSync } from "node:fs"; -import { websiteDownloads } from "../site/downloads"; - -const revision = "a".repeat(40); -const version = "v1.2.0"; -const github = { - tag_name: version, target_commitish: revision, draft: false, prerelease: false, - html_url: `https://github.com/TNTcraftHIM/Piik/releases/tag/${version}`, - assets: ["windows-amd64", "darwin-arm64", "linux-amd64"].map(target => { - const name = `piik-app-${target}.zip`; - return { name, state: "uploaded", size: 1024, - browser_download_url: `https://github.com/TNTcraftHIM/Piik/releases/download/${version}/${name}` }; - }), -}; -const mirror = { - tag_name: version, prerelease: false, body: ``, - assets: github.assets.map(asset => ({ ...asset, - browser_download_url: asset.browser_download_url.replace("github.com", "gitee.com") })), -}; - -describe("website package downloads", () => { - it("links each uploaded platform package and rejects an incomplete or mismatched GitHub release", () => { - const result = websiteDownloads({ github, mirror }); - expect(result.packages.map(item => item.url)).toEqual(github.assets.map(asset => - `https://github.com/TNTcraftHIM/Piik/releases/latest/download/${asset.name}`)); - expect(result.packages.map(item => item.mirrorURL)).toEqual(mirror.assets.map(asset => asset.browser_download_url)); - for (const changed of [ - { target_commitish: "main" }, { prerelease: true }, { draft: true }, - { assets: github.assets.slice(1) }, { assets: [...github.assets, github.assets[0]] }, - ...[ - { state: "new" }, { size: 0 }, - { name: github.assets[0]!.name.replace(".zip", "-aaaaaaa.zip") }, - { name: github.assets[0]!.name.replace(".zip", ".tar.gz") }, - { browser_download_url: github.assets[0]!.browser_download_url.replace("github.com", "evil.example") }, - { browser_download_url: github.assets[0]!.browser_download_url.replace(version, "v1.1.0") }, - ].map(change => ({ assets: [{ ...github.assets[0], ...change }, ...github.assets.slice(1)] })), - ]) expect(() => websiteDownloads({ github: { ...github, ...changed } })).toThrow(); - }); - - it("keeps GitHub URLs valid across releases and without build metadata", () => { - const nextVersion = "v1.2.1"; - const next = websiteDownloads({ github: { ...github, tag_name: nextVersion, - target_commitish: "b".repeat(40), html_url: github.html_url.replace(version, nextVersion), - assets: github.assets.map(asset => ({ ...asset, - browser_download_url: asset.browser_download_url.replace(version, nextVersion) })), - }, mirror }); - const current = websiteDownloads({ github, mirror }); - expect(next.packages.map(item => item.url)).toEqual(current.packages.map(item => item.url)); - expect(next.packages.every(item => item.mirrorURL === null)).toBe(true); - - const html = readFileSync(new URL("../site/index.html", import.meta.url), "utf8"); - for (const item of current.packages) { - const link = html.match(new RegExp(`]*data-download="${item.target}"[^>]*data-provider="github"[^>]*>[\\s\\S]*?<\\/a\\s*>`))?.[0]; - expect(link).toContain(`href="${item.url}"`); - expect(link).toContain("Download ZIP"); - expect(link).toContain("下载 ZIP"); - } - expect(html.match(/id="download-status">[\s\S]*?<\/p>/)?.[0]).not.toMatch(/v\d+\.\d+\.\d+/); - }); - - it("keeps mirror navigation as the fallback until the same release and attachment are confirmed", () => { - for (const changed of [undefined, null, { ...mirror, prerelease: true }, - { ...mirror, body: `` }, - { ...mirror, tag_name: "v1.1.0" }, { ...mirror, assets: [] }]) { - expect(websiteDownloads({ github, mirror: changed }).packages.every(item => item.mirrorURL === null)).toBe(true); - } - for (const change of [{ size: 2048 }, { browser_download_url: "https://evil.example/package.zip" }]) { - const result = websiteDownloads({ github, - mirror: { ...mirror, assets: [{ ...mirror.assets[0], ...change }, ...mirror.assets.slice(1)] } }); - expect(result.packages[0]!.mirrorURL).toBeNull(); - expect(result.packages[1]!.mirrorURL).toBe(mirror.assets[1]!.browser_download_url); - } - }); -});