Skip to content

Commit 52610a2

Browse files
committed
feat(ci): Add support for testing SPM-based quickstarts
1 parent 9bc71e3 commit 52610a2

File tree

3 files changed

+237
-0
lines changed

3 files changed

+237
-0
lines changed

scripts/setup_quickstart_spm.sh

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Copyright 2023 Google
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# Script to run in a CI `before_install` phase to setup a SPM-based
16+
# quickstart repo so that it can be used for integration testing.
17+
18+
set -xeuo pipefail
19+
20+
scripts_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21+
root_dir="$(dirname "$scripts_dir")"
22+
23+
# Source function to check if CI secrets are available.
24+
source $scripts_dir/check_secrets.sh
25+
26+
# Arguments:
27+
# SAMPLE: The name of the quickstart sample directory.
28+
# RELEASE_TESTING: Optional. Can be "nightly_release_testing" or "prerelease_testing".
29+
# VERSION: Required when RELEASE_TESTING is "nightly_release_testing".
30+
#
31+
# Environment Variable:
32+
# QUICKSTART_REPO: Optional. Path to a local clone of the quickstart-ios repo.
33+
# If not set, the script will clone it from GitHub.
34+
# Example:
35+
# QUICKSTART_REPO=/path/to/my/quickstart-ios ./scripts/setup_quickstart_spm.sh AppName
36+
SAMPLE=$1
37+
RELEASE_TESTING=${2-}
38+
VERSION=${3-}
39+
40+
QUICKSTART_PROJECT_DIR="quickstart-ios/${SAMPLE}"
41+
42+
# TODO: Investigate moving this to a shared prereq script.
43+
if ! gem list -i xcpretty > /dev/null; then
44+
gem install xcpretty
45+
fi
46+
47+
# Some quickstarts may not need a real GoogleService-Info.plist for their tests.
48+
# When QUICKSTART_REPO is set, we are running locally and should skip the secrets check.
49+
if [[ -n "${QUICKSTART_REPO:-}" ]] || check_secrets || [[ ${SAMPLE} == "installations" ]]; then
50+
51+
# Use local quickstart repo if QUICKSTART_REPO is set, otherwise clone it.
52+
if [[ -n "${QUICKSTART_REPO:-}" && -d "${QUICKSTART_REPO}" ]]; then
53+
echo "Using local quickstart repository at ${QUICKSTART_REPO}"
54+
QUICKSTART_DIR="${QUICKSTART_REPO}"
55+
else
56+
echo "Cloning quickstart repository into 'quickstart-ios' directory..."
57+
git clone https://github.com/firebase/quickstart-ios.git
58+
QUICKSTART_DIR="quickstart-ios"
59+
fi
60+
61+
QUICKSTART_PROJECT_DIR="${QUICKSTART_DIR}/${SAMPLE}"
62+
63+
# Find the .xcodeproj file within the sample directory.
64+
# Note: This assumes there is only one .xcodeproj file.
65+
PROJECT_FILE=$(find "$QUICKSTART_PROJECT_DIR" -maxdepth 1 -name "*.xcodeproj" | head -n 1)
66+
if [[ -z "$PROJECT_FILE" ]]; then
67+
echo "Error: Could not find .xcodeproj file in ${QUICKSTART_PROJECT_DIR}"
68+
exit 1
69+
fi
70+
71+
# The localization script needs an absolute path to the project file.
72+
# If QUICKSTART_REPO was provided, PROJECT_FILE is already an absolute or user-provided path.
73+
# Otherwise, it's relative to the firebase-ios-sdk root.
74+
if [[ -n "${QUICKSTART_REPO:-}" && -d "${QUICKSTART_REPO}" ]]; then
75+
ABSOLUTE_PROJECT_FILE="$PROJECT_FILE"
76+
else
77+
ABSOLUTE_PROJECT_FILE="$root_dir/$PROJECT_FILE"
78+
fi
79+
80+
# (cd "$QUICKSTART_DIR"; git checkout {BRANCH_NAME})
81+
82+
if [ "$RELEASE_TESTING" == "nightly_release_testing" ]; then
83+
if [[ -z "$VERSION" ]]; then
84+
echo "Error: VERSION (arg 3) is required for nightly_release_testing."
85+
exit 1
86+
fi
87+
# For release testing, point to the specific version tag.
88+
echo "Setting SPM dependency to version ${VERSION}"
89+
swift run --package-path "$scripts_dir/spm-localizer" SPMLocalize "$ABSOLUTE_PROJECT_FILE" --version "$VERSION"
90+
91+
elif [ "$RELEASE_TESTING" == "prerelease_testing" ]; then
92+
# For prerelease testing, point to the tip of the main branch.
93+
echo "Setting SPM dependency to the tip of the main branch."
94+
swift run --package-path "$scripts_dir/spm-localizer" SPMLocalize "$ABSOLUTE_PROJECT_FILE" --prerelease
95+
96+
else
97+
# For PR testing, point to the current commit.
98+
CURRENT_REVISION=$(git rev-parse HEAD)
99+
echo "Setting SPM dependency to current revision: ${CURRENT_REVISION}"
100+
swift run --package-path "$scripts_dir/spm-localizer" SPMLocalize "$ABSOLUTE_PROJECT_FILE" --revision "$CURRENT_REVISION"
101+
fi
102+
103+
else
104+
echo "Skipping quickstart setup: CI secrets are not available."
105+
fi
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// swift-tools-version:5.4
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "FirebaseSDKScripts",
6+
platforms: [.macOS(.v10_11)],
7+
dependencies: [
8+
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"),
9+
],
10+
targets: [
11+
.executableTarget(
12+
name: "SPMLocalize",
13+
dependencies: [
14+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
15+
],
16+
path: ".",
17+
sources: ["spm_localize_xcode_project.swift"]
18+
)
19+
]
20+
)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright 2025 Google
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
import ArgumentParser
17+
18+
struct SpmLocalizer: ParsableCommand {
19+
static let configuration = CommandConfiguration(
20+
abstract: "Updates an Xcode project's firebase-ios-sdk SPM dependency to point to a specific version, branch, or commit."
21+
)
22+
23+
@Argument(help: "Path to the .xcodeproj file.")
24+
var projectPath: String
25+
26+
@Option(name: .long, help: "The version to use for release testing (e.g., '10.24.0').")
27+
var version: String?
28+
29+
@Flag(name: .long, help: "Flag to point to the latest commit on the main branch for prerelease testing.")
30+
var prerelease = false
31+
32+
@Option(name: .long, help: "The commit hash to use for PR/branch testing.")
33+
var revision: String?
34+
35+
func run() throws {
36+
let pbxprojPath = "\(projectPath)/project.pbxproj"
37+
var pbxprojContents: String
38+
do {
39+
pbxprojContents = try String(contentsOfFile: pbxprojPath, encoding: .utf8)
40+
} catch {
41+
fatalError("Failed to read project.pbxproj file: \(error)")
42+
}
43+
44+
let requirement: String
45+
let indent4 = "\t\t\t\t"
46+
let indent3 = "\t\t\t"
47+
if let version = version {
48+
// Release testing: Point to CocoaPods-{VERSION} tag (as a branch)
49+
requirement = "{\n\(indent4)kind = branch;\n\(indent4)branch = \"CocoaPods-\(version)\";\n\(indent3)}"
50+
} else if prerelease {
51+
// Prerelease testing: Point to the tip of the main branch
52+
let commitHash = try getRemoteHeadRevision(branch: "main")
53+
requirement = "{\n\(indent4)kind = revision;\n\(indent4)revision = \"\(commitHash)\";\n\(indent3)}"
54+
} else if let revision = revision {
55+
// PR testing: Point to the specific commit hash of the current branch
56+
requirement = "{\n\(indent4)kind = revision;\n\(indent4)revision = \"\(revision)\";\n\(indent3)}"
57+
} else {
58+
fatalError("No dependency requirement specified. Please provide --version, --prerelease, or --revision.")
59+
}
60+
61+
let updatedContents = try replaceDependency(in: pbxprojContents, with: requirement)
62+
63+
do {
64+
try updatedContents.write(toFile: pbxprojPath, atomically: true, encoding: .utf8)
65+
print("Successfully updated SPM dependency in \(pbxprojPath)")
66+
} catch {
67+
fatalError("Failed to write updated contents to project.pbxproj: \(error)")
68+
}
69+
}
70+
71+
private func replaceDependency(in content: String, with requirement: String) throws -> String {
72+
let pattern = #"(repositoryURL = "https://github.com/firebase/firebase-ios-sdk";\s*requirement = )\{[^\}]+\};"#
73+
let regex = try NSRegularExpression(pattern: pattern, options: [])
74+
75+
let range = NSRange(content.startIndex..<content.endIndex, in: content)
76+
let template = "$1\(requirement);"
77+
78+
let modifiedContent = regex.stringByReplacingMatches(in: content, options: [], range: range, withTemplate: template)
79+
80+
if content == modifiedContent {
81+
fatalError("Failed to find and replace the firebase-ios-sdk dependency. Check the regex pattern and project file structure.")
82+
}
83+
84+
return modifiedContent
85+
}
86+
87+
private func getRemoteHeadRevision(branch: String) throws -> String {
88+
let process = Process()
89+
process.executableURL = URL(fileURLWithPath: "/usr/bin/git")
90+
process.arguments = ["ls-remote", "https://github.com/firebase/firebase-ios-sdk.git", branch]
91+
92+
let pipe = Pipe()
93+
process.standardOutput = pipe
94+
95+
try process.run()
96+
process.waitUntilExit()
97+
98+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
99+
guard let output = String(data: data, encoding: .utf8), !output.isEmpty else {
100+
fatalError("Failed to get remote revision for branch '\(branch)'. No output from git.")
101+
}
102+
103+
// Output is in the format: <hash>\trefs/heads/<branch>
104+
let components = output.components(separatedBy: CharacterSet.whitespacesAndNewlines)
105+
if components.isEmpty {
106+
fatalError("Invalid output from git ls-remote: \(output)")
107+
}
108+
return components[0]
109+
}
110+
}
111+
112+
SpmLocalizer.main()

0 commit comments

Comments
 (0)