Skip to content

Commit 600be41

Browse files
Merge branch 'main' into fix/restore-cross-platform-builds
2 parents 441d3d8 + 434963f commit 600be41

5 files changed

Lines changed: 336 additions & 172 deletions

File tree

.github/workflows/docs.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ on:
1111
paths:
1212
- "website/**"
1313
- ".github/workflows/docs.yml"
14+
# The /download page resolves the current release's assets at build time so it
15+
# can link each platform to its actual file. Without this trigger that data
16+
# would freeze at whatever the last website/** change saw, and the page would
17+
# keep serving the previous version's binaries after every release.
18+
# Pre-releases are skipped: /releases/latest ignores them, so the built output
19+
# would be byte-identical.
20+
release:
21+
types: [published]
1422
workflow_dispatch:
1523

1624
# Cancel in-flight runs on the same ref so fast follow-up pushes
@@ -26,6 +34,9 @@ jobs:
2634
build:
2735
name: Build site
2836
runs-on: ubuntu-latest
37+
# A pre-release does not change what /releases/latest resolves to, so
38+
# rebuilding for one would burn a run to produce identical output.
39+
if: github.event_name != 'release' || github.event.release.prerelease == false
2940
steps:
3041
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
3142
with:
@@ -53,7 +64,9 @@ jobs:
5364
name: Deploy to GitHub Pages
5465
runs-on: ubuntu-latest
5566
needs: build
56-
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
67+
if: >-
68+
(github.event_name == 'push' && github.ref == 'refs/heads/main')
69+
|| github.event_name == 'release'
5770
environment:
5871
name: github-pages
5972
url: ${{ steps.deployment.outputs.page_url }}

website/docusaurus.config.ts

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import type * as Preset from "@docusaurus/preset-classic";
22
import type { Config } from "@docusaurus/types";
33
import { themes as prismThemes } from "prism-react-renderer";
44

5+
import type { LatestRelease } from "./src/lib/release";
6+
57
const SITE_URL = "https://getopenscreen.com";
6-
const REPO_URL = "https://github.com/getopenscreen/openscreen";
8+
const REPO_SLUG = "getopenscreen/openscreen";
9+
const REPO_URL = `https://github.com/${REPO_SLUG}`;
710
const UPSTREAM_REPO_URL = "https://github.com/siddharthvaddem/openscreen";
811
const DISCORD_URL = "https://discord.gg/VvT6Vtnyh";
912

@@ -64,8 +67,73 @@ async function fetchStarCount(): Promise<number | null> {
6467
}
6568
}
6669

70+
const MONTHS = [
71+
"January",
72+
"February",
73+
"March",
74+
"April",
75+
"May",
76+
"June",
77+
"July",
78+
"August",
79+
"September",
80+
"October",
81+
"November",
82+
"December",
83+
];
84+
85+
/**
86+
* The published assets for the current stable release, resolved at build time
87+
* so /download can link each platform to its actual file instead of bouncing
88+
* everyone through the releases list.
89+
*
90+
* This is only safe because .github/workflows/docs.yml also rebuilds on
91+
* `release: published`. Without that trigger the data would go stale silently —
92+
* the workflow otherwise only fires on website/** changes, so shipping a new
93+
* version would leave this page advertising the previous one indefinitely.
94+
*
95+
* Returns null on any failure (the API is called unauthenticated, so a
96+
* rate-limited runner is a real possibility); the page falls back to
97+
* /releases/latest links, which are always correct.
98+
*/
99+
async function fetchLatestRelease(): Promise<LatestRelease> {
100+
try {
101+
const res = await fetch(`https://api.github.com/repos/${REPO_SLUG}/releases/latest`, {
102+
headers: { Accept: "application/vnd.github+json" },
103+
signal: AbortSignal.timeout(5000),
104+
});
105+
if (!res.ok) return null;
106+
const data = (await res.json()) as {
107+
tag_name?: unknown;
108+
published_at?: unknown;
109+
assets?: unknown;
110+
};
111+
if (typeof data.tag_name !== "string" || !Array.isArray(data.assets)) return null;
112+
113+
const assets = data.assets.flatMap((raw) => {
114+
const a = raw as { name?: unknown; browser_download_url?: unknown; size?: unknown };
115+
if (typeof a.name !== "string" || typeof a.browser_download_url !== "string") return [];
116+
return [{ name: a.name, url: a.browser_download_url, size: Number(a.size) || 0 }];
117+
});
118+
if (assets.length === 0) return null;
119+
120+
// Formatted here rather than in the component: toLocaleDateString would
121+
// resolve against the visitor's locale and time zone on hydration and
122+
// mismatch the server-rendered string.
123+
let published = "";
124+
if (typeof data.published_at === "string") {
125+
const [y, m, d] = data.published_at.slice(0, 10).split("-");
126+
if (y && m && d) published = `${Number(d)} ${MONTHS[Number(m) - 1]} ${y}`;
127+
}
128+
129+
return { tag: data.tag_name, published, assets };
130+
} catch {
131+
return null;
132+
}
133+
}
134+
67135
export default async function createConfig(): Promise<Config> {
68-
const starCount = await fetchStarCount();
136+
const [starCount, latestRelease] = await Promise.all([fetchStarCount(), fetchLatestRelease()]);
69137
const starBadge =
70138
starCount !== null
71139
? `<span class="navbar-github-stars">${STAR_SVG}${formatStarCount(starCount)}</span>`
@@ -93,6 +161,10 @@ export default async function createConfig(): Promise<Config> {
93161
organizationName: "getopenscreen",
94162
projectName: "openscreen",
95163

164+
// Read back by src/pages/download.tsx. Serialized into the client bundle,
165+
// so it stays plain JSON.
166+
customFields: { latestRelease },
167+
96168
onBrokenLinks: "throw",
97169
onBrokenAnchors: "throw",
98170

website/src/lib/release.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Shape of the build-time GitHub release lookup, shared between the config that
3+
* fetches it (docusaurus.config.ts) and the page that renders it
4+
* (src/pages/download.tsx), which reads it back off siteConfig.customFields.
5+
*/
6+
7+
export type ReleaseAsset = {
8+
name: string;
9+
url: string;
10+
size: number;
11+
};
12+
13+
/** null whenever the build-time lookup failed; callers must handle it. */
14+
export type LatestRelease = {
15+
tag: string;
16+
/** Pre-formatted at build time, e.g. "19 July 2026". Empty if unknown. */
17+
published: string;
18+
assets: ReleaseAsset[];
19+
} | null;
20+
21+
/**
22+
* Asset filenames carry the version (Openscreen-Mac-arm64-1.7.0.dmg), so these
23+
* match on the stable parts only — platform, arch, and extension — and keep
24+
* working across releases without a config change.
25+
*/
26+
export const ASSET_PATTERNS = {
27+
macArm: /Mac.*arm64.*\.dmg$/i,
28+
macIntel: /Mac.*x64.*\.dmg$/i,
29+
windows: /\.exe$/i,
30+
deb: /\.deb$/i,
31+
pacman: /\.pacman$/i,
32+
appImage: /\.AppImage$/i,
33+
} as const;
34+
35+
export type AssetKind = keyof typeof ASSET_PATTERNS;
36+
37+
export function findAsset(release: LatestRelease, kind: AssetKind): ReleaseAsset | null {
38+
return release?.assets.find((a) => ASSET_PATTERNS[kind].test(a.name)) ?? null;
39+
}
40+
41+
export function formatSize(bytes: number): string {
42+
if (!bytes) return "";
43+
return `${Math.round(bytes / 1048576)} MB`;
44+
}

0 commit comments

Comments
 (0)