• v1.0.0 0c97a17eb3

    v1.0.0
    All checks were successful
    test / test (push) Successful in 5m12s
    release / release (push) Successful in 25s
    Stable

    Ghost released this 2026-09-18 14:02:01 +00:00 | 0 commits to master since this release

    First stable release. The configuration format and the /routeros/index.json schema are now covered by
    Semantic Versioning: breaking changes only arrive with a new major version.

    Fixed

    • Truncated downloads were stored and served as complete files. When upstream dropped a connection
      mid-transfer the body simply ended early — no error — and the short file was renamed into place.
      MikroTik's CDN cuts long-running connections, so on a slow link this hit most large files: main
      routeros-*.npk packages, CHR images, netinstall archives, WinBox, and the all_packages bundles. A
      truncated bundle failed to unpack on every sweep (bundle <ver>/<arch> failed: File is not a zip file),
      so that arch never completed and stayed hidden — e.g. arm/arm64 and their zerotier package
      missing from a version — while being re-downloaded every sweep. Downloads are now verified against
      Content-Length and resumed with HTTP Range for as long as each request adds bytes (giving up
      after 3 consecutive requests without progress); nothing incomplete is ever written to the mirror.
      404s (package not built for an arch) are still not retried.
    • refresh left the sync worker down after the next reboot. It sent SIGHUP with docker compose kill,
      and Docker marks a container as manually stopped on docker kill with any signal; the
      unless-stopped restart policy then skipped sync at the next daemon start — e.g. the automatic
      reboot after security updates — while web and goaccess came back. refresh now signals the worker
      from inside the container. If you used refresh on an earlier version, run sudo ./src/deploy.sh start
      once (or check status) after the next reboot.

    Added

    • Integrity pass — deploy.sh verify (python -m packagemini --verify). Compares every mirrored
      file's size with upstream's Content-Length; a mismatch removes the file and reopens its arch
      (.complete / .size / "download all" zip) so the next sweep re-fetches it. Files whose upstream
      length can't be determined are left alone. The worker runs the pass once automatically on first
      start per mirror root (.verified marker), so mirrors filled by earlier releases repair themselves
      on upgrade. Upgrade note: expect affected arches to disappear from the listings and
      index.json until they have been re-fetched — on a slow link that can take hours.

    Changed

    • Terminology: "rootless" → "non-root containers" throughout the docs, README badge, compose
      comments and deploy.sh status. The containers run as uid:gid 1000, but the Docker daemon runs as
      root with no user-namespace remapping, which is not what "rootless Docker" means. No behaviour change;
      DESIGN.md now states the actual boundary.
    • README: removed the "under active development — feature-incomplete until v1.0" notice.
    • Default branch renamed main → master. The CI trigger, deploy.sh's no-tags fallback and the
      docs follow. Installs that track the branch tip must switch once: sudo ./src/deploy.sh update master
      (installs on release tags — the default — are unaffected).
    Downloads
  • v0.13.0 598ecdecd4

    v0.13.0
    All checks were successful
    test / test (push) Successful in 23s
    release / release (push) Successful in 22s
    Stable

    Ghost released this 2026-07-03 12:55:22 +00:00 | 6 commits to master since this release

    Added

    • Machine-readable package index. Every sweep now writes /routeros/index.json — a human-readable
      (2-space-indented), atomically-written inventory of every RouterOS .npk on the mirror (version → arch →
      packages with sizes, plus channel heads) for automated consumers, starting with rosbackup-ng package
      discovery. It lists only what's actually on disk (retention-pruned versions drop out); additive fields
      won't bump the top-level schema version. The JSON Schema is served alongside at
      /routeros/index.schema.json (canonical: packagemini-schema.json, drift-tested). Both are served
      behind the mirror's optional basic-auth and revalidated (not immutably cached). See
      doc/PACKAGE-INDEX.md.
    Downloads
  • v0.12.0 41a4d55b68

    v0.12.0
    All checks were successful
    test / test (push) Successful in 19s
    release / release (push) Successful in 21s
    Stable

    Ghost released this 2026-07-03 12:29:48 +00:00 | 7 commits to master since this release

    Security hardening sweep — a 4-agent audit (deploy.sh, Python worker, infra/web) plus a docs↔code
    security-claims cross-check. Fixes below, ranked by severity.

    Security

    • CRITICAL — root RCE via set-fqdn. An unvalidated FQDN (set-fqdn <arg> / PM_FQDN) was spliced
      into a sed -i script; a | or newline could trigger GNU sed's e command → arbitrary command
      execution as root (and YAML injection into packagemini.yml). FQDNs are now validated against a
      hostname charset (_check_fqdn) before they reach sed / the config writer.
    • HIGH — GoAccess board exposure. The board's :7443 listener — and its unauthenticated /ws
      WebSocket — inherited none of the mirror's IP allow/deny gate, rate-limit, or headers, so an
      allowlist didn't cover the board and basic-auth brute-force was unthrottled. The board block now applies
      the same allow/deny (covering /ws), a rate-limit, and headers; the raw log archive under
      /srv/report/archive is hidden from browsing.
    • HIGH — path traversal. version/arch/pin tokens reached mkdir before validation. A new
      safe_token() guard gates every token before it builds a filesystem path or URL — at discovery
      (sources.py), at config load (architectures/pin rejected), and defensively before mkdir.
    • MEDIUM — Caddyfile injection. public_base_url, tls_*, usernames, *_hash_env, and the server
      timeouts are validated at config load (reject braces/tabs/newlines; env-name + username charsets),
      matching the existing rate / CIDR validation.
    • MEDIUM — secret & permission exposure. Passwords are hashed via stdin (no longer visible in
      ps / argv); .env and the imported TLS key are written under a private umask (no world-readable
      window); the public-facing web container gets no-new-privileges (matching sync/goaccess).
    • MEDIUM — resource DoS. Response reads are capped (8 MiB text / 4 GiB artifact) so a misbehaving
      upstream can't exhaust worker memory; the main npk is size-checked after download (verify_npk wired
      into the sweep) and re-fetched if it fails.
    • LOW / defense-in-depth. git refs rejected if they start with - or contain metacharacters; strict
      CSP on the (fully self-contained) mirror site; the feed uses quoteattr for XML attributes; the
      SSH key-only guard now requires a non-empty authorized_keys (avoids lockout); marker dotfiles
      (.complete/.size/.absent) hidden from the file server.

    Docs

    • Corrected security claims that over-promised: sources.archive_backfill is now marked not yet
      implemented
      (it was a silent no-op stub), and the false "checks NPK magic bytes" claim is replaced
      with the actual behavior (TLS-verified fetch + a size sanity check; no cryptographic verification).
      SYSTEM-HARDENING documents the 7443/tcp firewall opening and the board's public-listener caveat.
    • New doc/REAL-TIME-MONITORING.md — the GoAccess board + live-update
      WebSocket architecture, with a dashboard screenshot.
    Downloads
  • v0.11.3 3a7a36bcc6

    v0.11.3
    All checks were successful
    release / release (push) Successful in 21s
    test / test (push) Successful in 23s
    Stable

    Ghost released this 2026-07-03 10:12:23 +00:00 | 8 commits to master since this release

    Fixed

    • Per-channel version count could drift above keep_last. seen.json[channel] accumulated targeted
      versions as an ever-growing union and was never capped, so churny pre-release channels (testing = rc,
      development = beta) reported e.g. 11 versions under status → Content → by channel even with
      keep_last: 10 — and seen.json grew unbounded. It's now capped to the newest keep_last per channel
      after the union (which still guards against a sweep that momentarily under-discovers). On-disk retention
      was already correct; this was a reporting/growth drift.
    Downloads
  • v0.11.2 4220749e4e

    v0.11.2
    All checks were successful
    test / test (push) Successful in 20s
    release / release (push) Successful in 22s
    Stable

    Ghost released this 2026-07-03 09:44:41 +00:00 | 9 commits to master since this release

    Fixed

    • status swappiness hint no longer repeats the value (read "0 0 = …"); now "lower = keep hot firmware
      in RAM cache, avoid swapping out".
    Downloads
  • v0.11.1 445f10e85a

    v0.11.1
    All checks were successful
    test / test (push) Successful in 21s
    release / release (push) Successful in 21s
    Stable

    Ghost released this 2026-07-03 09:42:38 +00:00 | 10 commits to master since this release

    Changed

    • status Performance section: each value now carries a short plain-language explanation (what the
      connection backlog / conntrack / congestion-control / swappiness number actually means) so the section
      is self-explanatory instead of opaque numbers.
    Downloads
  • v0.11.0 c1f2e2d305

    v0.11.0
    All checks were successful
    release / release (push) Successful in 22s
    test / test (push) Successful in 26s
    Stable

    Ghost released this 2026-07-03 09:35:29 +00:00 | 11 commits to master since this release

    Codebase audit sweep (5 parallel agents: Python source, tests, deploy.sh, docs, infra) — fixes below.

    Added

    • Continuous CI — a test workflow runs the suite on every push to main, not only on tag releases
      (regressions between versions were previously unseen until the next tag).
    • web waits on a sync healthcheck (Caddyfile present) via depends_on: condition: service_healthy,
      so it no longer crash-loops against a missing config on first boot.
    • More tests — config validation (rate / sync_interval / packages), MikroTik URL-builder token guards,
      HTML-escaping of operator/changelog fields, and asset cache-busting; plus an autouse fixture that resets
      site.py globals between tests (removes order-dependent flakiness).

    Fixed

    • config: web.rate_limit.rate is now format-validated at load — a typo like 600/m used to pass
      silently and render an invalid Caddyfile (written before downloads, so it could wedge first boot).
    • sweep: a version pinned into several channels is downloaded once — it was submitted to the pool
      per-channel, causing redundant CDN fetches and a race on the shared .part file.
    • deploy.sh: operator name/email and GoAccess/basic-auth usernames are escaped/validated before
      being written into packagemini.yml (a " / / / | could corrupt it); cmd_status now exports
      LC_ALL (was an inert local) and restores set -e; the three divergent "is GoAccess enabled" greps
      are unified into one _goaccess_enabled helper; the dnf-automatic sed/mkdir writes are guarded;
      set-cert warns when openssl is missing; dead hf_perms_state removed; the then keyword-variable
      renamed.
    • docs: corrected the reboot_command behaviour in UPDATES.md (packagemini always overwrites it,
      not "kept if present"); DESIGN.md GoAccess row (own port + port key); "five → six" hardening measures
      in SYSTEM-HARDENING.md; marked shipped TODO items (footer toggle, reports-over-web) done; fixed stale
      /goaccess/… URLs in the compose file, env.example, and docs; documented PM_GOACCESS_PORT.

    Changed

    • Docstrings added to run_once, render_caddyfile, classify, same_bytes; Config dataclass type
      hints aligned (tuple[str, ...] / dict[str, ChannelCfg]); .coverage git-ignored.
    Downloads
  • v0.10.4 66a4ce0fb9

    v0.10.4
    All checks were successful
    release / release (push) Successful in 19s
    Stable

    Ghost released this 2026-07-03 06:56:43 +00:00 | 12 commits to master since this release

    Changed

    • --help: moved --force from MANAGE to INSTALL so each section is stylistically uniform (INSTALL =
      --flags, MANAGE = bare-word commands). --force is a reinstall/rebuild of the current version, so it
      belongs with the install flags; behaviour is unchanged (deploy.sh --force still works).
    Downloads
  • v0.10.3 9dfdb4f2f8

    v0.10.3
    All checks were successful
    release / release (push) Successful in 15s
    Stable

    Ghost released this 2026-07-03 06:45:05 +00:00 | 13 commits to master since this release

    Fixed

    • Swept the last harden↔performance crossed wires after the 0.10.1 split (two-agent audit of the
      script + all docs): the status default-profile hint, the comment baked into the generated
      98-packagemini-performance.conf, the tune code comments, and config/packagemini.example.yml now
      all point at tune / tune --remove (never harden). Marked the now-shipped performance-tuning item
      done in TODO, and added PERFORMANCE-TUNING.md / MANAGE.md to the README + DESIGN doc indexes.
    Downloads
  • v0.10.2 0d273649e7

    v0.10.2
    All checks were successful
    release / release (push) Successful in 20s
    Stable

    Ghost released this 2026-07-03 06:38:46 +00:00 | 14 commits to master since this release

    Fixed

    • status Performance line pointed at harden to toggle tuning — now points at tune --remove (harden
      no longer touches performance).
    Downloads