Skip to content
Open
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
58 changes: 37 additions & 21 deletions .github/workflows/runtime-policy.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
# This workflow is managed by gh actions-lock.
# SPDX-License-Identifier: MPL-2.0
# This workflow is managed by gh actions-lock.
Comment thread
hyperpolymath marked this conversation as resolved.
# SPDX-License-Identifier: MPL-2.0
# Runtime and package-manager policy check.
#
# Authority: hyperpolymath/standards LANGUAGE-POLICY.adoc §1.
# Ordering: Bun (1st) > Deno (2nd) > pnpm (3rd) > npm (last resort).
# Authority: hyperpolymath/standards LANGUAGE-POLICY.adoc §1.3–§1.4 (owner ruling
# 2026-10-14). Ordering: Bun (1st) > Deno (2nd) > pnpm (3rd) > npm (last resort).
#
# One manifest, every runtime: package.json is the only manifest, and
# `js-runtimes regen` writes bun.lock, deno.lock and pnpm-lock.yaml from it. Those
# three locks are ONE dependency graph locked three ways, not three toolchains;
# governance's "JS runtimes" jobs check each one against package.json.
#
# REPLACES npm-bun-blocker.yml, which failed any build carrying `bun.lockb` with
# the message "npm/bun artifacts detected. Use Deno instead." That gate blocked
# what is now the FIRST-choice runtime and mandated the second. It was present in
# 55 repositories.
#
# What this fails on, deliberately:
# MIXED TOOLCHAINS -- two different package managers' lockfiles in one repo.
# That is real, actionable drift: two dependency graphs that can disagree.
# ANY tracked package-lock.json. npm is LAST but PERMITTED as a runtime, and
# it keeps no lock: package-lock.json is refused (standards#67). Next to the
# js-runtimes locks it is also MIXED TOOLCHAINS, two dependency graphs that
# can disagree.
# What it does NOT fail on:
# Using bun, deno, pnpm or npm. npm is LAST but PERMITTED; the check reports
# the tier in use so drift is visible without blocking legitimate work.
# The js-runtimes locks together, or any one of them alone; a deno.json is a
# notice (Deno is tier 2, LANGUAGE-POLICY §1.3). The check reports the tiers
# in use so drift is visible without blocking legitimate work.
name: Runtime Policy
on:
push:
Expand All @@ -33,7 +41,7 @@ permissions:
jobs:
runtime-policy:
name: Runtime Policy
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
Expand All @@ -44,30 +52,38 @@ jobs:
run: |
set -euo pipefail

bun=0; deno=0; pnpm=0; npm=0
bun=0; deno=0; pnpm=0; npm=0; deno_cfg=0
[ -f bun.lockb ] || [ -f bun.lock ] && bun=1 || true
[ -f deno.lock ] || [ -f deno.json ] || [ -f deno.jsonc ] && deno=1 || true
[ -f deno.lock ] && deno=1 || true
[ -f deno.json ] || [ -f deno.jsonc ] && deno_cfg=1 || true
[ -f pnpm-lock.yaml ] && pnpm=1 || true
[ -f package-lock.json ] && npm=1 || true

total=$((bun + deno + pnpm + npm))
# bun, deno and pnpm locks are the one graph js-runtimes regen writes.
regen=$((bun + deno + pnpm))
Comment thread
hyperpolymath marked this conversation as resolved.

if [ "$total" -eq 0 ]; then
echo "::notice::No JS/TS package manager in use — nothing to check."
if [ $((regen + npm + deno_cfg)) -eq 0 ]; then
echo "::notice::No JS package manager in use — nothing to check."
exit 0
fi

# Report the tier actually in use (LANGUAGE-POLICY.adoc §1).
# Report the tiers actually in use (LANGUAGE-POLICY.adoc §1).
[ "$bun" -eq 1 ] && echo "Bun — tier 1 (preferred)"
[ "$deno" -eq 1 ] && echo "Deno — tier 2 (accepted; existing projects are grandfathered)"
[ "$deno" -eq 1 ] && echo "Deno — tier 2"
[ "$deno_cfg" -eq 1 ] && echo "::notice::A deno.json is authored for Deno (tier 2, permitted; notice only, LANGUAGE-POLICY.adoc §1.3). Keep scripts in package.json so Bun runs them too."
[ "$pnpm" -eq 1 ] && echo "pnpm — tier 3"
[ "$npm" -eq 1 ] && echo "::warning::npm lockfile present. npm is tier 4, the last resort — permitted, never preferred. See LANGUAGE-POLICY.adoc §1."
[ "$npm" -eq 1 ] && echo "npm — tier 4 (last resort)"

if [ "$total" -gt 1 ]; then
echo "::error::Mixed toolchains: $total package managers have lockfiles in this repository."
echo "Two dependency graphs that can disagree is real drift. Pick one — preferring the"
echo "highest tier present — and delete the others' lockfiles."
if [ "$npm" -eq 1 ]; then
if [ "$regen" -gt 0 ]; then
echo "::error::Mixed toolchains: package-lock.json sits next to the js-runtimes locks."
echo "Two dependency graphs that can disagree is real drift."
else
echo "::error::package-lock.json is refused (standards#67). npm is the last resort, permitted but never preferred, and it keeps no lock."
fi
echo "Delete package-lock.json and let 'js-runtimes regen' write bun.lock, deno.lock"
echo "and pnpm-lock.yaml from package.json. See LANGUAGE-POLICY.adoc §1.4."
exit 1
fi

echo "✅ Single package manager in use."
echo "✅ One dependency graph in use."
8 changes: 4 additions & 4 deletions src/diagnostics/connectivity.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
# Connectivity diagnostics module

# Source utilities
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../utils/logging.sh"
source "${SCRIPT_DIR}/../utils/system.sh"
NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)"
source "${NR_UTILS_DIR}/logging.sh"
source "${NR_UTILS_DIR}/system.sh"

# Test basic connectivity
test_basic_connectivity() {
Expand Down Expand Up @@ -167,7 +167,7 @@ test_mtu() {
# Subtract 28 bytes for IP + ICMP header
local packet_size=$((size - 28))

if ping -c 1 -W 2 -M do -s ${packet_size} 8.8.8.8 >/dev/null 2>&1; then
if ping -c 1 -W 2 -M 'do' -s ${packet_size} 8.8.8.8 >/dev/null 2>&1; then
log_success " Packet size ${size} bytes: OK"
break
else
Expand Down
6 changes: 3 additions & 3 deletions src/diagnostics/dns.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
# DNS diagnostics module

# Source utilities
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../utils/logging.sh"
source "${SCRIPT_DIR}/../utils/system.sh"
NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)"
source "${NR_UTILS_DIR}/logging.sh"
source "${NR_UTILS_DIR}/system.sh"

# Check DNS configuration
check_dns_config() {
Expand Down
8 changes: 4 additions & 4 deletions src/diagnostics/firewall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
# Firewall diagnostics module

# Source utilities
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../utils/logging.sh"
source "${SCRIPT_DIR}/../utils/system.sh"
source "${SCRIPT_DIR}/../utils/privileges.sh"
NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)"
source "${NR_UTILS_DIR}/logging.sh"
source "${NR_UTILS_DIR}/system.sh"
source "${NR_UTILS_DIR}/privileges.sh"

# Check iptables rules
check_iptables() {
Expand Down
8 changes: 4 additions & 4 deletions src/diagnostics/interfaces.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
# Network interface diagnostics module

# Source utilities
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../utils/logging.sh"
source "${SCRIPT_DIR}/../utils/system.sh"
NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)"
source "${NR_UTILS_DIR}/logging.sh"
source "${NR_UTILS_DIR}/system.sh"

# Check network interfaces
check_interfaces() {
Expand Down Expand Up @@ -187,7 +187,7 @@ check_interface_conflicts() {
ip=$(get_interface_ip "${iface}")

if [[ -n "${ip}" ]]; then
if [[ -n "${ip_map[${ip}]}" ]]; then
if [[ -n "${ip_map[${ip}]:-}" ]]; then
log_error " Duplicate IP ${ip} on ${iface} and ${ip_map[${ip}]}"
issues=$((issues + 1))
else
Expand Down
6 changes: 3 additions & 3 deletions src/diagnostics/networkmanager.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
# NetworkManager diagnostics module

# Source utilities
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../utils/logging.sh"
source "${SCRIPT_DIR}/../utils/system.sh"
NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)"
source "${NR_UTILS_DIR}/logging.sh"
source "${NR_UTILS_DIR}/system.sh"

# Check NetworkManager status
check_nm_status() {
Expand Down
6 changes: 3 additions & 3 deletions src/diagnostics/routing.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
# Routing diagnostics module

# Source utilities
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../utils/logging.sh"
source "${SCRIPT_DIR}/../utils/system.sh"
NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)"
source "${NR_UTILS_DIR}/logging.sh"
source "${NR_UTILS_DIR}/system.sh"

# Check routing table
check_routing_table() {
Expand Down
23 changes: 9 additions & 14 deletions src/main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

set -euo pipefail

# Script directory
# Script directory. The modules sourced below resolve their own directory
# into NR_UTILS_DIR / NR_REPAIRS_DIR and must not assign SCRIPT_DIR.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Version
Expand Down Expand Up @@ -123,29 +124,23 @@ run_all_diagnostics() {
log_info "Network Manager: $(detect_network_manager)"
echo ""

# Run diagnostics
diagnose_interfaces
total_issues=$((total_issues + $?))
# Diagnostic failures report issues; collect them without triggering errexit.
diagnose_interfaces || total_issues=$((total_issues + $?))
echo ""

diagnose_routing
total_issues=$((total_issues + $?))
diagnose_routing || total_issues=$((total_issues + $?))
echo ""

diagnose_dns
total_issues=$((total_issues + $?))
diagnose_dns || total_issues=$((total_issues + $?))
echo ""

diagnose_connectivity
total_issues=$((total_issues + $?))
diagnose_connectivity || total_issues=$((total_issues + $?))
echo ""

diagnose_firewall
total_issues=$((total_issues + $?))
diagnose_firewall || total_issues=$((total_issues + $?))
echo ""

diagnose_networkmanager
total_issues=$((total_issues + $?))
diagnose_networkmanager || total_issues=$((total_issues + $?))
echo ""

# Summary
Expand Down
10 changes: 5 additions & 5 deletions src/repairs/dns.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
# DNS repair procedures

# Source utilities
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../utils/logging.sh"
source "${SCRIPT_DIR}/../utils/system.sh"
source "${SCRIPT_DIR}/../utils/privileges.sh"
source "${SCRIPT_DIR}/../utils/backup.sh"
NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)"
source "${NR_UTILS_DIR}/logging.sh"
source "${NR_UTILS_DIR}/system.sh"
source "${NR_UTILS_DIR}/privileges.sh"
source "${NR_UTILS_DIR}/backup.sh"

# Repair DNS configuration
repair_dns_config() {
Expand Down
8 changes: 4 additions & 4 deletions src/repairs/interfaces.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
# Network interface repair procedures

# Source utilities
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../utils/logging.sh"
source "${SCRIPT_DIR}/../utils/system.sh"
source "${SCRIPT_DIR}/../utils/privileges.sh"
NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)"
source "${NR_UTILS_DIR}/logging.sh"
source "${NR_UTILS_DIR}/system.sh"
source "${NR_UTILS_DIR}/privileges.sh"

# Bring up network interface
interface_up() {
Expand Down
8 changes: 4 additions & 4 deletions src/repairs/networkmanager.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
# NetworkManager repair procedures

# Source utilities
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../utils/logging.sh"
source "${SCRIPT_DIR}/../utils/system.sh"
source "${SCRIPT_DIR}/../utils/privileges.sh"
NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)"
source "${NR_UTILS_DIR}/logging.sh"
source "${NR_UTILS_DIR}/system.sh"
source "${NR_UTILS_DIR}/privileges.sh"

# Restart NetworkManager
restart_networkmanager() {
Expand Down
36 changes: 28 additions & 8 deletions src/repairs/routing.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
# Routing repair procedures

# Source utilities
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../utils/logging.sh"
source "${SCRIPT_DIR}/../utils/system.sh"
source "${SCRIPT_DIR}/../utils/privileges.sh"
NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)"
# Resolved now: the functions below source interfaces.sh after other
# modules have loaded.
NR_REPAIRS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${NR_UTILS_DIR}/logging.sh"
source "${NR_UTILS_DIR}/system.sh"
source "${NR_UTILS_DIR}/privileges.sh"

# Add default route
add_default_route() {
Expand Down Expand Up @@ -89,7 +92,16 @@ remove_duplicate_routes() {
return 0
}

# Repair default route
# Remove duplicate default routes if the initial route query succeeds;
# otherwise try renewing DHCP on the primary interface.
# Returns 0 when the initial `ip route show default` succeeds, even if its
# output is empty or the gateway is unreachable. The DHCP fallback returns 0
# only if renewal and a subsequent route query succeed; otherwise it returns 1,
# including when no primary interface is found.
# May remove routes and release/renew a DHCP lease. Requests root/sudo access
# and exits the shell if access cannot be obtained. With errexit active,
# unguarded failures such as interface lookup or sourcing helpers can terminate
# the caller before a return status is produced.
repair_default_route() {
log_section "Repair Default Route"

Expand Down Expand Up @@ -134,7 +146,7 @@ repair_default_route() {
log_step "Attempting to obtain gateway via DHCP"

# Renew DHCP which should set up routes
source "${SCRIPT_DIR}/interfaces.sh"
source "${NR_REPAIRS_DIR}/interfaces.sh"
if renew_dhcp "${primary}"; then
sleep 2

Expand All @@ -148,7 +160,15 @@ repair_default_route() {
return 1
}

# Flush and rebuild routing table
# Flush the main routing table and try to rebuild routes by renewing DHCP on
# the primary interface selected before the flush. May disconnect the network
# and release/renew a DHCP lease; no rollback is performed on failure.
# Returns 0 if DHCP renewal and the subsequent `ip route show default` succeed,
# even if the query output is empty. Returns 1 if no primary interface is found,
# renewal fails or the final query fails.
# Requests root/sudo access and exits the shell if access cannot be obtained.
# With errexit active, unguarded failures such as interface lookup, flushing
# routes or sourcing helpers can terminate the caller before returning.
flush_routing_table() {
log_section "Flush and Rebuild Routing Table"

Expand All @@ -171,7 +191,7 @@ flush_routing_table() {
log_step "Rebuilding routes via DHCP"

# Renew DHCP to rebuild routes
source "${SCRIPT_DIR}/interfaces.sh"
source "${NR_REPAIRS_DIR}/interfaces.sh"
if renew_dhcp "${primary}"; then
sleep 3

Expand Down
4 changes: 2 additions & 2 deletions src/utils/logging.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
# Logging utilities for network repair tool

# Source color utilities
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/colors.sh"
NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${NR_UTILS_DIR}/colors.sh"

# Log levels
declare -gA LOG_LEVELS=(
Expand Down
2 changes: 1 addition & 1 deletion src/utils/privileges.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ require_root() {
SUDO_KEEPER_PID=$!

# Kill sudo keeper on exit
trap "kill ${SUDO_KEEPER_PID} 2>/dev/null || true" EXIT
trap 'kill "${SUDO_KEEPER_PID}" 2>/dev/null || true' EXIT
}

# Run command with appropriate privileges
Expand Down
8 changes: 5 additions & 3 deletions tests/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,11 @@ test_safe_mode() {
print_result "FAIL" "Repairs should be blocked in safe mode"
fi

# Test that diagnose works without flags
if "${PROJECT_DIR}/network-repair" diagnose >/dev/null 2>&1 || \
"${PROJECT_DIR}/network-repair" diagnose 2>&1 | grep -q "Diagnostic"; then
# Diagnose returns 1 for network issues, but must complete in safe mode.
# Capture once so pipefail and grep closing early cannot mask the result.
local diagnose_status=0
output=$("${PROJECT_DIR}/network-repair" diagnose 2>&1) || diagnose_status=$?
if [[ ${diagnose_status} -le 1 && "${output}" == *"=== Diagnostic Summary ==="* ]]; then
print_result "PASS" "Diagnose works in safe mode"
else
print_result "FAIL" "Diagnose should work in safe mode"
Expand Down
Loading
Loading