Created
May 15, 2026 04:01
-
-
Save ruzickap/30bdc6c888647a1cab6a002091eb9f8d to your computer and use it in GitHub Desktop.
Build custom OpenWrt sysupgrade firmware via the official Sysupgrade API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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