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
+133 -59

No files matched your search

@@ -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
# Builds build/public/stemify-<version>.ipa on a Mac: ad-hoc signed, for releases and re-signing with Sideloadly/AltStore.
# With prov/Stemify.mobileprovision, prov/key.p12 and P12_PASS set, it also writes a signed
# build/prov/stemify-<version>-signed.ipa (keeps CarPlay/Siri) for the owner's own device.
# Finished IPAs are kept per version; build/work is scratch and is wiped each run.
# "build-ipa.sh dev" builds only build/dev/stemify-<version>-dev.ipa: bundle id org.cdnutter.stemify-dev,
# named "Stemify Dev", ad-hoc signed, so it installs next to the release app. For internal tests, never released.
# "build-ipa.sh prov" builds only the signed prov IPA (no public one).
# ios/scripts/build-remote.sh runs this on White-M1 and copies build/public and build/prov back to DevOps.
# Builds one Stemify IPA on a Mac. ios/scripts/build-remote.sh runs this on WHITE-M1 from DevOps.
# build-ipa.sh release build/release/stemify-<v>.ipa org.cdnutter.stemify, ad-hoc signed. Gitea releases (main) + Sideload.
# build-ipa.sh dev build/dev/stemify-<v>-dev.ipa org.cdnutter.stemify-dev, "Stemify Dev", ribbon icon.
# Gitea pre-releases (dev branch) + Sideload Developer.
# build-ipa.sh prov build/prov/stemify-<v>-prov.ipa org.cdnutter.stemify-prov, "Stemify Prov", signed with the
# personal profile (CarPlay, Siri). Never published anywhere.
# prov needs prov/Stemify.mobileprovision (App ID org.cdnutter.stemify-prov), prov/key.p12 and P12_PASS.
# 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
cd "$(dirname "$0")/.."
export PATH=/opt/homebrew/bin:$PATH
B=build; OUT=$B/work; APP=$OUT/Payload/Stemify.app; EXT=$APP/PlugIns/StemifyWidgets.appex
MODE=${1:-release}
if [ "$MODE" = dev ]; then APP_ID=org.cdnutter.stemify-dev; APP_NAME="Stemify Dev"; APP_ICON=AppIconDev; DD=$B/dd-dev
else APP_ID=org.cdnutter.stemify; APP_NAME=Stemify; APP_ICON=AppIcon; DD=$B/dd; fi
rm -rf $OUT && mkdir -p $OUT/Payload $B/public $B/prov $B/dev
MODE=${1:-dev}
case $MODE in
release) APP_ID=org.cdnutter.stemify; APP_NAME=Stemify; APP_ICON=AppIcon; SUFFIX= ;;
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.
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.
xcodegen generate -q
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
[ -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.
cat > $OUT/group.plist <<PL
if [ "$MODE" != prov ]; then
# 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"?>
<!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>
PL
codesign -f -s - --entitlements $OUT/group.plist $EXT
codesign -f -s - --entitlements $OUT/group.plist $APP
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
codesign -f -s - --entitlements $OUT/group.plist $EXT
codesign -f -s - --entitlements $OUT/group.plist $APP
else
KC=$PWD/$OUT/sign.keychain-db; KP=$(uuidgen)
security create-keychain -p "$KP" $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)
security cms -D -i prov/Stemify.mobileprovision > $OUT/prov.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
security cms -D -i prov/StemifyWidgets.mobileprovision > $OUT/wprov.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
security list-keychains -d user -s $(security list-keychains -d user | tr -d "\"" | grep -v sign.keychain)
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
rm -f $IPA
(cd $OUT && zip -qry ../$MODE/$(basename $IPA) Payload)
echo "$IPA"
+9 -15
View File
@@ -1,23 +1,17 @@
#!/bin/bash
# Run from DevOps: syncs ios/ to White-M1, builds there, and pulls the IPAs back into ios/build.
# build-remote.sh dev + prov (the usual build): build/dev and build/prov
# build-remote.sh public public + prov, only when a public release is asked for: build/public and build/prov
# build-remote.sh dev dev only
# Run from DevOps: syncs ios/ to WHITE-M1, builds there, and pulls the IPAs back into ios/build/<mode>.
# build-remote.sh dev build (the usual one): build/dev
# build-remote.sh release release build, only when a release is asked for: build/release
# build-remote.sh prov personal signed build: build/prov
# build-remote.sh dev prov ... several at once
set -euo pipefail
cd "$(dirname "$0")/.."
H=${BUILD_HOST:-white-m1}; R=stemify-build/ios
case "${1:-}" in
"") MODES="dev prov" ;;
public) MODES="release" ;;
dev) MODES="dev" ;;
*) echo "usage: $0 [public|dev]" >&2; exit 1 ;;
esac
MODES=${*:-dev}
for m in $MODES; do case $m in release|dev|prov) ;; *) echo "usage: $0 [release|dev|prov]..." >&2; exit 1 ;; esac; done
rsync -a --exclude build --exclude prov --exclude Stemify.xcodeproj ./ $H:$R/
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"
mkdir -p build/$m && rsync -a $H:$R/build/$m/ build/$m/
done
for m in $MODES; do
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
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
+2 -1
View File
@@ -36,7 +36,8 @@ versions = []
for rel in get(API + "/releases?limit=50"):
v = rel["tag_name"].removeprefix("ios-v")
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
versions.append({
"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",
"size": 1319672,
"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": {