Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
103 changes: 101 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ permissions:
contents: write # needed to create the GitHub Release

jobs:
android:
# PREFLIGHT. Every version guard lives here, in its own job, and BOTH
# release jobs depend on it. It used to sit inside `android`, which meant
# a version mismatch failed the APK while the `ios` job — having no
# `needs:` — carried on and published an IPA anyway. The iOS bundle guard
# in particular was protecting a job it could not stop.
preflight:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # need all tags so the versionCode guard can read prior releases' pubspec
fetch-depth: 0 # all tags, so the versionCode guard can read prior releases' pubspec
persist-credentials: false # nothing here writes to the repo

# Fail the release BEFORE building if the Android versionCode is missing or
# would go backwards. Flutter derives versionCode from the `+BUILD` suffix
Expand Down Expand Up @@ -65,6 +71,96 @@ jobs:
fi
echo "versionCode $build (versionName $name) OK — exceeds prior max $max."

# The iOS EMBEDDED bundles (widget extension, Watch app) build their
# Info.plist from Xcode build settings — GENERATE_INFOPLIST_FILE=YES —
# not from pubspec. Only the Runner target reads $(FLUTTER_BUILD_NAME),
# because Flutter's Generated.xcconfig is in scope for that target
# alone. So MARKETING_VERSION / CURRENT_PROJECT_VERSION in
# project.pbxproj are a SECOND, hand-maintained source of truth, and
# when they fall behind a pubspec bump the embedded bundles ship the
# old version. Apple rejects that outright — an extension or WatchKit
# app whose CFBundleShortVersionString differs from the host app fails
# App Store validation — so the whole upload bounces after the build.
#
# Pointing them at $(FLUTTER_BUILD_NAME) does NOT work: the variable is
# undefined for those targets and the versions come out EMPTY. Verified
# by building and reading the compiled bundles. So they stay literal,
# and this guard is what keeps them honest.
#
# The check is per build-configuration, not a flat grep over the file.
# A configuration with GENERATE_INFOPLIST_FILE=YES builds its Info.plist
# from these settings and must carry LITERAL versions matching pubspec;
# the Runner configurations have no such flag and correctly use
# $(FLUTTER_BUILD_*), because Runner's checked-in Info.plist substitutes
# them. Matching only values that start with a digit would skip a
# variable-valued setting entirely and silently pass the exact
# misconfiguration that ships empty versions.
VERSION_NAME="$name" VERSION_BUILD="$build" python3 - <<'PY'
import os, re, sys

name = os.environ['VERSION_NAME']
build = os.environ['VERSION_BUILD']
pbx = 'ios/Runner.xcodeproj/project.pbxproj'
why = ('App Store validation rejects an embedded bundle whose version '
'differs from the host app, so the upload bounces after the build.')

bad, checked = [], 0
for m in re.finditer(r'/\* (\w+) \*/ = \{\s*isa = XCBuildConfiguration;(.*?)\n\t\t\};',
open(pbx).read(), re.S):
cfg, body = m.groups()
if not re.search(r'\bGENERATE_INFOPLIST_FILE = YES;', body):
continue # Runner: versions come from Runner/Info.plist
checked += 1
for key, want in (('MARKETING_VERSION', name), ('CURRENT_PROJECT_VERSION', build)):
found = re.search(r'\b%s = ([^;]+);' % key, body)
if not found:
bad.append('%s: %s config has no %s, so its generated Info.plist '
'would omit the version. %s' % (pbx, cfg, key, why))
continue
val = found.group(1).strip().strip('"')
if '$(' in val:
bad.append('%s: %s config has %s = %s -- that build variable is '
'undefined for this target and produces an EMPTY version. %s'
% (pbx, cfg, key, val, why))
elif val != want:
bad.append('%s: %s config has %s = %s but pubspec says %s. %s'
% (pbx, cfg, key, val, want, why))

if checked == 0:
print('::error::%s: found no GENERATE_INFOPLIST_FILE build configurations -- '
'the iOS version guard is no longer checking anything.' % pbx)
sys.exit(1)

# The other half of the mechanism: the main app must keep tracking pubspec.
plist = open('ios/Runner/Info.plist').read()
for key, var in (('CFBundleShortVersionString', '$(FLUTTER_BUILD_NAME)'),
('CFBundleVersion', '$(FLUTTER_BUILD_NUMBER)')):
got = re.search(r'<key>%s</key>\s*<string>([^<]*)</string>' % key, plist)
if not got or got.group(1).strip() != var:
bad.append('ios/Runner/Info.plist: %s is %s, expected %s -- the main app '
'would stop tracking pubspec and this guard would stop meaning '
'anything.' % (key, got.group(1).strip() if got else 'absent', var))

for b in bad:
print('::error::%s' % b)
if bad:
sys.exit(1)
print('iOS embedded-bundle versions match pubspec (%s / %s) across %d build '
'configurations.' % (name, build, checked))
PY

android:
needs: preflight
runs-on: ubuntu-latest
steps:
# Shallow: the guard that needed full tag history now runs in `preflight`, and
# nothing in this job reads git. `generate_release_notes` builds its diff on
# GitHub's side from the tag, not from local history — the ios job has always
# run shallow and attaches to the same release without trouble.
- uses: actions/checkout@v4
with:
persist-credentials: false # nothing here writes to the repo

Comment thread
coderabbitai[bot] marked this conversation as resolved.
- uses: actions/setup-java@v4
with:
distribution: temurin
Expand Down Expand Up @@ -157,11 +253,14 @@ jobs:
# run`/Xcode direct install), where $(APP_BUNDLE_IDENTIFIER) threads through
# every target's plist, including this one, at build time.
ios:
needs: preflight
runs-on: macos-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false # nothing here writes to the repo

- uses: subosito/flutter-action@v2
with:
Expand Down
36 changes: 18 additions & 18 deletions ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -791,9 +791,9 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 52;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 0.9.21;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
Expand All @@ -809,9 +809,9 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 52;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 0.9.21;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
Expand All @@ -825,9 +825,9 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 52;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 0.9.21;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
Expand All @@ -849,7 +849,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = OpenStrapWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 51;
CURRENT_PROJECT_VERSION = 52;
DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)";
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
Expand All @@ -865,7 +865,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.9.20;
MARKETING_VERSION = 0.9.21;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_WIDGET_BUNDLE_IDENTIFIER)";
Expand Down Expand Up @@ -896,7 +896,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = OpenStrapWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 51;
CURRENT_PROJECT_VERSION = 52;
DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)";
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
Expand All @@ -912,7 +912,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.9.20;
MARKETING_VERSION = 0.9.21;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_WIDGET_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down Expand Up @@ -940,7 +940,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = OpenStrapWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 51;
CURRENT_PROJECT_VERSION = 52;
DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)";
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
Expand All @@ -956,7 +956,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.9.20;
MARKETING_VERSION = 0.9.21;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_WIDGET_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down Expand Up @@ -987,7 +987,7 @@
CODE_SIGN_ENTITLEMENTS = "OpenStrapWatch Watch App/OpenStrapWatch Watch App.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 51;
CURRENT_PROJECT_VERSION = 52;
DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
Expand All @@ -1003,7 +1003,7 @@
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.9.20;
MARKETING_VERSION = 0.9.21;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).watchkitapp";
Expand Down Expand Up @@ -1042,7 +1042,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "OpenStrapWatch Watch App/OpenStrapWatch Watch App.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 51;
CURRENT_PROJECT_VERSION = 52;
DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
Expand All @@ -1058,7 +1058,7 @@
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.9.20;
MARKETING_VERSION = 0.9.21;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).watchkitapp";
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down Expand Up @@ -1093,7 +1093,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "OpenStrapWatch Watch App/OpenStrapWatch Watch App.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 51;
CURRENT_PROJECT_VERSION = 52;
DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
Expand All @@ -1109,7 +1109,7 @@
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.9.20;
MARKETING_VERSION = 0.9.21;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).watchkitapp";
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ publish_to: 'none'
# Watch App" targets in ios/Runner.xcodeproj/project.pbxproj — they aren't wired
# to FLUTTER_BUILD_NAME/FLUTTER_BUILD_NUMBER. See guides/IOS_INSTALLATION.md
# "Version Numbers".
version: 0.9.20+51
version: 0.9.21+52

environment:
sdk: ^3.11.4
Expand Down
Loading