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: = 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