#!/bin/sh # Pinscope from CI: upload a build, and roll it out to devices in waves. # # curl -fsSL https://pinscope.dev/ci.sh | sh -s -- [options] FILE... # # FILE... .bin images and .elf files to upload (an image and its ELF # from the same build, for decoded crash reports) # --sbom FILE the build's SBOM (SPDX or CycloneDX, e.g. from idf.py sbom-create # or west spdx), attached to the uploaded image; may be repeated # --token TOKEN an API token that may deploy (or PINSCOPE_TOKEN) # --project ID the project's number, from its dashboard URL (or PINSCOPE_PROJECT) # --rollout start a rollout of the uploaded image # --tag TAG only to devices with this tag # --waves 10,50,100 wave sizes in percent (default 10,50,100) # --failures N stop after more than N failed devices (default 0) # --wait wait until the rollout ends; fail the job if it stopped # # Needs only sh and curl. Docs: https://pinscope.dev/docs#ci set -eu url=${PINSCOPE_URL:-https://pinscope.dev} token=${PINSCOPE_TOKEN:-} project=${PINSCOPE_PROJECT:-} rollout=0 wait=0 tag='' waves='10,50,100' failures=0 files='' sboms='' while [ $# -gt 0 ]; do case "$1" in --token) token=$2; shift 2 ;; --project) project=$2; shift 2 ;; --url) url=$2; shift 2 ;; --rollout) rollout=1; shift ;; --tag) tag=$2; shift 2 ;; --waves) waves=$2; shift 2 ;; --failures) failures=$2; shift 2 ;; --wait) wait=1; rollout=1; shift ;; --sbom) sboms="$sboms $2"; shift 2 ;; -h|--help) sed -n '2,22p' "$0" 2>/dev/null || true; exit 0 ;; -*) echo "pinscope: unknown option $1" >&2; exit 2 ;; *) files="$files $1"; shift ;; esac done [ -n "$token" ] || { echo "pinscope: give --token or set PINSCOPE_TOKEN" >&2; exit 2; } [ -n "$project" ] || { echo "pinscope: give --project or set PINSCOPE_PROJECT" >&2; exit 2; } [ -n "$files" ] || { echo "pinscope: name the files to upload" >&2; exit 2; } api() { # method path [curl options...] method=$1 path=$2; shift 2 out=$(curl -sS -X "$method" -H "Authorization: Bearer $token" -w '\n%{http_code}' "$@" "$url/api$path") || return 1 code=$(printf '%s' "$out" | tail -n 1) body=$(printf '%s' "$out" | sed '$d') if [ "$code" -ge 400 ]; then echo "pinscope: $method $path: $(printf '%s' "$body" | sed -n 's/.*"error":"\([^"]*\)".*/\1/p') ($code)" >&2 return 1 fi printf '%s' "$body" } image='' for f in $(echo "$files") $(echo "$sboms"); do [ -f "$f" ] || { echo "pinscope: no such file $f" >&2; exit 1; }; done for f in $(echo "$files"); do name=$(basename "$f") res=$(api POST "/projects/$project/firmware" -H 'content-type: application/octet-stream' -H "x-file-name: $name" --data-binary "@$f") uploaded=$(printf '%s' "$res" | sed -n 's/.*"uploaded":{\([^}]*\)}.*/\1/p') id=$(printf '%s' "$uploaded" | sed -n 's/.*"id":\([0-9]*\).*/\1/p') kind=$(printf '%s' "$uploaded" | sed -n 's/.*"kind":"\([a-z]*\)".*/\1/p') version=$(printf '%s' "$uploaded" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p') case "$uploaded" in *'"existing":true'*) note=' (already there)' ;; *) note='' ;; esac echo "pinscope: uploaded $name: $kind${version:+ $version}$note" [ "$kind" = bin ] && image=$id done if [ -n "$sboms" ]; then [ -n "$image" ] || { echo "pinscope: --sbom needs a .bin among the files to attach it to" >&2; exit 2; } for f in $(echo "$sboms"); do res=$(api POST "/projects/$project/firmware/$image/sbom" -H 'content-type: application/octet-stream' -H "x-file-name: $(basename "$f")" --data-binary "@$f") echo "pinscope: SBOM $(basename "$f"): $(printf '%s' "$res" | sed -n 's/.*"format":"\([^"]*\)".*"linked":\([0-9]*\).*/\1, \2 packages in the firmware/p')" done echo "pinscope: known vulnerabilities: $url/#/p/$project/security" fi [ "$rollout" = 1 ] || exit 0 [ -n "$image" ] || { echo "pinscope: --rollout needs a .bin among the files" >&2; exit 2; } json="{\"firmwareId\":$image,\"waves\":[$waves],\"maxFailures\":$failures${tag:+,\"tag\":\"$tag\"}}" res=$(api POST "/projects/$project/rollouts" -H 'content-type: application/json' --data "$json") rid=$(printf '%s' "$res" | sed -n 's/^{"id":\([0-9]*\).*/\1/p') total=$(printf '%s' "$res" | sed -n 's/.*"counts":{"total":\([0-9]*\).*/\1/p') echo "pinscope: rollout $rid started to $total devices: $url/#/p/$project/rollouts" [ "$wait" = 1 ] || exit 0 while :; do sleep 30 res=$(api GET "/projects/$project/rollouts/$rid") || continue st=$(printf '%s' "$res" | sed -n 's/.*"state":"\([a-z]*\)","reason".*/\1/p') counts=$(printf '%s' "$res" | sed -n 's/.*"counts":{\([^}]*\)}.*/\1/p' | tr -d '"') echo "pinscope: $st, $counts" case "$st" in done) exit 0 ;; stopped) echo "pinscope: $(printf '%s' "$res" | sed -n 's/.*"reason":"\([^"]*\)".*/\1/p')" >&2; exit 1 ;; esac done