iOS: release/dev/prov builds, publish.sh, dev icon variants

release = org.cdnutter.<name> (main, Gitea release + Sideload)
dev     = org.cdnutter.<name>-dev (dev branch, Gitea pre-release + Sideload Developer)
prov    = org.cdnutter.<name>-prov (personal signed, never published)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-11-10 10:18:32 -08:00
1 parent fabca5d1aa
commit ec1cbdfe75
9 files changed
+132 -58

No files matched your search

+11 -3
View File
@@ -338,12 +338,20 @@ docs/specs.txt original design doc for the desktop separator
```sh ```sh
cd ios cd ios
scripts/build-ipa.sh # unsigned build/public/stemify-<version>.ipa scripts/build-remote.sh # from DevOps: dev build on WHITE-M1, pulled back to build/dev
scripts/build-remote.sh # from DevOps: build on White-M1 and pull IPAs back to build/{public,prov} scripts/build-remote.sh release # release build, build/release
scripts/build-remote.sh prov # personal signed build, build/prov
scripts/build-ipa.sh release|dev|prov # same thing directly on the Mac
xcodegen generate # or open Stemify.xcodeproj in Xcode xcodegen generate # or open Stemify.xcodeproj in Xcode
``` ```
The version lives in `ios/project.yml` (`MARKETING_VERSION` and `CURRENT_PROJECT_VERSION`). To release, tag `ios-v<version>`, attach `stemify-<version>.ipa`, then run `GITEA_TOKEN=... ios/scripts/make-source.py` and commit the new `ios/source.json`. There are three builds, each with its own bundle ID so they install side by side:
- `org.cdnutter.stemify` (build/release): releases from `main`. `scripts/publish.sh release` tags `ios-v<version>`, uploads the IPA to Gitea and Sideload, and regenerates `ios/source.json`.
- `org.cdnutter.stemify-dev` (build/dev): "Stemify Dev" with the ribbon icon, built from the `dev` branch. `scripts/publish.sh dev` makes a Gitea pre-release `ios-v<version>-dev.<build>` and uploads it to Sideload Developer. Bump `CURRENT_PROJECT_VERSION` for each one.
- `org.cdnutter.stemify-prov` (build/prov): "Stemify Prov", signed with the personal profile in `ios/prov/` so CarPlay and Siri work. Never published.
The version lives in `ios/project.yml` (`VERSION_LABEL`, `MARKETING_VERSION` and `CURRENT_PROJECT_VERSION`).
**Android** needs JDK 21 and the Android SDK (`android/local.properties` points at it): **Android** needs JDK 21 and the Android SDK (`android/local.properties` points at it):
@@ -1 +1,8 @@
{"images":[{"filename":"icon.png","idiom":"universal","platform":"ios","size":"1024x1024"}],"info":{"author":"xcode","version":1}} {
"images" : [
{ "filename" : "icon.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" },
{ "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "icon-dark.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" },
{ "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "filename" : "icon-tinted.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }
],
"info" : { "author" : "xcode", "version" : 1 }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+36 -38
View File
@@ -1,23 +1,37 @@
#!/bin/bash #!/bin/bash
# Builds build/public/stemify-<version>.ipa on a Mac: ad-hoc signed, for releases and re-signing with Sideloadly/AltStore. # Builds one Stemify IPA on a Mac. ios/scripts/build-remote.sh runs this on WHITE-M1 from DevOps.
# With prov/Stemify.mobileprovision, prov/key.p12 and P12_PASS set, it also writes a signed # build-ipa.sh release build/release/stemify-<v>.ipa org.cdnutter.stemify, ad-hoc signed. Gitea releases (main) + Sideload.
# build/prov/stemify-<version>-signed.ipa (keeps CarPlay/Siri) for the owner's own device. # build-ipa.sh dev build/dev/stemify-<v>-dev.ipa org.cdnutter.stemify-dev, "Stemify Dev", ribbon icon.
# Finished IPAs are kept per version; build/work is scratch and is wiped each run. # Gitea pre-releases (dev branch) + Sideload Developer.
# "build-ipa.sh dev" builds only build/dev/stemify-<version>-dev.ipa: bundle id org.cdnutter.stemify-dev, # build-ipa.sh prov build/prov/stemify-<v>-prov.ipa org.cdnutter.stemify-prov, "Stemify Prov", signed with the
# named "Stemify Dev", ad-hoc signed, so it installs next to the release app. For internal tests, never released. # personal profile (CarPlay, Siri). Never published anywhere.
# "build-ipa.sh prov" builds only the signed prov IPA (no public one). # prov needs prov/Stemify.mobileprovision (App ID org.cdnutter.stemify-prov), prov/key.p12 and P12_PASS.
# ios/scripts/build-remote.sh runs this on White-M1 and copies build/public and build/prov back to DevOps. # prov/StemifyWidgets.mobileprovision (org.cdnutter.stemify-prov.widgets) is optional, without it the widgets are left out.
# build/work is scratch and is wiped each run.
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
export PATH=/opt/homebrew/bin:$PATH export PATH=/opt/homebrew/bin:$PATH
B=build; OUT=$B/work; APP=$OUT/Payload/Stemify.app; EXT=$APP/PlugIns/StemifyWidgets.appex B=build; OUT=$B/work; APP=$OUT/Payload/Stemify.app; EXT=$APP/PlugIns/StemifyWidgets.appex
MODE=${1:-release} MODE=${1:-dev}
if [ "$MODE" = dev ]; then APP_ID=org.cdnutter.stemify-dev; APP_NAME="Stemify Dev"; APP_ICON=AppIconDev; DD=$B/dd-dev case $MODE in
else APP_ID=org.cdnutter.stemify; APP_NAME=Stemify; APP_ICON=AppIcon; DD=$B/dd; fi release) APP_ID=org.cdnutter.stemify; APP_NAME=Stemify; APP_ICON=AppIcon; SUFFIX= ;;
rm -rf $OUT && mkdir -p $OUT/Payload $B/public $B/prov $B/dev dev) APP_ID=org.cdnutter.stemify-dev; APP_NAME="Stemify Dev"; APP_ICON=AppIconDev; SUFFIX=-dev ;;
prov) APP_ID=org.cdnutter.stemify-prov; APP_NAME="Stemify Prov"; APP_ICON=AppIcon; SUFFIX=-prov ;;
*) echo "usage: $0 release|dev|prov" >&2; exit 1 ;;
esac
DD=$B/dd-$MODE
# "1.0 RC 1" -> 1.0-rc1, used in file names and release tags. # "1.0 RC 1" -> 1.0-rc1, used in file names and release tags.
VERSION=$(sed -nE 's/^ *VERSION_LABEL: "(.*)"/\1/p' project.yml | tr 'A-Z' 'a-z' | sed -E 's/ rc /-rc/; s/ /-/g') VERSION=$(sed -nE 's/^ *VERSION_LABEL: "(.*)"/\1/p' project.yml | tr 'A-Z' 'a-z' | sed -E 's/ (rc|beta) /-\1/; s/ /-/g')
IPA=$B/$MODE/stemify-$VERSION$SUFFIX.ipa
if [ "$MODE" = prov ]; then
for f in prov/Stemify.mobileprovision prov/key.p12; do [ -f $f ] || { echo "missing $f" >&2; exit 1; }; done
[ -n "${P12_PASS:-}" ] || { echo "P12_PASS not set" >&2; exit 1; }
PROF_ID=$(security cms -D -i prov/Stemify.mobileprovision | plutil -extract Entitlements.application-identifier raw -)
[ "${PROF_ID#*.}" = "$APP_ID" ] || { echo "prov/Stemify.mobileprovision is for ${PROF_ID#*.}, need a profile for $APP_ID" >&2; exit 1; }
fi
rm -rf $OUT && mkdir -p $OUT/Payload $B/$MODE
# xcodebuild compiles the app, the widget extension, the asset catalog and the App Intents metadata. # xcodebuild compiles the app, the widget extension, the asset catalog and the App Intents metadata.
xcodegen generate -q xcodegen generate -q
xcodebuild -project Stemify.xcodeproj -scheme Stemify -configuration Release -sdk iphoneos \ xcodebuild -project Stemify.xcodeproj -scheme Stemify -configuration Release -sdk iphoneos \
@@ -25,30 +39,16 @@ xcodebuild -project Stemify.xcodeproj -scheme Stemify -configuration Release -sd
cp -R $DD/Build/Products/Release-iphoneos/Stemify.app $APP cp -R $DD/Build/Products/Release-iphoneos/Stemify.app $APP
[ -d $EXT ] || { echo "widget extension missing" >&2; exit 1; } [ -d $EXT ] || { echo "widget extension missing" >&2; exit 1; }
# Ad-hoc signature carrying only the App Group, so Sideloadly/AltStore keep the widgets' shared data when re-signing. if [ "$MODE" != prov ]; then
cat > $OUT/group.plist <<PL # Ad-hoc signature carrying only the App Group, so Sideloadly/AltStore keep the widgets' shared data when re-signing.
cat > $OUT/group.plist <<PL
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict><key>com.apple.security.application-groups</key><array><string>group.$APP_ID</string></array></dict></plist> <plist version="1.0"><dict><key>com.apple.security.application-groups</key><array><string>group.$APP_ID</string></array></dict></plist>
PL PL
codesign -f -s - --entitlements $OUT/group.plist $EXT codesign -f -s - --entitlements $OUT/group.plist $EXT
codesign -f -s - --entitlements $OUT/group.plist $APP codesign -f -s - --entitlements $OUT/group.plist $APP
else
if [ "$MODE" = dev ]; then
rm -f $B/dev/stemify-$VERSION-dev.ipa
(cd $OUT && zip -qry ../dev/stemify-$VERSION-dev.ipa Payload)
echo "$B/dev/stemify-$VERSION-dev.ipa"
exit 0
fi
# Public build, for the releases and AltStore/Sideloadly.
if [ "$MODE" != prov ]; then
rm -f $B/public/stemify-$VERSION.ipa
(cd $OUT && zip -qry ../public/stemify-$VERSION.ipa Payload)
echo "$B/public/stemify-$VERSION.ipa"
fi
if [ -f prov/Stemify.mobileprovision ] && [ -f prov/key.p12 ] && [ -n "${P12_PASS:-}" ]; then
KC=$PWD/$OUT/sign.keychain-db; KP=$(uuidgen) KC=$PWD/$OUT/sign.keychain-db; KP=$(uuidgen)
security create-keychain -p "$KP" $KC security create-keychain -p "$KP" $KC
security set-keychain-settings -lut 3600 $KC security set-keychain-settings -lut 3600 $KC
@@ -62,7 +62,6 @@ if [ -f prov/Stemify.mobileprovision ] && [ -f prov/key.p12 ] && [ -n "${P12_PAS
ID=$(security find-identity -v -p codesigning $KC | sed -nE 's/.*"(Apple Development:.*)"/\1/p' | head -1) ID=$(security find-identity -v -p codesigning $KC | sed -nE 's/.*"(Apple Development:.*)"/\1/p' | head -1)
security cms -D -i prov/Stemify.mobileprovision > $OUT/prov.plist security cms -D -i prov/Stemify.mobileprovision > $OUT/prov.plist
/usr/libexec/PlistBuddy -x -c 'Print :Entitlements' $OUT/prov.plist > $OUT/ent.plist /usr/libexec/PlistBuddy -x -c 'Print :Entitlements' $OUT/prov.plist > $OUT/ent.plist
# The widgets need their own profile (org.cdnutter.stemify.widgets with the App Group). Without one they're left out.
if [ -f prov/StemifyWidgets.mobileprovision ]; then if [ -f prov/StemifyWidgets.mobileprovision ]; then
security cms -D -i prov/StemifyWidgets.mobileprovision > $OUT/wprov.plist security cms -D -i prov/StemifyWidgets.mobileprovision > $OUT/wprov.plist
/usr/libexec/PlistBuddy -x -c 'Print :Entitlements' $OUT/wprov.plist > $OUT/went.plist /usr/libexec/PlistBuddy -x -c 'Print :Entitlements' $OUT/wprov.plist > $OUT/went.plist
@@ -75,9 +74,8 @@ if [ -f prov/Stemify.mobileprovision ] && [ -f prov/key.p12 ] && [ -n "${P12_PAS
codesign -f -s "$ID" --entitlements $OUT/ent.plist --timestamp=none $APP codesign -f -s "$ID" --entitlements $OUT/ent.plist --timestamp=none $APP
security list-keychains -d user -s $(security list-keychains -d user | tr -d "\"" | grep -v sign.keychain) security list-keychains -d user -s $(security list-keychains -d user | tr -d "\"" | grep -v sign.keychain)
security delete-keychain $KC security delete-keychain $KC
# Personal build for this device only: stays in build/prov (gitignored), never released.
rm -f $B/prov/stemify-$VERSION-signed.ipa
(cd $OUT && zip -qry ../prov/stemify-$VERSION-signed.ipa Payload)
echo "$B/prov/stemify-$VERSION-signed.ipa (signed as $ID)"
fi fi
rm -f $IPA
(cd $OUT && zip -qry ../$MODE/$(basename $IPA) Payload)
echo "$IPA"
+9 -15
View File
@@ -1,23 +1,17 @@
#!/bin/bash #!/bin/bash
# Run from DevOps: syncs ios/ to White-M1, builds there, and pulls the IPAs back into ios/build. # Run from DevOps: syncs ios/ to WHITE-M1, builds there, and pulls the IPAs back into ios/build/<mode>.
# build-remote.sh dev + prov (the usual build): build/dev and build/prov # build-remote.sh dev build (the usual one): build/dev
# build-remote.sh public public + prov, only when a public release is asked for: build/public and build/prov # build-remote.sh release release build, only when a release is asked for: build/release
# build-remote.sh dev dev only # build-remote.sh prov personal signed build: build/prov
# build-remote.sh dev prov ... several at once
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
H=${BUILD_HOST:-white-m1}; R=stemify-build/ios H=${BUILD_HOST:-white-m1}; R=stemify-build/ios
case "${1:-}" in MODES=${*:-dev}
"") MODES="dev prov" ;; for m in $MODES; do case $m in release|dev|prov) ;; *) echo "usage: $0 [release|dev|prov]..." >&2; exit 1 ;; esac; done
public) MODES="release" ;;
dev) MODES="dev" ;;
*) echo "usage: $0 [public|dev]" >&2; exit 1 ;;
esac
rsync -a --exclude build --exclude prov --exclude Stemify.xcodeproj ./ $H:$R/ rsync -a --exclude build --exclude prov --exclude Stemify.xcodeproj ./ $H:$R/
for m in $MODES; do for m in $MODES; do
ssh $H "cd $R && P12_PASS=\$(cat prov/cert_pass.txt 2>/dev/null) bash scripts/build-ipa.sh $m" ssh $H "cd $R && P12_PASS=\$(cat prov/cert_pass.txt 2>/dev/null) bash scripts/build-ipa.sh $m"
mkdir -p build/$m && rsync -a $H:$R/build/$m/ build/$m/
done done
for m in $MODES; do ls build/{release,dev,prov}/stemify-$(sed -nE 's/^ *VERSION_LABEL: "(.*)"/\1/p' project.yml | tr 'A-Z' 'a-z' | sed -E 's/ (rc|beta) /-\1/; s/ /-/g')*.ipa 2>/dev/null
case $m in dev) DIRS=dev ;; prov) DIRS=prov ;; release) DIRS="public prov" ;; esac
for d in $DIRS; do mkdir -p build/$d; rsync -a $H:$R/build/$d/ build/$d/; done
done
ls build/*/stemify-$(sed -nE 's/^ *VERSION_LABEL: "(.*)"/\1/p' project.yml | tr 'A-Z' 'a-z' | sed -E 's/ rc /-rc/; s/ /-/g')*.ipa 2>/dev/null
+2 -1
View File
@@ -36,7 +36,8 @@ versions = []
for rel in get(API + "/releases?limit=50"): for rel in get(API + "/releases?limit=50"):
v = rel["tag_name"].removeprefix("ios-v") v = rel["tag_name"].removeprefix("ios-v")
ipa = next((a for a in rel["assets"] if a["name"] == f"stemify-{v}.ipa"), None) ipa = next((a for a in rel["assets"] if a["name"] == f"stemify-{v}.ipa"), None)
if not rel["tag_name"].startswith("ios-v") or not ipa: # Pre-releases are dev builds (org.cdnutter.stemify-dev), they go to Sideload Developer instead.
if not rel["tag_name"].startswith("ios-v") or not ipa or rel["prerelease"]:
continue continue
versions.append({ versions.append({
"version": v.split("-")[0], "version": v.split("-")[0],
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
# Publishes an IPA built by build-remote.sh. Run from DevOps with everything committed.
# publish.sh dev on the dev branch: Gitea pre-release ios-v<v>-dev.<build> + Sideload Developer (WHITE-M1 :8097)
# publish.sh release on main, only when the owner says so: Gitea release ios-v<v> + Sideload (:8093), then source.json
# Release notes come from $NOTES, else the last commit subject. prov builds are never published.
set -euo pipefail
cd "$(dirname "$0")/.."
MODE=${1:-}; NAME=stemify; TAGP=ios-v
API=https://git.iamchrisama.com/api/v1/repos/cdnutter/stemify
T=$(sed -n 's#.*://cdnutter:\([^@]*\)@.*#\1#p' ~/.config/gitea/cdnutter.credentials | head -1)
V=$(sed -nE 's/^ *VERSION_LABEL: "(.*)"/\1/p' project.yml | tr 'A-Z' 'a-z' | sed -E 's/ (rc|beta) /-\1/; s/ /-/g')
BUILD=$(sed -nE 's/^ *CURRENT_PROJECT_VERSION: "(.*)"/\1/p' project.yml)
BRANCH=$(git rev-parse --abbrev-ref HEAD)
die() { echo "$*" >&2; exit 1; }
case $MODE in
dev) WANT=dev; IPA=build/dev/$NAME-$V-dev.ipa; TAG=$TAGP$V-dev.$BUILD; PRE=true; UP=http://10.0.0.11:8097 ;;
release) WANT=main; IPA=build/release/$NAME-$V.ipa; TAG=$TAGP$V; PRE=false; UP=http://10.0.0.11:8093 ;;
*) die "usage: $0 dev|release" ;;
esac
[ "$BRANCH" = "$WANT" ] || die "$MODE builds publish from $WANT (on $BRANCH)"
[ -f "$IPA" ] || die "missing $IPA, run scripts/build-remote.sh $MODE"
git diff --quiet HEAD || die "uncommitted changes"
[ "$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token $T" $API/releases/tags/$TAG)" = 404 ] \
|| die "$TAG already exists, bump CURRENT_PROJECT_VERSION (dev) or VERSION_LABEL (release)"
git push -q -u origin $BRANCH
BODY=${NOTES:-$(git log -1 --format=%s)}
ID=$(jq -n --arg t "$TAG" --arg c "$(git rev-parse HEAD)" --arg b "$BODY" --argjson p $PRE \
'{tag_name:$t, target_commitish:$c, name:$t, body:$b, prerelease:$p}' |
curl -fsS -H "Authorization: token $T" -H 'Content-Type: application/json' -d @- $API/releases | jq -r .id)
curl -fsS -o /dev/null -H "Authorization: token $T" -F "attachment=@$IPA" "$API/releases/$ID/assets?name=$(basename $IPA)"
curl -fsS -o /dev/null -X POST --data-binary @$IPA $UP/api/upload/ipa
echo "published $TAG ($(basename $IPA)) to Gitea and $UP"
if [ $MODE = release ]; then
GITEA_TOKEN=$T python3 scripts/make-source.py
git add source.json && git commit -qm "iOS source.json for $TAG" && git push -q origin main
fi
+30
View File
@@ -174,6 +174,36 @@
"downloadURL": "https://git.iamchrisama.com/cdnutter/stemify/releases/download/ios-v1.0.3/stemify-1.0.3.ipa", "downloadURL": "https://git.iamchrisama.com/cdnutter/stemify/releases/download/ios-v1.0.3/stemify-1.0.3.ipa",
"size": 1319672, "size": 1319672,
"minOSVersion": "17.0" "minOSVersion": "17.0"
},
{
"version": "1.0.2",
"marketingVersion": "1.0.2",
"buildVersion": "1.0.2",
"date": "2026-10-20T08:53:24-07:00",
"localizedDescription": "Unsigned, iOS 17 or newer. Sideload `Stemify.ipa` with Sideloadly or AltStore.\n\n- Shuffle on the Songs screen now picks from the whole library instead of only the songs loaded so far (the A's and B's)\n\nIncludes everything in 1.0.1. CarPlay and Siri still need entitlements a free Apple ID can't sign.",
"downloadURL": "https://git.iamchrisama.com/cdnutter/stemify/releases/download/ios-v1.0.2/stemify-1.0.2.ipa",
"size": 1319672,
"minOSVersion": "17.0"
},
{
"version": "1.0.1",
"marketingVersion": "1.0.1",
"buildVersion": "1.0.1",
"date": "2026-10-20T08:50:56-07:00",
"localizedDescription": "Unsigned, iOS 17 or newer. Sideload `Stemify.ipa` with Sideloadly or AltStore.\n\n- The mini player no longer covers the bottom of lists, and it is now a solid card that matches the app instead of system glass\n- More room between the title and the scrubber on Now Playing\n- The play button no longer spins forever after you change tracks\n- The stems and queue buttons now animate their panels in and out\n- Springier button presses and play/pause transitions\n\nCarPlay and Siri still need entitlements a free Apple ID can't sign.",
"downloadURL": "https://git.iamchrisama.com/cdnutter/stemify/releases/download/ios-v1.0.1/stemify-1.0.1.ipa",
"size": 1317680,
"minOSVersion": "17.0"
},
{
"version": "1.0",
"marketingVersion": "1.0",
"buildVersion": "1.0",
"date": "2026-10-20T07:16:41-07:00",
"localizedDescription": "First iOS build. Unsigned, iOS 17 or newer.\n\nSideload `Stemify.ipa` with Sideloadly or AltStore, which sign it with your Apple ID.\n\nCarPlay and Siri need entitlements a free Apple ID can't sign, so on a free sideload they won't show up. Everything on the phone works. Not yet tested on a device.",
"downloadURL": "https://git.iamchrisama.com/cdnutter/stemify/releases/download/ios-v1.0/stemify-1.0.ipa",
"size": 1305649,
"minOSVersion": "17.0"
} }
], ],
"appPermissions": { "appPermissions": {