Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add stable releases #308

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@
"react": "^18",
"react-device-detect": "^2.2.3",
"react-dom": "^18",
"recharts": "^2.12.7"
"recharts": "^2.12.7",
"semver": "^7.6.3"
},
"devDependencies": {
"@types/d3": "^7.4.3",
"@types/jsdom": "^21.1.7",
"@types/node": "^22",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/semver": "^7.5.8",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"d3": "^7.9.0",
Expand Down
5 changes: 5 additions & 0 deletions src/app/downloads/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,21 @@ import { SummaryStatistics } from "@/app/compatibility/avm2/report_utils";

export const repository = { owner: "ruffle-rs", repo: "ruffle" };

export const maxMinor = 3;
export const maxMajor = 1;
export const maxNightlies = 5;

export const githubReleasesUrl = `https://github.com/${repository.owner}/${repository.repo}/releases`;
export const githubStableReleasesUrl = `${githubReleasesUrl}?q=prerelease:false`;
export const githubNightlyReleasesUrl = `${githubReleasesUrl}?q=prerelease:true`;

export interface GithubRelease {
id: number;
name: string;
prerelease: boolean;
downloads: ReleaseDownloads;
url: string;
tag: string;
avm2_report_asset_id?: number;
}

Expand Down
155 changes: 123 additions & 32 deletions src/app/downloads/github.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,24 @@ import {
DownloadKey,
FilenamePatterns,
GithubRelease,
maxMajor,
maxMinor,
maxNightlies,
ReleaseDownloads,
repository,
} from "@/app/downloads/config";
import { Octokit } from "octokit";
import { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
import { parse } from "node-html-parser";
import semver from "semver/preload";
import { components } from "@octokit/openapi-types";

const octokit = new Octokit({ authStrategy: createGithubAuth });

const requestCache = {
// Set cache to 30 min to prevent rate limiting during development
request: { next: { revalidate: 1800 } },
};

function createGithubAuth() {
if (process.env.GITHUB_TOKEN) {
Expand All @@ -28,37 +39,65 @@ function throwBuildError() {
throw new Error("Build failed");
}

export async function getLatestReleases(): Promise<GithubRelease[]> {
const octokit = new Octokit({ authStrategy: createGithubAuth });
function mapRelease(release: components["schemas"]["release"]): GithubRelease {
const downloads: ReleaseDownloads = {};
let avm2_report_asset_id: number | undefined = undefined;
for (const asset of release.assets) {
if (asset.name === "avm2_report.json") {
avm2_report_asset_id = asset.id;
}
for (const [key, pattern] of Object.entries(FilenamePatterns)) {
if (asset.name.indexOf(pattern) > -1) {
downloads[key as DownloadKey] = asset.browser_download_url;
}
}
}

return {
id: release.id,
name: release.name || release.tag_name,
prerelease: release.prerelease,
url: release.html_url,
tag: release.tag_name,
downloads,
avm2_report_asset_id,
};
}

export async function getLatestRelease(): Promise<GithubRelease> {
try {
const response = await octokit.rest.repos.getLatestRelease({
...requestCache,
...repository,
});
return mapRelease(response.data);
} catch {
// There's no stable release, get the latest nightly.
}

const releases = await octokit.rest.repos.listReleases({
per_page: 1,
...requestCache,
...repository,
});
return mapRelease(releases.data[0]);
}

export async function getLatestNightlyReleases(): Promise<GithubRelease[]> {
try {
const releases = await octokit.rest.repos.listReleases({
per_page: maxNightlies + 2, // more than we need to account for a possible draft release + possible full release
request: { next: { revalidate: 1800 } },
// We have to take into account possible stable releases here
per_page: maxNightlies + 4,
...requestCache,
...repository,
});
const result = [];
let avm2_report_asset_id: number | undefined = undefined;
for (const release of releases.data) {
const downloads: ReleaseDownloads = {};
for (const asset of release.assets) {
if (asset.name === "avm2_report.json") {
avm2_report_asset_id = asset.id;
}
for (const [key, pattern] of Object.entries(FilenamePatterns)) {
if (asset.name.indexOf(pattern) > -1) {
downloads[key as DownloadKey] = asset.browser_download_url;
}
}
if (!release.prerelease) {
// Filter out stable releases
continue;
}

result.push({
id: release.id,
name: release.name || release.tag_name,
prerelease: release.prerelease,
url: release.html_url,
downloads,
avm2_report_asset_id,
});
result.push(mapRelease(release));
}
return result;
} catch (error) {
Expand All @@ -67,25 +106,79 @@ export async function getLatestReleases(): Promise<GithubRelease[]> {
}
}

export async function getLatestStableReleases(): Promise<GithubRelease[]> {
let newestMajor = null;

// Map representing releases from the current major version:
// `major.minor` -> `major.minor.patch`
// We want to ignore older patches and show last X minor versions.
const currentMajorReleases = new Map();

// Map representing releases from older major versions.
// `major` -> `major.minor.patch`
// We ignore here minor and patch versions, and
// gather the newest release per each major.
const olderMajors = new Map();

for (
let page = 1;
currentMajorReleases.size < maxMinor || olderMajors.size < maxMajor - 1;
++page
) {
const request = await octokit.rest.repos.listReleases({
// 100 per page disables cache as the result is >2MB
per_page: 80,
page: page,
...requestCache,
...repository,
});
if (request.status != 200 || request.data.length == 0) {
break;
}
for (const data of request.data) {
if (data.prerelease) {
continue;
}
const release = mapRelease(data);
const version = release.tag.replace(/^v/, "");
const major = semver.major(version);
const majorMinor = `${major}.${semver.minor(version)}`;
if (!newestMajor) {
newestMajor = major;
}
if (major === newestMajor) {
if (!currentMajorReleases.has(majorMinor)) {
currentMajorReleases.set(majorMinor, release);
}
} else {
if (!olderMajors.has(major)) {
olderMajors.set(major, release);
}
}
}
}

return Array.from(currentMajorReleases.values())
.slice(0, maxMinor)
.concat(Array.from(olderMajors.values()).slice(0, maxMajor - 1));
}

export async function getWeeklyContributions(): Promise<
RestEndpointMethodTypes["repos"]["getCommitActivityStats"]["response"]
> {
const octokit = new Octokit({ authStrategy: createGithubAuth });
return octokit.rest.repos.getCommitActivityStats(repository);
}
export async function fetchReport(): Promise<AVM2Report | undefined> {
const releases = await getLatestReleases();
const releases = await getLatestNightlyReleases();
const latest = releases.find(
(release) => release.avm2_report_asset_id !== undefined,
);
if (!latest?.avm2_report_asset_id) {
throwBuildError();
return;
}
const octokit = new Octokit({ authStrategy: createGithubAuth });
const asset = await octokit.rest.repos.getReleaseAsset({
owner: repository.owner,
repo: repository.repo,
...repository,
asset_id: latest.avm2_report_asset_id,
headers: {
accept: "application/octet-stream",
Expand All @@ -100,10 +193,8 @@ export async function fetchReport(): Promise<AVM2Report | undefined> {
}

export async function getAVM1Progress(): Promise<number> {
const octokit = new Octokit({ authStrategy: createGithubAuth });
const issues = await octokit.rest.issues.listForRepo({
owner: repository.owner,
repo: repository.repo,
...repository,
labels: "avm1-tracking",
state: "all",
per_page: 65,
Expand Down
21 changes: 13 additions & 8 deletions src/app/downloads/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@ import {
import classes from "./downloads.module.css";
import React from "react";
import { ExtensionList } from "@/app/downloads/extensions";
import { NightlyList } from "@/app/downloads/nightlies";
import { ReleaseList } from "@/app/downloads/releases";
import Link from "next/link";
import {
desktopLinks,
GithubRelease,
githubReleasesUrl,
maxNightlies,
} from "@/app/downloads/config";
import { getLatestReleases } from "@/app/downloads/github";
import {
getLatestNightlyReleases,
getLatestStableReleases,
} from "@/app/downloads/github";

function WebDownload({ latest }: { latest: GithubRelease | null }) {
return (
Expand Down Expand Up @@ -94,19 +97,21 @@ function DesktopDownload({ latest }: { latest: GithubRelease | null }) {
}

export default async function Page() {
const releases = await getLatestReleases();
const latest = releases.length > 0 ? releases[0] : null;
const nightlies = releases
const stableReleases = await getLatestStableReleases();
const nightlies = (await getLatestNightlyReleases())
.filter((release) => release.prerelease)
.slice(0, maxNightlies);
const latestStable =
stableReleases.length > 0 ? stableReleases[0] : nightlies[0];
return (
<Container size="xl" className={classes.container}>
<Stack gap="xl">
<ExtensionList />
<WebDownload latest={latest} />
<DesktopDownload latest={latest} />
<WebDownload latest={latestStable} />
<DesktopDownload latest={latestStable} />

<NightlyList nightlies={nightlies} />
<ReleaseList releases={stableReleases} nightly={false} />
<ReleaseList releases={nightlies} nightly={true} />
</Stack>
</Container>
);
Expand Down
Loading