release build and other CI

This commit is contained in:
2026-06-05 16:58:11 +09:30
parent b535e87023
commit 3f1910f610
18 changed files with 806 additions and 29 deletions
+4
View File
@@ -0,0 +1,4 @@
declare const __ENABLE_GH_RELEASE_UPDATE_CHECK__: boolean;
declare const __GH_RELEASE_REPO__: string;
declare const __UPDATE_CHANNEL__: "stable" | "nightly";
declare const __BUILD_LABEL__: string;
+44 -1
View File
@@ -19,6 +19,12 @@
import CloudPanel from "../components/CloudPanel.svelte";
import DisclaimerModal from "../components/DisclaimerModal.svelte";
import { settingsPopup } from "../hooks/SettingsPopup";
import {
checkGithubReleaseUpdate,
dismissNightlyUpdate,
isGhReleaseUpdateCheckEnabled,
type GhReleaseUpdateInfo,
} from "@/utils/githubReleaseUpdate";
let devModeSequence = "";
let settingsActiveTab = $state(0);
@@ -26,6 +32,18 @@
let disclaimerCallbacks = $state<{ onConfirm: () => void, onCancel: () => void } | null>(null);
let disclaimerTitle = $state("Confirm");
let disclaimerMessage = $state("");
const ghReleaseUpdateEnabled = isGhReleaseUpdateCheckEnabled();
let ghReleaseUpdate = $state<GhReleaseUpdateInfo | null>(null);
const openGhRelease = () => {
const url = ghReleaseUpdate?.url
?? "https://github.com/BetterSEQTA/BetterSEQTA-Plus/releases";
if (ghReleaseUpdate?.available) {
dismissNightlyUpdate();
}
window.open(url, "_blank");
closeExtensionPopup();
};
const handleDevModeToggle = () => {
const handleKeyDown = (event: KeyboardEvent) => {
@@ -98,6 +116,12 @@
if (standalone) {
StandaloneStore.setStandalone(true);
}
if (ghReleaseUpdateEnabled) {
void checkGithubReleaseUpdate().then((info) => {
ghReleaseUpdate = info;
});
}
});
</script>
@@ -134,7 +158,25 @@
/>
{#if !standalone}
<div class="flex absolute top-1 right-1 gap-1 items-center">
<div class="flex absolute top-1 right-1 gap-1 items-start">
{#if ghReleaseUpdateEnabled}
<div class="flex flex-col items-end gap-0.5 max-w-[9rem] mr-0.5">
{#if ghReleaseUpdate?.available}
<button
type="button"
onclick={openGhRelease}
class="px-1.5 py-0.5 text-[10px] font-semibold leading-tight text-white rounded-full bg-amber-500 hover:bg-amber-600 dark:bg-amber-600 dark:hover:bg-amber-500"
title="Open GitHub release"
>
Update available — {ghReleaseUpdate.label}
</button>
{/if}
<p class="text-[9px] leading-tight text-right text-zinc-500 dark:text-zinc-400">
GitHub release build — do not upload to extension stores.
</p>
</div>
{/if}
<div class="flex gap-1 items-center">
<button
onclick={openAbout}
class="flex justify-center items-center w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700"
@@ -156,6 +198,7 @@
>
{"\uecba"}
</button>
</div>
<!-- <button
onclick={openMinecraftServer}
@@ -585,6 +585,21 @@
{/if}
</div>
</div>
<div class="flex flex-col gap-2 px-4 py-3">
<div>
<h2 class="text-sm font-bold">GitHub latest version override</h2>
<p class="text-xs">Pretend a newer GitHub release exists to test the update badge. Only applies when dev mode is on.</p>
</div>
<input
type="text"
placeholder="e.g. 9.9.9"
value={$settingsState.devGhReleaseVersionOverride ?? ""}
oninput={(e) => {
settingsState.devGhReleaseVersionOverride = e.currentTarget.value;
}}
class="px-2 py-1 text-xs rounded border bg-white dark:bg-zinc-800 border-zinc-300 dark:border-zinc-700 text-zinc-900 dark:text-zinc-100"
/>
</div>
</div>
{/if}
</div>
+4
View File
@@ -49,6 +49,10 @@ export interface SettingsState {
animations: boolean;
defaultPage: string;
devMode?: boolean;
/** Dev-only: pretend this is the latest GitHub release version for update badge testing. */
devGhReleaseVersionOverride?: string;
/** ISO timestamp of the last acknowledged nightly release publish time. */
lastSeenNightlyPublishedAt?: string;
originalDarkMode?: boolean;
newsSource?: string;
mockNotices?: boolean;
+213
View File
@@ -0,0 +1,213 @@
import semver from "semver";
import browser from "webextension-polyfill";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
const CHECK_THROTTLE_MS = 6 * 60 * 60 * 1000;
const LAST_CHECK_KEY = "bsplus_lastGhReleaseCheck";
const NIGHTLY_TAG = "nightly";
let cachedResult: GhReleaseUpdateInfo | null = null;
export interface GhReleaseUpdateInfo {
available: boolean;
label: string;
url: string;
}
function isUpdateCheckEnabled(): boolean {
return typeof __ENABLE_GH_RELEASE_UPDATE_CHECK__ !== "undefined"
&& __ENABLE_GH_RELEASE_UPDATE_CHECK__;
}
function getRepoSlug(): string {
return typeof __GH_RELEASE_REPO__ !== "undefined"
? __GH_RELEASE_REPO__
: "BetterSEQTA/BetterSEQTA-Plus";
}
function getUpdateChannel(): "stable" | "nightly" {
return typeof __UPDATE_CHANNEL__ !== "undefined"
? __UPDATE_CHANNEL__
: "stable";
}
function getBuildLabel(): string {
return typeof __BUILD_LABEL__ !== "undefined" ? __BUILD_LABEL__ : "";
}
function getCurrentVersion(): string {
return browser.runtime.getManifest().version;
}
function releasesBaseUrl(): string {
return `https://github.com/${getRepoSlug()}/releases`;
}
function shouldThrottleCheck(): boolean {
try {
const last = localStorage.getItem(LAST_CHECK_KEY);
if (!last) return false;
return Date.now() - Number(last) < CHECK_THROTTLE_MS;
} catch {
return false;
}
}
function markChecked(): void {
try {
localStorage.setItem(LAST_CHECK_KEY, String(Date.now()));
} catch {
/* ignore */
}
}
function getDevOverrideVersion(): string | null {
if (!settingsState.devMode) return null;
const override = settingsState.devGhReleaseVersionOverride?.trim();
return override || null;
}
function compareWithOverride(current: string): GhReleaseUpdateInfo | null {
const override = getDevOverrideVersion();
if (!override) return null;
const currentCoerced = semver.coerce(current);
const overrideCoerced = semver.coerce(override);
if (!currentCoerced || !overrideCoerced) return null;
if (semver.gt(overrideCoerced, currentCoerced)) {
return {
available: true,
label: override,
url: releasesBaseUrl(),
};
}
return { available: false, label: "", url: releasesBaseUrl() };
}
interface GhRelease {
tag_name: string;
published_at: string;
prerelease: boolean;
}
async function fetchJson<T>(url: string): Promise<T | null> {
try {
const response = await fetch(url, {
headers: { Accept: "application/vnd.github+json" },
});
if (!response.ok) return null;
return (await response.json()) as T;
} catch {
return null;
}
}
function isStableSemverTag(tag: string): boolean {
if (tag === NIGHTLY_TAG) return false;
return semver.valid(semver.coerce(tag)) !== null;
}
async function checkStableUpdate(current: string): Promise<GhReleaseUpdateInfo> {
const url = releasesBaseUrl();
const releases = await fetchJson<GhRelease[]>(
`https://api.github.com/repos/${getRepoSlug()}/releases`,
);
if (!releases?.length) {
return { available: false, label: "", url };
}
let latestTag: string | null = null;
let latestVersion: semver.SemVer | null = null;
for (const release of releases) {
const tag = release.tag_name;
if (!isStableSemverTag(tag)) continue;
const coerced = semver.coerce(tag);
if (!coerced) continue;
if (!latestVersion || semver.gt(coerced, latestVersion)) {
latestVersion = coerced;
latestTag = tag;
}
}
const currentCoerced = semver.coerce(current);
if (!latestTag || !latestVersion || !currentCoerced) {
return { available: false, label: "", url };
}
if (semver.gt(latestVersion, currentCoerced)) {
return { available: true, label: latestTag, url: `${url}/tag/${latestTag}` };
}
return { available: false, label: "", url };
}
async function checkNightlyUpdate(): Promise<GhReleaseUpdateInfo> {
const url = `${releasesBaseUrl()}/tag/${NIGHTLY_TAG}`;
const release = await fetchJson<GhRelease>(
`https://api.github.com/repos/${getRepoSlug()}/releases/tags/${NIGHTLY_TAG}`,
);
if (!release?.published_at) {
return { available: false, label: "", url };
}
const lastSeen = settingsState.lastSeenNightlyPublishedAt;
const buildLabel = getBuildLabel();
const label = buildLabel ? `nightly #${buildLabel}` : "nightly";
if (!lastSeen) {
settingsState.lastSeenNightlyPublishedAt = release.published_at;
return { available: false, label: "", url };
}
if (new Date(release.published_at) > new Date(lastSeen)) {
return { available: true, label, url };
}
return { available: false, label: "", url };
}
export function isGhReleaseUpdateCheckEnabled(): boolean {
return isUpdateCheckEnabled();
}
export async function checkGithubReleaseUpdate(): Promise<GhReleaseUpdateInfo> {
const fallback = { available: false, label: "", url: releasesBaseUrl() };
if (!isUpdateCheckEnabled()) return fallback;
const current = getCurrentVersion();
const overrideResult = compareWithOverride(current);
if (overrideResult) return overrideResult;
if (shouldThrottleCheck()) {
return cachedResult ?? fallback;
}
markChecked();
const result =
getUpdateChannel() === "nightly"
? await checkNightlyUpdate()
: await checkStableUpdate(current);
cachedResult = result;
return result;
}
export function dismissNightlyUpdate(): void {
void (async () => {
const release = await fetchJson<GhRelease>(
`https://api.github.com/repos/${getRepoSlug()}/releases/tags/${NIGHTLY_TAG}`,
);
if (release?.published_at) {
settingsState.lastSeenNightlyPublishedAt = release.published_at;
}
})();
}