gowalk-cicd 1.0.78 → 1.0.80
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 +33 -3
- package/README.md +22 -18
- package/action/.daemux-version +1 -1
- package/action/action.yml +48 -38
- package/action/scripts/auto_detect.py +8 -8
- package/action/scripts/commit_bot_changes.sh +3 -29
- package/action/scripts/native_dependency_guard.py +59 -0
- package/action/scripts/native_pods.py +97 -0
- package/action/scripts/native_pods_diagnostics.py +53 -0
- package/action/scripts/native_pods_process.py +93 -0
- package/action/scripts/native_project_graph.py +148 -0
- package/action/scripts/source_maintenance.py +93 -0
- package/action/scripts/source_maintenance_fixture.py +58 -0
- package/action/scripts/test_auto_detect.py +11 -0
- package/action/scripts/test_native_dependency_guard.py +116 -0
- package/action/scripts/test_native_dependency_wiring.py +59 -0
- package/action/scripts/test_native_pods.py +106 -0
- package/action/scripts/test_native_pods_diagnostics.py +39 -0
- package/action/scripts/test_native_pods_process.py +41 -0
- package/action/scripts/test_source_maintenance.py +122 -0
- package/android-action/.daemux-version +1 -1
- package/android-action/action.yml +3 -1
- package/android-action/play-upload/action.yml +1 -1
- package/android-action/play-upload/owner.cjs +61 -0
- package/android-action/play-upload/owner_http.cjs +63 -0
- package/android-action/play-upload/owner_principal.cjs +15 -0
- package/android-action/play-upload/owner_record.cjs +63 -0
- package/android-action/play-upload/owner_run.cjs +62 -0
- package/android-action/play-upload/run.cjs +14 -3
- package/android-action/scripts/resolve_android.py +19 -1
- package/android-action/scripts/test_early_play_workflow.py +2 -2
- package/android-action/scripts/test_resolve_play_readiness.py +26 -0
- package/backend-action/.daemux-version +1 -1
- package/backend-action/remote_deploy.sh +5 -1
- package/package.json +1 -1
- package/templates/deploy.yml +8 -3
package/CLAUDE.md
CHANGED
|
@@ -177,7 +177,10 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
177
177
|
explicit workflow dispatch remains available. Keep backend deployment's own path filters
|
|
178
178
|
intact, and do not broadly exclude assets, root build settings or unknown app layouts.
|
|
179
179
|
- Domain-backed backend deploys fail closed on certificate issuance and the
|
|
180
|
-
public HTTPS health probe.
|
|
180
|
+
public HTTPS health probe. The probe requires a completed 2xx response with normal TLS
|
|
181
|
+
verification; a redirect, including one to the same URL, is not healthy. Each attempt
|
|
182
|
+
has bounded connection/total time and retains only curl exit and HTTP status diagnostics.
|
|
183
|
+
`select_certbot_account.sh` chooses one existing
|
|
181
184
|
Let's Encrypt ACME v2 account deterministically so multi-account hosts stay
|
|
182
185
|
non-interactive; legacy v1 account directories are not valid candidates.
|
|
183
186
|
`ensure_certificate.sh` preserves failed inventory status and retries only Certbot's
|
|
@@ -197,6 +200,15 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
197
200
|
the validated source path. Missing/ambiguous source plists fail; positively generated
|
|
198
201
|
plists retain the existing build-setting version path. Queries disable signing and package
|
|
199
202
|
updates, retain inherited proxies, and never enable provisioning network operations.
|
|
203
|
+
- Native Xcode validation and archives first check the selected project/workspace graph with
|
|
204
|
+
`native_dependency_guard.py ROOT --container PATH`. Xcode-managed Swift packages remain refused:
|
|
205
|
+
proxy environment variables do not protect SwiftPM downloads. The same app session preserves
|
|
206
|
+
source and uses the vendor's official CocoaPods distribution or verified local XCFrameworks.
|
|
207
|
+
`native_pods.py` installs a committed, unchanged `Podfile.lock` with `pod install --deployment`
|
|
208
|
+
through the existing Google relay, then checks the completed workspace. No Podfile means no
|
|
209
|
+
dependency download or Google proxy requirement. Initial local lock resolution may use the
|
|
210
|
+
explicit `--before-pods --initial-pods-lock` guard; CI never uses that development exception.
|
|
211
|
+
These checks cover the declared native dependency graph, not arbitrary custom build-script traffic.
|
|
200
212
|
- Version stamping is per-build-system: Flutter takes `--build-number`, Gradle
|
|
201
213
|
has no equivalent, so `set_gradle_version()` rewrites the literal in the
|
|
202
214
|
checkout the way the iOS action patches the pbxproj. Never commit that edit.
|
|
@@ -256,12 +268,12 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
256
268
|
- The iOS action reads every script and prompt from the snapshot it takes of
|
|
257
269
|
itself in its first step (`$SWIFT_APP_ACTION`, under `RUNNER_TEMP`), never
|
|
258
270
|
from `${{ github.action_path }}` after that step. The plugin self-update
|
|
259
|
-
(`autoupdate_check.sh`) runs right after the snapshot and
|
|
271
|
+
(`autoupdate_check.sh`) runs right after the snapshot and preserves its own source candidate,
|
|
260
272
|
before the version/signing/archive steps, so a red deploy still receives the
|
|
261
273
|
release that fixes it; the snapshot is what keeps that run on one version.
|
|
262
274
|
`test_action_step_order.py` pins both. A new script reference in
|
|
263
275
|
`action/action.yml` therefore uses `"$SWIFT_APP_ACTION/scripts/..."`.
|
|
264
|
-
-
|
|
276
|
+
- Delivery dependencies may remain after a green job; each is a typed
|
|
265
277
|
check-run annotation (`::notice title=<name>::<one-line JSON with a versioned
|
|
266
278
|
schema>`), never prose in a `::warning::`: `play_review_pending`
|
|
267
279
|
(`templates/deploy.yml`, Play refused auto-submit and the bundle landed
|
|
@@ -283,3 +295,21 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
283
295
|
Gradle plugin below 1.9.20 drops to 17 (older kapt cannot run under JDK 21).
|
|
284
296
|
Detection logic belongs in that script, where it is unit-tested — not inline
|
|
285
297
|
in the workflow template, where nothing can test it.
|
|
298
|
+
|
|
299
|
+
- CI-generated source is preserved by `source_maintenance.py` on a deterministic
|
|
300
|
+
`task/ci-maintenance-*` branch, with remote parent/tree/SHA readback. It never changes
|
|
301
|
+
the build HEAD, rebases a dirty checkout, force-pushes or pushes to the default branch.
|
|
302
|
+
Only managed action/version source is accepted; credential-shaped staging refuses.
|
|
303
|
+
`source_maintenance_pending` identifies the exact source for the same app session's
|
|
304
|
+
checked PR. Unconfirmed preservation is a job failure; a receipt is not a completed merge.
|
|
305
|
+
- Play preflight, version inventory and the pinned uploader share the panel's package edit owner.
|
|
306
|
+
The binding's nonsecret `principal_sha256` must match the selected service account's
|
|
307
|
+
`client_email` fingerprint before provider work. A mismatch requires supported CI-secret
|
|
308
|
+
provisioning; never accept the account ID or a caller-supplied fingerprint alone.
|
|
309
|
+
`APP_ROBOT_PLAY_EDIT_BINDING` is panel-provisioned repository data; workflows pass it as
|
|
310
|
+
`PLAY_EDIT_BINDING` with the repo-local GitHub token and `contents:write`/`actions:read`.
|
|
311
|
+
`play-upload/owner_run.cjs` protects Python entrypoints; `run.cjs` holds the same owner across
|
|
312
|
+
its permitted upload retry. Missing/retired bindings, unknown write outcomes and active owners
|
|
313
|
+
refuse before provider work; only exact terminal run/attempt/source proof permits recovery.
|
|
314
|
+
Other packages stay parallel. The GitHub coordination request is fixed-origin and does not
|
|
315
|
+
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
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
893
|
-
|
|
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
|
|
931
|
-
|
|
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
|
|
960
|
-
|
|
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
|
-
|
|
|
969
|
-
|
|
|
970
|
-
|
|
|
971
|
-
| Failure mode |
|
|
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
|
-
|
|
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
|
|
987
|
-
|
|
988
|
-
|
|
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
|
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.80
|
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
|
|
186
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
247
|
-
#
|
|
248
|
-
#
|
|
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
|
-
#
|
|
491
|
-
#
|
|
492
|
-
#
|
|
493
|
-
#
|
|
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
|
-
#
|
|
574
|
-
#
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
3
|
-
#
|
|
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
|
-
|
|
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
|