gowalk-cicd 1.0.44 → 1.0.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -122,6 +122,33 @@ node /path/to/gowalk-cicd/bin/cli.mjs
122
122
  number authoritative. Do NOT reach for `-Pandroid.injected.version.code`:
123
123
  those properties were removed in AGP 7.3 and are silently ignored by every
124
124
  AGP 8.x app in the fleet.
125
+ - Dart obfuscation is mandatory for Flutter apps and has no opt-out — do not
126
+ add an input, repo variable or config key for it. Both Flutter release
127
+ builds carry `--obfuscate --split-debug-info`: `flutter build appbundle` in
128
+ `android-action/action.yml`, and the `flutter build ios --config-only` call
129
+ in `templates/deploy.yml`, which is where the iOS build is obfuscated
130
+ (`--config-only` writes `DART_OBFUSCATION` / `SPLIT_DEBUG_INFO` into
131
+ `ios/Flutter/Generated.xcconfig`; the Flutter build phase inside the
132
+ `xcodebuild archive` reads them). The flags are one feature: `--obfuscate`
133
+ refuses to run without `--split-debug-info`, and the symbol files it writes
134
+ are the only way to read a stack trace from that build, so they are always
135
+ retained as an artifact (`android-symbols-*`, `ios-symbols-*`) and a build
136
+ that wrote none fails. Symbols are written under `RUNNER_TEMP`, never into
137
+ the checkout, so nothing that commits the working tree can pick them up.
138
+ Native Gradle, Swift and Flutter web builds are untouched (web has no such
139
+ flag). Wiring is pinned by `android-action/scripts/test_flutter_obfuscation.py`
140
+ and `test/install.test.mjs`.
141
+ - Whatever an obfuscation flag needs to be safe must travel in the same file
142
+ as the flag. The consumer-side autoupdate re-vendors the action directories
143
+ but can never push `deploy.yml`, so "new action + old workflow" is the
144
+ fleet's normal state. That is why the Android action uploads
145
+ `android-symbols-*` itself instead of leaving it to the workflow, and why
146
+ the iOS flags and the `ios-symbols-*` step sit together in `deploy.yml`.
147
+ The iOS retain step runs on `!cancelled()` because the swift-app action keeps
148
+ going after the TestFlight upload (metadata, "What's New"): a failure there
149
+ must not discard the symbols of a build that is already live. Symbol
150
+ artifacts carry no `retention-days` so the repository's setting governs and
151
+ can be raised to cover a release's life.
125
152
  - The JDK is chosen by `scripts/select_jdk.py`, called from the workflow before
126
153
  `setup-java`. The `JAVA_VERSION` repo variable always wins; otherwise Flutter
127
154
  gets 17 and native Gradle gets 21, except that a positively-detected Kotlin
package/README.md CHANGED
@@ -385,7 +385,7 @@ configuration for either:
385
385
  | Detected by | `pubspec.yaml` at the repo root | `settings.gradle(.kts)` + `gradlew` at the root or under `android/` |
386
386
  | App module | `android/app` | the one module applying `com.android.application` (version-catalog aliases are resolved; a module named `app` wins a tie against a wear/automotive sibling) |
387
387
  | Tests (`run-tests`) | `flutter analyze` + `flutter test` | `gradlew test` (all variants, all modules) |
388
- | Build | `flutter build appbundle --release` | `<module>:bundleRelease` |
388
+ | Build | `flutter build appbundle --release --obfuscate --split-debug-info` | `<module>:bundleRelease` |
389
389
  | Toolchain installed | Flutter + JDK 17 | JDK 21, or 17 on an old Kotlin — see [Choosing the JDK](#choosing-the-jdk) |
390
390
 
391
391
  A Flutter app also carries `android/settings.gradle`, so `pubspec.yaml` wins the
@@ -443,6 +443,89 @@ warnings.
443
443
  own naming is inconsistent across majors — `gradle-8.14-all.zip` but
444
444
  `gradle-9.0.0-all.zip`.)
445
445
 
446
+ ### Dart obfuscation (Flutter apps)
447
+
448
+ **Obfuscation is mandatory for Flutter apps.** Every Flutter release build this
449
+ package produces — the Android App Bundle and the iOS archive — is compiled with
450
+ `--obfuscate --split-debug-info`, and there is no input, repository variable or
451
+ config key that turns it off. Native Swift and Kotlin/Gradle apps are
452
+ unaffected: the flag belongs to the Dart compiler and they have no Dart code.
453
+ Flutter web builds are also untouched — `flutter build web` has no such flag;
454
+ dart2js minifies release output instead. An Android app delivered through
455
+ [Bitrise](#android-delivery-via-bitrise-alternative-mode) is built by Bitrise's
456
+ workflow, not by this action; put the same two flags in that workflow.
457
+
458
+ What it does: the Dart AOT compiler replaces class, function and library names
459
+ with short random identifiers, so the app's Dart symbols cannot be read out of
460
+ the binary. What it does not do: it is not encryption. String literals,
461
+ `--dart-define` values, assets and everything in `pubspec.yaml` ship as they
462
+ are, and the native halves of the app (Swift, Kotlin, plugins) are compiled by
463
+ Xcode and Gradle exactly as before. Do not put secrets in the app.
464
+
465
+ The two flags are one feature. `flutter build` refuses `--obfuscate` without
466
+ `--split-debug-info`, and the symbol files the latter writes are the only way
467
+ to read a stack trace from an obfuscated build. Every run therefore keeps them
468
+ as a workflow artifact next to the build, and fails when they are missing:
469
+
470
+ | Platform | Artifact | Contents | Retained by |
471
+ |---|---|---|---|
472
+ | Android | `android-symbols-<package>-<versionCode>` | `app.android-arm.symbols`, `app.android-arm64.symbols`, `app.android-x64.symbols` | the Android action, right after the build |
473
+ | iOS | `ios-symbols-<repo>-<TestFlight build number>` | `app.ios-arm64.symbols` | `deploy.yml`, after the archive |
474
+
475
+ The Android action fails the build before anything is signed if no symbol
476
+ files were written. On iOS the archive, TestFlight upload and App Store
477
+ metadata steps all run inside one composite action, so the check comes after
478
+ it: the retain step runs even when that action failed (a metadata error after
479
+ the upload must not discard the symbols of a build that is already live), and
480
+ an action that succeeded without writing symbols fails the job.
481
+
482
+ Artifacts follow the repository's retention setting — 90 days unless you
483
+ change it under *Settings → Actions → Artifact and log retention* (private
484
+ repositories allow up to 400). A production release usually outlives that, so
485
+ either raise the setting or, better, upload the symbols to your crash reporter,
486
+ which is where crashes arrive anyway.
487
+
488
+ To read an obfuscated crash, download the artifact for that exact build and
489
+ run `flutter symbolize` with the file matching the device's architecture:
490
+
491
+ ```bash
492
+ flutter symbolize -i crash.txt -d app.android-arm64.symbols
493
+ ```
494
+
495
+ A crash reporter never sees the artifact, and from the first obfuscated build
496
+ every Dart frame it shows is `***` until it has the symbols. If the app uses
497
+ Crashlytics, Sentry or similar, upload the same directory to that service — for
498
+ Crashlytics, `firebase crashlytics:symbols:upload --app=<FIREBASE_APP_ID>
499
+ <dir>` with Firebase CLI 11.9 or newer, once per platform with that platform's
500
+ app id (`mobilesdk_app_id` in `google-services.json`, `GOOGLE_APP_ID` in
501
+ `GoogleService-Info.plist`). This package does not do that upload for you: the
502
+ existing Crashlytics run-script phase on iOS uploads only the native dSYMs, and
503
+ App Store Connect's own symbol upload covers only those as well.
504
+
505
+ Obfuscation renames identifiers, so code that depends on their spelling breaks
506
+ at runtime, not at build time. Do not rely on `runtimeType.toString()`,
507
+ `Type.toString()`, or on matching class or function names in stack traces
508
+ (`Foo` becomes `Lk`, and a raw trace is a row of `***`) — compare types with
509
+ `is` instead. Enum values keep their names today (`Color.red.toString()` is
510
+ still `Color.red`), but `flutter build --help` lists `Enum.toString()` among
511
+ the methods that may return obfuscated results, so prefer `Enum.name` when the
512
+ identifier matters.
513
+
514
+ Where the flags are applied: Android passes them straight to
515
+ `flutter build appbundle`. iOS is less obvious — `deploy.yml` runs
516
+ `flutter build ios --config-only` with the flags, which writes
517
+ `DART_OBFUSCATION=true` and `SPLIT_DEBUG_INFO=<dir>` into
518
+ `ios/Flutter/Generated.xcconfig`, and the Flutter build phase inside the
519
+ `xcodebuild archive` that follows compiles against those settings. The symbol
520
+ directory lives under the runner's temp directory on both platforms, never in
521
+ the checkout.
522
+
523
+ Because the iOS half lives in `deploy.yml`, which
524
+ [auto-update never touches](#deployyml-is-not-auto-updated), an existing
525
+ Flutter repo gets Android obfuscation on its next auto-update but iOS
526
+ obfuscation only after a one-off `npx --yes gowalk-cicd`. The Android action
527
+ retains its own symbols precisely so that this half-updated state is safe.
528
+
446
529
  ### iOS delivery
447
530
 
448
531
  The iOS composite action runs on `macos-15` and:
@@ -761,6 +844,12 @@ and you must run `npx --yes gowalk-cicd` manually once to
761
844
  sync your `deploy.yml`. Existing deploy.yml stays untouched on every
762
845
  auto-update cycle until you do.
763
846
 
847
+ Changes that need that manual run:
848
+
849
+ - **Flutter iOS obfuscation** — the `--obfuscate --split-debug-info` flags on
850
+ the iOS `flutter build ios --config-only` call and the step that retains
851
+ `ios-symbols-*`. See [Dart obfuscation](#dart-obfuscation-flutter-apps).
852
+
764
853
  ### Opt out
765
854
 
766
855
  Pin the vendored copy by passing `auto-update: 'false'` to the action:
@@ -1 +1 @@
1
- 1.0.44
1
+ 1.0.46
package/action/action.yml CHANGED
@@ -830,7 +830,7 @@ runs:
830
830
  # rate limit, outage) must never fail a build whose TestFlight upload
831
831
  # already succeeded. The apply step below tolerates an empty response.
832
832
  continue-on-error: true
833
- uses: actions/ai-inference@v1
833
+ uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1.2.8 (last v1)
834
834
  with:
835
835
  prompt-file: ${{ runner.temp }}/rendered_phase1.yml
836
836
  model: ${{ inputs.ai-metadata-model }}
@@ -937,7 +937,7 @@ runs:
937
937
  || steps.phase1_apply.outputs.phase1_ok == 'true') }}
938
938
  # Best-effort (see Phase 1): never fail the build on a model API error.
939
939
  continue-on-error: true
940
- uses: actions/ai-inference@v1
940
+ uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1.2.8 (last v1)
941
941
  with:
942
942
  prompt-file: ${{ runner.temp }}/rendered_phase2.yml
943
943
  model: ${{ inputs.ai-metadata-model }}
@@ -1 +1 @@
1
- 1.0.44
1
+ 1.0.46
@@ -1,7 +1,8 @@
1
1
  name: Android build and signing
2
2
  description: >
3
- Auto-detect an Android app — Flutter or native Gradle — build a release AAB,
4
- and sign it with the upload key stored under creds/. Google Play upload is
3
+ Auto-detect an Android app — Flutter or native Gradle — build a release AAB
4
+ (Flutter apps are compiled with Dart obfuscation, which is mandatory), and
5
+ sign it with the upload key stored under creds/. Google Play upload is
5
6
  handled by the package workflow so the signed artifact is always retained
6
7
  first.
7
8
 
@@ -46,6 +47,12 @@ outputs:
46
47
  project-kind:
47
48
  description: Detected build system — `flutter` or `gradle`
48
49
  value: ${{ steps.config.outputs.project_kind }}
50
+ symbols-path:
51
+ description: >-
52
+ Directory holding the Dart symbol files (app.android-*.symbols) that
53
+ `flutter symbolize` needs to read a stack trace from this obfuscated
54
+ build. Empty for native Gradle apps, which have no Dart code.
55
+ value: ${{ steps.flutter_build.outputs.symbols_path }}
49
56
 
50
57
  runs:
51
58
  using: composite
@@ -89,21 +96,58 @@ runs:
89
96
  echo "No test/ directory; skipping flutter test."
90
97
  fi
91
98
 
99
+ # Dart obfuscation is mandatory for Flutter apps: every release build is
100
+ # compiled with --obfuscate, and there is deliberately no input or repo
101
+ # variable that switches it off. --split-debug-info is not optional
102
+ # either — `flutter build` refuses --obfuscate without it — and the symbol
103
+ # files it writes are the only way to read a stack trace from this build,
104
+ # so the build fails when they are missing. The directory lives under
105
+ # RUNNER_TEMP rather than in the checkout so nothing that inspects or
106
+ # commits the working tree can ever see it.
92
107
  - name: Build release Android App Bundle
108
+ id: flutter_build
93
109
  if: ${{ steps.config.outputs.project_kind == 'flutter' }}
94
110
  shell: bash
95
111
  env:
96
112
  BUILD_NAME: ${{ inputs.build-name }}
113
+ DART_SYMBOLS_DIR: ${{ runner.temp }}/dart-symbols/android
97
114
  run: |
98
115
  args=(
99
116
  build appbundle
100
117
  --release
101
118
  --build-number "$ANDROID_BUILD_NUMBER"
119
+ --obfuscate
120
+ --split-debug-info "$DART_SYMBOLS_DIR"
102
121
  )
103
122
  if [ -n "$BUILD_NAME" ]; then
104
123
  args+=(--build-name "$BUILD_NAME")
105
124
  fi
106
125
  flutter "${args[@]}"
126
+ if ! ls "$DART_SYMBOLS_DIR"/*.symbols >/dev/null 2>&1; then
127
+ echo "::error::flutter build wrote no Dart symbol files to $DART_SYMBOLS_DIR;" \
128
+ "a crash from this obfuscated build could never be read. Refusing to ship it."
129
+ exit 1
130
+ fi
131
+ ls -la "$DART_SYMBOLS_DIR"
132
+ echo "symbols_path=$DART_SYMBOLS_DIR" >> "$GITHUB_OUTPUT"
133
+
134
+ # The symbols are retained HERE, not in the workflow, on purpose. The
135
+ # consumer-side autoupdate re-vendors this action but can never touch
136
+ # deploy.yml (GITHUB_TOKEN cannot push workflow files), so an updated
137
+ # action paired with an older workflow is the normal state of the fleet.
138
+ # If the flag lived here and the safety net lived in the workflow, that
139
+ # pairing would ship obfuscated bundles and destroy the only copy of their
140
+ # symbols with the runner. Whatever obfuscation needs to be safe must
141
+ # travel in the same file as the flag. No retention-days: the repository's
142
+ # own artifact retention setting governs, and unlike a fixed number it can
143
+ # be raised to cover the life of a release.
144
+ - name: Retain Dart symbol files
145
+ if: ${{ steps.config.outputs.project_kind == 'flutter' }}
146
+ uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
147
+ with:
148
+ name: android-symbols-${{ steps.config.outputs.package_name }}-${{ steps.config.outputs.build_number }}
149
+ path: ${{ steps.flutter_build.outputs.symbols_path }}
150
+ if-no-files-found: error
107
151
 
108
152
  # --- Native Gradle --------------------------------------------------
109
153
  # resolve_android.py rewrites versionCode/versionName in the module build
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env python3
2
+ """Dart obfuscation is mandatory for Flutter apps — pin the wiring.
3
+
4
+ Nothing in this suite can run `flutter build`, so these tests read the action
5
+ text the way test_version_override.py does and check that
6
+
7
+ * the Flutter release build passes --obfuscate together with
8
+ --split-debug-info (the tool refuses one without the other),
9
+ * the symbol directory is written outside the checkout,
10
+ * the build refuses to ship when no symbol files were produced,
11
+ * the action itself retains the symbols as an artifact — the consumer-side
12
+ autoupdate re-vendors this action but never deploy.yml, so the flag and
13
+ its safety net must not be split across that boundary — and
14
+ * the native Gradle path — which has no Dart code — is left alone.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import re
20
+ import unittest
21
+ from pathlib import Path
22
+
23
+ ACTION_DIR = Path(__file__).resolve().parent.parent
24
+ ACTION_YML = ACTION_DIR / "action.yml"
25
+
26
+
27
+ def step(name: str) -> str:
28
+ """One composite step, as raw text, from its `- name:` line to the next."""
29
+ text = ACTION_YML.read_text()
30
+ pattern = rf" - name: {re.escape(name)}\n(?P<body>.*?)(?=\n - name:|\Z)"
31
+ match = re.search(pattern, text, re.DOTALL)
32
+ if match is None:
33
+ raise AssertionError(f"missing action step: {name}")
34
+ return match.group("body")
35
+
36
+
37
+ def outputs_block() -> str:
38
+ text = ACTION_YML.read_text()
39
+ return text[text.index("\noutputs:\n") : text.index("\nruns:\n")]
40
+
41
+
42
+ class FlutterObfuscationWiringTest(unittest.TestCase):
43
+ def setUp(self) -> None:
44
+ self.flutter = step("Build release Android App Bundle")
45
+ self.gradle = step("Build release Android App Bundle with Gradle")
46
+
47
+ def test_the_flutter_release_build_is_obfuscated(self) -> None:
48
+ self.assertIn("--release", self.flutter)
49
+ self.assertIn("--obfuscate", self.flutter)
50
+ # --obfuscate is rejected by the tool without --split-debug-info.
51
+ self.assertIn("--split-debug-info", self.flutter)
52
+
53
+ def test_both_flags_are_unconditional(self) -> None:
54
+ # Not inside an `if [ -n ... ]` like --build-name: obfuscation is
55
+ # mandatory, so the flags sit in the base argument list.
56
+ start = self.flutter.index("args=(")
57
+ base_args = self.flutter[start : self.flutter.index("\n )", start)]
58
+ self.assertIn("--obfuscate", base_args)
59
+ self.assertIn("--split-debug-info", base_args)
60
+
61
+ def test_there_is_no_opt_out(self) -> None:
62
+ text = ACTION_YML.read_text()
63
+ self.assertNotIn("--no-obfuscate", text)
64
+ self.assertNotRegex(text, r"inputs\.[a-z-]*obfusc")
65
+ self.assertNotRegex(text, r"vars\.[A-Z_]*OBFUSC")
66
+
67
+ def test_symbols_are_written_outside_the_checkout(self) -> None:
68
+ # The iOS action commits bot changes back to the repo; keeping the
69
+ # symbols under RUNNER_TEMP means no step can ever stage them.
70
+ self.assertIn("DART_SYMBOLS_DIR: ${{ runner.temp }}/", self.flutter)
71
+ self.assertIn('--split-debug-info "$DART_SYMBOLS_DIR"', self.flutter)
72
+
73
+ def test_the_build_fails_when_no_symbol_files_were_written(self) -> None:
74
+ guard = self.flutter[self.flutter.index('"$DART_SYMBOLS_DIR"/*.symbols') :]
75
+ self.assertIn("::error::", guard[: guard.index("fi\n")])
76
+ self.assertIn("exit 1", guard[: guard.index("fi\n")])
77
+ # ... and the check happens after the build, not before it.
78
+ self.assertLess(
79
+ self.flutter.index('flutter "${args[@]}"'),
80
+ self.flutter.index("*.symbols"),
81
+ )
82
+
83
+ def test_the_symbol_directory_reaches_the_workflow(self) -> None:
84
+ self.assertIn("id: flutter_build", self.flutter)
85
+ self.assertIn('echo "symbols_path=$DART_SYMBOLS_DIR" >> "$GITHUB_OUTPUT"', self.flutter)
86
+ outputs = outputs_block()
87
+ self.assertIn(" symbols-path:\n", outputs)
88
+ self.assertIn("${{ steps.flutter_build.outputs.symbols_path }}", outputs)
89
+
90
+ def test_the_action_retains_the_symbols_itself(self) -> None:
91
+ retain = step("Retain Dart symbol files")
92
+ self.assertIn("project_kind == 'flutter'", retain)
93
+ self.assertRegex(retain, r"uses: actions/upload-artifact@[0-9a-f]{40}")
94
+ self.assertIn("path: ${{ steps.flutter_build.outputs.symbols_path }}", retain)
95
+ self.assertIn("name: android-symbols-${{ steps.config.outputs.package_name }}", retain)
96
+ self.assertIn("if-no-files-found: error", retain)
97
+ # The repository's retention setting governs; a fixed number cannot be
98
+ # raised to cover the life of a release.
99
+ self.assertNotIn("retention-days", retain)
100
+ # ... after the build that writes them.
101
+ text = ACTION_YML.read_text()
102
+ self.assertLess(
103
+ text.index("- name: Build release Android App Bundle\n"),
104
+ text.index("- name: Retain Dart symbol files"),
105
+ )
106
+
107
+ def test_the_gradle_build_is_left_alone(self) -> None:
108
+ # A native app has no Dart code to obfuscate; the flag would be an
109
+ # unknown Gradle argument.
110
+ self.assertNotIn("obfuscate", self.gradle)
111
+ self.assertNotIn("split-debug-info", self.gradle)
112
+
113
+
114
+ if __name__ == "__main__":
115
+ unittest.main()
@@ -1 +1 @@
1
- 1.0.44
1
+ 1.0.46
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.44",
3
+ "version": "1.0.46",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -106,8 +106,18 @@ jobs:
106
106
  dart pub global activate flutterfire_cli >/dev/null 2>&1 || true
107
107
  echo "$HOME/.pub-cache/bin" >> "$GITHUB_PATH"
108
108
  fi
109
- flutter build ios --release --no-codesign --config-only
109
+ # Dart obfuscation is mandatory for Flutter apps, and this is where
110
+ # the iOS build gets it even though xcodebuild does the compiling:
111
+ # --config-only writes DART_OBFUSCATION=true and SPLIT_DEBUG_INFO=<dir>
112
+ # into ios/Flutter/Generated.xcconfig, and the Flutter build phase
113
+ # inside the archive below compiles against those settings. The
114
+ # symbol files land under RUNNER_TEMP (never in the checkout) and are
115
+ # retained after the archive — without them an obfuscated crash log
116
+ # cannot be read.
117
+ flutter build ios --release --no-codesign --config-only \
118
+ --obfuscate --split-debug-info="$RUNNER_TEMP/dart-symbols/ios"
110
119
  - uses: ./.github/actions/swift-app
120
+ id: ios
111
121
  with:
112
122
  project: ${{ steps.flutter.outputs.enabled == 'true' && 'ios/Runner.xcodeproj' || '' }}
113
123
  workspace: ${{ steps.flutter.outputs.enabled == 'true' && 'ios/Runner.xcworkspace' || '' }}
@@ -118,6 +128,46 @@ jobs:
118
128
  # is locked in review. Default-branch deploys retain metadata automation.
119
129
  manage-app-store-version: >-
120
130
  ${{ github.ref_name == github.event.repository.default_branch && 'true' || 'false' }}
131
+ # `flutter symbolize` with app.ios-arm64.symbols is the only way to read a
132
+ # stack trace from the build that just went to TestFlight (App Store
133
+ # Connect receives native dSYMs only), and this runner holds the only
134
+ # copy. The archive writes the symbols well before the upload, and the
135
+ # action keeps working after the upload (App Store metadata, "What's
136
+ # New"), so these two steps run on `!cancelled()`: a failure there must
137
+ # not discard the symbols of a build that is already live. An action that
138
+ # succeeded without writing symbols is a broken obfuscation setup and
139
+ # fails here rather than shipping crashes nobody can read; an action that
140
+ # failed earlier simply has nothing to retain and adds no second error.
141
+ - name: Locate Dart symbol files
142
+ id: ios_symbols
143
+ if: ${{ !cancelled() && steps.flutter.outputs.enabled == 'true' }}
144
+ shell: bash
145
+ env:
146
+ ACTION_OUTCOME: ${{ steps.ios.outcome }}
147
+ run: |
148
+ dir="$RUNNER_TEMP/dart-symbols/ios"
149
+ if ls "$dir"/*.symbols >/dev/null 2>&1; then
150
+ ls -la "$dir"
151
+ echo "present=true" >> "$GITHUB_OUTPUT"
152
+ elif [ "$ACTION_OUTCOME" = "success" ]; then
153
+ echo "::error::the archive wrote no Dart symbol files to $dir;" \
154
+ "a crash from this obfuscated build could never be read."
155
+ exit 1
156
+ else
157
+ echo "No Dart symbol files (the build did not get as far as the archive)."
158
+ echo "present=false" >> "$GITHUB_OUTPUT"
159
+ fi
160
+ # BUILD_NUMBER comes from the action through GITHUB_ENV, so the artifact
161
+ # is named after the TestFlight build it belongs to. No retention-days:
162
+ # the repository's artifact retention setting governs (90 days unless
163
+ # raised), and a crash reporter needs its own copy — see the README.
164
+ - name: Retain Dart symbol files
165
+ if: ${{ !cancelled() && steps.ios_symbols.outputs.present == 'true' }}
166
+ uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
167
+ with:
168
+ name: ios-symbols-${{ github.event.repository.name }}-${{ env.BUILD_NUMBER || github.run_number }}
169
+ path: ${{ runner.temp }}/dart-symbols/ios
170
+ if-no-files-found: error
121
171
 
122
172
  android:
123
173
  name: Android Google Play
@@ -324,6 +374,10 @@ jobs:
324
374
  path: ${{ steps.android.outputs.bundle-path }}
325
375
  if-no-files-found: error
326
376
  retention-days: 30
377
+ # A Flutter app's Dart symbol files (android-symbols-<package>-<code>)
378
+ # are retained by the action itself, right after the obfuscated build —
379
+ # the action autoupdates on consumers while this file does not, and the
380
+ # flag and its safety net must never be split across that boundary.
327
381
  - name: Install Google Play preflight dependencies
328
382
  if: ${{ steps.mode.outputs.mode == 'local' }}
329
383
  shell: bash