3 Commits
Author SHA1 Message Date
Ayushya AmitabhandClaude Sonnet 5.5 86e5e599b6 CI: pin iOS signing certificate by SHA-1 and log CI keychain identities
Build iOS / build (push) Successful in 4m0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 19:36:29 -04:00
Ayushya AmitabhandClaude Sonnet 5.5 0ea98363de CI: split release tags - RC-Android-* (APK), RC-iOS-* (iOS), Release-* (both)
Build iOS / build (push) Failing after 1m55s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 19:15:29 -04:00
Ayushya AmitabhandClaude Sonnet 5.5 ca4afb2ed7 CI: signed iOS build and TestFlight upload on Release tags
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 19:04:39 -04:00
7 changed files with 172 additions and 10 deletions
+5 -3
View File
@@ -113,9 +113,11 @@ in this order: a local `android/key.properties` (gitignored — points at a
gitignored keystore file, e.g. `android/app/release-keystore.jks`), then
CI env vars (`RELEASE_KEYSTORE_PATH`/`_PASSWORD`, `RELEASE_KEY_ALIAS`/
`_PASSWORD`, set from the same repo secrets by both `.gitea/workflows/
build.yml`, triggered by `RC*` tags and producing a sideloadable APK, and
`.gitea/workflows/release.yml`, triggered by `Release-*` tags and producing
the `.aab` Play Console wants), then falls back to the debug key if neither
build.yml`, triggered by `RC-Android-*` tags and producing a sideloadable
APK, and `.gitea/workflows/release.yml`, triggered by `Release-*` tags and
producing the `.aab` Play Console wants; `.gitea/workflows/ios.yml` builds
and uploads the signed iOS app to TestFlight on `RC-iOS-*` and `Release-*`
tags, on a self-hosted macOS runner), then falls back to the debug key if neither
is configured. As long as the same dedicated release keystore backs both
`key.properties` locally and the Gitea secrets, local release builds and
CI-built release APKs share one signature, so `adb install -r` works
+31
View File
@@ -0,0 +1,31 @@
# CI-only: switches the Release configuration of the Runner and ShareExtension
# targets to manual signing with the App Store profiles. The committed project
# stays on Automatic signing so local development is unaffected; this edit
# happens only inside the CI checkout. Run from the repo root:
# ruby .gitea/scripts/ios_ci_signing.rb
require "xcodeproj"
TEAM = "Q3JLTAG9PV"
# Pinned by SHA-1, not by the "Apple Distribution" name: a runner that also
# holds other same-named distribution certs (e.g. a dev Mac's login keychain)
# would otherwise let Xcode pick one the profiles don't include. Update this
# (and ios/ExportOptions.plist) when the certificate is renewed.
CERT_SHA1 = "2527806871D806E8E27221B15CA8A1938CF216F2"
PROFILES = {
"Runner" => "NooProfile",
"ShareExtension" => "NooShareSheetProfile",
}.freeze
project = Xcodeproj::Project.open("ios/Runner.xcodeproj")
PROFILES.each do |target_name, profile|
target = project.targets.find { |t| t.name == target_name } or abort("no target #{target_name}")
target.build_configurations.select { |c| c.name == "Release" }.each do |config|
settings = config.build_settings
settings["CODE_SIGN_STYLE"] = "Manual"
settings["DEVELOPMENT_TEAM"] = TEAM
settings["CODE_SIGN_IDENTITY"] = CERT_SHA1
settings["CODE_SIGN_IDENTITY[sdk=iphoneos*]"] = CERT_SHA1
settings["PROVISIONING_PROFILE_SPECIFIER"] = profile
end
end
project.save
+6 -5
View File
@@ -1,12 +1,13 @@
name: Build APK
# Deliberately not "on every push" - only when a release-candidate tag
# (RC1, RC2, RC2026.1, ...) is pushed. Tag the commit you want built:
# git tag RC1 && git push origin RC1
# Deliberately not "on every push" - only when an Android release-candidate
# tag (RC-Android-1, RC-Android-2, ...) is pushed. Tag the commit you want built:
# git tag RC-Android-1 && git push origin RC-Android-1
# iOS has its own RC-iOS-* tags (ios.yml); Release-* builds both platforms.
on:
push:
tags:
- "RC*"
- "RC-Android-*"
jobs:
build:
@@ -72,7 +73,7 @@ jobs:
# talks to this same Gitea instance automatically via the
# `github.token`/`github.server_url` context Gitea Actions
# provides for compatibility. Release is named/tagged after
# whatever tag triggered this run (e.g. "RC1").
# whatever tag triggered this run (e.g. "RC-Android-1").
uses: akkuman/gitea-release-action@v1
with:
files: ${{ env.APK_PATH }}
+103
View File
@@ -0,0 +1,103 @@
name: Build iOS
# Tag scheme (shared with the Android workflows):
# Release-<version> -> release.yml (Play bundle) AND this workflow
# RC-Android-<n> -> build.yml (APK) only
# RC-iOS-<n> -> this workflow only
# git tag RC-iOS-1 && git push origin RC-iOS-1
# Builds a signed .ipa and uploads it to TestFlight. Needs a Mac runner
# registered with the `macos` label (gitea-runner in host mode) with Xcode
# installed; Linux runners cannot build iOS.
on:
push:
tags:
- "Release-*"
- "RC-iOS-*"
jobs:
build:
runs-on: macos
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
- name: Check tag matches pubspec version
# Only Release-* tags carry a version; RC-iOS-* tags are free-form.
if: startsWith(github.ref_name, 'Release-')
run: |
VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //' | cut -d'+' -f1)
if [ "${GITHUB_REF_NAME}" != "Release-${VERSION}" ]; then
echo "Tag ${GITHUB_REF_NAME} does not match pubspec version ${VERSION}" >&2
exit 1
fi
- name: Install dependencies
run: flutter pub get
- name: Analyze
run: flutter analyze
- name: Install signing certificate and profiles
# A throwaway keychain keeps the distribution key off the runner's
# login keychain; it is deleted again in the cleanup step.
env:
CERT_P12_BASE64: ${{ secrets.IOS_DIST_CERT_P12_BASE64 }}
CERT_PASSWORD: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
APP_PROFILE_BASE64: ${{ secrets.IOS_APP_PROFILE_BASE64 }}
EXT_PROFILE_BASE64: ${{ secrets.IOS_EXT_PROFILE_BASE64 }}
run: |
KEYCHAIN="${{ runner.temp }}/ci-signing.keychain-db"
KEYCHAIN_PASSWORD=$(uuidgen)
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
echo "$CERT_P12_BASE64" | base64 -d > "${{ runner.temp }}/dist.p12"
security import "${{ runner.temp }}/dist.p12" -P "$CERT_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN"
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
rm -f "${{ runner.temp }}/dist.p12"
# Must list 2527806871D8... (the cert the profiles embed); if it
# doesn't, IOS_DIST_CERT_P12_BASE64 holds the wrong certificate.
security find-identity -v -p codesigning "$KEYCHAIN"
PROFILES_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
mkdir -p "$PROFILES_DIR"
echo "$APP_PROFILE_BASE64" | base64 -d > "$PROFILES_DIR/NooProfile.mobileprovision"
echo "$EXT_PROFILE_BASE64" | base64 -d > "$PROFILES_DIR/NooShareSheetProfile.mobileprovision"
- name: Switch Release config to manual signing
run: ruby .gitea/scripts/ios_ci_signing.rb
- name: Build signed IPA
run: flutter build ipa --release --export-options-plist=ios/ExportOptions.plist
- name: Rename IPA to Noo-<version>.ipa
run: |
VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //' | cut -d'+' -f1)
mkdir -p release
cp build/ios/ipa/*.ipa "release/Noo-${VERSION}.ipa"
echo "IPA_PATH=release/Noo-${VERSION}.ipa" >> "$GITHUB_ENV"
- name: Upload to TestFlight
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_P8_BASE64: ${{ secrets.ASC_KEY_P8_BASE64 }}
run: |
mkdir -p "$HOME/.appstoreconnect/private_keys"
echo "$ASC_KEY_P8_BASE64" | base64 -d > "$HOME/.appstoreconnect/private_keys/AuthKey_${ASC_KEY_ID}.p8"
xcrun altool --upload-app -f "$IPA_PATH" -t ios \
--apiKey "$ASC_KEY_ID" --apiIssuer "$ASC_ISSUER_ID"
- name: Cleanup signing material
if: always()
run: |
security delete-keychain "${{ runner.temp }}/ci-signing.keychain-db" || true
rm -f "$HOME/Library/MobileDevice/Provisioning Profiles/NooProfile.mobileprovision" \
"$HOME/Library/MobileDevice/Provisioning Profiles/NooShareSheetProfile.mobileprovision"
rm -rf "$HOME/.appstoreconnect/private_keys"
+2 -2
View File
@@ -3,7 +3,7 @@ name: Build App Bundle
# Deliberately not "on every push" - only when a real release tag (Release-1.0.0,
# Release-1.2.3, ...) is pushed. Tag the commit you want built:
# git tag Release-1.0.0 && git push origin Release-1.0.0
# Unlike the RC* flow (build.yml), this builds the .aab Play Console wants,
# Unlike the RC-Android-* flow (build.yml), this builds the .aab Play Console wants,
# not a sideloadable .apk.
on:
push:
@@ -56,7 +56,7 @@ jobs:
run: flutter analyze
- name: Decode release keystore
# Same key as the RC* APK builds, so a Play Store upload's signature
# Same key as the RC-Android-* APK builds, so a Play Store upload's signature
# matches anything sideloaded from a release build. See
# android/app/build.gradle.kts.
run: echo "$RELEASE_KEYSTORE_BASE64" | base64 -d > "${{ runner.temp }}/release-keystore.jks"
+23
View File
@@ -0,0 +1,23 @@
<?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>method</key>
<string>app-store-connect</string>
<key>teamID</key>
<string>Q3JLTAG9PV</string>
<key>signingStyle</key>
<string>manual</string>
<key>signingCertificate</key>
<string>2527806871D806E8E27221B15CA8A1938CF216F2</string>
<key>provisioningProfiles</key>
<dict>
<key>dev.ayushya.noo</key>
<string>NooProfile</string>
<key>dev.ayushya.noo.ShareExtension</key>
<string>NooShareSheetProfile</string>
</dict>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
+2
View File
@@ -86,5 +86,7 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
</dict>
</plist>