Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ jobs:
- name: Run Arkana
run: bundle exec arkana

- name: Modify test plan
run: |
mint run mikeger/XcodeSelectiveTesting@1c0927e5553a9ddb6b3634d6d13497651a0ef476 {PROJECT_NAME}.xcodeproj \
--test-plan {PROJECT_NAME}.xctestplan \
--base-branch origin/${{ github.base_ref }}
Comment on lines +44 to +48
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether mint is declared in mise configuration or mentioned in self-hosted setup
fd -H -t f '\.mise\.toml$' | xargs -I{} sh -c 'echo "--- {} ---"; cat "{}"'
rg -n --hidden 'mint' -g '!*.lock' -g '!node_modules/**'

Repository: nimblehq/ios-templates

Length of output: 1537


Add mint to .mise.toml or include an explicit Setup Mint step.

The self-hosted workflow uses mint run at line 46, but unlike the hosted workflow (which adds irgaly/setup-mint@v1), this file has no explicit Mint setup. The Install tools with mise step relies on .mise.toml, but mint is not declared there. This will cause the workflow to fail with command not found when attempting to run the Mint command.

Either add mint to the [tools] section in .mise.toml, or add a Setup Mint step symmetric to the hosted workflow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
@.cicdtemplate/.github/self-hosted-workflows/automatic_pull_request_review.yml
around lines 44 - 48, The workflow calls "mint run" but never installs Mint; fix
by either adding mint to the [tools] section in .mise.toml so the existing
"Install tools with mise" step provides it, or add an explicit "Setup Mint" step
(e.g., using irgaly/setup-mint@v1) before the job that runs mint; update the
self-hosted workflow in automatic_pull_request_review.yml to include one of
these options so the mint command is available when Modify test plan (mint run
...) executes.


- name: Build and Test
run: bundle exec fastlane buildAndTest
env:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ jobs:
restore-keys: |
${{ runner.os }}-gems-

- name: Setup Mint
uses: irgaly/setup-mint@8566ee44b3a79d20642d1924db21c8ab0402859f # irgaly/setup-mint@v1

Comment on lines +28 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use mise to manage the new CLI tool instead of introducing a new package manager?

- name: Setup CI environment
uses: ./.github/actions/setup-ci-environment
with:
Expand All @@ -41,6 +44,13 @@ jobs:
- name: Run Arkana
run: bundle exec arkana

# using mikeger/XcodeSelectiveTesting@0.14.5
- name: Modify test plan
run: |
mint run mikeger/XcodeSelectiveTesting@1c0927e5553a9ddb6b3634d6d13497651a0ef476 {PROJECT_NAME}.xcodeproj \
--test-plan {PROJECT_NAME}.xctestplan \
--base-branch origin/${{ github.base_ref }}

- name: Build and Test
run: bundle exec fastlane buildAndTest
env:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ class SetUpIOSProject {
try fileManager.rename(file: "template/\(CONSTANT_PROJECT_NAME)Tests", to: "template/\(projectNameNoSpace)Tests")
try fileManager.rename(file: "template/\(CONSTANT_PROJECT_NAME)KIFUITests", to: "template/\(projectNameNoSpace)KIFUITests")
try fileManager.rename(file: "template/\(CONSTANT_PROJECT_NAME)", to: "template/\(projectNameNoSpace)")
try fileManager.rename(file: "template/\(CONSTANT_PROJECT_NAME).xctestplan", to: "template/\(projectNameNoSpace).xctestplan")
}

private func createPlaceholderFiles() throws {
Expand Down
2 changes: 1 addition & 1 deletion template/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions template/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ extension Project {
.productionScheme(name: name),
.stagingScheme(name: name),
.devScheme(name: name)
],
additionalFiles: [
"\(name).xctestplan"
]
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,48 @@ import ProjectDescription

extension Scheme {
public static func productionScheme(name: String) -> Scheme {
let debugConfigName = BuildConfiguration.debugProduction.name
let releaseConfigName = BuildConfiguration.releaseProduction.name

return .scheme(
makeScheme(
name: name,
shared: true,
buildAction: .buildAction(targets: ["\(name)"]),
testAction: TestAction.targets(testTargets(for: name), configuration: debugConfigName),
runAction: .runAction(configuration: debugConfigName),
archiveAction: .archiveAction(configuration: releaseConfigName)
debugConfiguration: .debugProduction,
releaseConfiguration: .releaseProduction
)
}

public static func stagingScheme(name: String) -> Scheme {
let debugConfigName = BuildConfiguration.debugStaging.name
let releaseConfigName = BuildConfiguration.releaseStaging.name

return .scheme(
name: "\(name) Staging",
shared: true,
buildAction: .buildAction(targets: ["\(name)"]),
testAction: TestAction.targets(testTargets(for: name), configuration: debugConfigName),
runAction: .runAction(configuration: debugConfigName),
archiveAction: .archiveAction(configuration: releaseConfigName)
makeScheme(
name: name,
schemeSuffix: "Staging",
debugConfiguration: .debugStaging,
releaseConfiguration: .releaseStaging
)
}

public static func devScheme(name: String) -> Scheme {
let debugConfigName = BuildConfiguration.debugDev.name
let releaseConfigName = BuildConfiguration.releaseDev.name
makeScheme(
name: name,
schemeSuffix: "Dev",
debugConfiguration: .debugDev,
releaseConfiguration: .releaseDev
)
}

private static func makeScheme(
name: String,
schemeSuffix: String? = nil,
debugConfiguration: BuildConfiguration,
releaseConfiguration: BuildConfiguration
) -> Scheme {
let schemeName = [name, schemeSuffix]
.compactMap { $0 }
.joined(separator: " ")

return .scheme(
name: "\(name) Dev",
name: schemeName,
shared: true,
buildAction: .buildAction(targets: ["\(name)"]),
testAction: TestAction.targets(testTargets(for: name), configuration: debugConfigName),
runAction: .runAction(configuration: debugConfigName),
archiveAction: .archiveAction(configuration: releaseConfigName),
testAction: .testPlans([.path("\(name).xctestplan")], configuration: debugConfiguration.name),
runAction: .runAction(configuration: debugConfiguration.name),
archiveAction: .archiveAction(configuration: releaseConfiguration.name)
)
}

private static func testTargets(for name: String) -> [TestableTarget] {
[
.testableTarget(target: TargetReference(stringLiteral: Module.domain.name + Constant.testsPath)),
.testableTarget(target: TargetReference(stringLiteral: Module.data.name + Constant.testsPath)),
.testableTarget(target: TargetReference(stringLiteral: "\(name)Tests"))
]
}
}
17 changes: 17 additions & 0 deletions template/fastlane/Fastfile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ class Fastfile: LaneFile {

func buildAndTestLane() {
desc("Build and Test project")
guard testTargetCount() > 0 else {
echo(message: "🚨 Nothing to test")
return
}
Test.buildAndTest(
environment: .staging,
devices: Constant.devices
Expand All @@ -176,6 +180,10 @@ class Fastfile: LaneFile {

func buildAndTestDevLane() {
desc("Build and Test dev project")
guard testTargetCount() > 0 else {
echo(message: "🚨 Nothing to test")
return
}
Test.buildAndTest(
environment: .dev,
devices: Constant.devices
Expand Down Expand Up @@ -268,4 +276,13 @@ class Fastfile: LaneFile {
buildNumber: .userDefined("\(theLatestBuildNumber)")
)
}

private func testTargetCount(in testPlanPath: String = "\(Constant.projectName).xctestplan") -> Int {
guard let data = FileManager.default.contents(atPath: testPlanPath),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let testTargets = json?["testTargets"] as? [Any] else {
return 0
}
return testTargets.count
}
Comment on lines +280 to +287
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unnecessary optional chaining on json.

After the guard let json = ... unwrap, json is a non-optional [String: Any], so json?["testTargets"] is redundant optional chaining — the compiler will warn. Prefer json["testTargets"].

🛠️ Proposed fix
-    private func testTargetCount(in testPlanPath: String = "\(Constant.projectName).xctestplan") -> Int {
-        guard let data = FileManager.default.contents(atPath: testPlanPath),
-              let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
-              let testTargets = json?["testTargets"] as? [Any] else {
-            return 0
-        }
-        return testTargets.count
-    }
+    private func testTargetCount(in testPlanPath: String = "\(Constant.projectName).xctestplan") -> Int {
+        guard let data = FileManager.default.contents(atPath: testPlanPath),
+              let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
+              let testTargets = json["testTargets"] as? [Any] else {
+            return 0
+        }
+        return testTargets.count
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private func testTargetCount(in testPlanPath: String = "\(Constant.projectName).xctestplan") -> Int {
guard let data = FileManager.default.contents(atPath: testPlanPath),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let testTargets = json?["testTargets"] as? [Any] else {
return 0
}
return testTargets.count
}
private func testTargetCount(in testPlanPath: String = "\(Constant.projectName).xctestplan") -> Int {
guard let data = FileManager.default.contents(atPath: testPlanPath),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let testTargets = json["testTargets"] as? [Any] else {
return 0
}
return testTargets.count
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@template/fastlane/Fastfile.swift` around lines 280 - 287, In
testTargetCount(in:) the guard unnecessarily uses optional chaining on json
(declared non-optional); change json?["testTargets"] to json["testTargets"] in
the guard so it reads let testTargets = json["testTargets"] as? [Any], keeping
the rest of the guard and return logic unchanged to avoid the compiler warning
in the private func testTargetCount(in testPlanPath: String =
"\(Constant.projectName).xctestplan") -> Int.

}
1 change: 1 addition & 0 deletions template/fastlane/Helpers/Test.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ enum Test {
scheme: .userDefined(environment.scheme),
devices: .userDefined(devices),
onlyTesting: onlyTesting,
testplan: .userDefined("\(Constant.projectName)"),
codeCoverage: .userDefined(true),
outputDirectory: Constant.testOutputDirectoryPath,
resultBundle: .userDefined(true),
Expand Down
45 changes: 45 additions & 0 deletions template/{PROJECT_NAME}.xctestplan
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"configurations" : [
{
"id" : "C56A3802-C716-4C08-8B7A-53EFE53D4FE0",
"name" : "Configuration 1",
"options" : {

}
}
],
"defaultOptions" : {

},
"testTargets" : [
{
"target" : {
"containerPath" : "container:TestTemplate.xcodeproj",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test plan is still hardcoded to TestTemplate...please update it

Suggested change
"containerPath" : "container:TestTemplate.xcodeproj",
"containerPath" : "container:{PROJECT_NAME}.xcodeproj",

"identifier" : "PLACEHOLDER",
"name" : "DomainTests"
}
},
{
"target" : {
"containerPath" : "container:TestTemplate.xcodeproj",
"identifier" : "PLACEHOLDER",
"name" : "TestTemplateTests"
}
},
{
"target" : {
"containerPath" : "container:TestTemplate.xcodeproj",
"identifier" : "PLACEHOLDER",
"name" : "DataTests"
}
},
{
"target" : {
"containerPath" : "container:TestTemplate.xcodeproj",
"identifier" : "PLACEHOLDER",
"name" : "ModelTests"
}
}
],
Comment on lines +14 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Hardcoded TestTemplate breaks template substitution.

containerPath values (lines 17, 24, 31, 38) and the main-app test target name TestTemplateTests (line 26) are hardcoded to TestTemplate instead of using the {PROJECT_NAME} placeholder. After SetUpIOSProject.replaceTextInFiles() runs, it replaces {PROJECT_NAME} with projectNameNoSpace, but TestTemplate is not a placeholder and will be left untouched. For any generated project whose name isn't TestTemplate:

  • container:TestTemplate.xcodeproj will not resolve to the actual .xcodeproj (which is \(projectNameNoSpace).xcodeproj).
  • TestTemplateTests will not match the generated app test target \(name)Tests.

As a result, Xcode will refuse to load the test plan and buildAndTestLane will either fail or silently fall through the testTargetCount() > 0 guard (since the JSON is structurally valid, it will actually try to run and fail). Replace the hardcoded string with the project placeholder.

🐛 Proposed fix
     {
       "target" : {
-        "containerPath" : "container:TestTemplate.xcodeproj",
+        "containerPath" : "container:{PROJECT_NAME}.xcodeproj",
         "identifier" : "PLACEHOLDER",
         "name" : "DomainTests"
       }
     },
     {
       "target" : {
-        "containerPath" : "container:TestTemplate.xcodeproj",
+        "containerPath" : "container:{PROJECT_NAME}.xcodeproj",
         "identifier" : "PLACEHOLDER",
-        "name" : "TestTemplateTests"
+        "name" : "{PROJECT_NAME}Tests"
       }
     },
     {
       "target" : {
-        "containerPath" : "container:TestTemplate.xcodeproj",
+        "containerPath" : "container:{PROJECT_NAME}.xcodeproj",
         "identifier" : "PLACEHOLDER",
         "name" : "DataTests"
       }
     },
     {
       "target" : {
-        "containerPath" : "container:TestTemplate.xcodeproj",
+        "containerPath" : "container:{PROJECT_NAME}.xcodeproj",
         "identifier" : "PLACEHOLDER",
         "name" : "ModelTests"
       }
     }

As per coding guidelines: "Do not replace template placeholders (e.g., {PROJECT_NAME}, {BUNDLE_ID_STAGING}) with concrete values inside the template; preserve them for generation-time substitution".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"testTargets" : [
{
"target" : {
"containerPath" : "container:TestTemplate.xcodeproj",
"identifier" : "PLACEHOLDER",
"name" : "DomainTests"
}
},
{
"target" : {
"containerPath" : "container:TestTemplate.xcodeproj",
"identifier" : "PLACEHOLDER",
"name" : "TestTemplateTests"
}
},
{
"target" : {
"containerPath" : "container:TestTemplate.xcodeproj",
"identifier" : "PLACEHOLDER",
"name" : "DataTests"
}
},
{
"target" : {
"containerPath" : "container:TestTemplate.xcodeproj",
"identifier" : "PLACEHOLDER",
"name" : "ModelTests"
}
}
],
"testTargets" : [
{
"target" : {
"containerPath" : "container:{PROJECT_NAME}.xcodeproj",
"identifier" : "PLACEHOLDER",
"name" : "DomainTests"
}
},
{
"target" : {
"containerPath" : "container:{PROJECT_NAME}.xcodeproj",
"identifier" : "PLACEHOLDER",
"name" : "{PROJECT_NAME}Tests"
}
},
{
"target" : {
"containerPath" : "container:{PROJECT_NAME}.xcodeproj",
"identifier" : "PLACEHOLDER",
"name" : "DataTests"
}
},
{
"target" : {
"containerPath" : "container:{PROJECT_NAME}.xcodeproj",
"identifier" : "PLACEHOLDER",
"name" : "ModelTests"
}
}
],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@template/`{PROJECT_NAME}.xctestplan around lines 14 - 43, The test plan
contains hardcoded "TestTemplate" values that prevent template substitution;
update the four containerPath entries (currently
"container:TestTemplate.xcodeproj") and the test target name "TestTemplateTests"
to use the project placeholder {PROJECT_NAME} (so they resolve to
{PROJECT_NAME}.xcodeproj and {PROJECT_NAME}Tests at generation time) so
SetUpIOSProject.replaceTextInFiles() can correctly replace {PROJECT_NAME} with
projectNameNoSpace/name; ensure you only replace the literal "TestTemplate"
occurrences in the "testTargets" objects (containerPath and name fields) with
the {PROJECT_NAME} placeholder and do not pre-expand other placeholders.

"version" : 1
}