Skip to content

Commit 21bacce

Browse files
feat: add AltStore distribution via iOS CI workflow
Adds GitHub Actions workflow to build and release iOS IPA, AltStore v2 source JSON, and export options for ad-hoc signing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0c38ec4 commit 21bacce

4 files changed

Lines changed: 206 additions & 0 deletions

File tree

.github/workflows/release-ios.yml

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
name: Build and Release iOS
2+
3+
on:
4+
workflow_run:
5+
workflows: ["Build and Release Android"]
6+
types:
7+
- completed
8+
9+
permissions:
10+
contents: write
11+
12+
jobs:
13+
release-ios:
14+
runs-on: macos-latest
15+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
16+
17+
steps:
18+
- name: Checkout code
19+
uses: actions/checkout@v4
20+
with:
21+
token: ${{ secrets.GITHUB_TOKEN }}
22+
ref: main
23+
fetch-depth: 0
24+
25+
- name: Get version from package.json
26+
run: |
27+
VERSION=$(node -p "require('./package.json').version")
28+
echo "VERSION=$VERSION" >> $GITHUB_ENV
29+
30+
- name: Setup Node.js
31+
uses: actions/setup-node@v4
32+
with:
33+
node-version: '20'
34+
cache: 'npm'
35+
36+
- name: Install dependencies
37+
run: npm ci
38+
39+
- name: Setup Ruby
40+
uses: ruby/setup-ruby@v1
41+
with:
42+
ruby-version: '3.2'
43+
bundler-cache: true
44+
45+
- name: Install CocoaPods
46+
run: |
47+
gem install cocoapods
48+
cd ios && pod install
49+
50+
- name: Import signing certificate
51+
env:
52+
IOS_CERTIFICATE_P12: ${{ secrets.IOS_CERTIFICATE_P12 }}
53+
IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
54+
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
55+
run: |
56+
# Create temporary keychain
57+
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
58+
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
59+
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
60+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
61+
62+
# Import certificate
63+
CERT_PATH=$RUNNER_TEMP/certificate.p12
64+
echo -n "$IOS_CERTIFICATE_P12" | base64 --decode -o "$CERT_PATH"
65+
security import "$CERT_PATH" \
66+
-P "$IOS_CERTIFICATE_PASSWORD" \
67+
-A \
68+
-t cert \
69+
-f pkcs12 \
70+
-k "$KEYCHAIN_PATH"
71+
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
72+
security list-keychain -d user -s "$KEYCHAIN_PATH"
73+
74+
- name: Import provisioning profile
75+
env:
76+
IOS_PROVISION_PROFILE: ${{ secrets.IOS_PROVISION_PROFILE }}
77+
run: |
78+
PROFILE_PATH=$RUNNER_TEMP/profile.mobileprovision
79+
echo -n "$IOS_PROVISION_PROFILE" | base64 --decode -o "$PROFILE_PATH"
80+
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
81+
cp "$PROFILE_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/
82+
83+
- name: Sync version to Xcode project
84+
run: |
85+
VERSION_CODE=$(date +%s)
86+
sed -i '' "s/MARKETING_VERSION = .*/MARKETING_VERSION = ${{ env.VERSION }};/" \
87+
ios/OffgridMobile.xcodeproj/project.pbxproj
88+
sed -i '' "s/CURRENT_PROJECT_VERSION = .*/CURRENT_PROJECT_VERSION = $VERSION_CODE;/" \
89+
ios/OffgridMobile.xcodeproj/project.pbxproj
90+
91+
- name: Build archive
92+
run: |
93+
xcodebuild archive \
94+
-workspace ios/OffgridMobile.xcworkspace \
95+
-scheme OffgridMobile \
96+
-configuration Release \
97+
-archivePath $RUNNER_TEMP/OffgridMobile.xcarchive \
98+
-destination "generic/platform=iOS" \
99+
CODE_SIGN_STYLE=Manual \
100+
DEVELOPMENT_TEAM=84V6KCAC49 \
101+
| tail -20
102+
103+
- name: Export IPA
104+
run: |
105+
xcodebuild -exportArchive \
106+
-archivePath $RUNNER_TEMP/OffgridMobile.xcarchive \
107+
-exportOptionsPlist ios/ExportOptions.plist \
108+
-exportPath $RUNNER_TEMP/export
109+
110+
# Rename IPA
111+
mv $RUNNER_TEMP/export/OffgridMobile.ipa \
112+
$RUNNER_TEMP/export/OffgridMobile-v${{ env.VERSION }}.ipa
113+
114+
- name: Upload IPA to GitHub Release
115+
env:
116+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
117+
run: |
118+
gh release upload v${{ env.VERSION }} \
119+
"$RUNNER_TEMP/export/OffgridMobile-v${{ env.VERSION }}.ipa" \
120+
--clobber
121+
122+
- name: Update AltStore source JSON
123+
run: |
124+
IPA_SIZE=$(stat -f%z "$RUNNER_TEMP/export/OffgridMobile-v${{ env.VERSION }}.ipa")
125+
TODAY=$(date +%Y-%m-%d)
126+
DOWNLOAD_URL="https://github.com/alichherawalla/offline-mobile-llm-manager/releases/download/v${{ env.VERSION }}/OffgridMobile-v${{ env.VERSION }}.ipa"
127+
128+
# Update altstore-source.json using node for reliable JSON manipulation
129+
node -e "
130+
const fs = require('fs');
131+
const source = JSON.parse(fs.readFileSync('altstore-source.json', 'utf8'));
132+
const app = source.apps[0];
133+
const newVersion = {
134+
version: '${{ env.VERSION }}',
135+
date: '${TODAY}',
136+
size: ${IPA_SIZE},
137+
downloadURL: '${DOWNLOAD_URL}',
138+
localizedDescription: 'Update to v${{ env.VERSION }}'
139+
};
140+
// Replace existing entry for this version or prepend
141+
const idx = app.versions.findIndex(v => v.version === '${{ env.VERSION }}');
142+
if (idx >= 0) {
143+
app.versions[idx] = newVersion;
144+
} else {
145+
app.versions.unshift(newVersion);
146+
}
147+
// Keep only the last 10 versions
148+
app.versions = app.versions.slice(0, 10);
149+
fs.writeFileSync('altstore-source.json', JSON.stringify(source, null, 2) + '\n');
150+
"
151+
152+
- name: Commit updated AltStore source
153+
run: |
154+
git config user.name "github-actions[bot]"
155+
git config user.email "github-actions[bot]@users.noreply.github.com"
156+
git add altstore-source.json
157+
git diff --staged --quiet && echo "No changes to commit" && exit 0
158+
git commit -m "chore: update AltStore source for v${{ env.VERSION }} [skip ci]"
159+
git push
160+
161+
- name: Cleanup keychain
162+
if: always()
163+
run: |
164+
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db 2>/dev/null || true

.github/workflows/release.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
name: Build and Release Android
2+
# NOTE: The iOS workflow (release-ios.yml) triggers via workflow_run on this workflow.
3+
# If you rename this workflow, update the workflow_run trigger in release-ios.yml.
24

35
on:
46
push:

altstore-source.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "Off Grid",
3+
"identifier": "ai.offgridmobile.source",
4+
"subtitle": "On-device AI apps",
5+
"description": "AltStore source for Off Grid — run LLMs, generate images, and transcribe speech entirely on-device.",
6+
"iconURL": "https://raw.githubusercontent.com/alichherawalla/offline-mobile-llm-manager/main/ios/OffgridMobile/Images.xcassets/AppIcon.appiconset/icon.png",
7+
"headerURL": "",
8+
"website": "https://github.com/alichherawalla/offline-mobile-llm-manager",
9+
"sourceURL": "https://raw.githubusercontent.com/alichherawalla/offline-mobile-llm-manager/main/altstore-source.json",
10+
"apps": [
11+
{
12+
"name": "Off Grid",
13+
"bundleIdentifier": "ai.offgridmobile",
14+
"developerName": "Off Grid",
15+
"subtitle": "On-device AI",
16+
"localizedDescription": "Run large language models, generate images with Stable Diffusion, and transcribe speech with Whisper — all on-device, fully offline, no cloud required.",
17+
"iconURL": "https://raw.githubusercontent.com/alichherawalla/offline-mobile-llm-manager/main/ios/OffgridMobile/Images.xcassets/AppIcon.appiconset/icon.png",
18+
"tintColor": "000000",
19+
"versions": [
20+
{
21+
"version": "0.0.32",
22+
"date": "2026-02-14",
23+
"size": 0,
24+
"downloadURL": "https://github.com/alichherawalla/offline-mobile-llm-manager/releases/latest/download/OffgridMobile.ipa",
25+
"localizedDescription": "Initial AltStore release."
26+
}
27+
]
28+
}
29+
]
30+
}

ios/ExportOptions.plist

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>method</key>
6+
<string>development</string>
7+
<key>teamID</key>
8+
<string>84V6KCAC49</string>
9+
</dict>
10+
</plist>

0 commit comments

Comments
 (0)