| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
|
| 16 |
|
| 17 |
|
| 18 |
|
| 19 |
set -euo pipefail |
| 20 |
|
| 21 |
SERVER="${MNW_OTA_SERVER:-https://makenot.work}" |
| 22 |
EMAIL="${MNW_OTA_EMAIL:-}" |
| 23 |
PASSWORD="${MNW_OTA_PASSWORD:-}" |
| 24 |
API_KEY="${MNW_OTA_API_KEY:-}" |
| 25 |
|
| 26 |
SLUG="" |
| 27 |
VERSION="" |
| 28 |
TARGET="" |
| 29 |
ARCH="" |
| 30 |
ARTIFACT="" |
| 31 |
NOTES="" |
| 32 |
SIGNATURE="" |
| 33 |
|
| 34 |
usage() { |
| 35 |
echo "Usage: $0 --slug SLUG --version X.Y.Z --target OS --arch ARCH --artifact FILE" |
| 36 |
echo "" |
| 37 |
echo "Required:" |
| 38 |
echo " --slug App slug (e.g. goingson, balanced-breakfast, audiofiles)" |
| 39 |
echo " --version Semver version (e.g. 0.2.2)" |
| 40 |
echo " --target Target OS: linux, darwin, windows" |
| 41 |
echo " --arch Architecture: x86_64, aarch64" |
| 42 |
echo " --artifact Path to the built artifact file" |
| 43 |
echo "" |
| 44 |
echo "Optional:" |
| 45 |
echo " --notes Release notes (default: empty)" |
| 46 |
echo " --signature Minisign signature for Tauri verification" |
| 47 |
echo " --email MNW account email (overrides MNW_OTA_EMAIL)" |
| 48 |
echo " --password MNW account password (overrides MNW_OTA_PASSWORD)" |
| 49 |
echo " --api-key SyncKit API key (overrides MNW_OTA_API_KEY)" |
| 50 |
echo " --server Server URL (overrides MNW_OTA_SERVER)" |
| 51 |
echo "" |
| 52 |
echo "Environment variables: MNW_OTA_EMAIL, MNW_OTA_PASSWORD, MNW_OTA_API_KEY, MNW_OTA_SERVER" |
| 53 |
exit 1 |
| 54 |
} |
| 55 |
|
| 56 |
while [[ $# -gt 0 ]]; do |
| 57 |
case $1 in |
| 58 |
--slug) SLUG="$2"; shift 2 ;; |
| 59 |
--version) VERSION="$2"; shift 2 ;; |
| 60 |
--target) TARGET="$2"; shift 2 ;; |
| 61 |
--arch) ARCH="$2"; shift 2 ;; |
| 62 |
--artifact) ARTIFACT="$2"; shift 2 ;; |
| 63 |
--notes) NOTES="$2"; shift 2 ;; |
| 64 |
--signature) SIGNATURE="$2"; shift 2 ;; |
| 65 |
--email) EMAIL="$2"; shift 2 ;; |
| 66 |
--password) PASSWORD="$2"; shift 2 ;; |
| 67 |
--api-key) API_KEY="$2"; shift 2 ;; |
| 68 |
--server) SERVER="$2"; shift 2 ;; |
| 69 |
-h|--help) usage ;; |
| 70 |
*) echo "Unknown option: $1"; usage ;; |
| 71 |
esac |
| 72 |
done |
| 73 |
|
| 74 |
|
| 75 |
[[ -z "$SLUG" ]] && { echo "Error: --slug required"; usage; } |
| 76 |
[[ -z "$VERSION" ]] && { echo "Error: --version required"; usage; } |
| 77 |
[[ -z "$TARGET" ]] && { echo "Error: --target required"; usage; } |
| 78 |
[[ -z "$ARCH" ]] && { echo "Error: --arch required"; usage; } |
| 79 |
[[ -z "$ARTIFACT" ]] && { echo "Error: --artifact required"; usage; } |
| 80 |
[[ -z "$EMAIL" ]] && { echo "Error: MNW_OTA_EMAIL or --email required"; usage; } |
| 81 |
[[ -z "$PASSWORD" ]] && { echo "Error: MNW_OTA_PASSWORD or --password required"; usage; } |
| 82 |
[[ -z "$API_KEY" ]] && { echo "Error: MNW_OTA_API_KEY or --api-key required"; usage; } |
| 83 |
|
| 84 |
[[ ! -f "$ARTIFACT" ]] && { echo "Error: artifact file not found: $ARTIFACT"; exit 1; } |
| 85 |
|
| 86 |
FILE_SIZE=$(stat -f%z "$ARTIFACT" 2>/dev/null || stat -c%s "$ARTIFACT" 2>/dev/null) |
| 87 |
echo "Publishing OTA update: $SLUG v$VERSION ($TARGET/$ARCH, ${FILE_SIZE} bytes)" |
| 88 |
|
| 89 |
|
| 90 |
echo " Authenticating..." |
| 91 |
AUTH_RESPONSE=$(curl -sf -X POST "$SERVER/api/sync/auth" \ |
| 92 |
-H "Content-Type: application/json" \ |
| 93 |
-d "{\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\",\"api_key\":\"$API_KEY\"}") |
| 94 |
|
| 95 |
TOKEN=$(echo "$AUTH_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])") |
| 96 |
APP_ID=$(echo "$AUTH_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['app_id'])") |
| 97 |
echo " Authenticated (app: $APP_ID)" |
| 98 |
|
| 99 |
|
| 100 |
echo " Creating release v$VERSION..." |
| 101 |
RELEASE_BODY=$(python3 -c "import json,sys; print(json.dumps({'version':sys.argv[1],'notes':sys.argv[2],'signature':sys.argv[3]}))" "$VERSION" "$NOTES" "$SIGNATURE") |
| 102 |
RELEASE_RESPONSE=$(curl -sf -X POST "$SERVER/api/sync/ota/apps/$APP_ID/releases" \ |
| 103 |
-H "Content-Type: application/json" \ |
| 104 |
-H "Authorization: Bearer $TOKEN" \ |
| 105 |
-d "$RELEASE_BODY") |
| 106 |
|
| 107 |
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") |
| 108 |
echo " Release created: $RELEASE_ID" |
| 109 |
|
| 110 |
|
| 111 |
echo " Registering artifact ($TARGET/$ARCH, $FILE_SIZE bytes)..." |
| 112 |
ARTIFACT_BODY=$(printf '{"target":"%s","arch":"%s","file_size":%s}' "$TARGET" "$ARCH" "$FILE_SIZE") |
| 113 |
ARTIFACT_RESPONSE=$(curl -sf -X POST "$SERVER/api/sync/ota/apps/$APP_ID/releases/$RELEASE_ID/artifacts" \ |
| 114 |
-H "Content-Type: application/json" \ |
| 115 |
-H "Authorization: Bearer $TOKEN" \ |
| 116 |
-d "$ARTIFACT_BODY") |
| 117 |
|
| 118 |
UPLOAD_URL=$(echo "$ARTIFACT_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['upload_url'])") |
| 119 |
echo " Got presigned upload URL" |
| 120 |
|
| 121 |
|
| 122 |
echo " Uploading artifact..." |
| 123 |
curl -sf -X PUT "$UPLOAD_URL" \ |
| 124 |
-H "Content-Type: application/octet-stream" \ |
| 125 |
--data-binary "@$ARTIFACT" |
| 126 |
echo " Upload complete" |
| 127 |
|
| 128 |
|
| 129 |
echo " Verifying updater endpoint..." |
| 130 |
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ |
| 131 |
"$SERVER/api/sync/ota/$SLUG/$TARGET/$ARCH/0.0.1") |
| 132 |
|
| 133 |
if [[ "$HTTP_CODE" == "200" ]]; then |
| 134 |
echo " Updater check returns 200 — update is live" |
| 135 |
else |
| 136 |
echo " Warning: updater check returned $HTTP_CODE (expected 200)" |
| 137 |
echo " The release was created but the updater endpoint may not be serving it yet." |
| 138 |
fi |
| 139 |
|
| 140 |
echo "" |
| 141 |
echo "Published: $SLUG v$VERSION ($TARGET/$ARCH)" |
| 142 |
echo "Updater URL: $SERVER/api/sync/ota/$SLUG/$TARGET/$ARCH/$VERSION" |
| 143 |
|