Skip to content

Instantly share code, notes, and snippets.

@ruzickap
Created May 15, 2026 04:01
Show Gist options
  • Select an option

  • Save ruzickap/30bdc6c888647a1cab6a002091eb9f8d to your computer and use it in GitHub Desktop.

Select an option

Save ruzickap/30bdc6c888647a1cab6a002091eb9f8d to your computer and use it in GitHub Desktop.
Build custom OpenWrt sysupgrade firmware via the official Sysupgrade API
#!/usr/bin/env bash
# Build custom OpenWrt firmware using the Sysupgrade API
#
# Standalone script — no local Ansible code required.
# Intended for use as a GitHub Gist.
#
# Required environment variables:
# WIFI_SSID — WiFi network name (e.g. "MyNetwork")
# WIFI_PASSWORD — WiFi password (WPA3/SAE-mixed)
# PACKAGES — Space-separated list of extra packages to include
# (e.g. "bash htop luci-ssl mc")
# SSH_PUBLIC_KEY — SSH public key line for authorized_keys
# (e.g. "ssh-ed25519 AAAA... user@host")
#
# Optional environment variables:
# ROOT_PASSWORD — Root password; auto-generated if not set
#
# Usage:
# export WIFI_SSID="MyNetwork"
# export WIFI_PASSWORD="secret"
# export PACKAGES="bash block-mount bind-dig htop less mc msmtp-mta rsync telnet-bsd"
# export SSH_PUBLIC_KEY="$(cat ~/.ssh/id_ed25519.pub)"
# ./build-firmware-gist.sh ramips/mt7621 asus_rt-ax53u gate.xvx.cz
#
set -euo pipefail
TARGET="${1:?Usage: build-firmware-gist.sh <target> <profile> <hostname>}"
PROFILE="${2:?}"
HOSTNAME="${3:?}"
: "${WIFI_SSID:?Environment variable WIFI_SSID is not set}"
: "${WIFI_PASSWORD:?Environment variable WIFI_PASSWORD is not set}"
: "${PACKAGES:?Environment variable PACKAGES is not set}"
: "${SSH_PUBLIC_KEY:?Environment variable SSH_PUBLIC_KEY is not set}"
if [[ -z "${ROOT_PASSWORD:-}" ]]; then
RANDOM_SUFFIX=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 10 || true)
ROOT_PASSWORD="${HOSTNAME}12345${RANDOM_SUFFIX}"
fi
echo "Root password: ${ROOT_PASSWORD}"
SHORT_HOSTNAME="${HOSTNAME%%.*}"
DEFAULTS="exec > /root/uci-defaults.log 2>&1
set -x
# Set hostname
sed -i \"s/option hostname .*/option hostname '${SHORT_HOSTNAME}'/\" /etc/config/system
# Configure WiFi
uci delete wireless.default_radio1.disabled
uci set wireless.default_radio1.ssid='${WIFI_SSID} 5 GHz'
uci set wireless.default_radio1.encryption='sae-mixed'
uci set wireless.default_radio1.key='${WIFI_PASSWORD}'
uci commit wireless
# Set root password
printf '%s\n%s\n' \"${ROOT_PASSWORD}\" \"${ROOT_PASSWORD}\" | passwd root
# Allow SSH on WAN
cat >> /etc/config/firewall << 'FIREWALL_EOF'
config rule
option name 'Allow-SSH-WAN'
option src 'wan'
option dest_port '22'
option proto 'tcp'
option target 'ACCEPT'
FIREWALL_EOF
# Add SSH public key
mkdir -p /etc/dropbear
echo '${SSH_PUBLIC_KEY}' > /etc/dropbear/authorized_keys
chmod 600 /etc/dropbear/authorized_keys"
# Convert space-separated package list to JSON array
PACKAGES_JSON=$(echo "${PACKAGES}" | tr ' ' '\n' | jq -R . | jq -s .)
LATEST_STABLE=$(curl -sL --compressed https://sysupgrade.openwrt.org/api/v1/latest |
jq -r '.latest[] | select(test("-rc[0-9]+$") | not) | select(test("^[0-9]+\\.[0-9]+\\.[0-9]+$"))' |
sort -V | tail -n 1)
echo "Latest stable: ${LATEST_STABLE}"
BUILD_RESPONSE=$(curl -s --compressed -X POST https://sysupgrade.openwrt.org/api/v1/build \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg target "${TARGET}" \
--arg profile "${PROFILE}" \
--arg version "${LATEST_STABLE}" \
--argjson packages "${PACKAGES_JSON}" \
--arg defaults "${DEFAULTS}" \
'{target: $target, profile: $profile, version: $version, packages: $packages, diff_packages: false, defaults: $defaults}')")
REQUEST_HASH=$(jq -r '.request_hash' <<< "${BUILD_RESPONSE}")
if [[ "${REQUEST_HASH}" == "null" ]]; then
echo "Build request failed:"
jq . <<< "${BUILD_RESPONSE}"
exit 1
fi
while true; do
HTTP_CODE=$(curl -s --compressed -o /tmp/build_status.json -w '%{http_code}' \
"https://sysupgrade.openwrt.org/api/v1/build/${REQUEST_HASH}")
echo "Build status: ${HTTP_CODE} - waiting for build to complete..."
if [[ "${HTTP_CODE}" == "200" ]]; then
break
elif [[ "${HTTP_CODE}" == "202" ]]; then
sleep 10
else
echo "Build failed (HTTP ${HTTP_CODE}):"
jq . /tmp/build_status.json
exit 1
fi
done
BIN_DIR=$(jq -r '.bin_dir' /tmp/build_status.json)
IMAGE_NAME=$(jq -r '.images[] | select(.type == "sysupgrade") | .name' /tmp/build_status.json)
IMAGE_URL="https://sysupgrade.openwrt.org/store/${BIN_DIR}/${IMAGE_NAME}"
echo "Build status JSON: https://sysupgrade.openwrt.org/api/v1/build/${REQUEST_HASH}"
echo "sysupgrade -v -n -p ${IMAGE_URL}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment