gowalk-cicd 1.0.45 → 1.0.47
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 +27 -0
- package/README.md +90 -1
- package/action/.daemux-version +1 -1
- package/android-action/.daemux-version +1 -1
- package/android-action/action.yml +46 -2
- package/android-action/scripts/test_flutter_obfuscation.py +115 -0
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
- package/templates/deploy.yml +59 -4
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:
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.47
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.47
|
|
@@ -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
|
-
|
|
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.
|
|
1
|
+
1.0.47
|
package/package.json
CHANGED
package/templates/deploy.yml
CHANGED
|
@@ -72,10 +72,11 @@ jobs:
|
|
|
72
72
|
~/Library/Caches/CocoaPods
|
|
73
73
|
key: pods-${{ runner.os }}-${{ hashFiles('ios/Podfile.lock', 'pubspec.lock') }}
|
|
74
74
|
restore-keys: pods-${{ runner.os }}-
|
|
75
|
-
# Same as the android job:
|
|
76
|
-
#
|
|
75
|
+
# Same as the android job: an app that deliberately carries a private git
|
|
76
|
+
# dependency has it cloned by `flutter pub get` below. The runner has no
|
|
77
77
|
# SSH key, so an https url only resolves once this token rewrite is in
|
|
78
|
-
# place. No secret => no-op (
|
|
78
|
+
# place. No secret => no-op (panel-built apps depend on pub.dev only; a
|
|
79
|
+
# private dep must otherwise carry its own credentials).
|
|
79
80
|
- name: Authenticate private git dependencies
|
|
80
81
|
if: ${{ steps.flutter.outputs.enabled == 'true' }}
|
|
81
82
|
shell: bash
|
|
@@ -106,8 +107,18 @@ jobs:
|
|
|
106
107
|
dart pub global activate flutterfire_cli >/dev/null 2>&1 || true
|
|
107
108
|
echo "$HOME/.pub-cache/bin" >> "$GITHUB_PATH"
|
|
108
109
|
fi
|
|
109
|
-
|
|
110
|
+
# Dart obfuscation is mandatory for Flutter apps, and this is where
|
|
111
|
+
# the iOS build gets it even though xcodebuild does the compiling:
|
|
112
|
+
# --config-only writes DART_OBFUSCATION=true and SPLIT_DEBUG_INFO=<dir>
|
|
113
|
+
# into ios/Flutter/Generated.xcconfig, and the Flutter build phase
|
|
114
|
+
# inside the archive below compiles against those settings. The
|
|
115
|
+
# symbol files land under RUNNER_TEMP (never in the checkout) and are
|
|
116
|
+
# retained after the archive — without them an obfuscated crash log
|
|
117
|
+
# cannot be read.
|
|
118
|
+
flutter build ios --release --no-codesign --config-only \
|
|
119
|
+
--obfuscate --split-debug-info="$RUNNER_TEMP/dart-symbols/ios"
|
|
110
120
|
- uses: ./.github/actions/swift-app
|
|
121
|
+
id: ios
|
|
111
122
|
with:
|
|
112
123
|
project: ${{ steps.flutter.outputs.enabled == 'true' && 'ios/Runner.xcodeproj' || '' }}
|
|
113
124
|
workspace: ${{ steps.flutter.outputs.enabled == 'true' && 'ios/Runner.xcworkspace' || '' }}
|
|
@@ -118,6 +129,46 @@ jobs:
|
|
|
118
129
|
# is locked in review. Default-branch deploys retain metadata automation.
|
|
119
130
|
manage-app-store-version: >-
|
|
120
131
|
${{ github.ref_name == github.event.repository.default_branch && 'true' || 'false' }}
|
|
132
|
+
# `flutter symbolize` with app.ios-arm64.symbols is the only way to read a
|
|
133
|
+
# stack trace from the build that just went to TestFlight (App Store
|
|
134
|
+
# Connect receives native dSYMs only), and this runner holds the only
|
|
135
|
+
# copy. The archive writes the symbols well before the upload, and the
|
|
136
|
+
# action keeps working after the upload (App Store metadata, "What's
|
|
137
|
+
# New"), so these two steps run on `!cancelled()`: a failure there must
|
|
138
|
+
# not discard the symbols of a build that is already live. An action that
|
|
139
|
+
# succeeded without writing symbols is a broken obfuscation setup and
|
|
140
|
+
# fails here rather than shipping crashes nobody can read; an action that
|
|
141
|
+
# failed earlier simply has nothing to retain and adds no second error.
|
|
142
|
+
- name: Locate Dart symbol files
|
|
143
|
+
id: ios_symbols
|
|
144
|
+
if: ${{ !cancelled() && steps.flutter.outputs.enabled == 'true' }}
|
|
145
|
+
shell: bash
|
|
146
|
+
env:
|
|
147
|
+
ACTION_OUTCOME: ${{ steps.ios.outcome }}
|
|
148
|
+
run: |
|
|
149
|
+
dir="$RUNNER_TEMP/dart-symbols/ios"
|
|
150
|
+
if ls "$dir"/*.symbols >/dev/null 2>&1; then
|
|
151
|
+
ls -la "$dir"
|
|
152
|
+
echo "present=true" >> "$GITHUB_OUTPUT"
|
|
153
|
+
elif [ "$ACTION_OUTCOME" = "success" ]; then
|
|
154
|
+
echo "::error::the archive wrote no Dart symbol files to $dir;" \
|
|
155
|
+
"a crash from this obfuscated build could never be read."
|
|
156
|
+
exit 1
|
|
157
|
+
else
|
|
158
|
+
echo "No Dart symbol files (the build did not get as far as the archive)."
|
|
159
|
+
echo "present=false" >> "$GITHUB_OUTPUT"
|
|
160
|
+
fi
|
|
161
|
+
# BUILD_NUMBER comes from the action through GITHUB_ENV, so the artifact
|
|
162
|
+
# is named after the TestFlight build it belongs to. No retention-days:
|
|
163
|
+
# the repository's artifact retention setting governs (90 days unless
|
|
164
|
+
# raised), and a crash reporter needs its own copy — see the README.
|
|
165
|
+
- name: Retain Dart symbol files
|
|
166
|
+
if: ${{ !cancelled() && steps.ios_symbols.outputs.present == 'true' }}
|
|
167
|
+
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
|
168
|
+
with:
|
|
169
|
+
name: ios-symbols-${{ github.event.repository.name }}-${{ env.BUILD_NUMBER || github.run_number }}
|
|
170
|
+
path: ${{ runner.temp }}/dart-symbols/ios
|
|
171
|
+
if-no-files-found: error
|
|
121
172
|
|
|
122
173
|
android:
|
|
123
174
|
name: Android Google Play
|
|
@@ -324,6 +375,10 @@ jobs:
|
|
|
324
375
|
path: ${{ steps.android.outputs.bundle-path }}
|
|
325
376
|
if-no-files-found: error
|
|
326
377
|
retention-days: 30
|
|
378
|
+
# A Flutter app's Dart symbol files (android-symbols-<package>-<code>)
|
|
379
|
+
# are retained by the action itself, right after the obfuscated build —
|
|
380
|
+
# the action autoupdates on consumers while this file does not, and the
|
|
381
|
+
# flag and its safety net must never be split across that boundary.
|
|
327
382
|
- name: Install Google Play preflight dependencies
|
|
328
383
|
if: ${{ steps.mode.outputs.mode == 'local' }}
|
|
329
384
|
shell: bash
|