#!/usr/bin/env bash # Package an already-built native Agent and publish an independently signed feed. # Used after verify/web-build gates for dev, and after all release builds for tags. set -euo pipefail channel="${1:?usage: publish-openwrt.sh }" rid="${2:?}"; source_dir="${3:?}" [[ "$channel" == dev || "$channel" == release ]] || exit 2 [[ "$rid" == linux-musl-arm64 ]] || exit 2 if [[ "$channel" == release ]]; then [[ "${CI_COMMIT_TAG:-}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo 'release feed requires a stable version tag' >&2; exit 2; } else [[ -z "${CI_COMMIT_TAG:-}" && "${CI_PIPELINE_EVENT:-}" == push && "${CI_COMMIT_BRANCH:-}" == main && "${CI_COMMIT_MESSAGE:-}" == *'[CI]'* ]] \ || { echo 'dev feed requires a main push with [CI]' >&2; exit 2; } fi [[ -n "${RELEASE_SIGNING_KEY:-}" && -n "${GITEA_TOKEN:-}" ]] || { echo 'OpenWrt publishing requires signing and Gitea secrets' >&2; exit 2; } root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" source "$root/ci/version.sh" version="$MOSHIRO_VERSION" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT mkdir "$work/stage" install -m 755 "$source_dir/moshiro-agent" "$source_dir/fleet-enroll" "$work/stage/" for library in "$source_dir"/lib*.so.*; do [[ ! -f "$library" ]] || install -m 644 "$library" "$work/stage/" done install -m 755 "$root/packaging/openwrt/install.sh" "$work/stage/openwrt.sh" install -m 755 "$root/packaging/openwrt/native-agent.init" "$work/stage/agent.init" install -m 755 "$root/packaging/openwrt/updater.init" "$work/stage/updater.init" members=() for file in "$work/stage/"*; do members+=("$(basename "$file")"); done tar --sort=name --owner=0 --group=0 --numeric-owner -czf "$work/package.tar.gz" -C "$work/stage" "${members[@]}" hash="$(sha256sum "$work/package.tar.gz" | cut -d' ' -f1)" size="$(stat -c%s "$work/package.tar.gz")" package="agent-$rid-$hash.tar.gz" seq="$(git -C "$root" show -s --format=%ct HEAD)" # Commit time is stable across retries; equal timestamps intentionally do not auto-upgrade. python3 - "$channel" "$rid" "$version" "$package" "$hash" "$size" "$seq" > "$work/manifest.json" <<'PY' import json, sys channel, rid, version, package, sha256, size, seq = sys.argv[1:] print(json.dumps(dict(schema=1, component='agent', channel=channel, rid=rid, version=version, package=package, sha256=sha256, size=int(size), seq=int(seq)), separators=(',', ':')), end='') PY chmod 700 "$work" printf '%s\n' "$RELEASE_SIGNING_KEY" > "$work/key.pem" chmod 600 "$work/key.pem" openssl dgst -sha256 -sign "$work/key.pem" -out "$work/signature" "$work/manifest.json" base64 -w0 "$work/signature" > "$work/manifest.sig" base="${GITEA_BASE:-http://127.0.0.1:3100}" base="${base%/}/api/packages/justme/generic/moshiro-openwrt" # Credentials only in a protected curl config, never command-line arguments or output. printf 'header = "Authorization: token %s"\n' "$GITEA_TOKEN" > "$work/curl.conf" chmod 600 "$work/curl.conf" upload() { local path="$1" file="$2" mutable="${3:-false}" code if [[ "$mutable" == true ]]; then code="$(curl --silent --show-error --config "$work/curl.conf" --request DELETE --output "$work/response" --write-out '%{http_code}' "$base/$path")" [[ "$code" == 204 || "$code" == 200 || "$code" == 404 ]] || { echo "cannot replace OpenWrt feed: HTTP $code" >&2; return 1; } fi code="$(curl --silent --show-error --retry 3 --config "$work/curl.conf" --upload-file "$file" --output "$work/response" --write-out '%{http_code}' "$base/$path")" # Immutable content-addressed archives may already exist after a retry. [[ "$code" == 2* || ( "$mutable" == false && "$code" == 409 ) ]] || { echo "OpenWrt upload failed: HTTP $code" >&2; return 1; } } upload "$version/$package" "$work/package.tar.gz" # A transient manifest/signature mismatch is rejected, then retried by the device. # Publish the channel pointer only after its immutable payload is downloadable. upload "$channel/manifest-$rid.sig" "$work/manifest.sig" true upload "$channel/manifest-$rid.json" "$work/manifest.json" true echo "published signed OpenWrt Agent $version ($channel/$rid)"