Skip to content

Commit 31c168b

Browse files
authored
Merge pull request #1620 from rorkai/cursor/xcode-metadata-inject-0046
Add Xcode deployment metadata injection
2 parents f6c32c6 + ec6fb6c commit 31c168b

8 files changed

Lines changed: 1357 additions & 5 deletions

File tree

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -310,14 +310,15 @@ asc workflow run --dry-run testflight_beta VERSION:1.2.3
310310

311311
### Verified local Xcode -> TestFlight workflow
312312

313-
See [docs/WORKFLOWS.md](docs/WORKFLOWS.md) for a copyable `.asc/workflow.json`
314-
and `ExportOptions.plist` that use `asc builds next-build-number`, `asc xcode archive`,
315-
`asc xcode export --timeout 10m`, and `asc publish testflight --group ... --wait`. Add
316-
`--submit --confirm` when distributing to an external TestFlight group that needs
317-
beta app review submission.
313+
See [docs/WORKFLOWS.md](docs/WORKFLOWS.md) for a copyable `.asc/deployment.json`,
314+
`.asc/workflow.json`, and `ExportOptions.plist` that use `asc builds next-build-number`,
315+
`asc xcode inject`, `asc xcode archive`, `asc xcode export --timeout 10m`, and
316+
`asc publish testflight --group ... --wait`. Add `--submit --confirm` when
317+
distributing to an external TestFlight group that needs beta app review submission.
318318

319319
```bash
320320
asc workflow validate
321+
asc xcode inject --manifest .asc/deployment.json --set version=1.2.3 --set build_number=42 --dry-run --output json
321322
asc workflow run --dry-run testflight_beta VERSION:1.2.3
322323
asc workflow run testflight_beta VERSION:1.2.3
323324
```

docs/COMMANDS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ asc web apps compatibility edit --app "123456789" --ios-app-on-mac=false --ios-a
169169
# Upload a build
170170
asc builds upload --app "123456789" --ipa "/path/to/MyApp.ipa"
171171

172+
# Generate local Xcode metadata before archiving
173+
asc xcode inject --manifest .asc/deployment.json --set version=1.2.3 --set build_number=42 --dry-run --output json
174+
172175
# Stage an App Store version before submission
173176
asc release stage --app "123456789" --version "1.2.3" --build "BUILD_ID" --copy-metadata-from "1.2.2" --dry-run
174177

docs/WORKFLOWS.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ repeatable release pipelines once you know which top-level path you want.
1414
This pattern was validated against a real app using:
1515

1616
- `asc builds next-build-number` to choose the next build number for a version
17+
- `asc xcode inject` to materialize deployment metadata into generated Xcode
18+
plist/config files and asset paths before archiving
1719
- `asc xcode archive` to create a deterministic `.xcarchive`
1820
- `asc xcode export` to create a deterministic `.ipa`
1921
- `asc publish testflight --group ... --wait` to upload, wait for processing,
@@ -40,6 +42,46 @@ Create `.asc/export-options-app-store.plist`:
4042
</plist>
4143
```
4244

45+
Create `.asc/deployment.json`:
46+
47+
```json
48+
{
49+
"values": {
50+
"bundle_id": "com.example.app",
51+
"app_name": "Example",
52+
"version": "",
53+
"build_number": ""
54+
},
55+
"outputs": [
56+
{
57+
"type": "plist",
58+
"path": "../Generated/Info.generated.plist",
59+
"values": {
60+
"CFBundleIdentifier": "${bundle_id}",
61+
"CFBundleDisplayName": "${app_name}",
62+
"CFBundleShortVersionString": "${version}",
63+
"CFBundleVersion": "${build_number}"
64+
}
65+
},
66+
{
67+
"type": "text",
68+
"path": "../Generated/Deployment.xcconfig",
69+
"contents": "PRODUCT_BUNDLE_IDENTIFIER = ${bundle_id}\nMARKETING_VERSION = ${version}\nCURRENT_PROJECT_VERSION = ${build_number}\n"
70+
},
71+
{
72+
"type": "copy",
73+
"source": "../Assets/AppIcon.appiconset/Contents.json",
74+
"path": "../Generated/Assets.xcassets/AppIcon.appiconset/Contents.json"
75+
}
76+
]
77+
}
78+
```
79+
80+
Point your Xcode project at generated files such as `Generated/Info.generated.plist`
81+
or include `Generated/Deployment.xcconfig` from the build configuration. Then run
82+
`asc xcode inject` before archive time to fill in the release-specific values
83+
that previously came from Fastlane scripts.
84+
4385
Create `.asc/workflow.json`:
4486

4587
```json
@@ -68,6 +110,13 @@ Create `.asc/workflow.json`:
68110
"BUILD_NUMBER": "$.nextBuildNumber"
69111
}
70112
},
113+
{
114+
"name": "inject_metadata",
115+
"run": "asc xcode inject --manifest .asc/deployment.json --set version=\"$VERSION\" --set build_number=${steps.resolve_next_build.BUILD_NUMBER} --overwrite --output json",
116+
"outputs": {
117+
"GENERATED_FILES": "$.outputs"
118+
}
119+
},
71120
{
72121
"name": "archive",
73122
"run": "asc xcode archive --project \"$PROJECT_PATH\" --scheme \"$SCHEME\" --configuration \"$CONFIGURATION\" --archive-path \".asc/artifacts/App-$VERSION-${steps.resolve_next_build.BUILD_NUMBER}.xcarchive\" --clean --overwrite --xcodebuild-flag=-destination --xcodebuild-flag=generic/platform=iOS --xcodebuild-flag=-allowProvisioningUpdates --xcodebuild-flag=MARKETING_VERSION=$VERSION --xcodebuild-flag=CURRENT_PROJECT_VERSION=${steps.resolve_next_build.BUILD_NUMBER} --output json",

internal/cli/cmdtest/xcode_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
package cmdtest
22

33
import (
4+
"bytes"
45
"context"
56
"errors"
67
"flag"
78
"io"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
812
"strings"
913
"testing"
14+
15+
rootcmd "github.com/rudrankriyam/App-Store-Connect-CLI/cmd"
1016
)
1117

1218
func TestXcodeCommandExists(t *testing.T) {
@@ -130,6 +136,65 @@ func TestXcodeExportHelpMentionsDirectUploadMode(t *testing.T) {
130136
}
131137
}
132138

139+
func TestXcodeInjectInvalidFlagValuesExitUsage(t *testing.T) {
140+
bin := buildCLIBinary(t)
141+
dir := t.TempDir()
142+
manifestPath := filepath.Join(dir, "deployment.json")
143+
if err := os.WriteFile(manifestPath, []byte(`{
144+
"outputs": [
145+
{"type": "text", "path": "Generated.xcconfig", "contents": "VERSION = ${version}\n"}
146+
]
147+
}`), 0o644); err != nil {
148+
t.Fatalf("WriteFile() manifest error: %v", err)
149+
}
150+
151+
tests := []struct {
152+
name string
153+
args []string
154+
wantStderr string
155+
}{
156+
{
157+
name: "malformed set",
158+
args: []string{"xcode", "inject", "--manifest", manifestPath, "--set", "version"},
159+
wantStderr: "--set values must use key=value",
160+
},
161+
{
162+
name: "invalid dry-run boolean",
163+
args: []string{"xcode", "inject", "--manifest", manifestPath, "--dry-run=maybe"},
164+
wantStderr: `invalid boolean value "maybe" for -dry-run`,
165+
},
166+
{
167+
name: "invalid overwrite boolean",
168+
args: []string{"xcode", "inject", "--manifest", manifestPath, "--overwrite=inject"},
169+
wantStderr: `invalid boolean value "inject" for -overwrite`,
170+
},
171+
}
172+
173+
for _, test := range tests {
174+
t.Run(test.name, func(t *testing.T) {
175+
cmd := exec.Command(bin, test.args...)
176+
var stdout, stderr bytes.Buffer
177+
cmd.Stdout = &stdout
178+
cmd.Stderr = &stderr
179+
180+
err := cmd.Run()
181+
var exitErr *exec.ExitError
182+
if !errors.As(err, &exitErr) {
183+
t.Fatalf("expected exit error, got %v", err)
184+
}
185+
if code := exitErr.ExitCode(); code != rootcmd.ExitUsage {
186+
t.Fatalf("exit code = %d, want %d", code, rootcmd.ExitUsage)
187+
}
188+
if stdout.String() != "" {
189+
t.Fatalf("expected empty stdout, got %q", stdout.String())
190+
}
191+
if !strings.Contains(stderr.String(), test.wantStderr) {
192+
t.Fatalf("expected stderr to contain %q, got %q", test.wantStderr, stderr.String())
193+
}
194+
})
195+
}
196+
}
197+
133198
func TestXcodeValidateHelpMentionsAltool(t *testing.T) {
134199
root := RootCommand("1.2.3")
135200

0 commit comments

Comments
 (0)