Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
25892f0
Bring in appleApp and the Apple workflow from #5, drop iosApp
frahlg Oct 12, 2026
56210fc
Cut the Swift app down to the iPhone M1 foundation
frahlg Oct 12, 2026
5a14ff0
Sign with Sourceful's team, open pairing links, pair from a picture
frahlg Oct 12, 2026
f2eb4bc
Price widgets: the price now, the cheapest two hours next, and a grey…
frahlg Oct 12, 2026
53ca882
Follow car commands to their outcome and read the box's clock
frahlg Oct 12, 2026
91ebff4
Draw the curve's bars from zero and keep the last hour label whole
frahlg Oct 12, 2026
52ccaf7
Add an app store distribution permission to the license
frahlg Oct 12, 2026
669b830
Fit the lock screen, drop UIKit, and time the timeline
frahlg Oct 12, 2026
97b5048
Add the car view and cut Now to what is looked at daily
frahlg Oct 12, 2026
815215c
Write the cache off the main thread, skip unchanged prices, follow th…
frahlg Oct 12, 2026
56e4897
Archive the iPhone app and send it to TestFlight from CI
frahlg Oct 12, 2026
9e7db2e
Show Source & licenses, pinned to the build's commit, and remove this…
frahlg Oct 12, 2026
352e0a4
Give the app an iOS 26 icon and a launch screen that matches its firs…
frahlg Oct 12, 2026
64d1532
Declare privacy and export compliance for the App Store
frahlg Oct 12, 2026
98b463f
Keep the release key in a GitHub environment
frahlg Oct 12, 2026
1eed138
Write down the App Store steps and the store texts
frahlg Oct 12, 2026
f8b75d3
Make the car view and Now fast, native and timer-free
frahlg Oct 12, 2026
58c106c
Leave the pairing screen out of the store pictures
frahlg Oct 12, 2026
e76660d
Tidy Source & licenses after a look in the simulator
frahlg Oct 12, 2026
60e656f
Merge remote-tracking branch 'origin/claude/ios-m1-car' into ios-m1-i…
frahlg Oct 12, 2026
daa5f97
Merge remote-tracking branch 'origin/claude/ios-m1-release' into ios-…
frahlg Oct 12, 2026
833bcb9
Put the shortest way to an iPhone first, and add an upload fallback
frahlg Oct 12, 2026
071f0c5
Merge remote-tracking branch 'origin/claude/ios-m1-release' into ios-…
frahlg Oct 12, 2026
42b95dc
Link the privacy policy and support from inside the app
frahlg Oct 12, 2026
59cde99
Match the release guide to the store-ready branch's answers
frahlg Oct 12, 2026
e0f2b68
Merge remote-tracking branch 'origin/claude/ios-m1-store-ready-4df952…
frahlg Oct 12, 2026
98050e5
Leave France out of the first release
frahlg Oct 12, 2026
170b639
Merge remote-tracking branch 'origin/claude/ios-m1-release' into ios-…
frahlg Oct 12, 2026
5915f6c
Merge remote-tracking branch 'origin/license/app-store-permission' in…
frahlg Oct 12, 2026
515965e
Reflow the license header so the app shows it as one paragraph
frahlg Oct 12, 2026
a85f5df
Link the privacy, support and marketing pages on ftw.sourceful.energy
frahlg Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/apple.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Apple

on:
push:
branches: [main]
paths: ['appleApp/**', 'protocol/**', '.github/workflows/apple.yml']
pull_request:
paths: ['appleApp/**', 'protocol/**', '.github/workflows/apple.yml']

concurrency:
group: apple-${{ github.ref }}
cancel-in-progress: true

jobs:
apple:
name: FTWKit and the iPhone app
runs-on: macos-26
steps:
- uses: actions/checkout@v4
- name: Use the newest Xcode on the runner
run: |
latest=$(ls -d /Applications/Xcode_*.app | sort -V | tail -1)
sudo xcode-select -s "$latest"
xcodebuild -version
- name: Test FTWKit, FTWShared and FTWDemo
working-directory: appleApp/FTWKit
run: swift test
# Signed ad hoc, as FTW.xcconfig sets for the simulator, so the App
# Group entitlement travels with the app and the widget. Device
# builds need a team, which the project does not have yet.
- name: Build the app and its widgets for the iOS Simulator
working-directory: appleApp
run: >-
xcodebuild build -quiet -project FTW.xcodeproj -scheme FTW
-destination 'generic/platform=iOS Simulator'
-derivedDataPath build/ios
# For whoever reviews the screens: the demo, photographed in the
# simulator. Evidence, not a gate, so a flaky simulator does not turn
# the build red.
- name: Photograph the demo in the iOS Simulator
continue-on-error: true
working-directory: appleApp
run: scripts/demo-screenshots.sh build/screenshots
- name: Keep the screenshots
if: always()
uses: actions/upload-artifact@v4
with:
name: demo-screenshots
path: appleApp/build/screenshots
if-no-files-found: ignore
207 changes: 207 additions & 0 deletions .github/workflows/ios-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
name: iOS release

# Archives the iPhone app and sends it to App Store Connect, where it lands
# in TestFlight. A tag `ios-v1.2.3` ships version 1.2.3, and so do
# `ios-v1.2.3-1`, `ios-v1.2.3-2`: anything after a dash only tells tags
# apart. A manual run ships the version in appleApp/FTW.xcconfig. The build
# number is this workflow's run number. appleApp/RELEASING.md has the steps
# around it.
#
# Without the three ASC_* secrets it builds an unsigned archive instead and
# says that the upload was skipped. A pull request that changes this file or
# the export options always takes that path, secrets or not.

on:
workflow_dispatch:
push:
tags: ['ios-v*']
pull_request:
paths: ['.github/workflows/ios-release.yml', 'appleApp/ExportOptions.plist']

permissions:
contents: read

concurrency:
group: ios-release-${{ github.ref }}
cancel-in-progress: false

jobs:
release:
name: Archive the iPhone app for App Store Connect
# GitHub's image with Xcode 27 (public preview in October 2026). The
# macos-26 image stops at Xcode 26.6.
runs-on: xcode-27
timeout-minutes: 60
# The key's secrets may live in the repository or, safer, in this
# environment, which only main and ios-v* tags may use (RELEASING.md).
# A pull request needs no key.
environment: ${{ github.event_name != 'pull_request' && 'app-store' || '' }}
env:
# The release, by path. Picking the newest Xcode on this image would
# pick a beta.
DEVELOPER_DIR: /Applications/Xcode_27.0.app/Contents/Developer
steps:
- uses: actions/checkout@v4

- name: Show Xcode and the iOS SDK
run: |
xcodebuild -version
xcodebuild -version -sdk iphoneos SDKVersion

- name: Test FTWKit, FTWShared and FTWDemo
working-directory: appleApp/FTWKit
run: swift test

- name: Choose the version and build number
id: version
run: |
if [[ $GITHUB_REF_TYPE == tag && $GITHUB_REF_NAME == ios-v* ]]; then
version=${GITHUB_REF_NAME#ios-v}
version=${version%%-*}
else
version=$(sed -n 's/^MARKETING_VERSION *= *//p' appleApp/FTW.xcconfig)
fi
# What App Store Connect accepts: one to three whole numbers.
if ! [[ $version =~ ^[0-9]+(\.[0-9]+){0,2}$ ]]; then
echo "::error::'$version' is not a version App Store Connect accepts. Tag as ios-v1.2.3."
exit 1
fi
# A re-run keeps the run number, and App Store Connect refuses a
# build number twice.
build=$GITHUB_RUN_NUMBER
if (( GITHUB_RUN_ATTEMPT > 1 )); then build=$build.$GITHUB_RUN_ATTEMPT; fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "build=$build" >> "$GITHUB_OUTPUT"
echo "Version $version, build $build"

# Secrets cannot appear in an `if`, so this step turns them into a
# yes or no. It prints nothing about them. ASC_KEY_P8 is the .p8 file's
# PEM text as Apple gives it; the other two are plain text.
- name: Check for the App Store Connect key
id: key
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
run: |
if [[ $GITHUB_EVENT_NAME != pull_request && -n $ASC_KEY_ID && -n $ASC_ISSUER_ID && -n $ASC_KEY_P8 ]]; then
key=$RUNNER_TEMP/AuthKey.p8
(umask 077; printf '%s\n' "$ASC_KEY_P8" > "$key")
echo "path=$key" >> "$GITHUB_OUTPUT"
echo "upload=true" >> "$GITHUB_OUTPUT"
else
echo "upload=false" >> "$GITHUB_OUTPUT"
fi

# Xcode signs with Sourceful's team (FTW.xcconfig) and makes what it
# needs through the key: a development certificate on this runner for
# the archive, then the App Store profiles and a cloud-managed
# distribution certificate for the export.
- name: Archive, signed
if: steps.key.outputs.upload == 'true'
working-directory: appleApp
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
run: >-
xcodebuild archive -quiet -project FTW.xcodeproj -scheme FTW
-configuration Release -destination 'generic/platform=iOS'
-archivePath "$RUNNER_TEMP/FTW.xcarchive"
-allowProvisioningUpdates
-authenticationKeyPath "${{ steps.key.outputs.path }}"
-authenticationKeyID "$ASC_KEY_ID"
-authenticationKeyIssuerID "$ASC_ISSUER_ID"
MARKETING_VERSION=${{ steps.version.outputs.version }}
CURRENT_PROJECT_VERSION=${{ steps.version.outputs.build }}
FTW_SOURCE_REVISION=$GITHUB_SHA

- name: Archive, unsigned (dry run)
if: steps.key.outputs.upload != 'true'
working-directory: appleApp
run: >-
xcodebuild archive -quiet -project FTW.xcodeproj -scheme FTW
-configuration Release -destination 'generic/platform=iOS'
-archivePath "$RUNNER_TEMP/FTW.xcarchive"
CODE_SIGNING_ALLOWED=NO
MARKETING_VERSION=${{ steps.version.outputs.version }}
CURRENT_PROJECT_VERSION=${{ steps.version.outputs.build }}
FTW_SOURCE_REVISION=$GITHUB_SHA

# App Store Connect refuses a widget whose version differs from its
# app's, so check both before sending anything.
- name: Check the version in the app and the widget
run: |
app=$RUNNER_TEMP/FTW.xcarchive/Products/Applications/FTW.app
for plist in "$app/Info.plist" "$app"/PlugIns/*.appex/Info.plist; do
got=$(plutil -extract CFBundleShortVersionString raw "$plist")/$(plutil -extract CFBundleVersion raw "$plist")
echo "$got ${plist#$app/} $(plutil -extract DTSDKName raw "$plist")"
[[ $got == ${{ steps.version.outputs.version }}/${{ steps.version.outputs.build }} ]] || { echo "::error::$plist has $got"; exit 1; }
done

# Xcode signs and uploads in one step. If that fails, the next step
# tries the other way Apple documents.
- name: Upload to App Store Connect
id: upload
if: steps.key.outputs.upload == 'true'
continue-on-error: true
working-directory: appleApp
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
run: >-
xcodebuild -exportArchive
-archivePath "$RUNNER_TEMP/FTW.xcarchive"
-exportOptionsPlist ExportOptions.plist
-exportPath "$RUNNER_TEMP/export"
-allowProvisioningUpdates
-authenticationKeyPath "${{ steps.key.outputs.path }}"
-authenticationKeyID "$ASC_KEY_ID"
-authenticationKeyIssuerID "$ASC_ISSUER_ID"

# Apple's "Upload builds" help still lists altool beside Xcode and
# Transporter. Export the signed .ipa, then send it with the same key.
- name: Upload with altool instead
id: altool
if: steps.upload.outcome == 'failure'
working-directory: appleApp
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
run: |
echo "::warning title=Xcode upload failed::Exporting an .ipa and uploading it with altool instead."
plutil -replace destination -string export -o "$RUNNER_TEMP/ExportOptions-ipa.plist" ExportOptions.plist
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/FTW.xcarchive" \
-exportOptionsPlist "$RUNNER_TEMP/ExportOptions-ipa.plist" \
-exportPath "$RUNNER_TEMP/ipa" \
-allowProvisioningUpdates \
-authenticationKeyPath "${{ steps.key.outputs.path }}" \
-authenticationKeyID "$ASC_KEY_ID" \
-authenticationKeyIssuerID "$ASC_ISSUER_ID"
xcrun altool --upload-package "$RUNNER_TEMP"/ipa/*.ipa \
--api-key "$ASC_KEY_ID" --api-issuer "$ASC_ISSUER_ID" \
--p8-file-path "${{ steps.key.outputs.path }}"

- name: Report
run: |
v="${{ steps.version.outputs.version }} (${{ steps.version.outputs.build }})"
if [[ "${{ steps.key.outputs.upload }}" == true ]]; then
how=Xcode; [[ "${{ steps.altool.outcome }}" == success ]] && how=altool
echo "FTW $v went to App Store Connect through $how. It shows in TestFlight once Apple has processed it." >> "$GITHUB_STEP_SUMMARY"
else
echo "::warning title=Upload skipped::Dry run: FTW $v was archived unsigned and not sent to App Store Connect. A pull request never uploads; otherwise set ASC_KEY_ID, ASC_ISSUER_ID and ASC_KEY_P8 (appleApp/RELEASING.md)."
echo "**Upload skipped.** Dry run: FTW $v was archived unsigned and not sent to App Store Connect." >> "$GITHUB_STEP_SUMMARY"
fi

- name: Remove the key
if: always()
run: rm -f "$RUNNER_TEMP/AuthKey.p8"

# Xcode's own account of the export, for when App Store Connect says no.
- name: Keep the export logs
if: always() && steps.key.outputs.upload == 'true' && (failure() || steps.upload.outcome == 'failure')
uses: actions/upload-artifact@v4
with:
name: export-logs
path: ${{ runner.temp }}/export
if-no-files-found: ignore
10 changes: 8 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,16 @@ DerivedData/
*.aab
captures/

# iOS
iosApp/Pods/
# Xcode
*.xcworkspace/xcuserdata/

# Secrets
*.jks
keystore.properties

# Swift package
appleApp/FTWKit/.build/
appleApp/FTWKit/.swiftpm/
appleApp/build/
appleApp/FTW.xcodeproj/project.xcworkspace/xcuserdata/
appleApp/FTW.xcodeproj/xcuserdata/
Loading
Loading