gowalk-cicd 1.0.29 → 1.0.31
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 +4 -0
- package/README.md +8 -0
- package/action/.daemux-version +1 -1
- package/action/action.yml +22 -2
- package/action/scripts/test_testflight_version_slot_wiring.py +46 -0
- package/android-action/.daemux-version +1 -1
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
- package/src/install.mjs +13 -14
- package/templates/deploy.yml +51 -19
package/CLAUDE.md
CHANGED
|
@@ -92,6 +92,10 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
92
92
|
Detect and warn, tell the user to remove lines manually.
|
|
93
93
|
- Store API calls belong in GitHub Actions. Local tests may build/sign but must
|
|
94
94
|
never connect to App Store Connect or Google Play.
|
|
95
|
+
- Signing material should use encrypted Actions secrets. `deploy.yml`
|
|
96
|
+
materializes `ASC_KEY_P8` + its ID/issuer and the Android keystore,
|
|
97
|
+
properties and Play service account into an ephemeral runner checkout.
|
|
98
|
+
Tracked `creds/` files remain a backward-compatible fallback.
|
|
95
99
|
- The Android action serves two build systems. `android_config.project_kind()`
|
|
96
100
|
is the only place that decides which; every downstream step branches on the
|
|
97
101
|
`project_kind` output rather than re-sniffing the repo. Flutter wins the tie
|
package/README.md
CHANGED
|
@@ -437,6 +437,13 @@ The iOS composite action runs on `macos-15` and:
|
|
|
437
437
|
|
|
438
438
|
The iOS action requires only the p8. Everything else is derived.
|
|
439
439
|
|
|
440
|
+
Feature-branch runs installed from the standard workflow still archive and
|
|
441
|
+
upload to TestFlight, but pass `manage-app-store-version: 'false'`. This keeps
|
|
442
|
+
branch validation independent of the editable App Store release slot, so a
|
|
443
|
+
build can upload while the current release is locked in review. Default-branch
|
|
444
|
+
runs retain version creation and metadata automation. Direct action callers
|
|
445
|
+
keep the historical behavior because the new input defaults to `true`.
|
|
446
|
+
|
|
440
447
|
### App ID capabilities
|
|
441
448
|
|
|
442
449
|
A provisioning profile carries only the capabilities enabled on its App ID.
|
|
@@ -569,6 +576,7 @@ common ones:
|
|
|
569
576
|
| `uses-non-exempt-encryption` | Value for `ITSAppUsesNonExemptEncryption` |
|
|
570
577
|
| `archive` | `false` to build-only (PR runs without secrets) |
|
|
571
578
|
| `upload` | `false` to archive but not upload to TestFlight |
|
|
579
|
+
| `manage-app-store-version` | `false` to upload without creating/editing the App Store release slot |
|
|
572
580
|
| `app-store-whats-new` | Inline "What's New" text (overrides files) |
|
|
573
581
|
| `ai-metadata` | `false` to disable AI auto-fill of empty ASC metadata |
|
|
574
582
|
| `ai-metadata-model` | GitHub Models model id (default `openai/gpt-4o`) |
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.31
|
package/action/action.yml
CHANGED
|
@@ -64,6 +64,13 @@ inputs:
|
|
|
64
64
|
secrets. When 'false', `upload` is ignored.
|
|
65
65
|
required: false
|
|
66
66
|
default: "true"
|
|
67
|
+
manage-app-store-version:
|
|
68
|
+
description: >
|
|
69
|
+
Set to 'false' to archive and upload a TestFlight build without creating
|
|
70
|
+
or editing an App Store version. App Store metadata automation is skipped.
|
|
71
|
+
This is useful for feature-branch validation while a release is in review.
|
|
72
|
+
required: false
|
|
73
|
+
default: "true"
|
|
67
74
|
run-tests:
|
|
68
75
|
description: "Set to 'false' to skip the simulator test stage"
|
|
69
76
|
required: false
|
|
@@ -282,7 +289,7 @@ runs:
|
|
|
282
289
|
| tee -a "$GITHUB_ENV"
|
|
283
290
|
|
|
284
291
|
- name: Resolve App Store version slot (REUSE or CREATE)
|
|
285
|
-
if: ${{ inputs.archive == 'true' }}
|
|
292
|
+
if: ${{ inputs.archive == 'true' && inputs.manage-app-store-version == 'true' }}
|
|
286
293
|
shell: bash
|
|
287
294
|
env:
|
|
288
295
|
APP_STORE_APPLE_ID: ${{ inputs.app-store-apple-id || env.CFG_APP_STORE_APPLE_ID }}
|
|
@@ -328,6 +335,16 @@ runs:
|
|
|
328
335
|
echo "APP_STORE_VERSION_ID=$APP_STORE_VERSION_ID" >> "$GITHUB_ENV"
|
|
329
336
|
echo "Using marketing version $MARKETING_VERSION build $BUILD_NUMBER (decision=$DECISION_JSON)"
|
|
330
337
|
|
|
338
|
+
- name: Resolve TestFlight build number
|
|
339
|
+
if: ${{ inputs.archive == 'true' && inputs.manage-app-store-version == 'false' }}
|
|
340
|
+
shell: bash
|
|
341
|
+
run: |
|
|
342
|
+
set -o pipefail
|
|
343
|
+
BUILD_NUMBER=$(python3 "${{ github.action_path }}/scripts/next_build_number.py")
|
|
344
|
+
echo "BUILD_NUMBER=$BUILD_NUMBER" >> "$GITHUB_ENV"
|
|
345
|
+
echo "APP_STORE_VERSION_ID=" >> "$GITHUB_ENV"
|
|
346
|
+
echo "Using marketing version $MARKETING_VERSION build $BUILD_NUMBER without an App Store version slot"
|
|
347
|
+
|
|
331
348
|
- name: Notice on deprecated profile-name input
|
|
332
349
|
if: ${{ inputs.archive == 'true' && inputs.profile-name != '' }}
|
|
333
350
|
shell: bash
|
|
@@ -690,7 +707,9 @@ runs:
|
|
|
690
707
|
|
|
691
708
|
- name: Detect empty ASC metadata fields
|
|
692
709
|
id: detect_metadata
|
|
693
|
-
if:
|
|
710
|
+
if: >-
|
|
711
|
+
${{ inputs.archive == 'true' && inputs.upload == 'true'
|
|
712
|
+
&& inputs.manage-app-store-version == 'true' && inputs.ai-metadata != 'false' }}
|
|
694
713
|
shell: bash
|
|
695
714
|
env:
|
|
696
715
|
APP_STORE_APPLE_ID: ${{ inputs.app-store-apple-id || env.CFG_APP_STORE_APPLE_ID }}
|
|
@@ -945,6 +964,7 @@ runs:
|
|
|
945
964
|
# with the default (CFG_WHATS_NEW), preserving any AI-generated notes.
|
|
946
965
|
- name: Set App Store "What's New"
|
|
947
966
|
if: ${{ inputs.archive == 'true' && inputs.upload == 'true'
|
|
967
|
+
&& inputs.manage-app-store-version == 'true'
|
|
948
968
|
&& (inputs.app-store-whats-new != '' || env.CFG_WHATS_NEW != '') }}
|
|
949
969
|
shell: bash
|
|
950
970
|
env:
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
ACTION_YAML = Path(__file__).resolve().parents[1] / "action.yml"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def step_block(source: str, name: str) -> str:
|
|
10
|
+
pattern = rf" - name: {re.escape(name)}\n(?P<body>.*?)(?=\n - name:|\Z)"
|
|
11
|
+
match = re.search(pattern, source, re.DOTALL)
|
|
12
|
+
if match is None:
|
|
13
|
+
raise AssertionError(f"missing action step: {name}")
|
|
14
|
+
return match.group("body")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestTestFlightVersionSlotWiring(unittest.TestCase):
|
|
18
|
+
@classmethod
|
|
19
|
+
def setUpClass(cls) -> None:
|
|
20
|
+
cls.source = ACTION_YAML.read_text(encoding="utf-8")
|
|
21
|
+
|
|
22
|
+
def test_opt_out_is_backward_compatible(self) -> None:
|
|
23
|
+
self.assertRegex(
|
|
24
|
+
self.source,
|
|
25
|
+
r"(?m)^ manage-app-store-version:\n(?: .*\n)*? default: \"true\"$",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def test_opt_out_skips_store_version_but_keeps_build_number(self) -> None:
|
|
29
|
+
store_slot = step_block(self.source, "Resolve App Store version slot (REUSE or CREATE)")
|
|
30
|
+
testflight = step_block(self.source, "Resolve TestFlight build number")
|
|
31
|
+
|
|
32
|
+
self.assertIn("inputs.manage-app-store-version == 'true'", store_slot)
|
|
33
|
+
self.assertIn("inputs.manage-app-store-version == 'false'", testflight)
|
|
34
|
+
self.assertIn("next_build_number.py", testflight)
|
|
35
|
+
self.assertNotIn("manage_marketing_version.py", testflight)
|
|
36
|
+
|
|
37
|
+
def test_metadata_requires_a_managed_store_version(self) -> None:
|
|
38
|
+
detector = step_block(self.source, "Detect empty ASC metadata fields")
|
|
39
|
+
whats_new = step_block(self.source, 'Set App Store "What\'s New"')
|
|
40
|
+
|
|
41
|
+
self.assertIn("inputs.manage-app-store-version == 'true'", detector)
|
|
42
|
+
self.assertIn("inputs.manage-app-store-version == 'true'", whats_new)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
unittest.main()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.31
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.31
|
package/package.json
CHANGED
package/src/install.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
statSync,
|
|
17
17
|
writeFileSync,
|
|
18
18
|
} from 'node:fs';
|
|
19
|
-
import { join,
|
|
19
|
+
import { join, resolve } from 'node:path';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
import { dirname } from 'node:path';
|
|
22
22
|
|
|
@@ -182,29 +182,28 @@ function checkGitignore(repoRoot) {
|
|
|
182
182
|
function printGitignoreWarning(result) {
|
|
183
183
|
if (!result.exists || result.offenders.length === 0) return;
|
|
184
184
|
console.log('');
|
|
185
|
-
console.log('
|
|
185
|
+
console.log('NOTICE: .gitignore excludes checkout credentials.');
|
|
186
186
|
console.log(` File: ${result.path}`);
|
|
187
187
|
for (const { lineNumber, text } of result.offenders) {
|
|
188
188
|
console.log(` Line ${lineNumber}: ${text}`);
|
|
189
189
|
}
|
|
190
190
|
console.log('');
|
|
191
|
-
console.log('
|
|
192
|
-
console.log('
|
|
193
|
-
console.log(' (Your repo MUST be private — never do this in a public repo.)');
|
|
191
|
+
console.log(' Keep the ignore when using encrypted Actions secrets (recommended).');
|
|
192
|
+
console.log(' Otherwise the actions still support credentials already tracked in creds/.');
|
|
194
193
|
}
|
|
195
194
|
|
|
196
|
-
function printSummary(
|
|
195
|
+
function printSummary() {
|
|
197
196
|
console.log('');
|
|
198
197
|
console.log('Done.');
|
|
199
198
|
console.log('');
|
|
200
199
|
console.log('Next steps:');
|
|
201
|
-
console.log(
|
|
202
|
-
console.log(
|
|
203
|
-
console.log(
|
|
204
|
-
console.log(
|
|
205
|
-
console.log(
|
|
206
|
-
console.log(
|
|
207
|
-
console.log(
|
|
200
|
+
console.log(' Recommended: configure encrypted Actions secrets:');
|
|
201
|
+
console.log(' ASC_KEY_P8, ASC_KEY_ID, ASC_ISSUER_ID');
|
|
202
|
+
console.log(' ANDROID_UPLOAD_KEY_BASE64, ANDROID_SIGNING_PROPERTIES');
|
|
203
|
+
console.log(' GOOGLE_PLAY_SERVICE_ACCOUNT_JSON');
|
|
204
|
+
console.log(' The workflow materializes them with mode 0600 only on its ephemeral runner.');
|
|
205
|
+
console.log(' Existing checkout credentials remain a backward-compatible fallback.');
|
|
206
|
+
console.log(' Push to main after configuring the secrets; GitHub Actions builds both platforms.');
|
|
208
207
|
console.log('');
|
|
209
208
|
console.log('Re-run this installer anytime to update the vendored action:');
|
|
210
209
|
console.log(' npx --yes gowalk-cicd');
|
|
@@ -242,5 +241,5 @@ export async function runInstall({ dryRun = false } = {}) {
|
|
|
242
241
|
return;
|
|
243
242
|
}
|
|
244
243
|
|
|
245
|
-
printSummary(
|
|
244
|
+
printSummary();
|
|
246
245
|
}
|
package/templates/deploy.yml
CHANGED
|
@@ -35,6 +35,19 @@ jobs:
|
|
|
35
35
|
with:
|
|
36
36
|
fetch-depth: 0
|
|
37
37
|
persist-credentials: true
|
|
38
|
+
- name: Materialize encrypted iOS credentials
|
|
39
|
+
shell: bash
|
|
40
|
+
env:
|
|
41
|
+
CI_ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
|
|
42
|
+
CI_ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
|
43
|
+
CI_ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
|
44
|
+
run: |
|
|
45
|
+
[ -n "${CI_ASC_KEY_P8:-}" ] || exit 0
|
|
46
|
+
: "${CI_ASC_KEY_ID:?ASC_KEY_ID secret is required with ASC_KEY_P8}"
|
|
47
|
+
: "${CI_ASC_ISSUER_ID:?ASC_ISSUER_ID secret is required with ASC_KEY_P8}"
|
|
48
|
+
umask 077
|
|
49
|
+
mkdir -p creds
|
|
50
|
+
printf '%s' "$CI_ASC_KEY_P8" > "creds/AuthKey_${CI_ASC_KEY_ID}_Issuer_${CI_ASC_ISSUER_ID}.p8"
|
|
38
51
|
- name: Detect Flutter
|
|
39
52
|
id: flutter
|
|
40
53
|
shell: bash
|
|
@@ -74,7 +87,8 @@ jobs:
|
|
|
74
87
|
exit 0
|
|
75
88
|
fi
|
|
76
89
|
echo "::add-mask::$GIT_PRIVATE_TOKEN"
|
|
77
|
-
git config --global
|
|
90
|
+
git config --global \
|
|
91
|
+
url."https://x-access-token:${GIT_PRIVATE_TOKEN}@github.com/".insteadOf "https://github.com/"
|
|
78
92
|
echo "Configured authenticated github.com remotes for pub."
|
|
79
93
|
- name: Prepare Flutter iOS project
|
|
80
94
|
if: ${{ steps.flutter.outputs.enabled == 'true' }}
|
|
@@ -99,6 +113,11 @@ jobs:
|
|
|
99
113
|
workspace: ${{ steps.flutter.outputs.enabled == 'true' && 'ios/Runner.xcworkspace' || '' }}
|
|
100
114
|
scheme: ${{ steps.flutter.outputs.enabled == 'true' && 'Runner' || '' }}
|
|
101
115
|
run-tests: 'false' # tests belong to the CI gate, not the deploy path
|
|
116
|
+
# A feature-branch TestFlight build must not create or mutate a live
|
|
117
|
+
# App Store version. That also lets it upload while the current version
|
|
118
|
+
# is locked in review. Default-branch deploys retain metadata automation.
|
|
119
|
+
manage-app-store-version: >-
|
|
120
|
+
${{ github.ref_name == github.event.repository.default_branch && 'true' || 'false' }}
|
|
102
121
|
|
|
103
122
|
android:
|
|
104
123
|
name: Android Google Play
|
|
@@ -107,6 +126,22 @@ jobs:
|
|
|
107
126
|
timeout-minutes: 45
|
|
108
127
|
steps:
|
|
109
128
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
129
|
+
- name: Materialize encrypted Android credentials
|
|
130
|
+
shell: bash
|
|
131
|
+
env:
|
|
132
|
+
CI_ANDROID_KEYSTORE: ${{ secrets.ANDROID_UPLOAD_KEY_BASE64 }}
|
|
133
|
+
CI_ANDROID_PROPERTIES: ${{ secrets.ANDROID_SIGNING_PROPERTIES }}
|
|
134
|
+
CI_PLAY_ACCOUNT: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
|
|
135
|
+
run: |
|
|
136
|
+
[ -n "${CI_ANDROID_KEYSTORE:-}${CI_ANDROID_PROPERTIES:-}${CI_PLAY_ACCOUNT:-}" ] || exit 0
|
|
137
|
+
: "${CI_ANDROID_KEYSTORE:?ANDROID_UPLOAD_KEY_BASE64 secret is required}"
|
|
138
|
+
: "${CI_ANDROID_PROPERTIES:?ANDROID_SIGNING_PROPERTIES secret is required}"
|
|
139
|
+
: "${CI_PLAY_ACCOUNT:?GOOGLE_PLAY_SERVICE_ACCOUNT_JSON secret is required}"
|
|
140
|
+
umask 077
|
|
141
|
+
mkdir -p creds
|
|
142
|
+
printf '%s' "$CI_ANDROID_KEYSTORE" | base64 --decode > creds/android-upload-key.jks
|
|
143
|
+
printf '%s' "$CI_ANDROID_PROPERTIES" > creds/android-signing.properties
|
|
144
|
+
printf '%s' "$CI_PLAY_ACCOUNT" > creds/play-service-account.json
|
|
110
145
|
# Two Android delivery modes:
|
|
111
146
|
# * bitrise — the upload key + Play service account live in Bitrise,
|
|
112
147
|
# not the repo, so the GitHub runner cannot sign. Drop a
|
|
@@ -119,8 +154,9 @@ jobs:
|
|
|
119
154
|
id: mode
|
|
120
155
|
shell: bash
|
|
121
156
|
run: |
|
|
122
|
-
|
|
123
|
-
|
|
157
|
+
bitrise_enabled=$(python3 -c \
|
|
158
|
+
"import json;print(json.load(open('creds/bitrise.json')).get('enabled',False))" 2>/dev/null || true)
|
|
159
|
+
if [ -f creds/bitrise.json ] && [ "$bitrise_enabled" = "True" ]; then
|
|
124
160
|
echo "mode=bitrise" >> "$GITHUB_OUTPUT"
|
|
125
161
|
else
|
|
126
162
|
echo "mode=local" >> "$GITHUB_OUTPUT"
|
|
@@ -225,7 +261,8 @@ jobs:
|
|
|
225
261
|
exit 0
|
|
226
262
|
fi
|
|
227
263
|
echo "::add-mask::$GIT_PRIVATE_TOKEN"
|
|
228
|
-
git config --global
|
|
264
|
+
git config --global \
|
|
265
|
+
url."https://x-access-token:${GIT_PRIVATE_TOKEN}@github.com/".insteadOf "https://github.com/"
|
|
229
266
|
echo "Configured authenticated github.com remotes for pub."
|
|
230
267
|
# A Gradle daemon sized for a workstation does not fit a CI runner. Asking
|
|
231
268
|
# for -Xmx8G with a 4G metaspace reserves 12 GB before the Kotlin compile
|
|
@@ -322,13 +359,8 @@ jobs:
|
|
|
322
359
|
releaseFiles: ${{ steps.android.outputs.bundle-path }}
|
|
323
360
|
tracks: ${{ vars.GOOGLE_PLAY_TRACK || 'internal' }}
|
|
324
361
|
status: ${{ vars.GOOGLE_PLAY_STATUS || 'completed' }}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
# alongside status 'completed' is an error rather than a no-op.
|
|
328
|
-
userFraction: ${{ vars.GOOGLE_PLAY_STATUS == 'inProgress' && (vars.GOOGLE_PLAY_USER_FRACTION || '0.2') || '' }}
|
|
329
|
-
# Localized release notes: commit distribution/whatsnew/whatsnew-<bcp47>
|
|
330
|
-
# files (e.g. whatsnew-en-US, whatsnew-de-DE) and they ship with every
|
|
331
|
-
# release. Empty when the directory is absent — the action skips it.
|
|
362
|
+
userFraction: >-
|
|
363
|
+
${{ vars.GOOGLE_PLAY_STATUS == 'inProgress' && (vars.GOOGLE_PLAY_USER_FRACTION || '0.2') || '' }}
|
|
332
364
|
whatsNewDirectory: ${{ hashFiles('distribution/whatsnew/**') != '' && 'distribution/whatsnew' || '' }}
|
|
333
365
|
|
|
334
366
|
# The retry serves two different failures, and both need a fresh edit:
|
|
@@ -338,14 +370,12 @@ jobs:
|
|
|
338
370
|
# this once an app has changes a human must submit — after a policy
|
|
339
371
|
# rejection, for instance — and it fails the whole upload, so the built
|
|
340
372
|
# bundle never lands at all.
|
|
341
|
-
# Retrying with changesNotSentForReview gets the bundle onto Play either way.
|
|
342
|
-
# In the ordinary case the first attempt already submitted it and this is
|
|
343
|
-
# skipped; in the refused case the bundle is uploaded and waits for someone to
|
|
344
|
-
# press "Send changes for review" in Play Console, which is the only thing
|
|
345
|
-
# Google will not let a tool do.
|
|
346
373
|
- name: Retry Google Play upload without auto-submit
|
|
347
374
|
id: play-upload-retry
|
|
348
|
-
if:
|
|
375
|
+
if: >-
|
|
376
|
+
${{ steps.mode.outputs.mode == 'local'
|
|
377
|
+
&& steps.play.outputs.ready == 'true'
|
|
378
|
+
&& steps.play-upload.outcome == 'failure' }}
|
|
349
379
|
uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5
|
|
350
380
|
with:
|
|
351
381
|
serviceAccountJson: ${{ steps.android.outputs.play-service-account }}
|
|
@@ -353,8 +383,10 @@ jobs:
|
|
|
353
383
|
releaseFiles: ${{ steps.android.outputs.bundle-path }}
|
|
354
384
|
tracks: ${{ vars.GOOGLE_PLAY_TRACK || 'internal' }}
|
|
355
385
|
status: ${{ vars.GOOGLE_PLAY_STATUS || 'completed' }}
|
|
356
|
-
userFraction:
|
|
357
|
-
|
|
386
|
+
userFraction: >-
|
|
387
|
+
${{ vars.GOOGLE_PLAY_STATUS == 'inProgress' && (vars.GOOGLE_PLAY_USER_FRACTION || '0.2') || '' }}
|
|
388
|
+
whatsNewDirectory: >-
|
|
389
|
+
${{ hashFiles('distribution/whatsnew/**') != '' && 'distribution/whatsnew' || '' }}
|
|
358
390
|
changesNotSentForReview: true
|
|
359
391
|
|
|
360
392
|
- name: Say what still needs a human
|