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 }} apk_version: ${{ steps.version.outputs.apk_version }} release_channel: ${{ steps.channel.outputs.release_channel }} steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Derive package version id: version shell: bash run: | : ${GITHUB_OUTPUT:=/tmp/github_output} # package_version is the human-readable label used in artifact # filenames; apk_version is the apk-tools-compatible string embedded # in the .apk metadata. apk_version is built directly from the same # structured inputs (tag, or commit height) — no reparse of the # flattened package_version. See packaging/openwrt-apk/apk-version.sh. if [[ "$GITHUB_REF" == refs/tags/* ]]; then echo "package_version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" echo "apk_version=$(sh packaging/openwrt-apk/apk-version.sh tag "${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" echo "apk_version=$(sh packaging/openwrt-apk/apk-version.sh dev "${HEIGHT}")" >> "$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 compile-binaries: name: Cross-compile (${{ 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: nostr-relay-pool 0.44 uses std::sync::atomic::AtomicU64 # directly (fips's own atomics already use portable_atomic). Re-enable # once an upstream portable-atomic patch lands (or via [patch.crates-io]). # - 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@v6 with: fetch-depth: 0 - name: Install Rust toolchain (stable) if: matrix.rust_channel == 'stable' uses: actions-rust-lang/setup-rust-toolchain@v1 with: target: ${{ matrix.rust_target }} cache: false rustflags: '' - 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@v5 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 # Cross-compile + strip once; both the .ipk and .apk packagers consume # these artifacts via --bin-dir, so the Rust build runs a single time # per architecture instead of once per package format. - name: Cross-compile and strip binaries run: | set -euo pipefail cargo zigbuild --release --target ${{ matrix.rust_target }} \ --bin fips --bin fipsctl --bin fipstop --bin fips-gateway RELEASE_DIR="target/${{ matrix.rust_target }}/release" mkdir -p out for b in fips fipsctl fipstop fips-gateway; do llvm-strip "$RELEASE_DIR/$b" 2>/dev/null || true cp "$RELEASE_DIR/$b" "out/$b" done ls -lh out/ - name: Upload binaries artifact uses: actions/upload-artifact@v7 with: name: fips-bins-${{ matrix.openwrt_arch }} path: out/ retention-days: 1 build: name: Build .ipk (${{ matrix.openwrt_arch }}) runs-on: ubuntu-latest needs: [determine-versioning, compile-binaries] strategy: fail-fast: false matrix: # Must be a subset of compile-binaries' arches (this job consumes those # binary artifacts). Currently both ship aarch64 + x86_64. include: - build_arch: aarch64 openwrt_arch: aarch64_cortex-a53 - build_arch: x86_64 openwrt_arch: x86_64 steps: - uses: actions/checkout@v6 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: Download prebuilt binaries uses: actions/download-artifact@v8 with: name: fips-bins-${{ matrix.openwrt_arch }} path: bins - 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 }} run: ./packaging/openwrt-ipk/build-ipk.sh --arch ${{ matrix.build_arch }} --bin-dir "$GITHUB_WORKSPACE/bins" - 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 bins/fips bins/fipsctl bins/fipstop echo "==> Package:" sha256sum dist/${{ env.PACKAGE_FILENAME }} - name: Upload artifact (GitHub only) if: ${{ env.ACT != 'true' }} uses: actions/upload-artifact@v7 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 format="ipk" \ --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 }}" build-apk: name: Build .apk (${{ matrix.openwrt_arch }}) runs-on: ubuntu-latest needs: [determine-versioning, compile-binaries] strategy: fail-fast: false matrix: # Must be a subset of compile-binaries' arches. include: - build_arch: aarch64 openwrt_arch: aarch64_cortex-a53 # MT3000, MT6000, Flint 2, RPi 3/4/5 on OpenWrt 25+ - build_arch: x86_64 openwrt_arch: x86_64 # x86 routers / VMs on OpenWrt 25+ env: # apk-tools commit OpenWrt pins for the .apk (ADB) format. Keep in sync # with package/system/apk/Makefile in the targeted OpenWrt release so the # packages we produce are readable by the apk on the device. APK_TOOLS_VERSION: "3.0.5" APK_TOOLS_COMMIT: "b5a31c0d865342ad80be10d68f1bb3d3ad9b0866" steps: - uses: actions/checkout@v6 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 }}.apk echo "PACKAGE_FILENAME=$PACKAGE_FILENAME" >> $GITHUB_ENV - name: Download prebuilt binaries uses: actions/download-artifact@v8 with: name: fips-bins-${{ matrix.openwrt_arch }} path: bins - name: Install fakeroot run: sudo apt-get update && sudo apt-get install -y --no-install-recommends fakeroot # apk mkpkg lives in apk-tools v3, which is not packaged for Ubuntu, so we # build the pinned release from source. This is the SDK-free equivalent of # how the .ipk path uses plain tar — one small C tool, no OpenWrt SDK. - name: Build apk-tools (${{ env.APK_TOOLS_VERSION }}) from source run: | set -euo pipefail sudo apt-get install -y --no-install-recommends \ git ca-certificates build-essential meson ninja-build pkg-config \ zlib1g-dev libssl-dev libzstd-dev liblzma-dev lua5.4-dev scdoc git clone --quiet https://gitlab.alpinelinux.org/alpine/apk-tools.git /tmp/apk-tools cd /tmp/apk-tools git checkout --quiet "${APK_TOOLS_COMMIT}" meson setup build ninja -C build src/apk APK_BIN=/tmp/apk-tools/build/src/apk "$APK_BIN" --version 2>/dev/null || "$APK_BIN" version 2>/dev/null || true echo "APK_BIN=$APK_BIN" >> "$GITHUB_ENV" - name: Build .apk env: PKG_VERSION: ${{ needs.determine-versioning.outputs.package_version }} APK_VERSION: ${{ needs.determine-versioning.outputs.apk_version }} run: ./packaging/openwrt-apk/build-apk.sh --arch ${{ matrix.build_arch }} --bin-dir "$GITHUB_WORKSPACE/bins" - name: Verify apk structural integrity shell: bash run: | set -euo pipefail APK="dist/${{ env.PACKAGE_FILENAME }}" if [ ! -s "$APK" ]; then echo "FAIL: produced apk not found or empty at $APK" exit 1 fi echo "==> file type:"; file "$APK" # apk v3 packages are ADB containers; dump the whole manifest with the # apk-tools we just built. Print it in full so the exact schema is # always visible in the log if an assertion needs adjusting. DUMP=$(mktemp) "$APK_BIN" adbdump "$APK" > "$DUMP" 2>/dev/null || { echo "FAIL: 'apk adbdump' could not read $APK"; exit 1; } echo "==> full adbdump:"; cat "$DUMP" fail=0 # Package metadata (flat keys under info:). for needle in "name: fips" "version: ${{ needs.determine-versioning.outputs.apk_version }}" "arch: ${{ matrix.openwrt_arch }}"; do if grep -qF "$needle" "$DUMP"; then echo " PASS meta: $needle" else echo " FAIL meta: missing '$needle'"; fail=1 fi done # installed-size reflects the bundled binaries (4 stripped Rust # binaries, several MB). A payload regression that drops them shows up # here regardless of how the path tree is formatted. SIZE=$(awk '/^[[:space:]]*installed-size:/ {print $2; exit}' "$DUMP") echo " installed-size: ${SIZE:-unknown}" if [ -z "${SIZE:-}" ] || [ "$SIZE" -lt 1000000 ]; then echo " FAIL: installed-size implausibly small (binaries missing?)"; fail=1 else echo " PASS: installed-size >= 1MB" fi # The adbdump paths: block is hierarchical. Each directory is a # top-level list item "- name: "; its files are # "- name: " nested one indent level deeper under "files:". # (There are no "path:" keys.) Reconstruct full file paths by keying # off the indentation of the directory-level list items. RECON=$(awk ' /^paths:/ {p=1; diri=-1; next} p && /^[^ #-]/ {p=0} # a new top-level key ends paths: !p {next} match($0, /^ *- /) { ind=RLENGTH; rest=substr($0, RLENGTH+1) if (diri==-1) diri=ind # first list item = directory indent if (ind==diri) { # directory entry (or the root acl: entry) if (rest ~ /^name: /) { dir=rest; sub(/^name: /,"",dir) } else dir="" next } if (rest ~ /^name: /) { # deeper item = a file under files: f=rest; sub(/^name: /,"",f); print (dir==""?f:dir"/"f) } } ' "$DUMP") echo "==> reconstructed paths:"; printf '%s\n' "$RECON" for path in \ 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; do if printf '%s\n' "$RECON" | grep -qxF "$path"; then echo " PASS path: $path" else echo " FAIL path: missing $path"; fail=1 fi done if [ "$fail" -ne 0 ]; then echo "apk structural verification FAILED" exit 1 fi echo "apk structural verification PASS" - name: SHA-256 hashes run: | echo "==> Binaries:" sha256sum bins/fips bins/fipsctl bins/fipstop echo "==> Package:" sha256sum dist/${{ env.PACKAGE_FILENAME }} - name: Upload artifact (GitHub only) if: ${{ env.ACT != 'true' }} uses: actions/upload-artifact@v7 with: name: ${{ env.PACKAGE_FILENAME }} path: dist/${{ env.PACKAGE_FILENAME }} retention-days: 30 - 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: 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 }} (apk)" \ --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 format="apk" \ --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: Build Summary run: | echo "Build Summary for ${{ matrix.openwrt_arch }} (apk):" echo " Package: ${{ env.PACKAGE_FILENAME }}" echo " apk version: ${{ needs.determine-versioning.outputs.apk_version }}" 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, build-apk] if: startsWith(github.ref, 'refs/tags/') permissions: contents: write steps: - name: Download package artifacts uses: actions/download-artifact@v8 with: # Only the .ipk/.apk packages (named fips__.*), not the # fips-bins-* raw-binary artifacts shared between the build jobs. pattern: fips_* path: dist merge-multiple: true - name: Generate OpenWrt release checksums run: | cd dist find . -maxdepth 1 -type f \( -name '*.ipk' -o -name '*.apk' \) -printf '%P\n' \ | LC_ALL=C sort \ | xargs sha256sum \ > checksums-openwrt.txt - name: Create release uses: softprops/action-gh-release@v2 with: files: | dist/*.ipk dist/*.apk dist/checksums-openwrt.txt generate_release_notes: true