@invarn/cibuild 1.3.16 → 1.3.18
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/dist/cli.cjs +1 -1
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +987 -0
- package/dist/src/commands/android-scanner.d.ts +32 -0
- package/dist/src/commands/android-scanner.d.ts.map +1 -0
- package/dist/src/commands/android-scanner.js +667 -0
- package/dist/src/commands/build.d.ts +5 -0
- package/dist/src/commands/build.d.ts.map +1 -0
- package/dist/src/commands/build.js +1096 -0
- package/dist/src/commands/edit.d.ts +3 -0
- package/dist/src/commands/edit.d.ts.map +1 -0
- package/dist/src/commands/edit.js +651 -0
- package/dist/src/commands/file-secret-collector.d.ts +37 -0
- package/dist/src/commands/file-secret-collector.d.ts.map +1 -0
- package/dist/src/commands/file-secret-collector.js +199 -0
- package/dist/src/commands/github-workflow.d.ts +5 -0
- package/dist/src/commands/github-workflow.d.ts.map +1 -0
- package/dist/src/commands/github-workflow.js +45 -0
- package/dist/src/commands/ios-scanner.d.ts +27 -0
- package/dist/src/commands/ios-scanner.d.ts.map +1 -0
- package/dist/src/commands/ios-scanner.js +337 -0
- package/dist/src/commands/reset.d.ts +7 -0
- package/dist/src/commands/reset.d.ts.map +1 -0
- package/dist/src/commands/reset.js +81 -0
- package/dist/src/commands/secrets-sync-workflow.d.ts +15 -0
- package/dist/src/commands/secrets-sync-workflow.d.ts.map +1 -0
- package/dist/src/commands/secrets-sync-workflow.js +255 -0
- package/dist/src/commands/secrets-upload.d.ts +21 -0
- package/dist/src/commands/secrets-upload.d.ts.map +1 -0
- package/dist/src/commands/secrets-upload.js +177 -0
- package/dist/src/commands/secrets-upload.test.d.ts +5 -0
- package/dist/src/commands/secrets-upload.test.d.ts.map +1 -0
- package/dist/src/commands/secrets-upload.test.js +60 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +47 -0
- package/dist/src/envman/cli.d.ts +21 -0
- package/dist/src/envman/cli.d.ts.map +1 -0
- package/dist/src/envman/cli.js +240 -0
- package/dist/src/envman/envman.d.ts +83 -0
- package/dist/src/envman/envman.d.ts.map +1 -0
- package/dist/src/envman/envman.js +361 -0
- package/dist/src/envman/envman.test.d.ts +5 -0
- package/dist/src/envman/envman.test.d.ts.map +1 -0
- package/dist/src/envman/envman.test.js +236 -0
- package/dist/src/envman/index.d.ts +23 -0
- package/dist/src/envman/index.d.ts.map +1 -0
- package/dist/src/envman/index.js +23 -0
- package/dist/src/envman/types.d.ts +55 -0
- package/dist/src/envman/types.d.ts.map +1 -0
- package/dist/src/envman/types.js +12 -0
- package/dist/src/lib.d.ts +27 -0
- package/dist/src/lib.d.ts.map +1 -0
- package/dist/src/lib.js +32 -0
- package/dist/src/pipeline.d.ts +3 -0
- package/dist/src/pipeline.d.ts.map +1 -0
- package/dist/src/pipeline.js +57 -0
- package/dist/src/runner.d.ts +17 -0
- package/dist/src/runner.d.ts.map +1 -0
- package/dist/src/runner.js +234 -0
- package/dist/src/types.d.ts +58 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/yaml/bitrise-compat.d.ts +65 -0
- package/dist/src/yaml/bitrise-compat.d.ts.map +1 -0
- package/dist/src/yaml/bitrise-compat.js +206 -0
- package/dist/src/yaml/bitrise-compat.test.d.ts +5 -0
- package/dist/src/yaml/bitrise-compat.test.d.ts.map +1 -0
- package/dist/src/yaml/bitrise-compat.test.js +347 -0
- package/dist/src/yaml/converter.d.ts +33 -0
- package/dist/src/yaml/converter.d.ts.map +1 -0
- package/dist/src/yaml/converter.js +222 -0
- package/dist/src/yaml/converter.test.d.ts +5 -0
- package/dist/src/yaml/converter.test.d.ts.map +1 -0
- package/dist/src/yaml/converter.test.js +348 -0
- package/dist/src/yaml/e2e.test.d.ts +6 -0
- package/dist/src/yaml/e2e.test.d.ts.map +1 -0
- package/dist/src/yaml/e2e.test.js +446 -0
- package/dist/src/yaml/env-resolver.d.ts +120 -0
- package/dist/src/yaml/env-resolver.d.ts.map +1 -0
- package/dist/src/yaml/env-resolver.js +405 -0
- package/dist/src/yaml/env-resolver.test.d.ts +5 -0
- package/dist/src/yaml/env-resolver.test.d.ts.map +1 -0
- package/dist/src/yaml/env-resolver.test.js +502 -0
- package/dist/src/yaml/interactive-prompts.d.ts +71 -0
- package/dist/src/yaml/interactive-prompts.d.ts.map +1 -0
- package/dist/src/yaml/interactive-prompts.js +258 -0
- package/dist/src/yaml/missing-env-handler.d.ts +45 -0
- package/dist/src/yaml/missing-env-handler.d.ts.map +1 -0
- package/dist/src/yaml/missing-env-handler.js +64 -0
- package/dist/src/yaml/parser.d.ts +33 -0
- package/dist/src/yaml/parser.d.ts.map +1 -0
- package/dist/src/yaml/parser.js +145 -0
- package/dist/src/yaml/pipeline-with-secrets.d.ts +25 -0
- package/dist/src/yaml/pipeline-with-secrets.d.ts.map +1 -0
- package/dist/src/yaml/pipeline-with-secrets.js +76 -0
- package/dist/src/yaml/platform-detector.d.ts +83 -0
- package/dist/src/yaml/platform-detector.d.ts.map +1 -0
- package/dist/src/yaml/platform-detector.js +188 -0
- package/dist/src/yaml/platform-detector.test.d.ts +5 -0
- package/dist/src/yaml/platform-detector.test.d.ts.map +1 -0
- package/dist/src/yaml/platform-detector.test.js +414 -0
- package/dist/src/yaml/preflight-validation.d.ts +40 -0
- package/dist/src/yaml/preflight-validation.d.ts.map +1 -0
- package/dist/src/yaml/preflight-validation.js +152 -0
- package/dist/src/yaml/secrets-manager.d.ts +77 -0
- package/dist/src/yaml/secrets-manager.d.ts.map +1 -0
- package/dist/src/yaml/secrets-manager.js +219 -0
- package/dist/src/yaml/step-validator.d.ts +54 -0
- package/dist/src/yaml/step-validator.d.ts.map +1 -0
- package/dist/src/yaml/step-validator.js +403 -0
- package/dist/src/yaml/steps/android-sign.d.ts +35 -0
- package/dist/src/yaml/steps/android-sign.d.ts.map +1 -0
- package/dist/src/yaml/steps/android-sign.js +147 -0
- package/dist/src/yaml/steps/android-version.d.ts +26 -0
- package/dist/src/yaml/steps/android-version.d.ts.map +1 -0
- package/dist/src/yaml/steps/android-version.js +128 -0
- package/dist/src/yaml/steps/android-version.test.d.ts +5 -0
- package/dist/src/yaml/steps/android-version.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/android-version.test.js +196 -0
- package/dist/src/yaml/steps/android.d.ts +95 -0
- package/dist/src/yaml/steps/android.d.ts.map +1 -0
- package/dist/src/yaml/steps/android.js +916 -0
- package/dist/src/yaml/steps/app-store-deploy.d.ts +48 -0
- package/dist/src/yaml/steps/app-store-deploy.d.ts.map +1 -0
- package/dist/src/yaml/steps/app-store-deploy.js +162 -0
- package/dist/src/yaml/steps/base.d.ts +238 -0
- package/dist/src/yaml/steps/base.d.ts.map +1 -0
- package/dist/src/yaml/steps/base.js +345 -0
- package/dist/src/yaml/steps/bitrise-android-tools.d.ts +26 -0
- package/dist/src/yaml/steps/bitrise-android-tools.d.ts.map +1 -0
- package/dist/src/yaml/steps/bitrise-android-tools.js +198 -0
- package/dist/src/yaml/steps/bitrise-android-tools.test.d.ts +5 -0
- package/dist/src/yaml/steps/bitrise-android-tools.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/bitrise-android-tools.test.js +280 -0
- package/dist/src/yaml/steps/bitrise-apk-info.d.ts +22 -0
- package/dist/src/yaml/steps/bitrise-apk-info.d.ts.map +1 -0
- package/dist/src/yaml/steps/bitrise-apk-info.js +144 -0
- package/dist/src/yaml/steps/bitrise-apk-info.test.d.ts +5 -0
- package/dist/src/yaml/steps/bitrise-apk-info.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/bitrise-apk-info.test.js +331 -0
- package/dist/src/yaml/steps/bitrise-slack.d.ts +49 -0
- package/dist/src/yaml/steps/bitrise-slack.d.ts.map +1 -0
- package/dist/src/yaml/steps/bitrise-slack.js +280 -0
- package/dist/src/yaml/steps/bitrise-slack.test.d.ts +5 -0
- package/dist/src/yaml/steps/bitrise-slack.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/bitrise-slack.test.js +484 -0
- package/dist/src/yaml/steps/bitrise-ssh.d.ts +27 -0
- package/dist/src/yaml/steps/bitrise-ssh.d.ts.map +1 -0
- package/dist/src/yaml/steps/bitrise-ssh.js +134 -0
- package/dist/src/yaml/steps/bitrise-ssh.test.d.ts +5 -0
- package/dist/src/yaml/steps/bitrise-ssh.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/bitrise-ssh.test.js +205 -0
- package/dist/src/yaml/steps/cache.d.ts +52 -0
- package/dist/src/yaml/steps/cache.d.ts.map +1 -0
- package/dist/src/yaml/steps/cache.js +352 -0
- package/dist/src/yaml/steps/fastlane.d.ts +27 -0
- package/dist/src/yaml/steps/fastlane.d.ts.map +1 -0
- package/dist/src/yaml/steps/fastlane.js +79 -0
- package/dist/src/yaml/steps/file.d.ts +27 -0
- package/dist/src/yaml/steps/file.d.ts.map +1 -0
- package/dist/src/yaml/steps/file.js +35 -0
- package/dist/src/yaml/steps/flutter.d.ts +63 -0
- package/dist/src/yaml/steps/flutter.d.ts.map +1 -0
- package/dist/src/yaml/steps/flutter.js +215 -0
- package/dist/src/yaml/steps/git-clone.d.ts +26 -0
- package/dist/src/yaml/steps/git-clone.d.ts.map +1 -0
- package/dist/src/yaml/steps/git-clone.js +111 -0
- package/dist/src/yaml/steps/google-play-deploy.d.ts +37 -0
- package/dist/src/yaml/steps/google-play-deploy.d.ts.map +1 -0
- package/dist/src/yaml/steps/google-play-deploy.js +193 -0
- package/dist/src/yaml/steps/google-play-deploy.test.d.ts +5 -0
- package/dist/src/yaml/steps/google-play-deploy.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/google-play-deploy.test.js +310 -0
- package/dist/src/yaml/steps/index.d.ts +10 -0
- package/dist/src/yaml/steps/index.d.ts.map +1 -0
- package/dist/src/yaml/steps/index.js +1361 -0
- package/dist/src/yaml/steps/ios-deps.d.ts +43 -0
- package/dist/src/yaml/steps/ios-deps.d.ts.map +1 -0
- package/dist/src/yaml/steps/ios-deps.js +141 -0
- package/dist/src/yaml/steps/ios-deps.test.d.ts +5 -0
- package/dist/src/yaml/steps/ios-deps.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/ios-deps.test.js +90 -0
- package/dist/src/yaml/steps/ios-signing.d.ts +31 -0
- package/dist/src/yaml/steps/ios-signing.d.ts.map +1 -0
- package/dist/src/yaml/steps/ios-signing.js +144 -0
- package/dist/src/yaml/steps/ios-version.d.ts +47 -0
- package/dist/src/yaml/steps/ios-version.d.ts.map +1 -0
- package/dist/src/yaml/steps/ios-version.js +151 -0
- package/dist/src/yaml/steps/linting.d.ts +47 -0
- package/dist/src/yaml/steps/linting.d.ts.map +1 -0
- package/dist/src/yaml/steps/linting.js +148 -0
- package/dist/src/yaml/steps/phase2.test.d.ts +6 -0
- package/dist/src/yaml/steps/phase2.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/phase2.test.js +197 -0
- package/dist/src/yaml/steps/phase3.test.d.ts +5 -0
- package/dist/src/yaml/steps/phase3.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/phase3.test.js +144 -0
- package/dist/src/yaml/steps/phase4.test.d.ts +5 -0
- package/dist/src/yaml/steps/phase4.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/phase4.test.js +166 -0
- package/dist/src/yaml/steps/phase5.test.d.ts +6 -0
- package/dist/src/yaml/steps/phase5.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/phase5.test.js +263 -0
- package/dist/src/yaml/steps/registry.d.ts +88 -0
- package/dist/src/yaml/steps/registry.d.ts.map +1 -0
- package/dist/src/yaml/steps/registry.js +125 -0
- package/dist/src/yaml/steps/registry.test.d.ts +5 -0
- package/dist/src/yaml/steps/registry.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/registry.test.js +235 -0
- package/dist/src/yaml/steps/release.d.ts +50 -0
- package/dist/src/yaml/steps/release.d.ts.map +1 -0
- package/dist/src/yaml/steps/release.js +154 -0
- package/dist/src/yaml/steps/script.d.ts +23 -0
- package/dist/src/yaml/steps/script.d.ts.map +1 -0
- package/dist/src/yaml/steps/script.js +63 -0
- package/dist/src/yaml/steps/spec-validation.test.d.ts +6 -0
- package/dist/src/yaml/steps/spec-validation.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/spec-validation.test.js +130 -0
- package/dist/src/yaml/steps/steps.test.d.ts +6 -0
- package/dist/src/yaml/steps/steps.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/steps.test.js +505 -0
- package/dist/src/yaml/steps/test-config.d.ts +3 -0
- package/dist/src/yaml/steps/test-config.d.ts.map +1 -0
- package/dist/src/yaml/steps/test-config.js +17 -0
- package/dist/src/yaml/steps/xcode-new.test.d.ts +5 -0
- package/dist/src/yaml/steps/xcode-new.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/xcode-new.test.js +211 -0
- package/dist/src/yaml/steps/xcode.d.ts +222 -0
- package/dist/src/yaml/steps/xcode.d.ts.map +1 -0
- package/dist/src/yaml/steps/xcode.js +999 -0
- package/dist/src/yaml/types.d.ts +68 -0
- package/dist/src/yaml/types.d.ts.map +1 -0
- package/dist/src/yaml/types.js +5 -0
- package/dist/src/yaml/validation-types.d.ts +96 -0
- package/dist/src/yaml/validation-types.d.ts.map +1 -0
- package/dist/src/yaml/validation-types.js +8 -0
- package/dist/src/yaml/yaml-updater.d.ts +24 -0
- package/dist/src/yaml/yaml-updater.d.ts.map +1 -0
- package/dist/src/yaml/yaml-updater.js +128 -0
- package/package.json +16 -4
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Xcode step implementations (xcodebuild, xcode-test)
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { BaseStepExecutor } from './base.js';
|
|
6
|
+
/**
|
|
7
|
+
* Xcodebuild step executor
|
|
8
|
+
* Builds iOS/macOS projects using xcodebuild
|
|
9
|
+
*/
|
|
10
|
+
export class XcodeBuildStepExecutor extends BaseStepExecutor {
|
|
11
|
+
getValidationRequirements(inputs, _env, _config) {
|
|
12
|
+
const requirements = [];
|
|
13
|
+
// xcodebuild command must exist
|
|
14
|
+
requirements.push(this.requireCommand('xcodebuild', 'Xcode Command Line Tools are required for iOS builds', 'Install Xcode from the App Store, then run: xcode-select --install'));
|
|
15
|
+
// Required inputs
|
|
16
|
+
requirements.push(this.requireInput('project_path', inputs, 'Path to .xcodeproj or .xcworkspace', 'Provide project_path input in YAML'));
|
|
17
|
+
requirements.push(this.requireInput('scheme', inputs, 'Xcode scheme name', 'Provide scheme input in YAML'));
|
|
18
|
+
// Project file - runtime check only if it doesn't already exist locally
|
|
19
|
+
if (inputs.project_path && !existsSync(inputs.project_path)) {
|
|
20
|
+
requirements.push(this.runtimeRequirement(inputs.project_path, 'Xcode project/workspace file', 'file', 'git-clone'));
|
|
21
|
+
}
|
|
22
|
+
return requirements;
|
|
23
|
+
}
|
|
24
|
+
async execute(inputs, env, config) {
|
|
25
|
+
const stepName = 'xcodebuild';
|
|
26
|
+
// Get project path - required
|
|
27
|
+
let projectPath = this.getRequiredInput(inputs, 'project_path', stepName);
|
|
28
|
+
// Resolve path identifiers
|
|
29
|
+
if (projectPath.startsWith('builds:')) {
|
|
30
|
+
projectPath = projectPath.replace('builds:', `${config.paths.buildsDir}/`);
|
|
31
|
+
}
|
|
32
|
+
// Get scheme - required
|
|
33
|
+
const scheme = this.getRequiredInput(inputs, 'scheme', stepName);
|
|
34
|
+
// Get configuration - defaults to Release
|
|
35
|
+
const configuration = this.getInput(inputs, 'configuration', 'Release');
|
|
36
|
+
// Get destination - defaults to generic/platform=iOS
|
|
37
|
+
const destination = this.getInput(inputs, 'destination', 'generic/platform=iOS');
|
|
38
|
+
// Get output directory - defaults to build
|
|
39
|
+
const outputDir = this.getInput(inputs, 'output_dir', 'build');
|
|
40
|
+
// Get clean build flag
|
|
41
|
+
const isCleanBuild = this.getInput(inputs, 'is_clean_build', false);
|
|
42
|
+
// Get xcconfig content - optional
|
|
43
|
+
const xcconfigContent = this.getInput(inputs, 'xcconfig_content', '');
|
|
44
|
+
const commands = [];
|
|
45
|
+
commands.push('# Xcode Build');
|
|
46
|
+
commands.push(`echo "Building project: ${this.escapeBash(projectPath)}"`);
|
|
47
|
+
commands.push(`echo "Scheme: ${this.escapeBash(scheme)}"`);
|
|
48
|
+
commands.push(`echo "Configuration: ${this.escapeBash(configuration)}"`);
|
|
49
|
+
// Create output directory and resolve to absolute path
|
|
50
|
+
// Using an absolute path for CONFIGURATION_BUILD_DIR prevents xcodebuild from
|
|
51
|
+
// creating a 'build' directory inside each SPM package checkout in DerivedData,
|
|
52
|
+
// which causes "File exists but is not a directory" errors on repeated runs.
|
|
53
|
+
commands.push('');
|
|
54
|
+
commands.push('# Create output directory and resolve to absolute path');
|
|
55
|
+
commands.push(`mkdir -p '${this.escapeBash(outputDir)}'`);
|
|
56
|
+
commands.push(`OUTPUT_DIR="$(cd '${this.escapeBash(outputDir)}' && pwd)"`);
|
|
57
|
+
// Create xcconfig file if content provided
|
|
58
|
+
if (xcconfigContent && xcconfigContent.trim() !== '') {
|
|
59
|
+
commands.push('');
|
|
60
|
+
commands.push('# Create custom xcconfig file');
|
|
61
|
+
commands.push('XCCONFIG_FILE=".ci-custom.xcconfig"');
|
|
62
|
+
commands.push('cat > "$XCCONFIG_FILE" << \'EOF\'');
|
|
63
|
+
commands.push(xcconfigContent);
|
|
64
|
+
commands.push('EOF');
|
|
65
|
+
commands.push('echo "Custom xcconfig created"');
|
|
66
|
+
}
|
|
67
|
+
// Determine if workspace or project
|
|
68
|
+
commands.push('');
|
|
69
|
+
commands.push('# Determine project type');
|
|
70
|
+
const escapedPath = this.escapeBash(projectPath);
|
|
71
|
+
commands.push(`if [[ '${escapedPath}' == *.xcworkspace ]]; then`);
|
|
72
|
+
commands.push(' PROJECT_TYPE="-workspace"');
|
|
73
|
+
commands.push('else');
|
|
74
|
+
commands.push(' PROJECT_TYPE="-project"');
|
|
75
|
+
commands.push('fi');
|
|
76
|
+
// Build xcodebuild command
|
|
77
|
+
commands.push('');
|
|
78
|
+
commands.push('# Build xcodebuild command');
|
|
79
|
+
let xcodebuildCmd = 'xcodebuild';
|
|
80
|
+
// Add clean if requested
|
|
81
|
+
if (isCleanBuild) {
|
|
82
|
+
xcodebuildCmd += ' clean';
|
|
83
|
+
}
|
|
84
|
+
xcodebuildCmd += ' build';
|
|
85
|
+
xcodebuildCmd += ` "$PROJECT_TYPE" '${escapedPath}'`;
|
|
86
|
+
xcodebuildCmd += ` -scheme '${this.escapeBash(scheme)}'`;
|
|
87
|
+
xcodebuildCmd += ` -configuration '${this.escapeBash(configuration)}'`;
|
|
88
|
+
xcodebuildCmd += ` -destination '${this.escapeBash(destination)}'`;
|
|
89
|
+
// Add build path (absolute, resolved above)
|
|
90
|
+
xcodebuildCmd += ` CONFIGURATION_BUILD_DIR="$OUTPUT_DIR"`;
|
|
91
|
+
// Add xcconfig if provided
|
|
92
|
+
if (xcconfigContent && xcconfigContent.trim() !== '') {
|
|
93
|
+
xcodebuildCmd += ' -xcconfig "$XCCONFIG_FILE"';
|
|
94
|
+
}
|
|
95
|
+
commands.push(xcodebuildCmd);
|
|
96
|
+
// Show build results
|
|
97
|
+
commands.push('');
|
|
98
|
+
commands.push('# Build completed');
|
|
99
|
+
commands.push('echo "Build completed successfully"');
|
|
100
|
+
commands.push('echo "Build artifacts in: $OUTPUT_DIR"');
|
|
101
|
+
commands.push('ls -lh "$OUTPUT_DIR" || true');
|
|
102
|
+
const script = this.createBashScriptFromCommands(commands, stepName);
|
|
103
|
+
return this.createScriptStep(script, stepName);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Xcode test step executor
|
|
108
|
+
* Runs tests for iOS/macOS projects using xcodebuild test
|
|
109
|
+
*/
|
|
110
|
+
export class XcodeTestStepExecutor extends BaseStepExecutor {
|
|
111
|
+
getValidationRequirements(inputs, _env, _config) {
|
|
112
|
+
const requirements = [];
|
|
113
|
+
// xcodebuild command must exist
|
|
114
|
+
requirements.push(this.requireCommand('xcodebuild', 'Xcode Command Line Tools are required for iOS tests', 'Install Xcode from the App Store, then run: xcode-select --install'));
|
|
115
|
+
// Required inputs
|
|
116
|
+
requirements.push(this.requireInput('project_path', inputs, 'Path to .xcodeproj or .xcworkspace', 'Provide project_path input in YAML'));
|
|
117
|
+
requirements.push(this.requireInput('scheme', inputs, 'Xcode scheme name', 'Provide scheme input in YAML'));
|
|
118
|
+
// Project file - runtime check only if it doesn't already exist locally
|
|
119
|
+
if (inputs.project_path && !existsSync(inputs.project_path)) {
|
|
120
|
+
requirements.push(this.runtimeRequirement(inputs.project_path, 'Xcode project/workspace file', 'file', 'git-clone'));
|
|
121
|
+
}
|
|
122
|
+
return requirements;
|
|
123
|
+
}
|
|
124
|
+
async execute(inputs, env, config) {
|
|
125
|
+
const stepName = 'xcode-test';
|
|
126
|
+
// Get project path - required
|
|
127
|
+
let projectPath = this.getRequiredInput(inputs, 'project_path', stepName);
|
|
128
|
+
// Resolve path identifiers
|
|
129
|
+
if (projectPath.startsWith('builds:')) {
|
|
130
|
+
projectPath = projectPath.replace('builds:', `${config.paths.buildsDir}/`);
|
|
131
|
+
}
|
|
132
|
+
// Get scheme - required
|
|
133
|
+
const scheme = this.getRequiredInput(inputs, 'scheme', stepName);
|
|
134
|
+
// Get destination - defaults to platform=iOS Simulator
|
|
135
|
+
const destination = this.getInput(inputs, 'destination', 'platform=iOS Simulator,name=iPhone 14,OS=latest');
|
|
136
|
+
// Get test plan - optional
|
|
137
|
+
const testPlan = this.getInput(inputs, 'test_plan', '');
|
|
138
|
+
// Get code coverage flag
|
|
139
|
+
const isCodeCoverageEnabled = this.getInput(inputs, 'is_code_coverage_enabled', false);
|
|
140
|
+
const commands = [];
|
|
141
|
+
commands.push('# Xcode Test');
|
|
142
|
+
commands.push(`echo "Testing project: ${this.escapeBash(projectPath)}"`);
|
|
143
|
+
commands.push(`echo "Scheme: ${this.escapeBash(scheme)}"`);
|
|
144
|
+
// Determine if workspace or project
|
|
145
|
+
commands.push('');
|
|
146
|
+
commands.push('# Determine project type');
|
|
147
|
+
const escapedPath = this.escapeBash(projectPath);
|
|
148
|
+
commands.push(`if [[ '${escapedPath}' == *.xcworkspace ]]; then`);
|
|
149
|
+
commands.push(' PROJECT_TYPE="-workspace"');
|
|
150
|
+
commands.push('else');
|
|
151
|
+
commands.push(' PROJECT_TYPE="-project"');
|
|
152
|
+
commands.push('fi');
|
|
153
|
+
// Build xcodebuild test command
|
|
154
|
+
commands.push('');
|
|
155
|
+
commands.push('# Run tests');
|
|
156
|
+
let xcodebuildCmd = 'xcodebuild test';
|
|
157
|
+
xcodebuildCmd += ` "$PROJECT_TYPE" '${escapedPath}'`;
|
|
158
|
+
xcodebuildCmd += ` -scheme '${this.escapeBash(scheme)}'`;
|
|
159
|
+
xcodebuildCmd += ` -destination '${this.escapeBash(destination)}'`;
|
|
160
|
+
// Add test plan if specified
|
|
161
|
+
if (testPlan && testPlan.trim() !== '') {
|
|
162
|
+
xcodebuildCmd += ` -testPlan '${this.escapeBash(testPlan)}'`;
|
|
163
|
+
}
|
|
164
|
+
// Add code coverage if enabled
|
|
165
|
+
if (isCodeCoverageEnabled) {
|
|
166
|
+
xcodebuildCmd += ' -enableCodeCoverage YES';
|
|
167
|
+
}
|
|
168
|
+
commands.push(xcodebuildCmd);
|
|
169
|
+
// Show test results
|
|
170
|
+
commands.push('');
|
|
171
|
+
commands.push('# Tests completed');
|
|
172
|
+
commands.push('echo "Tests completed successfully"');
|
|
173
|
+
const script = this.createBashScriptFromCommands(commands, stepName);
|
|
174
|
+
return this.createScriptStep(script, stepName);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* xcode-archive step executor
|
|
179
|
+
* Archives and exports an iOS app to IPA, mirroring bitrise xcode-archive@5.
|
|
180
|
+
*/
|
|
181
|
+
export class XcodeArchiveStepExecutor extends BaseStepExecutor {
|
|
182
|
+
getValidationRequirements(inputs, _env, _config) {
|
|
183
|
+
const requirements = [];
|
|
184
|
+
requirements.push(this.requireCommand('xcodebuild', 'Xcode Command Line Tools are required for iOS builds', 'Install Xcode from the App Store, then run: xcode-select --install'));
|
|
185
|
+
requirements.push(this.requireInput('project_path', inputs, 'Path to .xcodeproj or .xcworkspace', 'Provide project_path input in YAML'));
|
|
186
|
+
requirements.push(this.requireInput('scheme', inputs, 'Xcode scheme name', 'Provide scheme input in YAML'));
|
|
187
|
+
// Project file - runtime check only if it doesn't already exist locally
|
|
188
|
+
if (inputs.project_path && !existsSync(inputs.project_path)) {
|
|
189
|
+
requirements.push(this.runtimeRequirement(inputs.project_path, 'Xcode project/workspace file', 'file', 'git-clone'));
|
|
190
|
+
}
|
|
191
|
+
return requirements;
|
|
192
|
+
}
|
|
193
|
+
getOutputs() {
|
|
194
|
+
return [
|
|
195
|
+
{ name: 'CIBUILD_IPA_PATH', type: 'environment', description: 'Path to the exported IPA file' },
|
|
196
|
+
{ name: 'CIBUILD_XCARCHIVE_PATH', type: 'environment', description: 'Path to the generated .xcarchive' },
|
|
197
|
+
{ name: 'CIBUILD_DSYM_PATH', type: 'environment', description: 'Path to the zipped dSYM files' },
|
|
198
|
+
{ name: 'CIBUILD_APP_PATH', type: 'environment', description: 'Path to the .app inside the xcarchive (when code signing is off)' },
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
async execute(inputs, env, config) {
|
|
202
|
+
const stepName = 'xcode-archive';
|
|
203
|
+
let projectPath = this.getRequiredInput(inputs, 'project_path', stepName);
|
|
204
|
+
if (projectPath.startsWith('builds:')) {
|
|
205
|
+
projectPath = projectPath.replace('builds:', `${config.paths.buildsDir}/`);
|
|
206
|
+
}
|
|
207
|
+
const scheme = this.getRequiredInput(inputs, 'scheme', stepName);
|
|
208
|
+
const configuration = this.getInput(inputs, 'configuration', 'Release');
|
|
209
|
+
const platform = this.getInput(inputs, 'platform', 'iOS');
|
|
210
|
+
const distributionMethod = this.getInput(inputs, 'distribution_method', 'development');
|
|
211
|
+
const performClean = this.getInput(inputs, 'perform_clean_action', 'no');
|
|
212
|
+
const compileBitcode = this.getInput(inputs, 'compile_bitcode', 'yes');
|
|
213
|
+
const uploadBitcode = this.getInput(inputs, 'upload_bitcode', 'yes');
|
|
214
|
+
const automaticCodeSigning = this.getInput(inputs, 'automatic_code_signing', 'off');
|
|
215
|
+
const xcodebuildOptions = this.getInput(inputs, 'xcodebuild_options', '');
|
|
216
|
+
const xcconfigContent = this.getInput(inputs, 'xcconfig_content', 'COMPILER_INDEX_STORE_ENABLE = NO');
|
|
217
|
+
const exportOptionsPlistContent = this.getInput(inputs, 'export_options_plist_content', '');
|
|
218
|
+
const outputDir = this.getInput(inputs, 'output_dir', 'build/ipa');
|
|
219
|
+
const artifactName = this.getInput(inputs, 'artifact_name', '');
|
|
220
|
+
const escapedPath = this.escapeBash(projectPath);
|
|
221
|
+
const escapedScheme = this.escapeBash(scheme);
|
|
222
|
+
const escapedConfig = this.escapeBash(configuration);
|
|
223
|
+
const commands = [];
|
|
224
|
+
commands.push('# xcode-archive — archive and export iOS app to IPA');
|
|
225
|
+
commands.push(`echo "Project: ${escapedPath}"`);
|
|
226
|
+
commands.push(`echo "Scheme: ${escapedScheme}"`);
|
|
227
|
+
commands.push(`echo "Configuration: ${escapedConfig}"`);
|
|
228
|
+
commands.push(`echo "Distribution method: ${this.escapeBash(distributionMethod)}"`);
|
|
229
|
+
commands.push('');
|
|
230
|
+
// Determine project type
|
|
231
|
+
commands.push('# Determine project type (-workspace vs -project)');
|
|
232
|
+
commands.push(`if [[ '${escapedPath}' == *.xcworkspace ]]; then`);
|
|
233
|
+
commands.push(' PROJECT_TYPE="-workspace"');
|
|
234
|
+
commands.push('else');
|
|
235
|
+
commands.push(' PROJECT_TYPE="-project"');
|
|
236
|
+
commands.push('fi');
|
|
237
|
+
commands.push('');
|
|
238
|
+
// Derive artifact name
|
|
239
|
+
if (artifactName) {
|
|
240
|
+
commands.push(`ARTIFACT_NAME="${this.escapeBash(artifactName)}"`);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
commands.push(`ARTIFACT_NAME="${escapedScheme}"`);
|
|
244
|
+
}
|
|
245
|
+
// Paths
|
|
246
|
+
commands.push(`OUTPUT_DIR="${this.escapeBash(outputDir)}"`);
|
|
247
|
+
commands.push('ARCHIVE_PATH="$OUTPUT_DIR/archive/${ARTIFACT_NAME}.xcarchive"');
|
|
248
|
+
commands.push('mkdir -p "$(dirname "$ARCHIVE_PATH")"');
|
|
249
|
+
commands.push('mkdir -p "$OUTPUT_DIR"');
|
|
250
|
+
commands.push('');
|
|
251
|
+
// Write xcconfig override
|
|
252
|
+
commands.push('# Write xcconfig overrides');
|
|
253
|
+
commands.push('XCCONFIG_PATH="$(mktemp -t cibuild-xcconfig).xcconfig"');
|
|
254
|
+
// When automatic_code_signing is off (default), disable code signing so
|
|
255
|
+
// archives succeed on CI without a development team / provisioning profile.
|
|
256
|
+
let effectiveXcconfig = xcconfigContent.trim() || 'COMPILER_INDEX_STORE_ENABLE = NO';
|
|
257
|
+
if (automaticCodeSigning === 'off') {
|
|
258
|
+
effectiveXcconfig += '\nCODE_SIGNING_ALLOWED = NO';
|
|
259
|
+
effectiveXcconfig += '\nCODE_SIGN_IDENTITY = -';
|
|
260
|
+
}
|
|
261
|
+
commands.push(`cat > "$XCCONFIG_PATH" << 'XCCONFIG_EOF'`);
|
|
262
|
+
commands.push(effectiveXcconfig);
|
|
263
|
+
commands.push('XCCONFIG_EOF');
|
|
264
|
+
commands.push('');
|
|
265
|
+
// Build xcodebuild archive command
|
|
266
|
+
commands.push('# Run xcodebuild archive');
|
|
267
|
+
let archiveCmd = 'xcodebuild';
|
|
268
|
+
if (performClean === 'yes')
|
|
269
|
+
archiveCmd += ' clean';
|
|
270
|
+
archiveCmd += ' archive';
|
|
271
|
+
archiveCmd += ` "$PROJECT_TYPE" '${escapedPath}'`;
|
|
272
|
+
archiveCmd += ` -scheme '${escapedScheme}'`;
|
|
273
|
+
archiveCmd += ` -configuration '${escapedConfig}'`;
|
|
274
|
+
archiveCmd += ` -destination 'generic/platform=${this.escapeBash(platform)}'`;
|
|
275
|
+
archiveCmd += ' -archivePath "$ARCHIVE_PATH"';
|
|
276
|
+
archiveCmd += ' -xcconfig "$XCCONFIG_PATH"';
|
|
277
|
+
// When code signing is off, also pass settings as command-line build
|
|
278
|
+
// settings so they take highest priority and override any target-level
|
|
279
|
+
// signing configuration in the Xcode project.
|
|
280
|
+
if (automaticCodeSigning === 'off') {
|
|
281
|
+
archiveCmd += ' CODE_SIGNING_ALLOWED=NO CODE_SIGN_IDENTITY=- CODE_SIGNING_REQUIRED=NO';
|
|
282
|
+
}
|
|
283
|
+
if (xcodebuildOptions) {
|
|
284
|
+
archiveCmd += ` ${xcodebuildOptions}`;
|
|
285
|
+
}
|
|
286
|
+
commands.push(archiveCmd);
|
|
287
|
+
commands.push('');
|
|
288
|
+
commands.push('echo "✓ Archive created: $ARCHIVE_PATH"');
|
|
289
|
+
commands.push('');
|
|
290
|
+
// Write output env var helper (used in both signing and non-signing paths)
|
|
291
|
+
commands.push('# Export output environment variables');
|
|
292
|
+
commands.push('ENVSTORE_FILE="${ENVMAN_ENVSTORE_PATH:-.ci/.envstore.json}"');
|
|
293
|
+
commands.push('export_env() {');
|
|
294
|
+
commands.push(' local key="$1" val="$2"');
|
|
295
|
+
commands.push(' if [ -f "$ENVSTORE_FILE" ]; then');
|
|
296
|
+
commands.push(' node -e "');
|
|
297
|
+
commands.push(' const fs=require(\'fs\');');
|
|
298
|
+
commands.push(' const s=JSON.parse(fs.readFileSync(process.argv[1],\'utf-8\'));');
|
|
299
|
+
commands.push(' s.envs=s.envs||[];');
|
|
300
|
+
commands.push(' s.envs=s.envs.filter(e=>e.key!==process.argv[2]);');
|
|
301
|
+
commands.push(' s.envs.push({key:process.argv[2],value:process.argv[3]});');
|
|
302
|
+
commands.push(' fs.writeFileSync(process.argv[1],JSON.stringify(s));');
|
|
303
|
+
commands.push(' " "$ENVSTORE_FILE" "$key" "$val" 2>/dev/null || true');
|
|
304
|
+
commands.push(' fi');
|
|
305
|
+
commands.push(' export "$key=$val"');
|
|
306
|
+
commands.push(' echo " $key=$val"');
|
|
307
|
+
commands.push('}');
|
|
308
|
+
commands.push('');
|
|
309
|
+
if (automaticCodeSigning === 'off') {
|
|
310
|
+
// Signing is disabled — skip IPA export (unsigned archives cannot be exported).
|
|
311
|
+
// Output the xcarchive path and the .app inside it instead.
|
|
312
|
+
commands.push('# Code signing disabled — export xcarchive path only');
|
|
313
|
+
commands.push('export_env CIBUILD_XCARCHIVE_PATH "$(pwd)/$ARCHIVE_PATH"');
|
|
314
|
+
commands.push('');
|
|
315
|
+
commands.push('# Locate .app inside the xcarchive for downstream use');
|
|
316
|
+
commands.push('APP_PATH=$(find "$ARCHIVE_PATH/Products" -name "*.app" -maxdepth 3 2>/dev/null | head -1 || true)');
|
|
317
|
+
commands.push('[ -n "$APP_PATH" ] && export_env CIBUILD_APP_PATH "$(pwd)/$APP_PATH" || true');
|
|
318
|
+
commands.push('');
|
|
319
|
+
commands.push('echo "✓ xcode-archive complete (unsigned — code signing disabled)"');
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
// Signing enabled — generate ExportOptions.plist and export to IPA
|
|
323
|
+
commands.push('# Generate ExportOptions.plist');
|
|
324
|
+
commands.push('EXPORT_PLIST="$(mktemp -t ExportOptions).plist"');
|
|
325
|
+
if (exportOptionsPlistContent.trim()) {
|
|
326
|
+
commands.push(`cat > "$EXPORT_PLIST" << 'PLIST_EOF'`);
|
|
327
|
+
commands.push(exportOptionsPlistContent);
|
|
328
|
+
commands.push('PLIST_EOF');
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
// Map deprecated "development" method name to "debugging" (Apple renamed it in Xcode 15)
|
|
332
|
+
const exportMethod = distributionMethod === 'development' ? 'debugging' : distributionMethod;
|
|
333
|
+
const compileBitcodeVal = compileBitcode === 'yes' ? 'true' : 'false';
|
|
334
|
+
const uploadBitcodeVal = uploadBitcode === 'yes' ? 'true' : 'false';
|
|
335
|
+
const signingStyle = 'automatic';
|
|
336
|
+
commands.push(`cat > "$EXPORT_PLIST" << 'PLIST_EOF'`);
|
|
337
|
+
commands.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
338
|
+
commands.push('<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">');
|
|
339
|
+
commands.push('<plist version="1.0">');
|
|
340
|
+
commands.push('<dict>');
|
|
341
|
+
commands.push(' <key>method</key>');
|
|
342
|
+
commands.push(` <string>${exportMethod}</string>`);
|
|
343
|
+
commands.push(' <key>signingStyle</key>');
|
|
344
|
+
commands.push(` <string>${signingStyle}</string>`);
|
|
345
|
+
if (exportMethod !== 'app-store') {
|
|
346
|
+
commands.push(' <key>compileBitcode</key>');
|
|
347
|
+
commands.push(` <${compileBitcodeVal}/>`);
|
|
348
|
+
}
|
|
349
|
+
if (exportMethod === 'app-store') {
|
|
350
|
+
commands.push(' <key>uploadBitcode</key>');
|
|
351
|
+
commands.push(` <${uploadBitcodeVal}/>`);
|
|
352
|
+
}
|
|
353
|
+
commands.push('</dict>');
|
|
354
|
+
commands.push('</plist>');
|
|
355
|
+
commands.push('PLIST_EOF');
|
|
356
|
+
}
|
|
357
|
+
commands.push('');
|
|
358
|
+
// Export archive to IPA
|
|
359
|
+
commands.push('# Export archive to IPA');
|
|
360
|
+
let exportCmd = 'xcodebuild -exportArchive';
|
|
361
|
+
exportCmd += ' -archivePath "$ARCHIVE_PATH"';
|
|
362
|
+
exportCmd += ' -exportOptionsPlist "$EXPORT_PLIST"';
|
|
363
|
+
exportCmd += ' -exportPath "$OUTPUT_DIR"';
|
|
364
|
+
commands.push(exportCmd);
|
|
365
|
+
commands.push('');
|
|
366
|
+
// Locate generated IPA
|
|
367
|
+
commands.push('# Locate generated IPA and export paths');
|
|
368
|
+
commands.push('IPA_PATH=$(find "$OUTPUT_DIR" -name "*.ipa" | head -1)');
|
|
369
|
+
commands.push('DSYM_PATH=$(find "$OUTPUT_DIR" -name "*.dSYM.zip" 2>/dev/null | head -1 || true)');
|
|
370
|
+
commands.push('');
|
|
371
|
+
commands.push('if [ -z "$IPA_PATH" ]; then');
|
|
372
|
+
commands.push(' echo "Error: IPA not found in $OUTPUT_DIR" >&2');
|
|
373
|
+
commands.push(' exit 1');
|
|
374
|
+
commands.push('fi');
|
|
375
|
+
commands.push('');
|
|
376
|
+
commands.push('echo "✓ IPA created: $IPA_PATH"');
|
|
377
|
+
commands.push('ls -lh "$IPA_PATH"');
|
|
378
|
+
commands.push('');
|
|
379
|
+
commands.push('export_env CIBUILD_IPA_PATH "$IPA_PATH"');
|
|
380
|
+
commands.push('export_env CIBUILD_XCARCHIVE_PATH "$(pwd)/$ARCHIVE_PATH"');
|
|
381
|
+
commands.push('[ -n "$DSYM_PATH" ] && export_env CIBUILD_DSYM_PATH "$DSYM_PATH" || true');
|
|
382
|
+
commands.push('');
|
|
383
|
+
commands.push('echo "✓ xcode-archive complete"');
|
|
384
|
+
commands.push('rm -f "$EXPORT_PLIST"');
|
|
385
|
+
}
|
|
386
|
+
commands.push('rm -f "$XCCONFIG_PATH"');
|
|
387
|
+
const script = this.createBashScriptFromCommands(commands, stepName);
|
|
388
|
+
return this.createScriptStep(script, stepName);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* iOS Archive step executor
|
|
393
|
+
* Creates an IPA file from a .app bundle
|
|
394
|
+
*/
|
|
395
|
+
export class IosArchiveStepExecutor extends BaseStepExecutor {
|
|
396
|
+
getValidationRequirements(inputs, _env, _config) {
|
|
397
|
+
const requirements = [];
|
|
398
|
+
// Required input
|
|
399
|
+
requirements.push(this.requireInput('app_path', inputs, 'Path to .app bundle', 'Provide app_path input pointing to your built .app file'));
|
|
400
|
+
// zip command for creating IPA
|
|
401
|
+
requirements.push(this.requireCommand('zip', 'zip utility for creating IPA files', 'zip should be pre-installed on macOS'));
|
|
402
|
+
// .app file - runtime check (created by xcodebuild)
|
|
403
|
+
if (inputs.app_path) {
|
|
404
|
+
requirements.push(this.runtimeRequirement(inputs.app_path, '.app bundle file', 'directory', 'xcodebuild'));
|
|
405
|
+
}
|
|
406
|
+
return requirements;
|
|
407
|
+
}
|
|
408
|
+
async execute(inputs, env, config) {
|
|
409
|
+
const stepName = 'ios-archive';
|
|
410
|
+
// Get app path - required
|
|
411
|
+
let appPath = this.getRequiredInput(inputs, 'app_path', stepName);
|
|
412
|
+
// Resolve path identifiers
|
|
413
|
+
if (appPath.startsWith('builds:')) {
|
|
414
|
+
appPath = appPath.replace('builds:', `${config.paths.buildsDir}/`);
|
|
415
|
+
}
|
|
416
|
+
// Get output path - defaults to artifacts/
|
|
417
|
+
const outputPath = this.getInput(inputs, 'output_path', 'artifacts');
|
|
418
|
+
// Get output name - optional, will derive from .app name if not provided
|
|
419
|
+
const outputName = this.getInput(inputs, 'output_name', '');
|
|
420
|
+
const commands = [];
|
|
421
|
+
commands.push('# iOS Archive - Create IPA');
|
|
422
|
+
commands.push(`echo "Creating IPA from: ${this.escapeBash(appPath)}"`);
|
|
423
|
+
// Check if .app exists
|
|
424
|
+
commands.push('');
|
|
425
|
+
commands.push('# Verify .app bundle exists');
|
|
426
|
+
commands.push(`if [ ! -d '${this.escapeBash(appPath)}' ]; then`);
|
|
427
|
+
commands.push(` echo "Error: .app bundle not found at ${this.escapeBash(appPath)}"`);
|
|
428
|
+
commands.push(' exit 1');
|
|
429
|
+
commands.push('fi');
|
|
430
|
+
// Determine output IPA name
|
|
431
|
+
commands.push('');
|
|
432
|
+
commands.push('# Determine IPA name');
|
|
433
|
+
if (outputName && outputName.trim() !== '') {
|
|
434
|
+
// Use provided name
|
|
435
|
+
const cleanName = outputName.replace(/\.ipa$/, '');
|
|
436
|
+
commands.push(`IPA_NAME="${this.escapeBash(cleanName)}"`);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
// Derive from .app bundle name
|
|
440
|
+
commands.push(`APP_BUNDLE='${this.escapeBash(appPath)}'`);
|
|
441
|
+
commands.push('IPA_NAME=$(basename "$APP_BUNDLE" .app)');
|
|
442
|
+
}
|
|
443
|
+
commands.push('echo "IPA name: $IPA_NAME.ipa"');
|
|
444
|
+
// Create IPA
|
|
445
|
+
commands.push('');
|
|
446
|
+
commands.push('# Create IPA structure');
|
|
447
|
+
commands.push('TEMP_DIR=$(mktemp -d)');
|
|
448
|
+
commands.push('mkdir -p "$TEMP_DIR/Payload"');
|
|
449
|
+
commands.push('');
|
|
450
|
+
commands.push('# Copy .app to Payload directory');
|
|
451
|
+
commands.push(`cp -r '${this.escapeBash(appPath)}' "$TEMP_DIR/Payload/"`);
|
|
452
|
+
commands.push('');
|
|
453
|
+
commands.push('# Create IPA (zip with .ipa extension)');
|
|
454
|
+
commands.push('cd "$TEMP_DIR"');
|
|
455
|
+
commands.push(`zip -r "$IPA_NAME.ipa" Payload -q`);
|
|
456
|
+
commands.push('cd - > /dev/null');
|
|
457
|
+
commands.push('');
|
|
458
|
+
commands.push('# Create output directory and move IPA');
|
|
459
|
+
commands.push(`mkdir -p '${this.escapeBash(outputPath)}'`);
|
|
460
|
+
commands.push(`mv "$TEMP_DIR/$IPA_NAME.ipa" '${this.escapeBash(outputPath)}/'`);
|
|
461
|
+
commands.push('');
|
|
462
|
+
commands.push('# Cleanup temp directory');
|
|
463
|
+
commands.push('rm -rf "$TEMP_DIR"');
|
|
464
|
+
commands.push('');
|
|
465
|
+
commands.push('# Show result');
|
|
466
|
+
commands.push('echo "✓ IPA created successfully"');
|
|
467
|
+
commands.push(`ls -lh '${this.escapeBash(outputPath)}'/"$IPA_NAME.ipa"`);
|
|
468
|
+
const script = this.createBashScriptFromCommands(commands, stepName);
|
|
469
|
+
return this.createScriptStep(script, stepName);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* OTA install step executor.
|
|
474
|
+
* Generates manifest.plist and a QR code for itms-services:// OTA installation.
|
|
475
|
+
* The IPA and manifest must be hosted at user-provided HTTPS URLs.
|
|
476
|
+
*/
|
|
477
|
+
export class OtaInstallStepExecutor extends BaseStepExecutor {
|
|
478
|
+
getValidationRequirements(inputs, _env, _config) {
|
|
479
|
+
return [
|
|
480
|
+
this.requireInput('ipa_url', inputs, 'HTTPS URL where the IPA file will be hosted', 'Provide the full HTTPS URL to where the IPA will be uploaded'),
|
|
481
|
+
this.requireInput('bundle_id', inputs, 'iOS bundle identifier (e.g. com.example.app)'),
|
|
482
|
+
this.requireInput('bundle_version', inputs, 'App version string (e.g. 1.2.3)'),
|
|
483
|
+
this.requireInput('title', inputs, 'App display name shown during install'),
|
|
484
|
+
];
|
|
485
|
+
}
|
|
486
|
+
getOutputs() {
|
|
487
|
+
return [
|
|
488
|
+
{
|
|
489
|
+
name: 'CIBUILD_PUBLIC_INSTALL_PAGE_QR_CODE_IMAGE_URL',
|
|
490
|
+
type: 'environment',
|
|
491
|
+
description: 'QR code image URL for the OTA install page (api.qrserver.com link)',
|
|
492
|
+
},
|
|
493
|
+
];
|
|
494
|
+
}
|
|
495
|
+
async execute(inputs, _env, _config) {
|
|
496
|
+
const stepName = 'ota-install';
|
|
497
|
+
const ipaUrl = this.getRequiredInput(inputs, 'ipa_url', stepName);
|
|
498
|
+
const bundleId = this.getRequiredInput(inputs, 'bundle_id', stepName);
|
|
499
|
+
const bundleVersion = this.getRequiredInput(inputs, 'bundle_version', stepName);
|
|
500
|
+
const title = this.getRequiredInput(inputs, 'title', stepName);
|
|
501
|
+
const outputDir = this.getInput(inputs, 'output_dir', '.ci/artifacts');
|
|
502
|
+
const manifestUrlInput = this.getInput(inputs, 'manifest_url', '');
|
|
503
|
+
const escapedIpaUrl = this.escapeBash(ipaUrl);
|
|
504
|
+
const escapedBundleId = this.escapeBash(bundleId);
|
|
505
|
+
const escapedBundleVersion = this.escapeBash(bundleVersion);
|
|
506
|
+
const escapedTitle = this.escapeBash(title);
|
|
507
|
+
const escapedOutputDir = this.escapeBash(outputDir);
|
|
508
|
+
const commands = [];
|
|
509
|
+
commands.push('# ota-install — generate OTA manifest + QR code');
|
|
510
|
+
commands.push(`IPA_URL='${escapedIpaUrl}'`);
|
|
511
|
+
commands.push(`BUNDLE_ID='${escapedBundleId}'`);
|
|
512
|
+
commands.push(`BUNDLE_VERSION='${escapedBundleVersion}'`);
|
|
513
|
+
commands.push(`TITLE='${escapedTitle}'`);
|
|
514
|
+
commands.push(`OUTPUT_DIR='${escapedOutputDir}'`);
|
|
515
|
+
commands.push('');
|
|
516
|
+
// Derive manifest URL if not provided
|
|
517
|
+
if (manifestUrlInput.trim()) {
|
|
518
|
+
commands.push(`MANIFEST_URL='${this.escapeBash(manifestUrlInput)}'`);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
commands.push('# Derive manifest URL from IPA URL (same directory, manifest.plist filename)');
|
|
522
|
+
commands.push('MANIFEST_URL="$(dirname "$IPA_URL")/manifest.plist"');
|
|
523
|
+
}
|
|
524
|
+
commands.push('');
|
|
525
|
+
// Build OTA link
|
|
526
|
+
commands.push('OTA_URL="itms-services://?action=download-manifest&url=${MANIFEST_URL}"');
|
|
527
|
+
commands.push('');
|
|
528
|
+
// Create output dir and write manifest.plist
|
|
529
|
+
commands.push('mkdir -p "$OUTPUT_DIR"');
|
|
530
|
+
commands.push('');
|
|
531
|
+
commands.push('# Write manifest.plist');
|
|
532
|
+
commands.push('cat > "$OUTPUT_DIR/manifest.plist" << PLIST_EOF');
|
|
533
|
+
commands.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
534
|
+
commands.push('<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">');
|
|
535
|
+
commands.push('<plist version="1.0">');
|
|
536
|
+
commands.push('<dict>');
|
|
537
|
+
commands.push(' <key>items</key>');
|
|
538
|
+
commands.push(' <array>');
|
|
539
|
+
commands.push(' <dict>');
|
|
540
|
+
commands.push(' <key>assets</key>');
|
|
541
|
+
commands.push(' <array>');
|
|
542
|
+
commands.push(' <dict>');
|
|
543
|
+
commands.push(' <key>kind</key><string>software-package</string>');
|
|
544
|
+
commands.push(' <key>url</key><string>${IPA_URL}</string>');
|
|
545
|
+
commands.push(' </dict>');
|
|
546
|
+
commands.push(' </array>');
|
|
547
|
+
commands.push(' <key>metadata</key>');
|
|
548
|
+
commands.push(' <dict>');
|
|
549
|
+
commands.push(' <key>bundle-identifier</key><string>${BUNDLE_ID}</string>');
|
|
550
|
+
commands.push(' <key>bundle-version</key><string>${BUNDLE_VERSION}</string>');
|
|
551
|
+
commands.push(' <key>kind</key><string>software</string>');
|
|
552
|
+
commands.push(' <key>title</key><string>${TITLE}</string>');
|
|
553
|
+
commands.push(' </dict>');
|
|
554
|
+
commands.push(' </dict>');
|
|
555
|
+
commands.push(' </array>');
|
|
556
|
+
commands.push('</dict>');
|
|
557
|
+
commands.push('</plist>');
|
|
558
|
+
commands.push('PLIST_EOF');
|
|
559
|
+
commands.push('');
|
|
560
|
+
commands.push('echo "✓ manifest.plist written to $OUTPUT_DIR/manifest.plist"');
|
|
561
|
+
commands.push('');
|
|
562
|
+
// Generate QR code (qrencode preferred; graceful fallback)
|
|
563
|
+
commands.push('# Generate QR code');
|
|
564
|
+
commands.push('if command -v qrencode &>/dev/null; then');
|
|
565
|
+
commands.push(' echo ""');
|
|
566
|
+
commands.push(' echo "Scan to install:"');
|
|
567
|
+
commands.push(' qrencode -t ansiutf8 "$OTA_URL"');
|
|
568
|
+
commands.push('else');
|
|
569
|
+
commands.push(' echo "ℹ Install qrencode for terminal QR: brew install qrencode"');
|
|
570
|
+
commands.push('fi');
|
|
571
|
+
commands.push('');
|
|
572
|
+
// Print summary
|
|
573
|
+
commands.push('echo ""');
|
|
574
|
+
commands.push('echo "─────────────────────────────────────────"');
|
|
575
|
+
commands.push('echo "OTA Install Summary"');
|
|
576
|
+
commands.push('echo "─────────────────────────────────────────"');
|
|
577
|
+
commands.push('echo "IPA URL: $IPA_URL"');
|
|
578
|
+
commands.push('echo "Manifest URL: $MANIFEST_URL"');
|
|
579
|
+
commands.push('echo "OTA Link: $OTA_URL"');
|
|
580
|
+
commands.push('echo ""');
|
|
581
|
+
commands.push('echo "Host the IPA and manifest.plist at the URLs above, then share the OTA link."');
|
|
582
|
+
commands.push('echo "─────────────────────────────────────────"');
|
|
583
|
+
commands.push('');
|
|
584
|
+
// Export QR image URL to envstore (api.qrserver.com image link, usable in notifications)
|
|
585
|
+
commands.push('# Export QR image URL to envstore for downstream steps (e.g. Slack notifications)');
|
|
586
|
+
commands.push('ENCODED_OTA="$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$OTA_URL" 2>/dev/null || printf "%s" "$OTA_URL" | sed \'s/ /%20/g; s/:/%3A/g; s|/|%2F|g; s/?/%3F/g; s/=/%3D/g; s/&/%26/g\')"');
|
|
587
|
+
commands.push('QR_IMAGE_URL="https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${ENCODED_OTA}"');
|
|
588
|
+
commands.push('');
|
|
589
|
+
commands.push('ENVSTORE_FILE="${ENVMAN_ENVSTORE_PATH:-.ci/.envstore.json}"');
|
|
590
|
+
commands.push('if [ -f "$ENVSTORE_FILE" ]; then');
|
|
591
|
+
commands.push(' node -e "');
|
|
592
|
+
commands.push(' const fs=require(\'fs\');');
|
|
593
|
+
commands.push(' const s=JSON.parse(fs.readFileSync(process.argv[1],\'utf-8\'));');
|
|
594
|
+
commands.push(' s.envs=s.envs||[];');
|
|
595
|
+
commands.push(' s.envs=s.envs.filter(e=>e.key!==\'CIBUILD_PUBLIC_INSTALL_PAGE_QR_CODE_IMAGE_URL\');');
|
|
596
|
+
commands.push(' s.envs.push({key:\'CIBUILD_PUBLIC_INSTALL_PAGE_QR_CODE_IMAGE_URL\',value:process.argv[2]});');
|
|
597
|
+
commands.push(' fs.writeFileSync(process.argv[1],JSON.stringify(s));');
|
|
598
|
+
commands.push(' " "$ENVSTORE_FILE" "$QR_IMAGE_URL" 2>/dev/null || true');
|
|
599
|
+
commands.push('fi');
|
|
600
|
+
commands.push('export CIBUILD_PUBLIC_INSTALL_PAGE_QR_CODE_IMAGE_URL="$QR_IMAGE_URL"');
|
|
601
|
+
const script = this.createBashScriptFromCommands(commands, stepName);
|
|
602
|
+
return this.createScriptStep(script, stepName);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Builds the app and tests without executing them.
|
|
607
|
+
* Produces an .xctestrun file for use with xcode-test-without-building.
|
|
608
|
+
*/
|
|
609
|
+
export class XcodeBuildForTestStepExecutor extends BaseStepExecutor {
|
|
610
|
+
getValidationRequirements(inputs, _env, _config) {
|
|
611
|
+
const requirements = [];
|
|
612
|
+
requirements.push(this.requireCommand('xcodebuild', 'Xcode Command Line Tools are required', 'Install Xcode, then run: xcode-select --install'));
|
|
613
|
+
requirements.push(this.requireInput('project_path', inputs, 'Path to .xcodeproj or .xcworkspace'));
|
|
614
|
+
requirements.push(this.requireInput('scheme', inputs, 'Xcode scheme name'));
|
|
615
|
+
if (inputs.project_path && !existsSync(inputs.project_path)) {
|
|
616
|
+
requirements.push(this.runtimeRequirement(inputs.project_path, 'Xcode project/workspace file', 'file', 'git-clone'));
|
|
617
|
+
}
|
|
618
|
+
return requirements;
|
|
619
|
+
}
|
|
620
|
+
getOutputs() {
|
|
621
|
+
return [
|
|
622
|
+
{ name: 'CIBUILD_TEST_BUNDLE_PATH', type: 'environment', description: 'Directory containing the built test bundle' },
|
|
623
|
+
{ name: 'CIBUILD_XCTESTRUN_FILE_PATH', type: 'environment', description: 'Path to the generated .xctestrun file' },
|
|
624
|
+
];
|
|
625
|
+
}
|
|
626
|
+
async execute(inputs, _env, _config) {
|
|
627
|
+
const stepName = 'xcode-build-for-test';
|
|
628
|
+
const projectPath = this.getRequiredInput(inputs, 'project_path', stepName);
|
|
629
|
+
const scheme = this.getRequiredInput(inputs, 'scheme', stepName);
|
|
630
|
+
const configuration = this.getInput(inputs, 'configuration', 'Debug');
|
|
631
|
+
const destination = this.getInput(inputs, 'destination', 'generic/platform=iOS Simulator');
|
|
632
|
+
const testPlan = this.getInput(inputs, 'test_plan', '');
|
|
633
|
+
const xcconfigContent = this.getInput(inputs, 'xcconfig_content', '');
|
|
634
|
+
const xcodebuildOptions = this.getInput(inputs, 'xcodebuild_options', '');
|
|
635
|
+
const outputDir = this.getInput(inputs, 'output_dir', 'build');
|
|
636
|
+
const escapedPath = this.escapeBash(projectPath);
|
|
637
|
+
const escapedScheme = this.escapeBash(scheme);
|
|
638
|
+
const commands = [];
|
|
639
|
+
commands.push('# xcode-build-for-test — build-for-testing');
|
|
640
|
+
commands.push(`echo "Building for testing: ${escapedPath}"`);
|
|
641
|
+
commands.push(`echo "Scheme: ${escapedScheme}"`);
|
|
642
|
+
commands.push('');
|
|
643
|
+
// Output directory
|
|
644
|
+
commands.push(`mkdir -p '${this.escapeBash(outputDir)}'`);
|
|
645
|
+
commands.push(`OUTPUT_DIR="$(cd '${this.escapeBash(outputDir)}' && pwd)"`);
|
|
646
|
+
commands.push('');
|
|
647
|
+
// xcconfig
|
|
648
|
+
if (xcconfigContent.trim()) {
|
|
649
|
+
commands.push('XCCONFIG_FILE="$(mktemp -t cibuild-xcconfig).xcconfig"');
|
|
650
|
+
commands.push(`cat > "$XCCONFIG_FILE" << 'XCCONFIG_EOF'`);
|
|
651
|
+
commands.push(xcconfigContent);
|
|
652
|
+
commands.push('XCCONFIG_EOF');
|
|
653
|
+
commands.push('');
|
|
654
|
+
}
|
|
655
|
+
// Determine project type
|
|
656
|
+
commands.push(`if [[ '${escapedPath}' == *.xcworkspace ]]; then`);
|
|
657
|
+
commands.push(' PROJECT_TYPE="-workspace"');
|
|
658
|
+
commands.push('else');
|
|
659
|
+
commands.push(' PROJECT_TYPE="-project"');
|
|
660
|
+
commands.push('fi');
|
|
661
|
+
commands.push('');
|
|
662
|
+
// Build command
|
|
663
|
+
let cmd = 'xcodebuild build-for-testing';
|
|
664
|
+
cmd += ` "$PROJECT_TYPE" '${escapedPath}'`;
|
|
665
|
+
cmd += ` -scheme '${escapedScheme}'`;
|
|
666
|
+
cmd += ` -configuration '${this.escapeBash(configuration)}'`;
|
|
667
|
+
cmd += ` -destination '${this.escapeBash(destination)}'`;
|
|
668
|
+
cmd += ` -derivedDataPath "$OUTPUT_DIR/DerivedData"`;
|
|
669
|
+
if (testPlan) {
|
|
670
|
+
cmd += ` -testPlan '${this.escapeBash(testPlan)}'`;
|
|
671
|
+
}
|
|
672
|
+
if (xcconfigContent.trim()) {
|
|
673
|
+
cmd += ' -xcconfig "$XCCONFIG_FILE"';
|
|
674
|
+
}
|
|
675
|
+
if (xcodebuildOptions) {
|
|
676
|
+
cmd += ` ${xcodebuildOptions}`;
|
|
677
|
+
}
|
|
678
|
+
commands.push(cmd);
|
|
679
|
+
commands.push('');
|
|
680
|
+
// Locate .xctestrun file
|
|
681
|
+
commands.push('# Locate generated .xctestrun file');
|
|
682
|
+
commands.push('XCTESTRUN_PATH=$(find "$OUTPUT_DIR/DerivedData/Build/Products" -name "*.xctestrun" | head -1)');
|
|
683
|
+
commands.push('if [ -z "$XCTESTRUN_PATH" ]; then');
|
|
684
|
+
commands.push(' echo "❌ Error: .xctestrun file not found"');
|
|
685
|
+
commands.push(' exit 1');
|
|
686
|
+
commands.push('fi');
|
|
687
|
+
commands.push('');
|
|
688
|
+
commands.push('TEST_BUNDLE_DIR="$(dirname "$XCTESTRUN_PATH")"');
|
|
689
|
+
commands.push('echo "✅ Build for testing complete"');
|
|
690
|
+
commands.push('echo "xctestrun: $XCTESTRUN_PATH"');
|
|
691
|
+
commands.push('');
|
|
692
|
+
// Export env vars
|
|
693
|
+
commands.push('envman add --key CIBUILD_TEST_BUNDLE_PATH --value "$TEST_BUNDLE_DIR"');
|
|
694
|
+
commands.push('envman add --key CIBUILD_XCTESTRUN_FILE_PATH --value "$XCTESTRUN_PATH"');
|
|
695
|
+
if (xcconfigContent.trim()) {
|
|
696
|
+
commands.push('rm -f "$XCCONFIG_FILE"');
|
|
697
|
+
}
|
|
698
|
+
const script = this.createBashScriptFromCommands(commands, stepName);
|
|
699
|
+
return this.createScriptStep(script, stepName);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Runs pre-compiled tests from an .xctestrun file.
|
|
704
|
+
*/
|
|
705
|
+
export class XcodeTestWithoutBuildingStepExecutor extends BaseStepExecutor {
|
|
706
|
+
getValidationRequirements(_inputs, _env, _config) {
|
|
707
|
+
return [
|
|
708
|
+
this.requireCommand('xcodebuild', 'Xcode Command Line Tools are required', 'Install Xcode, then run: xcode-select --install'),
|
|
709
|
+
];
|
|
710
|
+
}
|
|
711
|
+
getOutputs() {
|
|
712
|
+
return [
|
|
713
|
+
{ name: 'CIBUILD_XCRESULT_PATH', type: 'environment', description: 'Path to the .xcresult test result bundle' },
|
|
714
|
+
];
|
|
715
|
+
}
|
|
716
|
+
async execute(inputs, _env, _config) {
|
|
717
|
+
const stepName = 'xcode-test-without-building';
|
|
718
|
+
const xctestrun = this.getInput(inputs, 'xctestrun', '$CIBUILD_XCTESTRUN_FILE_PATH');
|
|
719
|
+
const destination = this.getInput(inputs, 'destination', 'platform=iOS Simulator,name=iPhone 15,OS=latest');
|
|
720
|
+
const onlyTesting = this.getInput(inputs, 'only_testing', '');
|
|
721
|
+
const skipTesting = this.getInput(inputs, 'skip_testing', '');
|
|
722
|
+
const repetitionMode = this.getInput(inputs, 'test_repetition_mode', 'none');
|
|
723
|
+
const maxRepetitions = this.getInput(inputs, 'maximum_test_repetitions', '3');
|
|
724
|
+
const xcodebuildOptions = this.getInput(inputs, 'xcodebuild_options', '');
|
|
725
|
+
const commands = [];
|
|
726
|
+
commands.push('# xcode-test-without-building');
|
|
727
|
+
commands.push('echo "Running pre-built tests..."');
|
|
728
|
+
commands.push('');
|
|
729
|
+
// Resolve xctestrun path
|
|
730
|
+
commands.push(`XCTESTRUN_FILE="${xctestrun}"`);
|
|
731
|
+
commands.push('if [ -z "$XCTESTRUN_FILE" ]; then');
|
|
732
|
+
commands.push(' echo "❌ Error: xctestrun file path is empty. Run xcode-build-for-test first."');
|
|
733
|
+
commands.push(' exit 1');
|
|
734
|
+
commands.push('fi');
|
|
735
|
+
commands.push('if [ ! -f "$XCTESTRUN_FILE" ]; then');
|
|
736
|
+
commands.push(' echo "❌ Error: xctestrun file not found: $XCTESTRUN_FILE"');
|
|
737
|
+
commands.push(' exit 1');
|
|
738
|
+
commands.push('fi');
|
|
739
|
+
commands.push('echo "xctestrun: $XCTESTRUN_FILE"');
|
|
740
|
+
commands.push('');
|
|
741
|
+
// Result bundle path
|
|
742
|
+
commands.push('RESULT_BUNDLE_PATH="build/test-results/$(date +%Y%m%d_%H%M%S).xcresult"');
|
|
743
|
+
commands.push('mkdir -p "$(dirname "$RESULT_BUNDLE_PATH")"');
|
|
744
|
+
commands.push('');
|
|
745
|
+
// Build command
|
|
746
|
+
let cmd = 'xcodebuild test-without-building';
|
|
747
|
+
cmd += ' -xctestrun "$XCTESTRUN_FILE"';
|
|
748
|
+
cmd += ` -destination '${this.escapeBash(destination)}'`;
|
|
749
|
+
cmd += ' -resultBundlePath "$RESULT_BUNDLE_PATH"';
|
|
750
|
+
// Test filtering
|
|
751
|
+
if (onlyTesting) {
|
|
752
|
+
for (const test of onlyTesting.split('\n').filter(Boolean)) {
|
|
753
|
+
cmd += ` -only-testing:'${this.escapeBash(test.trim())}'`;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (skipTesting) {
|
|
757
|
+
for (const test of skipTesting.split('\n').filter(Boolean)) {
|
|
758
|
+
cmd += ` -skip-testing:'${this.escapeBash(test.trim())}'`;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// Repetition
|
|
762
|
+
if (repetitionMode && repetitionMode !== 'none') {
|
|
763
|
+
if (repetitionMode === 'until_failure') {
|
|
764
|
+
cmd += ' -run-tests-until-failure';
|
|
765
|
+
}
|
|
766
|
+
else if (repetitionMode === 'retry_on_failure') {
|
|
767
|
+
cmd += ' -retry-tests-on-failure';
|
|
768
|
+
}
|
|
769
|
+
cmd += ` -test-iterations ${maxRepetitions}`;
|
|
770
|
+
}
|
|
771
|
+
if (xcodebuildOptions) {
|
|
772
|
+
cmd += ` ${xcodebuildOptions}`;
|
|
773
|
+
}
|
|
774
|
+
commands.push(cmd);
|
|
775
|
+
commands.push('');
|
|
776
|
+
commands.push('echo "✅ Tests completed"');
|
|
777
|
+
commands.push('echo "Result bundle: $RESULT_BUNDLE_PATH"');
|
|
778
|
+
commands.push('');
|
|
779
|
+
// Export
|
|
780
|
+
commands.push('envman add --key CIBUILD_XCRESULT_PATH --value "$RESULT_BUNDLE_PATH"');
|
|
781
|
+
const script = this.createBashScriptFromCommands(commands, stepName);
|
|
782
|
+
return this.createScriptStep(script, stepName);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Builds an iOS/tvOS/watchOS app for the simulator.
|
|
787
|
+
* Produces a .app bundle with code signing disabled by default.
|
|
788
|
+
*/
|
|
789
|
+
export class XcodeBuildForSimulatorStepExecutor extends BaseStepExecutor {
|
|
790
|
+
getValidationRequirements(inputs, _env, _config) {
|
|
791
|
+
const requirements = [];
|
|
792
|
+
requirements.push(this.requireCommand('xcodebuild', 'Xcode Command Line Tools are required', 'Install Xcode, then run: xcode-select --install'));
|
|
793
|
+
requirements.push(this.requireInput('project_path', inputs, 'Path to .xcodeproj or .xcworkspace'));
|
|
794
|
+
requirements.push(this.requireInput('scheme', inputs, 'Xcode scheme name'));
|
|
795
|
+
if (inputs.project_path && !existsSync(inputs.project_path)) {
|
|
796
|
+
requirements.push(this.runtimeRequirement(inputs.project_path, 'Xcode project/workspace file', 'file', 'git-clone'));
|
|
797
|
+
}
|
|
798
|
+
return requirements;
|
|
799
|
+
}
|
|
800
|
+
getOutputs() {
|
|
801
|
+
return [
|
|
802
|
+
{ name: 'CIBUILD_APP_DIR_PATH', type: 'environment', description: 'Path to the generated .app directory' },
|
|
803
|
+
];
|
|
804
|
+
}
|
|
805
|
+
async execute(inputs, _env, _config) {
|
|
806
|
+
const stepName = 'xcode-build-for-simulator';
|
|
807
|
+
const projectPath = this.getRequiredInput(inputs, 'project_path', stepName);
|
|
808
|
+
const scheme = this.getRequiredInput(inputs, 'scheme', stepName);
|
|
809
|
+
const configuration = this.getInput(inputs, 'configuration', '');
|
|
810
|
+
const destination = this.getInput(inputs, 'destination', 'generic/platform=iOS Simulator');
|
|
811
|
+
const performClean = this.getInput(inputs, 'perform_clean_action', 'no');
|
|
812
|
+
const xcconfigContent = this.getInput(inputs, 'xcconfig_content', 'CODE_SIGNING_ALLOWED=NO');
|
|
813
|
+
const xcodebuildOptions = this.getInput(inputs, 'xcodebuild_options', '');
|
|
814
|
+
const outputDir = this.getInput(inputs, 'output_dir', 'build');
|
|
815
|
+
const escapedPath = this.escapeBash(projectPath);
|
|
816
|
+
const escapedScheme = this.escapeBash(scheme);
|
|
817
|
+
const commands = [];
|
|
818
|
+
commands.push('# xcode-build-for-simulator');
|
|
819
|
+
commands.push(`echo "Building for simulator: ${escapedPath}"`);
|
|
820
|
+
commands.push(`echo "Scheme: ${escapedScheme}"`);
|
|
821
|
+
commands.push('');
|
|
822
|
+
// Output dir
|
|
823
|
+
commands.push(`mkdir -p '${this.escapeBash(outputDir)}'`);
|
|
824
|
+
commands.push(`OUTPUT_DIR="$(cd '${this.escapeBash(outputDir)}' && pwd)"`);
|
|
825
|
+
commands.push('');
|
|
826
|
+
// xcconfig
|
|
827
|
+
commands.push('XCCONFIG_FILE="$(mktemp -t cibuild-xcconfig).xcconfig"');
|
|
828
|
+
if (xcconfigContent.trim()) {
|
|
829
|
+
commands.push(`cat > "$XCCONFIG_FILE" << 'XCCONFIG_EOF'`);
|
|
830
|
+
commands.push(xcconfigContent);
|
|
831
|
+
commands.push('XCCONFIG_EOF');
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
commands.push('echo "CODE_SIGNING_ALLOWED=NO" > "$XCCONFIG_FILE"');
|
|
835
|
+
}
|
|
836
|
+
commands.push('');
|
|
837
|
+
// Project type
|
|
838
|
+
commands.push(`if [[ '${escapedPath}' == *.xcworkspace ]]; then`);
|
|
839
|
+
commands.push(' PROJECT_TYPE="-workspace"');
|
|
840
|
+
commands.push('else');
|
|
841
|
+
commands.push(' PROJECT_TYPE="-project"');
|
|
842
|
+
commands.push('fi');
|
|
843
|
+
commands.push('');
|
|
844
|
+
// Build command
|
|
845
|
+
let cmd = 'xcodebuild';
|
|
846
|
+
if (performClean === 'yes') {
|
|
847
|
+
cmd += ' clean';
|
|
848
|
+
}
|
|
849
|
+
cmd += ' build';
|
|
850
|
+
cmd += ` "$PROJECT_TYPE" '${escapedPath}'`;
|
|
851
|
+
cmd += ` -scheme '${escapedScheme}'`;
|
|
852
|
+
if (configuration) {
|
|
853
|
+
cmd += ` -configuration '${this.escapeBash(configuration)}'`;
|
|
854
|
+
}
|
|
855
|
+
cmd += ` -destination '${this.escapeBash(destination)}'`;
|
|
856
|
+
cmd += ` CONFIGURATION_BUILD_DIR="$OUTPUT_DIR"`;
|
|
857
|
+
cmd += ' -xcconfig "$XCCONFIG_FILE"';
|
|
858
|
+
if (xcodebuildOptions) {
|
|
859
|
+
cmd += ` ${xcodebuildOptions}`;
|
|
860
|
+
}
|
|
861
|
+
commands.push(cmd);
|
|
862
|
+
commands.push('');
|
|
863
|
+
// Locate .app
|
|
864
|
+
commands.push('# Locate generated .app');
|
|
865
|
+
commands.push('APP_PATH=$(find "$OUTPUT_DIR" -maxdepth 1 -name "*.app" -type d | head -1)');
|
|
866
|
+
commands.push('if [ -z "$APP_PATH" ]; then');
|
|
867
|
+
commands.push(' echo "❌ Error: .app not found in $OUTPUT_DIR"');
|
|
868
|
+
commands.push(' exit 1');
|
|
869
|
+
commands.push('fi');
|
|
870
|
+
commands.push('');
|
|
871
|
+
commands.push('echo "✅ Simulator build complete"');
|
|
872
|
+
commands.push('echo "App: $APP_PATH"');
|
|
873
|
+
commands.push('');
|
|
874
|
+
// Export
|
|
875
|
+
commands.push('envman add --key CIBUILD_APP_DIR_PATH --value "$APP_PATH"');
|
|
876
|
+
commands.push('rm -f "$XCCONFIG_FILE"');
|
|
877
|
+
const script = this.createBashScriptFromCommands(commands, stepName);
|
|
878
|
+
return this.createScriptStep(script, stepName);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Exports an IPA from an existing .xcarchive using xcodebuild -exportArchive.
|
|
883
|
+
*/
|
|
884
|
+
export class ExportXcarchiveStepExecutor extends BaseStepExecutor {
|
|
885
|
+
getValidationRequirements(_inputs, _env, _config) {
|
|
886
|
+
return [
|
|
887
|
+
this.requireCommand('xcodebuild', 'Xcode is required for archive export'),
|
|
888
|
+
];
|
|
889
|
+
}
|
|
890
|
+
getOutputs() {
|
|
891
|
+
return [
|
|
892
|
+
{ name: 'CIBUILD_IPA_PATH', type: 'environment', description: 'Path to the exported IPA file' },
|
|
893
|
+
{ name: 'CIBUILD_DSYM_PATH', type: 'environment', description: 'Path to the zipped dSYM files' },
|
|
894
|
+
];
|
|
895
|
+
}
|
|
896
|
+
async execute(inputs, _env, _config) {
|
|
897
|
+
const stepName = 'export-xcarchive';
|
|
898
|
+
const archivePath = this.getRequiredInput(inputs, 'archive_path', stepName);
|
|
899
|
+
const product = this.getInput(inputs, 'product', 'app');
|
|
900
|
+
const distributionMethod = this.getInput(inputs, 'distribution_method', 'development');
|
|
901
|
+
const compileBitcode = this.getInput(inputs, 'compile_bitcode', 'yes') === 'yes';
|
|
902
|
+
const uploadBitcode = this.getInput(inputs, 'upload_bitcode', 'yes') === 'yes';
|
|
903
|
+
const exportOptionsPlistContent = this.getInput(inputs, 'export_options_plist_content', '');
|
|
904
|
+
const verbose = this.getInput(inputs, 'verbose_log', 'no') === 'yes';
|
|
905
|
+
const commands = [];
|
|
906
|
+
commands.push('# export-xcarchive — export IPA from .xcarchive');
|
|
907
|
+
commands.push('echo "📦 Exporting IPA from xcarchive..."');
|
|
908
|
+
commands.push('');
|
|
909
|
+
commands.push(`ARCHIVE_PATH='${this.escapeBash(archivePath)}'`);
|
|
910
|
+
commands.push('');
|
|
911
|
+
// Validate archive exists
|
|
912
|
+
commands.push('if [ ! -d "$ARCHIVE_PATH" ]; then');
|
|
913
|
+
commands.push(' echo "❌ Error: Archive not found: $ARCHIVE_PATH"');
|
|
914
|
+
commands.push(' exit 1');
|
|
915
|
+
commands.push('fi');
|
|
916
|
+
commands.push('');
|
|
917
|
+
// Set up export directory
|
|
918
|
+
commands.push('EXPORT_DIR="${CIBUILD_DEPLOY_DIR:-./artifacts}/export"');
|
|
919
|
+
commands.push('mkdir -p "$EXPORT_DIR"');
|
|
920
|
+
commands.push('');
|
|
921
|
+
// Generate or use provided ExportOptions.plist
|
|
922
|
+
commands.push('EXPORT_OPTIONS="$EXPORT_DIR/ExportOptions.plist"');
|
|
923
|
+
if (exportOptionsPlistContent) {
|
|
924
|
+
commands.push('cat > "$EXPORT_OPTIONS" << \'PLIST_EOF\'');
|
|
925
|
+
commands.push(exportOptionsPlistContent);
|
|
926
|
+
commands.push('PLIST_EOF');
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
// Map distribution method for Xcode 15.3+
|
|
930
|
+
commands.push('# Determine distribution method');
|
|
931
|
+
commands.push(`METHOD='${this.escapeBash(distributionMethod)}'`);
|
|
932
|
+
commands.push('XCODE_VERSION=$(xcodebuild -version | head -1 | grep -oE "[0-9]+\\.[0-9]+" || echo "0.0")');
|
|
933
|
+
commands.push('XCODE_MAJOR=$(echo "$XCODE_VERSION" | cut -d. -f1)');
|
|
934
|
+
commands.push('XCODE_MINOR=$(echo "$XCODE_VERSION" | cut -d. -f2)');
|
|
935
|
+
commands.push('if [ "$XCODE_MAJOR" -ge 16 ] || ([ "$XCODE_MAJOR" -eq 15 ] && [ "$XCODE_MINOR" -ge 3 ]); then');
|
|
936
|
+
commands.push(' case "$METHOD" in');
|
|
937
|
+
commands.push(' development) METHOD="debugging" ;;');
|
|
938
|
+
commands.push(' app-store) METHOD="app-store-connect" ;;');
|
|
939
|
+
commands.push(' ad-hoc) METHOD="release-testing" ;;');
|
|
940
|
+
commands.push(' esac');
|
|
941
|
+
commands.push('fi');
|
|
942
|
+
commands.push('');
|
|
943
|
+
commands.push('cat > "$EXPORT_OPTIONS" << PLIST_EOF');
|
|
944
|
+
commands.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
945
|
+
commands.push('<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">');
|
|
946
|
+
commands.push('<plist version="1.0">');
|
|
947
|
+
commands.push('<dict>');
|
|
948
|
+
commands.push(' <key>method</key>');
|
|
949
|
+
commands.push(' <string>$METHOD</string>');
|
|
950
|
+
commands.push(` <key>compileBitcode</key>`);
|
|
951
|
+
commands.push(` <${compileBitcode}/>`);
|
|
952
|
+
commands.push(` <key>uploadBitcode</key>`);
|
|
953
|
+
commands.push(` <${uploadBitcode}/>`);
|
|
954
|
+
if (product === 'app-clip') {
|
|
955
|
+
commands.push(' <key>exportForThinning</key>');
|
|
956
|
+
commands.push(' <string>app-clip</string>');
|
|
957
|
+
}
|
|
958
|
+
commands.push('</dict>');
|
|
959
|
+
commands.push('</plist>');
|
|
960
|
+
commands.push('PLIST_EOF');
|
|
961
|
+
}
|
|
962
|
+
commands.push('');
|
|
963
|
+
// Run xcodebuild -exportArchive
|
|
964
|
+
const verboseFlag = verbose ? '' : ' -quiet';
|
|
965
|
+
commands.push('echo "Exporting with method: $METHOD"');
|
|
966
|
+
commands.push(`xcodebuild -exportArchive \\`);
|
|
967
|
+
commands.push(` -archivePath "$ARCHIVE_PATH" \\`);
|
|
968
|
+
commands.push(` -exportPath "$EXPORT_DIR" \\`);
|
|
969
|
+
commands.push(` -exportOptionsPlist "$EXPORT_OPTIONS"${verboseFlag}`);
|
|
970
|
+
commands.push('');
|
|
971
|
+
// Find IPA
|
|
972
|
+
commands.push('# Find exported IPA');
|
|
973
|
+
commands.push('IPA_PATH=$(find "$EXPORT_DIR" -name "*.ipa" | head -1)');
|
|
974
|
+
commands.push('if [ -z "$IPA_PATH" ]; then');
|
|
975
|
+
commands.push(' echo "❌ Error: No IPA found in export directory"');
|
|
976
|
+
commands.push(' exit 1');
|
|
977
|
+
commands.push('fi');
|
|
978
|
+
commands.push('echo "✅ Exported IPA: $IPA_PATH"');
|
|
979
|
+
commands.push('');
|
|
980
|
+
// Collect dSYMs
|
|
981
|
+
commands.push('# Collect dSYMs');
|
|
982
|
+
commands.push('DSYM_DIR="$ARCHIVE_PATH/dSYMs"');
|
|
983
|
+
commands.push('DSYM_ZIP=""');
|
|
984
|
+
commands.push('if [ -d "$DSYM_DIR" ] && [ "$(ls -A "$DSYM_DIR" 2>/dev/null)" ]; then');
|
|
985
|
+
commands.push(' DSYM_ZIP="$EXPORT_DIR/dSYMs.zip"');
|
|
986
|
+
commands.push(' cd "$DSYM_DIR" && zip -r "$DSYM_ZIP" . && cd -');
|
|
987
|
+
commands.push(' echo "dSYMs: $DSYM_ZIP"');
|
|
988
|
+
commands.push('fi');
|
|
989
|
+
commands.push('');
|
|
990
|
+
// Export env vars
|
|
991
|
+
commands.push('envman add --key CIBUILD_IPA_PATH --value "$IPA_PATH"');
|
|
992
|
+
commands.push('if [ -n "$DSYM_ZIP" ]; then');
|
|
993
|
+
commands.push(' envman add --key CIBUILD_DSYM_PATH --value "$DSYM_ZIP"');
|
|
994
|
+
commands.push('fi');
|
|
995
|
+
const script = this.createBashScriptFromCommands(commands, stepName);
|
|
996
|
+
return this.createScriptStep(script, stepName);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
//# sourceMappingURL=xcode.js.map
|