gowalk-cicd 1.0.78 → 1.0.79

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.
Files changed (35) hide show
  1. package/CLAUDE.md +29 -2
  2. package/README.md +22 -18
  3. package/action/.daemux-version +1 -1
  4. package/action/action.yml +48 -38
  5. package/action/scripts/auto_detect.py +8 -8
  6. package/action/scripts/commit_bot_changes.sh +3 -29
  7. package/action/scripts/native_dependency_guard.py +59 -0
  8. package/action/scripts/native_pods.py +97 -0
  9. package/action/scripts/native_pods_diagnostics.py +53 -0
  10. package/action/scripts/native_pods_process.py +93 -0
  11. package/action/scripts/native_project_graph.py +148 -0
  12. package/action/scripts/source_maintenance.py +93 -0
  13. package/action/scripts/source_maintenance_fixture.py +58 -0
  14. package/action/scripts/test_auto_detect.py +11 -0
  15. package/action/scripts/test_native_dependency_guard.py +116 -0
  16. package/action/scripts/test_native_dependency_wiring.py +59 -0
  17. package/action/scripts/test_native_pods.py +106 -0
  18. package/action/scripts/test_native_pods_diagnostics.py +39 -0
  19. package/action/scripts/test_native_pods_process.py +41 -0
  20. package/action/scripts/test_source_maintenance.py +122 -0
  21. package/android-action/.daemux-version +1 -1
  22. package/android-action/action.yml +3 -1
  23. package/android-action/play-upload/action.yml +1 -1
  24. package/android-action/play-upload/owner.cjs +61 -0
  25. package/android-action/play-upload/owner_http.cjs +63 -0
  26. package/android-action/play-upload/owner_principal.cjs +15 -0
  27. package/android-action/play-upload/owner_record.cjs +63 -0
  28. package/android-action/play-upload/owner_run.cjs +62 -0
  29. package/android-action/play-upload/run.cjs +14 -3
  30. package/android-action/scripts/resolve_android.py +19 -1
  31. package/android-action/scripts/test_early_play_workflow.py +2 -2
  32. package/android-action/scripts/test_resolve_play_readiness.py +26 -0
  33. package/backend-action/.daemux-version +1 -1
  34. package/package.json +1 -1
  35. package/templates/deploy.yml +8 -3
package/CLAUDE.md CHANGED
@@ -197,6 +197,15 @@ node /path/to/gowalk-cicd/bin/cli.mjs
197
197
  the validated source path. Missing/ambiguous source plists fail; positively generated
198
198
  plists retain the existing build-setting version path. Queries disable signing and package
199
199
  updates, retain inherited proxies, and never enable provisioning network operations.
200
+ - Native Xcode validation and archives first check the selected project/workspace graph with
201
+ `native_dependency_guard.py ROOT --container PATH`. Xcode-managed Swift packages remain refused:
202
+ proxy environment variables do not protect SwiftPM downloads. The same app session preserves
203
+ source and uses the vendor's official CocoaPods distribution or verified local XCFrameworks.
204
+ `native_pods.py` installs a committed, unchanged `Podfile.lock` with `pod install --deployment`
205
+ through the existing Google relay, then checks the completed workspace. No Podfile means no
206
+ dependency download or Google proxy requirement. Initial local lock resolution may use the
207
+ explicit `--before-pods --initial-pods-lock` guard; CI never uses that development exception.
208
+ These checks cover the declared native dependency graph, not arbitrary custom build-script traffic.
200
209
  - Version stamping is per-build-system: Flutter takes `--build-number`, Gradle
201
210
  has no equivalent, so `set_gradle_version()` rewrites the literal in the
202
211
  checkout the way the iOS action patches the pbxproj. Never commit that edit.
@@ -256,12 +265,12 @@ node /path/to/gowalk-cicd/bin/cli.mjs
256
265
  - The iOS action reads every script and prompt from the snapshot it takes of
257
266
  itself in its first step (`$SWIFT_APP_ACTION`, under `RUNNER_TEMP`), never
258
267
  from `${{ github.action_path }}` after that step. The plugin self-update
259
- (`autoupdate_check.sh`) runs right after the snapshot and commits on its own,
268
+ (`autoupdate_check.sh`) runs right after the snapshot and preserves its own source candidate,
260
269
  before the version/signing/archive steps, so a red deploy still receives the
261
270
  release that fixes it; the snapshot is what keeps that run on one version.
262
271
  `test_action_step_order.py` pins both. A new script reference in
263
272
  `action/action.yml` therefore uses `"$SWIFT_APP_ACTION/scripts/..."`.
264
- - Three situations end a green job with work for the panel, and each is a typed
273
+ - Delivery dependencies may remain after a green job; each is a typed
265
274
  check-run annotation (`::notice title=<name>::<one-line JSON with a versioned
266
275
  schema>`), never prose in a `::warning::`: `play_review_pending`
267
276
  (`templates/deploy.yml`, Play refused auto-submit and the bundle landed
@@ -283,3 +292,21 @@ node /path/to/gowalk-cicd/bin/cli.mjs
283
292
  Gradle plugin below 1.9.20 drops to 17 (older kapt cannot run under JDK 21).
284
293
  Detection logic belongs in that script, where it is unit-tested — not inline
285
294
  in the workflow template, where nothing can test it.
295
+
296
+ - CI-generated source is preserved by `source_maintenance.py` on a deterministic
297
+ `task/ci-maintenance-*` branch, with remote parent/tree/SHA readback. It never changes
298
+ the build HEAD, rebases a dirty checkout, force-pushes or pushes to the default branch.
299
+ Only managed action/version source is accepted; credential-shaped staging refuses.
300
+ `source_maintenance_pending` identifies the exact source for the same app session's
301
+ checked PR. Unconfirmed preservation is a job failure; a receipt is not a completed merge.
302
+ - Play preflight, version inventory and the pinned uploader share the panel's package edit owner.
303
+ The binding's nonsecret `principal_sha256` must match the selected service account's
304
+ `client_email` fingerprint before provider work. A mismatch requires supported CI-secret
305
+ provisioning; never accept the account ID or a caller-supplied fingerprint alone.
306
+ `APP_ROBOT_PLAY_EDIT_BINDING` is panel-provisioned repository data; workflows pass it as
307
+ `PLAY_EDIT_BINDING` with the repo-local GitHub token and `contents:write`/`actions:read`.
308
+ `play-upload/owner_run.cjs` protects Python entrypoints; `run.cjs` holds the same owner across
309
+ its permitted upload retry. Missing/retired bindings, unknown write outcomes and active owners
310
+ refuse before provider work; only exact terminal run/attempt/source proof permits recovery.
311
+ Other packages stay parallel. The GitHub coordination request is fixed-origin and does not
312
+ change the Google account proxy. Update workflow wiring as well as action directories.
package/README.md CHANGED
@@ -292,6 +292,7 @@ the situation and whose **message** is one line of JSON carrying a versioned
292
292
  | `play_upload_result` | `gowalk-cicd/play-upload.v1` | `ok`, `code`, `attempts`, `review_pending`, upstream identity |
293
293
  | `store_version_locked` | `gowalk-cicd/store-version-locked.v1` | `version`, `state`, `build_number` |
294
294
  | `firebase_symbols_pending` | `gowalk-cicd/firebase-symbols-pending.v1` | `platform`, `app_id`, `source_sha`, `artifact`, `file`, `sha256`, `status` |
295
+ | `source_maintenance_pending` | `gowalk-cicd/source-maintenance-pending.v1` | `repo`, `base_sha`, `head_sha`, `branch`, `run_id` |
295
296
  | `apple_build_upload_failed` (error) | `gowalk-cicd/apple-build-upload-failed.v2` | `category`, `stage`, `receipt_available`, receipt identifiers (`cfBundleVersion`, `upload_id`, `file_id`, `state`, `provider_status`, `provider_code`, `provider_pointer`), `action` |
296
297
 
297
298
  Add a field by bumping the schema version; never change the meaning of an
@@ -300,10 +301,12 @@ existing one.
300
301
  ### The plugin self-update runs first
301
302
 
302
303
  The vendored iOS action checks npm for a newer `gowalk-cicd` at the **start** of
303
- every default-branch run and commits the refreshed action directories in their
304
- own commit, before the version, signing and archive steps. A deploy that is red
305
- in one of those steps therefore still picks up the plugin release that fixes it,
306
- on the very run that fails, and the next push runs the fixed plugin. The run that
304
+ every default-branch run and preserves the refreshed action directories on a verified
305
+ task branch before the version, signing and archive steps. The
306
+ `source_maintenance_pending` receipt lets the same app session adopt that exact commit
307
+ into its current task branch and land it through the normal checked PR, including
308
+ when later deploy steps fail. CI never pushes to the default branch or rebases its
309
+ dirty build checkout; a failed preservation is a job failure. The run that
307
310
  performed the update keeps executing the version it started with: the action
308
311
  snapshots itself to `$RUNNER_TEMP` before anything mutates and reads every script
309
312
  and prompt from that copy.
@@ -889,8 +892,9 @@ and configure that one in the Apple Developer portal, then re-run.
889
892
 
890
893
  When the ASC combined floor (max of pending review, `preReleaseVersions`,
891
894
  or builds-via-`preReleaseVersion`) exceeds your project's
892
- `MARKETING_VERSION`, the action auto-bumps and commits the new value as
893
- part of the same bot commit that handles cert refresh / autoupdate.
895
+ `MARKETING_VERSION`, the action auto-bumps and preserves the staged source on a
896
+ verified task branch. The app session resolves its `source_maintenance_pending`
897
+ receipt through its normal checked PR before source delivery is complete.
894
898
 
895
899
  Default policy is `rollover` — patch with carry: at `.9` it rolls into
896
900
  the next minor (`1.0.9` → `1.1.0`), and at minor=9 it cascades into the
@@ -927,8 +931,8 @@ do not override the input.
927
931
  In `'none'` mode, the floor check fails the build and you must bump
928
932
  `MARKETING_VERSION` manually before retrying.
929
933
 
930
- **Side effect:** the bot commit subject reflects what was changed, e.g.
931
- `ci: refresh signing identity + bump MARKETING_VERSION [skip ci]`.
934
+ **Side effect:** the retained source commit identifies the version change.
935
+ Signing credentials are refused from maintenance commits; use encrypted repository secrets.
932
936
 
933
937
  **Source-of-truth resolution.** The auto-bump writes the new value
934
938
  into the file your project actually reads from, in this order:
@@ -956,8 +960,8 @@ The vendored action ships with a per-run autoupdate check. On every
956
960
  push to your default branch, the action queries npm for the latest
957
961
  `gowalk-cicd`, compares against the local marker at
958
962
  `.github/actions/swift-app/.daemux-version`, and if newer, re-vendors
959
- the package via `npx --yes` and commits the refreshed action files
960
- back (under both `.github/actions/swift-app/` and
963
+ the package via `npx --yes` and preserves the refreshed action files
964
+ on a verified task branch (under both `.github/actions/swift-app/` and
961
965
  `.github/actions/android-app/`).
962
966
  `.github/workflows/deploy.yml` is NEVER auto-committed — see
963
967
  "deploy.yml is not auto-updated" below.
@@ -965,15 +969,15 @@ back (under both `.github/actions/swift-app/` and
965
969
  | Aspect | Behaviour |
966
970
  |--------|-----------|
967
971
  | Trigger | Push to default branch only (PR / branch runs do nothing) |
968
- | Lag | One run — the *next* push after a new release picks up the update |
969
- | Suppression | `[skip ci]` in the commit subject + `paths-ignore` for both action directories |
970
- | Combined commit | Cert refresh + autoupdate share a single commit when both fire in the same run |
971
- | Failure mode | Non-fatal: a failed `npm view` or `npx` emits `::warning::` and the build continues |
972
+ | Landing | The app session adopts the exact receipt on its current task branch and uses the protected PR |
973
+ | Isolation | CI leaves HEAD and worktree bytes intact; no default-branch push or rebase |
974
+ | Candidates | Action updates and later version changes have separate verified receipts |
975
+ | Failure mode | npm discovery remains optional; unconfirmed source preservation fails the job |
972
976
 
973
977
  The shipped `deploy.yml` template already includes the required
974
978
  `paths-ignore` entries for both action directories. If you have an
975
979
  older `deploy.yml` checked in, add both entries to prevent the
976
- autoupdate commit-back from re-triggering the workflow.
980
+ tooling-only source landing from re-triggering the mobile workflow.
977
981
 
978
982
  ### deploy.yml is not auto-updated
979
983
 
@@ -983,9 +987,9 @@ of `contents: write` — this is a built-in safeguard against CI
983
987
  self-modification. When a new version of `gowalk-cicd`
984
988
  requires `deploy.yml` schema changes (e.g., new permissions, new
985
989
  paths-ignore entries), the action's release notes will call this out
986
- and you must run `npx --yes gowalk-cicd` manually once to
987
- sync your `deploy.yml`. Existing deploy.yml stays untouched on every
988
- auto-update cycle until you do.
990
+ and the app session runs the pinned `npx --yes gowalk-cicd` adoption in its task
991
+ workspace, preserving app-specific workflow inputs and landing the full change through
992
+ its checked PR. The CI-side update does not commit the workflow.
989
993
 
990
994
  Changes that need that manual run:
991
995
 
@@ -1 +1 @@
1
- 1.0.78
1
+ 1.0.79
package/action/action.yml CHANGED
@@ -182,8 +182,8 @@ inputs:
182
182
  re-vendors the package via `npx --yes` if the published version is
183
183
  newer than the marker at .github/actions/swift-app/.daemux-version.
184
184
  The refreshed action files (NOT deploy.yml, which GITHUB_TOKEN
185
- cannot push) are committed alongside any cert refresh in a single
186
- combined commit. Set to 'false' to pin the vendored copy.
185
+ cannot push) are retained on a verified task branch for the app session's
186
+ checked PR. Set to 'false' to pin the vendored copy.
187
187
  required: false
188
188
  default: "true"
189
189
 
@@ -205,15 +205,23 @@ runs:
205
205
  rm -rf "$RUNNER_TEMP/swift-app-action"
206
206
  mkdir -p "$RUNNER_TEMP/swift-app-action"
207
207
  cp -R "${{ github.action_path }}/." "$RUNNER_TEMP/swift-app-action/"
208
+ # Native repositories receive the same canonical Google transport as Android.
209
+ mkdir -p "$RUNNER_TEMP/swift-app-action/google-transport"
210
+ for helper in play_store_proxy.py jvm_proxy_relay.py; do
211
+ if [ -f "$GITHUB_WORKSPACE/.github/actions/android-app/scripts/$helper" ]; then
212
+ cp "$GITHUB_WORKSPACE/.github/actions/android-app/scripts/$helper" \
213
+ "$RUNNER_TEMP/swift-app-action/google-transport/$helper"
214
+ fi
215
+ done
208
216
  echo "SWIFT_APP_ACTION=$RUNNER_TEMP/swift-app-action" >> "$GITHUB_ENV"
209
217
 
210
218
  # The plugin self-update runs BEFORE the version, signing and archive
211
- # steps and lands in its own commit. It used to run after them, so a
219
+ # steps and preserves its own source candidate. It used to run after them, so a
212
220
  # deploy that was red in the version step (App Store Connect refusing a
213
221
  # new version while one is in review, GoVolt run 33743284641) never
214
222
  # reached the self-update, and the plugin release that fixed exactly
215
223
  # that failure could not arrive in the repository that needed it.
216
- # Committing here rather than with the cert refresh below is the same
224
+ # Preserving here rather than after signing is the same
217
225
  # point: a red step later in this run must not take the update with it.
218
226
  - name: Stage bot-side scripts to runner temp
219
227
  if: ${{ (inputs.archive == 'true' || inputs.auto-update == 'true')
@@ -227,6 +235,8 @@ runs:
227
235
  "$RUNNER_TEMP/swift-app-bot-scripts/"
228
236
  cp "$SWIFT_APP_ACTION/scripts/commit_bot_changes.sh" \
229
237
  "$RUNNER_TEMP/swift-app-bot-scripts/"
238
+ cp "$SWIFT_APP_ACTION/scripts/source_maintenance.py" \
239
+ "$RUNNER_TEMP/swift-app-bot-scripts/"
230
240
  chmod +x "$RUNNER_TEMP/swift-app-bot-scripts/"*.sh
231
241
  echo "BOT_SCRIPTS_DIR=$RUNNER_TEMP/swift-app-bot-scripts" >> "$GITHUB_ENV"
232
242
 
@@ -243,9 +253,9 @@ runs:
243
253
  run: bash "$BOT_SCRIPTS_DIR/autoupdate_check.sh"
244
254
 
245
255
  - name: Commit + push refreshed app-ci
246
- # Its own commit, immediately: the refreshed action files must reach
247
- # the default branch even when the version or signing steps of this
248
- # very run fail. deploy.yml is deliberately not staged (GITHUB_TOKEN
256
+ # Preserve a source candidate immediately, even when later version/signing
257
+ # fails. The app session lands it through its normal protected PR.
258
+ # deploy.yml is deliberately not staged (GITHUB_TOKEN
249
259
  # cannot push workflow files). No-op when the plugin was current.
250
260
  if: ${{ inputs.auto-update == 'true'
251
261
  && github.event_name == 'push'
@@ -256,11 +266,6 @@ runs:
256
266
  GITHUB_REF_NAME: ${{ github.ref_name }}
257
267
  run: |
258
268
  set -euo pipefail
259
- # A shallow checkout (fetch-depth 1 for direct action callers) must
260
- # be able to rebase on a moved default branch inside the push retry.
261
- git fetch --unshallow 2>/dev/null \
262
- || git fetch --depth=2147483647 2>/dev/null \
263
- || true
264
269
  bash "$BOT_SCRIPTS_DIR/commit_bot_changes.sh"
265
270
 
266
271
  - name: Resolve credentials + auto-detect Xcode project
@@ -293,6 +298,21 @@ runs:
293
298
  echo "Installing xcodegen (project.yml present, binary missing)…"
294
299
  brew install xcodegen >/dev/null 2>&1 || HOMEBREW_NO_AUTO_UPDATE=1 brew install xcodegen
295
300
  fi
301
+ if ! python3 "$SWIFT_APP_ACTION/scripts/native_pods.py" "$GITHUB_WORKSPACE" \
302
+ --container "${INPUT_WORKSPACE:-$INPUT_PROJECT}" \
303
+ --proxy-helper "$SWIFT_APP_ACTION/google-transport/play_store_proxy.py" \
304
+ > "$RUNNER_TEMP/native-pods.json"; then
305
+ cat "$RUNNER_TEMP/native-pods.json"
306
+ exit 1
307
+ fi
308
+ # CocoaPods adds the dependency workspace even when the app supplied only its project.
309
+ INPUT_WORKSPACE=$(python3 - <<'PY_NATIVE_WORKSPACE'
310
+ import json, os
311
+ with open(os.environ["RUNNER_TEMP"] + "/native-pods.json") as receipt:
312
+ print(json.load(receipt).get("workspace", os.environ.get("INPUT_WORKSPACE", "")))
313
+ PY_NATIVE_WORKSPACE
314
+ )
315
+ export INPUT_WORKSPACE
296
316
  python3 "$SWIFT_APP_ACTION/scripts/read_config.py"
297
317
 
298
318
  - name: Select latest Xcode
@@ -320,6 +340,8 @@ runs:
320
340
  || 'platform=iOS Simulator,name=iPhone 15' }}
321
341
  run: |
322
342
  set -o pipefail
343
+ python3 "$SWIFT_APP_ACTION/scripts/native_dependency_guard.py" "$GITHUB_WORKSPACE" \
344
+ --container "${WORKSPACE:-$PROJECT}"
323
345
  if [ -n "$TEST_COMMAND" ]; then
324
346
  echo "Running custom test command: $TEST_COMMAND"
325
347
  bash -c "$TEST_COMMAND"
@@ -338,6 +360,7 @@ runs:
338
360
  -scheme "$SCHEME" \
339
361
  -configuration "$CONFIGURATION" \
340
362
  -destination "$TEST_DESTINATION" \
363
+ -disableAutomaticPackageResolution -skipPackageUpdates \
341
364
  CODE_SIGNING_ALLOWED=NO \
342
365
  CODE_SIGNING_REQUIRED=NO \
343
366
  CODE_SIGN_IDENTITY="" \
@@ -376,6 +399,8 @@ runs:
376
399
  CONFIGURATION: ${{ inputs.configuration || env.CFG_CONFIGURATION }}
377
400
  run: |
378
401
  set -o pipefail
402
+ python3 "$SWIFT_APP_ACTION/scripts/native_dependency_guard.py" "$GITHUB_WORKSPACE" \
403
+ --container "${WORKSPACE:-$PROJECT}"
379
404
  python3 "$SWIFT_APP_ACTION/scripts/resolve_marketing_version.py" \
380
405
  | tee -a "$GITHUB_ENV"
381
406
 
@@ -487,30 +512,10 @@ runs:
487
512
  echo "::error::Signing preparation failed; preserving any generated cache before failing."
488
513
  fi
489
514
 
490
- # DESIGN DECISION (intentional, not a bug):
491
- # The next five steps stage + commit-back the Apple Distribution
492
- # code-signing identity (.p12 + provisioning profiles) AND any
493
- # autoupdate of the vendored app-ci actions into the consumer
494
- # repo's default branch. This is a deliberate trade-off chosen over
495
- # GitHub Secrets-based storage:
496
- # - Consumer repos already commit the ASC API key (.p8) to creds/, so
497
- # the threat model already accepts repo-stored Apple credentials.
498
- # - Each app uses a different Apple Developer account / .p8, so
499
- # org-level GitHub Secrets do not amortize setup work.
500
- # - "Drop file in creds/" remains the consistent UX for new projects.
501
- # Non-negotiable mitigations (see IOS_NATIVE_CI_SETUP.md):
502
- # - Repo MUST stay private (branch protection on visibility recommended).
503
- # - permissions: contents: write is required; expands attack surface
504
- # of any compromised step in the workflow — keep workflow minimal.
505
- # - .gitignore must NOT block the new creds/ files (see setup doc).
506
- #
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.
513
-
515
+ # Encrypted signing material is the supported factory path and disables
516
+ # the legacy signing-cache staging below. Source maintenance refuses staged
517
+ # credential paths; it never pushes them or bypasses branch protection.
518
+ # A version bump is preserved separately from the earlier action update.
514
519
 
515
520
  - name: Stage refreshed signing identity
516
521
  # Only stage on push to the default branch — PR runs and feature
@@ -570,8 +575,8 @@ runs:
570
575
  fi
571
576
 
572
577
  - name: Commit + push staged bot changes
573
- # The cert refresh (and any marketing-version auto-bump the version
574
- # step staged). The autoupdate landed in its own commit at the top of
578
+ # Any marketing-version auto-bump the version step staged is preserved.
579
+ # The autoupdate has its own candidate at the top of
575
580
  # this action; this script is a no-op when nothing is staged, so a
576
581
  # PR / feature-branch run that somehow reaches this step does no
577
582
  # harm — but we still gate on default-branch push for clarity.
@@ -604,6 +609,8 @@ runs:
604
609
  BUNDLE_ID: ${{ inputs.bundle-id || env.CFG_BUNDLE_ID }}
605
610
  run: |
606
611
  set -o pipefail
612
+ python3 "$SWIFT_APP_ACTION/scripts/native_dependency_guard.py" "$GITHUB_WORKSPACE" \
613
+ --container "${WORKSPACE:-$PROJECT}"
607
614
  # Read every target's complete settings and preserve Xcode's actual status/error.
608
615
  # The selected application's source root can differ from its containing workspace.
609
616
  INFOPLIST_PATH=$(python3 "$SWIFT_APP_ACTION/scripts/resolve_info_plist.py")
@@ -662,6 +669,8 @@ runs:
662
669
  CONFIGURATION: ${{ inputs.configuration || env.CFG_CONFIGURATION }}
663
670
  run: |
664
671
  set -o pipefail
672
+ python3 "$SWIFT_APP_ACTION/scripts/native_dependency_guard.py" "$GITHUB_WORKSPACE" \
673
+ --container "${WORKSPACE:-$PROJECT}"
665
674
  if [ -n "$WORKSPACE" ]; then
666
675
  PROJ_ARGS=(-workspace "$WORKSPACE")
667
676
  else
@@ -680,6 +689,7 @@ runs:
680
689
  -configuration "$CONFIGURATION" \
681
690
  -destination "generic/platform=iOS" \
682
691
  -archivePath "$RUNNER_TEMP/app.xcarchive" \
692
+ -disableAutomaticPackageResolution -skipPackageUpdates \
683
693
  -skipPackagePluginValidation \
684
694
  -skipMacroValidation \
685
695
  CURRENT_PROJECT_VERSION="$BUILD_NUMBER" \
@@ -47,6 +47,7 @@ from pathlib import Path
47
47
  from typing import Optional
48
48
 
49
49
  from cfg_io import log, notice
50
+ import native_dependency_guard
50
51
 
51
52
 
52
53
  APP_PRODUCT_TYPE = "com.apple.product-type.application"
@@ -231,12 +232,8 @@ def _pick_by_basename(matches: list[Path], workspace: Path, label: str) -> Path:
231
232
  def _resolve_package_dependencies(
232
233
  workspace: Path, project: str, workspace_file: str
233
234
  ) -> bool:
234
- """Resolve the SPM graph once so later xcodebuild calls are cheap.
235
-
236
- Returns True when resolution finished (regardless of exit status — a
237
- project with no packages still exits cleanly and a partial resolve may
238
- be enough for ``-list``), False when it could not be run at all.
239
- """
235
+ """Retain local Xcode recovery only after refusing an unverified package graph."""
236
+ native_dependency_guard.check(workspace, workspace_file or project)
240
237
  cmd = ["xcodebuild", "-resolvePackageDependencies"]
241
238
  if workspace_file:
242
239
  cmd += ["-workspace", workspace_file]
@@ -269,7 +266,8 @@ def _list_schemes(
269
266
  if key in _list_cache:
270
267
  return _list_cache[key]
271
268
 
272
- cmd = ["xcodebuild", "-list", "-json"]
269
+ native_dependency_guard.check(workspace, workspace_file or project)
270
+ cmd = ["xcodebuild", "-list", "-json", "-disableAutomaticPackageResolution", "-skipPackageUpdates"]
273
271
  if workspace_file:
274
272
  cmd += ["-workspace", workspace_file]
275
273
  elif project:
@@ -343,7 +341,9 @@ def _show_build_settings(
343
341
  if key in _settings_cache:
344
342
  return _settings_cache[key]
345
343
 
346
- cmd = ["xcodebuild", "-showBuildSettings", "-json", "-scheme", scheme]
344
+ native_dependency_guard.check(workspace, workspace_file or project)
345
+ cmd = ["xcodebuild", "-showBuildSettings", "-json", "-scheme", scheme,
346
+ "-disableAutomaticPackageResolution", "-skipPackageUpdates"]
347
347
  if configuration:
348
348
  cmd += ["-configuration", configuration]
349
349
  if workspace_file:
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env bash
2
- # Commits whatever previous steps staged (cert refresh and/or autoupdate),
3
- # pushes with up-to-3 attempts (rebase between). Exits 0 with ::warning::
4
- # on exhausted retries so the wrapping job stays green.
2
+ # Preserve staged source on a verified task branch for the app session's normal PR.
3
+ # The build checkout, protected default branch and unrelated dirty files stay intact.
5
4
  set -euo pipefail
6
5
 
7
6
  if git diff --cached --quiet; then
@@ -54,29 +53,4 @@ git config user.name >/dev/null 2>&1 || git config user.name "github-actions[b
54
53
  git config user.email >/dev/null 2>&1 || \
55
54
  git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
56
55
 
57
- git commit -m "$subject" \
58
- -m "Co-Authored-By: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
59
-
60
- branch="${GITHUB_REF_NAME:-$(git rev-parse --abbrev-ref HEAD)}"
61
-
62
- attempt=1
63
- max=3
64
- while (( attempt <= max )); do
65
- if git push origin "HEAD:$branch"; then
66
- echo "commit_bot_changes: push succeeded on attempt $attempt"
67
- exit 0
68
- fi
69
- echo "::warning::commit_bot_changes: push attempt $attempt failed"
70
- if (( attempt < max )); then
71
- git fetch origin "$branch" || true
72
- git pull --rebase origin "$branch" || {
73
- echo "::warning::commit_bot_changes: rebase failed, aborting"
74
- git rebase --abort >/dev/null 2>&1 || true
75
- exit 0
76
- }
77
- fi
78
- attempt=$((attempt + 1))
79
- done
80
-
81
- echo "::warning::commit_bot_changes: exhausted $max push attempts"
82
- exit 0
56
+ exec python3 "$(dirname "$0")/source_maintenance.py" "$subject"
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env python3
2
+ """Refuse unverified Xcode package networking in the selected native dependency graph."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from pathlib import Path
8
+ import sys
9
+
10
+ from native_project_graph import Refused, project, selected, workspace
11
+
12
+ REMEDIATION = ("Preserve source; use the vendor's official CocoaPods distribution through the assigned Google proxy "
13
+ "and commit Podfile.lock, or verified local XCFrameworks; rerun the same task's build.")
14
+
15
+
16
+ def check(root: Path, container: str = "", *, before_pods: bool = False, initial_pods_lock: bool = False) -> dict:
17
+ if initial_pods_lock and not before_pods:
18
+ raise Refused("native_initial_lock_phase_required")
19
+ root = root.resolve(strict=True)
20
+ pending, visited, count = [selected(root, container, before_pods)], set(), 0
21
+ while pending:
22
+ path = pending.pop()
23
+ if path in visited:
24
+ continue
25
+ visited.add(path)
26
+ if len(visited) > 200:
27
+ raise Refused("native_project_inventory_unverified")
28
+ if path.suffix == ".xcworkspace":
29
+ pending.extend(workspace(root, path, before_pods, initial_pods_lock))
30
+ elif path.suffix == ".xcodeproj":
31
+ pending.extend(project(root, path))
32
+ count += 1
33
+ else:
34
+ raise Refused("native_workspace_reference_unverified")
35
+ if not count:
36
+ raise Refused("native_project_inventory_missing")
37
+ return {"ok": True, "projects": count, "xcode_package_resolution": "unused",
38
+ "phase": "before_pods" if before_pods else "complete"}
39
+
40
+
41
+ def main(argv: list[str]) -> int:
42
+ parser = argparse.ArgumentParser(description=__doc__)
43
+ parser.add_argument("root", type=Path)
44
+ parser.add_argument("--container", default="")
45
+ parser.add_argument("--before-pods", action="store_true")
46
+ parser.add_argument("--initial-pods-lock", action="store_true")
47
+ args = parser.parse_args(argv)
48
+ try:
49
+ answer = check(args.root, args.container, before_pods=args.before_pods,
50
+ initial_pods_lock=args.initial_pods_lock)
51
+ except (Refused, OSError) as exc:
52
+ code = str(exc) if isinstance(exc, Refused) else "native_project_inventory_unverified"
53
+ answer = {"ok": False, "code": code, "next": REMEDIATION}
54
+ print(json.dumps(answer, sort_keys=True))
55
+ return 0 if answer["ok"] else 1
56
+
57
+
58
+ if __name__ == "__main__":
59
+ raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env python3
2
+ """Install committed native CocoaPods dependencies through the existing account relay."""
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 signal
11
+ import subprocess
12
+ import sys
13
+ import tempfile
14
+
15
+ import native_dependency_guard as guard
16
+ from native_pods_diagnostics import refuse
17
+ import native_pods_process
18
+
19
+
20
+ def committed(root: Path, lock: Path) -> str:
21
+ if not lock.is_file() or lock.is_symlink():
22
+ raise guard.Refused("native_podfile_lock_required")
23
+ result = subprocess.run(["git", "show", "HEAD:" + lock.relative_to(root).as_posix()],
24
+ cwd=root, capture_output=True, check=False, timeout=15)
25
+ if result.returncode or result.stdout != lock.read_bytes():
26
+ raise guard.Refused("native_podfile_lock_commit_required")
27
+ return hashlib.sha256(result.stdout).hexdigest()
28
+
29
+
30
+ def command(argv: list[str], cwd: Path, log) -> int:
31
+ try:
32
+ return native_pods_process.command(argv, cwd, log)
33
+ except native_pods_process.Interrupted:
34
+ raise guard.Refused("native_pods_operation_interrupted") from None
35
+ except (OSError, RuntimeError, subprocess.SubprocessError):
36
+ raise guard.Refused("native_pods_process_unverified") from None
37
+
38
+
39
+ def prepare(root: Path, container: str, proxy_helper: Path) -> dict:
40
+ root = root.resolve(strict=True)
41
+ folder = (root / container).parent.resolve(strict=True) if container else root
42
+ if not folder.is_relative_to(root):
43
+ raise guard.Refused("native_workspace_reference_unverified")
44
+ if not (folder / "Podfile").is_file():
45
+ return {"ok": True, "pods": "not_configured"}
46
+ if (folder / "Podfile").is_symlink():
47
+ raise guard.Refused("native_workspace_reference_unverified")
48
+ digest = committed(root, folder / "Podfile.lock")
49
+ if not proxy_helper.is_file():
50
+ raise guard.Refused("native_google_transport_helper_required")
51
+ with tempfile.TemporaryFile() as log:
52
+ if command([sys.executable, str(proxy_helper)], root, log):
53
+ raise guard.Refused("native_google_proxy_required")
54
+ if not list(folder.glob("*.xcodeproj")) and (folder / "project.yml").is_file():
55
+ if command(["xcodegen", "generate"], folder, log):
56
+ refuse("native_project_generation_failed", log, guard.Refused)
57
+ initial = container or str(guard.selected(root, "", True).relative_to(root))
58
+ guard.check(root, initial, before_pods=True)
59
+ if command([sys.executable, str(proxy_helper), "--", "pod", "install", "--deployment", "--no-repo-update"],
60
+ folder, log):
61
+ refuse("native_pods_install_failed", log, guard.Refused)
62
+ if digest != hashlib.sha256((folder / "Podfile.lock").read_bytes()).hexdigest():
63
+ raise guard.Refused("native_podfile_lock_changed")
64
+ workspaces = sorted(folder.glob("*.xcworkspace"))
65
+ final = container if container.endswith(".xcworkspace") else ""
66
+ if not final:
67
+ if len(workspaces) != 1:
68
+ raise guard.Refused("native_project_selection_required")
69
+ final = str(workspaces[0].relative_to(root))
70
+ result = guard.check(root, final)
71
+ return {**result, "pods": "installed", "lock_sha256": digest, "workspace": final}
72
+
73
+
74
+ def interrupted(*_args) -> None:
75
+ raise KeyboardInterrupt
76
+
77
+
78
+ def main(argv: list[str]) -> int:
79
+ signal.signal(signal.SIGTERM, interrupted)
80
+ parser = argparse.ArgumentParser(description=__doc__)
81
+ parser.add_argument("root", type=Path)
82
+ parser.add_argument("--container", default="")
83
+ parser.add_argument("--proxy-helper", type=Path, required=True)
84
+ args = parser.parse_args(argv)
85
+ try:
86
+ answer = prepare(args.root, args.container, args.proxy_helper)
87
+ except (guard.Refused, OSError, subprocess.SubprocessError) as exc:
88
+ code = str(exc) if isinstance(exc, guard.Refused) else "native_pods_operation_unverified"
89
+ answer = {"ok": False, "code": code, "next": guard.REMEDIATION}
90
+ if getattr(exc, "diagnostic", ""):
91
+ answer["diagnostic"] = exc.diagnostic
92
+ print(json.dumps(answer, sort_keys=True))
93
+ return 0 if answer["ok"] else 1
94
+
95
+
96
+ if __name__ == "__main__":
97
+ raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,53 @@
1
+ """Bounded native dependency errors; inspect only this child's private output and environment."""
2
+ import base64
3
+ import os
4
+ import re
5
+ from urllib.parse import unquote, urlsplit
6
+
7
+ SENSITIVE = re.compile(r"SECRET|TOKEN|PASSWORD|PROXY|PRIVATE_KEY|SERVICE_ACCOUNT|CREDENTIAL", re.I)
8
+
9
+
10
+ def values(environment: dict[str, str]) -> set[str]:
11
+ found = set()
12
+ for key, value in environment.items():
13
+ if not SENSITIVE.search(key) or len(value) < 4:
14
+ continue
15
+ found.add(value)
16
+ try:
17
+ proxy = urlsplit(value)
18
+ if proxy.username is not None:
19
+ user, password = unquote(proxy.username), unquote(proxy.password or "")
20
+ found.update((user, password, base64.b64encode(f"{user}:{password}".encode()).decode()))
21
+ except ValueError:
22
+ pass
23
+ return {value for value in found if value}
24
+
25
+
26
+ def diagnostic(log, environment: dict[str, str] | None = None) -> str:
27
+ log.flush()
28
+ log.seek(0, os.SEEK_END)
29
+ start = max(0, log.tell() - 16384)
30
+ log.seek(start)
31
+ body = log.read()
32
+ if start:
33
+ body = body.partition(b"\n")[2]
34
+ text = body.decode("utf-8", errors="replace")
35
+ for value in sorted(values(environment if environment is not None else dict(os.environ)), key=len, reverse=True):
36
+ text = text.replace(value, "[redacted]")
37
+ text = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", text)
38
+ text = re.sub(r"-----BEGIN [^-]*PRIVATE KEY-----.*?(?:-----END [^-]*PRIVATE KEY-----|$)",
39
+ "[private key redacted]", text, flags=re.S)
40
+ text = re.sub(r"(?im)^\s*[A-Za-z0-9+/=]{48,}\s*$", "[encoded value redacted]", text)
41
+ text = re.sub(r"(?i)\b(?:proxy-authorization|authorization)\s*:[^\r\n]*", "authorization: [redacted]", text)
42
+ text = re.sub(r'(?i)("(?:private_key|client_secret|password|access_token|refresh_token)"\s*:\s*)"(?:\\.|[^"\\])*"',
43
+ r'\1"[redacted]"', text)
44
+ text = re.sub(r"\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+", "[token redacted]", text)
45
+ text = re.sub(r"https?://[^\s\"'<>]+", "[URL redacted]", text)
46
+ text = re.sub(r"[A-Za-z0-9_+/=-]{32,}", "[long value redacted]", text)
47
+ return "\n".join(text.splitlines()[-30:])[-4000:]
48
+
49
+
50
+ def refuse(code: str, log, exception):
51
+ error = exception(code)
52
+ error.diagnostic = diagnostic(log)
53
+ raise error