gowalk-cicd 1.0.57 → 1.0.59
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 +22 -27
- package/README.md +30 -65
- package/action/.daemux-version +1 -1
- package/action/action.yml +39 -33
- package/action/scripts/apple_store_proxy.py +28 -0
- package/action/scripts/asc_common.py +5 -3
- package/action/scripts/crashlytics_dsyms.py +9 -31
- package/action/scripts/prepare_crashlytics_build.py +77 -0
- package/action/scripts/prepare_crashlytics_dsyms.py +59 -0
- package/action/scripts/test_action_step_order.py +1 -1
- package/action/scripts/test_apple_store_proxy.py +44 -0
- package/action/scripts/test_asc_common_timeouts.py +1 -0
- package/action/scripts/test_crashlytics_dsyms.py +8 -11
- package/action/scripts/test_prepare_crashlytics.py +94 -0
- package/action/scripts/test_upload_build.py +118 -0
- package/action/scripts/upload_build.py +164 -0
- package/android-action/.daemux-version +1 -1
- package/android-action/action.yml +14 -0
- package/android-action/scripts/crashlytics_symbols.py +6 -3
- package/android-action/scripts/play_preflight.py +11 -4
- package/android-action/scripts/play_store_proxy.py +37 -0
- package/android-action/scripts/resolve_android.py +7 -5
- package/android-action/scripts/test_crashlytics_symbols.py +4 -1
- package/android-action/scripts/test_play_store_proxy.py +40 -0
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
- package/templates/deploy.yml +15 -0
package/CLAUDE.md
CHANGED
|
@@ -98,6 +98,12 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
98
98
|
Detect and warn, tell the user to remove lines manually.
|
|
99
99
|
- Store API calls belong in GitHub Actions. Local tests may build/sign but must
|
|
100
100
|
never connect to App Store Connect or Google Play.
|
|
101
|
+
- Every Apple/Google store API call, OAuth exchange and binary upload requires its
|
|
102
|
+
assigned `APPLE_STORE_PROXY_URL` / `GOOGLE_STORE_PROXY_URL` repository secret.
|
|
103
|
+
Python clients enforce explicit proxies; Play upload steps set the proxy for the
|
|
104
|
+
pinned Google client. Missing configuration must stop before a provider call.
|
|
105
|
+
Apple IPA uploads use `action/scripts/upload_build.py`, not native upload tools.
|
|
106
|
+
It retains provider IDs/checksums and verifies matching IPA identity before resume.
|
|
101
107
|
- Signing material should use encrypted Actions secrets. `deploy.yml`
|
|
102
108
|
materializes `ASC_KEY_P8` + its ID/issuer, the Apple distribution identity
|
|
103
109
|
(`IOS_DISTRIBUTION_P12_BASE64`, `IOS_DISTRIBUTION_CERT_META_BASE64`,
|
|
@@ -155,31 +161,19 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
155
161
|
must not discard the symbols of a build that is already live. Symbol
|
|
156
162
|
artifacts carry no `retention-days` so the repository's setting governs and
|
|
157
163
|
can be raised to cover a release's life.
|
|
158
|
-
- Crashlytics
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
`action/scripts/crashlytics_dsyms.py` — because Flutter's `.symbols` are
|
|
172
|
-
ELF debug info the Firebase CLI's generators reject for Apple targets, and
|
|
173
|
-
`App.framework.dSYM` is the documented Apple path. Both run before the
|
|
174
|
-
store upload and fail closed — except an iOS id on an app with no
|
|
175
|
-
FirebaseCrashlytics `upload-symbols` tool, which is a warn-and-skip: an app
|
|
176
|
-
that does not embed the SDK has nothing to symbolicate. Logic belongs in
|
|
177
|
-
those scripts, where it is unit-tested, not inline in YAML. Supporting
|
|
178
|
-
invariants: the workflow's disk-space step must never remove
|
|
179
|
-
`/usr/local/lib/node_modules` (npm and npx live there); `firebase-tools` is
|
|
180
|
-
pinned exactly because the Android job holds the keystore and the Play
|
|
181
|
-
service account; the CLI runs from a temp cwd so its `.crashlytics/` debris
|
|
182
|
-
never lands in the checkout.
|
|
164
|
+
- Firebase Crashlytics traffic follows the Google account proxy. Android builds and
|
|
165
|
+
the pinned Firebase CLI receive the explicit proxy environment; Crashlytics Gradle
|
|
166
|
+
2.9.2+ and the pinned buildtools support it (generic JVM networking does not).
|
|
167
|
+
Native iOS upload-symbols uses NSURLSession, so it is never executed. Before
|
|
168
|
+
archiving, dedicated Crashlytics upload phases in the ephemeral Xcode projects
|
|
169
|
+
are deferred; mixed/unidentified upload phases refuse the archive for repair.
|
|
170
|
+
CI retains the archive's dSYMs as a ZIP with UUIDs, source SHA and SHA-256, then
|
|
171
|
+
emits `firebase_symbols_pending` (`gowalk-cicd/firebase-symbols-pending.v1`).
|
|
172
|
+
The app session downloads that exact artifact and uploads it through its
|
|
173
|
+
account-pinned Firebase console, reading processing back before completing the
|
|
174
|
+
delivery request. This requires no human handoff. A green CI run alone is not
|
|
175
|
+
evidence that the deferred symbols reached Firebase. Android retains its Dart
|
|
176
|
+
symbols and uploads them before the store bundle, using the configured proxy.
|
|
183
177
|
- The iOS action reads every script and prompt from the snapshot it takes of
|
|
184
178
|
itself in its first step (`$SWIFT_APP_ACTION`, under `RUNNER_TEMP`), never
|
|
185
179
|
from `${{ github.action_path }}` after that step. The plugin self-update
|
|
@@ -188,14 +182,15 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
188
182
|
release that fixes it; the snapshot is what keeps that run on one version.
|
|
189
183
|
`test_action_step_order.py` pins both. A new script reference in
|
|
190
184
|
`action/action.yml` therefore uses `"$SWIFT_APP_ACTION/scripts/..."`.
|
|
191
|
-
-
|
|
185
|
+
- Three situations end a green job with work for the panel, and each is a typed
|
|
192
186
|
check-run annotation (`::notice title=<name>::<one-line JSON with a versioned
|
|
193
187
|
schema>`), never prose in a `::warning::`: `play_review_pending`
|
|
194
188
|
(`templates/deploy.yml`, Play refused auto-submit and the bundle landed
|
|
195
189
|
unsubmitted) and `store_version_locked` (`action/action.yml`, the
|
|
196
190
|
`TESTFLIGHT_ONLY` decision of `manage_marketing_version.py`: an App Store
|
|
197
191
|
version is under review, so the build shipped to TestFlight under that
|
|
198
|
-
version and metadata was skipped)
|
|
192
|
+
version and metadata was skipped), and `firebase_symbols_pending` (the dSYM
|
|
193
|
+
archive awaits the session's proxied console upload). The README lists the schemas; add a
|
|
199
194
|
field by bumping the version.
|
|
200
195
|
- The JDK is chosen by `scripts/select_jdk.py`, called from the workflow before
|
|
201
196
|
`setup-java`. The `JAVA_VERSION` repo variable always wins; otherwise Flutter
|
package/README.md
CHANGED
|
@@ -279,6 +279,7 @@ the situation and whose **message** is one line of JSON carrying a versioned
|
|
|
279
279
|
| --- | --- | --- |
|
|
280
280
|
| `play_review_pending` | `gowalk-cicd/play-review-pending.v1` | `package`, `track`, `version_code` |
|
|
281
281
|
| `store_version_locked` | `gowalk-cicd/store-version-locked.v1` | `version`, `state`, `build_number` |
|
|
282
|
+
| `firebase_symbols_pending` | `gowalk-cicd/firebase-symbols-pending.v1` | `platform`, `app_id`, `source_sha`, `artifact`, `file`, `sha256`, `status` |
|
|
282
283
|
|
|
283
284
|
Add a field by bumping the schema version; never change the meaning of an
|
|
284
285
|
existing one.
|
|
@@ -329,7 +330,7 @@ The Android job removes the preinstalled toolchains a Flutter build never uses
|
|
|
329
330
|
(.NET, the Android NDK, GHC, PowerShell, Swift, Chromium) and prunes Docker
|
|
330
331
|
images, reclaiming roughly 25 GB in a few seconds. It prints `df -h /` before
|
|
331
332
|
and after. Linux only; skipped in Bitrise mode. npm survives on purpose: the
|
|
332
|
-
[Crashlytics symbol upload](#crashlytics-symbol-
|
|
333
|
+
[Crashlytics symbol upload](#crashlytics-symbol-delivery-firebase_app_id) runs
|
|
333
334
|
the Firebase CLI through `npx`.
|
|
334
335
|
|
|
335
336
|
### Private git dependencies (Flutter)
|
|
@@ -572,77 +573,41 @@ the workflow uploads them itself once the repository variable
|
|
|
572
573
|
**`FIREBASE_APP_ID`** is set — see below. Sentry or others: upload the same
|
|
573
574
|
directory to that service from your own pipeline.
|
|
574
575
|
|
|
575
|
-
#### Crashlytics symbol
|
|
576
|
+
#### Crashlytics symbol delivery (`FIREBASE_APP_ID`)
|
|
576
577
|
|
|
577
|
-
Set the repository variable
|
|
578
|
-
`1:<project
|
|
579
|
-
|
|
578
|
+
Set the repository variable to the Firebase App IDs for the shipping platforms
|
|
579
|
+
(`1:<project>:ios:<hash>` and/or `1:<project>:android:<hash>`). The workflow passes it
|
|
580
|
+
as the `firebase-app-id` input. Configure the account's `GOOGLE_STORE_PROXY_URL`
|
|
581
|
+
repository secret before any provider operation.
|
|
580
582
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
583
|
+
Android builds and the pinned Firebase CLI receive the proxy explicitly, including
|
|
584
|
+
Crashlytics mapping/native-symbol uploads. Crashlytics Gradle 2.9.2+ supports proxy
|
|
585
|
+
environment variables; older versions must be updated before use. Dart symbols are
|
|
586
|
+
retained as artifacts and uploaded through that transport before the bundle ships.
|
|
584
587
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
`ios
|
|
588
|
-
|
|
589
|
-
|
|
588
|
+
Native iOS `upload-symbols` is refused because it does not use this environment
|
|
589
|
+
transport. CI defers dedicated native upload phases in its ephemeral projects and
|
|
590
|
+
retains `ios-crashlytics-symbols-<run>-<attempt>` with `ios-dsyms.zip` and a manifest
|
|
591
|
+
containing UUIDs, source SHA and SHA-256. The typed `firebase_symbols_pending`
|
|
592
|
+
annotation uses schema `gowalk-cicd/firebase-symbols-pending.v1` and names the exact
|
|
593
|
+
artifact. The autonomous app session must upload it through the account-pinned
|
|
594
|
+
Firebase console's Crashlytics dSYMs tab and read back processing; no human action
|
|
595
|
+
is required. A completed CI run does not mean this deferred upload is complete.
|
|
596
|
+
[Firebase documents the ZIP upload interface](https://firebase.google.com/docs/crashlytics/ios/get-deobfuscated-reports).
|
|
590
597
|
|
|
591
|
-
The variable reaches the actions through `deploy.yml`, which
|
|
592
|
-
[auto-update never touches](#deployyml-is-not-auto-updated) — an existing repo
|
|
593
|
-
must run `npx --yes gowalk-cicd` once before setting it does anything.
|
|
594
|
-
|
|
595
|
-
| | Android | iOS |
|
|
596
|
-
|---|---|---|
|
|
597
|
-
| What is uploaded | the `--split-debug-info` directory, with the command Firebase documents for Flutter: `firebase crashlytics:symbols:upload --app=<android id> <dir>` (Firebase CLI through `npx`, major-pinned) | every dSYM in the archive — `App.framework.dSYM` carries the Dart frames — with the `upload-symbols` tool from the FirebaseCrashlytics pod: `upload-symbols -ai <ios id> -p ios -- <archive>/dSYMs` |
|
|
598
|
-
| When | in the Android action, right after the build and before the bundle goes to Play | in the iOS action, right after the IPA export and before the TestFlight upload |
|
|
599
|
-
| If it fails | the deploy fails; nothing ships with crashes nobody can read. The symbols are still in the `android-symbols-*` artifact for a manual upload | same — the failure lands before the TestFlight upload, so nothing shipped; fix the cause and re-run the deploy |
|
|
600
|
-
| Variable unset | a `::warning::` when the app depends on `firebase_crashlytics` (every Dart frame will be `***`) | nothing — the run-script phase that `flutterfire configure` adds already uploads `App.framework.dSYM`, and the workflow installs the flutterfire CLI it needs |
|
|
601
|
-
| Variable set for the other platform only | a `::warning::`, no upload | a `::notice::`, no upload |
|
|
602
|
-
|
|
603
|
-
Why the two platforms differ: Flutter's `.symbols` files are ELF debug info,
|
|
604
|
-
which the Firebase CLI's symbol generators accept for Android and reject for
|
|
605
|
-
Apple targets; Firebase documents dSYMs as the Apple path, and Flutter 3.12+
|
|
606
|
-
puts the Dart debug info into `App.framework.dSYM`. On iOS the explicit
|
|
607
|
-
upload is a belt-and-braces over the run-script phase — it also covers the
|
|
608
|
-
native `Runner.app.dSYM` and every plugin framework — so a phase that is
|
|
609
|
-
missing or failing can never leave a build unreadable.
|
|
610
|
-
|
|
611
|
-
A value that is not a Firebase App ID fails the deploy rather than being
|
|
612
|
-
skipped: a typo must not become a fleet that silently stops uploading symbols.
|
|
613
|
-
An iOS id on an app that does not embed Crashlytics (no `upload-symbols` tool
|
|
614
|
-
anywhere under the project) is a warning, not a failure — an app without the
|
|
615
|
-
SDK has no Crashlytics crashes to symbolicate. Android apps delivered through
|
|
616
|
-
Bitrise are built by Bitrise and are not covered.
|
|
617
|
-
|
|
618
|
-
Obfuscation renames identifiers, so code that depends on their spelling breaks
|
|
619
|
-
at runtime, not at build time. Do not rely on `runtimeType.toString()`,
|
|
620
|
-
`Type.toString()`, or on matching class or function names in stack traces
|
|
621
|
-
(`Foo` becomes `Lk`, and a raw trace is a row of `***`) — compare types with
|
|
622
|
-
`is` instead. Enum values keep their names today (`Color.red.toString()` is
|
|
623
|
-
still `Color.red`), but `flutter build --help` lists `Enum.toString()` among
|
|
624
|
-
the methods that may return obfuscated results, so prefer `Enum.name` when the
|
|
625
|
-
identifier matters.
|
|
626
|
-
|
|
627
|
-
Where the flags are applied: Android passes them straight to
|
|
628
|
-
`flutter build appbundle`. iOS is less obvious — `deploy.yml` runs
|
|
629
|
-
`flutter build ios --config-only` with the flags, which writes
|
|
630
|
-
`DART_OBFUSCATION=true` and `SPLIT_DEBUG_INFO=<dir>` into
|
|
631
|
-
`ios/Flutter/Generated.xcconfig`, and the Flutter build phase inside the
|
|
632
|
-
`xcodebuild archive` that follows compiles against those settings. The symbol
|
|
633
|
-
directory lives under the runner's temp directory on both platforms, never in
|
|
634
|
-
the checkout.
|
|
635
|
-
|
|
636
|
-
Because the iOS half lives in `deploy.yml`, which
|
|
637
|
-
[auto-update never touches](#deployyml-is-not-auto-updated), an existing
|
|
638
|
-
Flutter repo gets Android obfuscation on its next auto-update but iOS
|
|
639
|
-
obfuscation only after a one-off `npx --yes gowalk-cicd`. The Android action
|
|
640
|
-
retains its own symbols precisely so that this half-updated state is safe.
|
|
641
598
|
|
|
642
599
|
### iOS delivery
|
|
643
600
|
|
|
644
601
|
The iOS composite action runs on `macos-15` and:
|
|
645
602
|
|
|
603
|
+
All Apple and Google store requests require an account-pinned proxy, including OAuth,
|
|
604
|
+
metadata and binary transfer. Provision `APPLE_STORE_PROXY_URL` and
|
|
605
|
+
`GOOGLE_STORE_PROXY_URL` as encrypted repository secrets before deployment. A missing
|
|
606
|
+
proxy refuses provider calls; there is no direct fallback. Refresh existing workflow
|
|
607
|
+
files with the released installer while preserving application-specific build inputs.
|
|
608
|
+
The Apple uploader retains `apple-upload-<run>-<attempt>` receipts with IPA SHA-256,
|
|
609
|
+
upload ID and processing readback, and resumes matching bytes after interruption.
|
|
610
|
+
|
|
646
611
|
1. **Auto-detects** your `.xcodeproj` / `.xcworkspace`, scheme, bundle ID,
|
|
647
612
|
and `team_id` (from the ASC API key). No `ci.config.yaml` required —
|
|
648
613
|
override via action inputs only if auto-detection fails.
|
|
@@ -659,7 +624,7 @@ The iOS composite action runs on `macos-15` and:
|
|
|
659
624
|
`CI-<bundle_id>`. Patches the `.pbxproj` to use Manual signing against those
|
|
660
625
|
profiles.
|
|
661
626
|
6. **Archives** with `xcodebuild archive`, exports the IPA, and uploads via
|
|
662
|
-
`
|
|
627
|
+
Apple's build-upload REST API through `APPLE_STORE_PROXY_URL`.
|
|
663
628
|
7. **Sets "What's New"** on every declared localization (reads
|
|
664
629
|
`fastlane/metadata/ios/<locale>/release_notes.txt` if present, or from the
|
|
665
630
|
`app-store-whats-new` input).
|
|
@@ -967,7 +932,7 @@ Changes that need that manual run:
|
|
|
967
932
|
`ios-symbols-*`. See [Dart obfuscation](#dart-obfuscation-flutter-apps).
|
|
968
933
|
- **Crashlytics symbol upload** — `firebase-app-id: ${{ vars.FIREBASE_APP_ID }}`
|
|
969
934
|
passed to both actions. Until it is, setting the variable does nothing.
|
|
970
|
-
See [Crashlytics symbol upload](#crashlytics-symbol-
|
|
935
|
+
See [Crashlytics symbol upload](#crashlytics-symbol-delivery-firebase_app_id).
|
|
971
936
|
|
|
972
937
|
### Opt out
|
|
973
938
|
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.59
|
package/action/action.yml
CHANGED
|
@@ -345,7 +345,7 @@ runs:
|
|
|
345
345
|
test
|
|
346
346
|
fi
|
|
347
347
|
|
|
348
|
-
- name: Stage ASC API key for
|
|
348
|
+
- name: Stage ASC API key for authenticated REST uploads
|
|
349
349
|
if: ${{ inputs.archive == 'true' }}
|
|
350
350
|
shell: bash
|
|
351
351
|
run: |
|
|
@@ -353,7 +353,7 @@ runs:
|
|
|
353
353
|
# prints env: blocks on step-group expansion, which leaks secrets.
|
|
354
354
|
# read_config.py already validated creds/AuthKey_<KEY_ID>_Issuer_<UUID>.p8
|
|
355
355
|
# exists and exported ASC_KEY_P8_PATH; we copy the file on disk and
|
|
356
|
-
# stage it
|
|
356
|
+
# stage it for the authenticated REST clients only.
|
|
357
357
|
: "${ASC_KEY_ID:?ASC_KEY_ID env var is required}"
|
|
358
358
|
: "${ASC_ISSUER_ID:?ASC_ISSUER_ID env var is required}"
|
|
359
359
|
: "${ASC_KEY_P8_PATH:?ASC_KEY_P8_PATH env var is required (set by read_config.py)}"
|
|
@@ -364,9 +364,6 @@ runs:
|
|
|
364
364
|
mkdir -p "$RUNNER_TEMP/asc"
|
|
365
365
|
cp "$ASC_KEY_P8_PATH" "$RUNNER_TEMP/asc/AuthKey.p8"
|
|
366
366
|
chmod 600 "$RUNNER_TEMP/asc/AuthKey.p8"
|
|
367
|
-
mkdir -p ~/.appstoreconnect/private_keys
|
|
368
|
-
cp "$RUNNER_TEMP/asc/AuthKey.p8" \
|
|
369
|
-
~/.appstoreconnect/private_keys/AuthKey_${ASC_KEY_ID}.p8
|
|
370
367
|
echo "ASC_KEY_PATH=$RUNNER_TEMP/asc/AuthKey.p8" >> "$GITHUB_ENV"
|
|
371
368
|
|
|
372
369
|
- name: Resolve marketing version (project source of truth)
|
|
@@ -696,6 +693,16 @@ runs:
|
|
|
696
693
|
|| /usr/libexec/PlistBuddy -c "Add :ITSAppUsesNonExemptEncryption bool $VALUE" "$INFOPLIST_PATH"
|
|
697
694
|
echo "Set ITSAppUsesNonExemptEncryption=$VALUE in $INFOPLIST_PATH"
|
|
698
695
|
|
|
696
|
+
- name: Defer native Firebase upload phases to the proxied console
|
|
697
|
+
if: ${{ inputs.archive == 'true' }}
|
|
698
|
+
shell: bash
|
|
699
|
+
env:
|
|
700
|
+
PROJECT: ${{ inputs.project || env.CFG_PROJECT }}
|
|
701
|
+
WORKSPACE: ${{ inputs.workspace || env.CFG_WORKSPACE }}
|
|
702
|
+
run: |
|
|
703
|
+
python3 "$SWIFT_APP_ACTION/scripts/prepare_crashlytics_build.py" \
|
|
704
|
+
--search-root "$(dirname "${WORKSPACE:-$PROJECT}")"
|
|
705
|
+
|
|
699
706
|
- name: Archive
|
|
700
707
|
if: ${{ inputs.archive == 'true' }}
|
|
701
708
|
shell: bash
|
|
@@ -790,47 +797,46 @@ runs:
|
|
|
790
797
|
# set (deploy.yml passes the FIREBASE_APP_ID repository variable) every
|
|
791
798
|
# dSYM in the archive is uploaded here as well, with the upload-symbols
|
|
792
799
|
# tool from the FirebaseCrashlytics pod, so a missing or broken phase
|
|
793
|
-
# cannot
|
|
794
|
-
#
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
if: ${{ inputs.archive == 'true' && inputs.upload == 'true' && inputs.firebase-app-id != '' }}
|
|
800
|
+
# cannot lose the exact build's symbols. The autonomous session completes
|
|
801
|
+
# the upload through the proxied Firebase console from this typed artifact.
|
|
802
|
+
- name: Retain dSYMs for the proxied Firebase console
|
|
803
|
+
if: ${{ inputs.archive == 'true' && inputs.upload == 'true' }}
|
|
798
804
|
shell: bash
|
|
799
805
|
env:
|
|
800
806
|
FIREBASE_APP_ID: ${{ inputs.firebase-app-id }}
|
|
801
807
|
PROJECT: ${{ inputs.project || env.CFG_PROJECT }}
|
|
802
808
|
WORKSPACE: ${{ inputs.workspace || env.CFG_WORKSPACE }}
|
|
803
809
|
run: |
|
|
804
|
-
python3 "$SWIFT_APP_ACTION/scripts/
|
|
810
|
+
python3 "$SWIFT_APP_ACTION/scripts/prepare_crashlytics_dsyms.py" \
|
|
805
811
|
--archive "$RUNNER_TEMP/app.xcarchive" \
|
|
806
|
-
--search-root "$(dirname "${WORKSPACE:-$PROJECT}")"
|
|
812
|
+
--search-root "$(dirname "${WORKSPACE:-$PROJECT}")" \
|
|
813
|
+
--output "$RUNNER_TEMP/firebase-symbols"
|
|
814
|
+
|
|
815
|
+
- name: Preserve the Firebase console upload artifact
|
|
816
|
+
if: ${{ always() && inputs.archive == 'true' }}
|
|
817
|
+
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
|
818
|
+
with:
|
|
819
|
+
name: ios-crashlytics-symbols-${{ github.run_id }}-${{ github.run_attempt }}
|
|
820
|
+
path: ${{ runner.temp }}/firebase-symbols/
|
|
821
|
+
if-no-files-found: ignore
|
|
807
822
|
|
|
808
823
|
- name: Upload to TestFlight
|
|
809
824
|
if: ${{ inputs.archive == 'true' && inputs.upload == 'true' }}
|
|
810
825
|
shell: bash
|
|
826
|
+
env:
|
|
827
|
+
APP_STORE_APPLE_ID: ${{ inputs.app-store-apple-id || env.CFG_APP_STORE_APPLE_ID }}
|
|
811
828
|
run: |
|
|
812
829
|
IPA=$(ls "$RUNNER_TEMP"/export/*.ipa | head -1)
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
set -e
|
|
824
|
-
# altool occasionally exits 0 even on failure; verify the final outcome marker.
|
|
825
|
-
# Apple's altool prints transient "ERROR:" lines for retried network blips
|
|
826
|
-
# before emitting "UPLOAD SUCCEEDED" — don't false-positive on those.
|
|
827
|
-
if [ "$ALTOOL_RC" -ne 0 ] \
|
|
828
|
-
|| grep -qE '^Failed to upload|ERROR ITMS-' "$LOG" \
|
|
829
|
-
|| ! grep -qE 'UPLOAD SUCCEEDED|No errors uploading archive' "$LOG"; then
|
|
830
|
-
echo "altool reported an upload failure (rc=$ALTOOL_RC)"
|
|
831
|
-
exit 1
|
|
832
|
-
fi
|
|
833
|
-
echo "Upload succeeded"
|
|
830
|
+
python3 "$SWIFT_APP_ACTION/scripts/upload_build.py" "$IPA" \
|
|
831
|
+
--report "$RUNNER_TEMP/apple-build-upload.json"
|
|
832
|
+
|
|
833
|
+
- name: Retain Apple upload receipt
|
|
834
|
+
if: ${{ always() && inputs.archive == 'true' && inputs.upload == 'true' }}
|
|
835
|
+
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
|
836
|
+
with:
|
|
837
|
+
name: apple-upload-${{ github.run_id }}-${{ github.run_attempt }}
|
|
838
|
+
path: ${{ runner.temp }}/apple-build-upload.json
|
|
839
|
+
if-no-files-found: ignore
|
|
834
840
|
|
|
835
841
|
# Both App Store metadata writers are skipped while an App Store version
|
|
836
842
|
# is locked (STORE_VERSION_LOCKED, the TESTFLIGHT_ONLY decision): there is
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Explicit account-pinned transport for CI; missing proxy never means direct egress."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from urllib.parse import urlsplit
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def proxies() -> dict[str, str]:
|
|
11
|
+
value = os.environ.get("APPLE_STORE_PROXY_URL", "").strip()
|
|
12
|
+
try:
|
|
13
|
+
parsed = urlsplit(value)
|
|
14
|
+
valid = parsed.scheme in {"http", "https"} and parsed.hostname and parsed.port
|
|
15
|
+
except ValueError:
|
|
16
|
+
valid = False
|
|
17
|
+
if not valid:
|
|
18
|
+
raise SystemExit("APPLE_STORE_PROXY_URL must contain the account-pinned proxy; direct requests are refused")
|
|
19
|
+
return {"http": value, "https": value}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def request(method: str, url: str, **kwargs) -> requests.Response:
|
|
23
|
+
# An explicit map also wins over NO_PROXY and any unrelated runner proxy.
|
|
24
|
+
try:
|
|
25
|
+
return requests.request(method, url, proxies=proxies(), **kwargs)
|
|
26
|
+
except requests.RequestException:
|
|
27
|
+
# Proxy errors can embed credentials and signed upload URLs.
|
|
28
|
+
raise RuntimeError("Apple request failed through the configured proxy") from None
|
|
@@ -16,6 +16,8 @@ from typing import Any
|
|
|
16
16
|
import jwt
|
|
17
17
|
import requests
|
|
18
18
|
|
|
19
|
+
from apple_store_proxy import proxies
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
ASC_BASE = "https://api.appstoreconnect.apple.com/v1"
|
|
21
23
|
|
|
@@ -162,11 +164,11 @@ def _retry_network_error(
|
|
|
162
164
|
if attempt >= total - 1:
|
|
163
165
|
raise SystemExit(
|
|
164
166
|
f"ASC {method} {path} network error after {total} "
|
|
165
|
-
|
|
167
|
+
"attempts through the configured proxy"
|
|
166
168
|
)
|
|
167
169
|
delay = backoffs[attempt]
|
|
168
170
|
print(
|
|
169
|
-
f"ASC {method} {path} network error
|
|
171
|
+
f"ASC {method} {path} network error through the configured proxy; "
|
|
170
172
|
f"retrying in {delay}s ({attempt + 1}/{total - 1})",
|
|
171
173
|
file=sys.stderr,
|
|
172
174
|
)
|
|
@@ -222,7 +224,7 @@ def request(
|
|
|
222
224
|
try:
|
|
223
225
|
resp = requests.request(
|
|
224
226
|
method, url, headers=headers, params=params,
|
|
225
|
-
json=json_body, timeout=timeout,
|
|
227
|
+
json=json_body, timeout=timeout, proxies=proxies(),
|
|
226
228
|
)
|
|
227
229
|
except requests.RequestException as exc:
|
|
228
230
|
_retry_network_error(method, path, attempt, total, backoffs, exc)
|
|
@@ -1,28 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
--split-debug-info builds (the .symbols files those flags write are ELF
|
|
8
|
-
debug info that the Firebase CLI's symbol generators reject). The run-script
|
|
9
|
-
phase `flutterfire configure` adds already uploads that one dSYM during the
|
|
10
|
-
archive; this script uploads all of them (App.framework, the app binary,
|
|
11
|
-
plugin frameworks) with the `upload-symbols` tool that ships in the
|
|
12
|
-
FirebaseCrashlytics pod, so a missing or broken phase cannot leave a build
|
|
13
|
-
unreadable, and native frames get their symbols too.
|
|
14
|
-
|
|
15
|
-
upload-symbols -ai <FIREBASE_APP_ID> -p ios -- <archive>/dSYMs
|
|
16
|
-
|
|
17
|
-
No credentials: the tool authenticates by app id alone.
|
|
18
|
-
|
|
19
|
-
Gating: FIREBASE_APP_ID holds the app's Firebase App ID(s),
|
|
20
|
-
`1:<project number>:<platform>:<hash>`, comma- or whitespace-separated when an
|
|
21
|
-
app ships both platforms; the iOS one is used and its absence is a notice, not
|
|
22
|
-
a failure (the run-script phase is the documented default). With an iOS id
|
|
23
|
-
present, a missing tool or a failed upload fails the step — the action runs it
|
|
24
|
-
before the TestFlight upload, so a deploy never goes out with crashes nobody
|
|
25
|
-
can read.
|
|
2
|
+
"""Shared Crashlytics app/tool discovery and the retired native uploader interface.
|
|
3
|
+
|
|
4
|
+
CI uses prepare_crashlytics_dsyms.py to retain an archive and a typed console-upload
|
|
5
|
+
receipt. Native upload-symbols uses NSURLSession without the task proxy environment,
|
|
6
|
+
so executing it from this legacy interface is refused. No direct fallback is allowed.
|
|
26
7
|
"""
|
|
27
8
|
|
|
28
9
|
from __future__ import annotations
|
|
@@ -104,11 +85,8 @@ def upload_command(tool: Path, app_id: str, dsyms_dir: Path) -> list[str]:
|
|
|
104
85
|
|
|
105
86
|
|
|
106
87
|
def _default_run(cmd: list[str]) -> int:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
except OSError as exc:
|
|
110
|
-
print(f"::error::could not run {cmd[0]}: {exc}")
|
|
111
|
-
return 126
|
|
88
|
+
raise SystemExit("Native Crashlytics upload is refused; use prepare_crashlytics_dsyms.py "
|
|
89
|
+
"and complete the upload through the account-proxied console")
|
|
112
90
|
|
|
113
91
|
|
|
114
92
|
def main(argv: list[str] | None = None, environ: dict[str, str] | None = None, run=None,
|
|
@@ -134,10 +112,10 @@ def main(argv: list[str] | None = None, environ: dict[str, str] | None = None, r
|
|
|
134
112
|
if ids:
|
|
135
113
|
print(
|
|
136
114
|
"::notice::FIREBASE_APP_ID names no iOS app (1:<project number>:ios:<hash>); "
|
|
137
|
-
"
|
|
115
|
+
"native upload is disabled; use the proxied console delivery."
|
|
138
116
|
)
|
|
139
117
|
else:
|
|
140
|
-
print("FIREBASE_APP_ID unset;
|
|
118
|
+
print("FIREBASE_APP_ID unset; native upload is disabled; configure proxied console delivery.")
|
|
141
119
|
return 0
|
|
142
120
|
|
|
143
121
|
dsyms_dir = args.archive / "dSYMs"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Defer dedicated native Crashlytics upload phases to the proxied console delivery."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import plistlib
|
|
9
|
+
import re
|
|
10
|
+
import shlex
|
|
11
|
+
import subprocess
|
|
12
|
+
|
|
13
|
+
MARKER = "echo 'Crashlytics dSYMs are retained by CI for the account-proxied console upload'"
|
|
14
|
+
UPLOADS = ("upload-crashlytics-symbols", "upload-symbols", "Crashlytics/run")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def dedicated(script: str) -> bool:
|
|
18
|
+
lines = [line.strip() for line in script.replace("\\\n", " ").splitlines()
|
|
19
|
+
if line.strip() and not line.lstrip().startswith("#")]
|
|
20
|
+
commands = [line for line in lines if not re.fullmatch(r'(?:export )?PATH=[^;&|<>`]+', line)]
|
|
21
|
+
if len(commands) != 1 or any(token in script for token in ("$(", "`")):
|
|
22
|
+
return False
|
|
23
|
+
lexer = shlex.shlex(commands[0], posix=True, punctuation_chars=";&|<>")
|
|
24
|
+
lexer.whitespace_split = True
|
|
25
|
+
tokens = list(lexer)
|
|
26
|
+
if not tokens or any(set(token) <= set(";&|<>") for token in tokens):
|
|
27
|
+
return False
|
|
28
|
+
return (tokens[0].endswith(("upload-symbols", "Crashlytics/run"))
|
|
29
|
+
or (tokens[0] == "flutterfire" and tokens[1:2] == ["upload-crashlytics-symbols"]))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def rewrite(document: dict) -> list[str]:
|
|
33
|
+
changed = []
|
|
34
|
+
for identity, phase in document.get("objects", {}).items():
|
|
35
|
+
if phase.get("isa") != "PBXShellScriptBuildPhase":
|
|
36
|
+
continue
|
|
37
|
+
script = phase.get("shellScript", "")
|
|
38
|
+
active = "\n".join(line for line in script.splitlines() if not line.lstrip().startswith("#"))
|
|
39
|
+
if not any(token in active for token in UPLOADS):
|
|
40
|
+
continue
|
|
41
|
+
if "crashlytics" not in str(phase.get("name", "")).lower() or not dedicated(script):
|
|
42
|
+
raise ValueError("Crashlytics upload is in a mixed build phase; separate it before archiving")
|
|
43
|
+
phase["shellScript"] = MARKER
|
|
44
|
+
changed.append(identity)
|
|
45
|
+
return changed
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def prepare(root: Path) -> int:
|
|
49
|
+
root = root.resolve()
|
|
50
|
+
projects = sorted(root.rglob("project.pbxproj"))
|
|
51
|
+
prepared = []
|
|
52
|
+
for project in projects:
|
|
53
|
+
if project.is_symlink() or root not in project.resolve().parents:
|
|
54
|
+
raise ValueError("Xcode project leaves the CI workspace")
|
|
55
|
+
raw = subprocess.check_output(["plutil", "-convert", "json", "-o", "-", str(project)], timeout=30)
|
|
56
|
+
document = json.loads(raw)
|
|
57
|
+
changed = rewrite(document)
|
|
58
|
+
if changed:
|
|
59
|
+
prepared.append((project, document, changed))
|
|
60
|
+
# Validate every project before changing one; a mixed phase cannot produce
|
|
61
|
+
# a partially rewritten workspace that a caller accidentally archives.
|
|
62
|
+
for project, document, changed in prepared:
|
|
63
|
+
temporary = project.with_suffix(".proxy-new")
|
|
64
|
+
try:
|
|
65
|
+
temporary.write_bytes(plistlib.dumps(document, sort_keys=False))
|
|
66
|
+
temporary.replace(project)
|
|
67
|
+
finally:
|
|
68
|
+
temporary.unlink(missing_ok=True)
|
|
69
|
+
print(f"Deferred {len(changed)} Crashlytics upload phase(s) in {project.relative_to(root)}")
|
|
70
|
+
return sum(len(changed) for _, _, changed in prepared)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
75
|
+
parser.add_argument("--search-root", required=True, type=Path)
|
|
76
|
+
args = parser.parse_args()
|
|
77
|
+
prepare(args.search_root)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Retain iOS dSYMs and a typed receipt for automatic upload through the proxied console."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import subprocess
|
|
11
|
+
import zipfile
|
|
12
|
+
|
|
13
|
+
from crashlytics_dsyms import find_upload_symbols, select_app_id
|
|
14
|
+
|
|
15
|
+
SCHEMA = "gowalk-cicd/firebase-symbols-pending.v1"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def prepare(archive: Path, output: Path, app_id: str) -> dict:
|
|
19
|
+
root = (archive / "dSYMs").resolve()
|
|
20
|
+
bundles = sorted(root.glob("*.dSYM"))
|
|
21
|
+
files = sorted(path for bundle in bundles for path in bundle.rglob("*") if path.is_file())
|
|
22
|
+
if not files or any(path.is_symlink() or root not in path.resolve().parents for path in files):
|
|
23
|
+
raise ValueError("Archive must contain dSYM files owned by this build")
|
|
24
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
target = output / "ios-dsyms.zip"
|
|
26
|
+
with zipfile.ZipFile(target, "w", compression=zipfile.ZIP_DEFLATED) as zipped:
|
|
27
|
+
for path in files:
|
|
28
|
+
zipped.write(path, path.relative_to(root))
|
|
29
|
+
with target.open("rb") as stream:
|
|
30
|
+
digest = hashlib.file_digest(stream, "sha256").hexdigest()
|
|
31
|
+
uuids = subprocess.check_output(["dwarfdump", "--uuid", *(str(bundle) for bundle in bundles)],
|
|
32
|
+
text=True, timeout=60).splitlines()
|
|
33
|
+
artifact = f"ios-crashlytics-symbols-{os.environ['GITHUB_RUN_ID']}-{os.environ['GITHUB_RUN_ATTEMPT']}"
|
|
34
|
+
receipt = {"schema": SCHEMA, "platform": "ios", "app_id": app_id,
|
|
35
|
+
"source_sha": os.environ["GITHUB_SHA"], "artifact": artifact,
|
|
36
|
+
"file": target.name, "sha256": digest, "status": "pending_console_upload"}
|
|
37
|
+
(output / "firebase-symbols.json").write_text(json.dumps({**receipt, "uuids": uuids}, indent=2))
|
|
38
|
+
return receipt
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main() -> None:
|
|
42
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
43
|
+
parser.add_argument("--archive", required=True, type=Path)
|
|
44
|
+
parser.add_argument("--search-root", required=True, type=Path)
|
|
45
|
+
parser.add_argument("--output", required=True, type=Path)
|
|
46
|
+
args = parser.parse_args()
|
|
47
|
+
app_id = select_app_id(os.environ.get("FIREBASE_APP_ID", ""), "ios")
|
|
48
|
+
tool = find_upload_symbols(args.search_root)
|
|
49
|
+
if not tool:
|
|
50
|
+
print("No Crashlytics SDK upload tool found; no Firebase symbol delivery is required.")
|
|
51
|
+
return
|
|
52
|
+
if not app_id:
|
|
53
|
+
raise SystemExit("The app embeds Crashlytics but FIREBASE_APP_ID names no iOS app")
|
|
54
|
+
receipt = prepare(args.archive, args.output, app_id)
|
|
55
|
+
print("::notice title=firebase_symbols_pending::" + json.dumps(receipt, separators=(",", ":")))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
main()
|
|
@@ -47,7 +47,7 @@ class TestActionStepOrder(unittest.TestCase):
|
|
|
47
47
|
order["Commit + push refreshed app-ci"])
|
|
48
48
|
# ... and before every other step that talks to a store or signs.
|
|
49
49
|
for later in ("Resolve credentials + auto-detect Xcode project",
|
|
50
|
-
"Stage ASC API key for
|
|
50
|
+
"Stage ASC API key for authenticated REST uploads",
|
|
51
51
|
"Prepare signing (cert + keychain + per-target profiles + pbxproj)",
|
|
52
52
|
"Archive"):
|
|
53
53
|
self.assertLess(order["Commit + push refreshed app-ci"], order[later], later)
|