@daemux/store-automator 0.10.73 → 0.10.75
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-plugin/marketplace.json +2 -2
- package/package.json +1 -1
- package/plugins/store-automator/.claude-plugin/plugin.json +1 -1
- package/templates/ci.config.yaml.template +5 -0
- package/templates/github/IOS_NATIVE_CI_SETUP.md +80 -0
- package/templates/github/workflows/deploy.yml +18 -0
- package/templates/ios-native-ci.config.yaml.template +30 -0
- package/templates/scripts/ci/ios-native/cfg_io.py +27 -0
- package/templates/scripts/ci/ios-native/cfg_resolve.py +212 -0
- package/templates/scripts/ci/ios-native/lookup_app_id.py +51 -0
- package/templates/scripts/ci/ios-native/read_config.py +289 -0
- package/templates/github/workflows/ios-native-release.yml +0 -66
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "App Store & Google Play automation for Flutter apps",
|
|
8
|
-
"version": "0.10.
|
|
8
|
+
"version": "0.10.75"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "store-automator",
|
|
13
13
|
"source": "./plugins/store-automator",
|
|
14
14
|
"description": "3 agents for app store publishing: reviewer, meta-creator, media-designer",
|
|
15
|
-
"version": "0.10.
|
|
15
|
+
"version": "0.10.75",
|
|
16
16
|
"keywords": [
|
|
17
17
|
"flutter",
|
|
18
18
|
"app-store",
|
package/package.json
CHANGED
|
@@ -57,3 +57,8 @@ web:
|
|
|
57
57
|
jurisdiction: "State of California, United States"
|
|
58
58
|
app_store_url: ""
|
|
59
59
|
google_play_url: ""
|
|
60
|
+
|
|
61
|
+
# -----------------------------------------------------------------------------
|
|
62
|
+
# For native iOS apps (Swift/SwiftUI without Flutter), use
|
|
63
|
+
# ios-native-ci.config.yaml.template instead of this file.
|
|
64
|
+
# -----------------------------------------------------------------------------
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# iOS Native TestFlight CI — Setup Guide
|
|
2
|
+
|
|
3
|
+
Drop-in TestFlight automation for native Swift/SwiftUI iOS apps. One workflow, one config file, one credential file. All logic lives in the shared composite action `daemux/daemux-plugins/.github/actions/ios-native-testflight`.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- **Private GitHub repo** (holds the ASC API key `.p8` file).
|
|
8
|
+
- **Apple Developer Program** membership with an App Store Connect user that can create API keys.
|
|
9
|
+
- **ASC API Key** with `App Manager` role, downloaded as `AuthKey_<KEY_ID>.p8` (Apple lets you download this exactly once).
|
|
10
|
+
- Xcode project builds locally on macOS 15 / Xcode 16.
|
|
11
|
+
|
|
12
|
+
## Step 1 — Copy the workflow
|
|
13
|
+
|
|
14
|
+
Copy `.github/workflows/deploy.yml` verbatim from this template. No edits required. It triggers on push to `main` and via `workflow_dispatch`.
|
|
15
|
+
|
|
16
|
+
## Step 2 — Copy `ci.config.yaml`
|
|
17
|
+
|
|
18
|
+
Copy `ios-native-ci.config.yaml.template` to your repo root as `ci.config.yaml`. Fill in:
|
|
19
|
+
|
|
20
|
+
- `app.bundle_id` — REQUIRED (must match your App Store Connect app record).
|
|
21
|
+
- `xcode.scheme` — REQUIRED (the shared scheme the CI should archive).
|
|
22
|
+
|
|
23
|
+
Every other field has a sensible default. `xcode.project` auto-detects if there is exactly one `*.xcodeproj` at the repo root. `app.app_store_apple_id` is auto-discovered via the ASC API from the bundle id.
|
|
24
|
+
|
|
25
|
+
## Step 3 — Drop in the ASC API key
|
|
26
|
+
|
|
27
|
+
Rename the downloaded key to include both the key id and the issuer uuid, then put it under `creds/`:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
creds/AuthKey_<KEY_ID>_Issuer_<ISSUER_UUID>.p8
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Example: `creds/AuthKey_5NBDY6YXJ6_Issuer_69a6de77-xxxx-xxxx-xxxx-xxxxxxxxxxxx.p8`.
|
|
34
|
+
|
|
35
|
+
The composite action parses the key id and issuer id from the filename — no GitHub secrets to configure.
|
|
36
|
+
|
|
37
|
+
## Step 4 — Register the app in App Store Connect (one-time)
|
|
38
|
+
|
|
39
|
+
App Store Connect requires a human-in-the-loop 2FA step the first time. Either:
|
|
40
|
+
|
|
41
|
+
- Run `fastlane create_app_ios` locally with `APPLE_ID`, `BUNDLE_ID`, `APP_NAME` exported, or
|
|
42
|
+
- Create the app manually in the App Store Connect web UI (My Apps → + → New App).
|
|
43
|
+
|
|
44
|
+
Subsequent builds need neither — the ASC API key handles everything.
|
|
45
|
+
|
|
46
|
+
## Step 5 — MARKETING_VERSION
|
|
47
|
+
|
|
48
|
+
Leave your Xcode project's `MARKETING_VERSION` at `1.0` initially. CI auto-rolls the marketing version forward (patch bump) whenever the prior version hits `READY_FOR_SALE` in App Store Connect. Until then, the pipeline keeps bumping the build number against the current marketing version.
|
|
49
|
+
|
|
50
|
+
## Step 6 — Push to `main`
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git add .github/workflows/deploy.yml ci.config.yaml creds/AuthKey_*.p8
|
|
54
|
+
git commit -m "ci: add iOS TestFlight pipeline"
|
|
55
|
+
git push origin main
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
GitHub Actions triggers the workflow. On success, the build appears in TestFlight within 5–15 minutes (Apple processing delay).
|
|
59
|
+
|
|
60
|
+
## Troubleshooting
|
|
61
|
+
|
|
62
|
+
| Error | Likely Cause | Fix |
|
|
63
|
+
|-------|--------------|-----|
|
|
64
|
+
| `No ASC key found in creds/` | File missing or misnamed | Filename must match `AuthKey_<KEY_ID>_Issuer_<ISSUER_UUID>.p8` exactly |
|
|
65
|
+
| `No ASC app found for bundle <id>` | App not yet registered in ASC | Run Step 4 (create the app record) |
|
|
66
|
+
| `MARKETING_VERSION not set` | Missing in `.pbxproj` | Set `MARKETING_VERSION = 1.0;` in your target's build settings |
|
|
67
|
+
| Apple refuses new version | Prior version not in `READY_FOR_SALE` | Let the existing version finish review, or manually bump `MARKETING_VERSION` in Xcode |
|
|
68
|
+
| `Scheme not found` | Scheme not shared | In Xcode: Product → Scheme → Manage Schemes → tick "Shared", commit `*.xcscheme` |
|
|
69
|
+
|
|
70
|
+
## Credential rotation
|
|
71
|
+
|
|
72
|
+
ASC API keys can be revoked at any time. Rotate via:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
rm creds/AuthKey_OLD*.p8
|
|
76
|
+
cp ~/Downloads/AuthKey_NEW_*.p8 creds/AuthKey_NEW_Issuer_<ISSUER_UUID>.p8
|
|
77
|
+
git add creds/
|
|
78
|
+
git commit -m "ci: rotate ASC API key"
|
|
79
|
+
git push
|
|
80
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: iOS Deploy
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
paths-ignore: ['**/*.md', '.gitignore']
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ios-deploy-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
deploy:
|
|
14
|
+
runs-on: macos-15
|
|
15
|
+
timeout-minutes: 60
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: daemux/daemux-plugins/.github/actions/ios-native-testflight@main
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# iOS TestFlight CI configuration — read by daemux/daemux-plugins/.github/actions/ios-native-testflight
|
|
2
|
+
# Place this at the repo root as ci.config.yaml.
|
|
3
|
+
# Also place your ASC API key at creds/AuthKey_<KEY_ID>_Issuer_<ISSUER_UUID>.p8
|
|
4
|
+
|
|
5
|
+
app:
|
|
6
|
+
bundle_id: "com.example.myapp" # REQUIRED
|
|
7
|
+
team_id: "ABCDE12345" # optional (used for manual signing)
|
|
8
|
+
app_store_apple_id: "" # optional — auto-discovered via ASC API by bundle_id
|
|
9
|
+
|
|
10
|
+
xcode:
|
|
11
|
+
project: "" # auto-detected if exactly one *.xcodeproj at root
|
|
12
|
+
workspace: "" # wins over project when set
|
|
13
|
+
scheme: "MyApp" # REQUIRED — no safe auto-detect
|
|
14
|
+
configuration: "Release" # default: Release
|
|
15
|
+
profile_name: "" # default: "<scheme> CI"
|
|
16
|
+
|
|
17
|
+
ios:
|
|
18
|
+
uses_non_exempt_encryption: false # false | true | "" to skip write
|
|
19
|
+
|
|
20
|
+
testflight:
|
|
21
|
+
whats_new: |
|
|
22
|
+
- Improved performance
|
|
23
|
+
- Bug fixes
|
|
24
|
+
- Enhanced security
|
|
25
|
+
locale: "en-US"
|
|
26
|
+
|
|
27
|
+
ci:
|
|
28
|
+
run_tests: false
|
|
29
|
+
test_command: "" # optional
|
|
30
|
+
test_destination: "platform=iOS Simulator,name=iPhone 15"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tiny shared IO helpers for ios-native-testflight scripts.
|
|
4
|
+
|
|
5
|
+
Kept deliberately minimal so that read_config.py and lookup_app_id.py can
|
|
6
|
+
import without pulling in heavier dependencies (PyYAML, requests, PyJWT).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def log(msg: str) -> None:
|
|
15
|
+
"""Debug log to stderr (never contains .p8 contents)."""
|
|
16
|
+
print(msg, file=sys.stderr)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def notice(msg: str) -> None:
|
|
20
|
+
"""GitHub Actions workflow-command notice."""
|
|
21
|
+
print(f"::notice::{msg}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def fail(msg: str) -> str:
|
|
25
|
+
"""Emit a GitHub Actions workflow-command error and exit 1."""
|
|
26
|
+
print(f"::error::{msg}", file=sys.stderr)
|
|
27
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Config loading + value-resolution helpers for ios-native-testflight.
|
|
4
|
+
|
|
5
|
+
Pulled out of read_config.py to keep any single file under the 400-line /
|
|
6
|
+
10-function project limits.
|
|
7
|
+
|
|
8
|
+
Contents:
|
|
9
|
+
load_config YAML loader (returns {} if missing)
|
|
10
|
+
emit $GITHUB_ENV writer (handles multi-line via heredoc)
|
|
11
|
+
dig / pick Safe nested dict access
|
|
12
|
+
pick_with_source Flat-path-first, legacy-alias-second lookup
|
|
13
|
+
resolve Apply input -> config -> auto -> default precedence
|
|
14
|
+
auto_project_glob Repo-root *.xcodeproj / *.xcworkspace autodetect
|
|
15
|
+
as_str_bool YAML bool -> "true"/"false"/None
|
|
16
|
+
find_p8 Locate ASC .p8 key in creds/
|
|
17
|
+
lookup_app_id_via_api Subprocess helper — calls lookup_app_id.py
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
import yaml
|
|
29
|
+
|
|
30
|
+
from cfg_io import fail, log, notice
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
P8_RE = re.compile(
|
|
34
|
+
r"^AuthKey_([A-Z0-9]{8,10})(?:_Issuer_([0-9a-fA-F-]{36}))?\.p8$"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_config(workspace: Path, rel_path: str) -> dict:
|
|
39
|
+
"""Load a YAML config from ``workspace/rel_path``. Returns {} if missing."""
|
|
40
|
+
cfg_path = workspace / rel_path
|
|
41
|
+
if not cfg_path.is_file():
|
|
42
|
+
notice(
|
|
43
|
+
f"{rel_path} not found at {cfg_path}; proceeding with inputs and defaults only"
|
|
44
|
+
)
|
|
45
|
+
return {}
|
|
46
|
+
try:
|
|
47
|
+
with cfg_path.open("r") as fh:
|
|
48
|
+
data = yaml.safe_load(fh)
|
|
49
|
+
except yaml.YAMLError as exc:
|
|
50
|
+
fail(f"failed to parse {rel_path}: {exc}")
|
|
51
|
+
if data is None:
|
|
52
|
+
return {}
|
|
53
|
+
if not isinstance(data, dict):
|
|
54
|
+
fail(f"{rel_path} root must be a mapping")
|
|
55
|
+
log(f"loaded config from {cfg_path}")
|
|
56
|
+
return data
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def emit(env_file: Path, name: str, value: str) -> None:
|
|
60
|
+
"""Append NAME=VALUE (or multi-line NAME<<EOF ... EOF) to $GITHUB_ENV."""
|
|
61
|
+
if value is None:
|
|
62
|
+
value = ""
|
|
63
|
+
if "\n" in value:
|
|
64
|
+
delim = "EOF"
|
|
65
|
+
while delim in value:
|
|
66
|
+
delim += "X"
|
|
67
|
+
with env_file.open("a") as fh:
|
|
68
|
+
fh.write(f"{name}<<{delim}\n{value}\n{delim}\n")
|
|
69
|
+
else:
|
|
70
|
+
with env_file.open("a") as fh:
|
|
71
|
+
fh.write(f"{name}={value}\n")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def dig(cfg: dict, *path: str, default: Any = None) -> Any:
|
|
75
|
+
"""Safe nested .get across dict path. Returns default if any key missing."""
|
|
76
|
+
node: Any = cfg
|
|
77
|
+
for key in path:
|
|
78
|
+
if not isinstance(node, dict):
|
|
79
|
+
return default
|
|
80
|
+
node = node.get(key)
|
|
81
|
+
if node is None:
|
|
82
|
+
return default
|
|
83
|
+
return node
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def pick(cfg: dict, *paths: tuple) -> Any:
|
|
87
|
+
"""Return the first non-None/non-empty value among a series of paths."""
|
|
88
|
+
for path in paths:
|
|
89
|
+
val = dig(cfg, *path)
|
|
90
|
+
if val not in (None, ""):
|
|
91
|
+
return val
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def pick_with_source(cfg: dict, flat: tuple, legacy: tuple) -> tuple[Any, str]:
|
|
96
|
+
"""Return (value, source) — flat path wins; legacy alias as fallback."""
|
|
97
|
+
flat_val = dig(cfg, *flat)
|
|
98
|
+
if flat_val not in (None, ""):
|
|
99
|
+
return flat_val, "config"
|
|
100
|
+
legacy_val = dig(cfg, *legacy)
|
|
101
|
+
if legacy_val not in (None, ""):
|
|
102
|
+
return legacy_val, "config(legacy)"
|
|
103
|
+
return None, "config"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def resolve(
|
|
107
|
+
input_val: str,
|
|
108
|
+
cfg_val: Any,
|
|
109
|
+
*,
|
|
110
|
+
cfg_source: str = "config",
|
|
111
|
+
auto_val: str = "",
|
|
112
|
+
default: str = "",
|
|
113
|
+
) -> tuple[str, str]:
|
|
114
|
+
"""Apply precedence: input > config > auto > default > empty."""
|
|
115
|
+
if input_val:
|
|
116
|
+
return input_val, "input"
|
|
117
|
+
if cfg_val not in (None, ""):
|
|
118
|
+
return str(cfg_val), cfg_source
|
|
119
|
+
if auto_val:
|
|
120
|
+
return auto_val, "auto-detect"
|
|
121
|
+
if default:
|
|
122
|
+
return default, "default"
|
|
123
|
+
return "", "empty"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def auto_project_glob(workspace: Path, suffix: str) -> str:
|
|
127
|
+
"""Return the single matching *suffix file at repo root, else empty."""
|
|
128
|
+
matches = sorted(workspace.glob(f"*{suffix}"))
|
|
129
|
+
if len(matches) == 1:
|
|
130
|
+
return matches[0].name
|
|
131
|
+
return ""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def as_str_bool(val: Any) -> str | None:
|
|
135
|
+
"""Normalize YAML bool / string / None to 'true'|'false'|None."""
|
|
136
|
+
if val is None:
|
|
137
|
+
return None
|
|
138
|
+
if isinstance(val, bool):
|
|
139
|
+
return "true" if val else "false"
|
|
140
|
+
s = str(val).strip().lower()
|
|
141
|
+
if s in ("true", "yes", "1"):
|
|
142
|
+
return "true"
|
|
143
|
+
if s in ("false", "no", "0"):
|
|
144
|
+
return "false"
|
|
145
|
+
return s # let downstream validate
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def find_p8(workspace: Path, cfg: dict) -> tuple[Path, str, str | None]:
|
|
149
|
+
"""Locate the ASC .p8 key. Returns (path, key_id, issuer_from_filename)."""
|
|
150
|
+
creds_dir = workspace / "creds"
|
|
151
|
+
matches = sorted(creds_dir.glob("AuthKey_*.p8"))
|
|
152
|
+
if not matches:
|
|
153
|
+
fail(
|
|
154
|
+
"No ASC key found. Place "
|
|
155
|
+
"AuthKey_<KEY_ID>_Issuer_<ISSUER_UUID>.p8 in creds/."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
parsed: list[tuple[Path, str, str | None]] = []
|
|
159
|
+
for p in matches:
|
|
160
|
+
m = P8_RE.match(p.name)
|
|
161
|
+
if not m:
|
|
162
|
+
log(f"skipping {p.name}: does not match AuthKey_<KEY_ID>[_Issuer_<UUID>].p8")
|
|
163
|
+
continue
|
|
164
|
+
parsed.append((p, m.group(1), m.group(2)))
|
|
165
|
+
|
|
166
|
+
if not parsed:
|
|
167
|
+
fail(
|
|
168
|
+
"Found .p8 file(s) in creds/ but none match the required naming "
|
|
169
|
+
"pattern AuthKey_<KEY_ID>[_Issuer_<UUID>].p8"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if len(parsed) == 1:
|
|
173
|
+
return parsed[0]
|
|
174
|
+
|
|
175
|
+
config_key_id = pick(cfg, ("credentials", "apple", "key_id"), ("asc", "key_id"))
|
|
176
|
+
if not config_key_id:
|
|
177
|
+
names = ", ".join(p.name for p, _, _ in parsed)
|
|
178
|
+
fail(
|
|
179
|
+
f"Multiple ASC keys found in creds/ ({names}). "
|
|
180
|
+
"Set credentials.apple.key_id in ci.config.yaml to choose one."
|
|
181
|
+
)
|
|
182
|
+
for entry in parsed:
|
|
183
|
+
if entry[1] == config_key_id:
|
|
184
|
+
return entry
|
|
185
|
+
fail(
|
|
186
|
+
f"credentials.apple.key_id={config_key_id} did not match any .p8 in "
|
|
187
|
+
f"creds/ (found key_ids: {', '.join(k for _, k, _ in parsed)})"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def lookup_app_id_via_api(bundle_id: str, scripts_dir: Path) -> str:
|
|
192
|
+
"""Invoke lookup_app_id.py to resolve apple_id via ASC API."""
|
|
193
|
+
import subprocess
|
|
194
|
+
|
|
195
|
+
script = scripts_dir / "lookup_app_id.py"
|
|
196
|
+
if not script.is_file():
|
|
197
|
+
fail(f"lookup_app_id.py not found at {script}")
|
|
198
|
+
env = dict(os.environ)
|
|
199
|
+
env["BUNDLE_ID"] = bundle_id
|
|
200
|
+
result = subprocess.run(
|
|
201
|
+
[sys.executable, str(script)],
|
|
202
|
+
env=env,
|
|
203
|
+
capture_output=True,
|
|
204
|
+
text=True,
|
|
205
|
+
)
|
|
206
|
+
if result.returncode != 0:
|
|
207
|
+
sys.stderr.write(result.stderr)
|
|
208
|
+
fail(
|
|
209
|
+
f"No ASC app found for bundle {bundle_id}. "
|
|
210
|
+
"Run `fastlane create_app_ios` once manually to register the app."
|
|
211
|
+
)
|
|
212
|
+
return result.stdout.strip()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Resolve the App Store Connect numeric app id for a bundle id.
|
|
4
|
+
|
|
5
|
+
Environment:
|
|
6
|
+
ASC_KEY_ID - API key ID
|
|
7
|
+
ASC_ISSUER_ID - API issuer ID
|
|
8
|
+
ASC_KEY_PATH - Path to the .p8 key file
|
|
9
|
+
BUNDLE_ID - Application bundle identifier
|
|
10
|
+
|
|
11
|
+
Prints the app id to stdout on success. On 0 or >1 matches, exits 1 with a
|
|
12
|
+
guidance message on stderr.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
from asc_common import get_json, make_jwt
|
|
20
|
+
from cfg_io import fail
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def env(name: str) -> str:
|
|
24
|
+
val = os.environ.get(name)
|
|
25
|
+
if not val:
|
|
26
|
+
fail(f"missing env var: {name}")
|
|
27
|
+
return val
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main() -> None:
|
|
31
|
+
bundle_id = env("BUNDLE_ID")
|
|
32
|
+
token = make_jwt(env("ASC_KEY_ID"), env("ASC_ISSUER_ID"), env("ASC_KEY_PATH"))
|
|
33
|
+
data = get_json("/apps", token, params={"filter[bundleId]": bundle_id, "limit": "10"})
|
|
34
|
+
apps = data.get("data", [])
|
|
35
|
+
if not apps:
|
|
36
|
+
fail(
|
|
37
|
+
f"No ASC app found for bundleId={bundle_id}. "
|
|
38
|
+
"Run `fastlane create_app_ios` once manually to register the app, "
|
|
39
|
+
"or set app.app_store_apple_id in ci.config.yaml."
|
|
40
|
+
)
|
|
41
|
+
if len(apps) > 1:
|
|
42
|
+
ids = ", ".join(a.get("id", "?") for a in apps)
|
|
43
|
+
fail(
|
|
44
|
+
f"Multiple ASC apps match bundleId={bundle_id} (ids: {ids}). "
|
|
45
|
+
"Set app.app_store_apple_id in ci.config.yaml to disambiguate."
|
|
46
|
+
)
|
|
47
|
+
print(apps[0]["id"])
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
main()
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Read ci.config.yaml + discover ASC API key in creds/, then emit derived CI
|
|
4
|
+
environment variables to $GITHUB_ENV.
|
|
5
|
+
|
|
6
|
+
Schema (flat — primary, canonical):
|
|
7
|
+
app.{bundle_id, team_id, app_store_apple_id}
|
|
8
|
+
xcode.{project, workspace, scheme, configuration, profile_name}
|
|
9
|
+
ios.{uses_non_exempt_encryption}
|
|
10
|
+
testflight.{whats_new, locale}
|
|
11
|
+
ci.{run_tests, test_command, test_destination}
|
|
12
|
+
credentials.apple.{key_id, issuer_id} OR asc.{key_id, issuer_id}
|
|
13
|
+
|
|
14
|
+
Legacy aliases (accepted for back-compat, never preferred):
|
|
15
|
+
ios.native.{project, workspace, scheme, configuration, profile_name,
|
|
16
|
+
team_id, app_store_apple_id, uses_non_exempt_encryption,
|
|
17
|
+
whats_new, locale, run_tests, test_command, test_destination,
|
|
18
|
+
tests.{run, command, destination}}
|
|
19
|
+
app_store.locale (third-tier fallback only)
|
|
20
|
+
|
|
21
|
+
Precedence for every CFG_* value:
|
|
22
|
+
1. Explicit action input (INPUT_* env var)
|
|
23
|
+
2. Flat-schema config value
|
|
24
|
+
3. Legacy ios.native.* alias
|
|
25
|
+
4. Auto-detected value (xcodeproj/xcworkspace glob) or built-in default
|
|
26
|
+
5. Empty string (composite action step applies its own default / skips)
|
|
27
|
+
|
|
28
|
+
Built-in defaults (emitted when no config/input exists):
|
|
29
|
+
CFG_CONFIGURATION=Release, CFG_USES_NON_EXEMPT=false,
|
|
30
|
+
CFG_RUN_TESTS=false, CFG_LOCALE=en-US, CFG_PROFILE_NAME="<scheme> CI".
|
|
31
|
+
All other CFG_* fields emit empty and rely on action.yml-level fallbacks.
|
|
32
|
+
|
|
33
|
+
The ASC .p8 key is located by globbing ``creds/AuthKey_*.p8`` with filename
|
|
34
|
+
regex AuthKey_<KEY_ID>[_Issuer_<ISSUER_UUID>].p8. Multiple matches require
|
|
35
|
+
``credentials.apple.key_id`` (or ``asc.key_id``) in config to disambiguate.
|
|
36
|
+
|
|
37
|
+
Writes to $GITHUB_ENV:
|
|
38
|
+
ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_P8_PATH, ASC_KEY_P8,
|
|
39
|
+
CFG_PROJECT, CFG_WORKSPACE, CFG_SCHEME, CFG_CONFIGURATION,
|
|
40
|
+
CFG_BUNDLE_ID, CFG_TEAM_ID, CFG_APP_STORE_APPLE_ID, CFG_PROFILE_NAME,
|
|
41
|
+
CFG_USES_NON_EXEMPT, CFG_WHATS_NEW, CFG_LOCALE,
|
|
42
|
+
CFG_RUN_TESTS, CFG_TEST_COMMAND, CFG_TEST_DESTINATION
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import os
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
|
|
50
|
+
from cfg_io import fail, log
|
|
51
|
+
from cfg_resolve import (
|
|
52
|
+
as_str_bool,
|
|
53
|
+
auto_project_glob,
|
|
54
|
+
dig,
|
|
55
|
+
emit,
|
|
56
|
+
find_p8,
|
|
57
|
+
load_config,
|
|
58
|
+
lookup_app_id_via_api,
|
|
59
|
+
pick,
|
|
60
|
+
pick_with_source,
|
|
61
|
+
resolve,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
DEFAULT_WHATS_NEW = (
|
|
66
|
+
"- Improved performance: faster, smoother app experience\n"
|
|
67
|
+
"- Bug fixes: minor issues resolved for seamless use\n"
|
|
68
|
+
"- Enhanced security: updated to protect your data\n"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def emit_credentials(env_file: Path, cfg: dict, workspace: Path) -> dict:
|
|
73
|
+
"""Discover .p8, derive ASC_KEY_ID / ISSUER / PATH, emit to env_file.
|
|
74
|
+
|
|
75
|
+
Returns {'key_id', 'issuer_id', 'key_path'} for downstream use.
|
|
76
|
+
"""
|
|
77
|
+
p8_path, key_id_from_file, issuer_from_file = find_p8(workspace, cfg)
|
|
78
|
+
try:
|
|
79
|
+
p8_contents = p8_path.read_text()
|
|
80
|
+
except OSError as exc:
|
|
81
|
+
fail(f"cannot read {p8_path}: {exc}")
|
|
82
|
+
|
|
83
|
+
cfg_key_id = pick(cfg, ("credentials", "apple", "key_id"), ("asc", "key_id"))
|
|
84
|
+
cfg_issuer = pick(cfg, ("credentials", "apple", "issuer_id"), ("asc", "issuer_id"))
|
|
85
|
+
asc_key_id = cfg_key_id or key_id_from_file
|
|
86
|
+
asc_issuer = cfg_issuer or issuer_from_file
|
|
87
|
+
if not asc_issuer:
|
|
88
|
+
fail(
|
|
89
|
+
"ASC issuer id unknown: neither the filename contains _Issuer_<UUID> "
|
|
90
|
+
"nor credentials.apple.issuer_id is set in ci.config.yaml."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
emit(env_file, "ASC_KEY_ID", str(asc_key_id))
|
|
94
|
+
emit(env_file, "ASC_ISSUER_ID", str(asc_issuer))
|
|
95
|
+
emit(env_file, "ASC_KEY_P8_PATH", str(p8_path))
|
|
96
|
+
emit(env_file, "ASC_KEY_P8", p8_contents)
|
|
97
|
+
log(f"ASC_KEY_ID={asc_key_id} (source: {'config' if cfg_key_id else 'filename'})")
|
|
98
|
+
log(f"ASC_ISSUER_ID={asc_issuer} (source: {'config' if cfg_issuer else 'filename'})")
|
|
99
|
+
log(f"ASC_KEY_P8_PATH={p8_path}")
|
|
100
|
+
return {"key_id": str(asc_key_id), "issuer_id": str(asc_issuer), "key_path": str(p8_path)}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def resolve_xcode(cfg: dict, workspace: Path, inp: dict) -> dict:
|
|
104
|
+
"""Resolve project / workspace / scheme / configuration / profile_name."""
|
|
105
|
+
proj_cfg, proj_src = pick_with_source(cfg, ("xcode", "project"), ("ios", "native", "project"))
|
|
106
|
+
ws_cfg, ws_src = pick_with_source(cfg, ("xcode", "workspace"), ("ios", "native", "workspace"))
|
|
107
|
+
scheme_cfg, scheme_src = pick_with_source(cfg, ("xcode", "scheme"), ("ios", "native", "scheme"))
|
|
108
|
+
config_cfg, config_src = pick_with_source(
|
|
109
|
+
cfg, ("xcode", "configuration"), ("ios", "native", "configuration"),
|
|
110
|
+
)
|
|
111
|
+
profile_cfg, profile_src = pick_with_source(
|
|
112
|
+
cfg, ("xcode", "profile_name"), ("ios", "native", "profile_name"),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
project = resolve(
|
|
116
|
+
inp["PROJECT"], proj_cfg, cfg_source=proj_src,
|
|
117
|
+
auto_val=auto_project_glob(workspace, ".xcodeproj"),
|
|
118
|
+
)
|
|
119
|
+
ws_val = resolve(
|
|
120
|
+
inp["WORKSPACE"], ws_cfg, cfg_source=ws_src,
|
|
121
|
+
auto_val=auto_project_glob(workspace, ".xcworkspace"),
|
|
122
|
+
)
|
|
123
|
+
scheme = resolve(inp["SCHEME"], scheme_cfg, cfg_source=scheme_src)
|
|
124
|
+
configuration = resolve(
|
|
125
|
+
inp["CONFIGURATION"], config_cfg, cfg_source=config_src, default="Release",
|
|
126
|
+
)
|
|
127
|
+
default_profile = f"{scheme[0]} CI" if scheme[0] else ""
|
|
128
|
+
profile_name = resolve(
|
|
129
|
+
inp["PROFILE_NAME"], profile_cfg, cfg_source=profile_src, default=default_profile,
|
|
130
|
+
)
|
|
131
|
+
return {
|
|
132
|
+
"project": project,
|
|
133
|
+
"workspace": ws_val,
|
|
134
|
+
"scheme": scheme,
|
|
135
|
+
"configuration": configuration,
|
|
136
|
+
"profile_name": profile_name,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def resolve_app(cfg: dict, inp: dict, creds: dict, scripts_dir: Path) -> dict:
|
|
141
|
+
"""Resolve bundle_id / team_id / app_store_apple_id (with ASC-API fallback)."""
|
|
142
|
+
bundle_cfg, bundle_src = pick_with_source(cfg, ("app", "bundle_id"), ("ios", "native", "bundle_id"))
|
|
143
|
+
team_cfg, team_src = pick_with_source(cfg, ("app", "team_id"), ("ios", "native", "team_id"))
|
|
144
|
+
apple_cfg, apple_src = pick_with_source(
|
|
145
|
+
cfg, ("app", "app_store_apple_id"), ("ios", "native", "app_store_apple_id"),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
bundle = resolve(inp["BUNDLE_ID"], bundle_cfg, cfg_source=bundle_src)
|
|
149
|
+
team = resolve(inp["TEAM_ID"], team_cfg, cfg_source=team_src)
|
|
150
|
+
|
|
151
|
+
if inp["APP_STORE_APPLE_ID"]:
|
|
152
|
+
apple = (inp["APP_STORE_APPLE_ID"], "input")
|
|
153
|
+
elif apple_cfg not in (None, ""):
|
|
154
|
+
apple = (str(apple_cfg), apple_src)
|
|
155
|
+
elif bundle[0]:
|
|
156
|
+
os.environ["ASC_KEY_ID"] = creds["key_id"]
|
|
157
|
+
os.environ["ASC_ISSUER_ID"] = creds["issuer_id"]
|
|
158
|
+
os.environ["ASC_KEY_PATH"] = creds["key_path"]
|
|
159
|
+
apple = (lookup_app_id_via_api(bundle[0], scripts_dir), "api-lookup")
|
|
160
|
+
else:
|
|
161
|
+
apple = ("", "empty")
|
|
162
|
+
return {"bundle_id": bundle, "team_id": team, "app_store_apple_id": apple}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def resolve_testflight(cfg: dict, inp: dict) -> dict:
|
|
166
|
+
"""Resolve whats_new / locale from testflight.* or legacy aliases."""
|
|
167
|
+
whats_cfg, whats_src = pick_with_source(
|
|
168
|
+
cfg, ("testflight", "whats_new"), ("ios", "native", "whats_new"),
|
|
169
|
+
)
|
|
170
|
+
locale_cfg, locale_src = pick_with_source(
|
|
171
|
+
cfg, ("testflight", "locale"), ("ios", "native", "locale"),
|
|
172
|
+
)
|
|
173
|
+
if locale_cfg in (None, ""):
|
|
174
|
+
alt = dig(cfg, "app_store", "locale")
|
|
175
|
+
if alt not in (None, ""):
|
|
176
|
+
locale_cfg, locale_src = alt, "config(legacy)"
|
|
177
|
+
return {
|
|
178
|
+
"whats_new": resolve(
|
|
179
|
+
inp["WHATS_NEW"], whats_cfg, cfg_source=whats_src, default=DEFAULT_WHATS_NEW,
|
|
180
|
+
),
|
|
181
|
+
"locale": resolve(inp["LOCALE"], locale_cfg, cfg_source=locale_src, default="en-US"),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def resolve_tests(cfg: dict, inp: dict) -> dict:
|
|
186
|
+
"""Resolve run_tests / test_command / test_destination from ci.* or legacy."""
|
|
187
|
+
run_flat = dig(cfg, "ci", "run_tests")
|
|
188
|
+
run_legacy = dig(cfg, "ios", "native", "run_tests")
|
|
189
|
+
if run_legacy is None:
|
|
190
|
+
run_legacy = dig(cfg, "ios", "native", "tests", "run")
|
|
191
|
+
run_cfg, run_src = (run_flat, "config") if run_flat is not None else (run_legacy, "config(legacy)")
|
|
192
|
+
|
|
193
|
+
cmd_flat = dig(cfg, "ci", "test_command")
|
|
194
|
+
cmd_legacy = dig(cfg, "ios", "native", "test_command") or dig(cfg, "ios", "native", "tests", "command")
|
|
195
|
+
cmd_cfg, cmd_src = (
|
|
196
|
+
(cmd_flat, "config") if cmd_flat not in (None, "") else (cmd_legacy, "config(legacy)")
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
dest_flat = dig(cfg, "ci", "test_destination")
|
|
200
|
+
dest_legacy = (
|
|
201
|
+
dig(cfg, "ios", "native", "test_destination")
|
|
202
|
+
or dig(cfg, "ios", "native", "tests", "destination")
|
|
203
|
+
)
|
|
204
|
+
dest_cfg, dest_src = (
|
|
205
|
+
(dest_flat, "config") if dest_flat not in (None, "") else (dest_legacy, "config(legacy)")
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
"run_tests": resolve(
|
|
210
|
+
inp["RUN_TESTS"], as_str_bool(run_cfg), cfg_source=run_src, default="false",
|
|
211
|
+
),
|
|
212
|
+
"test_command": resolve(inp["TEST_COMMAND"], cmd_cfg, cfg_source=cmd_src),
|
|
213
|
+
"test_destination": resolve(inp["TEST_DESTINATION"], dest_cfg, cfg_source=dest_src),
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def resolve_ios(cfg: dict, inp: dict) -> dict:
|
|
218
|
+
"""Resolve ios.uses_non_exempt_encryption (flat) with legacy fallback."""
|
|
219
|
+
val_flat = dig(cfg, "ios", "uses_non_exempt_encryption")
|
|
220
|
+
# Flat lookup can accidentally return the legacy 'native' sub-dict; ignore.
|
|
221
|
+
if isinstance(val_flat, dict):
|
|
222
|
+
val_flat = None
|
|
223
|
+
val_legacy = dig(cfg, "ios", "native", "uses_non_exempt_encryption")
|
|
224
|
+
if val_flat is not None:
|
|
225
|
+
raw, src = val_flat, "config"
|
|
226
|
+
elif val_legacy is not None:
|
|
227
|
+
raw, src = val_legacy, "config(legacy)"
|
|
228
|
+
else:
|
|
229
|
+
raw, src = None, "config"
|
|
230
|
+
return {
|
|
231
|
+
"uses_non_exempt": resolve(
|
|
232
|
+
inp["USES_NON_EXEMPT"], as_str_bool(raw), cfg_source=src, default="false",
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def collect_inputs() -> dict:
|
|
238
|
+
"""Read all INPUT_* env vars the composite action passes in."""
|
|
239
|
+
names = (
|
|
240
|
+
"PROJECT", "WORKSPACE", "SCHEME", "CONFIGURATION", "PROFILE_NAME",
|
|
241
|
+
"BUNDLE_ID", "TEAM_ID", "APP_STORE_APPLE_ID",
|
|
242
|
+
"USES_NON_EXEMPT", "WHATS_NEW", "LOCALE",
|
|
243
|
+
"RUN_TESTS", "TEST_COMMAND", "TEST_DESTINATION",
|
|
244
|
+
)
|
|
245
|
+
return {name: os.environ.get(f"INPUT_{name}", "") for name in names}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def main() -> None:
|
|
249
|
+
workspace = Path(os.environ.get("GITHUB_WORKSPACE", "."))
|
|
250
|
+
scripts_dir = Path(__file__).resolve().parent
|
|
251
|
+
env_file_path = os.environ.get("GITHUB_ENV")
|
|
252
|
+
if not env_file_path:
|
|
253
|
+
fail("GITHUB_ENV not set; this script must run inside a GitHub Actions step")
|
|
254
|
+
env_file = Path(env_file_path)
|
|
255
|
+
|
|
256
|
+
cfg = load_config(workspace, os.environ.get("CONFIG_PATH", "ci.config.yaml"))
|
|
257
|
+
inputs = collect_inputs()
|
|
258
|
+
|
|
259
|
+
creds = emit_credentials(env_file, cfg, workspace)
|
|
260
|
+
xc = resolve_xcode(cfg, workspace, inputs)
|
|
261
|
+
app = resolve_app(cfg, inputs, creds, scripts_dir)
|
|
262
|
+
tf = resolve_testflight(cfg, inputs)
|
|
263
|
+
ios = resolve_ios(cfg, inputs)
|
|
264
|
+
tests = resolve_tests(cfg, inputs)
|
|
265
|
+
|
|
266
|
+
derived = [
|
|
267
|
+
("CFG_PROJECT", xc["project"]),
|
|
268
|
+
("CFG_WORKSPACE", xc["workspace"]),
|
|
269
|
+
("CFG_SCHEME", xc["scheme"]),
|
|
270
|
+
("CFG_CONFIGURATION", xc["configuration"]),
|
|
271
|
+
("CFG_BUNDLE_ID", app["bundle_id"]),
|
|
272
|
+
("CFG_TEAM_ID", app["team_id"]),
|
|
273
|
+
("CFG_APP_STORE_APPLE_ID", app["app_store_apple_id"]),
|
|
274
|
+
("CFG_PROFILE_NAME", xc["profile_name"]),
|
|
275
|
+
("CFG_USES_NON_EXEMPT", ios["uses_non_exempt"]),
|
|
276
|
+
("CFG_WHATS_NEW", tf["whats_new"]),
|
|
277
|
+
("CFG_LOCALE", tf["locale"]),
|
|
278
|
+
("CFG_RUN_TESTS", tests["run_tests"]),
|
|
279
|
+
("CFG_TEST_COMMAND", tests["test_command"]),
|
|
280
|
+
("CFG_TEST_DESTINATION", tests["test_destination"]),
|
|
281
|
+
]
|
|
282
|
+
for name, (value, src) in derived:
|
|
283
|
+
emit(env_file, name, value)
|
|
284
|
+
shown = value if name != "CFG_WHATS_NEW" else value.replace("\n", " / ")
|
|
285
|
+
log(f"{name}={shown!r} (source: {src})")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
if __name__ == "__main__":
|
|
289
|
+
main()
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
name: iOS Deploy
|
|
2
|
-
|
|
3
|
-
# Native-Swift iOS release pipeline. On PRs and non-main branches, runs the
|
|
4
|
-
# simulator test suite (build-only, no signing). On push to main, runs tests,
|
|
5
|
-
# archives the app, and uploads to TestFlight via the App Store Connect API.
|
|
6
|
-
#
|
|
7
|
-
# Required repository secrets (for upload, main branch only):
|
|
8
|
-
# ASC_KEY_ID App Store Connect API key id (e.g. "5NBDY6YXJ6")
|
|
9
|
-
# ASC_ISSUER_ID App Store Connect API issuer id (uuid)
|
|
10
|
-
# ASC_KEY_P8 Full contents of the AuthKey_*.p8 file
|
|
11
|
-
#
|
|
12
|
-
# Edit the `with:` blocks below to match your project.
|
|
13
|
-
|
|
14
|
-
on:
|
|
15
|
-
push:
|
|
16
|
-
branches: [main]
|
|
17
|
-
pull_request:
|
|
18
|
-
branches: [main]
|
|
19
|
-
workflow_dispatch:
|
|
20
|
-
|
|
21
|
-
concurrency:
|
|
22
|
-
group: ios-${{ github.workflow }}-${{ github.ref }}
|
|
23
|
-
cancel-in-progress: false
|
|
24
|
-
|
|
25
|
-
jobs:
|
|
26
|
-
# PRs and non-main pushes: build + test only, no signing or upload.
|
|
27
|
-
build-and-test:
|
|
28
|
-
if: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref != 'refs/heads/main') }}
|
|
29
|
-
runs-on: macos-latest
|
|
30
|
-
timeout-minutes: 45
|
|
31
|
-
steps:
|
|
32
|
-
- uses: actions/checkout@v4
|
|
33
|
-
|
|
34
|
-
- uses: daemux/daemux-plugins/.github/actions/ios-native-testflight@main
|
|
35
|
-
with:
|
|
36
|
-
project: MyApp.xcodeproj
|
|
37
|
-
scheme: MyApp
|
|
38
|
-
bundle-id: com.example.myapp
|
|
39
|
-
team-id: ABCDE12345
|
|
40
|
-
app-store-apple-id: "1234567890"
|
|
41
|
-
run-tests: "true"
|
|
42
|
-
archive: "false"
|
|
43
|
-
upload: "false"
|
|
44
|
-
|
|
45
|
-
# Main branch: test + archive + upload to TestFlight.
|
|
46
|
-
deploy:
|
|
47
|
-
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
|
48
|
-
runs-on: macos-latest
|
|
49
|
-
timeout-minutes: 60
|
|
50
|
-
steps:
|
|
51
|
-
- uses: actions/checkout@v4
|
|
52
|
-
|
|
53
|
-
- uses: daemux/daemux-plugins/.github/actions/ios-native-testflight@main
|
|
54
|
-
with:
|
|
55
|
-
project: MyApp.xcodeproj
|
|
56
|
-
scheme: MyApp
|
|
57
|
-
bundle-id: com.example.myapp
|
|
58
|
-
team-id: ABCDE12345
|
|
59
|
-
app-store-apple-id: "1234567890"
|
|
60
|
-
run-tests: "true"
|
|
61
|
-
archive: "true"
|
|
62
|
-
upload: "true"
|
|
63
|
-
env:
|
|
64
|
-
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
|
65
|
-
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
|
66
|
-
ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
|