feat: add prerelease RPM builds from upstream main branch
Some checks failed
deploy-ui / build-and-deploy (push) Has been cancelled
Some checks failed
deploy-ui / build-and-deploy (push) Has been cancelled
Poll upstream main branch HEAD alongside release tags. When a new commit is detected, build and publish prerelease RPMs to a separate unstable repo at rpm.lair.cafe/fedora/$releasever/$basearch/unstable/. RPM versioning uses the Fedora snapshot convention (e.g. 0.8.1-0.1.20260511git1a2b3c4.fc43) so stable releases automatically supersede any installed prerelease. - RPM spec: conditional Release field via mistralrs_prerelease define - poll-upstream.yml: new check-prerelease job fetches main HEAD + Cargo.toml version - build-prerelease.yml: new workflow for commit-based builds without --locked - UI: fetch both stable/unstable manifests, show channel badges, add unstable repo setup instructions to home page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
209
.gitea/workflows/build-prerelease.yml
Normal file
209
.gitea/workflows/build-prerelease.yml
Normal file
@@ -0,0 +1,209 @@
|
||||
name: build-prerelease
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
commit:
|
||||
description: "mistral.rs upstream commit SHA"
|
||||
required: true
|
||||
type: string
|
||||
version:
|
||||
description: "Version from upstream Cargo.toml (e.g. 0.8.1)"
|
||||
required: true
|
||||
type: string
|
||||
date:
|
||||
description: "Commit date as YYYYMMDD"
|
||||
required: true
|
||||
type: string
|
||||
short_sha:
|
||||
description: "Short commit SHA (7 chars)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: prerelease-build
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: cuda13
|
||||
fedora_version: "43"
|
||||
runner: cuda-13.0
|
||||
cuda_home: /usr/local/cuda-13.0
|
||||
cargo_features: "cuda cudnn flash-attn nccl"
|
||||
compute_caps: "120"
|
||||
build_jobs: 12
|
||||
nvcc_threads: 4
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install/update Rust toolchain
|
||||
run: |
|
||||
if command -v rustup &> /dev/null; then
|
||||
rustup update stable
|
||||
else
|
||||
curl --proto '=https' --tlsv1.2 --silent --show-error --fail https://sh.rustup.rs | sh -s -- -y
|
||||
fi
|
||||
echo "${HOME}/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Clone mistral.rs at commit
|
||||
run: |
|
||||
git clone https://github.com/EricLBuehler/mistral.rs.git src/
|
||||
cd src
|
||||
git checkout "${{ inputs.commit }}"
|
||||
|
||||
- name: Build mistralrs
|
||||
run: |
|
||||
export PATH="${{ matrix.cuda_home }}/bin:${PATH}"
|
||||
export LD_LIBRARY_PATH="${{ matrix.cuda_home }}/targets/x86_64-linux/lib:${{ matrix.cuda_home }}/lib64:${LD_LIBRARY_PATH:-}"
|
||||
export LIBRARY_PATH="${{ matrix.cuda_home }}/targets/x86_64-linux/lib:${{ matrix.cuda_home }}/lib64:${LIBRARY_PATH:-}"
|
||||
cd src
|
||||
cargo build --release --features "${{ matrix.cargo_features }}"
|
||||
env:
|
||||
CUDA_COMPUTE_CAP: ${{ matrix.compute_caps }}
|
||||
CARGO_BUILD_JOBS: ${{ matrix.build_jobs }}
|
||||
NVCC_THREADS: ${{ matrix.nvcc_threads }}
|
||||
|
||||
- name: Collect artifacts
|
||||
run: |
|
||||
mkdir --parents artifacts
|
||||
cp src/target/release/mistralrs "artifacts/mistralrs-${{ matrix.name }}"
|
||||
echo "built: $(artifacts/mistralrs-${{ matrix.name }} --version 2>&1 | head -1)"
|
||||
|
||||
- name: Upload binary artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: mistralrs-${{ matrix.name }}-fc${{ matrix.fedora_version }}
|
||||
path: artifacts/mistralrs-${{ matrix.name }}
|
||||
retention-days: 1
|
||||
|
||||
package:
|
||||
needs: build
|
||||
runs-on: fedora-43
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: cuda13
|
||||
fedora_version: "43"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download binary
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: mistralrs-${{ matrix.name }}-fc${{ matrix.fedora_version }}
|
||||
path: artifacts/
|
||||
|
||||
- name: Build RPM
|
||||
run: |
|
||||
rm -f ~/.rpmmacros
|
||||
rpmdev-setuptree
|
||||
cp artifacts/mistralrs-${{ matrix.name }} ~/rpmbuild/SOURCES/
|
||||
cp rpm/systemd/mistralrs@.service ~/rpmbuild/SOURCES/
|
||||
cp rpm/systemd/mistralrs@.conf.example ~/rpmbuild/SOURCES/
|
||||
rpmbuild -bb rpm/mistralrs.spec \
|
||||
--define "mistralrs_version ${{ inputs.version }}" \
|
||||
--define "mistralrs_flavour ${{ matrix.name }}" \
|
||||
--define "mistralrs_prerelease 0.1.${{ inputs.date }}git${{ inputs.short_sha }}" \
|
||||
--undefine dist \
|
||||
--define "dist .fc${{ matrix.fedora_version }}"
|
||||
|
||||
- name: Upload RPM
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: rpm-${{ matrix.name }}-fc${{ matrix.fedora_version }}
|
||||
path: ~/rpmbuild/RPMS/x86_64/*.rpm
|
||||
retention-days: 7
|
||||
|
||||
publish:
|
||||
needs: package
|
||||
runs-on: fedora-43
|
||||
concurrency:
|
||||
group: rpm-publish
|
||||
cancel-in-progress: false
|
||||
env:
|
||||
RPM_REPO_HOST: oolon.kosherinata.internal
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- fedora_version: "43"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download RPMs for fc${{ matrix.fedora_version }}
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: rpms/
|
||||
pattern: rpm-*-fc${{ matrix.fedora_version }}
|
||||
|
||||
- name: Flatten RPM artifacts
|
||||
run: |
|
||||
find rpms/ -name '*.rpm' -exec mv --target-directory=rpms/ {} +
|
||||
find rpms/ -mindepth 1 -type d -empty -delete
|
||||
|
||||
- name: Check for sequoia-sq
|
||||
run: |
|
||||
if ! command -v sq &> /dev/null; then
|
||||
echo "ERROR: sequoia-sq is not installed. Install with: sudo dnf install sequoia-sq"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Import signing key
|
||||
run: |
|
||||
echo "${{ secrets.RPM_SIGNING_KEY }}" | gpg --batch --import
|
||||
fpr=$(gpg --batch --with-colons --list-keys "${{ secrets.RPM_SIGNING_KEY_ID }}" | awk -F: '/^fpr:/ { print $10; exit }')
|
||||
echo "${fpr}:6:" | gpg --batch --import-ownertrust
|
||||
sed "s/@GPG_NAME@/${{ secrets.RPM_SIGNING_KEY_ID }}/" rpm/rpmmacros > ~/.rpmmacros
|
||||
|
||||
- name: Sign RPMs
|
||||
run: |
|
||||
for rpm in rpms/*.rpm; do
|
||||
echo "signing ${rpm}..."
|
||||
rpm --addsign "${rpm}"
|
||||
done
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
install --directory --mode 700 ~/.ssh
|
||||
echo "${RSYNC_SSH_KEY}" | install --mode 600 /dev/stdin ~/.ssh/id_ed25519
|
||||
env:
|
||||
RSYNC_SSH_KEY: ${{ secrets.RSYNC_SSH_KEY }}
|
||||
|
||||
- name: Test SSH connectivity
|
||||
run: |
|
||||
ssh -o StrictHostKeyChecking=accept-new "gitea_ci@${RPM_REPO_HOST}" exit
|
||||
|
||||
- name: Ensure unstable repo directory exists
|
||||
run: |
|
||||
ssh "gitea_ci@${RPM_REPO_HOST}" \
|
||||
"mkdir --parents /var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/unstable"
|
||||
|
||||
- name: Sync RPMs to unstable repo
|
||||
run: |
|
||||
rsync \
|
||||
--archive \
|
||||
--verbose \
|
||||
--chmod D755,F644 \
|
||||
rpms/*.rpm \
|
||||
"gitea_ci@${RPM_REPO_HOST}:/var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/unstable/"
|
||||
|
||||
- name: Update unstable repo metadata
|
||||
run: |
|
||||
ssh "gitea_ci@${RPM_REPO_HOST}" \
|
||||
"cd /var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/unstable && createrepo_c --update ."
|
||||
|
||||
- name: Generate packages.json for unstable
|
||||
run: |
|
||||
scp script/generate-packages-json.py "gitea_ci@${RPM_REPO_HOST}:/tmp/"
|
||||
ssh "gitea_ci@${RPM_REPO_HOST}" \
|
||||
"python3 /tmp/generate-packages-json.py \
|
||||
--repodata-dir /var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/unstable/repodata \
|
||||
--output /var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/unstable/packages.json \
|
||||
--base-url https://rpm.lair.cafe/fedora/${{ matrix.fedora_version }}/x86_64/unstable"
|
||||
@@ -83,3 +83,73 @@ jobs:
|
||||
--header 'Content-Type: application/json' \
|
||||
--url "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/workflows/build-release.yml/dispatches" \
|
||||
--data "{\"ref\":\"refs/heads/main\",\"inputs\":{\"tag\":\"${{ steps.upstream.outputs.tag }}\"}}"
|
||||
|
||||
check-prerelease:
|
||||
runs-on: fedora-43
|
||||
steps:
|
||||
- name: Get upstream main branch HEAD
|
||||
id: upstream
|
||||
run: |
|
||||
response=$(curl --silent --show-error --fail --location \
|
||||
--header 'Accept: application/vnd.github+json' \
|
||||
--url 'https://api.github.com/repos/EricLBuehler/mistral.rs/commits/main')
|
||||
sha=$(echo "${response}" | jq -r .sha)
|
||||
short_sha=$(echo "${sha}" | head --bytes=7)
|
||||
date=$(echo "${response}" | jq -r '.commit.committer.date[:10]' | tr -d '-')
|
||||
echo "sha=${sha}" >> "$GITHUB_OUTPUT"
|
||||
echo "short_sha=${short_sha}" >> "$GITHUB_OUTPUT"
|
||||
echo "date=${date}" >> "$GITHUB_OUTPUT"
|
||||
echo "Upstream main HEAD: ${sha} (${date})"
|
||||
|
||||
- name: Get version from upstream Cargo.toml
|
||||
id: version
|
||||
run: |
|
||||
version=$(curl --silent --show-error --fail --location \
|
||||
--header 'Accept: application/vnd.github.raw+json' \
|
||||
--url "https://api.github.com/repos/EricLBuehler/mistral.rs/contents/Cargo.toml?ref=${{ steps.upstream.outputs.sha }}" \
|
||||
| grep '^version' | head --lines=1 | sed 's/.*"\(.*\)".*/\1/')
|
||||
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||
echo "Upstream Cargo.toml version: ${version}"
|
||||
|
||||
- name: Check if prerelease is already published
|
||||
id: published
|
||||
run: |
|
||||
prerelease="0.1.${UPSTREAM_DATE}git${UPSTREAM_SHORT_SHA}"
|
||||
needs_build=false
|
||||
for target in "43:cuda13"; do
|
||||
fedora_version="${target%%:*}"
|
||||
flavour="${target##*:}"
|
||||
base_url="https://rpm.lair.cafe/fedora/${fedora_version}/x86_64/unstable"
|
||||
rpm_name="mistralrs-${flavour}-${UPSTREAM_VERSION}-${prerelease}.fc${fedora_version}.x86_64.rpm"
|
||||
|
||||
http_code=$(curl \
|
||||
--silent \
|
||||
--write-out "%{http_code}" \
|
||||
--output /dev/null \
|
||||
--head \
|
||||
--url "${base_url}/${rpm_name}")
|
||||
if [ "${http_code}" = "404" ]; then
|
||||
echo "missing: ${base_url}/${rpm_name}"
|
||||
needs_build=true
|
||||
continue
|
||||
elif [ "${http_code}" != "200" ]; then
|
||||
echo "unexpected HTTP ${http_code} for ${base_url}/${rpm_name}"
|
||||
exit 1
|
||||
fi
|
||||
echo "found: ${base_url}/${rpm_name}"
|
||||
done
|
||||
echo "already_built=$( [ "${needs_build}" = "true" ] && echo false || echo true )" >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
UPSTREAM_VERSION: ${{ steps.version.outputs.version }}
|
||||
UPSTREAM_DATE: ${{ steps.upstream.outputs.date }}
|
||||
UPSTREAM_SHORT_SHA: ${{ steps.upstream.outputs.short_sha }}
|
||||
|
||||
- name: Trigger prerelease build workflow
|
||||
if: steps.published.outputs.already_built == 'false'
|
||||
run: |
|
||||
curl --fail --silent --show-error --location \
|
||||
--request POST \
|
||||
--header "Authorization: token ${{ secrets.DISPATCH_TOKEN }}" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--url "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/workflows/build-prerelease.yml/dispatches" \
|
||||
--data "{\"ref\":\"refs/heads/main\",\"inputs\":{\"commit\":\"${{ steps.upstream.outputs.sha }}\",\"version\":\"${{ steps.version.outputs.version }}\",\"date\":\"${{ steps.upstream.outputs.date }}\",\"short_sha\":\"${{ steps.upstream.outputs.short_sha }}\"}}"
|
||||
|
||||
@@ -6,9 +6,16 @@
|
||||
%{!?mistralrs_version: %global mistralrs_version 0.7.0}
|
||||
%{!?mistralrs_flavour: %global mistralrs_flavour cuda13}
|
||||
|
||||
# For prerelease builds, pass --define "mistralrs_prerelease 0.1.YYYYMMDDgitSHORTSHA"
|
||||
%if 0%{?mistralrs_prerelease:1}
|
||||
%global mistralrs_release %{mistralrs_prerelease}
|
||||
%else
|
||||
%global mistralrs_release 1
|
||||
%endif
|
||||
|
||||
Name: mistralrs-%{mistralrs_flavour}
|
||||
Version: %{mistralrs_version}
|
||||
Release: 1%{?dist}
|
||||
Release: %{mistralrs_release}%{?dist}
|
||||
Summary: Fast, flexible LLM inference server (mistral.rs, %{mistralrs_flavour} flavour)
|
||||
|
||||
License: MIT
|
||||
|
||||
@@ -1,23 +1,47 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { PackagesManifest } from "../types/packages.ts";
|
||||
import type { Channel, PackagesManifest, PackageVersion } from "../types/packages.ts";
|
||||
|
||||
const MANIFEST_URL = "/fedora/43/x86_64/packages.json";
|
||||
const STABLE_URL = "/fedora/43/x86_64/packages.json";
|
||||
const UNSTABLE_URL = "/fedora/43/x86_64/unstable/packages.json";
|
||||
|
||||
function tagPackages(
|
||||
manifest: PackagesManifest,
|
||||
channel: Channel,
|
||||
): PackageVersion[] {
|
||||
return manifest.packages.map((p) => ({
|
||||
...p,
|
||||
channel,
|
||||
baseUrl: manifest.baseUrl,
|
||||
}));
|
||||
}
|
||||
|
||||
export function usePackages() {
|
||||
const [manifest, setManifest] = useState<PackagesManifest | null>(null);
|
||||
const [packages, setPackages] = useState<PackageVersion[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
fetch(MANIFEST_URL)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json() as Promise<PackagesManifest>;
|
||||
})
|
||||
.then((data) => {
|
||||
if (!cancelled) setManifest(data);
|
||||
const fetchManifest = async (
|
||||
url: string,
|
||||
channel: Channel,
|
||||
): Promise<PackageVersion[]> => {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
if (res.status === 404) return [];
|
||||
throw new Error(`HTTP ${res.status} fetching ${channel} manifest`);
|
||||
}
|
||||
const data = (await res.json()) as PackagesManifest;
|
||||
return tagPackages(data, channel);
|
||||
};
|
||||
|
||||
Promise.all([
|
||||
fetchManifest(STABLE_URL, "stable"),
|
||||
fetchManifest(UNSTABLE_URL, "unstable"),
|
||||
])
|
||||
.then(([stable, unstable]) => {
|
||||
if (!cancelled) setPackages([...stable, ...unstable]);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (!cancelled)
|
||||
@@ -32,5 +56,5 @@ export function usePackages() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { manifest, loading, error };
|
||||
return { packages, loading, error };
|
||||
}
|
||||
|
||||
@@ -10,6 +10,13 @@ enabled=1
|
||||
gpgcheck=1
|
||||
gpgkey=${GPG_KEY_URL}`;
|
||||
|
||||
const UNSTABLE_REPO_FILE = `[lair-cafe-unstable]
|
||||
name=lair.cafe RPM Repository (unstable)
|
||||
baseurl=https://rpm.lair.cafe/fedora/$releasever/$basearch/unstable/
|
||||
enabled=0
|
||||
gpgcheck=1
|
||||
gpgkey=${GPG_KEY_URL}`;
|
||||
|
||||
export function Home() {
|
||||
return (
|
||||
<>
|
||||
@@ -43,6 +50,50 @@ export function Home() {
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col lg={12}>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title>Unstable (prerelease) packages</Card.Title>
|
||||
<p>
|
||||
Unstable packages are built automatically from the latest
|
||||
upstream <code>main</code> branch commit. They use the
|
||||
next release version from <code>Cargo.toml</code> with a
|
||||
prerelease suffix (e.g.{" "}
|
||||
<code>0.8.1-0.1.20260511git1a2b3c4</code>). When the
|
||||
upstream version is officially released, the stable package
|
||||
will automatically supersede any installed prerelease.
|
||||
</p>
|
||||
|
||||
<h6 className="mt-4">Add the unstable repository</h6>
|
||||
<p className="text-body-secondary">
|
||||
The unstable repo is disabled by default. Add it alongside the
|
||||
stable repo:
|
||||
</p>
|
||||
<CodeBlock language="bash">
|
||||
{`sudo dnf config-manager addrepo --from-repofile=/dev/stdin <<'EOF'\n${UNSTABLE_REPO_FILE}\nEOF`}
|
||||
</CodeBlock>
|
||||
|
||||
<h6 className="mt-4">
|
||||
Install or update from unstable
|
||||
</h6>
|
||||
<CodeBlock language="bash">
|
||||
{`sudo dnf --enablerepo=lair-cafe-unstable install mistralrs-cuda13`}
|
||||
</CodeBlock>
|
||||
|
||||
<h6 className="mt-4">
|
||||
Pin to stable
|
||||
</h6>
|
||||
<p className="text-body-secondary">
|
||||
If you have the unstable repo enabled and want to stay on
|
||||
stable releases, exclude prerelease versions:
|
||||
</p>
|
||||
<CodeBlock language="bash">
|
||||
{`sudo dnf --disablerepo=lair-cafe-unstable update mistralrs-cuda13`}
|
||||
</CodeBlock>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -11,13 +11,13 @@ function formatBytes(bytes: number): string {
|
||||
|
||||
export function PackageDetail() {
|
||||
const { name } = useParams<{ name: string }>();
|
||||
const { manifest, loading, error } = usePackages();
|
||||
const { packages, loading, error } = usePackages();
|
||||
|
||||
if (loading) return <Spinner animation="border" />;
|
||||
if (error) return <Alert variant="danger">Failed to load packages: {error}</Alert>;
|
||||
if (!manifest) return <Alert variant="info">No package data available.</Alert>;
|
||||
if (packages.length === 0) return <Alert variant="info">No package data available.</Alert>;
|
||||
|
||||
const versions = manifest.packages
|
||||
const versions = packages
|
||||
.filter((p) => p.name === name)
|
||||
.sort((a, b) => b.buildTime - a.buildTime);
|
||||
|
||||
@@ -25,6 +25,7 @@ export function PackageDetail() {
|
||||
return <Alert variant="warning">Package not found: {name}</Alert>;
|
||||
|
||||
const latest = versions[0];
|
||||
const hasUnstable = versions.some((v) => v.channel === "unstable");
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -33,6 +34,14 @@ export function PackageDetail() {
|
||||
|
||||
<CodeBlock language="bash">{`sudo dnf install ${name}`}</CodeBlock>
|
||||
|
||||
{hasUnstable && (
|
||||
<div className="mt-3">
|
||||
<CodeBlock language="bash">
|
||||
{`# install latest unstable version\nsudo dnf --enablerepo=lair-cafe-unstable install ${name}`}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h2 className="mt-4 mb-3">
|
||||
Versions <Badge bg="secondary">{versions.length}</Badge>
|
||||
</h2>
|
||||
@@ -41,6 +50,7 @@ export function PackageDetail() {
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<th>Channel</th>
|
||||
<th>Size</th>
|
||||
<th>Built</th>
|
||||
<th>Download</th>
|
||||
@@ -48,14 +58,19 @@ export function PackageDetail() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{versions.map((pkg) => (
|
||||
<tr key={`${pkg.version}-${pkg.release}`}>
|
||||
<tr key={`${pkg.version}-${pkg.release}-${pkg.channel}`}>
|
||||
<td>
|
||||
{pkg.version}-{pkg.release}
|
||||
</td>
|
||||
<td>
|
||||
<Badge bg={pkg.channel === "stable" ? "success" : "warning"}>
|
||||
{pkg.channel}
|
||||
</Badge>
|
||||
</td>
|
||||
<td>{formatBytes(pkg.size)}</td>
|
||||
<td>{new Date(pkg.buildTime * 1000).toLocaleDateString()}</td>
|
||||
<td>
|
||||
<a href={`${manifest.baseUrl}/${pkg.rpmFilename}`}>
|
||||
<a href={`${pkg.baseUrl}/${pkg.rpmFilename}`}>
|
||||
{pkg.rpmFilename}
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
import { Alert, Spinner, Table } from "react-bootstrap";
|
||||
import { Alert, Badge, Spinner, Table } from "react-bootstrap";
|
||||
import { Link } from "react-router";
|
||||
import { usePackages } from "../hooks/usePackages.ts";
|
||||
|
||||
export function PackageList() {
|
||||
const { manifest, loading, error } = usePackages();
|
||||
const { packages, loading, error } = usePackages();
|
||||
|
||||
if (loading) return <Spinner animation="border" />;
|
||||
if (error) return <Alert variant="danger">Failed to load packages: {error}</Alert>;
|
||||
if (!manifest || manifest.packages.length === 0)
|
||||
if (packages.length === 0)
|
||||
return <Alert variant="info">No packages published yet.</Alert>;
|
||||
|
||||
const byName = Map.groupBy(manifest.packages, (p) => p.name);
|
||||
const byName = Map.groupBy(packages, (p) => p.name);
|
||||
const summaries = [...byName.entries()].map(([name, versions]) => {
|
||||
const stable = versions.filter((v) => v.channel === "stable");
|
||||
const unstable = versions.filter((v) => v.channel === "unstable");
|
||||
const latest = versions.reduce((a, b) =>
|
||||
a.buildTime >= b.buildTime ? a : b,
|
||||
);
|
||||
return { name, latest, versionCount: versions.length };
|
||||
return {
|
||||
name,
|
||||
latest,
|
||||
stableCount: stable.length,
|
||||
unstableCount: unstable.length,
|
||||
versionCount: versions.length,
|
||||
};
|
||||
});
|
||||
summaries.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
@@ -33,15 +41,25 @@ export function PackageList() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{summaries.map(({ name, latest, versionCount }) => (
|
||||
{summaries.map(({ name, latest, stableCount, unstableCount }) => (
|
||||
<tr key={name}>
|
||||
<td>
|
||||
<Link to={`/packages/${name}`}>{name}</Link>
|
||||
</td>
|
||||
<td>
|
||||
{latest.version}-{latest.release}
|
||||
{latest.version}-{latest.release}{" "}
|
||||
<Badge bg={latest.channel === "stable" ? "success" : "warning"} className="ms-1">
|
||||
{latest.channel}
|
||||
</Badge>
|
||||
</td>
|
||||
<td>
|
||||
{stableCount > 0 && (
|
||||
<Badge bg="success" className="me-1">{stableCount} stable</Badge>
|
||||
)}
|
||||
{unstableCount > 0 && (
|
||||
<Badge bg="warning">{unstableCount} unstable</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td>{versionCount}</td>
|
||||
<td>{latest.summary}</td>
|
||||
<td>{new Date(latest.buildTime * 1000).toLocaleDateString()}</td>
|
||||
</tr>
|
||||
|
||||
@@ -4,6 +4,8 @@ export interface ChangelogEntry {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export type Channel = "stable" | "unstable";
|
||||
|
||||
export interface PackageVersion {
|
||||
name: string;
|
||||
version: string;
|
||||
@@ -14,6 +16,8 @@ export interface PackageVersion {
|
||||
buildTime: number;
|
||||
rpmFilename: string;
|
||||
changelog: ChangelogEntry[];
|
||||
channel: Channel;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
export interface PackagesManifest {
|
||||
|
||||
Reference in New Issue
Block a user