gowalk-cicd 1.0.51 → 1.0.53
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 +17 -0
- package/README.md +68 -6
- package/action/.daemux-version +1 -1
- package/action/action.yml +130 -63
- package/action/scripts/keychain.py +33 -3
- package/action/scripts/manage_marketing_version.py +84 -13
- package/action/scripts/test_action_step_order.py +83 -0
- package/action/scripts/test_keychain.py +145 -0
- package/action/scripts/test_manage_marketing_version_autoroll.py +33 -24
- package/action/scripts/test_manage_marketing_version_match.py +30 -24
- package/action/scripts/test_mmv_decide_create.py +17 -11
- package/action/scripts/test_mmv_testflight_only.py +147 -0
- package/action/scripts/test_testflight_version_slot_wiring.py +25 -0
- package/android-action/.daemux-version +1 -1
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
- package/templates/deploy.yml +12 -5
package/CLAUDE.md
CHANGED
|
@@ -178,6 +178,23 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
178
178
|
pinned exactly because the Android job holds the keystore and the Play
|
|
179
179
|
service account; the CLI runs from a temp cwd so its `.crashlytics/` debris
|
|
180
180
|
never lands in the checkout.
|
|
181
|
+
- The iOS action reads every script and prompt from the snapshot it takes of
|
|
182
|
+
itself in its first step (`$SWIFT_APP_ACTION`, under `RUNNER_TEMP`), never
|
|
183
|
+
from `${{ github.action_path }}` after that step. The plugin self-update
|
|
184
|
+
(`autoupdate_check.sh`) runs right after the snapshot and commits on its own,
|
|
185
|
+
before the version/signing/archive steps, so a red deploy still receives the
|
|
186
|
+
release that fixes it; the snapshot is what keeps that run on one version.
|
|
187
|
+
`test_action_step_order.py` pins both. A new script reference in
|
|
188
|
+
`action/action.yml` therefore uses `"$SWIFT_APP_ACTION/scripts/..."`.
|
|
189
|
+
- Two situations end a green job with work for the panel, and each is a typed
|
|
190
|
+
check-run annotation (`::notice title=<name>::<one-line JSON with a versioned
|
|
191
|
+
schema>`), never prose in a `::warning::`: `play_review_pending`
|
|
192
|
+
(`templates/deploy.yml`, Play refused auto-submit and the bundle landed
|
|
193
|
+
unsubmitted) and `store_version_locked` (`action/action.yml`, the
|
|
194
|
+
`TESTFLIGHT_ONLY` decision of `manage_marketing_version.py`: an App Store
|
|
195
|
+
version is under review, so the build shipped to TestFlight under that
|
|
196
|
+
version and metadata was skipped). The README table lists the schemas; add a
|
|
197
|
+
field by bumping the version.
|
|
181
198
|
- The JDK is chosen by `scripts/select_jdk.py`, called from the workflow before
|
|
182
199
|
`setup-java`. The `JAVA_VERSION` repo variable always wins; otherwise Flutter
|
|
183
200
|
gets 17 and native Gradle gets 21, except that a positively-detected Kotlin
|
package/README.md
CHANGED
|
@@ -217,13 +217,72 @@ sent for review from the Google Play Console UI.
|
|
|
217
217
|
|
|
218
218
|
That fails the whole upload, so the bundle you just built never reaches Play at
|
|
219
219
|
all. The retry re-uploads with `changesNotSentForReview`, which lands the bundle
|
|
220
|
-
and leaves it waiting, then
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
220
|
+
and leaves it waiting, then records that fact as a typed check-run annotation on
|
|
221
|
+
the job (see [Check-run annotations](#check-run-annotations-the-panel-reads)):
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
::notice title=play_review_pending::{"schema":"gowalk-cicd/play-review-pending.v1",
|
|
225
|
+
"package":"<applicationId>","track":"<track>","version_code":"<versionCode>"}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
There is no API for the **Send changes for review** press — Google reserves it
|
|
229
|
+
for the Play Console — so the panel that merged the commit reads the annotation
|
|
230
|
+
and opens its own Play console step, which promotes the uploaded version code to
|
|
231
|
+
the track Google named and presses it. Nobody is asked to notice a warning.
|
|
224
232
|
|
|
225
233
|
In the ordinary case the first upload submits normally and the retry is skipped.
|
|
226
234
|
|
|
235
|
+
### When an App Store version is in review
|
|
236
|
+
|
|
237
|
+
While any App Store version of the app is `WAITING_FOR_REVIEW`, `IN_REVIEW` or
|
|
238
|
+
`PENDING_DEVELOPER_RELEASE`, App Store Connect refuses to create or edit **any**
|
|
239
|
+
version — every `POST /appStoreVersions` answers
|
|
240
|
+
`409 ENTITY_ERROR.RELATIONSHIP.INVALID` ("You cannot create a new version of the
|
|
241
|
+
App in the current state"), whatever version string is asked for. The version
|
|
242
|
+
step used to see only rows at the project's own `MARKETING_VERSION`, so a sibling
|
|
243
|
+
in review (1.6.3 under review, project at 1.6.4) went unnoticed, CREATE was
|
|
244
|
+
attempted and the deploy failed on every push until the review ended.
|
|
245
|
+
|
|
246
|
+
The decision is now `TESTFLIGHT_ONLY`: the build is stamped with the **locked**
|
|
247
|
+
version string (so it joins that version's TestFlight train instead of opening a
|
|
248
|
+
new marketing-version train), the build number still advances, the archive is
|
|
249
|
+
uploaded to TestFlight, and the App Store metadata steps are skipped. The project
|
|
250
|
+
file is never touched — `MARKETING_VERSION` stays the source of truth for the next
|
|
251
|
+
release. The job records a typed annotation so the panel knows the build went to
|
|
252
|
+
TestFlight only:
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
::notice title=store_version_locked::{"schema":"gowalk-cicd/store-version-locked.v1",
|
|
256
|
+
"version":"1.6.3","state":"WAITING_FOR_REVIEW","build_number":"239"}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Check-run annotations the panel reads
|
|
260
|
+
|
|
261
|
+
Two situations end a green job with work left for something other than this
|
|
262
|
+
workflow. Each is recorded as a GitHub check-run annotation whose **title** names
|
|
263
|
+
the situation and whose **message** is one line of JSON carrying a versioned
|
|
264
|
+
`schema`. The panel that merged the commit reads them through the check-runs API
|
|
265
|
+
— never by parsing log text — so the title and schema are a contract:
|
|
266
|
+
|
|
267
|
+
| title | schema | fields |
|
|
268
|
+
| --- | --- | --- |
|
|
269
|
+
| `play_review_pending` | `gowalk-cicd/play-review-pending.v1` | `package`, `track`, `version_code` |
|
|
270
|
+
| `store_version_locked` | `gowalk-cicd/store-version-locked.v1` | `version`, `state`, `build_number` |
|
|
271
|
+
|
|
272
|
+
Add a field by bumping the schema version; never change the meaning of an
|
|
273
|
+
existing one.
|
|
274
|
+
|
|
275
|
+
### The plugin self-update runs first
|
|
276
|
+
|
|
277
|
+
The vendored iOS action checks npm for a newer `gowalk-cicd` at the **start** of
|
|
278
|
+
every default-branch run and commits the refreshed action directories in their
|
|
279
|
+
own commit, before the version, signing and archive steps. A deploy that is red
|
|
280
|
+
in one of those steps therefore still picks up the plugin release that fixes it,
|
|
281
|
+
on the very run that fails, and the next push runs the fixed plugin. The run that
|
|
282
|
+
performed the update keeps executing the version it started with: the action
|
|
283
|
+
snapshots itself to `$RUNNER_TEMP` before anything mutates and reads every script
|
|
284
|
+
and prompt from that copy.
|
|
285
|
+
|
|
227
286
|
### Google Play upload retry
|
|
228
287
|
|
|
229
288
|
A Play edit is a short-lived server-side object, and an AAB upload that runs
|
|
@@ -604,8 +663,11 @@ Feature-branch runs installed from the standard workflow still archive and
|
|
|
604
663
|
upload to TestFlight, but pass `manage-app-store-version: 'false'`. This keeps
|
|
605
664
|
branch validation independent of the editable App Store release slot, so a
|
|
606
665
|
build can upload while the current release is locked in review. Default-branch
|
|
607
|
-
runs retain version creation and metadata automation
|
|
608
|
-
|
|
666
|
+
runs retain version creation and metadata automation — and while a version is
|
|
667
|
+
locked in review they take the same TestFlight-only path on their own (see
|
|
668
|
+
[When an App Store version is in review](#when-an-app-store-version-is-in-review)).
|
|
669
|
+
Direct action callers keep the historical behavior because the new input
|
|
670
|
+
defaults to `true`.
|
|
609
671
|
|
|
610
672
|
### App ID capabilities
|
|
611
673
|
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.53
|
package/action/action.yml
CHANGED
|
@@ -187,6 +187,79 @@ inputs:
|
|
|
187
187
|
runs:
|
|
188
188
|
using: composite
|
|
189
189
|
steps:
|
|
190
|
+
# Every later step reads this action's scripts and prompts from the
|
|
191
|
+
# snapshot, never from ${action_path}. The autoupdate below re-vendors
|
|
192
|
+
# .github/actions/swift-app/ — the same directory as ${action_path} for
|
|
193
|
+
# local-action consumers — while this run is still executing the
|
|
194
|
+
# action.yml the runner loaded at job start. Reading scripts from the
|
|
195
|
+
# refreshed tree under the old step definitions is a mid-run version
|
|
196
|
+
# mix; one copy taken before anything mutates keeps the whole run on
|
|
197
|
+
# one version of the plugin.
|
|
198
|
+
- name: Snapshot this action for the whole run
|
|
199
|
+
shell: bash
|
|
200
|
+
run: |
|
|
201
|
+
set -euo pipefail
|
|
202
|
+
rm -rf "$RUNNER_TEMP/swift-app-action"
|
|
203
|
+
mkdir -p "$RUNNER_TEMP/swift-app-action"
|
|
204
|
+
cp -R "${{ github.action_path }}/." "$RUNNER_TEMP/swift-app-action/"
|
|
205
|
+
echo "SWIFT_APP_ACTION=$RUNNER_TEMP/swift-app-action" >> "$GITHUB_ENV"
|
|
206
|
+
|
|
207
|
+
# The plugin self-update runs BEFORE the version, signing and archive
|
|
208
|
+
# steps and lands in its own commit. It used to run after them, so a
|
|
209
|
+
# deploy that was red in the version step (App Store Connect refusing a
|
|
210
|
+
# new version while one is in review, GoVolt run 33743284641) never
|
|
211
|
+
# reached the self-update, and the plugin release that fixed exactly
|
|
212
|
+
# that failure could not arrive in the repository that needed it.
|
|
213
|
+
# Committing here rather than with the cert refresh below is the same
|
|
214
|
+
# point: a red step later in this run must not take the update with it.
|
|
215
|
+
- name: Stage bot-side scripts to runner temp
|
|
216
|
+
if: ${{ (inputs.archive == 'true' || inputs.auto-update == 'true')
|
|
217
|
+
&& github.event_name == 'push'
|
|
218
|
+
&& github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
|
219
|
+
shell: bash
|
|
220
|
+
run: |
|
|
221
|
+
set -euo pipefail
|
|
222
|
+
mkdir -p "$RUNNER_TEMP/swift-app-bot-scripts"
|
|
223
|
+
cp "$SWIFT_APP_ACTION/scripts/autoupdate_check.sh" \
|
|
224
|
+
"$RUNNER_TEMP/swift-app-bot-scripts/"
|
|
225
|
+
cp "$SWIFT_APP_ACTION/scripts/commit_bot_changes.sh" \
|
|
226
|
+
"$RUNNER_TEMP/swift-app-bot-scripts/"
|
|
227
|
+
chmod +x "$RUNNER_TEMP/swift-app-bot-scripts/"*.sh
|
|
228
|
+
echo "BOT_SCRIPTS_DIR=$RUNNER_TEMP/swift-app-bot-scripts" >> "$GITHUB_ENV"
|
|
229
|
+
|
|
230
|
+
- name: Check for app-ci update
|
|
231
|
+
# Default branch + push event only. `auto-update` is on by default;
|
|
232
|
+
# consumers can pin via `false`. Runs from $BOT_SCRIPTS_DIR
|
|
233
|
+
# (RUNNER_TEMP) so its own `npx` overwriting ${action_path} cannot
|
|
234
|
+
# swap the script under us.
|
|
235
|
+
if: ${{ inputs.auto-update == 'true'
|
|
236
|
+
&& github.event_name == 'push'
|
|
237
|
+
&& github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
|
238
|
+
shell: bash
|
|
239
|
+
working-directory: ${{ github.workspace }}
|
|
240
|
+
run: bash "$BOT_SCRIPTS_DIR/autoupdate_check.sh"
|
|
241
|
+
|
|
242
|
+
- name: Commit + push refreshed app-ci
|
|
243
|
+
# Its own commit, immediately: the refreshed action files must reach
|
|
244
|
+
# the default branch even when the version or signing steps of this
|
|
245
|
+
# very run fail. deploy.yml is deliberately not staged (GITHUB_TOKEN
|
|
246
|
+
# cannot push workflow files). No-op when the plugin was current.
|
|
247
|
+
if: ${{ inputs.auto-update == 'true'
|
|
248
|
+
&& github.event_name == 'push'
|
|
249
|
+
&& github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
|
250
|
+
shell: bash
|
|
251
|
+
working-directory: ${{ github.workspace }}
|
|
252
|
+
env:
|
|
253
|
+
GITHUB_REF_NAME: ${{ github.ref_name }}
|
|
254
|
+
run: |
|
|
255
|
+
set -euo pipefail
|
|
256
|
+
# A shallow checkout (fetch-depth 1 for direct action callers) must
|
|
257
|
+
# be able to rebase on a moved default branch inside the push retry.
|
|
258
|
+
git fetch --unshallow 2>/dev/null \
|
|
259
|
+
|| git fetch --depth=2147483647 2>/dev/null \
|
|
260
|
+
|| true
|
|
261
|
+
bash "$BOT_SCRIPTS_DIR/commit_bot_changes.sh"
|
|
262
|
+
|
|
190
263
|
- name: Resolve credentials + auto-detect Xcode project
|
|
191
264
|
shell: bash
|
|
192
265
|
env:
|
|
@@ -217,7 +290,7 @@ runs:
|
|
|
217
290
|
echo "Installing xcodegen (project.yml present, binary missing)…"
|
|
218
291
|
brew install xcodegen >/dev/null 2>&1 || HOMEBREW_NO_AUTO_UPDATE=1 brew install xcodegen
|
|
219
292
|
fi
|
|
220
|
-
python3 "$
|
|
293
|
+
python3 "$SWIFT_APP_ACTION/scripts/read_config.py"
|
|
221
294
|
|
|
222
295
|
- name: Select latest Xcode
|
|
223
296
|
shell: bash
|
|
@@ -303,10 +376,12 @@ runs:
|
|
|
303
376
|
CONFIGURATION: ${{ inputs.configuration || env.CFG_CONFIGURATION }}
|
|
304
377
|
run: |
|
|
305
378
|
set -o pipefail
|
|
306
|
-
python3 "$
|
|
379
|
+
python3 "$SWIFT_APP_ACTION/scripts/resolve_marketing_version.py" \
|
|
307
380
|
| tee -a "$GITHUB_ENV"
|
|
308
381
|
|
|
309
382
|
- name: Resolve App Store version slot (REUSE or CREATE)
|
|
383
|
+
# A third answer, TESTFLIGHT_ONLY, is handled inside: see the
|
|
384
|
+
# STORE_VERSION_LOCKED branch of the script.
|
|
310
385
|
if: ${{ inputs.archive == 'true' && inputs.manage-app-store-version == 'true' }}
|
|
311
386
|
shell: bash
|
|
312
387
|
env:
|
|
@@ -332,19 +407,40 @@ runs:
|
|
|
332
407
|
CONFIGURATION: ${{ inputs.configuration || env.CFG_CONFIGURATION }}
|
|
333
408
|
run: |
|
|
334
409
|
set -o pipefail
|
|
335
|
-
BUILD_NUMBER=$(python3 "$
|
|
336
|
-
DECISION_JSON=$(python3 "$
|
|
410
|
+
BUILD_NUMBER=$(python3 "$SWIFT_APP_ACTION/scripts/next_build_number.py")
|
|
411
|
+
DECISION_JSON=$(python3 "$SWIFT_APP_ACTION/scripts/manage_marketing_version.py")
|
|
337
412
|
APP_STORE_VERSION_ID=$(python3 -c \
|
|
338
413
|
"import json,sys; d=json.loads(sys.argv[1]); print(d.get('appStoreVersionId',''))" \
|
|
339
414
|
"$DECISION_JSON")
|
|
415
|
+
DECISION=$(python3 -c \
|
|
416
|
+
"import json,sys; d=json.loads(sys.argv[1]); print(d.get('decision',''))" \
|
|
417
|
+
"$DECISION_JSON")
|
|
340
418
|
# The decision's versionString is authoritative: it reflects any
|
|
341
419
|
# auto-bump that mmv applied to MARKETING_VERSION when the project
|
|
342
|
-
# was below the ASC combined floor
|
|
343
|
-
#
|
|
420
|
+
# was below the ASC combined floor — or, for TESTFLIGHT_ONLY, the
|
|
421
|
+
# App Store version Apple is holding, which is the train this build
|
|
422
|
+
# joins. Re-export so subsequent steps (Stamp Info.plist, Archive)
|
|
423
|
+
# see that value.
|
|
344
424
|
EFFECTIVE_MV=$(python3 -c \
|
|
345
425
|
"import json,sys; d=json.loads(sys.argv[1]); print(d.get('versionString',''))" \
|
|
346
426
|
"$DECISION_JSON")
|
|
347
|
-
if [
|
|
427
|
+
if [ "$DECISION" = "TESTFLIGHT_ONLY" ]; then
|
|
428
|
+
# An App Store version is under review or awaiting the developer's
|
|
429
|
+
# release: App Store Connect refuses to create or edit ANY version
|
|
430
|
+
# (409 ENTITY_ERROR.RELATIONSHIP.INVALID). The build still ships to
|
|
431
|
+
# TestFlight, stamped with the locked version so it lands in that
|
|
432
|
+
# version's train; App Store metadata below is skipped
|
|
433
|
+
# (STORE_VERSION_LOCKED), and the typed check-run annotation tells
|
|
434
|
+
# the panel that merged this commit exactly what happened.
|
|
435
|
+
LOCKED_STATE=$(python3 -c \
|
|
436
|
+
"import json,sys; d=json.loads(sys.argv[1]); print(d.get('state',''))" \
|
|
437
|
+
"$DECISION_JSON")
|
|
438
|
+
echo "::notice::App Store version $EFFECTIVE_MV is $LOCKED_STATE; uploading build $BUILD_NUMBER to TestFlight under $EFFECTIVE_MV and skipping App Store metadata"
|
|
439
|
+
echo "STORE_VERSION_LOCKED=$EFFECTIVE_MV" >> "$GITHUB_ENV"
|
|
440
|
+
echo "::notice title=store_version_locked::{\"schema\":\"gowalk-cicd/store-version-locked.v1\",\"version\":\"$EFFECTIVE_MV\",\"state\":\"$LOCKED_STATE\",\"build_number\":\"$BUILD_NUMBER\"}"
|
|
441
|
+
echo "MARKETING_VERSION=$EFFECTIVE_MV" >> "$GITHUB_ENV"
|
|
442
|
+
MARKETING_VERSION="$EFFECTIVE_MV"
|
|
443
|
+
elif [ -n "$EFFECTIVE_MV" ] && [ "$EFFECTIVE_MV" != "$MARKETING_VERSION" ]; then
|
|
348
444
|
echo "::notice::auto-bump rewrote MARKETING_VERSION $MARKETING_VERSION -> $EFFECTIVE_MV"
|
|
349
445
|
echo "MARKETING_VERSION=$EFFECTIVE_MV" >> "$GITHUB_ENV"
|
|
350
446
|
MARKETING_VERSION="$EFFECTIVE_MV"
|
|
@@ -360,7 +456,7 @@ runs:
|
|
|
360
456
|
APP_STORE_APPLE_ID: ${{ inputs.app-store-apple-id || env.CFG_APP_STORE_APPLE_ID }}
|
|
361
457
|
run: |
|
|
362
458
|
set -o pipefail
|
|
363
|
-
BUILD_NUMBER=$(python3 "$
|
|
459
|
+
BUILD_NUMBER=$(python3 "$SWIFT_APP_ACTION/scripts/next_build_number.py")
|
|
364
460
|
echo "BUILD_NUMBER=$BUILD_NUMBER" >> "$GITHUB_ENV"
|
|
365
461
|
echo "APP_STORE_VERSION_ID=" >> "$GITHUB_ENV"
|
|
366
462
|
echo "Using marketing version $MARKETING_VERSION build $BUILD_NUMBER without an App Store version slot"
|
|
@@ -383,7 +479,7 @@ runs:
|
|
|
383
479
|
CERTIFICATE_CAP_POLICY: ${{ inputs.certificate-cap-policy }}
|
|
384
480
|
run: |
|
|
385
481
|
set +e
|
|
386
|
-
python3 "$
|
|
482
|
+
python3 "$SWIFT_APP_ACTION/scripts/prepare_signing.py"
|
|
387
483
|
signing_exit=$?
|
|
388
484
|
set -e
|
|
389
485
|
echo "SIGNING_PREP_EXIT=$signing_exit" >> "$GITHUB_ENV"
|
|
@@ -408,36 +504,13 @@ runs:
|
|
|
408
504
|
# of any compromised step in the workflow — keep workflow minimal.
|
|
409
505
|
# - .gitignore must NOT block the new creds/ files (see setup doc).
|
|
410
506
|
#
|
|
411
|
-
#
|
|
412
|
-
#
|
|
413
|
-
#
|
|
414
|
-
#
|
|
415
|
-
#
|
|
416
|
-
#
|
|
417
|
-
# (5) re-raise a deferred prepare failure after preserving its cache.
|
|
507
|
+
# The autoupdate half runs FIRST in this action (see the top of the
|
|
508
|
+
# step list) and lands in its own commit, so a fixed plugin reaches a
|
|
509
|
+
# repository whose deploy is failing in the version or signing steps
|
|
510
|
+
# below. What remains here: (1) stage cert files, (2) commit + push
|
|
511
|
+
# whatever is staged, (3) re-raise a deferred prepare failure after
|
|
512
|
+
# preserving its cache.
|
|
418
513
|
|
|
419
|
-
# Stage scripts to RUNNER_TEMP so the autoupdate step's `npx` (which
|
|
420
|
-
# overwrites .github/actions/swift-app/ — the same dir as ${action_path}
|
|
421
|
-
# for local-action consumers) cannot mid-run swap the script bytes
|
|
422
|
-
# under us. action.yml step definitions are already loaded by the
|
|
423
|
-
# runner at job start, but the shell scripts are read fresh per step,
|
|
424
|
-
# so without this stage we'd risk loading commit_bot_changes.sh from
|
|
425
|
-
# the post-`npx` (newer) tree while the surrounding step definitions
|
|
426
|
-
# came from the pre-`npx` (older) action.yml — a mid-run version mix.
|
|
427
|
-
- name: Stage bot-side scripts to runner temp
|
|
428
|
-
if: ${{ (inputs.archive == 'true' || inputs.auto-update == 'true')
|
|
429
|
-
&& github.event_name == 'push'
|
|
430
|
-
&& github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
|
431
|
-
shell: bash
|
|
432
|
-
run: |
|
|
433
|
-
set -euo pipefail
|
|
434
|
-
mkdir -p "$RUNNER_TEMP/swift-app-bot-scripts"
|
|
435
|
-
cp "${{ github.action_path }}/scripts/autoupdate_check.sh" \
|
|
436
|
-
"$RUNNER_TEMP/swift-app-bot-scripts/"
|
|
437
|
-
cp "${{ github.action_path }}/scripts/commit_bot_changes.sh" \
|
|
438
|
-
"$RUNNER_TEMP/swift-app-bot-scripts/"
|
|
439
|
-
chmod +x "$RUNNER_TEMP/swift-app-bot-scripts/"*.sh
|
|
440
|
-
echo "BOT_SCRIPTS_DIR=$RUNNER_TEMP/swift-app-bot-scripts" >> "$GITHUB_ENV"
|
|
441
514
|
|
|
442
515
|
- name: Stage refreshed signing identity
|
|
443
516
|
# Only stage on push to the default branch — PR runs and feature
|
|
@@ -495,25 +568,14 @@ runs:
|
|
|
495
568
|
sed 's/^/ /' "$bump_marker"
|
|
496
569
|
fi
|
|
497
570
|
|
|
498
|
-
- name: Check for app-ci update
|
|
499
|
-
# Same gate as cert staging: default branch + push event only.
|
|
500
|
-
# `auto-update` is on by default; consumers can pin via `false`.
|
|
501
|
-
# Runs from $BOT_SCRIPTS_DIR (RUNNER_TEMP) so its own `npx`
|
|
502
|
-
# overwriting ${action_path} mid-run can't swap the script under us.
|
|
503
|
-
if: ${{ inputs.auto-update == 'true'
|
|
504
|
-
&& github.event_name == 'push'
|
|
505
|
-
&& github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
|
506
|
-
shell: bash
|
|
507
|
-
working-directory: ${{ github.workspace }}
|
|
508
|
-
run: bash "$BOT_SCRIPTS_DIR/autoupdate_check.sh"
|
|
509
|
-
|
|
510
571
|
- name: Commit + push staged bot changes
|
|
511
|
-
#
|
|
512
|
-
#
|
|
572
|
+
# The cert refresh (and any marketing-version auto-bump the version
|
|
573
|
+
# step staged). The autoupdate landed in its own commit at the top of
|
|
574
|
+
# this action; this script is a no-op when nothing is staged, so a
|
|
513
575
|
# PR / feature-branch run that somehow reaches this step does no
|
|
514
576
|
# harm — but we still gate on default-branch push for clarity.
|
|
515
|
-
# Runs from $BOT_SCRIPTS_DIR (RUNNER_TEMP)
|
|
516
|
-
#
|
|
577
|
+
# Runs from $BOT_SCRIPTS_DIR (RUNNER_TEMP): the autoupdate re-vendored
|
|
578
|
+
# ${action_path} earlier in this run and must not swap scripts under us.
|
|
517
579
|
if: ${{ (inputs.archive == 'true' || inputs.auto-update == 'true')
|
|
518
580
|
&& github.event_name == 'push'
|
|
519
581
|
&& github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
|
@@ -735,7 +797,7 @@ runs:
|
|
|
735
797
|
PROJECT: ${{ inputs.project || env.CFG_PROJECT }}
|
|
736
798
|
WORKSPACE: ${{ inputs.workspace || env.CFG_WORKSPACE }}
|
|
737
799
|
run: |
|
|
738
|
-
python3 "$
|
|
800
|
+
python3 "$SWIFT_APP_ACTION/scripts/crashlytics_dsyms.py" \
|
|
739
801
|
--archive "$RUNNER_TEMP/app.xcarchive" \
|
|
740
802
|
--search-root "$(dirname "${WORKSPACE:-$PROJECT}")"
|
|
741
803
|
|
|
@@ -766,18 +828,22 @@ runs:
|
|
|
766
828
|
fi
|
|
767
829
|
echo "Upload succeeded"
|
|
768
830
|
|
|
831
|
+
# Both App Store metadata writers are skipped while an App Store version
|
|
832
|
+
# is locked (STORE_VERSION_LOCKED, the TESTFLIGHT_ONLY decision): there is
|
|
833
|
+
# no editable version id to write to, and each script requires one.
|
|
769
834
|
- name: Detect empty ASC metadata fields
|
|
770
835
|
id: detect_metadata
|
|
771
836
|
if: >-
|
|
772
837
|
${{ inputs.archive == 'true' && inputs.upload == 'true'
|
|
773
|
-
&& inputs.manage-app-store-version == 'true' && inputs.ai-metadata != 'false'
|
|
838
|
+
&& inputs.manage-app-store-version == 'true' && inputs.ai-metadata != 'false'
|
|
839
|
+
&& env.STORE_VERSION_LOCKED == '' }}
|
|
774
840
|
shell: bash
|
|
775
841
|
env:
|
|
776
842
|
APP_STORE_APPLE_ID: ${{ inputs.app-store-apple-id || env.CFG_APP_STORE_APPLE_ID }}
|
|
777
843
|
APP_STORE_VERSION_ID: ${{ env.APP_STORE_VERSION_ID }}
|
|
778
844
|
run: |
|
|
779
845
|
set -o pipefail
|
|
780
|
-
python3 "$
|
|
846
|
+
python3 "$SWIFT_APP_ACTION/scripts/asc_metadata_detector.py" \
|
|
781
847
|
> "$RUNNER_TEMP/asc_empty_metadata.json" || {
|
|
782
848
|
echo "::warning::metadata detector failed; skipping AI step"
|
|
783
849
|
echo "has_empty=false" >> "$GITHUB_OUTPUT"
|
|
@@ -800,7 +866,7 @@ runs:
|
|
|
800
866
|
BUNDLE_ID: ${{ inputs.bundle-id || env.CFG_BUNDLE_ID }}
|
|
801
867
|
SCHEME: ${{ inputs.scheme || env.CFG_SCHEME }}
|
|
802
868
|
run: |
|
|
803
|
-
python3 "$
|
|
869
|
+
python3 "$SWIFT_APP_ACTION/scripts/app_context_scanner.py" \
|
|
804
870
|
> "$RUNNER_TEMP/app_context.txt"
|
|
805
871
|
wc -c "$RUNNER_TEMP/app_context.txt"
|
|
806
872
|
|
|
@@ -834,7 +900,7 @@ runs:
|
|
|
834
900
|
if: ${{ steps.derive_phases.outputs.needs_phase1 == 'true' }}
|
|
835
901
|
shell: bash
|
|
836
902
|
env:
|
|
837
|
-
TEMPLATE_PATH: ${{
|
|
903
|
+
TEMPLATE_PATH: ${{ env.SWIFT_APP_ACTION }}/prompts/generate_descriptions.prompt.yml
|
|
838
904
|
run: |
|
|
839
905
|
# Pre-render via PyYAML so multi-line app_context stays a block scalar
|
|
840
906
|
# and doesn't break ai-inference@v1's naive string substitution.
|
|
@@ -905,7 +971,7 @@ runs:
|
|
|
905
971
|
echo "phase1_ok=false" >> "$GITHUB_OUTPUT"
|
|
906
972
|
exit 0
|
|
907
973
|
fi
|
|
908
|
-
python3 "$
|
|
974
|
+
python3 "$SWIFT_APP_ACTION/scripts/asc_metadata_applier.py" \
|
|
909
975
|
--state "$RUNNER_TEMP/asc_empty_metadata.json" \
|
|
910
976
|
--response "$OUT" \
|
|
911
977
|
--fields-filter description
|
|
@@ -917,7 +983,7 @@ runs:
|
|
|
917
983
|
|| steps.phase1_apply.outputs.phase1_ok == 'true') }}
|
|
918
984
|
shell: bash
|
|
919
985
|
env:
|
|
920
|
-
TEMPLATE_PATH: ${{
|
|
986
|
+
TEMPLATE_PATH: ${{ env.SWIFT_APP_ACTION }}/prompts/generate_dependent_fields.prompt.yml
|
|
921
987
|
run: |
|
|
922
988
|
# Merge existing_fields.{locale}.description with Phase-1 outputs to
|
|
923
989
|
# produce authoritative per-locale description context, then render
|
|
@@ -1010,7 +1076,7 @@ runs:
|
|
|
1010
1076
|
head -c 500 "$OUT" || true
|
|
1011
1077
|
echo
|
|
1012
1078
|
fi
|
|
1013
|
-
python3 "$
|
|
1079
|
+
python3 "$SWIFT_APP_ACTION/scripts/asc_metadata_applier.py" \
|
|
1014
1080
|
--state "$RUNNER_TEMP/asc_empty_metadata.json" \
|
|
1015
1081
|
--response "$OUT" \
|
|
1016
1082
|
--fields-filter name,subtitle,keywords,promotionalText,whatsNew
|
|
@@ -1026,6 +1092,7 @@ runs:
|
|
|
1026
1092
|
- name: Set App Store "What's New"
|
|
1027
1093
|
if: ${{ inputs.archive == 'true' && inputs.upload == 'true'
|
|
1028
1094
|
&& inputs.manage-app-store-version == 'true'
|
|
1095
|
+
&& env.STORE_VERSION_LOCKED == ''
|
|
1029
1096
|
&& (inputs.app-store-whats-new != '' || env.CFG_WHATS_NEW != '') }}
|
|
1030
1097
|
shell: bash
|
|
1031
1098
|
env:
|
|
@@ -1051,4 +1118,4 @@ runs:
|
|
|
1051
1118
|
WHATS_NEW_ONLY_IF_EMPTY=false
|
|
1052
1119
|
fi
|
|
1053
1120
|
export APP_STORE_WHATS_NEW_FILE WHATS_NEW_ONLY_IF_EMPTY
|
|
1054
|
-
python3 "$
|
|
1121
|
+
python3 "$SWIFT_APP_ACTION/scripts/set_app_store_whats_new.py"
|
|
@@ -8,6 +8,16 @@ into it, and prepends it to the user's keychain search list so
|
|
|
8
8
|
keychain is purposely short-lived (default 6h auto-lock) and disposable
|
|
9
9
|
— each CI run re-creates it from scratch.
|
|
10
10
|
|
|
11
|
+
The user's *default* keychain is deliberately never pointed at the
|
|
12
|
+
throwaway keychain: the search list (plus the partition list set at
|
|
13
|
+
import) is what identity resolution actually uses. On a persistent
|
|
14
|
+
runner (a self-hosted Mac), claiming the default meant every credential
|
|
15
|
+
consumer on the host wrote into the throwaway keychain, and once its
|
|
16
|
+
temp dir was reclaimed the dangling default made macOS pop a
|
|
17
|
+
"Keychain ... cannot be found to store" dialog at every credential
|
|
18
|
+
write. Setup instead prunes search-list entries whose backing file is
|
|
19
|
+
gone and repoints a dangling default back at the login keychain.
|
|
20
|
+
|
|
11
21
|
The keychain password (``"ci"``) is intentionally hardcoded: the
|
|
12
22
|
keychain never leaves the runner (it lives in ``$RUNNER_TEMP`` which
|
|
13
23
|
GitHub deletes when the job ends), so encrypting it with a sourced
|
|
@@ -77,15 +87,35 @@ def _prepend_to_user_search_list(keychain_path: str) -> None:
|
|
|
77
87
|
for line in existing.splitlines()
|
|
78
88
|
if line.strip()
|
|
79
89
|
]
|
|
90
|
+
# Prune entries whose file is gone: dangling throwaway keychains from
|
|
91
|
+
# earlier runs on a persistent Mac would otherwise accumulate forever.
|
|
80
92
|
new_list = [keychain_path] + [
|
|
81
|
-
k for k in existing_list
|
|
93
|
+
k for k in existing_list
|
|
94
|
+
if k != keychain_path and os.path.exists(k)
|
|
82
95
|
]
|
|
83
96
|
subprocess.check_call(
|
|
84
97
|
["security", "list-keychains", "-d", "user", "-s", *new_list]
|
|
85
98
|
)
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
_repair_default_keychain()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _repair_default_keychain() -> None:
|
|
103
|
+
"""Repoint an unset or dangling default keychain at the login keychain.
|
|
104
|
+
|
|
105
|
+
Heals hosts damaged by earlier releases (which made the throwaway CI
|
|
106
|
+
keychain the default) without ever claiming the default for this run.
|
|
107
|
+
A healthy default — whatever it points at — is left alone.
|
|
108
|
+
"""
|
|
109
|
+
login = os.path.expanduser("~/Library/Keychains/login.keychain-db")
|
|
110
|
+
if not os.path.exists(login):
|
|
111
|
+
return
|
|
112
|
+
probe = subprocess.run(
|
|
113
|
+
["security", "default-keychain"], capture_output=True, text=True
|
|
88
114
|
)
|
|
115
|
+
current = probe.stdout.strip().strip('"') if probe.returncode == 0 else ""
|
|
116
|
+
if current and os.path.exists(current):
|
|
117
|
+
return
|
|
118
|
+
subprocess.check_call(["security", "default-keychain", "-s", login])
|
|
89
119
|
|
|
90
120
|
|
|
91
121
|
def setup_keychain(p12_path: Path, p12_pass: str, runner_temp: str) -> str:
|