mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
The package-openwrt.yml shellcheck step was failing on warnings that
are false positives for OpenWrt scripts:
SC2034 — USE_PROCD, START, STOP in /etc/init.d scripts are read by
rc.common, not by the script itself; standard shellcheck
cannot see the indirection.
SC3043 — `local` is undefined in strict POSIX sh, but OpenWrt uses
ash which supports it. The init scripts use `local`
extensively for parameter scoping.
SC2086, SC2089, SC2090 — firewall.sh constructs nft match clauses
with literal quotes that need to survive variable expansion
(`match='iifname "$TUN"'` then `nft ... $match accept`). The
lack of double-quoting around `$match` is intentional so
word-splitting yields separate nft arguments.
Adding these to the exclude list. SC2317 (unreachable code) and
SC1008 (rc.common shebang form) were already excluded.
528 lines
18 KiB
YAML
528 lines
18 KiB
YAML
name: OpenWrt Package
|
|
on:
|
|
push:
|
|
branches:
|
|
- master
|
|
- maint
|
|
- next
|
|
tags:
|
|
- "v*"
|
|
pull_request:
|
|
workflow_dispatch:
|
|
inputs:
|
|
arch:
|
|
description: "Target architecture (leave empty to build all)"
|
|
required: false
|
|
default: ""
|
|
|
|
env:
|
|
CARGO_TERM_COLOR: always
|
|
PACKAGE_NAME: "fips"
|
|
|
|
jobs:
|
|
determine-versioning:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
package_version: ${{ steps.version.outputs.package_version }}
|
|
release_channel: ${{ steps.channel.outputs.release_channel }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Derive package version
|
|
id: version
|
|
shell: bash
|
|
run: |
|
|
: ${GITHUB_OUTPUT:=/tmp/github_output}
|
|
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
|
|
echo "package_version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
|
else
|
|
BRANCH=$(echo "$GITHUB_REF_NAME" | sed 's|/|-|g')
|
|
HEIGHT=$(git rev-list --count HEAD)
|
|
HASH=$(git rev-parse --short HEAD)
|
|
echo "package_version=${BRANCH}.${HEIGHT}.${HASH}" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Determine release channel
|
|
id: channel
|
|
shell: bash
|
|
run: |
|
|
: ${GITHUB_OUTPUT:=/tmp/github_output}
|
|
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
|
|
TAG_NAME=${GITHUB_REF#refs/tags/}
|
|
if [[ $TAG_NAME =~ ^v[0-9]+\.[0-9]+\.[0-9]+-alpha ]]; then
|
|
echo "release_channel=alpha" >> "$GITHUB_OUTPUT"
|
|
elif [[ $TAG_NAME =~ ^v[0-9]+\.[0-9]+\.[0-9]+-beta ]]; then
|
|
echo "release_channel=beta" >> "$GITHUB_OUTPUT"
|
|
elif [[ $TAG_NAME =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
echo "release_channel=stable" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "release_channel=dev" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
else
|
|
echo "release_channel=dev" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
build:
|
|
name: Build .ipk (${{ matrix.openwrt_arch }})
|
|
runs-on: ubuntu-latest
|
|
needs: determine-versioning
|
|
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- build_arch: aarch64
|
|
openwrt_arch: aarch64_cortex-a53
|
|
rust_target: aarch64-unknown-linux-musl
|
|
rust_channel: stable
|
|
# MT3000, MT6000, Flint 2, RPi 3/4/5
|
|
# MIPS disabled: 32-bit MIPS lacks AtomicU64; needs portable-atomic crate
|
|
# - build_arch: mipsel
|
|
# openwrt_arch: mipsel_24kc
|
|
# rust_target: mipsel-unknown-linux-musl
|
|
# rust_channel: nightly
|
|
# - build_arch: mips
|
|
# openwrt_arch: mips_24kc
|
|
# rust_target: mips-unknown-linux-musl
|
|
# rust_channel: nightly
|
|
- build_arch: x86_64
|
|
openwrt_arch: x86_64
|
|
rust_target: x86_64-unknown-linux-musl
|
|
rust_channel: stable
|
|
# x86 routers / VMs
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Set SOURCE_DATE_EPOCH from git
|
|
run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV"
|
|
|
|
- name: Initialize
|
|
run: |
|
|
PACKAGE_FILENAME=${{ env.PACKAGE_NAME }}_${{ needs.determine-versioning.outputs.package_version }}_${{ matrix.openwrt_arch }}.ipk
|
|
echo "PACKAGE_FILENAME=$PACKAGE_FILENAME" >> $GITHUB_ENV
|
|
|
|
- name: Install Rust toolchain (stable)
|
|
if: matrix.rust_channel == 'stable'
|
|
uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
targets: ${{ matrix.rust_target }}
|
|
|
|
- name: Install Rust toolchain (nightly, Tier 3)
|
|
if: matrix.rust_channel == 'nightly'
|
|
uses: dtolnay/rust-toolchain@nightly
|
|
with:
|
|
components: rust-src
|
|
|
|
- name: Cache Cargo registry + build
|
|
if: ${{ env.ACT != 'true' }}
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
~/.cargo/registry
|
|
~/.cargo/git
|
|
target/${{ matrix.rust_target }}
|
|
key: openwrt-${{ matrix.rust_target }}-${{ hashFiles('**/Cargo.lock') }}
|
|
restore-keys: |
|
|
openwrt-${{ matrix.rust_target }}-
|
|
|
|
- name: Install cargo-zigbuild
|
|
run: cargo install cargo-zigbuild --version 0.19.8 --locked
|
|
|
|
- name: Install zig (required by cargo-zigbuild)
|
|
run: |
|
|
ZIG_VERSION="0.13.0"
|
|
ARCH=$(uname -m)
|
|
case "$ARCH" in
|
|
x86_64|amd64) ZIG_ARCH="x86_64" ;;
|
|
aarch64|arm64) ZIG_ARCH="aarch64" ;;
|
|
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
|
|
esac
|
|
curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}.tar.xz" | sudo tar xJ -C /opt
|
|
sudo ln -sf /opt/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}/zig /usr/local/bin/zig
|
|
zig version
|
|
|
|
- name: Install llvm-strip
|
|
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends llvm
|
|
|
|
- name: Install nak
|
|
shell: bash
|
|
run: |
|
|
NAK_VERSION="0.16.2"
|
|
ARCH=$(uname -m)
|
|
case "$ARCH" in
|
|
x86_64|amd64) NAK_ARCH="amd64" ;;
|
|
aarch64|arm64) NAK_ARCH="arm64" ;;
|
|
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
|
|
esac
|
|
curl -fsSL "https://github.com/fiatjaf/nak/releases/download/v${NAK_VERSION}/nak-v${NAK_VERSION}-linux-${NAK_ARCH}" \
|
|
-o /usr/local/bin/nak
|
|
chmod +x /usr/local/bin/nak
|
|
nak --version
|
|
|
|
- name: Install jq
|
|
run: |
|
|
if ! command -v jq &>/dev/null; then
|
|
sudo apt-get update && sudo apt-get install -y jq
|
|
fi
|
|
|
|
# Priority: HIVE_CI_NSEC from env (loom job) > repo secret > generate ephemeral
|
|
- name: Resolve signing key
|
|
id: keys
|
|
shell: bash
|
|
env:
|
|
SECRET_NSEC: ${{ secrets.HIVE_CI_NSEC }}
|
|
run: |
|
|
: ${GITHUB_OUTPUT:=/tmp/github_output}
|
|
if [ -n "${HIVE_CI_NSEC:-}" ]; then
|
|
echo "Using HIVE_CI_NSEC from loom job environment"
|
|
NSEC="$HIVE_CI_NSEC"
|
|
elif [ -n "$SECRET_NSEC" ]; then
|
|
echo "Using HIVE_CI_NSEC from repository secrets"
|
|
NSEC="$SECRET_NSEC"
|
|
else
|
|
echo "No nsec provided -- generating ephemeral keypair"
|
|
NSEC=$(nak key generate)
|
|
fi
|
|
|
|
PUBKEY=$(echo "$NSEC" | nak key public)
|
|
echo "::add-mask::$NSEC"
|
|
echo "nsec=$NSEC" >> "$GITHUB_OUTPUT"
|
|
echo "pubkey=$PUBKEY" >> "$GITHUB_OUTPUT"
|
|
echo "Publisher pubkey (hex): $PUBKEY"
|
|
- name: Build .ipk
|
|
env:
|
|
PKG_VERSION: ${{ needs.determine-versioning.outputs.package_version }}
|
|
LLVM_STRIP: llvm-strip
|
|
run: ./packaging/openwrt-ipk/build-ipk.sh --arch ${{ matrix.build_arch }}
|
|
|
|
- name: Install shellcheck (if missing)
|
|
shell: bash
|
|
run: |
|
|
if ! command -v shellcheck >/dev/null 2>&1; then
|
|
sudo apt-get update && sudo apt-get install -y --no-install-recommends shellcheck
|
|
fi
|
|
shellcheck --version
|
|
|
|
- name: Lint shipped shell scripts
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
FILES_DIR=packaging/openwrt-ipk/files
|
|
# Scripts shipped inside the .ipk. The init scripts use the OpenWrt
|
|
# `#!/bin/sh /etc/rc.common` shebang; tell shellcheck to treat them
|
|
# as POSIX sh and silence the unrecognized-shebang warning (SC1008).
|
|
# SC2317 (unreachable command) fires on rc.common's externally-invoked
|
|
# start_service/stop_service/reload_service hooks.
|
|
TARGETS=(
|
|
"$FILES_DIR/etc/init.d/fips"
|
|
"$FILES_DIR/etc/init.d/fips-gateway"
|
|
"$FILES_DIR/etc/fips/firewall.sh"
|
|
"$FILES_DIR/etc/hotplug.d/net/99-fips"
|
|
"$FILES_DIR/etc/uci-defaults/90-fips-setup"
|
|
)
|
|
fail=0
|
|
for f in "${TARGETS[@]}"; do
|
|
if [ ! -f "$f" ]; then
|
|
echo "FAIL: missing $f"
|
|
fail=1
|
|
continue
|
|
fi
|
|
echo "==> shellcheck $f"
|
|
if shellcheck --shell=sh --exclude=SC1008,SC2317,SC2034,SC3043,SC2086,SC2089,SC2090 "$f"; then
|
|
echo " PASS"
|
|
else
|
|
echo " FAIL"
|
|
fail=1
|
|
fi
|
|
done
|
|
if [ "$fail" -ne 0 ]; then
|
|
echo "shellcheck FAILED"
|
|
exit 1
|
|
fi
|
|
echo "shellcheck PASS (${#TARGETS[@]} scripts)"
|
|
|
|
- name: Sysctl drop-in syntax check
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
FILES_DIR=packaging/openwrt-ipk/files
|
|
TARGETS=(
|
|
"$FILES_DIR/etc/sysctl.d/fips-gateway.conf"
|
|
"$FILES_DIR/etc/sysctl.d/fips-bridge.conf"
|
|
)
|
|
fail=0
|
|
for conf in "${TARGETS[@]}"; do
|
|
if [ ! -f "$conf" ]; then
|
|
echo "FAIL: missing $conf"
|
|
fail=1
|
|
continue
|
|
fi
|
|
echo "==> validating $conf"
|
|
lineno=0
|
|
file_ok=1
|
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
lineno=$((lineno + 1))
|
|
# Skip comments and blank lines.
|
|
case "$line" in
|
|
''|\#*) continue ;;
|
|
esac
|
|
# Match: <key> = <value> where key is dotted lower-id and value is
|
|
# an integer (sysctl drop-ins shipped here are all numeric toggles).
|
|
if ! [[ "$line" =~ ^[a-z0-9_.-]+[[:space:]]*=[[:space:]]*-?[0-9]+[[:space:]]*$ ]]; then
|
|
echo " FAIL line $lineno: $line"
|
|
file_ok=0
|
|
fi
|
|
done < "$conf"
|
|
if [ "$file_ok" -eq 1 ]; then
|
|
echo " PASS"
|
|
else
|
|
fail=1
|
|
fi
|
|
done
|
|
if [ "$fail" -ne 0 ]; then
|
|
echo "sysctl drop-in syntax check FAILED"
|
|
exit 1
|
|
fi
|
|
echo "sysctl drop-in syntax check PASS"
|
|
|
|
- name: Verify ipk structural integrity
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
IPK="dist/${{ env.PACKAGE_FILENAME }}"
|
|
if [ ! -f "$IPK" ]; then
|
|
echo "FAIL: produced ipk not found at $IPK"
|
|
exit 1
|
|
fi
|
|
echo "==> file type:"
|
|
file "$IPK"
|
|
|
|
# OpenWrt .ipk = tar.gz containing debian-binary + control.tar.gz +
|
|
# data.tar.gz (NOT an ar archive like Debian's .deb).
|
|
WORK=$(mktemp -d)
|
|
trap 'rm -rf "$WORK"' EXIT
|
|
|
|
tar -xzf "$IPK" -C "$WORK"
|
|
echo "==> top-level entries:"
|
|
ls -la "$WORK"
|
|
|
|
# Top-level structural assertions.
|
|
fail=0
|
|
for entry in debian-binary control.tar.gz data.tar.gz; do
|
|
if [ ! -f "$WORK/$entry" ]; then
|
|
echo "FAIL: missing top-level $entry"
|
|
fail=1
|
|
else
|
|
echo " PASS top-level: $entry"
|
|
fi
|
|
done
|
|
if [ "$fail" -ne 0 ]; then exit 1; fi
|
|
|
|
# debian-binary content sanity.
|
|
dbin_content=$(cat "$WORK/debian-binary" | tr -d '[:space:]')
|
|
if [ "$dbin_content" != "2.0" ]; then
|
|
echo "FAIL: debian-binary content is '$dbin_content' (expected 2.0)"
|
|
exit 1
|
|
fi
|
|
echo " PASS debian-binary content: 2.0"
|
|
|
|
# Inspect data.tar.gz contents.
|
|
DATA_LIST="$WORK/data.list"
|
|
tar -tzf "$WORK/data.tar.gz" > "$DATA_LIST"
|
|
echo "==> data.tar.gz entry count: $(wc -l < "$DATA_LIST")"
|
|
|
|
# Required filesystem entries inside data.tar.gz. Entries are
|
|
# produced with a leading "./" by build-ipk.sh.
|
|
REQUIRED=(
|
|
./usr/bin/fips
|
|
./usr/bin/fipsctl
|
|
./usr/bin/fipstop
|
|
./usr/bin/fips-gateway
|
|
./etc/init.d/fips
|
|
./etc/init.d/fips-gateway
|
|
./etc/fips/fips.yaml
|
|
./etc/fips/firewall.sh
|
|
./etc/dnsmasq.d/fips.conf
|
|
./etc/sysctl.d/fips-gateway.conf
|
|
./etc/sysctl.d/fips-bridge.conf
|
|
./etc/hotplug.d/net/99-fips
|
|
./etc/uci-defaults/90-fips-setup
|
|
./lib/upgrade/keep.d/fips
|
|
)
|
|
for path in "${REQUIRED[@]}"; do
|
|
if grep -Fxq "$path" "$DATA_LIST"; then
|
|
echo " PASS data: $path"
|
|
else
|
|
echo " FAIL data: missing $path"
|
|
fail=1
|
|
fi
|
|
done
|
|
|
|
# Inspect control.tar.gz: must contain control file + maintainer scripts.
|
|
CTRL_LIST="$WORK/control.list"
|
|
tar -tzf "$WORK/control.tar.gz" > "$CTRL_LIST"
|
|
echo "==> control.tar.gz entry count: $(wc -l < "$CTRL_LIST")"
|
|
for path in ./control ./conffiles ./postinst ./prerm; do
|
|
if grep -Fxq "$path" "$CTRL_LIST"; then
|
|
echo " PASS control: $path"
|
|
else
|
|
echo " FAIL control: missing $path"
|
|
fail=1
|
|
fi
|
|
done
|
|
|
|
if [ "$fail" -ne 0 ]; then
|
|
echo "ipk structural verification FAILED"
|
|
exit 1
|
|
fi
|
|
echo "ipk structural verification PASS"
|
|
|
|
- name: SHA-256 hashes
|
|
run: |
|
|
echo "==> Binaries:"
|
|
sha256sum target/${{ matrix.rust_target }}/release/fips target/${{ matrix.rust_target }}/release/fipsctl target/${{ matrix.rust_target }}/release/fipstop
|
|
echo "==> Package:"
|
|
sha256sum dist/${{ env.PACKAGE_FILENAME }}
|
|
|
|
- name: Upload artifact (GitHub only)
|
|
if: ${{ env.ACT != 'true' }}
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: ${{ env.PACKAGE_FILENAME }}
|
|
path: dist/${{ env.PACKAGE_FILENAME }}
|
|
retention-days: 30
|
|
|
|
- name: Upload to Blossom
|
|
id: blossom_upload
|
|
shell: bash
|
|
env:
|
|
BLOSSOM_SERVER: "https://blossom.primal.net"
|
|
NSEC: ${{ steps.keys.outputs.nsec }}
|
|
run: |
|
|
: ${GITHUB_OUTPUT:=/tmp/github_output}
|
|
|
|
UPLOAD_RESPONSE=$(nak blossom upload \
|
|
--server "$BLOSSOM_SERVER" \
|
|
--sec "$NSEC" \
|
|
"dist/${{ env.PACKAGE_FILENAME }}" < /dev/null)
|
|
|
|
echo "Upload response:"
|
|
echo "$UPLOAD_RESPONSE"
|
|
|
|
FILE_HASH=$(echo "$UPLOAD_RESPONSE" | jq -r '.sha256')
|
|
if [ -z "$FILE_HASH" ] || [ "$FILE_HASH" = "null" ]; then
|
|
echo "Failed to extract hash from upload response"
|
|
exit 1
|
|
fi
|
|
|
|
BLOSSOM_URL="${BLOSSOM_SERVER}/${FILE_HASH}"
|
|
echo "url=$BLOSSOM_URL" >> "$GITHUB_OUTPUT"
|
|
echo "hash=$FILE_HASH" >> "$GITHUB_OUTPUT"
|
|
echo "Uploaded to Blossom: $BLOSSOM_URL"
|
|
|
|
- name: Publish NIP-94 release event
|
|
id: publish
|
|
shell: bash
|
|
env:
|
|
RELAYS: "wss://relay.damus.io wss://nos.lol wss://nostr.mom wss://offchain.pub"
|
|
NSEC: ${{ steps.keys.outputs.nsec }}
|
|
run: |
|
|
: ${GITHUB_OUTPUT:=/tmp/github_output}
|
|
set -e
|
|
|
|
VERSION="${{ needs.determine-versioning.outputs.package_version }}"
|
|
CHANNEL="${{ needs.determine-versioning.outputs.release_channel }}"
|
|
|
|
nak event --sec "$NSEC" -k 1063 \
|
|
-c "FIPS Package: ${{ env.PACKAGE_NAME }} for ${{ matrix.openwrt_arch }}" \
|
|
--tag url="${{ steps.blossom_upload.outputs.url }}" \
|
|
--tag m="application/octet-stream" \
|
|
--tag x="${{ steps.blossom_upload.outputs.hash }}" \
|
|
--tag ox="${{ steps.blossom_upload.outputs.hash }}" \
|
|
--tag filename="${{ env.PACKAGE_FILENAME }}" \
|
|
--tag A="${{ matrix.openwrt_arch }}" \
|
|
--tag v="$VERSION" \
|
|
--tag n="${{ env.PACKAGE_NAME }}" \
|
|
--tag compression="none" \
|
|
> event.json 2> event.err
|
|
|
|
if [ ! -s event.json ]; then
|
|
echo "Failed to create event"
|
|
cat event.err 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
|
|
echo "=== Event JSON ==="
|
|
cat event.json
|
|
echo "=================="
|
|
|
|
EVENT_ID=$(jq -r '.id' event.json)
|
|
if [ -z "$EVENT_ID" ] || [ "$EVENT_ID" = "null" ]; then
|
|
echo "Failed to extract event ID"
|
|
exit 1
|
|
fi
|
|
|
|
# Publish to relays
|
|
cat event.json | nak event $RELAYS 2>&1
|
|
|
|
echo "eventId=$EVENT_ID" >> "$GITHUB_OUTPUT"
|
|
echo "Published NIP-94 event: $EVENT_ID"
|
|
|
|
- name: Verify NIP-94 event on relays
|
|
if: ${{ steps.publish.outputs.eventId != '' }}
|
|
env:
|
|
EVENT_ID: ${{ steps.publish.outputs.eventId }}
|
|
RELAYS: "wss://relay.damus.io wss://nos.lol wss://nostr.mom wss://offchain.pub"
|
|
run: |
|
|
echo "Verifying event $EVENT_ID on relays..."
|
|
FOUND=0
|
|
for relay in $RELAYS; do
|
|
echo "Checking $relay..."
|
|
RESULT=$(nak req -i "$EVENT_ID" "$relay" 2>/dev/null || echo "")
|
|
if [ -n "$RESULT" ]; then
|
|
echo "Found on $relay"
|
|
FOUND=1
|
|
else
|
|
echo "Not found on $relay"
|
|
fi
|
|
done
|
|
|
|
if [ $FOUND -eq 0 ]; then
|
|
echo "Warning: Event not found on any relay yet (may still be propagating)"
|
|
else
|
|
echo "Event verified on at least one relay"
|
|
fi
|
|
|
|
- name: Build Summary
|
|
run: |
|
|
echo "Build Summary for ${{ matrix.openwrt_arch }}:"
|
|
echo " Package: ${{ env.PACKAGE_FILENAME }}"
|
|
echo " Release EventId: ${{ steps.publish.outputs.eventId }}"
|
|
echo " Blossom URL: ${{ steps.blossom_upload.outputs.url }}"
|
|
|
|
release:
|
|
name: Publish GitHub Release (github only)
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
if: startsWith(github.ref, 'refs/tags/')
|
|
permissions:
|
|
contents: write
|
|
|
|
steps:
|
|
- name: Download all .ipk artifacts
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
path: dist
|
|
merge-multiple: true
|
|
|
|
- name: Create release
|
|
uses: softprops/action-gh-release@v2
|
|
with:
|
|
files: dist/*.ipk
|
|
generate_release_notes: true
|