diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..f96b514 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,14 @@ +changelog: + categories: + - title: SemVer Major + labels: + - ⚠️ semver/major + - title: SemVer Minor + labels: + - semver/minor + - title: SemVer Patch + labels: + - semver/patch + - title: Other Changes + labels: + - semver/none diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..fa1d4b0 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,18 @@ +name: Main + +on: + push: + branches: [main] + schedule: + - cron: "0 8,20 * * *" + +jobs: + unit-tests: + name: Unit tests + uses: apple/swift-nio/.github/workflows/unit_tests.yml@main + with: + linux_5_9_arguments_override: "--explicit-target-dependency-import-check error" + linux_5_10_arguments_override: --explicit-target-dependency-import-check error" + linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..6215da3 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,58 @@ +name: PR + +on: + pull_request: + types: [opened, reopened, synchronize] + +jobs: + soundness: + name: Soundness + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main + with: + license_header_check_project_name: "swift-etcd-client-gsoc" + + unit-tests: + name: Unit tests + uses: apple/swift-nio/.github/workflows/unit_tests.yml@main + with: + linux_5_9_arguments_override: "--explicit-target-dependency-import-check error" + linux_5_10_arguments_override: "--explicit-target-dependency-import-check error" + linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + + # Integration tests use a running etcd so this job uses a service container. + integration-tests: + name: Integration tests + runs-on: ubuntu-latest + services: + etcd: + image: quay.io/coreos/etcd:v3.5.6 + env: + ETCD_ADVERTISE_CLIENT_URLS: http://0.0.0.0:2379 + ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379 + ETCD_INITIAL_CLUSTER_STATE: new + ports: + - 2379:2379 + container: + image: swift:6.0-noble + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Build package + run: swift build --build-tests + - name: Run integration tests + shell: bash # explicitly choose bash, which ensures -o pipefail + run: swift test --filter "IntegrationTests" | tee test.out + env: + SWIFT_ETCD_CLIENT_INTEGRATION_TEST_ENABLED: true + ETCD_HOST: etcd + ETCD_PORT: 2379 + - name: Check integration tests actually did run + run: test -r test.out && ! grep -i -e "executed 0 tests" -e "skipped" test.out + + cxx-interop: + name: Cxx interop + uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main diff --git a/.github/workflows/pull_request_label.yml b/.github/workflows/pull_request_label.yml new file mode 100644 index 0000000..8fd47c1 --- /dev/null +++ b/.github/workflows/pull_request_label.yml @@ -0,0 +1,18 @@ +name: PR label + +on: + pull_request: + types: [labeled, unlabeled, opened, reopened, synchronize] + +jobs: + semver-label-check: + name: Semantic version label check + runs-on: ubuntu-latest + timeout-minutes: 1 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Check for Semantic Version label + uses: apple/swift-nio/.github/actions/pull_request_semver_label_checker@main diff --git a/.licenseignore b/.licenseignore new file mode 100644 index 0000000..2e49a7e --- /dev/null +++ b/.licenseignore @@ -0,0 +1,40 @@ +.gitignore +**/.gitignore +.licenseignore +.gitattributes +.git-blame-ignore-revs +.mailfilter +.mailmap +.spi.yml +.swiftformat +.swift-format +.swiftformatignore +.editorconfig +.github/* +*.md +*.txt +*.yml +*.yaml +*.json +*.proto +*.pb.swift +*.grpc.swift +Package.swift +**/Package.swift +Package@-*.swift +**/Package@-*.swift +Package.resolved +**/Package.resolved +Makefile +*.modulemap +**/*.modulemap +**/*.docc/* +*.xcprivacy +**/*.xcprivacy +*.symlink +**/*.symlink +Dockerfile +**/Dockerfile +Snippets/* +dev/git.commit.template +.unacceptablelanguageignore diff --git a/Package.swift b/Package.swift index 8b21c53..86cd67c 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 5.9 //===----------------------------------------------------------------------===// // // This source file is part of the swift-etcd-client-gsoc open source project @@ -27,7 +27,7 @@ let package = Package( .library( name: "ETCD", targets: ["ETCD"] - ), + ) ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.56.0"), @@ -59,3 +59,9 @@ let package = Package( ), ] ) + +for target in package.targets { + var settings = target.swiftSettings ?? [] + settings.append(.enableExperimentalFeature("StrictConcurrency=complete")) + target.swiftSettings = settings +} diff --git a/Sources/ETCD/DeleteRangeRequest.swift b/Sources/ETCD/DeleteRangeRequest.swift index f831082..1d5346e 100644 --- a/Sources/ETCD/DeleteRangeRequest.swift +++ b/Sources/ETCD/DeleteRangeRequest.swift @@ -19,13 +19,13 @@ public struct DeleteRangeRequest { public var key: Data public var rangeEnd: Data? public var prevKV: Bool = false - + init(protoDeleteRangeRequest: Etcdserverpb_DeleteRangeRequest) { self.key = protoDeleteRangeRequest.key self.rangeEnd = protoDeleteRangeRequest.rangeEnd.isEmpty ? nil : protoDeleteRangeRequest.rangeEnd self.prevKV = protoDeleteRangeRequest.prevKv } - + /// Struct representing a deleteRangeRequest in etcd. /// /// - Parameters: @@ -37,7 +37,7 @@ public struct DeleteRangeRequest { self.rangeEnd = rangeEnd self.prevKV = prevKV } - + func toProto() -> Etcdserverpb_DeleteRangeRequest { var protoDeleteRangeRequest = Etcdserverpb_DeleteRangeRequest() protoDeleteRangeRequest.key = self.key diff --git a/Sources/ETCD/ETCDClient.swift b/Sources/ETCD/ETCDClient.swift index 1ba08cc..6072031 100644 --- a/Sources/ETCD/ETCDClient.swift +++ b/Sources/ETCD/ETCDClient.swift @@ -110,7 +110,10 @@ public final class EtcdClient: @unchecked Sendable { /// - Parameters: /// - key: The key for which the value is watched. Parameter is of type Sequence. /// - operation: The operation to be run on the WatchAsyncSequence for the key. - public func watch(_ key: some Sequence, _ operation: (WatchAsyncSequence) async throws -> Result) async throws -> Result { + public func watch( + _ key: some Sequence, + _ operation: (WatchAsyncSequence) async throws -> Result + ) async throws -> Result { let request = [Etcdserverpb_WatchRequest.with { $0.createRequest.key = Data(key) }] let watchAsyncSequence = WatchAsyncSequence(grpcAsyncSequence: watchClient.watch(request)) return try await operation(watchAsyncSequence) @@ -121,7 +124,10 @@ public final class EtcdClient: @unchecked Sendable { /// - Parameters: /// - key: The key for which the value is watched. Parameter is of type String. /// - operation: The operation to be run on the WatchAsyncSequence for the key. - public func watch(_ key: String, _ operation: (WatchAsyncSequence) async throws -> Result) async throws -> Result { + public func watch( + _ key: String, + _ operation: (WatchAsyncSequence) async throws -> Result + ) async throws -> Result { try await watch(key.utf8, operation) } } diff --git a/Sources/ETCDExample/ETCDExample.swift b/Sources/ETCDExample/ETCDExample.swift index 9b5eef0..5ee22d3 100644 --- a/Sources/ETCDExample/ETCDExample.swift +++ b/Sources/ETCDExample/ETCDExample.swift @@ -1,5 +1,3 @@ -import Dispatch - //===----------------------------------------------------------------------===// // // This source file is part of the swift-etcd-client-gsoc open source project @@ -13,6 +11,8 @@ import Dispatch // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// + +import Dispatch import ETCD import NIO diff --git a/Tests/ETCDTests/ETCDTests.swift b/Tests/IntegrationTests/IntegrationTests.swift similarity index 76% rename from Tests/ETCDTests/ETCDTests.swift rename to Tests/IntegrationTests/IntegrationTests.swift index 178d925..16918f3 100644 --- a/Tests/ETCDTests/ETCDTests.swift +++ b/Tests/IntegrationTests/IntegrationTests.swift @@ -1,6 +1,3 @@ -import ETCD -import NIO - //===----------------------------------------------------------------------===// // // This source file is part of the swift-etcd-client-gsoc open source project @@ -14,18 +11,22 @@ import NIO // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// + +import ETCD +import NIO import XCTest -final class EtcdClientTests: XCTestCase { - var eventLoopGroup: EventLoopGroup! - var etcdClient: EtcdClient! +final class IntegrationTests: XCTestCase { + + private static let integrationTestEnabled = getBoolEnv("SWIFT_ETCD_CLIENT_INTEGRATION_TEST_ENABLED") ?? false override func setUp() async throws { - eventLoopGroup = MultiThreadedEventLoopGroup.singleton - etcdClient = EtcdClient(host: "localhost", port: 2379, eventLoopGroup: eventLoopGroup) + try XCTSkipUnless(Self.integrationTestEnabled) } func testSetAndGetStringValue() async throws { + let etcdClient = EtcdClient.testClient + try await etcdClient.set("testKey", value: "testValue") let key = "testKey".data(using: .utf8)! let rangeRequest = RangeRequest(key: key) @@ -36,6 +37,8 @@ final class EtcdClientTests: XCTestCase { } func testGetNonExistentKey() async throws { + let etcdClient = EtcdClient.testClient + let key = "nonExistentKey".data(using: .utf8)! let rangeRequest = RangeRequest(key: key) let result = try await etcdClient.getRange(rangeRequest) @@ -43,6 +46,8 @@ final class EtcdClientTests: XCTestCase { } func testDeleteKeyExists() async throws { + let etcdClient = EtcdClient.testClient + let key = "testKey" let value = "testValue" try await etcdClient.set(key, value: value) @@ -54,12 +59,14 @@ final class EtcdClientTests: XCTestCase { let deleteRangeRequest = DeleteRangeRequest(key: rangeRequestKey) try await etcdClient.deleteRange(deleteRangeRequest) - + fetchedValue = try await etcdClient.getRange(rangeRequest) XCTAssertNil(fetchedValue) } func testDeleteNonExistentKey() async throws { + let etcdClient = EtcdClient.testClient + let key = "testKey".data(using: .utf8)! let rangeRequest = RangeRequest(key: key) @@ -68,12 +75,14 @@ final class EtcdClientTests: XCTestCase { let deleteRangeRequest = DeleteRangeRequest(key: key) try await etcdClient.deleteRange(deleteRangeRequest) - + fetchedValue = try await etcdClient.getRange(rangeRequest) XCTAssertNil(fetchedValue) } func testUpdateExistingKey() async throws { + let etcdClient = EtcdClient.testClient + let key = "testKey" let value = "testValue" try await etcdClient.set(key, value: value) @@ -95,6 +104,8 @@ final class EtcdClientTests: XCTestCase { } func testWatch() async throws { + let etcdClient = EtcdClient.testClient + let key = "testKey" let value = "testValue".data(using: .utf8)! @@ -102,7 +113,7 @@ final class EtcdClientTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try await self.etcdClient.watch(key) { watchAsyncSequence in + try await etcdClient.watch(key) { watchAsyncSequence in var iterator = watchAsyncSequence.makeAsyncIterator() let events = try await iterator.next() guard let events = events else { @@ -122,8 +133,27 @@ final class EtcdClientTests: XCTestCase { } try await Task.sleep(nanoseconds: 1_000_000_000) - try await self.etcdClient.put(key, value: String(data: value, encoding: .utf8)!) + try await etcdClient.put(key, value: String(data: value, encoding: .utf8)!) group.cancelAll() } } } + +extension EtcdClient { + fileprivate static let testClient = EtcdClient( + host: ProcessInfo.processInfo.environment["ETCD_HOST"] ?? "localhost", + port: getIntEnv("ETCD_PORT") ?? 2379, + eventLoopGroup: .singletonMultiThreadedEventLoopGroup + ) +} + +/// Returns true if `key` is a truthy string, otherwise returns false. +private func getBoolEnv(_ key: String) -> Bool? { + switch ProcessInfo.processInfo.environment[key]?.lowercased() { + case .none: return nil + case "true", "y", "yes", "on", "1": return true + default: return false + } +} + +private func getIntEnv(_ key: String) -> Int? { ProcessInfo.processInfo.environment[key].flatMap(Int.init(_:)) } diff --git a/scripts/generate_contributors_list.sh b/scripts/generate_contributors_list.sh deleted file mode 100755 index c90c5ed..0000000 --- a/scripts/generate_contributors_list.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the swift-etcd-client-gsoc open source project -## -## Copyright (c) 2024 Apple Inc. and the swift-etcd-client-gsoc project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of swift-etcd-client-gsoc project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -set -eu -here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -contributors=$( cd "$here"/.. && git shortlog -es | cut -f2 | sed 's/^/- /' ) - -cat > "$here/../CONTRIBUTORS.txt" <<- EOF - For the purpose of tracking copyright, this is the list of individuals and - organizations who have contributed source code to swift-etcd-client-gsoc. - - For employees of an organization/company where the copyright of work done - by employees of that company is held by the company itself, only the company - needs to be listed here. - - ## COPYRIGHT HOLDERS - - - Apple Inc. (all contributors with '@apple.com') - - ### Contributors - - $contributors - - **Updating this list** - - Please do not edit this file manually. It is generated using \`./scripts/generate_contributors_list.sh\`. If a name is misspelled or appearing multiple times: add an entry in \`./.mailmap\` -EOF diff --git a/scripts/run-swift-format.sh b/scripts/run-swift-format.sh deleted file mode 100755 index 17aa008..0000000 --- a/scripts/run-swift-format.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the swift-etcd-client-gsoc open source project -## -## Copyright (c) 2024 Apple Inc. and the swift-etcd-client-gsoc project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of swift-etcd-client-gsoc project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftCertificates open source project -## -## Copyright (c) 2023 Apple Inc. and the SwiftCertificates project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftCertificates project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftOpenAPIGenerator open source project -## -## Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## -set -euo pipefail - -function log() { printf -- "** %s\n" "$*" >&2; } -function error() { printf -- "** ERROR: %s\n" "$*" >&2; } -function fatal() { error "$*"; exit 1; } - -current_script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -repo_root="$(git -C "${current_script_dir}" rev-parse --show-toplevel)" - -swift format lint \ - --parallel --recursive --strict \ - "${repo_root}/Sources" "${repo_root}/Tests" \ - "${repo_root}/Benchmarks/Benchmarks" \ - && swift_format_rc=$? || swift_format_rc=$? - -if [[ "${swift_format_rc}" -ne 0 ]]; then - fatal "❌ Running swift-format produced errors. - - To fix, run the following command: - - % swift format format --parallel --recursive --in-place Sources Tests Benchmarks/Benchmarks - " - exit "${swift_format_rc}" -fi - -log "✅ Ran swift-format with no errors." diff --git a/scripts/soundness.sh b/scripts/soundness.sh deleted file mode 100755 index 45171f9..0000000 --- a/scripts/soundness.sh +++ /dev/null @@ -1,175 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the swift-etcd-client-gsoc open source project -## -## Copyright (c) 2023 Apple Inc. and the swift-etcd-client-gsoc project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of swift-etcd-client-gsoc project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -set -eu -here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -function replace_acceptable_years() { - # this needs to replace all acceptable forms with 'YEARS' - sed -e 's/20[12][78901234]-20[12][8901234]/YEARS/' -e 's/20[12][8901234]/YEARS/' -} - -printf "=> Checking for unacceptable language... " -# This greps for unacceptable terminology. The square bracket[s] are so that -# "git grep" doesn't find the lines that greps :). -unacceptable_terms=( - -e blacklis[t] - -e whitelis[t] - -e slav[e] - -e sanit[y] -) - -# We have to exclude the code of conduct because it gives examples of unacceptable language. -exclude_files=( - CODE_OF_CONDUCT.md -) -for word in "${exclude_files[@]}"; do - exclude_files+=(":(exclude)$word") -done -exclude_files_str=$(printf " %s" "${exclude_files[@]}") - -if git grep --color=never -i "${unacceptable_terms[@]}" -- . $exclude_files_str > /dev/null; then - printf "\033[0;31mUnacceptable language found.\033[0m\n" - git grep -i "${unacceptable_terms[@]}" -- . $exclude_files_str - exit 1 -fi -printf "\033[0;32mokay.\033[0m\n" - -printf "=> Checking format... " -# swift-format -swift_format_script="${here}/run-swift-format.sh" -if ! bash "${swift_format_script}"; then - exit 1 -fi - - -printf "=> Checking license headers... " -tmp=$(mktemp /tmp/.swift-etcd-client-gsoc-soundness_XXXXXX) - -for language in swift-or-c bash dtrace python; do - declare -a matching_files - declare -a exceptions - expections=( ) - matching_files=( -name '*' ) - case "$language" in - swift-or-c) - exceptions=( -name Package.swift ) - matching_files=( -name '*.swift' -o -name '*.c' -o -name '*.h' ) - cat > "$tmp" <<"EOF" -//===----------------------------------------------------------------------===// -// -// This source file is part of the swift-etcd-client-gsoc open source project -// -// Copyright (c) YEARS Apple Inc. and the swift-etcd-client-gsoc project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of swift-etcd-client-gsoc project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// -EOF - ;; - bash) - matching_files=( -name '*.sh' ) - cat > "$tmp" <<"EOF" -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the swift-etcd-client-gsoc open source project -## -## Copyright (c) YEARS Apple Inc. and the swift-etcd-client-gsoc project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of swift-etcd-client-gsoc project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## -EOF - ;; - python) - matching_files=( -name '*.py' ) - cat > "$tmp" <<"EOF" -#!/usr/bin/env python3 -##===----------------------------------------------------------------------===## -## -## This source file is part of the swift-etcd-client-gsoc open source project -## -## Copyright (c) YEARS Apple Inc. and the swift-etcd-client-gsoc project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of swift-etcd-client-gsoc project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## -EOF - ;; - dtrace) - matching_files=( -name '*.d' ) - cat > "$tmp" <<"EOF" -#!/usr/sbin/dtrace -q -s -/*===----------------------------------------------------------------------===* - * - * This source file is part of the swift-etcd-client-gsoc open source project - * - * Copyright (c) YEARS Apple Inc. and the swift-etcd-client-gsoc project authors - * Licensed under Apache License v2.0 - * - * See LICENSE.txt for license information - * See CONTRIBUTORS.txt for the list of swift-etcd-client-gsoc project authors - * - * SPDX-License-Identifier: Apache-2.0 - * - *===----------------------------------------------------------------------===*/ -EOF - ;; - *) - echo >&2 "ERROR: unknown language '$language'" - ;; - esac - - expected_lines=$(cat "$tmp" | wc -l) - expected_sha=$(cat "$tmp" | shasum) - - ( - cd "$here/.." - { - find . \ - \( \! -path '*/.build/*' -a \ - \( "${matching_files[@]}" \) -a \ - \( \! \( "${exceptions[@]}" \) \) \) - - if [[ "$language" = bash ]]; then - # add everything with a shell shebang too - git grep --full-name -l '#!/bin/bash' - git grep --full-name -l '#!/bin/sh' - fi - } | while read line; do - if [[ "$(cat "$line" | replace_acceptable_years | head -n $expected_lines | shasum)" != "$expected_sha" ]]; then - printf "\033[0;31mmissing headers in file '$line'!\033[0m\n" - diff -u <(cat "$line" | replace_acceptable_years | head -n $expected_lines) "$tmp" - exit 1 - fi - done - printf "\033[0;32mokay.\033[0m\n" - ) -done - -rm "$tmp"