diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-08-05 19:46:00 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-08-05 19:46:00 +0900 |
| commit | af776eac4fd971483397f40e8c8452df485892e0 (patch) | |
| tree | 3100e0da58e750f3c3e26f8293b169192f57059a | |
| parent | ce1a6123f7d48100ba3b216746127ba269fe21fb (diff) | |
| download | LunaticChat-af776eac4fd971483397f40e8c8452df485892e0.tar.gz LunaticChat-af776eac4fd971483397f40e8c8452df485892e0.tar.bz2 LunaticChat-af776eac4fd971483397f40e8c8452df485892e0.zip | |
fix: read plugin versions from release JAR names, not tag names
The download cards and the compatibility matrix derived each plugin's version
from the release tag, which cannot hold for a unified `vX.Y.Z` release: it
carries both JARs and their versions need not agree. v1.3.0 shipped Paper 1.3.0
alongside Velocity 1.2.0.
So the download page advertised Paper v1.2.2 and Velocity v1.1.0, two
generations stale, because it searched `paper/v` and `velocity/v` tags first and
fell back to a unified tag only when none existed. The matrix meanwhile put a
Velocity v1.3.0 on its axis that was never released, leaving no row for the
proxy build operators actually run.
Both now pick releases by the JARs attached to them and take the version from
the file name, the only place it is stated. Tag shape stops mattering: a
platform-specific tag and a unified one are alike just releases that happen to
carry a given JAR.
Releases rank by publication time rather than by API response order, which
follows tag creation instead: velocity/v1.1.0 precedes the later-published
paper/v1.2.2.
A version is listed once even when a later unified release re-attaches an
unchanged JAR, which release.yaml does whenever only the other platform was
bumped. Otherwise the matrix would carry one version twice, under two protocol
versions if the protocol had moved in between.
Tests run in CI from here on. They need an explicit path because Bun does not
discover tests inside dot-directories.
Co-Authored-By: Claude <noreply@anthropic.com>
| -rw-r--r-- | .github/workflows/ci.yaml | 3 | ||||
| -rw-r--r-- | website/.vitepress/theme/components/DownloadCard.vue | 14 | ||||
| -rw-r--r-- | website/.vitepress/theme/components/releaseAssets.test.ts | 144 | ||||
| -rw-r--r-- | website/.vitepress/theme/components/releaseAssets.ts | 78 | ||||
| -rw-r--r-- | website/.vitepress/theme/components/useCompatibilityData.ts | 60 | ||||
| -rw-r--r-- | website/.vitepress/theme/components/useDownloadData.ts | 70 | ||||
| -rw-r--r-- | website/package.json | 1 |
7 files changed, 281 insertions, 89 deletions
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 28e2dfb..16e4b50 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -118,6 +118,9 @@ jobs: - name: Check lints run: bun run lint + - name: Run tests + run: bun run test + - name: Build documentation run: bun run build diff --git a/website/.vitepress/theme/components/DownloadCard.vue b/website/.vitepress/theme/components/DownloadCard.vue index add8d45..046de21 100644 --- a/website/.vitepress/theme/components/DownloadCard.vue +++ b/website/.vitepress/theme/components/DownloadCard.vue @@ -71,14 +71,12 @@ const t = computed(() => }, ); -function formatSize(bytes: number | null): string { - if (!bytes) return '-'; +function formatSize(bytes: number): string { const mb = bytes / (1024 * 1024); return `${mb.toFixed(1)} MB`; } -function formatDate(dateStr: string | null): string { - if (!dateStr) return '-'; +function formatDate(dateStr: string): string { const locale = isEn.value ? 'en-US' : 'ja-JP'; return new Date(dateStr).toLocaleDateString(locale, { year: 'numeric', @@ -138,11 +136,11 @@ function formatDate(dateStr: string | null): string { <dd>{{ formatSize(data.paper.fileSize) }}</dd> </div> <div> - <dd><code>{{ data.paper.fileName ?? '-' }}</code></dd> + <dd><code>{{ data.paper.fileName }}</code></dd> </div> </dl> <div class="download-actions"> - <a v-if="data.paper.downloadUrl" :href="data.paper.downloadUrl" class="download-btn primary">{{ t.download }}</a> + <a :href="data.paper.downloadUrl" class="download-btn primary">{{ t.download }}</a> <a :href="data.paper.releaseUrl" class="download-btn" target="_blank" rel="noopener">{{ t.releaseNotes }}</a> </div> </div> @@ -172,11 +170,11 @@ function formatDate(dateStr: string | null): string { <dd>{{ formatSize(data.velocity.fileSize) }}</dd> </div> <div> - <dd><code>{{ data.velocity.fileName ?? '-' }}</code></dd> + <dd><code>{{ data.velocity.fileName }}</code></dd> </div> </dl> <div class="download-actions"> - <a v-if="data.velocity.downloadUrl" :href="data.velocity.downloadUrl" class="download-btn primary">{{ t.download }}</a> + <a :href="data.velocity.downloadUrl" class="download-btn primary">{{ t.download }}</a> <a :href="data.velocity.releaseUrl" class="download-btn" target="_blank" rel="noopener">{{ t.releaseNotes }}</a> </div> </div> diff --git a/website/.vitepress/theme/components/releaseAssets.test.ts b/website/.vitepress/theme/components/releaseAssets.test.ts new file mode 100644 index 0000000..32dea7b --- /dev/null +++ b/website/.vitepress/theme/components/releaseAssets.test.ts @@ -0,0 +1,144 @@ +import { describe, expect, test } from 'bun:test'; +import { + findPlatformAsset, + type GitHubRelease, + latestPlatformRelease, + platformReleases, +} from './releaseAssets'; + +function release( + tag: string, + publishedAt: string, + assetNames: string[], +): GitHubRelease { + return { + tag_name: tag, + published_at: publishedAt, + html_url: `https://github.com/m1sk9/LunaticChat/releases/tag/${tag}`, + assets: assetNames.map((name) => ({ + name, + size: 1024, + browser_download_url: `https://example.invalid/${name}`, + })), + }; +} + +const UNIFIED_1_3_0 = release('v1.3.0', '2026-08-04T20:05:51Z', [ + 'LunaticChat-1.2.0-velocity.jar', + 'LunaticChat-1.3.0.jar', +]); +const PAPER_1_2_2 = release('paper/v1.2.2', '2026-06-12T07:57:55Z', [ + 'LunaticChat-1.2.2.jar', +]); +const VELOCITY_1_1_0 = release('velocity/v1.1.0', '2026-06-12T07:57:33Z', [ + 'LunaticChat-1.1.0-velocity.jar', +]); + +describe('findPlatformAsset', () => { + test('reads each platform version from its own JAR, not from the shared tag', () => { + expect(findPlatformAsset(UNIFIED_1_3_0, 'paper')?.version).toBe('1.3.0'); + expect(findPlatformAsset(UNIFIED_1_3_0, 'velocity')?.version).toBe('1.2.0'); + }); + + test('returns the asset backing the reported version', () => { + expect(findPlatformAsset(UNIFIED_1_3_0, 'velocity')?.asset.name).toBe( + 'LunaticChat-1.2.0-velocity.jar', + ); + }); + + test('does not mistake the Velocity JAR for the Paper JAR', () => { + expect(findPlatformAsset(VELOCITY_1_1_0, 'paper')).toBeNull(); + }); + + test('returns null when the release carries no JAR for the platform', () => { + expect(findPlatformAsset(PAPER_1_2_2, 'velocity')).toBeNull(); + }); +}); + +describe('latestPlatformRelease', () => { + const releases = [UNIFIED_1_3_0, VELOCITY_1_1_0, PAPER_1_2_2]; + + test('prefers a newer unified release over an older platform-specific tag', () => { + expect(latestPlatformRelease(releases, 'paper')?.version).toBe('1.3.0'); + expect(latestPlatformRelease(releases, 'velocity')?.version).toBe('1.2.0'); + }); + + test('prefers a newer platform-specific tag over an older unified release', () => { + const paperOnly = release('paper/v1.4.0', '2026-09-01T00:00:00Z', [ + 'LunaticChat-1.4.0.jar', + ]); + const withPaperOnly = [...releases, paperOnly]; + + expect(latestPlatformRelease(withPaperOnly, 'paper')?.version).toBe( + '1.4.0', + ); + expect(latestPlatformRelease(withPaperOnly, 'velocity')?.version).toBe( + '1.2.0', + ); + }); + + test('advances one platform without disturbing the other', () => { + const velocityOnly = release('velocity/v1.3.0', '2026-09-01T00:00:00Z', [ + 'LunaticChat-1.3.0-velocity.jar', + ]); + const withVelocityOnly = [...releases, velocityOnly]; + + expect(latestPlatformRelease(withVelocityOnly, 'velocity')?.version).toBe( + '1.3.0', + ); + expect(latestPlatformRelease(withVelocityOnly, 'paper')?.version).toBe( + '1.3.0', + ); + }); + + test('ranks by publication time rather than by response order', () => { + const outOfOrder = [PAPER_1_2_2, UNIFIED_1_3_0]; + expect(latestPlatformRelease(outOfOrder, 'paper')?.release.tag_name).toBe( + 'v1.3.0', + ); + }); + + test('returns null when no release carries a JAR for the platform', () => { + expect(latestPlatformRelease([PAPER_1_2_2], 'velocity')).toBeNull(); + }); +}); + +describe('platformReleases', () => { + test('lists every release carrying the platform JAR, newest first', () => { + expect( + platformReleases([PAPER_1_2_2, UNIFIED_1_3_0], 'paper').map( + (e) => e.version, + ), + ).toEqual(['1.3.0', '1.2.2']); + }); + + test('excludes releases that ship only the other platform', () => { + expect(platformReleases([VELOCITY_1_1_0], 'paper')).toEqual([]); + }); + + test('lists a version once when a later unified release re-attaches its JAR', () => { + // v1.4.0 bumps Paper alone, so it re-attaches the unchanged Velocity 1.2.0 JAR. + const unified_1_4_0 = release('v1.4.0', '2026-09-01T00:00:00Z', [ + 'LunaticChat-1.2.0-velocity.jar', + 'LunaticChat-1.4.0.jar', + ]); + + expect( + platformReleases([UNIFIED_1_3_0, unified_1_4_0], 'velocity').map( + (e) => e.version, + ), + ).toEqual(['1.2.0']); + }); + + test('attributes a re-attached JAR to the release that shipped it last', () => { + const unified_1_4_0 = release('v1.4.0', '2026-09-01T00:00:00Z', [ + 'LunaticChat-1.2.0-velocity.jar', + 'LunaticChat-1.4.0.jar', + ]); + + expect( + platformReleases([UNIFIED_1_3_0, unified_1_4_0], 'velocity')[0]?.release + .tag_name, + ).toBe('v1.4.0'); + }); +}); diff --git a/website/.vitepress/theme/components/releaseAssets.ts b/website/.vitepress/theme/components/releaseAssets.ts new file mode 100644 index 0000000..f05977b --- /dev/null +++ b/website/.vitepress/theme/components/releaseAssets.ts @@ -0,0 +1,78 @@ +export type Platform = 'paper' | 'velocity'; + +export interface ReleaseAsset { + name: string; + size: number; + browser_download_url: string; +} + +export interface GitHubRelease { + tag_name: string; + published_at: string; + html_url: string; + assets: ReleaseAsset[]; +} + +export interface PlatformAsset { + version: string; + asset: ReleaseAsset; +} + +export interface PlatformRelease extends PlatformAsset { + release: GitHubRelease; +} + +// A unified `vX.Y.Z` tag ships both platforms and their versions need not agree +// (v1.3.0 carried Velocity 1.2.0), so a tag name can never stand in for a +// platform version — only the JAR file name states it. +const JAR_PATTERN: Record<Platform, RegExp> = { + paper: /^LunaticChat-(\d+\.\d+\.\d+)\.jar$/, + velocity: /^LunaticChat-(\d+\.\d+\.\d+)-velocity\.jar$/, +}; + +export function findPlatformAsset( + release: GitHubRelease, + platform: Platform, +): PlatformAsset | null { + for (const asset of release.assets) { + const version = JAR_PATTERN[platform].exec(asset.name)?.[1]; + if (version) return { version, asset }; + } + return null; +} + +export function platformReleases( + releases: GitHubRelease[], + platform: Platform, +): PlatformRelease[] { + // The API orders releases by tag creation, not by publication — velocity/v1.1.0 + // precedes the later-published paper/v1.2.2 — so response order cannot decide + // which release is the newest. + const found = releases + .flatMap((release) => { + const asset = findPlatformAsset(release, platform); + return asset ? [{ release, ...asset }] : []; + }) + .sort( + (a, b) => + Date.parse(b.release.published_at) - Date.parse(a.release.published_at), + ); + + // A unified `vX.Y.Z` release always attaches both JARs, so a platform version + // reappears under a new tag whenever only the other platform was bumped. The + // newest publication is the one that shipped, and keeping both would list the + // same version twice — with two protocol versions when the protocol moved. + const seen = new Set<string>(); + return found.filter((entry) => { + if (seen.has(entry.version)) return false; + seen.add(entry.version); + return true; + }); +} + +export function latestPlatformRelease( + releases: GitHubRelease[], + platform: Platform, +): PlatformRelease | null { + return platformReleases(releases, platform)[0] ?? null; +} diff --git a/website/.vitepress/theme/components/useCompatibilityData.ts b/website/.vitepress/theme/components/useCompatibilityData.ts index d0de01d..2952355 100644 --- a/website/.vitepress/theme/components/useCompatibilityData.ts +++ b/website/.vitepress/theme/components/useCompatibilityData.ts @@ -1,15 +1,14 @@ -import { ref, onMounted } from 'vue'; +import { onMounted, ref } from 'vue'; +import { + type GitHubRelease, + type Platform, + platformReleases, +} from './releaseAssets'; const REPO = 'm1sk9/LunaticChat'; const PROTOCOL_FILE_PATH = 'engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersion.kt'; -interface GitHubRelease { - tag_name: string; - published_at: string; - html_url: string; -} - export interface ProtocolVersion { major: number; minor: number; @@ -18,7 +17,7 @@ export interface ProtocolVersion { } export interface PlatformReleaseEntry { - platform: 'paper' | 'velocity'; + platform: Platform; version: string; tag: string; publishedAt: string; @@ -55,7 +54,9 @@ function parseProtocolVersion(source: string): ProtocolVersion | null { return { major, minor, patch, minSupportedMinor }; } -async function fetchProtocolAtTag(tag: string): Promise<ProtocolVersion | null> { +async function fetchProtocolAtTag( + tag: string, +): Promise<ProtocolVersion | null> { const url = `https://raw.githubusercontent.com/${REPO}/${encodeURIComponent(tag)}/${PROTOCOL_FILE_PATH}`; try { const res = await fetch(url); @@ -67,19 +68,18 @@ async function fetchProtocolAtTag(tag: string): Promise<ProtocolVersion | null> } } -function buildEntry( - release: GitHubRelease, - platform: 'paper' | 'velocity', -): PlatformReleaseEntry { - const version = release.tag_name.replace(/^(paper\/|velocity\/)?v/, ''); - return { +function buildEntries( + releases: GitHubRelease[], + platform: Platform, +): PlatformReleaseEntry[] { + return platformReleases(releases, platform).map((entry) => ({ platform, - version, - tag: release.tag_name, - publishedAt: release.published_at, - releaseUrl: release.html_url, + version: entry.version, + tag: entry.release.tag_name, + publishedAt: entry.release.published_at, + releaseUrl: entry.release.html_url, protocol: null, - }; + })); } export function useCompatibilityData() { @@ -99,18 +99,8 @@ export function useCompatibilityData() { const releases: GitHubRelease[] = await res.json(); - const isUnified = (tag: string) => /^v\d/.test(tag); - const isPaperTag = (tag: string) => - tag.startsWith('paper/v') || isUnified(tag); - const isVelocityTag = (tag: string) => - tag.startsWith('velocity/v') || isUnified(tag); - - const paper = releases - .filter((r) => isPaperTag(r.tag_name)) - .map((r) => buildEntry(r, 'paper')); - const velocity = releases - .filter((r) => isVelocityTag(r.tag_name)) - .map((r) => buildEntry(r, 'velocity')); + const paper = buildEntries(releases, 'paper'); + const velocity = buildEntries(releases, 'velocity'); const all = [...paper, ...velocity]; const protocols = await Promise.all( @@ -134,7 +124,11 @@ export function useCompatibilityData() { return { data, loading, error }; } -export type CompatibilityResult = 'compatible' | 'major-mismatch' | 'paper-too-new' | 'paper-too-old'; +export type CompatibilityResult = + | 'compatible' + | 'major-mismatch' + | 'paper-too-new' + | 'paper-too-old'; // Mirrors the gatekeeping done by Velocity in // platform-velocity/.../PluginMessageHandler.kt — Paper does not validate. diff --git a/website/.vitepress/theme/components/useDownloadData.ts b/website/.vitepress/theme/components/useDownloadData.ts index 0c9e7e0..8ed264a 100644 --- a/website/.vitepress/theme/components/useDownloadData.ts +++ b/website/.vitepress/theme/components/useDownloadData.ts @@ -1,27 +1,19 @@ -import { ref, onMounted } from 'vue'; +import { onMounted, ref } from 'vue'; +import { + type GitHubRelease, + latestPlatformRelease, + type Platform, +} from './releaseAssets'; const REPO = 'm1sk9/LunaticChat'; -interface ReleaseAsset { - name: string; - size: number; - browser_download_url: string; -} - -interface GitHubRelease { - tag_name: string; - published_at: string; - html_url: string; - assets: ReleaseAsset[]; -} - export interface PlatformRelease { version: string; publishedAt: string; releaseUrl: string; - downloadUrl: string | null; - fileName: string | null; - fileSize: number | null; + downloadUrl: string; + fileName: string; + fileSize: number; } export interface DownloadData { @@ -30,22 +22,20 @@ export interface DownloadData { ci: { url: string }; } -function parsePlatformRelease( - release: GitHubRelease | null, - jarPattern: RegExp, +function resolvePlatformRelease( + releases: GitHubRelease[], + platform: Platform, ): PlatformRelease | null { - if (!release) return null; - - const asset = release.assets.find((a) => jarPattern.test(a.name)); - const version = release.tag_name.replace(/^(paper\/|velocity\/)?v/, ''); + const latest = latestPlatformRelease(releases, platform); + if (!latest) return null; return { - version, - publishedAt: release.published_at, - releaseUrl: release.html_url, - downloadUrl: asset?.browser_download_url ?? null, - fileName: asset?.name ?? null, - fileSize: asset?.size ?? null, + version: latest.version, + publishedAt: latest.release.published_at, + releaseUrl: latest.release.html_url, + downloadUrl: latest.asset.browser_download_url, + fileName: latest.asset.name, + fileSize: latest.asset.size, }; } @@ -72,25 +62,9 @@ export function useDownloadData() { const releases: GitHubRelease[] = await res.json(); - const paperRelease = - releases.find((r) => r.tag_name.startsWith('paper/v')) ?? - releases.find((r) => /^v\d/.test(r.tag_name)) ?? - null; - - const velocityRelease = - releases.find((r) => r.tag_name.startsWith('velocity/v')) ?? - releases.find((r) => /^v\d/.test(r.tag_name)) ?? - null; - data.value = { - paper: parsePlatformRelease( - paperRelease, - /^LunaticChat-[\d.]+\.jar$/, - ), - velocity: parsePlatformRelease( - velocityRelease, - /^LunaticChat-[\d.]+-velocity\.jar$/, - ), + paper: resolvePlatformRelease(releases, 'paper'), + velocity: resolvePlatformRelease(releases, 'velocity'), ci: data.value.ci, }; } catch { diff --git a/website/package.json b/website/package.json index 48c2a6b..5b18155 100644 --- a/website/package.json +++ b/website/package.json @@ -3,6 +3,7 @@ "dev": "vitepress dev", "build": "vitepress build", "preview": "vitepress preview", + "test": "bun test ./.vitepress/", "format": "biome format --write .", "format:check": "biome ci .", "lint": "biome lint .", |
