diff --git a/.github/workflows/package-ffi-engine.yml b/.github/workflows/package-ffi-engine.yml index 9735d164..18aabfaf 100644 --- a/.github/workflows/package-ffi-engine.yml +++ b/.github/workflows/package-ffi-engine.yml @@ -55,10 +55,15 @@ jobs: target: x86_64-pc-windows-msvc use_cross: false - # - name: iOS-arm64 - # os: macos-latest - # target: aarch64-apple-ios - # use_cross: false + - name: iOS-arm64 + os: macos-latest + target: aarch64-apple-ios + use_cross: false + + - name: iOS-arm64-sim + os: macos-latest + target: aarch64-apple-ios-sim + use_cross: false # - name: Android-arm64 # os: ubuntu-latest @@ -125,7 +130,6 @@ jobs: 7z a -tzip flipt-engine-ffi-${{ matrix.platform.name }}.zip ` target\${{ matrix.platform.target }}\release\fliptengine.* ` || true - - name: Upload To Pull Request (Pull Request) uses: actions/upload-artifact@v4 if: github.event_name == 'pull_request' && startsWith(github.head_ref, 'package/') diff --git a/.github/workflows/package-swift-sdk.yml b/.github/workflows/package-swift-sdk.yml new file mode 100644 index 00000000..5e709e34 --- /dev/null +++ b/.github/workflows/package-swift-sdk.yml @@ -0,0 +1,17 @@ +name: Package Swift SDK +on: + push: + tags: + - "flipt-client-swift-**" + +permissions: + contents: write + id-token: write + +jobs: + build: + uses: ./.github/workflows/package-ffi-sdks.yml + with: + sdks: "swift" + tag: ${{ github.ref }} + secrets: inherit diff --git a/.github/workflows/test-swift-sdk.yml b/.github/workflows/test-swift-sdk.yml new file mode 100644 index 00000000..8458f8f5 --- /dev/null +++ b/.github/workflows/test-swift-sdk.yml @@ -0,0 +1,57 @@ +name: Test Swift SDK +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +permissions: + contents: write + id-token: write + +jobs: + test: + name: Integration Tests + runs-on: macos-latest + steps: + - name: Checkout Sources + uses: actions/checkout@v4 + + - name: Install Flipt + uses: flipt-io/setup-action@v0.1.1 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: 1.75.0 + override: true + + - name: Install Swift + uses: swift-actions/setup-swift@v1 + with: + swift-version: "5.9" + + - name: run flipt + env: + FLIPT_STORAGE_TYPE: "local" + FLIPT_STORAGE_LOCAL_PATH: "./test/fixtures/testdata" + run: flipt& + + - name: Install Rust Targets + run: | + rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim + + - name: Build Engine + run: | + cd ./flipt-engine-ffi + ./build.sh + cd .. + + - name: Run Integration Tests + env: + FLIPT_URL: "http://0.0.0.0:8080" + FLIPT_AUTH_TOKEN: "secret" + run: | + cd ./flipt-client-swift + swift test diff --git a/flipt-client-swift/.gitignore b/flipt-client-swift/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/flipt-client-swift/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/flipt-client-swift/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/flipt-client-swift/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/flipt-client-swift/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/flipt-client-swift/Package.swift b/flipt-client-swift/Package.swift new file mode 100644 index 00000000..d542462f --- /dev/null +++ b/flipt-client-swift/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version: 5.9.2 + +import PackageDescription + +let package = Package( + name: "FliptClient", + platforms: [.iOS(.v13), .macOS(.v10_15)], + products: [ + .library( + name: "FliptClient", + targets: ["FliptClient"]), + ], + targets: [ + .target( + name: "FliptClient", + dependencies: ["FliptEngineFFI"], + linkerSettings: [ + .linkedFramework("SystemConfiguration"), + .linkedLibrary("fliptengine") + ] + ), + .binaryTarget( + name: "FliptEngineFFI", + path: "./Sources/FliptEngineFFI.xcframework"), + .testTarget( + name: "FliptClientTests", + dependencies: ["FliptClient"]), + ] +) diff --git a/flipt-client-swift/Sources/FliptClient/FliptClient.swift b/flipt-client-swift/Sources/FliptClient/FliptClient.swift new file mode 100644 index 00000000..00d95bc3 --- /dev/null +++ b/flipt-client-swift/Sources/FliptClient/FliptClient.swift @@ -0,0 +1,282 @@ +import Foundation +import FliptEngineFFI + +public class FliptClient { + private var engine: UnsafeMutableRawPointer? + private var namespace: String = "default" + private var url: String = "" + private var authentication: Authentication? + private var ref: String = "" + private var updateInterval: Int = 0 + private var fetchMode: FetchMode = .polling + + init(namespace: String = "default", + url: String = "", + authentication: Authentication? = nil, + ref: String = "", + updateInterval: Int = 0, + fetchMode: FetchMode = .polling) throws { + self.namespace = namespace + self.url = url + self.authentication = authentication + self.ref = ref + self.updateInterval = updateInterval + self.fetchMode = fetchMode + + let clientOptions = ClientOptions( + url: url, + authentication: authentication, + updateInterval: updateInterval, + reference: ref, + fetchMode: fetchMode + ) + + guard let jsonData = try? JSONEncoder().encode(clientOptions) else { + throw ClientError.invalidOptions + } + + let jsonStr = String(data: jsonData, encoding: .utf8) + let namespaceCString = strdup(namespace) + let clientOptionsCString = strdup(jsonStr) + + engine = initialize_engine(namespaceCString, clientOptionsCString) + + free(namespaceCString) + free(clientOptionsCString) + } + + deinit { + close() + } + + func close() { + if let engine = engine { + destroy_engine(engine) + self.engine = nil + } + } + + func evaluateVariant(flagKey: String, entityID: String, evalContext: [String: String]) throws -> VariantEvaluationResponse { + let evaluationRequest = EvaluationRequest( + flag_key: flagKey, + entity_id: entityID, + context: evalContext + ) + + guard let requestData = try? JSONEncoder().encode(evaluationRequest) else { + throw ClientError.invalidRequest + } + + let requestCString = strdup(String(data: requestData, encoding: .utf8)) + + let variantResponse = evaluate_variant(engine, requestCString) + free(requestCString) + + let responseString = String(cString: variantResponse!) + destroy_string(UnsafeMutablePointer(mutating: variantResponse)) + + do { + let variantResult = try JSONDecoder().decode(VariantResult.self, from: Data(responseString.utf8)) + // Use variantResult here + if variantResult.status != "success" { + throw ClientError.evaluationFailed(message: variantResult.error_message ?? "Unknown error") + } + guard let result = variantResult.result else { + throw ClientError.evaluationFailed(message: "missing result") + } + + return result + } catch { + throw ClientError.parsingError + } + + } + + func evaluateBoolean(flagKey: String, entityID: String, evalContext: [String: String]) throws -> BooleanEvaluationResponse { + let evaluationRequest = EvaluationRequest( + flag_key: flagKey, + entity_id: entityID, + context: evalContext + ) + + guard let requestData = try? JSONEncoder().encode(evaluationRequest) else { + throw ClientError.invalidRequest + } + + let requestCString = strdup(String(data: requestData, encoding: .utf8)) + + let booleanResponse = evaluate_boolean(engine, requestCString) + free(requestCString) + + let responseString = String(cString: booleanResponse!) + destroy_string(UnsafeMutablePointer(mutating: booleanResponse)) + + guard let booleanResult = try? JSONDecoder().decode(BooleanResult.self, from: Data(responseString.utf8)) else { + throw ClientError.parsingError + } + + if booleanResult.status != "success" { + throw ClientError.evaluationFailed(message: booleanResult.error_message ?? "Unknown error") + } + + return booleanResult.result! + } + + func listFlags() throws -> [Flag] { + let flagsResponse = list_flags(engine) + + let responseString = String(cString: flagsResponse!) + destroy_string(UnsafeMutablePointer(mutating: flagsResponse)) + + guard let listFlagsResult = try? JSONDecoder().decode(ListFlagsResult.self, from: Data(responseString.utf8)) else { + throw ClientError.parsingError + } + + if listFlagsResult.status != "success" { + throw ClientError.evaluationFailed(message: listFlagsResult.error_message ?? "Unknown error") + } + + return listFlagsResult.result! + } + + func evaluateBatch(requests: [EvaluationRequest]) throws -> BatchEvaluationResponse { + guard let requestsData = try? JSONEncoder().encode(requests) else { + throw ClientError.invalidRequest + } + + let requestCString = strdup(String(data: requestsData, encoding: .utf8)) + + let batchResponse = evaluate_batch(engine, requestCString) + free(requestCString) + + let responseString = String(cString: batchResponse!) + destroy_string(UnsafeMutablePointer(mutating: batchResponse)) + + guard let batchResult = try? JSONDecoder().decode(BatchResult.self, from: Data(responseString.utf8)) else { + throw ClientError.parsingError + } + + if batchResult.status != "success" { + throw ClientError.evaluationFailed(message: batchResult.error_message ?? "Unknown error") + } + + return batchResult.result! + } + + + + + enum ClientError: Error { + case invalidOptions + case invalidRequest + case evaluationFailed(message: String) + case parsingError + } +} + +enum Authentication: Encodable { + case clientToken(String) + case jwtToken(String) + + // Custom Codable logic to encode/decode based on the case + private enum CodingKeys: String, CodingKey { + case client_token + case jwt_token + } + + enum AuthenticationType: String, Codable { + case clientToken, jwtToken + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .clientToken(let token): + try container.encode(token, forKey: .client_token) + case .jwtToken(let token): + try container.encode(token, forKey: .jwt_token) + } + } +} + +struct EvaluationRequest: Codable { + let flag_key: String + let entity_id: String + let context: [String: String] +} + +struct ClientTokenAuthentication: Codable { + let token: String +} + +struct JWTAuthentication: Codable { + let token: String +} + +enum FetchMode: String, Codable { + case streaming + case polling +} + +struct ClientOptions: Encodable { + let url: String + let authentication: T? + let updateInterval: Int + let reference: String + let fetchMode: FetchMode +} + +struct Flag: Codable { + let key: String + let enabled: Bool + let type: String +} + +struct VariantEvaluationResponse: Codable { + let match: Bool + let segment_keys: [String] + let reason: String + let flag_key: String + let variant_key: String + let variant_attachment: String? + let request_duration_millis: Double + let timestamp: String +} + +struct BooleanEvaluationResponse: Codable { + let enabled: Bool + let flag_key: String + let reason: String + let request_duration_millis: Double + let timestamp: String +} + +struct ErrorEvaluationResponse: Codable { + let flag_key: String + let namespace_key: String + let reason: String +} + +struct BatchEvaluationResponse: Codable { + let responses: [Response] + let request_duration_millis: Double +} + +struct Response: Codable { + let type: String + let variant_evaluation_response: VariantEvaluationResponse? + let boolean_evaluation_response: BooleanEvaluationResponse? + let error_evaluation_response: ErrorEvaluationResponse? +} + +struct Result: Codable { + let status: String + let result: R? + let error_message: String? +} + +// Specific result types +typealias VariantResult = Result +typealias BooleanResult = Result +typealias BatchResult = Result +typealias ListFlagsResult = Result<[Flag]> diff --git a/flipt-client-swift/Sources/FliptEngineFFI.xcframework/Info.plist b/flipt-client-swift/Sources/FliptEngineFFI.xcframework/Info.plist new file mode 100644 index 00000000..3e9609bd --- /dev/null +++ b/flipt-client-swift/Sources/FliptEngineFFI.xcframework/Info.plist @@ -0,0 +1,63 @@ + + + + + AvailableLibraries + + + BinaryPath + libfliptengine.a + HeadersPath + Headers + LibraryIdentifier + ios-arm64-simulator + LibraryPath + libfliptengine.a + SupportedArchitectures + + arm64 + + SupportedPlatform + ios + SupportedPlatformVariant + simulator + + + BinaryPath + libfliptengine.a + HeadersPath + Headers + LibraryIdentifier + ios-arm64 + LibraryPath + libfliptengine.a + SupportedArchitectures + + arm64 + + SupportedPlatform + ios + + + BinaryPath + libfliptengine.a + HeadersPath + Headers + LibraryIdentifier + macos-arm64 + LibraryPath + libfliptengine.a + SupportedArchitectures + + arm64 + + SupportedPlatform + macos + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + diff --git a/flipt-client-swift/Sources/FliptEngineFFI.xcframework/ios-arm64-simulator/Headers/module.modulemap b/flipt-client-swift/Sources/FliptEngineFFI.xcframework/ios-arm64-simulator/Headers/module.modulemap new file mode 100644 index 00000000..b7484109 --- /dev/null +++ b/flipt-client-swift/Sources/FliptEngineFFI.xcframework/ios-arm64-simulator/Headers/module.modulemap @@ -0,0 +1,4 @@ +module FliptEngineFFI { + header "flipt_engine.h" + export * +} diff --git a/flipt-client-swift/Sources/FliptEngineFFI.xcframework/ios-arm64/Headers/module.modulemap b/flipt-client-swift/Sources/FliptEngineFFI.xcframework/ios-arm64/Headers/module.modulemap new file mode 100644 index 00000000..b7484109 --- /dev/null +++ b/flipt-client-swift/Sources/FliptEngineFFI.xcframework/ios-arm64/Headers/module.modulemap @@ -0,0 +1,4 @@ +module FliptEngineFFI { + header "flipt_engine.h" + export * +} diff --git a/flipt-client-swift/Sources/FliptEngineFFI.xcframework/macos-arm64/Headers/module.modulemap b/flipt-client-swift/Sources/FliptEngineFFI.xcframework/macos-arm64/Headers/module.modulemap new file mode 100644 index 00000000..b7484109 --- /dev/null +++ b/flipt-client-swift/Sources/FliptEngineFFI.xcframework/macos-arm64/Headers/module.modulemap @@ -0,0 +1,4 @@ +module FliptEngineFFI { + header "flipt_engine.h" + export * +} diff --git a/flipt-client-swift/Tests/FliptClientTests/FliptClientTests.swift b/flipt-client-swift/Tests/FliptClientTests/FliptClientTests.swift new file mode 100644 index 00000000..b5a82b66 --- /dev/null +++ b/flipt-client-swift/Tests/FliptClientTests/FliptClientTests.swift @@ -0,0 +1,156 @@ +import XCTest +@testable import FliptClient + +class FliptClientTests: XCTestCase { + + var evaluationClient: FliptClient! + var fliptUrl: String = "" + var authToken: String = "" + + override func setUp() { + super.setUp() + + guard let fliptUrl = ProcessInfo.processInfo.environment["FLIPT_URL"], + let authToken = ProcessInfo.processInfo.environment["FLIPT_AUTH_TOKEN"] else { + XCTFail("FLIPT_URL and FLIPT_AUTH_TOKEN must be set") + return + } + + do { + evaluationClient = try FliptClient( + namespace: "default", + url: fliptUrl, + authentication: .clientToken(authToken) + ) + } catch { + XCTFail("Failed to initialize EvaluationClient: \(error)") + } + } + + override func tearDown() { + evaluationClient = nil + super.tearDown() + } + + func testInvalidAuthentication() { + do { + let client = try FliptClient( + namespace: "default", + url: fliptUrl, + authentication: .clientToken("invalid") + ) + + let _ = try client.evaluateVariant(flagKey: "flag1", entityID: "someentity", evalContext: ["fizz": "buzz"]) + XCTFail("Expected an error, but got none") + } catch let error as FliptClient.ClientError { + XCTAssertTrue(error.localizedDescription != "") // this could be better + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + + func testVariant() { + do { + let variant = try evaluationClient.evaluateVariant( + flagKey: "flag1", + entityID: "someentity", + evalContext: ["fizz": "buzz"] + ) + XCTAssertTrue(variant.match) + XCTAssertEqual(variant.flag_key, "flag1") + XCTAssertEqual(variant.reason, "MATCH_EVALUATION_REASON") + XCTAssertTrue(variant.segment_keys.contains("segment1")) + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + func testBoolean() { + do { + let boolean = try evaluationClient.evaluateBoolean( + flagKey: "flag_boolean", + entityID: "someentity", + evalContext: ["fizz": "buzz"] + ) + XCTAssertEqual(boolean.flag_key, "flag_boolean") + XCTAssertTrue(boolean.enabled) + XCTAssertEqual(boolean.reason, "MATCH_EVALUATION_REASON") + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + func testBatch() { + do { + let requests = [ + EvaluationRequest(flag_key: "flag1", entity_id: "someentity", context: ["fizz": "buzz"]), + EvaluationRequest(flag_key: "flag_boolean", entity_id: "someentity", context: ["fizz": "buzz"]), + EvaluationRequest(flag_key: "notfound", entity_id: "someentity", context: ["fizz": "buzz"]) + ] + + let batch = try evaluationClient.evaluateBatch(requests: requests) + XCTAssertEqual(batch.responses.count, 3) + + let variant = batch.responses[0] + XCTAssertEqual(variant.type, "VARIANT_EVALUATION_RESPONSE_TYPE") + guard let variantResp = variant.variant_evaluation_response else { + XCTFail("unexpected response type") + return + } + XCTAssertTrue(variantResp.match) + XCTAssertEqual(variantResp.flag_key, "flag1") + XCTAssertEqual(variantResp.reason, "MATCH_EVALUATION_REASON") + XCTAssertTrue(variantResp.segment_keys.contains("segment1")) + + let boolean = batch.responses[1] + XCTAssertEqual(boolean.type, "BOOLEAN_EVALUATION_RESPONSE_TYPE") + guard let boolResp = boolean.boolean_evaluation_response else { + XCTFail("unexpected response type") + return + } + XCTAssertEqual(boolResp.flag_key, "flag_boolean") + XCTAssertTrue(boolResp.enabled) + XCTAssertEqual(boolResp.reason, "MATCH_EVALUATION_REASON") + + let errorResponse = batch.responses[2] + XCTAssertEqual(errorResponse.type, "ERROR_EVALUATION_RESPONSE_TYPE") + guard let errResp = errorResponse.error_evaluation_response else { + XCTFail("unexpected response type") + return + } + XCTAssertEqual(errResp.flag_key, "notfound") + XCTAssertEqual(errResp.namespace_key, "default") + XCTAssertEqual(errResp.reason, "NOT_FOUND_ERROR_EVALUATION_REASON") + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + func testListFlags() { + do { + let flags = try evaluationClient.listFlags() + XCTAssertFalse(flags.isEmpty) + XCTAssertEqual(flags.count, 2) + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + func testVariantFailure() { + do { + let _ = try evaluationClient.evaluateVariant( + flagKey: "nonexistent", + entityID: "someentity", + evalContext: ["fizz": "buzz"] + ) + XCTFail("Expected an error, but got none") + } catch let error as FliptClient.ClientError { + XCTAssertTrue(error.localizedDescription != "") // this could be better + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + +} diff --git a/flipt-engine-ffi/build.sh b/flipt-engine-ffi/build.sh new file mode 100755 index 00000000..086e1b60 --- /dev/null +++ b/flipt-engine-ffi/build.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +cargo build -p flipt-engine-ffi --release --target=aarch64-apple-ios-sim +cargo build -p flipt-engine-ffi --release --target=aarch64-apple-ios +cargo build -p flipt-engine-ffi --release --target=aarch64-apple-darwin + +rm -rf ../flipt-client-swift/Sources/FliptEngineFFI.xcframework + +xcodebuild -create-xcframework \ + -library ../target/aarch64-apple-ios-sim/release/libfliptengine.a -headers ./include/ \ + -library ../target/aarch64-apple-ios/release/libfliptengine.a -headers ./include/ \ + -library ../target/aarch64-apple-darwin/release/libfliptengine.a -headers ./include/ \ + -output "../flipt-client-swift/Sources/FliptEngineFFI.xcframework" diff --git a/flipt-engine-ffi/include/module.modulemap b/flipt-engine-ffi/include/module.modulemap new file mode 100644 index 00000000..b7484109 --- /dev/null +++ b/flipt-engine-ffi/include/module.modulemap @@ -0,0 +1,4 @@ +module FliptEngineFFI { + header "flipt_engine.h" + export * +} diff --git a/package/ffi/main.go b/package/ffi/main.go index 91aa28d3..43f02df8 100644 --- a/package/ffi/main.go +++ b/package/ffi/main.go @@ -31,6 +31,7 @@ var ( "java-musl": javaMuslBuild, "dart": dartBuild, "csharp": csharpBuild, + "swift": swiftBuild, } sema = make(chan struct{}, 5) // defaultInclude is the default include for all builds to copy over the @@ -150,6 +151,8 @@ var ( {id: "Linux-arm64-musl", target: "aarch64-unknown-linux-musl", ext: "tar.gz", libc: musl}, {id: "Linux-x86_64-musl", target: "x86_64-unknown-linux-musl", ext: "tar.gz", libc: musl}, {id: "Darwin-arm64", target: "aarch64-apple-darwin", ext: "tar.gz", libc: both}, + {id: "iOS-arm64", target: "aarch64-apple-ios", ext: "tar.gz", libc: both}, + {id: "iOS-arm64-sim", target: "aarch64-apple-ios-sim", ext: "tar.gz", libc: both}, {id: "Darwin-x86_64", target: "x86_64-apple-darwin", ext: "tar.gz", libc: both}, {id: "Windows-x86_64", target: "x86_64-pc-windows-msvc", ext: "zip", libc: both}, } @@ -172,10 +175,10 @@ func getLatestEngineTag() (string, error) { // use github api to get the latest release of the ffi engine cmd := exec.Command("sh", "-c", ` - curl -s "https://api.github.com/repos/flipt-io/flipt-client-sdks/releases" | - jq -r '.[] | select(.tag_name | startswith("flipt-engine-ffi-")) | .tag_name' | - sort -Vr | - head -n 1 | + curl -s "https://api.github.com/repos/flipt-io/flipt-client-sdks/releases" | + jq -r '.[] | select(.tag_name | startswith("flipt-engine-ffi-")) | .tag_name' | + sort -Vr | + head -n 1 | sed "s/^flipt-engine-ffi-//" `) @@ -588,6 +591,92 @@ func csharpBuild(ctx context.Context, client *dagger.Client, hostDirectory *dagg return err } +func swiftBuild(ctx context.Context, client *dagger.Client, hostDirectory *dagger.Directory, opts ...buildOptionsFn) error { + buildOpts := buildOptions{ + libc: glibc, + } + + for _, opt := range opts { + opt(&buildOpts) + } + + pat := os.Getenv("GITHUB_TOKEN") + if pat == "" { + return errors.New("GITHUB_TOKEN environment variable must be set") + } + + var ( + encodedPAT = base64.URLEncoding.EncodeToString([]byte("pat:" + pat)) + ghToken = client.SetSecret("gh-token", encodedPAT) + ) + + var gitUserName = os.Getenv("GIT_USER_NAME") + if gitUserName == "" { + gitUserName = "flipt-bot" + } + + var gitUserEmail = os.Getenv("GIT_USER_EMAIL") + if gitUserEmail == "" { + gitUserEmail = "dev@flipt.io" + } + + git := client.Container().From("golang:1.21.3-bookworm"). // probably don't need golang here but i assume it has good deps + WithSecretVariable("GITHUB_TOKEN", ghToken). + WithExec([]string{"git", "config", "--global", "user.email", gitUserEmail}). + WithExec([]string{"git", "config", "--global", "user.name", gitUserName}). + WithExec([]string{"sh", "-c", `git config --global http.https://github.com/.extraheader "AUTHORIZATION: Basic ${GITHUB_TOKEN}"`}) + + repository := git. + WithExec([]string{"git", "clone", "https://github.com/flipt-io/flipt-client-sdks.git", "/src"}). + WithWorkdir("/src"). + WithFile("Sources/FliptEngineFFI.xcframework/ios-arm64/Headers/flipt_engine.h", hostDirectory.File("flipt-engine-ffi/include/flipt_engine.h")). + WithFile("Sources/FliptEngineFFI.xcframework/ios-arm64-simulator/Headers/flipt_engine.h", hostDirectory.File("flipt-engine-ffi/include/flipt_engine.h")). + WithFile("Sources/FliptEngineFFI.xcframework/ios-arm64/libfliptengine.a", hostDirectory.File("target/aarch64-apple-ios/release/libfliptengine.a")). + WithFile("Sources/FliptEngineFFI.xcframework/ios-arm64-simulator/libfliptengine.a", hostDirectory.File("target/aarch64-apple-ios-sim/release/libfliptengine.a")) + + filtered := repository. + WithEnvVariable("FILTER_BRANCH_SQUELCH_WARNING", "1"). + WithExec([]string{"git", "filter-branch", "-f", "--prune-empty", + "--subdirectory-filter", "flipt-client-swift", + "--tree-filter", "cp -r /tmp/ext .", + "--", tag}) + + _, err := filtered.Sync(ctx) + if !push { + return err + } + + if tag == "" { + return fmt.Errorf("tag is not set") + } + + const tagPrefix = "refs/tags/flipt-client-ios-" + if !strings.HasPrefix(tag, tagPrefix) { + return fmt.Errorf("tag %q must start with %q", tag, tagPrefix) + } + + // because of how Go modules work, we need to create a new repo that contains + // only the go client code. This is because the go client code is in a subdirectory + // we also need to copy the ext directory into the tag of this new repo so that it can be used + targetRepo := os.Getenv("TARGET_REPO") + if targetRepo == "" { + targetRepo = "https://github.com/flipt-io/flipt-client-swift.git" + } + + targetTag := strings.TrimPrefix(tag, tagPrefix) + + // push to target repo/tag + _, err = filtered.WithExec([]string{ + "git", + "push", + "-f", + targetRepo, + fmt.Sprintf("%s:%s", tag, targetTag)}). + Sync(ctx) + + return err +} + func isDirEmptyOrNotExist(path string) (bool, error) { f, err := os.Open(path) if err != nil {