name: Build on: schedule: - cron: '0 3 * * *' workflow_dispatch: inputs: platform: description: Platform required: false jobs: preflight: name: Preflight runs-on: ubuntu-latest outputs: should_build: ${{ steps.gate.outputs.should_build }} head_sha: ${{ steps.gate.outputs.head_sha }} short_sha: ${{ steps.gate.outputs.short_sha }} build_id: ${{ steps.gate.outputs.build_id }} built_at: ${{ steps.gate.outputs.built_at }} steps: - uses: actions/checkout@v4 - id: gate env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | HEAD=$(git rev-parse HEAD) SHORT=$(git rev-parse --short HEAD) BUILD_ID="nightly-$(date -u +%Y%m%d)-${SHORT}" BUILT_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ) # Nightly rebuilds unconditionally on schedule. builder.sh re-clones # OpenIPC/firmware at HEAD on every run, so the relevant inputs are # mostly outside this repo (firmware HEAD, toolchain release, kernel # tarball, vendor osdrv, individual package upstreams). The previous # gate skipped when only this repo's HEAD matched the last published # nightly, which made upstream fixes (e.g. an openhisilicon mipi_rx # regression fix that lands in firmware) invisible to users until # something here happened to change. Always build on schedule. echo "Build: $BUILD_ID (event=${{ github.event_name }})" echo "should_build=true" >> "$GITHUB_OUTPUT" echo "head_sha=$HEAD" >> "$GITHUB_OUTPUT" echo "short_sha=$SHORT" >> "$GITHUB_OUTPUT" echo "build_id=$BUILD_ID" >> "$GITHUB_OUTPUT" echo "built_at=$BUILT_AT" >> "$GITHUB_OUTPUT" buildroot: name: Firmware needs: preflight if: needs.preflight.outputs.should_build == 'true' runs-on: ubuntu-latest env: BUILD_ID: ${{ needs.preflight.outputs.build_id }} BUILD_SHA: ${{ needs.preflight.outputs.head_sha }} BUILD_PLATFORM: ${{ matrix.platform }} strategy: fail-fast: false matrix: platform: - gk7202v300_lite_cootli_camv0103 - gk7202v300_lite_ipg-g3-wr - gk7202v300_lite_xg521 - gk7205v200_lite_cpplus-cp-unc-ta21l2c - gk7205v200_lite_tiandy-tc-c321n - gk7205v200_lite_vixand-ipc-1 - gk7205v200_lite_vixand-iph-5-4g - gk7205v210_lite_tiandy-tc-c32qn - gk7205v210_lite_vixand-ivg-g3s - gk7205v210_lite_vixand-ivg-g4f-a - gk7205v210_lite_vixand-ivg-g4f-a-w - gk7205v210_lite_vixand-ivg-g4h - gk7205v300_lite_vixand-ivg-g6s - gk7205v300_lite_vixand-ivg-g6s-w - hi3516cv200_lite_trassir-tr-d4121ir1-v2 - hi3516ev300_ultimate_rostelecom-ipc8232swc-we - hi3516ev300_ultimate_rvi-1ncmw2028 - hi3518ev200_lite_smartwares-cip-37210 - hi3518ev200_lite_switcam-hs303 - hi3518ev200_lite_switcam-hs303-v2 - hi3518ev200_lite_vstarcam-c8892wip - hi3518ev200_lite_qtech-qvc-ipc-136w - hi3518ev300_lite_bathhouse - hi3518ev300_lite_xiaomi-mjsxj02hl - hi3518ev200_ultimate_lenovo-snowman-1080p - ssc30kd_lite_cmcc-ds-ytj5301 - ssc325_lite_imilab-ec3-cmsxj25a - ssc325_lite_imou-c22cp - ssc325_lite_tp-link-tapo-c310-v1 - ssc325_lite_trassir-tr-w2c1-v1 - ssc325de_lite_imou-c22ep-s2 - ssc333_lite_meari-speed-6s - ssc333_lite_tp-link-tapo-c110-v2 - ssc333_lite_tp-link-tapo-c110-v26 - ssc333_lite_babysense-see-hd-ip206 - ssc333_lite_vstarcam-c43s_b - ssc335_lite_tp-link-tapo-c110-v1 - ssc335_lite_tp-link-tapo-c310-v220 - ssc335_lite_trassir-tr-w2c1-v2 - ssc335de_lite_dahua-hfw1230sp-v2 - ssc335de_lite_imou-c22e-s2-v2 - ssc335de_lite_uniview-c1l-2wn-g - ssc337_lite_h3c-tc2101 - ssc337_lite_tiandy-tc-c321n-v2 - ssc337_lite_tp-link-tapo-c110-v1 - ssc337de_ultimate_foscam-x5 - ssc338q_fpv_caddx-fly - ssc338q_fpv_emax-wyvern-link - ssc338q_fpv_openipc-mario-aio - ssc338q_fpv_openipc-thinker-aio - ssc338q_fpv_openipc-urllc-aio - ssc338q_fpv_runcam-wifilink - ssc377qe_fpv_ccdcam-im50q01-tipoman - t10_lite_hb-wifi-z6 - t10_lite_jvs-ingt10-gqs60ep - t20_ultimate_azarton-c1 - t20_lite_ec37-t11 - t31_lite_vstarcam-cs55 - t21_lite_chinamobile-hdc-51-a5-v12 - t21_lite_smartwares-cip-37210at - t21_lite_xyx-06s - t21_lite_wansview-q5-1080p - t23_lite_jooan-a6m-u - t23_lite_jooan-q3r-u - t23_lite_lsc-3215672 - t31_lite_aceline-aip-o4 - t31_lite_wansview-q5-2k - t31_lite_aoni-ep01j05 - t31_lite_chinamobile-hdc-51-a6-v11 - t31_lite_cmcc-hdc-51-a6-v10 - t31_lite_chinatelecom-y4h-50 - t31_lite_wyze-v3b - t31_lite_xiaomi-mjsxj03hl - t31_lite_xiaomi-mjsxj03hl-jxq03 - t31_lite_tuya-gv7630-t31-ptz - t31_ultimate_azarton-c1-t31x - t31_ultimate_gcraftsman-gca50 - t31_lite_zte-k540 - t40_lite_movols-mo-805p # APFPV - ssc30kq_apfpv_greg-generic-bu-eu - ssc30kq_apfpv_greg-generic-cu-eu - ssc338q_apfpv - ssc338q_apfpv_greg-generic-bu-eu - ssc338q_apfpv_greg-generic-cu-eu - ssc378qe_apfpv # FPV - hi3516ev200_fpv - hi3516ev300_fpv - hi3536dv100_fpv - gk7205v200_fpv - gk7205v210_fpv - gk7205v300_fpv - ssc30kq_fpv - ssc338q_fpv - ssc378qe_fpv # Ruby - ssc30kq_rubyfpv_generic - ssc338q_rubyfpv_generic - ssc338q_rubyfpv_thinker_internal_wifi # LTE - hi3516ev200_lte - hi3516ev300_lte - gk7205v200_lte - gk7205v300_lte # Venc - gk7205v200_venc - gk7205v210_venc - gk7205v300_venc # Mini - hi3516cv300_mini - hi3518ev200_mini steps: - name: Checkout source uses: actions/checkout@v4 - name: Prepare firmware run: | echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf echo CACHE_DATE=$(date +%m) >> ${GITHUB_ENV} INPUT=${{inputs.platform}} MATRIX=$(echo ${{matrix.platform}} | cut -d_ -f1) if [ -z ${INPUT} ] || [ ${INPUT} = ${MATRIX} ]; then echo RUN=${MATRIX} >> ${GITHUB_ENV} fi - name: Setup ccache if: env.RUN uses: actions/cache@v4 with: path: /tmp/ccache key: ${{matrix.platform}}-${{env.CACHE_DATE}} # builder.sh does `rm -rf openipc` at the top of every run to force a # clean re-clone of OpenIPC/firmware, which also nukes the default # `openipc/output/dl/` directory Buildroot uses for downloaded source # tarballs. Without a cache, every cron run re-downloads ~1-3 GB of # tarballs from upstream mirrors — and any one of them randomly # failing (e.g. cgit `snapshot/` URLs that regenerate tarballs per # request with shifting sha256, like wireguard-tools on # git.zx2c4.com) takes down the matrix entry for that board. See # kaeru wireguard-zx2c4-tarball-regeneration-hash-drift for the # specific incident this caching addresses. # # Cache to /tmp/builder-dl (outside the openipc/ tree so builder.sh's # rm -rf doesn't touch it) and point Buildroot at it via BR2_DL_DIR # env var (honored per Buildroot manual section "Location of source # tarballs"). Mirrors OpenIPC/firmware/.github/workflows/build.yml's # output/dl cache, adapted for builder's clean-clone behavior. - name: Setup dl cache if: env.RUN uses: actions/cache@v4 with: path: /tmp/builder-dl key: builder-dl-${{env.CACHE_DATE}} restore-keys: builder-dl- - name: Refresh moving-ref package downloads if: env.RUN run: | # builder caches BR2_DL_DIR (/tmp/builder-dl) under a monthly key # (date +%m) and actions/cache only saves on a key miss, so the first build # of the month freezes it for weeks. Packages pinned to a moving ref keep a # constant download filename, so buildroot reuses the stale tarball — even # though builder re-clones firmware HEAD each run for freshness. Two forms: # - dash: -.tar.gz e.g. majestic-webui-dist.tar.gz # - dot: ..tar.bz2 e.g. majestic.t31.lite.master.tar.bz2 # The dot form is majestic's S3 tarball: it LOOKS S3-pinned but is a moving # `master` build re-uploaded every majestic CI run, with no .hash to force a # refetch — so it must be evicted too. (Empirically: a 06-14 nightly shipped a # ~2-week-old majestic-webui; later a t31 lite image shipped a pre-x-groups # majestic against the current webui — "No settings groups in schema".) Drop # both forms so buildroot re-fetches the current ref. Only genuinely # content-addressed tarballs (semver / SHA in the name) keep their cache. find /tmp/builder-dl -type f -regextype posix-extended \ -regex '.*[-.](HEAD|master|main|dist)\.tar\.(gz|bz2|xz)' \ -print -delete 2>/dev/null || true - name: Build firmware if: env.RUN run: | export GIT_HASH=$(git rev-parse --short ${GITHUB_SHA}) export GIT_BRANCH=${GITHUB_REF_NAME} echo GIT_HASH=${GIT_HASH} >> ${GITHUB_ENV} echo GIT_BRANCH=${GIT_BRANCH} >> ${GITHUB_ENV} mkdir -p /tmp/ccache ln -s /tmp/ccache ${HOME}/.ccache mkdir -p /tmp/builder-dl export BR2_DL_DIR=/tmp/builder-dl NAME=${{matrix.platform}} # Retry budget for transient upstream toolchain/CDN flakes # (matches OpenIPC/firmware/.github/workflows/build.yml). backoffs="30 60 120 300 600 1200" attempt=1 for sleep_for in $backoffs ""; do bash builder.sh ${NAME} && break if [ -z "$sleep_for" ]; then echo "::error::build failed after ${attempt} attempts" exit 1 fi echo "::warning::attempt ${attempt} failed, retrying after ${sleep_for}s" sleep "$sleep_for" attempt=$((attempt + 1)) done cd openipc TIME=$(date -d @${SECONDS} +%M:%S) echo TIME=${TIME} >> ${GITHUB_ENV} COMMON=$(echo ${NAME} | awk -F_ '{print NF-1}') NORFW=$(find output/images -name openipc*nor*) if [ ! -z ${NORFW} ]; then if [ ${COMMON} -eq 1 ]; then echo NORFW=openipc/${NORFW} >> ${GITHUB_ENV} else mv ${NORFW} ../${NAME}-nor.tgz echo NORFW=${NAME}-nor.tgz >> ${GITHUB_ENV} fi fi NANDFW=$(find output/images -name openipc*nand*) if [ ! -z ${NANDFW} ]; then if [ ${COMMON} -eq 1 ]; then echo NANDFW=openipc/${NANDFW} >> ${GITHUB_ENV} else mv ${NANDFW} ../${NAME}-nand.tgz echo NANDFW=${NAME}-nand.tgz >> ${GITHUB_ENV} fi fi if [ -z ${NORFW} ] && [ -z ${NANDFW} ]; then exit 1 fi - name: Build size report if: env.NORFW || env.NANDFW run: | cd openipc NAME=${{matrix.platform}} make BOARD=${NAME} size-report || \ { echo "::warning::size-report failed for ${NAME}"; exit 0; } SIZES=$(find output/images -name 'sizes.*.json' | head -1) if [ -z "${SIZES}" ]; then exit 0 fi # Mirror the .tgz rename trick: compound matrix entries # (`__-`) share `_` and # would collide on the firmware-side `sizes.-.json` # filename when uploaded to the same release tag. Rename to # `sizes..json` so every compound device gets its own asset. COMMON=$(echo ${NAME} | awk -F_ '{print NF-1}') if [ ${COMMON} -eq 1 ]; then echo "SIZES=openipc/${SIZES}" >> ${GITHUB_ENV} else mv ${SIZES} ../sizes.${NAME}.json echo "SIZES=sizes.${NAME}.json" >> ${GITHUB_ENV} fi - name: Upload firmware (dated) if: env.NORFW || env.NANDFW uses: softprops/action-gh-release@v2 with: tag_name: ${{ needs.preflight.outputs.build_id }} prerelease: true body: | sha=${{ needs.preflight.outputs.head_sha }} short=${{ needs.preflight.outputs.short_sha }} built_at=${{ needs.preflight.outputs.built_at }} files: | ${{env.NORFW}} ${{env.NANDFW}} ${{env.SIZES}} - name: Upload firmware (rolling nightly) if: env.NORFW || env.NANDFW uses: softprops/action-gh-release@v2 with: tag_name: nightly body: | sha=${{ needs.preflight.outputs.head_sha }} short=${{ needs.preflight.outputs.short_sha }} built_at=${{ needs.preflight.outputs.built_at }} files: | ${{env.NORFW}} ${{env.NANDFW}} ${{env.SIZES}} - name: Upload firmware (latest — legacy alias) if: env.NORFW || env.NANDFW uses: softprops/action-gh-release@v2 with: tag_name: latest files: | ${{env.NORFW}} ${{env.NANDFW}} ${{env.SIZES}} - name: Send binary if: env.NORFW run: | TG_MSG="Build: ${BUILD_ID}\nCommit: ${GIT_HASH}\nBranch: ${GIT_BRANCH}\nTime: ${TIME}\n\n" TG_ICON="\xE2\x9C\x85 GitHub Actions" TG_HEADER=$(echo -e ${TG_MSG}${TG_ICON}) TG_TOKEN=${{secrets.TELEGRAM_TOKEN_BOT_OPENIPC}} TG_CHANNEL=${{secrets.TELEGRAM_CHANNEL_OPENIPC_DEV}} HTTP=$(curl -s -o /dev/null -w %{http_code} https://api.telegram.org/bot${TG_TOKEN}/sendDocument -F chat_id=${TG_CHANNEL} -F caption="${TG_HEADER}" -F document=@${NORFW}) echo Telegram response: ${HTTP}