This commit is contained in:
Mateusz Gruszczyński committed 2026-10-30 13:11:36 +01:00
1 parent 0efaae1715
commit c1a2a8cc30
13 files changed
+176 -133

No files matched your search

+77 -65
View File
@@ -1,30 +1,41 @@
# rustbox-package-builder
Linux-only builder i repozytorium pakietów dla RustBox. Projekt jest uproszczoną wersją pipeline'u używanego dla RustProxy: nie zawiera FreeBSD, OPNsense ani dodatkowego agenta.
Linux-only package builder and signed repository publisher for RustBox. It watches the RustBox source repository, builds release binaries, produces distribution-specific packages, signs repository metadata, and serves a small read-only repository dashboard.
## Co buduje
## Supported Linux families
Z jednego commita RustBox dla `amd64/x86_64`:
A single RustBox source commit is published for `amd64/x86_64` in three equal Linux families:
- podpisany DEB i repo APT,
- podpisany RPM i repo DNF/YUM,
- opcjonalny osobny RPM dla SUSE/openSUSE i repo Zypper,
- panel WWW repozytorium w stylistyce RustBox,
- publiczny JSON `GET /api/v1/packages`.
- Debian / Ubuntu: DEB package and signed APT repository
- RHEL / AlmaLinux / Rocky Linux: RPM package and signed DNF/YUM repository
- SUSE / openSUSE: RPM package and signed Zypper repository
Builder obserwuje `SOURCE_BRANCH`. Nowy commit lub zmiana `PACKAGING_REVISION` uruchamia testy, release build i publikację nowego snapshotu. `current` jest przełączany atomowo dopiero po udanym pakowaniu i podpisaniu metadanych.
The dashboard and API use these three families directly. There is no separate generic `Linux` channel.
## Wymaganie po stronie źródeł RustBox
## Repository dashboard
Repo RustBox musi obsługiwać natywne logowanie plikowe przez `log_dir`. Pakiet ustawia:
The repository UI is English-only and intentionally text-only. It has no logo, favicon, decorative icons, or letter badge. The default public HTTP port is `18965`.
The dashboard provides:
- latest RustBox package version
- status for Debian / Ubuntu, RHEL / AlmaLinux / Rocky Linux, and SUSE / openSUSE
- downloadable package list
- installation commands for each Linux family
- repository metadata and build information
- JSON API at `GET /api/v1/packages`
## RustBox source requirement
RustBox must support native file logging through `log_dir`. The package configuration uses:
```toml
log_dir = "/var/log/rustbox"
```
Builder celowo przerywa build, jeśli w źródłach nie ma obsługi `log_dir`. Dzięki temu logi aplikacji trafiają do `/var/log/rustbox/rustbox.log`, a nie do stdout usługi systemd.
The builder rejects RustBox sources that do not contain this setting. Application logs are written to `/var/log/rustbox/rustbox.log` instead of normal systemd stdout logging.
## Szybki start
## Quick start
```bash
cp .env.example .env
@@ -32,30 +43,26 @@ nano .env
./start.sh
```
Minimalnie ustaw:
Minimum configuration:
```env
SOURCE_REPO=https://git.example.com/rustbox/RustBox.git
SOURCE_BRANCH=master
REPOSITORY_URL=https://repo.example.com
REPOSITORY_URL=http://127.0.0.1:18965
HTTP_PORT=18965
```
Dla lokalnego HTTP można użyć:
For a public installation, set `REPOSITORY_URL` to the externally reachable URL. If a reverse proxy terminates HTTPS, it can forward traffic to local port `18965`.
```env
REPOSITORY_URL=http://192.168.1.100
HTTP_PORT=18765
```
`start.sh` creates a persistent GPG signing key in `gpg/`, builds the containers, and starts:
`start.sh` utworzy trwały klucz GPG w `gpg/`, zbuduje obrazy i uruchomi:
- `rustbox-package-builder`: source watcher, build, package and publication process
- `repository-dashboard`: read-only dashboard and API
- `repository-http`: Nginx serving the dashboard and package repositories on port `18965` by default
- `rustbox-package-builder` — watcher + build + publikacja,
- `repository-dashboard` — read-only panel/API,
- `repository-http` — Nginx udostępniający panel oraz artefakty APT/RPM.
Persistent builder state is stored in `data/`.
Dane buildera są trwałe w `data/`.
## Domyślne ustawienia builda
## Default build settings
```env
POLL_INTERVAL=300
@@ -64,15 +71,16 @@ REPO_COMPONENT=main
PACKAGE_RELEASE=auto
RUN_LINUX_TESTS=compile
MAX_GLIBC_VERSION=2.34
BUILD_SUSE=1
RELEASE_RETENTION=10
```
`RUN_LINUX_TESTS=compile` uruchamia `cargo test --release --locked --no-run`. Ustaw `full`, aby uruchamiać wszystkie testy, albo `0`, aby je wyłączyć.
`RUN_LINUX_TESTS=compile` runs `cargo test --release --locked --no-run`. Use `full` to run the test suite or `0` to skip Rust tests.
## Instalacja pakietu RustBox
All three supported Linux package families are built on every successful release. SUSE is not treated as a separate non-Linux target.
Pakiet instaluje:
## Installed RustBox layout
The generated packages install:
```text
/usr/local/bin/rustbox
@@ -81,7 +89,7 @@ Pakiet instaluje:
/etc/logrotate.d/rustbox
```
oraz tworzy:
They create and maintain:
```text
/var/lib/rustbox rustbox:rustbox 0700
@@ -89,11 +97,11 @@ oraz tworzy:
/etc/rustbox rustbox:rustbox 0750
```
`/etc/rustbox/rustbox.toml` ma finalnie `rustbox:rustbox 0640` i jest oznaczony jako `config|noreplace`, więc aktualizacja pakietu nie nadpisuje lokalnej konfiguracji.
`/etc/rustbox/rustbox.toml` is owned by `rustbox:rustbox`, mode `0640`, and packaged as a non-replacing configuration file so package upgrades preserve local changes.
### systemd
## systemd
Jednostka używa:
The service uses the dedicated `rustbox` user and hardened systemd settings:
```ini
User=rustbox
@@ -108,19 +116,13 @@ ProtectHome=true
NoNewPrivileges=true
```
Logi tracing RustBox trafiają do `/var/log/rustbox/rustbox.log`. `StandardOutput=null`; `StandardError=journal` zostaje tylko dla błędów startowych, które wystąpią zanim aplikacja uruchomi własny logger.
RustBox tracing logs are written to `/var/log/rustbox/rustbox.log`. `StandardOutput=null` is used for normal output. `StandardError=journal` remains available for startup failures that happen before the application logger is initialized.
### logrotate
## logrotate
`/etc/logrotate.d/rustbox`:
`/etc/logrotate.d/rustbox` rotates `/var/log/rustbox/rustbox.log` daily, keeps 7 rotations, compresses old files, and recreates the active log with `rustbox:rustbox` ownership.
```text
/var/log/rustbox/rustbox.log
```
jest obracany codziennie i przechowywany przez 7 rotacji, z kompresją i właścicielem `rustbox:rustbox`.
## Domyślna konfiguracja pakietu
## Default RustBox configuration
```toml
host = "0.0.0.0"
@@ -156,51 +158,58 @@ pgp_max_key_bytes = 1048576
pgp_allow_private_export = true
```
## Repo APT
Panel pokazuje gotowe polecenia. Ręcznie:
## Debian / Ubuntu
```bash
curl -fsSL https://repo.example.com/repo-key.gpg \
curl -fsSL http://repo.example.com:18965/repo-key.gpg \
| sudo gpg --dearmor -o /usr/share/keyrings/rustbox.gpg
echo "deb [signed-by=/usr/share/keyrings/rustbox.gpg] https://repo.example.com/apt stable main" \
echo "deb [signed-by=/usr/share/keyrings/rustbox.gpg] http://repo.example.com:18965/apt stable main" \
| sudo tee /etc/apt/sources.list.d/rustbox.list
sudo apt update
sudo apt install rustbox
```
## Repo DNF/YUM
## RHEL / AlmaLinux / Rocky Linux
```bash
sudo rpm --import https://repo.example.com/repo-key.gpg
sudo rpm --import http://repo.example.com:18965/repo-key.gpg
sudo tee /etc/yum.repos.d/rustbox.repo >/dev/null <<'REPO'
[rustbox]
name=RustBox Repository
baseurl=https://repo.example.com/rpm/$basearch
name=RustBox Package Repository
baseurl=http://repo.example.com:18965/rpm/$basearch
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://repo.example.com/repo-key.gpg
gpgkey=http://repo.example.com:18965/repo-key.gpg
REPO
sudo dnf install rustbox
```
## Repo SUSE/Zypper
## SUSE / openSUSE
```bash
sudo rpm --import https://repo.example.com/repo-key.gpg
sudo zypper addrepo --refresh --gpgcheck https://repo.example.com/suse/x86_64/ rustbox
sudo rpm --import http://repo.example.com:18965/repo-key.gpg
sudo zypper addrepo --refresh --gpgcheck http://repo.example.com:18965/suse/x86_64/ rustbox
sudo zypper refresh rustbox
sudo zypper install rustbox
```
## Reverse proxy
## Repository paths
Nginx buildera nasłuchuje lokalnie na `HTTP_PORT` (domyślnie `18765`). Dla publicznego HTTPS zalecany jest zewnętrzny reverse proxy kierujący na ten port.
```text
/apt/ Debian / Ubuntu APT repository
/rpm/x86_64/ RHEL / AlmaLinux / Rocky Linux RPM repository
/suse/x86_64/ SUSE / openSUSE RPM repository
/repo-key.gpg public package signing key
/DEBIAN-BUILD-INFO.txt Debian / Ubuntu build metadata
/RHEL-BUILD-INFO.txt RHEL-family build metadata
/SUSE-BUILD-INFO.txt SUSE-family build metadata
/api/v1/packages repository JSON API
```
## Logi buildera
## Builder logs
```bash
docker compose --env-file .env logs -f rustbox-package-builder
@@ -208,11 +217,14 @@ docker compose --env-file .env logs -f repository-http
docker compose --env-file .env logs -f repository-dashboard
```
## Licencja paczki
## Runtime package dependencies
Dostarczone źródła RustBox nie zawierają autorytatywnego pliku `LICENSE`; `NOTICE.md` zaleca weryfikację licencji upstream przed publiczną redystrybucją. Dlatego builder ustawia obecnie metadane pakietu na `UNKNOWN`, zamiast zgadywać licencję. Przed publicznym repozytorium warto ustalić właściwe oznaczenie i wtedy zmienić je w `scripts/prepare-package.sh`.
Generated packages declare the system OpenSSL and GnuPG dependencies used by RustBox:
## Zależności runtime
- Debian / Ubuntu: `libssl3` or `libssl3t64`, plus `gnupg`
- RHEL / AlmaLinux / Rocky Linux: `openssl-libs` and `gnupg2`
- SUSE / openSUSE: `libopenssl3` and `gpg2`
Pakiety deklarują jawnie systemowy OpenSSL i GnuPG używane przez RustBox. Dla APT są to `libssl3`/`libssl3t64` i `gnupg`, dla RHEL/Rocky `openssl-libs` i `gnupg2`, a dla SUSE `libopenssl3` i `gpg2`.
## Package license metadata
The supplied RustBox source archive does not contain an authoritative `LICENSE` file. The builder therefore keeps package license metadata as `UNKNOWN` instead of guessing. Set the correct license in `scripts/prepare-package.sh` before public redistribution once the upstream license has been confirmed.
+7 -7
View File
@@ -18,9 +18,9 @@ REPOSITORY = Path(os.environ.get("REPOSITORY_ROOT", "/srv/repository"))
WEB = Path(__file__).resolve().parent / "web"
PORT = int(os.environ.get("DASHBOARD_PORT", "8000"))
CACHE_SECONDS = max(1, int(os.environ.get("DASHBOARD_CACHE_SECONDS", "15")))
REPOSITORY_URL = os.environ.get("REPOSITORY_URL", "http://localhost:18765").rstrip("/")
SITES = ("linux", "suse")
META_PATHS = {"linux": Path("LINUX-BUILD-INFO.txt"), "suse": Path("SUSE-BUILD-INFO.txt")}
REPOSITORY_URL = os.environ.get("REPOSITORY_URL", "http://localhost:18965").rstrip("/")
SITES = ("debian", "rhel", "suse")
META_PATHS = {"debian": Path("DEBIAN-BUILD-INFO.txt"), "rhel": Path("RHEL-BUILD-INFO.txt"), "suse": Path("SUSE-BUILD-INFO.txt")}
DEB_FILENAME = re.compile(r"^(?P<name>.+?)_(?P<version>[^_]+)_(?P<arch>[^_]+)\.deb$")
RPM_FILENAME = re.compile(r"^(?P<name>.+)-(?P<version>\d[^-]*)-(?P<release>[^/]+)\.(?P<arch>x86_64|amd64|aarch64|noarch|arm64)\.rpm$")
@@ -88,8 +88,8 @@ def generate_catalog(repository: Path = REPOSITORY) -> dict[str, object]:
packages: list[dict[str, object]] = []
if current:
scans = [
("linux", "DEB", "deb", current / "apt" / "pool", "*.deb"),
("linux", "RPM", "rpm", current / "rpm" / "x86_64", "*.rpm"),
("debian", "DEB", "deb", current / "apt" / "pool", "*.deb"),
("rhel", "RPM", "rpm", current / "rpm" / "x86_64", "*.rpm"),
("suse", "RPM", "rpm", current / "suse" / "x86_64", "*.rpm"),
]
for channel, label, kind, directory, pattern in scans:
@@ -161,9 +161,9 @@ class Handler(BaseHTTPRequestHandler):
self.send_bytes(json.dumps(CACHE.get(), ensure_ascii=False, separators=(",", ":")).encode(), "application/json; charset=utf-8", "no-store")
elif path in ("/", "/index.html"):
self.send_bytes((WEB / "index.html").read_bytes(), "text/html; charset=utf-8", "no-store", True)
elif path in ("/ui/style.css", "/ui/app.js", "/ui/favicon.svg"):
elif path in ("/ui/style.css", "/ui/app.js"):
name = path.rsplit("/", 1)[-1]
mime = {"style.css": "text/css", "app.js": "application/javascript", "favicon.svg": "image/svg+xml"}[name]
mime = {"style.css": "text/css", "app.js": "application/javascript"}[name]
self.send_bytes((WEB / name).read_bytes(), f"{mime}; charset=utf-8", "public, max-age=600")
else:
self.send_error(HTTPStatus.NOT_FOUND)
+10 -9
View File
@@ -2,24 +2,25 @@
const $ = (id) => document.getElementById(id);
const esc = (value) => String(value ?? '').replace(/[&<>"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const state = {data:null, view:'overview', install:'apt', search:'', channel:'all', format:'all'};
const titles = {overview:'Przegląd',packages:'Paczki',install:'Instalacja',repositories:'Repozytoria'};
const fmtDate = (value) => value ? new Intl.DateTimeFormat('pl-PL',{dateStyle:'medium',timeStyle:'short'}).format(new Date(value)) : '—';
const titles = {overview:'Overview',packages:'Packages',install:'Installation',repositories:'Repositories'};
const channelNames = {debian:'Debian / Ubuntu',rhel:'RHEL / AlmaLinux / Rocky Linux',suse:'SUSE / openSUSE'};
const fmtDate = (value) => value ? new Intl.DateTimeFormat('en-GB',{dateStyle:'medium',timeStyle:'short'}).format(new Date(value)) : '-';
const fmtSize = (n) => n < 1024 ? `${n} B` : n < 1048576 ? `${(n/1024).toFixed(1)} KiB` : `${(n/1048576).toFixed(1)} MiB`;
function toast(text){const t=$('toast');t.textContent=text;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),1600)}
function navigate(view){state.view=view;document.querySelectorAll('.view').forEach(x=>x.classList.toggle('active',x.id===view));document.querySelectorAll('.nav').forEach(x=>x.classList.toggle('active',x.dataset.view===view));$('view-title').textContent=titles[view]||view;location.hash=view}
function packageRow(p){return `<div class="package-row"><div><div class="pkg-name">${esc(p.name)}</div><div class="muted">${esc(p.version)}</div></div><span class="pill">${esc(p.channel.toUpperCase())}</span><span class="pill">${esc(p.format)}</span><span class="muted">${esc(fmtSize(p.size_bytes))}</span><a class="download" href="${esc(p.download_url)}">Pobierz</a></div>`}
function renderOverview(){const d=state.data;if(!d)return;const linux=d.builds.linux||{};const suse=d.builds.suse||{};$('latest-version').textContent=linux.app_version||'—';$('latest-build').textContent=fmtDate(linux.built_at);$('packages-count').textContent=d.summary.packages_count;$('linux-status').textContent=linux.published?'GOTOWE':'BRAK';$('suse-status').textContent=suse.published?'GOTOWE':'BRAK';$('recent-packages').innerHTML=d.packages.slice(0,6).map(packageRow).join('')||'<div class="empty">Brak opublikowanych paczek.</div>'}
function renderPackages(){if(!state.data)return;const q=state.search.toLowerCase();const list=state.data.packages.filter(p=>(state.channel==='all'||p.channel===state.channel)&&(state.format==='all'||p.format===state.format)&&(!q||`${p.name} ${p.version} ${p.architecture}`.toLowerCase().includes(q)));$('packages-table').innerHTML=list.map(p=>`<div class="table-row"><div><div class="pkg-name">${esc(p.name)}</div><div class="muted">${esc(p.version)}</div></div><span class="pill">${esc(p.channel.toUpperCase())}</span><span class="pill">${esc(p.format)}</span><span class="muted">${esc(fmtDate(p.built_at))}</span><a class="download" href="${esc(p.download_url)}">Pobierz</a></div>`).join('')||'<div class="empty">Brak wyników.</div>'}
function instruction(kind){const d=state.data;const url=d.repository_url,dist=d.apt_distribution,comp=d.apt_component;if(kind==='dnf')return {title:'RHEL / Rocky / Alma / Fedora',desc:'Instalacja przez podpisane repozytorium DNF/YUM.',code:`sudo rpm --import ${url}/repo-key.gpg\n\nsudo tee /etc/yum.repos.d/rustbox.repo >/dev/null <<'REPO'\n[rustbox]\nname=RustBox Repository\nbaseurl=${url}/rpm/$basearch\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=${url}/repo-key.gpg\nREPO\n\nsudo dnf install rustbox`};if(kind==='suse')return {title:'SUSE / openSUSE',desc:'Instalacja przez podpisane repozytorium Zypper.',code:`sudo rpm --import ${url}/repo-key.gpg\nsudo zypper addrepo --refresh --gpgcheck ${url}/suse/x86_64/ rustbox\nsudo zypper refresh rustbox\nsudo zypper install rustbox`};return {title:'Debian / Ubuntu',desc:'Instalacja przez podpisane repozytorium APT.',code:`curl -fsSL ${url}/repo-key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/rustbox.gpg\n\necho "deb [signed-by=/usr/share/keyrings/rustbox.gpg] ${url}/apt ${dist} ${comp}" | sudo tee /etc/apt/sources.list.d/rustbox.list\n\nsudo apt update\nsudo apt install rustbox`}}
function packageRow(p){return `<div class="package-row"><div><div class="pkg-name">${esc(p.name)}</div><div class="muted">${esc(p.version)}</div></div><span class="pill">${esc(channelNames[p.channel]||p.channel)}</span><span class="pill">${esc(p.format)}</span><span class="muted">${esc(fmtSize(p.size_bytes))}</span><a class="download" href="${esc(p.download_url)}">Download</a></div>`}
function renderOverview(){const d=state.data;if(!d)return;const debian=d.builds.debian||{},rhel=d.builds.rhel||{},suse=d.builds.suse||{};const latest=[debian,rhel,suse].find(x=>x.app_version)||{};$('latest-version').textContent=latest.app_version||'-';$('latest-build').textContent=fmtDate(latest.built_at);$('debian-status').textContent=debian.published?'READY':'NOT BUILT';$('rhel-status').textContent=rhel.published?'READY':'NOT BUILT';$('suse-status').textContent=suse.published?'READY':'NOT BUILT';$('recent-packages').innerHTML=d.packages.slice(0,6).map(packageRow).join('')||'<div class="empty">No packages have been published yet.</div>'}
function renderPackages(){if(!state.data)return;const q=state.search.toLowerCase();const list=state.data.packages.filter(p=>(state.channel==='all'||p.channel===state.channel)&&(state.format==='all'||p.format===state.format)&&(!q||`${p.name} ${p.version} ${p.architecture}`.toLowerCase().includes(q)));$('packages-table').innerHTML=list.map(p=>`<div class="table-row"><div><div class="pkg-name">${esc(p.name)}</div><div class="muted">${esc(p.version)}</div></div><span class="pill">${esc(channelNames[p.channel]||p.channel)}</span><span class="pill">${esc(p.format)}</span><span class="muted">${esc(fmtDate(p.built_at))}</span><a class="download" href="${esc(p.download_url)}">Download</a></div>`).join('')||'<div class="empty">No matching packages.</div>'}
function instruction(kind){const d=state.data;const url=d.repository_url,dist=d.apt_distribution,comp=d.apt_component;if(kind==='dnf')return {title:'RHEL / AlmaLinux / Rocky Linux',desc:'Install from the signed DNF repository.',code:`sudo rpm --import ${url}/repo-key.gpg\n\nsudo tee /etc/yum.repos.d/rustbox.repo >/dev/null <<'REPO'\n[rustbox]\nname=RustBox Package Repository\nbaseurl=${url}/rpm/$basearch\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=${url}/repo-key.gpg\nREPO\n\nsudo dnf install rustbox`};if(kind==='suse')return {title:'SUSE / openSUSE',desc:'Install from the signed Zypper repository.',code:`sudo rpm --import ${url}/repo-key.gpg\nsudo zypper addrepo --refresh --gpgcheck ${url}/suse/x86_64/ rustbox\nsudo zypper refresh rustbox\nsudo zypper install rustbox`};return {title:'Debian / Ubuntu',desc:'Install from the signed APT repository.',code:`curl -fsSL ${url}/repo-key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/rustbox.gpg\n\necho "deb [signed-by=/usr/share/keyrings/rustbox.gpg] ${url}/apt ${dist} ${comp}" | sudo tee /etc/apt/sources.list.d/rustbox.list\n\nsudo apt update\nsudo apt install rustbox`}}
function renderInstall(){if(!state.data)return;const x=instruction(state.install);$('install-title').textContent=x.title;$('install-desc').textContent=x.desc;$('install-command').textContent=x.code;document.querySelectorAll('[data-install]').forEach(b=>b.classList.toggle('active',b.dataset.install===state.install))}
function repoCard(title,url,links){return `<article class="repo-card"><h3>${esc(title)}</h3><a href="${esc(url)}">${esc(url)}</a><div class="repo-links">${links.map(([n,u])=>`<a href="${esc(u)}"><span>${esc(n)}</span></a>`).join('')}</div></article>`}
function renderRepositories(){const d=state.data;if(!d)return;const u=d.repository_url,dist=encodeURIComponent(d.apt_distribution);$('repo-cards').innerHTML=repoCard('APT + DNF',u,[['GPG public key',`${u}/repo-key.gpg`],['APT InRelease',`${u}/apt/dists/${dist}/InRelease`],['RPM repomd.xml',`${u}/rpm/x86_64/repodata/repomd.xml`],['Build info',`${u}/LINUX-BUILD-INFO.txt`]])+repoCard('SUSE / Zypper',`${u}/suse/x86_64`,[['RPM repomd.xml',`${u}/suse/x86_64/repodata/repomd.xml`],['Metadata signature',`${u}/suse/x86_64/repodata/repomd.xml.asc`],['Build info',`${u}/SUSE-BUILD-INFO.txt`]]);$('metadata').innerHTML=['linux','suse'].map(ch=>{const b=d.builds[ch]||{};return `<div class="meta-block"><h3>${ch==='linux'?'Linux':'SUSE'}</h3>${[['Wersja',b.app_version],['Wydanie paczki',b.package_release],['Build',fmtDate(b.built_at)],['Commit',b.source_commit?b.source_commit.slice(0,12):'—'],['Rewizja pakowania',b.packaging_revision]].map(([a,v])=>`<div class="meta-row"><span>${esc(a)}</span><strong>${esc(v||'—')}</strong></div>`).join('')}</div>`}).join('')}
function renderRepositories(){const d=state.data;if(!d)return;const u=d.repository_url,dist=encodeURIComponent(d.apt_distribution);$('repo-cards').innerHTML=repoCard('Debian / Ubuntu',`${u}/apt`,[['GPG public key',`${u}/repo-key.gpg`],['APT InRelease',`${u}/apt/dists/${dist}/InRelease`],['Build info',`${u}/DEBIAN-BUILD-INFO.txt`]])+repoCard('RHEL / AlmaLinux / Rocky Linux',`${u}/rpm/x86_64`,[['GPG public key',`${u}/repo-key.gpg`],['RPM repomd.xml',`${u}/rpm/x86_64/repodata/repomd.xml`],['Metadata signature',`${u}/rpm/x86_64/repodata/repomd.xml.asc`],['Build info',`${u}/RHEL-BUILD-INFO.txt`]])+repoCard('SUSE / openSUSE',`${u}/suse/x86_64`,[['GPG public key',`${u}/repo-key.gpg`],['RPM repomd.xml',`${u}/suse/x86_64/repodata/repomd.xml`],['Metadata signature',`${u}/suse/x86_64/repodata/repomd.xml.asc`],['Build info',`${u}/SUSE-BUILD-INFO.txt`]]);$('metadata').innerHTML=['debian','rhel','suse'].map(ch=>{const b=d.builds[ch]||{};return `<div class="meta-block"><h3>${esc(channelNames[ch])}</h3>${[['Version',b.app_version],['Package release',b.package_release],['Build time',fmtDate(b.built_at)],['Commit',b.source_commit?b.source_commit.slice(0,12):'-'],['Packaging revision',b.packaging_revision]].map(([a,v])=>`<div class="meta-row"><span>${esc(a)}</span><strong>${esc(v||'-')}</strong></div>`).join('')}</div>`}).join('')}
function renderAll(){renderOverview();renderPackages();renderInstall();renderRepositories()}
async function refresh(){const btn=$('refresh');btn.disabled=true;try{const r=await fetch('/api/v1/packages',{cache:'no-store'});if(!r.ok)throw new Error(`HTTP ${r.status}`);state.data=await r.json();renderAll();$('last-sync').textContent=`Ostatnia synchronizacja: ${fmtDate(state.data.generated_at)}`;$('status-dot').classList.add('ok');$('status-text').textContent=state.data.ready?'Repozytorium aktywne':'Oczekiwanie na build'}catch(e){$('status-dot').classList.remove('ok');$('status-text').textContent='Błąd odczytu repozytorium';console.error(e)}finally{btn.disabled=false}}
async function refresh(){const btn=$('refresh');btn.disabled=true;try{const r=await fetch('/api/v1/packages',{cache:'no-store'});if(!r.ok)throw new Error(`HTTP ${r.status}`);state.data=await r.json();renderAll();$('last-sync').textContent=`Last refresh: ${fmtDate(state.data.generated_at)}`;$('status-text').textContent=state.data.ready?'Repository ready':'Waiting for first build'}catch(e){$('status-text').textContent='Repository unavailable';console.error(e)}finally{btn.disabled=false}}
function setTheme(theme){document.documentElement.dataset.theme=theme;localStorage.setItem('rustbox-repo-theme',theme)}
document.querySelectorAll('[data-view]').forEach(b=>b.addEventListener('click',()=>navigate(b.dataset.view)));
document.querySelectorAll('[data-go]').forEach(b=>b.addEventListener('click',()=>navigate(b.dataset.go)));
document.querySelectorAll('[data-install]').forEach(b=>b.addEventListener('click',()=>{state.install=b.dataset.install;renderInstall()}));
$('search').addEventListener('input',e=>{state.search=e.target.value;renderPackages()});$('channel').addEventListener('change',e=>{state.channel=e.target.value;renderPackages()});$('format').addEventListener('change',e=>{state.format=e.target.value;renderPackages()});$('refresh').addEventListener('click',refresh);$('copy-install').addEventListener('click',async()=>{try{await navigator.clipboard.writeText($('install-command').textContent);toast('Skopiowano')}catch{toast('Nie udało się skopiować')}});$('theme-toggle').addEventListener('click',()=>setTheme(document.documentElement.dataset.theme==='dark'?'light':'dark'));
$('search').addEventListener('input',e=>{state.search=e.target.value;renderPackages()});$('channel').addEventListener('change',e=>{state.channel=e.target.value;renderPackages()});$('format').addEventListener('change',e=>{state.format=e.target.value;renderPackages()});$('refresh').addEventListener('click',refresh);$('copy-install').addEventListener('click',async()=>{try{await navigator.clipboard.writeText($('install-command').textContent);toast('Copied')}catch{toast('Copy failed')}});$('theme-toggle').addEventListener('click',()=>setTheme(document.documentElement.dataset.theme==='dark'?'light':'dark'));
try{setTheme(localStorage.getItem('rustbox-repo-theme')||'dark')}catch{}navigate(location.hash.slice(1)||'overview');window.addEventListener('hashchange',()=>navigate(location.hash.slice(1)||'overview'));refresh();setInterval(()=>{if(!document.hidden)refresh()},60000);
})();
+21 -22
View File
@@ -1,60 +1,59 @@
<!doctype html>
<html lang="pl" data-theme="dark">
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="dark light">
<title>RustBox Repository</title>
<link rel="icon" href="/ui/favicon.svg">
<title>RustBox Package Repository</title>
<link rel="stylesheet" href="/ui/style.css">
</head>
<body>
<div class="shell">
<aside class="sidebar">
<div class="brand"><div class="brand-icon">R</div><div><strong>RustBox</strong><span>Package Repository</span></div></div>
<div class="brand"><div><strong>RustBox</strong><span>Package Repository</span></div></div>
<nav>
<button class="nav active" data-view="overview">Przegląd</button>
<button class="nav" data-view="packages">Paczki</button>
<button class="nav" data-view="install">Instalacja</button>
<button class="nav" data-view="repositories">Repozytoria</button>
<button class="nav active" data-view="overview">Overview</button>
<button class="nav" data-view="packages">Packages</button>
<button class="nav" data-view="install">Installation</button>
<button class="nav" data-view="repositories">Repositories</button>
</nav>
<div class="sidebar-foot"><span id="status-dot" class="status-dot"></span><span id="status-text">Ładowanie…</span></div>
<div class="sidebar-foot"><span id="status-text">Loading...</span></div>
</aside>
<main>
<header class="topbar">
<div><h1 id="view-title">Przegląd</h1><p id="last-sync">—</p></div>
<div class="toolbar"><button id="theme-toggle" title="Motyw">◐</button><button id="refresh" title="Odśwież">↻</button></div>
<div><h1 id="view-title">Overview</h1><p id="last-sync">-</p></div>
<div class="toolbar"><button id="theme-toggle" type="button">Theme</button><button id="refresh" type="button">Refresh</button></div>
</header>
<section id="overview" class="view active">
<div class="hero">
<div><span class="eyebrow">RUSTBOX / LINUX</span><h2>Repozytorium wydań</h2><p>Podpisane paczki APT, DNF/RPM i Zypper budowane bezpośrednio ze źródeł RustBox.</p></div>
<div class="version-box"><span>NAJNOWSZA WERSJA</span><strong id="latest-version">—</strong><small id="latest-build">—</small></div>
<div><span class="eyebrow">RUSTBOX PACKAGE REPOSITORY</span><h2>Linux packages</h2><p>Signed packages and repository metadata built directly from RustBox source code.</p></div>
<div class="version-box"><span>LATEST VERSION</span><strong id="latest-version">-</strong><small id="latest-build">-</small></div>
</div>
<div class="cards">
<article><span>Paczki</span><strong id="packages-count">0</strong><small>opublikowane artefakty</small></article>
<article><span>Linux</span><strong id="linux-status">—</strong><small>APT + DNF</small></article>
<article><span>SUSE</span><strong id="suse-status">—</strong><small>Zypper RPM</small></article>
<article><span>Debian / Ubuntu</span><strong id="debian-status">-</strong><small>APT / DEB</small></article>
<article><span>RHEL / AlmaLinux / Rocky Linux</span><strong id="rhel-status">-</strong><small>DNF / RPM</small></article>
<article><span>SUSE / openSUSE</span><strong id="suse-status">-</strong><small>Zypper / RPM</small></article>
</div>
<div class="panel"><div class="panel-head"><h3>Ostatnie paczki</h3><button data-go="packages">Pokaż wszystkie</button></div><div id="recent-packages" class="package-list"></div></div>
<div class="panel"><div class="panel-head"><h3>Recent packages</h3><button data-go="packages" type="button">View all</button></div><div id="recent-packages" class="package-list"></div></div>
</section>
<section id="packages" class="view">
<div class="panel">
<div class="filters"><input id="search" type="search" placeholder="Szukaj paczki lub wersji"><select id="channel"><option value="all">Wszystkie kanały</option><option value="linux">Linux</option><option value="suse">SUSE</option></select><select id="format"><option value="all">Wszystkie formaty</option><option value="DEB">DEB</option><option value="RPM">RPM</option></select></div>
<div class="filters"><input id="search" type="search" placeholder="Search package or version"><select id="channel"><option value="all">All distributions</option><option value="debian">Debian / Ubuntu</option><option value="rhel">RHEL / AlmaLinux / Rocky Linux</option><option value="suse">SUSE / openSUSE</option></select><select id="format"><option value="all">All formats</option><option value="DEB">DEB</option><option value="RPM">RPM</option></select></div>
<div id="packages-table" class="table"></div>
</div>
</section>
<section id="install" class="view">
<div class="install-tabs"><button class="active" data-install="apt">Debian / Ubuntu</button><button data-install="dnf">RHEL / Rocky / Alma / Fedora</button><button data-install="suse">SUSE / openSUSE</button></div>
<div class="panel command-panel"><div class="panel-head"><div><h3 id="install-title">Debian / Ubuntu</h3><p id="install-desc">Instalacja przez podpisane repozytorium APT.</p></div><button id="copy-install">Kopiuj</button></div><pre><code id="install-command"></code></pre></div>
<div class="note">Po instalacji konfiguracja znajduje się w <code>/etc/rustbox/rustbox.toml</code>, dane w <code>/var/lib/rustbox</code>, a logi aplikacji w <code>/var/log/rustbox/rustbox.log</code>.</div>
<div class="install-tabs"><button class="active" data-install="apt" type="button">Debian / Ubuntu</button><button data-install="dnf" type="button">RHEL / AlmaLinux / Rocky Linux</button><button data-install="suse" type="button">SUSE / openSUSE</button></div>
<div class="panel command-panel"><div class="panel-head"><div><h3 id="install-title">Debian / Ubuntu</h3><p id="install-desc">Install from the signed APT repository.</p></div><button id="copy-install" type="button">Copy</button></div><pre><code id="install-command"></code></pre></div>
<div class="note">After installation, configuration is stored in <code>/etc/rustbox/rustbox.toml</code>, application data in <code>/var/lib/rustbox</code>, and logs in <code>/var/log/rustbox/rustbox.log</code>.</div>
</section>
<section id="repositories" class="view">
<div id="repo-cards" class="repo-grid"></div>
<div class="panel"><div class="panel-head"><h3>Metadane buildu</h3></div><div id="metadata" class="metadata"></div></div>
<div class="panel"><div class="panel-head"><h3>Build metadata</h3></div><div id="metadata" class="metadata"></div></div>
</section>
</main>
</div>
+1 -1
View File
@@ -1,3 +1,3 @@
:root{color-scheme:dark;--bg:#121513;--panel:#181c19;--panel2:#1e231f;--panel3:#242a25;--sidebar:#101310;--line:#313832;--line2:#485149;--text:#edf0ec;--muted:#9da79f;--subtle:#7c877f;--accent:#8fb9a4;--accent2:#a4c9b6;--good:#a7c892;--field:#121713;--hover:#202621;--shadow:rgba(0,0,0,.32);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
html[data-theme="light"]{color-scheme:light;--bg:#f2f4f1;--panel:#fff;--panel2:#f7f8f6;--panel3:#edf1ec;--sidebar:#e9eee9;--line:#cbd2cb;--line2:#aeb8af;--text:#202622;--muted:#647068;--subtle:#7a847d;--accent:#507b67;--accent2:#416b58;--good:#587a49;--field:#fff;--hover:#e8ede8;--shadow:rgba(31,39,33,.16)}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-size:14px}.shell{min-height:100vh;display:grid;grid-template-columns:228px minmax(0,1fr)}.sidebar{position:sticky;top:0;height:100vh;background:var(--sidebar);border-right:1px solid var(--line);padding:18px 12px;display:flex;flex-direction:column}.brand{display:flex;gap:11px;align-items:center;padding:5px 8px 24px}.brand-icon{width:34px;height:34px;border-radius:9px;background:var(--accent);color:#102018;display:grid;place-items:center;font-weight:800;font-size:18px}.brand strong,.brand span{display:block}.brand span{color:var(--muted);font-size:11px;margin-top:2px}.sidebar nav{display:grid;gap:3px}.nav{border:0;background:transparent;color:var(--muted);text-align:left;padding:10px 12px;border-radius:5px;cursor:pointer;font-weight:600}.nav:hover{background:var(--hover);color:var(--text)}.nav.active{background:var(--panel3);color:var(--text);box-shadow:inset 2px 0 var(--accent)}.sidebar-foot{margin-top:auto;border-top:1px solid var(--line);padding:15px 8px 2px;color:var(--muted);display:flex;align-items:center;gap:8px;font-size:12px}.status-dot{width:8px;height:8px;border-radius:50%;background:var(--subtle)}.status-dot.ok{background:var(--good);box-shadow:0 0 0 3px color-mix(in srgb,var(--good) 15%,transparent)}main{min-width:0}.topbar{height:72px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 24px;background:color-mix(in srgb,var(--bg) 84%,transparent);backdrop-filter:blur(10px);position:sticky;top:0;z-index:5}.topbar h1{font-size:18px;margin:0}.topbar p{margin:3px 0 0;color:var(--subtle);font-size:11px}.toolbar{display:flex;gap:7px}.toolbar button,.panel button,.install-tabs button{border:1px solid var(--line);background:var(--panel);color:var(--text);min-height:32px;padding:0 11px;border-radius:5px;cursor:pointer}.toolbar button:hover,.panel button:hover,.install-tabs button:hover{border-color:var(--line2);background:var(--hover)}.view{display:none;padding:24px;max-width:1320px;margin:0 auto}.view.active{display:block}.hero{display:grid;grid-template-columns:minmax(0,1fr) 240px;gap:18px;background:linear-gradient(135deg,var(--panel),var(--panel2));border:1px solid var(--line);border-radius:7px;padding:28px}.hero h2{font-size:30px;margin:7px 0 8px}.hero p{color:var(--muted);max-width:700px;margin:0;line-height:1.55}.eyebrow{color:var(--accent2);font-size:11px;font-weight:800;letter-spacing:.14em}.version-box{background:var(--field);border:1px solid var(--line);border-radius:6px;padding:18px;display:flex;flex-direction:column;justify-content:center}.version-box span{color:var(--subtle);font-size:10px;letter-spacing:.1em}.version-box strong{font-size:27px;margin:5px 0}.version-box small{color:var(--muted)}.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:12px 0}.cards article{background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:17px}.cards span,.cards small{display:block;color:var(--muted)}.cards strong{display:block;font-size:22px;margin:5px 0}.panel{background:var(--panel);border:1px solid var(--line);border-radius:6px;margin-top:12px;overflow:hidden}.panel-head{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid var(--line)}.panel-head h3{margin:0;font-size:14px}.panel-head p{margin:4px 0 0;color:var(--muted)}.package-row,.table-row{display:grid;grid-template-columns:minmax(220px,1fr) 150px 90px 110px 150px;gap:12px;align-items:center;padding:12px 16px;border-bottom:1px solid var(--line)}.package-row:last-child,.table-row:last-child{border-bottom:0}.package-row:hover,.table-row:hover{background:var(--hover)}.pkg-name{font-weight:650;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.muted{color:var(--muted)}.pill{display:inline-flex;align-items:center;border:1px solid var(--line2);border-radius:99px;padding:3px 7px;font-size:10px;color:var(--muted);width:max-content}.download{color:var(--accent2);text-decoration:none;font-weight:650}.filters{display:grid;grid-template-columns:minmax(240px,1fr) 190px 170px;gap:8px;padding:12px;border-bottom:1px solid var(--line)}input,select{width:100%;height:34px;background:var(--field);color:var(--text);border:1px solid var(--line);border-radius:5px;padding:0 10px;outline:none}input:focus,select:focus{border-color:var(--accent)}.install-tabs{display:flex;gap:7px;flex-wrap:wrap}.install-tabs button.active{background:var(--accent);border-color:var(--accent);color:#102018;font-weight:700}.command-panel pre{margin:0;background:#0c0f0d;color:#e8efe9;padding:18px;overflow:auto;font:12px/1.6 ui-monospace,SFMono-Regular,Consolas,monospace}.note{margin-top:12px;border:1px solid var(--line);background:var(--panel2);padding:14px;border-radius:6px;color:var(--muted)}code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}.repo-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}.repo-card{background:var(--panel);bordLine truncated
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-size:14px}.shell{min-height:100vh;display:grid;grid-template-columns:228px minmax(0,1fr)}.sidebar{position:sticky;top:0;height:100vh;background:var(--sidebar);border-right:1px solid var(--line);padding:18px 12px;display:flex;flex-direction:column}.brand{display:flex;align-items:center;padding:7px 8px 24px}.brand strong,.brand span{display:block}.brand span{color:var(--muted);font-size:11px;margin-top:2px}.sidebar nav{display:grid;gap:3px}.nav{border:0;background:transparent;color:var(--muted);text-align:left;padding:10px 12px;border-radius:5px;cursor:pointer;font-weight:600}.nav:hover{background:var(--hover);color:var(--text)}.nav.active{background:var(--panel3);color:var(--text);box-shadow:inset 2px 0 var(--accent)}.sidebar-foot{margin-top:auto;border-top:1px solid var(--line);padding:15px 8px 2px;color:var(--muted);display:flex;align-items:center;gap:8px;font-size:12px}main{min-width:0}.topbar{height:72px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 24px;background:color-mix(in srgb,var(--bg) 84%,transparent);backdrop-filter:blur(10px);position:sticky;top:0;z-index:5}.topbar h1{font-size:18px;margin:0}.topbar p{margin:3px 0 0;color:var(--subtle);font-size:11px}.toolbar{display:flex;gap:7px}.toolbar button,.panel button,.install-tabs button{border:1px solid var(--line);background:var(--panel);color:var(--text);min-height:32px;padding:0 11px;border-radius:5px;cursor:pointer}.toolbar button:hover,.panel button:hover,.install-tabs button:hover{border-color:var(--line2);background:var(--hover)}.view{display:none;padding:24px;max-width:1320px;margin:0 auto}.view.active{display:block}.hero{display:grid;grid-template-columns:minmax(0,1fr) 240px;gap:18px;background:linear-gradient(135deg,var(--panel),var(--panel2));border:1px solid var(--line);border-radius:7px;padding:28px}.hero h2{font-size:30px;margin:7px 0 8px}.hero p{color:var(--muted);max-width:700px;margin:0;line-height:1.55}.eyebrow{color:var(--accent2);font-size:11px;font-weight:800;letter-spacing:.14em}.version-box{background:var(--field);border:1px solid var(--line);border-radius:6px;padding:18px;display:flex;flex-direction:column;justify-content:center}.version-box span{color:var(--subtle);font-size:10px;letter-spacing:.1em}.version-box strong{font-size:27px;margin:5px 0}.version-box small{color:var(--muted)}.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:12px 0}.cards article{background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:17px}.cards span,.cards small{display:block;color:var(--muted)}.cards strong{display:block;font-size:22px;margin:5px 0}.panel{background:var(--panel);border:1px solid var(--line);border-radius:6px;margin-top:12px;overflow:hidden}.panel-head{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid var(--line)}.panel-head h3{margin:0;font-size:14px}.panel-head p{margin:4px 0 0;color:var(--muted)}.package-row,.table-row{display:grid;grid-template-columns:minmax(220px,1fr) 150px 90px 110px 150px;gap:12px;align-items:center;padding:12px 16px;border-bottom:1px solid var(--line)}.package-row:last-child,.table-row:last-child{border-bottom:0}.package-row:hover,.table-row:hover{background:var(--hover)}.pkg-name{font-weight:650;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.muted{color:var(--muted)}.pill{display:inline-flex;align-items:center;border:1px solid var(--line2);border-radius:99px;padding:3px 7px;font-size:10px;color:var(--muted);width:max-content}.download{color:var(--accent2);text-decoration:none;font-weight:650}.filters{display:grid;grid-template-columns:minmax(240px,1fr) 190px 170px;gap:8px;padding:12px;border-bottom:1px solid var(--line)}input,select{width:100%;height:34px;background:var(--field);color:var(--text);border:1px solid var(--line);border-radius:5px;padding:0 10px;outline:none}input:focus,select:focus{border-color:var(--accent)}.install-tabs{display:flex;gap:7px;flex-wrap:wrap}.install-tabs button.active{background:var(--accent);border-color:var(--accent);color:#102018;font-weight:700}.command-panel pre{margin:0;background:#0c0f0d;color:#e8efe9;padding:18px;overflow:auto;font:12px/1.6 ui-monospace,SFMono-Regular,Consolas,monospace}.note{margin-top:12px;border:1px solid var(--line);background:var(--panel2);padding:14px;border-radius:6px;color:var(--muted)}code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}.repo-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}.repo-card{background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:16px}.repo-card h3{margin:0 0 8px}.repo-card>a{color:var(--accent2);word-break:break-all}.repo-links{display:grid;margin-top:12px;border-top:1px solid var(--line)}.repo-links a{padding:9px 0;color:var(--muted);text-decoration:none;border-bottom:1px solid var(--line)}.metadata{display:grid;grid-Line truncated
-1
View File
@@ -13,7 +13,6 @@ services:
PACKAGE_RELEASE: "${PACKAGE_RELEASE:-auto}"
RUN_LINUX_TESTS: "${RUN_LINUX_TESTS:-compile}"
MAX_GLIBC_VERSION: "${MAX_GLIBC_VERSION:-2.34}"
BUILD_SUSE: "${BUILD_SUSE:-1}"
RELEASE_RETENTION: "${RELEASE_RETENTION:-10}"
volumes:
- ./data:/var/lib/rustbox-package-builder
+12 -16
View File
@@ -68,17 +68,15 @@ RPM="$OUT/rustbox-${VERSION}-${RELEASE}.x86_64.rpm"
SUSE="$OUT/rustbox-${VERSION}-${RELEASE}.suse.x86_64.rpm"
nfpm package --config "$PKG/nfpm-deb.yaml" --packager deb --target "$DEB"
nfpm package --config "$PKG/nfpm-rpm.yaml" --packager rpm --target "$RPM"
if [[ "${BUILD_SUSE:-1}" == 1 ]]; then
nfpm package --config "$PKG/nfpm-suse.yaml" --packager rpm --target "$SUSE"
fi
ar t "$DEB" | grep '^_gpgorigin$' >/dev/null || fatal "DEB package is not signed"
rpm -qpi "$RPM" | awk -F: '/^Signature/ && $0 !~ /\(none\)/ {ok=1} END{exit !ok}' || fatal "RPM package is not signed"
if [[ -f "$SUSE" ]]; then rpm -qpi "$SUSE" | awk -F: '/^Signature/ && $0 !~ /\(none\)/ {ok=1} END{exit !ok}' || fatal "SUSE RPM package is not signed"; fi
rpm -qpi "$SUSE" | awk -F: '/^Signature/ && $0 !~ /\(none\)/ {ok=1} END{exit !ok}' || fatal "SUSE RPM package is not signed"
cp -f "$DEB" "$DATA_DIR/packages/deb/"
cp -f "$RPM" "$DATA_DIR/packages/rpm/x86_64/"
[[ -f "$SUSE" ]] && cp -f "$SUSE" "$DATA_DIR/packages/suse/x86_64/"
cp -f "$SUSE" "$DATA_DIR/packages/suse/x86_64/"
aptly -config="$APTLY_CONF" repo add -force-replace rustbox "$DEB" >/dev/null
SNAPSHOT="rustbox-${VERSION}-${SHORT_SHA}-$(date +%s)"
@@ -97,25 +95,27 @@ RELEASE_DIR="$REPOSITORY/releases/$RELEASE_ID"
mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/suse/x86_64"
rsync -a --delete "$APT_PUBLIC/" "$RELEASE_DIR/apt/"
rsync -a --delete "$DATA_DIR/packages/rpm/x86_64/" "$RELEASE_DIR/rpm/x86_64/"
if [[ "${BUILD_SUSE:-1}" == 1 ]]; then rsync -a --delete "$DATA_DIR/packages/suse/x86_64/" "$RELEASE_DIR/suse/x86_64/"; fi
rsync -a --delete "$DATA_DIR/packages/suse/x86_64/" "$RELEASE_DIR/suse/x86_64/"
for rpm_root in "$RELEASE_DIR/rpm/x86_64"; do
rm -rf "$rpm_root/repodata"; createrepo_c --update "$rpm_root" >/dev/null
done
if [[ "${BUILD_SUSE:-1}" == 1 ]]; then
rm -rf "$RELEASE_DIR/suse/x86_64/repodata"; createrepo_c --update "$RELEASE_DIR/suse/x86_64" >/dev/null
fi
GPG_SIGN=(--batch --yes --pinentry-mode loopback)
for rpm_root in "$RELEASE_DIR/rpm/x86_64"; do
gpg "${GPG_SIGN[@]}" --armor --detach-sign --local-user "$GPG_KEY_ID" --output "$rpm_root/repodata/repomd.xml.asc" "$rpm_root/repodata/repomd.xml"
done
if [[ "${BUILD_SUSE:-1}" == 1 ]]; then
gpg "${GPG_SIGN[@]}" --armor --detach-sign --local-user "$GPG_KEY_ID" --output "$RELEASE_DIR/suse/x86_64/repodata/repomd.xml.asc" "$RELEASE_DIR/suse/x86_64/repodata/repomd.xml"
fi
gpg --batch --armor --export "$GPG_KEY_ID" > "$RELEASE_DIR/repo-key.gpg"
BUILT_AT=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
cat > "$RELEASE_DIR/LINUX-BUILD-INFO.txt" <<INFO
for family in debian rhel suse; do
case "$family" in
debian) info_file="$RELEASE_DIR/DEBIAN-BUILD-INFO.txt" ;;
rhel) info_file="$RELEASE_DIR/RHEL-BUILD-INFO.txt" ;;
suse) info_file="$RELEASE_DIR/SUSE-BUILD-INFO.txt" ;;
esac
cat > "$info_file" <<INFO
app_version=$VERSION
package_release=$RELEASE
built_at=$BUILT_AT
@@ -124,13 +124,9 @@ source_url=$SOURCE_REPO
source_ref=$SOURCE_BRANCH
source_commit=$SHA
build_reason=source-or-packaging-change
package_family=$family
INFO
if [[ "${BUILD_SUSE:-1}" == 1 ]]; then
cp "$RELEASE_DIR/LINUX-BUILD-INFO.txt" "$RELEASE_DIR/SUSE-BUILD-INFO.txt"
printf '%s\n' 'rpm_flavor=suse' >> "$RELEASE_DIR/SUSE-BUILD-INFO.txt"
else
: > "$RELEASE_DIR/SUSE-BUILD-INFO.txt"
fi
done
rm -f "$REPOSITORY/current.new"
ln -s "releases/$RELEASE_ID" "$REPOSITORY/current.new"
+3 -3
View File
@@ -13,9 +13,9 @@ git fetch --no-tags --prune origin "$SOURCE_BRANCH"
SHA=$(git rev-parse FETCH_HEAD)
CURRENT_SHA=""
CURRENT_REV=""
if [[ -r "$REPOSITORY/current/LINUX-BUILD-INFO.txt" ]]; then
CURRENT_SHA=$(sed -n 's/^source_commit=//p' "$REPOSITORY/current/LINUX-BUILD-INFO.txt" | head -n1)
CURRENT_REV=$(sed -n 's/^packaging_revision=//p' "$REPOSITORY/current/LINUX-BUILD-INFO.txt" | head -n1)
if [[ -r "$REPOSITORY/current/DEBIAN-BUILD-INFO.txt" ]]; then
CURRENT_SHA=$(sed -n 's/^source_commit=//p' "$REPOSITORY/current/DEBIAN-BUILD-INFO.txt" | head -n1)
CURRENT_REV=$(sed -n 's/^packaging_revision=//p' "$REPOSITORY/current/DEBIAN-BUILD-INFO.txt" | head -n1)
fi
if [[ "$CURRENT_SHA" == "$SHA" && "$CURRENT_REV" == "$PACKAGING_REVISION" ]]; then
log "no new RustBox build: commit=$SHA packaging_revision=$PACKAGING_REVISION"
+1 -2
View File
@@ -10,9 +10,8 @@ source /opt/rustbox-package-builder/scripts/lib.sh
: "${PACKAGE_RELEASE:=auto}"
: "${RUN_LINUX_TESTS:=compile}"
: "${MAX_GLIBC_VERSION:=2.34}"
: "${BUILD_SUSE:=1}"
: "${RELEASE_RETENTION:=10}"
export SOURCE_BRANCH POLL_INTERVAL REPO_DISTRIBUTION REPO_COMPONENT PACKAGE_RELEASE RUN_LINUX_TESTS MAX_GLIBC_VERSION BUILD_SUSE RELEASE_RETENTION
export SOURCE_BRANCH POLL_INTERVAL REPO_DISTRIBUTION REPO_COMPONENT PACKAGE_RELEASE RUN_LINUX_TESTS MAX_GLIBC_VERSION RELEASE_RETENTION
mkdir -p "$DATA_DIR"/{state,repository,work,cargo,packages} /tmp/rustbox-gnupg
chmod 0700 /tmp/rustbox-gnupg
export GNUPGHOME=/tmp/rustbox-gnupg
+3 -2
View File
@@ -21,12 +21,13 @@ export APTLY_CONF
if ! aptly -config="$APTLY_CONF" repo list -raw | grep -Fx rustbox >/dev/null; then
aptly -config="$APTLY_CONF" repo create -distribution="$REPO_DISTRIBUTION" -component="$REPO_COMPONENT" rustbox >/dev/null
fi
for item in apt rpm suse repo-key.gpg LINUX-BUILD-INFO.txt SUSE-BUILD-INFO.txt; do
for item in apt rpm suse repo-key.gpg DEBIAN-BUILD-INFO.txt RHEL-BUILD-INFO.txt SUSE-BUILD-INFO.txt; do
[[ ! -e "$REPOSITORY/$item" || -L "$REPOSITORY/$item" ]] || fatal "$REPOSITORY/$item exists and is not a symlink"
done
ln -sfn current/apt "$REPOSITORY/apt"
ln -sfn current/rpm "$REPOSITORY/rpm"
ln -sfn current/suse "$REPOSITORY/suse"
ln -sfn current/repo-key.gpg "$REPOSITORY/repo-key.gpg"
ln -sfn current/LINUX-BUILD-INFO.txt "$REPOSITORY/LINUX-BUILD-INFO.txt"
ln -sfn current/DEBIAN-BUILD-INFO.txt "$REPOSITORY/DEBIAN-BUILD-INFO.txt"
ln -sfn current/RHEL-BUILD-INFO.txt "$REPOSITORY/RHEL-BUILD-INFO.txt"
ln -sfn current/SUSE-BUILD-INFO.txt "$REPOSITORY/SUSE-BUILD-INFO.txt"
+1 -1
View File
@@ -6,7 +6,7 @@ cd "$ROOT"
ENV_FILE="$ROOT/.env"
DATA_DIR="$ROOT/data"
GPG_DIR="$ROOT/gpg"
DEFAULT_HTTP_PORT=18765
DEFAULT_HTTP_PORT=18965
log() { printf '[rustbox-package-builder] %s\n' "$*"; }
die() { printf '[rustbox-package-builder] ERROR: %s\n' "$*" >&2; exit 1; }
+19 -4
View File
@@ -15,20 +15,35 @@ def test_package_filename_parsing():
assert app.parse_package(Path("rustbox-0.0.5-1.x86_64.rpm"), "rpm") == ("rustbox", "0.0.5-1", "x86_64")
def test_catalog_reads_linux_snapshot():
def test_catalog_uses_three_linux_distribution_families():
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
rel = root / "releases/one"
(rel / "apt/pool/main/r/rustbox").mkdir(parents=True)
(rel / "rpm/x86_64").mkdir(parents=True)
(rel / "suse/x86_64").mkdir(parents=True)
(rel / "LINUX-BUILD-INFO.txt").write_text("app_version=0.0.5\npackage_release=1\nbuilt_at=2026-10-30T12:00:00Z\nsource_commit=abc\npackaging_revision=1\n")
(rel / "SUSE-BUILD-INFO.txt").write_text("app_version=0.0.5\npackage_release=1\nbuilt_at=2026-10-30T12:00:00Z\nsource_commit=abc\npackaging_revision=1\n")
common = "app_version=0.0.5\npackage_release=1\nbuilt_at=2026-10-30T12:00:00Z\nsource_commit=abc\npackaging_revision=1\n"
(rel / "DEBIAN-BUILD-INFO.txt").write_text(common + "package_family=debian\n")
(rel / "RHEL-BUILD-INFO.txt").write_text(common + "package_family=rhel\n")
(rel / "SUSE-BUILD-INFO.txt").write_text(common + "package_family=suse\n")
(rel / "apt/pool/main/r/rustbox/rustbox_0.0.5-1_amd64.deb").write_bytes(b"deb")
(rel / "rpm/x86_64/rustbox-0.0.5-1.x86_64.rpm").write_bytes(b"rpm")
(rel / "suse/x86_64/rustbox-0.0.5-1.x86_64.rpm").write_bytes(b"rpm")
(root / "current").symlink_to("releases/one")
catalog = app.generate_catalog(root)
assert catalog["ready"] is True
assert catalog["builds"]["linux"]["app_version"] == "0.0.5"
assert set(catalog["builds"]) == {"debian", "rhel", "suse"}
assert catalog["builds"]["debian"]["app_version"] == "0.0.5"
assert {pkg["channel"] for pkg in catalog["packages"]} == {"debian", "rhel", "suse"}
assert len(catalog["packages"]) == 3
def test_dashboard_is_english_text_only():
html = (ROOT / "dashboard/web/index.html").read_text()
js = (ROOT / "dashboard/web/app.js").read_text()
assert 'lang="en"' in html
assert "brand-icon" not in html
assert "favicon" not in html
assert "status-dot" not in html
for label in ("Overview", "Packages", "Installation", "Repositories", "Debian / Ubuntu", "RHEL / AlmaLinux / Rocky Linux", "SUSE / openSUSE"):
assert label in html or label in js
+21
View File
@@ -57,3 +57,24 @@ def test_docker_context_excludes_secrets_and_state():
assert ".env" in data
assert "gpg/" in data
assert "data/" in data
def test_repository_public_port_is_18965():
env = (ROOT / ".env.example").read_text()
compose = (ROOT / "docker-compose.yml").read_text()
start = (ROOT / "start.sh").read_text()
assert "HTTP_PORT=18965" in env
assert "127.0.0.1:18965" in env
assert "${HTTP_PORT:-18965}:8080" in compose
assert "DEFAULT_HTTP_PORT=18965" in start
def test_all_three_linux_families_are_always_published():
build = (ROOT / "scripts/build-release.sh").read_text()
init = (ROOT / "scripts/init-storage.sh").read_text()
compose = (ROOT / "docker-compose.yml").read_text()
assert "BUILD_SUSE" not in build
assert "BUILD_SUSE" not in compose
for name in ("DEBIAN-BUILD-INFO.txt", "RHEL-BUILD-INFO.txt", "SUSE-BUILD-INFO.txt"):
assert name in build
assert name in init