feat(offline-repo): ship the reviewed package set so installs never download

The field installer needs no network at all, which means the packages have to
travel with the repository. This adds the resolved pool and pins every package
to the exact version the pool carries.

Package pool
- 1312 .deb files (884 MB), resolved on Ubuntu 26.04 in a container so the
  closure matches the release the fleet runs: a .deb built against another glibc
  cannot be installed on the devices.
- Tracked through Git LFS (*.deb), so history stays navigable while the working
  tree keeps the real files the installer reads from disk.
- Two RustDesk dependencies (libxdo3, libva-x11-2) were missing: the vendor .deb
  is installed with dpkg, so nothing else pulls them in and `dpkg -i rustdesk`
  would have left the package unconfigured on a device.
- Every file validated in the same release: 1312/1312 readable, 0 corrupt, 0
  foreign architecture, and the pool satisfies all 24 pins (PLAN READY).

Pins
- All 24 UNPINNED sentinels replaced with the versions read from the packages
  themselves (dpkg-deb), with requirements.tsv updated in the same change so the
  contract has no drift.

build-index.sh: stop destroying the shipped record
- The repository record next to this script is the reviewed contract template
  (state: UNBUILT, packages: []) that documents the schema. Its default record
  destination pointed at that same file, so any default-path run overwrote the
  template with BUILT rows -- which happened twice, once from a hand run and
  once while this change was being developed.
- The record now defaults to <output>/repo-manifest.yaml, and pointing it at the
  template is refused before any work starts. The template header documents this.

Tests
- The unpinned-pin and missing-pin cases read the sentinel out of the real
  manifests, so they became no-ops the moment the contract was filled instead of
  failing. They now set up the condition explicitly, and the fixture manifests
  carry the synthetic versions their pool is built from.
- check-provisioning-static required packages/.gitkeep; the pool is what keeps
  that directory in git now, so it asserts a populated pool directory instead.
This commit is contained in:
emirhanmamak-blueforce 2026-09-16 19:03:45 +03:00
parent 1b2d9aa478
commit 57fbccd4d0
1322 changed files with 4065 additions and 42 deletions

4
.gitignore vendored
View file

@ -3,6 +3,10 @@
# Local agent/plan state — never committed
.hermes/
# Scratch scripts and build experiments (package collection, pin extraction).
# Nothing here ships; the reviewed tooling lives in provisioning/offline-repo/.
/tmp/
# ---------------------------------------------------------------------------
# Field OS build artifacts.
# These are large, generated, and reproducible from a verified upstream ISO.

View file

@ -34,7 +34,15 @@ OUTPUT="$SCRIPT_DIR/built"
# BF_MANIFEST_DIR points a test run at a fixture directory; a real build always
# reads the reviewed manifests next to this script.
MANIFEST_DIR="${BF_MANIFEST_DIR:-$SCRIPT_DIR/manifests}"
MANIFEST_OUT="$SCRIPT_DIR/repo-manifest.yaml"
# The repository record belongs to the build it describes, so it is written
# inside the output directory by default, resolved once OUTPUT is final (see the
# guard below). It deliberately does NOT default to the shipped
# repo-manifest.yaml next to this script: that file is the reviewed contract
# template (state: UNBUILT, packages: []) which documents the schema and which
# the static suite asserts on, and a default-path write silently replaced it with
# BUILT rows twice. Keeping the two paths distinct is what makes it safe;
# --manifest-out still overrides the destination explicitly.
MANIFEST_OUT=''
TARGET_ARCH='amd64'
ALLOWED_ARCH_RE='^(amd64|all)$'
PACKAGE_NAME_RE='^[a-z0-9][a-z0-9+.-]*$'
@ -56,7 +64,8 @@ Seçenekler:
--packages DIZIN Kaynak paket dizini (varsayılan: <script>/packages)
--output DIZIN Çıktı dizini (varsayılan: <script>/built)
--manifest-dir DIZIN Pin manifestlerinin dizini (varsayılan: <script>/manifests)
--manifest-out DOSYA repo-manifest.yaml yolu (varsayılan: <script>/repo-manifest.yaml)
--manifest-out DOSYA repo-manifest.yaml yolu (varsayılan: <output>/repo-manifest.yaml;
repodaki şablon dosyasına yazmak reddedilir)
--check Yalnızca doğrular: hiçbir şey yazmaz, eksikleri listeler
-h, --help Bu yardımı yazar
@ -105,6 +114,25 @@ now_utc() {
fi
}
# Guard: resolve the record destination and protect the shipped template.
#
# repo-manifest.yaml next to this script is the reviewed *contract template*
# (state: UNBUILT, packages: []) that the static suite asserts on and that
# documents the schema. The record of a build belongs with that build, so the
# default destination is <output>/repo-manifest.yaml -- never the template. A run
# that is explicitly pointed back at the template is refused, because writing
# BUILT package rows over it destroys the document (which has happened twice).
resolve_manifest_out() {
[[ -n "$MANIFEST_OUT" ]] || MANIFEST_OUT="$OUTPUT/repo-manifest.yaml"
if [[ "$MANIFEST_OUT" == "$SCRIPT_DIR/repo-manifest.yaml" ]]; then
printf 'HATA: hedef künye (%s) korunan şablon dosyasıdır.\n' "$(rel_to_repo "$MANIFEST_OUT")" >&2
printf ' Derleme kaydı için varsayılanı kullanın (<output>/repo-manifest.yaml)\n' >&2
printf ' ya da --manifest-out ile başka bir yol verin.\n' >&2
printf ' Şablon state: UNBUILT olarak kalmalıdır; üzerine BUILT kaydı yazılmaz.\n' >&2
exit 2
fi
}
# Canonical stanza order: Package, then Version, then the order apt-ftparchive
# produced (kept as a tie breaker for two builds of one version). Paragraph
# newlines travel through one line of `sort` escaped as 0x1e and are restored
@ -156,6 +184,7 @@ fi
[[ "$OUTPUT" = /* ]] || OUTPUT="$PWD/$OUTPUT"
[[ "$OUTPUT" != '/' && "$OUTPUT" != "$HOME" && "$OUTPUT" != "$SCRIPT_DIR" && "$OUTPUT" != "$SCRIPT_DIR/packages" && "$OUTPUT" != "$SCRIPT_DIR/manifests" ]] \
|| { printf 'HATA: --output güvenli olmayan bir dizini gösteriyor: %s\n' "$OUTPUT" >&2; exit 2; }
resolve_manifest_out
printf '=== Blueforce offline APT deposu derlemesi ===\n'
printf 'Paket dizini : %s\n' "$PACKAGES_DIR"

View file

@ -20,27 +20,27 @@
# change: update manifests/requirements.tsv in the same edit or the plan fails.
# --- base tooling (installer module 02-system.sh, BASE_PKGS) ---
curl=UNPINNED
wget=UNPINNED
ca-certificates=UNPINNED
gnupg=UNPINNED
lsb-release=UNPINNED
curl=8.18.0-1ubuntu2.5
wget=1.25.0-2ubuntu4.4
ca-certificates=20260601~26.04.1
gnupg=2.4.8-4ubuntu3.1
lsb-release=12.1-2build1
# Transitional dummy on apt >= 2.0 (https is built into apt). Kept for air-gapped
# parity with the reference build; the LAB may drop it after review.
apt-transport-https=UNPINNED
apt-transport-https=3.2.0
# --- remote shell and encrypted transport (modules 07-ssh.sh, 06-wireguard.sh) ---
openssh-server=UNPINNED
wireguard-tools=UNPINNED
openssh-server=1:10.2p1-2ubuntu3.6
wireguard-tools=1.0.20250521-1ubuntu1
# --- field diagnostics: SMART disk health evidence ---
# Consumed by scripts/diagnostics/bf-diagnostics and bf-hardware-inventory.sh.
smartmontools=UNPINNED
smartmontools=7.5-2
# --- firewall baseline (module 13-firewall.sh) ---
ufw=UNPINNED
iptables=UNPINNED
ufw=0.36.2-9build1
iptables=1.8.11-2ubuntu3
# --- monitoring (module 12-monitoring.sh) ---
# The textfile collector ships inside this package; no extra dependency is needed.
prometheus-node-exporter=UNPINNED
prometheus-node-exporter=1.10.2-1

View file

@ -23,8 +23,8 @@
# CE repository snapshot and must be supplied as local .deb files. containerd.io is
# the runtime bundled with Docker CE, not Ubuntu's "containerd" package.
# Installer module 10-docker.sh installs exactly this set in --offline mode.
docker-ce=UNPINNED
docker-ce-cli=UNPINNED
containerd.io=UNPINNED
docker-buildx-plugin=UNPINNED
docker-compose-plugin=UNPINNED
docker-ce=5:29.8.1-1~ubuntu.26.04~resolute
docker-ce-cli=5:29.8.1-1~ubuntu.26.04~resolute
containerd.io=2.3.5-1~ubuntu.26.04~resolute
docker-buildx-plugin=0.37.1-1~ubuntu.26.04~resolute
docker-compose-plugin=5.5.1-1~ubuntu.26.04~resolute

View file

@ -20,16 +20,24 @@
# change: update manifests/requirements.tsv in the same edit or the plan fails.
# --- remote desktop (installer module 08-rdp.sh) ---
xrdp=UNPINNED
xrdp=0.10.1-4.1
# X11 backend used by the GNOME session behind xRDP. xrdp recommends it; the LAB
# must confirm the dependency closure of the reviewed pool before the build.
xorgxrdp=UNPINNED
gnome-session=UNPINNED
xorgxrdp=1:0.10.2-1build1
gnome-session=50.1-0ubuntu0.1
# --- full desktop stack (installer module 16-gui.sh) ---
ubuntu-desktop-minimal=UNPINNED
ubuntu-desktop-minimal=1.570.3
# --- RustDesk: vendor .deb, absent from the Ubuntu archive ---
# Version is the reviewed self-hosted client build. Record where that .deb came
# from in the bundle lock row for the package.
rustdesk=UNPINNED
rustdesk=1.4.9
# rustdesk 1.4.9's own Depends line names these two, and nothing else in the
# manifest pulls them in: the vendor .deb is installed with dpkg, so its
# dependencies are not resolved from a remote source at install time. Without
# them `dpkg -i rustdesk` leaves the package unconfigured on a device whose
# desktop closure we do not ship.
libxdo3=1:3.20160805.1-5.1build1
libva-x11-2=2.23.0-1ubuntu1

View file

@ -23,6 +23,8 @@ base-packages.txt ufw scripts/install/installer/modules/13-firewall.sh host fire
base-packages.txt iptables scripts/install/installer/modules/13-firewall.sh rule backends used by the firewall module
base-packages.txt prometheus-node-exporter scripts/install/installer/modules/12-monitoring.sh node exporter including the textfile collector
remote-access.txt rustdesk scripts/install/installer/modules/09-rustdesk.sh pinned vendor .deb; not in the Ubuntu archive
remote-access.txt libxdo3 scripts/install/installer/modules/09-rustdesk.sh rustdesk 1.4.9 Depends; pulled explicitly because the vendor .deb is installed with dpkg, not from an APT source
remote-access.txt libva-x11-2 scripts/install/installer/modules/09-rustdesk.sh rustdesk 1.4.9 Depends; pulled explicitly because the vendor .deb is installed with dpkg, not from an APT source
remote-access.txt xrdp scripts/install/installer/modules/08-rdp.sh RDP access channel
remote-access.txt xorgxrdp scripts/install/installer/modules/08-rdp.sh X11 backend recommended by xrdp for the GNOME session
remote-access.txt gnome-session scripts/install/installer/modules/08-rdp.sh session started by xRDP logins

Can't render this file because it has a wrong number of fields in line 13.

View file

@ -1 +0,0 @@
# Package artifacts are intentionally excluded. Supply reviewed .deb files locally at build time.

Binary file not shown.

Binary file not shown.

BIN
provisioning/offline-repo/packages/acl_2.3.2-2_amd64.deb (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
provisioning/offline-repo/packages/baobab_49.1-2_amd64.deb (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
provisioning/offline-repo/packages/bolt_0.9.10-1_amd64.deb (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
provisioning/offline-repo/packages/colord_1.4.8-3_amd64.deb (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more