gowalk-cicd 1.0.0

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 (84) hide show
  1. package/CLAUDE.md +94 -0
  2. package/README.md +417 -0
  3. package/action/.daemux-version +1 -0
  4. package/action/MIGRATION.md +232 -0
  5. package/action/action.yml +973 -0
  6. package/action/prompts/generate_dependent_fields.prompt.yml +78 -0
  7. package/action/prompts/generate_descriptions.prompt.yml +59 -0
  8. package/action/scripts/app_context_scanner.py +239 -0
  9. package/action/scripts/asc_build_history.py +284 -0
  10. package/action/scripts/asc_common.py +246 -0
  11. package/action/scripts/asc_metadata_applier.py +368 -0
  12. package/action/scripts/asc_metadata_detector.py +250 -0
  13. package/action/scripts/asc_version_create.py +365 -0
  14. package/action/scripts/asc_version_fetch.py +274 -0
  15. package/action/scripts/asc_version_reuse.py +177 -0
  16. package/action/scripts/auto_detect.py +391 -0
  17. package/action/scripts/autoupdate_check.sh +118 -0
  18. package/action/scripts/cert_factory.py +190 -0
  19. package/action/scripts/cfg_io.py +27 -0
  20. package/action/scripts/cfg_resolve.py +147 -0
  21. package/action/scripts/commit_bot_changes.sh +82 -0
  22. package/action/scripts/creds_store.py +328 -0
  23. package/action/scripts/keychain.py +103 -0
  24. package/action/scripts/lookup_app_id.py +51 -0
  25. package/action/scripts/manage_marketing_version.py +337 -0
  26. package/action/scripts/metadata_constants.py +102 -0
  27. package/action/scripts/mmv_decide_create.py +181 -0
  28. package/action/scripts/mmv_floor_check.py +265 -0
  29. package/action/scripts/next_build_number.py +195 -0
  30. package/action/scripts/pbxproj_editor.py +220 -0
  31. package/action/scripts/prepare_signing.py +223 -0
  32. package/action/scripts/profile_io.py +64 -0
  33. package/action/scripts/profile_manager.py +307 -0
  34. package/action/scripts/read_config.py +315 -0
  35. package/action/scripts/resolve_marketing_version.py +173 -0
  36. package/action/scripts/set_app_store_whats_new.py +317 -0
  37. package/action/scripts/team_resolver.py +130 -0
  38. package/action/scripts/test_app_context_scanner.py +167 -0
  39. package/action/scripts/test_asc_build_history.py +221 -0
  40. package/action/scripts/test_asc_builds_prerelease.py +219 -0
  41. package/action/scripts/test_asc_common_timeouts.py +121 -0
  42. package/action/scripts/test_asc_metadata_applier.py +850 -0
  43. package/action/scripts/test_asc_metadata_detector.py +445 -0
  44. package/action/scripts/test_auto_detect.py +432 -0
  45. package/action/scripts/test_cfg_resolve_credentials.py +48 -0
  46. package/action/scripts/test_compute_next_version.py +95 -0
  47. package/action/scripts/test_compute_next_version_patch_backcompat.py +41 -0
  48. package/action/scripts/test_compute_next_version_strictness.py +67 -0
  49. package/action/scripts/test_fetch_versions.py +163 -0
  50. package/action/scripts/test_ground_truth_floor.py +104 -0
  51. package/action/scripts/test_manage_marketing_version.py +134 -0
  52. package/action/scripts/test_manage_marketing_version_autoroll.py +149 -0
  53. package/action/scripts/test_manage_marketing_version_floor.py +170 -0
  54. package/action/scripts/test_manage_marketing_version_match.py +187 -0
  55. package/action/scripts/test_mmv_409_classifier.py +231 -0
  56. package/action/scripts/test_mmv_autobump_persist_gate.py +131 -0
  57. package/action/scripts/test_mmv_decide_create.py +119 -0
  58. package/action/scripts/test_mmv_floor_check.py +158 -0
  59. package/action/scripts/test_mmv_floor_crosscheck.py +132 -0
  60. package/action/scripts/test_mmv_helpers.py +57 -0
  61. package/action/scripts/test_next_build_number.py +174 -0
  62. package/action/scripts/test_read_config_auto_detect.py +257 -0
  63. package/action/scripts/test_resolve_marketing_version.py +298 -0
  64. package/action/scripts/test_reuse_stale_editable.py +182 -0
  65. package/action/scripts/test_set_app_store_whats_new.py +71 -0
  66. package/action/scripts/test_team_fallback_wiring.py +157 -0
  67. package/action/scripts/test_team_resolver.py +249 -0
  68. package/action/scripts/tests_common.py +167 -0
  69. package/action/scripts/version_utils.py +373 -0
  70. package/android-action/.daemux-version +1 -0
  71. package/android-action/action.yml +92 -0
  72. package/android-action/scripts/android_config.py +130 -0
  73. package/android-action/scripts/bitrise_deploy.py +160 -0
  74. package/android-action/scripts/find_bundle.py +25 -0
  75. package/android-action/scripts/play_preflight.py +69 -0
  76. package/android-action/scripts/resolve_android.py +70 -0
  77. package/android-action/scripts/sign_bundle.py +70 -0
  78. package/android-action/scripts/test_android_config.py +97 -0
  79. package/android-action/scripts/test_bitrise_deploy.py +124 -0
  80. package/android-action/scripts/test_sign_bundle.py +82 -0
  81. package/bin/cli.mjs +63 -0
  82. package/package.json +57 -0
  83. package/src/install.mjs +208 -0
  84. package/templates/deploy.yml +150 -0
package/CLAUDE.md ADDED
@@ -0,0 +1,94 @@
1
+ # gowalk-cicd — Development Guide
2
+
3
+ ## Purpose
4
+
5
+ npm package that distributes iOS TestFlight and Flutter Android Google Play
6
+ actions. Consumer runs `npx gowalk-cicd` and gets:
7
+
8
+ - `.github/actions/swift-app/` — vendored composite action
9
+ - `.github/actions/android-app/` — vendored Flutter Android action
10
+ - `.github/workflows/deploy.yml` — dual-platform workflow
11
+
12
+ Re-running the command is both install and update. No scope flag, no
13
+ uninstall. Keep it simple.
14
+
15
+ ## Structure
16
+
17
+ ```
18
+ gowalk-cicd/ ← repo root IS the gowalk-cicd package
19
+ ├── action/ ← iOS action — canonical source of truth
20
+ │ ├── action.yml
21
+ │ └── scripts/*.py
22
+ ├── android-action/ ← Android action — canonical source of truth
23
+ │ ├── action.yml
24
+ │ └── scripts/*.py
25
+ ├── bin/cli.mjs ← CLI entrypoint (parses flags, calls runInstall)
26
+ ├── src/install.mjs ← copy logic, gitignore-check, summary print
27
+ ├── templates/deploy.yml ← dual-platform workflow copied verbatim
28
+ ├── scripts/auto-version.mjs ← patch-bump-from-npm used by the publish workflow
29
+ ├── .github/workflows/publish.yml ← auto-publish to npm on push to main
30
+ ├── README.md ← consumer docs
31
+ ├── CLAUDE.md ← this file (for AI agents)
32
+ └── package.json
33
+ ```
34
+
35
+ ## Key invariant: action directories are canonical sources
36
+
37
+ The composite actions under `action/` and `android-action/` are the source of
38
+ truth. Edit them directly here; consumer copies are generated by the installer.
39
+
40
+ There is no upstream copy to sync from. This package was split out of the
41
+ daemux-plugins monorepo, where `action/` used to be a vendored mirror of
42
+ `.github/actions/ios-native-testflight/` refreshed by a `sync-action.mjs`
43
+ step. That relationship no longer applies — the sync script and the
44
+ vendoring concept are gone.
45
+
46
+ ## Publishing
47
+
48
+ Auto-publish on every push to `main` (the entire repo IS the package). The
49
+ workflow (`.github/workflows/publish.yml`):
50
+
51
+ 1. Runs `scripts/auto-version.mjs .` which queries npm for the current
52
+ published version of `gowalk-cicd` and bumps the patch. A first publish
53
+ keeps the seed version from `package.json`.
54
+ 2. Stamps the new version into both action directories (consumed by the
55
+ consumer-side autoupdate check) before publishing.
56
+ 3. Runs `npm publish --access public` using npm Trusted Publishing. The new
57
+ package name requires one bootstrap publish via `NPM_TOKEN` before npm can
58
+ attach the `publish.yml` trusted-publisher relationship; the workflow
59
+ supplies that token as a fallback and uses OIDC after trust is configured.
60
+
61
+ Manual publish (from repo root):
62
+
63
+ ```bash
64
+ npm publish --access public
65
+ ```
66
+
67
+ ## Testing locally
68
+
69
+ From a scratch directory:
70
+
71
+ ```bash
72
+ mkdir /tmp/scratch && cd /tmp/scratch && git init
73
+ node /path/to/gowalk-cicd/bin/cli.mjs --dry-run
74
+ ```
75
+
76
+ Or install into a real app repo (overwrites both action directories and
77
+ `.github/workflows/deploy.yml`):
78
+
79
+ ```bash
80
+ cd ~/path/to/app-project
81
+ node /path/to/gowalk-cicd/bin/cli.mjs
82
+ ```
83
+
84
+ ## Conventions
85
+
86
+ - Use Node's built-in `fs`/`path`/`url` modules; no runtime deps except
87
+ `update-notifier` for the nag banner.
88
+ - Never hardcode paths — use `fileURLToPath(import.meta.url)` and derive
89
+ from `__dirname`.
90
+ - Keep the CLI surface minimal: `--dry-run`, `-v`, `-h`. Resist adding flags.
91
+ - `.gitignore` in the consumer repo is git-tracked. Never auto-edit it.
92
+ Detect and warn, tell the user to remove lines manually.
93
+ - Store API calls belong in GitHub Actions. Local tests may build/sign but must
94
+ never connect to App Store Connect or Google Play.
package/README.md ADDED
@@ -0,0 +1,417 @@
1
+ # gowalk-cicd
2
+
3
+ One installer for iOS TestFlight and Android Google Play delivery. It supports
4
+ native Swift/SwiftUI iOS projects and Flutter apps. Put the platform keys under
5
+ `creds/`, push to `main`, and both store builds run only on GitHub Actions.
6
+
7
+ ## Install
8
+
9
+ From the root of your app repo:
10
+
11
+ ```bash
12
+ npx --yes gowalk-cicd
13
+ ```
14
+
15
+ Writes three things into your repo:
16
+
17
+ - `.github/actions/swift-app/` — the vendored composite action (action.yml + scripts)
18
+ - `.github/actions/android-app/` — Flutter Android build/sign action
19
+ - `.github/workflows/deploy.yml` — workflow that builds and deploys both platforms
20
+
21
+ Re-run the same command anytime to pull the latest version.
22
+
23
+ ## iOS credentials
24
+
25
+ 1. Create an **App Store Connect API key** with the **App Manager** role
26
+ (App Store Connect → Users and Access → Keys → generate).
27
+ 2. Download the `.p8` and save it into your repo at:
28
+ ```
29
+ creds/AuthKey_<KEY_ID>_Issuer_<ISSUER_UUID>.p8
30
+ ```
31
+ Example: `creds/AuthKey_ABC123DEFG_Issuer_69a6de70-xxxx-47e3-e053-5b8c7c11a4d1.p8`
32
+ The filename encodes both the `KEY_ID` and the `ISSUER_ID` — the composite
33
+ action parses them from the filename.
34
+ 3. The repo **must be private**. The p8 is a long-lived credential; never
35
+ commit it to a public repo.
36
+ 4. (First time only, per app) Create the app record in App Store Connect. See
37
+ [First-time app setup](#first-time-app-setup) below.
38
+ 5. The existing metadata-rich form
39
+ `*(key_id_<KEY_ID>_issuer_<UUID>_vendor_id_<ID>).p8` is also accepted.
40
+
41
+ ## Android credentials
42
+
43
+ Place these files under `creds/`:
44
+
45
+ 1. `android-upload-key.jks` — the long-lived Google Play upload keystore.
46
+ 2. `android-signing.properties` — signing metadata in this exact format:
47
+
48
+ ```properties
49
+ storeFile=android-upload-key.jks
50
+ storePassword=<keystore password>
51
+ keyAlias=<upload key alias>
52
+ keyPassword=<upload key password>
53
+ ```
54
+
55
+ 3. One Google service-account JSON key. Its filename is unrestricted
56
+ (`Gowalk.json` is valid); the action identifies it by the
57
+ `type: service_account` JSON fields.
58
+
59
+ The service account must have Google Play Console access to the app and the
60
+ Android Publisher API must be enabled. Keep the repository private. The
61
+ workflow reads these files from the checkout, so they must be available to
62
+ GitHub Actions (this package's zero-config convention is to track them only in
63
+ a private repository).
64
+
65
+ Google Play requires the first AAB to be uploaded through Play Console. The
66
+ first CI run still succeeds and retains the signed AAB as an
67
+ `android-<package>-<versionCode>` workflow artifact. Upload that artifact once
68
+ in Play Console; every later run detects API readiness and uploads to the
69
+ `internal` track automatically. Override the track or status with repository
70
+ variables `GOOGLE_PLAY_TRACK` and `GOOGLE_PLAY_STATUS`.
71
+
72
+ ## Android delivery via Bitrise (alternative mode)
73
+
74
+ Use this when the Android **upload keystore password lives in Bitrise** (not in
75
+ the repo) — the GitHub runner then cannot sign the AAB locally. In this mode the
76
+ Android job triggers a Bitrise workflow (which holds the keystore + Play
77
+ service-account secrets) over the Bitrise API and waits for it, mirroring the
78
+ build result back into the GitHub Actions run. iOS still ships via GitHub
79
+ Actions as normal.
80
+
81
+ Enable it by committing `creds/bitrise.json`:
82
+
83
+ ```json
84
+ {
85
+ "enabled": true,
86
+ "app_slug": "8b2a7a24-....",
87
+ "workflow": "android-deploy",
88
+ "branch": "main",
89
+ "api_token": "bitpat_...."
90
+ }
91
+ ```
92
+
93
+ - `app_slug` — the Bitrise app slug (from the app's Bitrise URL or
94
+ `GET https://api.bitrise.io/v0.1/apps`).
95
+ - `workflow` — the Bitrise workflow that builds, signs, and runs
96
+ `google-play-deploy` (default `android-deploy`).
97
+ - `branch` — optional; defaults to the pushed ref.
98
+ - `api_token` — a Bitrise personal access token. Committing it to a **private**
99
+ repo's `creds/` follows the same trust model as the ASC `.p8`. For non-private
100
+ repos, omit it here and set the `BITRISE_API_TOKEN` Actions secret instead.
101
+
102
+ When `creds/bitrise.json` is present with `enabled: true`, the local
103
+ build/sign/upload steps are skipped and `bitrise_deploy.py` drives the Bitrise
104
+ build; otherwise the local path above runs unchanged.
105
+
106
+ The Bitrise `android-deploy` workflow must produce a versionCode higher than
107
+ what is already on Play. The convention used by this fleet is a `+200` offset
108
+ over `$BITRISE_BUILD_NUMBER` via a `change-android-versioncode-and-versionname`
109
+ step, with the versionName read from `pubspec.yaml`.
110
+
111
+ ## Sharing one distribution cert across a team's apps
112
+
113
+ The iOS action caches the Apple Distribution cert under `creds/` and reuses it
114
+ across runs (cert alive + NotAfter > 30 days). But Apple caps a team at **2**
115
+ distribution certs, and the cache is per-repo. If **three or more apps share one
116
+ Apple Developer team**, they cannot each hold their own cert — every run finds
117
+ its cached cert already revoked by a sibling, mints a new one, and revokes
118
+ another. The symptom is a "Your Certificate Has Been Revoked" email on nearly
119
+ every deploy.
120
+
121
+ Fix: apps in the same Apple team should **share one cert**. Commit the *same*
122
+ `creds/cert.p12` + `creds/cert.meta.json` into every repo of that team (an Apple
123
+ Distribution cert is team-scoped, not app-scoped, so one cert signs them all).
124
+ Each app then gets a cache hit and no new cert is ever created. Provisioning
125
+ profiles stay per-app (named `CI-<bundle_id>`) and regenerate against the shared
126
+ cert on the next run. When onboarding a new app into an existing team, copy a
127
+ sibling's `cert.p12` + `cert.meta.json` in rather than letting the first run
128
+ create a fresh cert.
129
+
130
+ ## How it works
131
+
132
+ On every push, iOS and Android run in parallel. Flutter is installed only on
133
+ the GitHub-hosted runners. The Android job runs `flutter analyze`,
134
+ `flutter test`, builds an AAB with `GITHUB_RUN_NUMBER` as its monotonically
135
+ increasing version code, replaces any development signature with the upload
136
+ key, and retains the signed bundle before contacting Google Play.
137
+
138
+ ### iOS delivery
139
+
140
+ The iOS composite action runs on `macos-15` and:
141
+
142
+ 1. **Auto-detects** your `.xcodeproj` / `.xcworkspace`, scheme, bundle ID,
143
+ and `team_id` (from the ASC API key). No `ci.config.yaml` required —
144
+ override via action inputs only if auto-detection fails.
145
+ 2. **Reads the ASC key** from `creds/AuthKey_*.p8` and uses it to authenticate
146
+ to App Store Connect via JWT.
147
+ 3. **Decides the marketing version**: either reuses the current `PREPARE_FOR_SUBMISSION`
148
+ version on App Store Connect, or creates a new version if the highest
149
+ declared version is already live.
150
+ 4. **Computes the next build number** by querying ASC for the latest uploaded
151
+ build and incrementing.
152
+ 5. **Provisions signing** at runtime: generates a throwaway Apple Distribution
153
+ cert + a per-target App Store provisioning profile named `CI-<bundle_id>`.
154
+ Patches the `.pbxproj` to use Manual signing against those profiles.
155
+ 6. **Archives** with `xcodebuild archive`, exports the IPA, and uploads via
156
+ `xcrun altool`.
157
+ 7. **Sets "What's New"** on every declared localization (reads
158
+ `fastlane/metadata/ios/<locale>/release_notes.txt` if present, or from the
159
+ `app-store-whats-new` input).
160
+ 8. **Auto-fills empty App Store metadata** (name, subtitle, keywords,
161
+ description, promotional text, what's new) via GitHub Models AI, on every
162
+ locale that has gaps. See [AI metadata auto-fill](#ai-metadata-auto-fill)
163
+ below.
164
+
165
+ The iOS action requires only the p8. Everything else is derived.
166
+
167
+ ## AI metadata auto-fill
168
+
169
+ On every run, after the TestFlight upload succeeds, the action:
170
+
171
+ 1. Queries App Store Connect for every `appInfoLocalization` and
172
+ `appStoreVersionLocalization` on the editable version.
173
+ 2. Computes the set of empty fields per locale (URL fields are always
174
+ skipped — you must set those manually in ASC).
175
+ 3. Scans your repo for context (README, Info.plist, dependency files, top
176
+ Swift files) and feeds it to `openai/gpt-4o` via GitHub Models with a
177
+ strict JSON schema.
178
+ 4. PATCHes only the fields that were empty — never overwrites existing
179
+ content.
180
+
181
+ Fully idempotent: a second run with no empty fields skips the AI step
182
+ entirely (zero requests, zero PATCHes).
183
+
184
+ ### Requirement: `permissions: models: read`
185
+
186
+ `actions/ai-inference` needs the `models: read` permission. The template
187
+ workflow written by `npx gowalk-cicd` already includes it:
188
+
189
+ ```yaml
190
+ permissions:
191
+ contents: read
192
+ models: read
193
+ ```
194
+
195
+ **Existing consumers must add this block to their `deploy.yml`** at the
196
+ workflow or job level. If it's missing, the AI step fails open with a
197
+ `::warning::` and the rest of the workflow continues unaffected.
198
+
199
+ ### Rate limits
200
+
201
+ GitHub Models free tier allows **50 `gpt-4o` requests per day** (10 per
202
+ minute). One workflow run = one request. If you run many apps from the
203
+ same GitHub account, or trigger several builds per day, switch to the
204
+ cheaper mini model:
205
+
206
+ ```yaml
207
+ - uses: ./.github/actions/swift-app
208
+ with:
209
+ ai-metadata-model: openai/gpt-4o-mini
210
+ ```
211
+
212
+ `gpt-4o-mini` has a much higher free-tier quota.
213
+
214
+ ### Disabling AI metadata
215
+
216
+ Pass `ai-metadata: 'false'` to skip the AI steps entirely:
217
+
218
+ ```yaml
219
+ - uses: ./.github/actions/swift-app
220
+ with:
221
+ ai-metadata: 'false'
222
+ ```
223
+
224
+ ## Update
225
+
226
+ ```bash
227
+ npx --yes gowalk-cicd
228
+ ```
229
+
230
+ Overwrites both vendored actions and `.github/workflows/deploy.yml` with the
231
+ latest versions. Because the actions are vendored locally, builds use the
232
+ reviewed copy in your app repository; updates arrive through this npm package.
233
+
234
+ ## Override config (rare)
235
+
236
+ Most projects never need this. If auto-detection fails or you have multiple
237
+ schemes, pass inputs in `.github/workflows/deploy.yml`:
238
+
239
+ ```yaml
240
+ - uses: ./.github/actions/swift-app
241
+ with:
242
+ scheme: MyAppRelease
243
+ bundle-id: com.example.myapp
244
+ run-tests: 'false'
245
+ uses-non-exempt-encryption: 'false'
246
+ ```
247
+
248
+ All inputs are declared in `.github/actions/swift-app/action.yml`. The
249
+ common ones:
250
+
251
+ | Input | Purpose |
252
+ |-------|---------|
253
+ | `project` / `workspace` | Path to `.xcodeproj` or `.xcworkspace` |
254
+ | `scheme` | Xcode scheme to archive |
255
+ | `configuration` | `Release` (default) or custom |
256
+ | `bundle-id` | Override the auto-detected bundle identifier |
257
+ | `team-id` | Override the auto-detected team ID |
258
+ | `app-store-apple-id` | Numeric ASC app ID (override auto-lookup) |
259
+ | `run-tests` | `false` to skip the simulator test stage |
260
+ | `uses-non-exempt-encryption` | Value for `ITSAppUsesNonExemptEncryption` |
261
+ | `archive` | `false` to build-only (PR runs without secrets) |
262
+ | `upload` | `false` to archive but not upload to TestFlight |
263
+ | `app-store-whats-new` | Inline "What's New" text (overrides files) |
264
+ | `ai-metadata` | `false` to disable AI auto-fill of empty ASC metadata |
265
+ | `ai-metadata-model` | GitHub Models model id (default `openai/gpt-4o`) |
266
+
267
+ ## First-time app setup
268
+
269
+ The app record must exist in App Store Connect before the first CI upload.
270
+ Create it once in the App Store Connect web interface with the same bundle ID.
271
+ Do not run store deployment tools locally; builds, signing, and uploads remain
272
+ inside GitHub Actions.
273
+
274
+ Once the app exists, all subsequent builds and uploads are fully automated via
275
+ the ASC API key.
276
+
277
+ ## Troubleshooting
278
+
279
+ **"No app found for bundle ID"** — the app record doesn't exist yet. Run the
280
+ first-time setup above.
281
+
282
+ **"MARKETING_VERSION is not set"** — the action requires `MARKETING_VERSION`
283
+ to be declared in your target's build settings. Open the target in Xcode →
284
+ Build Settings → Versioning → set `MARKETING_VERSION` (and
285
+ `CURRENT_PROJECT_VERSION`) to `$(MARKETING_VERSION)` /
286
+ `$(CURRENT_PROJECT_VERSION)` respectively.
287
+
288
+ **"You must accept the latest Program License Agreement"** — go to
289
+ developer.apple.com and App Store Connect as the account holder, accept any
290
+ pending agreements, retry.
291
+
292
+ **Upload fails with provisioning errors** — delete any stale profiles named
293
+ `CI-<bundle_id>` on developer.apple.com and re-run; the action will regenerate.
294
+
295
+ ## Auto-bumping MARKETING_VERSION
296
+
297
+ When the ASC combined floor (max of pending review, `preReleaseVersions`,
298
+ or builds-via-`preReleaseVersion`) exceeds your project's
299
+ `MARKETING_VERSION`, the action auto-bumps and commits the new value as
300
+ part of the same bot commit that handles cert refresh / autoupdate.
301
+
302
+ Default policy is `rollover` — patch with carry: at `.9` it rolls into
303
+ the next minor (`1.0.9` → `1.1.0`), and at minor=9 it cascades into the
304
+ next major (`1.9.9` → `2.0.0`). Major has no upper limit (`9.9.9` →
305
+ `10.0.0`). This produces the more natural human progression most
306
+ projects want — patch numbers never silently grow past 9.
307
+
308
+ **Four policies** are supported:
309
+
310
+ | Policy | Example bump | When to use |
311
+ |--------|--------------|-------------|
312
+ | `rollover` (default) | `1.0.5` → `1.0.6`; `1.0.9` → `1.1.0` | Natural progression, carry at .9. |
313
+ | `patch` | `1.0.5` → `1.0.6`; `1.0.9` → `1.0.10` | Legacy unbounded patch — pinned for backward compat. |
314
+ | `minor` | `1.0.5` → `1.1.0`; `1.0.9` → `1.1.0` | Projects that ship every release as a minor. |
315
+ | `none` | (fails the build) | Explicit semver control via human bump. |
316
+
317
+ Full rollover behaviour: `1.0.9` → `1.1.0` (patch overflow), `1.9.9`
318
+ → `2.0.0` (minor cascade), `9.9.9` → `10.0.0` (major no upper limit).
319
+
320
+ **Backward compat:** existing consumers on `0.0.27` that explicitly pin
321
+ `marketing-version-auto-bump: 'patch'` keep their current unbounded
322
+ behavior — the `'patch'` policy is unchanged. The default change from
323
+ `'patch'` → `'rollover'` only affects new installs and consumers that
324
+ do not override the input.
325
+
326
+ **Opt out** via the action input:
327
+
328
+ ```yaml
329
+ - uses: ./.github/actions/swift-app
330
+ with:
331
+ marketing-version-auto-bump: 'none'
332
+ ```
333
+
334
+ In `'none'` mode, the floor check fails the build and you must bump
335
+ `MARKETING_VERSION` manually before retrying.
336
+
337
+ **Side effect:** the bot commit subject reflects what was changed, e.g.
338
+ `ci: refresh signing identity + bump MARKETING_VERSION [skip ci]`.
339
+
340
+ **Source-of-truth resolution.** The auto-bump writes the new value
341
+ into the file your project actually reads from, in this order:
342
+
343
+ 1. **xcodegen `project.yml`** (preferred when present): regex-rewrite
344
+ of the `MARKETING_VERSION:` key, preserving formatting. The
345
+ generated `*.xcodeproj` is regenerated on every build, so editing
346
+ it directly would lose the bump.
347
+ 2. **`*.xcconfig`** sitting alongside the project: handles non-xcodegen
348
+ projects that hoist `MARKETING_VERSION` into xcconfig.
349
+ 3. **`*.xcodeproj/project.pbxproj`**: only when no xcodegen spec is
350
+ present.
351
+ 4. **`Info.plist` `CFBundleShortVersionString`**: last-ditch fallback.
352
+
353
+ If your project uses xcodegen but `MARKETING_VERSION` lives somewhere
354
+ not in `project.yml` or `.xcconfig`, the action emits a `::warning::`
355
+ and falls back to fail-on-floor (refusing to silently edit the
356
+ generated pbxproj). Either move `MARKETING_VERSION` under
357
+ `settings.base` in `project.yml`, or pin
358
+ `marketing-version-auto-bump: 'none'` and bump manually.
359
+
360
+ ## Auto-updates
361
+
362
+ The vendored action ships with a per-run autoupdate check. On every
363
+ push to your default branch, the action queries npm for the latest
364
+ `gowalk-cicd`, compares against the local marker at
365
+ `.github/actions/swift-app/.daemux-version`, and if newer, re-vendors
366
+ the package via `npx --yes` and commits the refreshed action files
367
+ back (under both `.github/actions/swift-app/` and
368
+ `.github/actions/android-app/`).
369
+ `.github/workflows/deploy.yml` is NEVER auto-committed — see
370
+ "deploy.yml is not auto-updated" below.
371
+
372
+ | Aspect | Behaviour |
373
+ |--------|-----------|
374
+ | Trigger | Push to default branch only (PR / branch runs do nothing) |
375
+ | Lag | One run — the *next* push after a new release picks up the update |
376
+ | Suppression | `[skip ci]` in the commit subject + `paths-ignore` for both action directories |
377
+ | Combined commit | Cert refresh + autoupdate share a single commit when both fire in the same run |
378
+ | Failure mode | Non-fatal: a failed `npm view` or `npx` emits `::warning::` and the build continues |
379
+
380
+ The shipped `deploy.yml` template already includes the required
381
+ `paths-ignore` entries for both action directories. If you have an
382
+ older `deploy.yml` checked in, add both entries to prevent the
383
+ autoupdate commit-back from re-triggering the workflow.
384
+
385
+ ### deploy.yml is not auto-updated
386
+
387
+ **deploy.yml is NOT auto-updated.** GitHub's `GITHUB_TOKEN` cannot
388
+ push changes to workflow files (`.github/workflows/*.yml`) regardless
389
+ of `contents: write` — this is a built-in safeguard against CI
390
+ self-modification. When a new version of `gowalk-cicd`
391
+ requires `deploy.yml` schema changes (e.g., new permissions, new
392
+ paths-ignore entries), the action's release notes will call this out
393
+ and you must run `npx --yes gowalk-cicd` manually once to
394
+ sync your `deploy.yml`. Existing deploy.yml stays untouched on every
395
+ auto-update cycle until you do.
396
+
397
+ ### Opt out
398
+
399
+ Pin the vendored copy by passing `auto-update: 'false'` to the action:
400
+
401
+ ```yaml
402
+ - uses: ./.github/actions/swift-app
403
+ with:
404
+ auto-update: 'false'
405
+ ```
406
+
407
+ ### First-run bootstrap
408
+
409
+ The marker is written by `npx gowalk-cicd` itself. A repo
410
+ without a marker (e.g. an old hand-vendored copy) will be treated as
411
+ out-of-date on its first run, after which updates land incrementally.
412
+ Run `npx --yes gowalk-cicd` once locally if you want to skip
413
+ even that first auto-bootstrap.
414
+
415
+ ## License
416
+
417
+ MIT
@@ -0,0 +1 @@
1
+ 1.0.0