Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .github/workflows/bake-rootfs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ on:
description: "Cancel every node as soon as one fails (nothing publishes anyway)."
type: boolean
default: true
run_prebuild_tests:
description: "Run the pre-build tests first and gate the bake on them. Leave on unless you must bypass."
type: boolean
default: true
go_version:
description: "Go toolchain for mkmetalink (pinned; blank = latest from go.dev)."
default: "go1.27.3"
Expand Down Expand Up @@ -82,6 +86,12 @@ concurrency:
cancel-in-progress: false

jobs:
# K2GO-567: fast native tests gate the bake. The test logic lives in its own workflow (editable
# without touching this one, which stays pure construction); the bake runs only if it passes.
prebuild-tests:
if: ${{ inputs.run_prebuild_tests }}
uses: ./.github/workflows/prebuild-tests.yml

# The Debian base every tier is built on is a proot-distro v4.29.0 release asset
# (github.com/termux/proot-distro), fetched from the upstream release. It is STATIC, so seed it
# into R2 ONCE; every later run only HEADs it (no re-download). Bumping PD_VERSION seeds a
Expand Down Expand Up @@ -168,7 +178,10 @@ jobs:
echo "layout=$LAYOUT max_parallel=$maxp matrix=$m"

bake:
needs: [seed-base, plan] # the static base must be present on R2 before we publish artifacts
needs: [prebuild-tests, seed-base, plan] # K2GO-567: tests gate the bake; base present before publish
# always() so a SKIPPED prebuild-tests (run_prebuild_tests=false) does not auto-skip the bake; the
# bake still runs only if prebuild-tests did not FAIL/cancel and the base + plan succeeded.
if: ${{ always() && needs.prebuild-tests.result != 'failure' && needs.prebuild-tests.result != 'cancelled' && needs.seed-base.result == 'success' && needs.plan.result == 'success' }}
name: RootFS ${{ matrix.arch }} ${{ matrix.tier }}
# Native arm64 runner (the builder uses native proot, not QEMU). Adjust the label
# if the org uses a different arm64 runner (e.g. a self-hosted one).
Expand Down
76 changes: 76 additions & 0 deletions .github/workflows/prebuild-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Pre-build tests (gate the rootfs bake)

# K2GO-567. Fast, NATIVE tests that must pass BEFORE the rootfs bake (bake-rootfs.yml). No proot,
# no root. Runs the dash-node unit tests plus the contract tests, which check the REAL upstream
# artifacts with our tools/upstream-patches applied, so upstream drift is caught before a build
# ships the break. Kept SEPARATE from bake-rootfs.yml (pure construction) so the test logic is
# edited here; bake-rootfs.yml calls this as its first job (needs:). On failure GitHub emails the
# run's actor (built-in notification) and the bake does not run.

on:
pull_request:
branches: [ "main" ]
workflow_dispatch:
inputs:
iiab_ref:
description: "iiab/iiab ref to check the upstream contract against."
default: "master"
workflow_call:
inputs:
iiab_ref:
type: string
required: false
default: "master"

permissions:
contents: read

concurrency:
group: prebuild-tests-${{ github.ref }}
cancel-in-progress: true

jobs:
tests:
name: dash-node + contract tests
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Set up Node
uses: actions/setup-node@v5
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: static/dashboard/package-lock.json

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: dash-node tests (typecheck + unit)
working-directory: static/dashboard
run: |
npm ci --ignore-scripts
npm run typecheck
npm test

# Fetch the real upstream maps role (the artifact the bake uses) and apply our in-flight
# patches, so the contract test checks what will actually be baked, not a frozen fixture.
# A patch that no longer applies (context drift) fails here, which is itself a useful signal.
- name: Fetch upstream iiab and apply our patches
run: |
set -euo pipefail
ref='${{ inputs.iiab_ref }}'
git clone --depth 1 --branch "${ref:-master}" https://github.com/iiab/iiab.git /tmp/iiab
tools/upstream-patches/apply-upstream-patches.sh --root /tmp/iiab
echo "TILE_EXTRACT_PY=/tmp/iiab/roles/maps/templates/tile-extract.py.j2" >> "$GITHUB_ENV"

- name: Contract tests (real + patched upstream)
run: |
set -euo pipefail
test -f "$TILE_EXTRACT_PY" \
|| { echo "::error::upstream tile-extract.py.j2 missing at $TILE_EXTRACT_PY (contract drift)"; exit 1; }
bash tools/rootfs-builder/tests/run-contract-tests.sh
2 changes: 1 addition & 1 deletion static/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"test": "node --require ts-node/register --test sockets/maps.socket.test.ts sockets/rolling-log.test.ts sockets/kolibri.session.test.ts sockets/credentials.test.ts sockets/net-retry.test.ts sockets/services.test.ts sockets/log-rotate.test.ts sockets/rebuild-status.test.ts sockets/conex-proxy.test.ts",
"test": "node --require ts-node/register --test sockets/maps.socket.test.ts sockets/rolling-log.test.ts sockets/kolibri.session.test.ts sockets/credentials.test.ts sockets/net-retry.test.ts sockets/services.test.ts sockets/log-rotate.test.ts sockets/rebuild-status.test.ts sockets/conex-proxy.test.ts sockets/maps.fqr-behavior.test.ts",
"test:db": "node --require ts-node/register --test sockets/jobs.test.ts",
"typecheck": "tsc --noEmit",
"build": "tsc",
Expand Down
54 changes: 54 additions & 0 deletions static/dashboard/sockets/maps.fqr-behavior.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/// <reference types="node" />
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

// K2GO-566 BEHAVIOR test, owned by the K2GO-567 pre-build belt.
//
// It checks exactly what K2GO-566 fixes: an existing FQR (a "<name>.fqr/" directory with a
// meta.json {"bbox":[...]}) MUST be listed by dash-node, so the in-app "Downloaded regions" list
// shows it and the user can delete it. Before K2GO-566 dash-node read the retired extracts.json and
// listed nothing, so this test FAILS without the fix (listFqrRegions is absent / the region does
// not appear), and PASSES with it.
//
// It is deliberately INDEPENDENT of the upstream contract test (tests/maps/fqr/test_fqr_contract.py):
// that one checks that UPSTREAM still ships the ".fqr"/meta.json/bbox format; this one checks that
// OUR code consumes it. Both must stay aligned; if either side drifts, the matching test breaks.
//
// A dynamic require (not a static import) is used on purpose: when listFqrRegions does not exist yet
// the test reports a clear assertion failure instead of a module-load/compile crash.

test('K2GO-566: an existing .fqr region is listed so it can be managed and deleted', () => {
const maps = require('./maps.socket') as {
listFqrRegions?: (dir: string) => Record<string, { bbox?: number[]; ui_bounds?: number[] }>;
};
assert.equal(
typeof maps.listFqrRegions, 'function',
'dash-node must expose listFqrRegions() to read the new .fqr format (K2GO-566); ' +
'before the fix it read the retired extracts.json and listed nothing',
);

const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fqr-behavior-'));
try {
fs.mkdirSync(path.join(root, 'ttest.fqr'));
fs.writeFileSync(
path.join(root, 'ttest.fqr', 'meta.json'),
JSON.stringify({ bbox: [-108.5577, 28.0298, -101.073, 36.1073] }),
);

const regions = maps.listFqrRegions!(root);
assert.ok(
regions.ttest,
'an existing FQR must appear in the list so it can be managed and deleted; it did not ' +
'(this is the K2GO-566 bug: the list read the retired extracts.json)',
);
assert.deepEqual(
regions.ttest.ui_bounds, [-108.5577, 28.0298, -101.073, 36.1073],
'the listed region must carry its bounds (ui_bounds) from meta.json bbox',
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
23 changes: 23 additions & 0 deletions tools/rootfs-builder/tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Pre-build tests (K2GO-567)

Fast, native tests that run BEFORE the rootfs bake (no proot, no root), with our
upstream patches applied. If they fail, the bake must not run. The workflow is
`.github/workflows/prebuild-tests.yml`.

Layout: one directory per area/feature, so files never mix. Each test owns its
directory and fixtures.

tests/
maps/fqr/ - maps Full Quality Region (FQR) contract (K2GO-566)
kiwix/downloads/ - (future) Kiwix binary download + R2 rescue

Conventions:

- A test file is `test_*.py` (run by `run-contract-tests.sh`), or a dash-node unit
test under `static/dashboard` (run by `npm test`).
- A CONTRACT test checks the REAL upstream artifact (the version the bake uses),
not a frozen fixture, so upstream drift is caught. It reads the artifact from an
env var (for example `TILE_EXTRACT_PY`) and SKIPS when the var is unset, so it is
safe to run anywhere; the pre-build workflow sets the var before it runs.
- Keep tests native: pure Python stdlib or `npm`. No proot, no root, no network
unless the test owns a cheap, pinned fetch.
44 changes: 44 additions & 0 deletions tools/rootfs-builder/tests/maps/fqr/test_fqr_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
# K2GO-567 / K2GO-566: contract test for the upstream maps FQR format.
#
# The in-app "Downloaded regions" list and delete (K2GO-566) depend on tile-extract.py keeping the
# per-region "<name>.fqr/" directory layout, with meta.json {"bbox":[...]} inside, and the
# "extract"/"delete" commands. Upstream changed this once with no notice (it dropped the central
# extracts.json catalog), which silently broke the feature. This test fails fast when that contract
# drifts again, BEFORE a rootfs bake ships the break.
#
# It checks the REAL tile-extract.py source (the copy the bake will use), not a frozen fixture.
# Point TILE_EXTRACT_PY at it; the pre-build workflow sets it. The test SKIPs when the var is unset,
# so it is safe to run in any environment.
import os
import re
import sys

script = os.environ.get("TILE_EXTRACT_PY", "")
if not script or not os.path.isfile(script):
print("SKIP test_fqr_contract: set TILE_EXTRACT_PY to the upstream tile-extract.py[.j2]")
sys.exit(0)

with open(script, encoding="utf-8") as handle:
src = handle.read()

# Each marker is something the dash-node enumerator (listFqrRegions) or the app/delete path needs.
REQUIRED = [
(r"\*\.fqr", 'regions are "<name>.fqr/" directories (the "*.fqr" glob)'),
(r"meta\.json", "each region has a meta.json metadata file"),
(r"['\"]?bbox['\"]?", 'meta.json carries a "bbox"'),
(r"['\"]extract['\"]", 'the "extract" command'),
(r"['\"]delete['\"]", 'the "delete" command'),
]

missing = [desc for pattern, desc in REQUIRED if re.search(pattern, src) is None]
if missing:
print("FQR CONTRACT DRIFT in " + script + ":")
for desc in missing:
print(" MISSING: " + desc)
print("The in-app FQR list/delete (K2GO-566) assume this contract. Update them together,")
print("then update this test. Do NOT ship a bake until they match.")
sys.exit(1)

print("OK test_fqr_contract: FQR contract markers present in " + script)
sys.exit(0)
15 changes: 15 additions & 0 deletions tools/rootfs-builder/tests/run-contract-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# K2GO-567: run every contract test (test_*.py) under this tests/ tree. Native, no root.
# Each test SKIPs (exit 0) when its upstream-artifact env var is unset; the pre-build workflow
# sets those vars (for example TILE_EXTRACT_PY) so the tests actually run and gate the bake.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
rc=0
while IFS= read -r t; do
echo "== $t"
python3 "$t" || rc=1
done < <(find "$HERE" -type f -name 'test_*.py' | sort)
if [ "$rc" -ne 0 ]; then
echo "CONTRACT TESTS FAILED"
fi
exit "$rc"
Loading