From c1a2a8cc30265eccc93154eb0360f5a369a44d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 30 Oct 2026 13:11:36 +0100 Subject: [PATCH] v2 --- README.md | 142 ++++++++++++++++++++----------------- dashboard/app.py | 14 ++-- dashboard/web/app.js | 19 ++--- dashboard/web/index.html | 43 ++++++----- dashboard/web/style.css | 2 +- docker-compose.yml | 3 +- scripts/build-release.sh | 34 ++++----- scripts/check-and-build.sh | 6 +- scripts/entrypoint.sh | 3 +- scripts/init-storage.sh | 5 +- start.sh | 2 +- tests/test_dashboard.py | 23 ++++-- tests/test_packaging.py | 21 ++++++ 13 files changed, 180 insertions(+), 137 deletions(-) diff --git a/README.md b/README.md index a779016..a7af650 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/dashboard/app.py b/dashboard/app.py index 49262d9..0c7908a 100644 --- a/dashboard/app.py +++ b/dashboard/app.py @@ -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.+?)_(?P[^_]+)_(?P[^_]+)\.deb$") RPM_FILENAME = re.compile(r"^(?P.+)-(?P\d[^-]*)-(?P[^/]+)\.(?Px86_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) diff --git a/dashboard/web/app.js b/dashboard/web/app.js index f7fe4c3..ddd1a93 100644 --- a/dashboard/web/app.js +++ b/dashboard/web/app.js @@ -2,24 +2,25 @@ const $ = (id) => document.getElementById(id); const esc = (value) => String(value ?? '').replace(/[&<>"']/g, (c) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 `
${esc(p.name)}
${esc(p.version)}
${esc(p.channel.toUpperCase())}${esc(p.format)}${esc(fmtSize(p.size_bytes))}Pobierz
`} - 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('')||'
Brak opublikowanych paczek.
'} - 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=>`
${esc(p.name)}
${esc(p.version)}
${esc(p.channel.toUpperCase())}${esc(p.format)}${esc(fmtDate(p.built_at))}Pobierz
`).join('')||'
Brak wyników.
'} - 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 `
${esc(p.name)}
${esc(p.version)}
${esc(channelNames[p.channel]||p.channel)}${esc(p.format)}${esc(fmtSize(p.size_bytes))}Download
`} + 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('')||'
No packages have been published yet.
'} + 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=>`
${esc(p.name)}
${esc(p.version)}
${esc(channelNames[p.channel]||p.channel)}${esc(p.format)}${esc(fmtDate(p.built_at))}Download
`).join('')||'
No matching packages.
'} + 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 ``} - 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 `

${ch==='linux'?'Linux':'SUSE'}

${[['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])=>`
${esc(a)}${esc(v||'—')}
`).join('')}
`}).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 `

${esc(channelNames[ch])}

${[['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])=>`
${esc(a)}${esc(v||'-')}
`).join('')}
`}).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); })(); diff --git a/dashboard/web/index.html b/dashboard/web/index.html index 695e82c..53219d9 100644 --- a/dashboard/web/index.html +++ b/dashboard/web/index.html @@ -1,60 +1,59 @@ - + - RustBox Repository - + RustBox Package Repository
-

Przegląd

—

-
+

Overview

-

+
-
RUSTBOX / LINUX

Repozytorium wydań

Podpisane paczki APT, DNF/RPM i Zypper budowane bezpośrednio ze źródeł RustBox.

-
NAJNOWSZA WERSJA——
+
RUSTBOX PACKAGE REPOSITORY

Linux packages

Signed packages and repository metadata built directly from RustBox source code.

+
LATEST VERSION--
-
Paczki0opublikowane artefakty
-
Linux—APT + DNF
-
SUSE—Zypper RPM
+
Debian / Ubuntu-APT / DEB
+
RHEL / AlmaLinux / Rocky Linux-DNF / RPM
+
SUSE / openSUSE-Zypper / RPM
-

Ostatnie paczki

+

Recent packages

-
+
-
-

Debian / Ubuntu

Instalacja przez podpisane repozytorium APT.

-
Po instalacji konfiguracja znajduje się w /etc/rustbox/rustbox.toml, dane w /var/lib/rustbox, a logi aplikacji w /var/log/rustbox/rustbox.log.
+
+

Debian / Ubuntu

Install from the signed APT repository.

+
After installation, configuration is stored in /etc/rustbox/rustbox.toml, application data in /var/lib/rustbox, and logs in /var/log/rustbox/rustbox.log.
-

Metadane buildu

+

Build metadata

diff --git a/dashboard/web/style.css b/dashboard/web/style.css index f58630f..b14b67f 100644 --- a/dashboard/web/style.css +++ b/dashboard/web/style.css @@ -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);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-template-columns:repeat(2,1fr)}.meta-block{padding:16px}.meta-row{display:flex;justify-content:space-between;gap:18px;padding:8px 0;border-bottom:1px solid var(--line)}.meta-row span{color:var(--muted)}.toast{position:fixed;right:18px;bottom:18px;background:var(--panel3);border:1px solid var(--line2);box-shadow:0 12px 35px var(--shadow);padding:10px 14px;border-radius:6px;opacity:0;transform:translateY(8px);pointer-events:none;transition:.18s}.toast.show{opacity:1;transform:none}.empty{padding:28px 16px;color:var(--muted);text-align:center}@media(max-width:860px){.shell{grid-template-columns:1fr}.sidebar{position:static;height:auto;border-right:0;border-bottom:1px solid var(--line);padding:10px}.brand{padding:4px 5px 10px}.sidebar nav{grid-template-columns:repeat(4,1fr)}.nav{text-align:center;padding:9px 5px;font-size:12px}.sidebar-foot{display:none}.topbar{top:0;height:62px;padding:0 14px}.view{padding:14px}.hero{grid-template-columns:1fr;padding:20px}.cards{grid-template-columns:1fr}.repo-grid,.metadata{grid-template-columns:1fr}.filters{grid-template-columns:1fr}.package-row,.table-row{grid-template-columns:minmax(0,1fr) 90px 72px}.package-row>*:nth-child(4),.package-row>*:nth-child(5),.table-row>*:nth-child(4),.table-row>*:nth-child(5){display:none}.hero h2{font-size:24px}} +*{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-template-columns:repeat(3,1fr)}.meta-block{padding:16px}.meta-row{display:flex;justify-content:space-between;gap:18px;padding:8px 0;border-bottom:1px solid var(--line)}.meta-row span{color:var(--muted)}.toast{position:fixed;right:18px;bottom:18px;background:var(--panel3);border:1px solid var(--line2);box-shadow:0 12px 35px var(--shadow);padding:10px 14px;border-radius:6px;opacity:0;transform:translateY(8px);pointer-events:none;transition:.18s}.toast.show{opacity:1;transform:none}.empty{padding:28px 16px;color:var(--muted);text-align:center}@media(max-width:860px){.shell{grid-template-columns:1fr}.sidebar{position:static;height:auto;border-right:0;border-bottom:1px solid var(--line);padding:10px}.brand{padding:4px 5px 10px}.sidebar nav{grid-template-columns:repeat(4,1fr)}.nav{text-align:center;padding:9px 5px;font-size:12px}.sidebar-foot{display:none}.topbar{top:0;height:62px;padding:0 14px}.view{padding:14px}.hero{grid-template-columns:1fr;padding:20px}.cards{grid-template-columns:1fr}.repo-grid,.metadata{grid-template-columns:1fr}.filters{grid-template-columns:1fr}.package-row,.table-row{grid-template-columns:minmax(0,1fr) 90px 72px}.package-row>*:nth-child(4),.package-row>*:nth-child(5),.table-row>*:nth-child(4),.table-row>*:nth-child(5){display:none}.hero h2{font-size:24px}} diff --git a/docker-compose.yml b/docker-compose.yml index ae9c69a..ff416f3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 @@ -47,7 +46,7 @@ services: volumes: - ./data/repository:/srv/repository:ro - ./http/nginx.conf:/etc/nginx/nginx.conf:ro - entrypoint: [ "nginx", "-g", "daemon off;" ] + entrypoint: ["nginx", "-g", "daemon off;"] read_only: true tmpfs: - /tmp:rw,noexec,nosuid,size=16m diff --git a/scripts/build-release.sh b/scripts/build-release.sh index 0947a2b..e586241 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -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 +nfpm package --config "$PKG/nfpm-suse.yaml" --packager rpm --target "$SUSE" 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 +rm -rf "$RELEASE_DIR/suse/x86_64/repodata"; createrepo_c --update "$RELEASE_DIR/suse/x86_64" >/dev/null 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 "${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" 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_file" <> "$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" diff --git a/scripts/check-and-build.sh b/scripts/check-and-build.sh index a948284..cb37e4d 100755 --- a/scripts/check-and-build.sh +++ b/scripts/check-and-build.sh @@ -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" diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index ca30c44..e46cd58 100755 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -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 diff --git a/scripts/init-storage.sh b/scripts/init-storage.sh index 62cdbae..dbfbc41 100755 --- a/scripts/init-storage.sh +++ b/scripts/init-storage.sh @@ -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" diff --git a/start.sh b/start.sh index db22ddc..eba2e52 100755 --- a/start.sh +++ b/start.sh @@ -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; } diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index e6c6004..9a070e6 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -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 diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 5c57eee..8cd56dc 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -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