* moved CI to Xcode 14.2
* added flaky-tests and clusters api support * rewrote transport tests. they might work? * more sendable conformances
This commit is contained in:
parent
0a12829df3
commit
23cea0da0e
|
@ -11,15 +11,13 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
xcode:
|
||||
- '14.0.1'
|
||||
- '14.2'
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run tests
|
||||
run: make test-library-xcode
|
||||
- name: Compile documentation
|
||||
run: make test-docs
|
||||
|
||||
library-linux:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -33,8 +31,8 @@ jobs:
|
|||
examples:
|
||||
runs-on: macos-12
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_14.0.app/Contents/Developer
|
||||
DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run tests
|
||||
run: make test-examples
|
||||
run: make build-examples
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// swift-tools-version:5.6
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
|
|
|
@ -32,7 +32,8 @@ extension BuildkiteClient: AsyncMiddleware {
|
|||
}
|
||||
|
||||
guard
|
||||
let secretKey = Environment
|
||||
let secretKey =
|
||||
Environment
|
||||
.get("BUILDKITE_WEBHOOK_SECRET")?
|
||||
.data(using: .utf8)
|
||||
else {
|
||||
|
@ -42,12 +43,14 @@ extension BuildkiteClient: AsyncMiddleware {
|
|||
)
|
||||
}
|
||||
|
||||
let replayLimit = Environment
|
||||
let replayLimit =
|
||||
Environment
|
||||
.get("BUILDKITE_WEBHOOK_REPLAY_LIMIT")
|
||||
.flatMap(TimeInterval.init)
|
||||
|
||||
if let signature = request.headers.buildkiteSignature,
|
||||
let payload = request.body.data {
|
||||
let payload = request.body.data
|
||||
{
|
||||
try validateWebhookPayload(
|
||||
signatureHeader: signature,
|
||||
body: Data(buffer: payload),
|
||||
|
|
|
@ -15,7 +15,7 @@ import Vapor
|
|||
let app = try Application(.detect())
|
||||
defer { app.shutdown() }
|
||||
|
||||
let buildkite = BuildkiteClient(/* transport: app.client */)
|
||||
let buildkite = BuildkiteClient( /* transport: app.client */)
|
||||
|
||||
app.group(buildkite) {
|
||||
$0.post("buildkite_webhook") { req in
|
||||
|
|
82
Makefile
82
Makefile
|
@ -14,66 +14,75 @@ FORMAT_PATHS := $(GIT_REPO_TOPLEVEL)/Examples $(GIT_REPO_TOPLEVEL)/Package.swift
|
|||
|
||||
# Tasks
|
||||
|
||||
.PHONY: default
|
||||
default: test-all
|
||||
|
||||
test-all: test-library test-library-xcode test-examples
|
||||
.PHONY: test-all
|
||||
test-all: test-library test-library-xcode build-examples
|
||||
|
||||
.PHONY: test-library
|
||||
test-library:
|
||||
swift test --parallel
|
||||
|
||||
test-library-xcode:
|
||||
.PHONY: test-library-xcode
|
||||
test-library-xcode: test-library-xcode-ios test-library-xcode-macos test-library-xcode-tvos test-library-xcode-watchos
|
||||
|
||||
.PHONY: test-library-xcode-ios
|
||||
test-library-xcode-ios:
|
||||
xcodebuild test \
|
||||
-scheme Buildkite \
|
||||
-destination "$(DESTINATION_PLATFORM_IOS_SIMULATOR)" \
|
||||
-quiet
|
||||
|
||||
.PHONY: test-library-xcode-macos
|
||||
test-library-xcode-macos:
|
||||
xcodebuild test \
|
||||
-scheme Buildkite \
|
||||
-destination "$(DESTINATION_PLATFORM_MACOS)" \
|
||||
-quiet
|
||||
|
||||
.PHONY: test-library-xcode-tvos
|
||||
test-library-xcode-tvos:
|
||||
xcodebuild test \
|
||||
-scheme Buildkite \
|
||||
-destination "$(DESTINATION_PLATFORM_TVOS_SIMULATOR)" \
|
||||
-quiet
|
||||
|
||||
.PHONY: test-library-xcode-watchos
|
||||
test-library-xcode-watchos:
|
||||
xcodebuild \
|
||||
-scheme Buildkite \
|
||||
-destination "$(DESTINATION_PLATFORM_WATCHOS_SIMULATOR)" \
|
||||
-quiet
|
||||
|
||||
test-examples:
|
||||
xcodebuild build \
|
||||
-scheme simple \
|
||||
-destination "$(DESTINATION_PLATFORM_MACOS)" \
|
||||
-quiet
|
||||
xcodebuild build \
|
||||
-scheme graphql \
|
||||
-destination "$(DESTINATION_PLATFORM_MACOS)" \
|
||||
-quiet
|
||||
xcodebuild build \
|
||||
-scheme advanced-authorization \
|
||||
-destination "$(DESTINATION_PLATFORM_MACOS)" \
|
||||
-quiet
|
||||
xcodebuild build \
|
||||
-scheme test-analytics \
|
||||
-destination "$(DESTINATION_PLATFORM_MACOS)" \
|
||||
-quiet
|
||||
xcodebuild build \
|
||||
-scheme webhooks \
|
||||
-destination "$(DESTINATION_PLATFORM_MACOS)" \
|
||||
-quiet
|
||||
.PHONY: build-examples
|
||||
build-examples: build-examples-all
|
||||
|
||||
DOC_WARNINGS := $(shell xcodebuild clean docbuild \
|
||||
-scheme Buildkite \
|
||||
-destination "$(DESTINATION_PLATFORM_MACOS)" \
|
||||
-quiet \
|
||||
2>&1 \
|
||||
| grep "couldn't be resolved to known documentation" \
|
||||
| sed 's|$(PWD)|.|g' \
|
||||
| tr '\n' '\1')
|
||||
test-docs:
|
||||
@test "$(DOC_WARNINGS)" = "" \
|
||||
|| (echo "xcodebuild docbuild failed:\n\n$(DOC_WARNINGS)" | tr '\1' '\n' \
|
||||
&& exit 1)
|
||||
.PHONY: build-examples-all
|
||||
build-examples-all:
|
||||
swift build --package-path Examples
|
||||
|
||||
.PHONY: build-examples-simple
|
||||
build-examples-simple:
|
||||
swift build --package-path Examples --product simple
|
||||
|
||||
.PHONY: build-examples-graphql
|
||||
build-examples-graphql:
|
||||
swift build --package-path Examples --product graphql
|
||||
|
||||
.PHONY: build-examples-advanced-authorization
|
||||
build-examples-advanced-authorization:
|
||||
swift build --package-path Examples --product advanced-authorization
|
||||
|
||||
.PHONY: build-examples-test-analytics
|
||||
build-examples-test-analytics:
|
||||
swift build --package-path Examples --product test-analytics
|
||||
|
||||
.PHONY: build-examples-webhooks
|
||||
build-examples-webhooks:
|
||||
swift build --package-path Examples --product webhooks
|
||||
|
||||
.PHONY: format
|
||||
format:
|
||||
$(SWIFT_FORMAT_BIN) \
|
||||
--configuration $(SWIFT_FORMAT_CONFIG_FILE) \
|
||||
|
@ -82,11 +91,10 @@ format:
|
|||
--recursive \
|
||||
$(FORMAT_PATHS)
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
$(SWIFT_FORMAT_BIN) lint \
|
||||
--configuration $(SWIFT_FORMAT_CONFIG_FILE) \
|
||||
--ignore-unparsable-files \
|
||||
--recursive \
|
||||
$(FORMAT_PATHS)
|
||||
|
||||
.PHONY: format lint test-all test-library test-library-xcode test-examples test-docs
|
||||
|
|
|
@ -17,8 +17,7 @@ let package = Package(
|
|||
)
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apple/swift-crypto.git", .upToNextMajor(from: "2.0.0")),
|
||||
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
|
||||
.package(url: "https://github.com/apple/swift-crypto.git", .upToNextMajor(from: "2.0.0"))
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
|
|
@ -73,7 +73,7 @@ public struct Artifact: Codable, Equatable, Hashable, Identifiable, Sendable {
|
|||
case deleted
|
||||
}
|
||||
|
||||
public struct URLs: Codable, Equatable {
|
||||
public struct URLs: Codable, Equatable, Sendable {
|
||||
public var url: URL
|
||||
|
||||
public init(
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
//
|
||||
// Cluster.swift
|
||||
// Buildkite
|
||||
//
|
||||
// Created by Aaron Sky on 6/18/23.
|
||||
// Copyright © 2023 Aaron Sky. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
/// A cluster is an isolated set of agents and pipelines within an organization.
|
||||
|
||||
public struct Cluster: Codable, Equatable, Hashable, Identifiable, Sendable {
|
||||
/// ID of the cluster.
|
||||
public var id: UUID
|
||||
/// ID of the cluster to be used with the GraphQL API.
|
||||
public var graphqlId: String
|
||||
/// ID of the cluster's default queue. Agents that connect to the cluster without specifying a queue will accept jobs from this queue.
|
||||
public var defaultQueueId: UUID
|
||||
/// Name of the cluster.
|
||||
public var name: String
|
||||
/// Description of the cluster.
|
||||
public var description: String
|
||||
/// Emoji for the cluster using the emoji syntax.
|
||||
public var emoji: String
|
||||
/// Color hex code for the cluster.
|
||||
public var color: String
|
||||
/// Followable URL to fetch this specific cluster.
|
||||
public var url: Followable<Cluster.Resources.Get>
|
||||
/// Human-readable URL of this cluster in the Buildkite dashboard.
|
||||
public var webURL: URL
|
||||
/// Followable URL to fetch this cluster's queues.
|
||||
public var queuesURL: Followable<ClusterQueue.Resources.List>
|
||||
/// Followable URL to fetch this cluster's default queue.
|
||||
public var defaultQueueURL: Followable<ClusterQueue.Resources.Get>
|
||||
/// When the cluster was created.
|
||||
public var createdAt: Date
|
||||
/// User who created the cluster.
|
||||
public var createdBy: User?
|
||||
|
||||
public init(
|
||||
id: UUID,
|
||||
graphqlId: String,
|
||||
defaultQueueId: UUID,
|
||||
name: String,
|
||||
description: String,
|
||||
emoji: String,
|
||||
color: String,
|
||||
url: Followable<Cluster.Resources.Get>,
|
||||
webURL: URL,
|
||||
queuesURL: Followable<ClusterQueue.Resources.List>,
|
||||
defaultQueueURL: Followable<ClusterQueue.Resources.Get>,
|
||||
createdAt: Date,
|
||||
createdBy: User? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.graphqlId = graphqlId
|
||||
self.defaultQueueId = defaultQueueId
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.emoji = emoji
|
||||
self.color = color
|
||||
self.url = url
|
||||
self.webURL = webURL
|
||||
self.queuesURL = queuesURL
|
||||
self.defaultQueueURL = defaultQueueURL
|
||||
self.createdAt = createdAt
|
||||
self.createdBy = createdBy
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case graphqlId = "graphql_id"
|
||||
case defaultQueueId = "default_queue_id"
|
||||
case name
|
||||
case description
|
||||
case emoji
|
||||
case color
|
||||
case url
|
||||
case webURL = "web_url"
|
||||
case queuesURL = "queues_url"
|
||||
case defaultQueueURL = "default_queue_url"
|
||||
case createdAt = "created_at"
|
||||
case createdBy = "created_by"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
//
|
||||
// ClusterQueue.swift
|
||||
// Buildkite
|
||||
//
|
||||
// Created by Aaron Sky on 6/18/23.
|
||||
// Copyright © 2023 Aaron Sky. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
/// Cluster queues are discrete groups of agents within a cluster. Pipelines in that cluster can target cluster queues to run jobs on agents assigned to those queues.
|
||||
public struct ClusterQueue: Codable, Equatable, Hashable, Identifiable, Sendable {
|
||||
/// ID of the cluster.
|
||||
public var id: UUID
|
||||
/// ID of the cluster to be used with the GraphQL API.
|
||||
public var graphqlId: String
|
||||
/// The queue key.
|
||||
public var key: String
|
||||
/// Description of the queue.
|
||||
public var description: String
|
||||
/// Followable URL to fetch this specific queue.
|
||||
public var url: Followable<ClusterQueue.Resources.Get>
|
||||
/// Human-readable URL of this queue in the Buildkite dashboard.
|
||||
public var webURL: URL
|
||||
/// Followable URL to fetch the cluster the queue belongs to.
|
||||
public var clusterURL: Followable<Cluster.Resources.Get>
|
||||
/// Indicates whether the queue has paused dispatching jobs to associated agents.
|
||||
public var dispatchPaused: Bool
|
||||
/// User who paused the queue.
|
||||
public var dispatchPausedBy: User?
|
||||
/// When the queue was paused.
|
||||
public var dispatchPausedAt: Date?
|
||||
/// The note left when the queue was paused.
|
||||
public var dispatchPausedNote: String?
|
||||
/// When the queue was created.
|
||||
public var createdAt: Date
|
||||
/// User who created the queue.
|
||||
public var createdBy: User?
|
||||
|
||||
public init(
|
||||
id: UUID,
|
||||
graphqlId: String,
|
||||
key: String,
|
||||
description: String,
|
||||
url: Followable<ClusterQueue.Resources.Get>,
|
||||
webURL: URL,
|
||||
clusterURL: Followable<Cluster.Resources.Get>,
|
||||
dispatchPaused: Bool,
|
||||
dispatchPausedBy: User? = nil,
|
||||
dispatchPausedAt: Date? = nil,
|
||||
dispatchPausedNote: String? = nil,
|
||||
createdAt: Date,
|
||||
createdBy: User? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.graphqlId = graphqlId
|
||||
self.key = key
|
||||
self.description = description
|
||||
self.url = url
|
||||
self.webURL = webURL
|
||||
self.clusterURL = clusterURL
|
||||
self.dispatchPaused = dispatchPaused
|
||||
self.dispatchPausedBy = dispatchPausedBy
|
||||
self.dispatchPausedAt = dispatchPausedAt
|
||||
self.dispatchPausedNote = dispatchPausedNote
|
||||
self.createdAt = createdAt
|
||||
self.createdBy = createdBy
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case graphqlId = "graphql_id"
|
||||
case key
|
||||
case description
|
||||
case url
|
||||
case webURL = "web_url"
|
||||
case clusterURL = "cluster_url"
|
||||
case dispatchPaused = "dispatch_paused"
|
||||
case dispatchPausedBy = "dispatch_paused_by"
|
||||
case dispatchPausedAt = "dispatch_paused_at"
|
||||
case dispatchPausedNote = "dispatch_paused_note"
|
||||
case createdAt = "created_at"
|
||||
case createdBy = "created_by"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
//
|
||||
// ClusterToken.swift
|
||||
// Buildkite
|
||||
//
|
||||
// Created by Aaron Sky on 6/18/23.
|
||||
// Copyright © 2023 Aaron Sky. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
/// A cluster token is used to connect agents to a cluster.
|
||||
public struct ClusterToken: Codable, Equatable, Hashable, Identifiable, Sendable {
|
||||
/// ID of the cluster token.
|
||||
public var id: UUID
|
||||
/// ID of the token to be used with the GraphQL API.
|
||||
public var graphqlId: String
|
||||
/// Description of the token.
|
||||
public var description: String
|
||||
/// Followable URL to fetch this specific cluster token..
|
||||
public var url: Followable<ClusterToken.Resources.Get>
|
||||
/// Followable URL to the cluster the token belongs to..
|
||||
public var clusterURL: Followable<Cluster.Resources.Get>
|
||||
/// When the token was created.
|
||||
public var createdAt: Date
|
||||
/// User who created the token.
|
||||
public var createdBy: User?
|
||||
/// The token.
|
||||
///
|
||||
/// To ensure the security of tokens, the value is only included in the response for the request to create the token.
|
||||
/// Subsequent responses do not contain the token value.
|
||||
public var token: String?
|
||||
|
||||
public init(
|
||||
id: UUID,
|
||||
graphqlId: String,
|
||||
description: String,
|
||||
url: Followable<ClusterToken.Resources.Get>,
|
||||
clusterURL: Followable<Cluster.Resources.Get>,
|
||||
createdAt: Date,
|
||||
createdBy: User? = nil,
|
||||
token: String? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.graphqlId = graphqlId
|
||||
self.description = description
|
||||
self.url = url
|
||||
self.clusterURL = clusterURL
|
||||
self.createdAt = createdAt
|
||||
self.createdBy = createdBy
|
||||
self.token = token
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case graphqlId = "graphql_id"
|
||||
case description
|
||||
case url
|
||||
case clusterURL = "cluster_url"
|
||||
case createdAt = "created_at"
|
||||
case createdBy = "created_by"
|
||||
case token
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// FlakyTest.swift
|
||||
// Buildkite
|
||||
//
|
||||
// Created by Aaron Sky on 6/18/23.
|
||||
// Copyright © 2023 Aaron Sky. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Information about a flaky test that has been identified in a Test Analytics test suite.
|
||||
public struct FlakyTest: Codable, Equatable, Hashable, Identifiable, Sendable {
|
||||
/// ID of the flaky test.
|
||||
public var id: UUID
|
||||
/// ID of the user to be used with the GraphQL API.
|
||||
public var graphqlId: String
|
||||
/// Human-readable URL of this agent in the Buildkite dashboard.
|
||||
public var webURL: URL
|
||||
/// Scope of the test in the source code.
|
||||
public var scope: String
|
||||
/// Name of the test.
|
||||
public var name: String
|
||||
/// Path and line number to the test file.
|
||||
public var location: String
|
||||
/// Path to the test file.
|
||||
public var fileName: String
|
||||
/// Number of instances the test has "flaked".
|
||||
public var instances: Int
|
||||
/// The latest occurrence of the flake.
|
||||
public var mostRecentInstanceAt: Date
|
||||
|
||||
public init(
|
||||
id: UUID,
|
||||
graphqlId: String,
|
||||
webURL: URL,
|
||||
scope: String,
|
||||
name: String,
|
||||
location: String,
|
||||
fileName: String,
|
||||
instances: Int,
|
||||
mostRecentInstanceAt: Date
|
||||
) {
|
||||
self.id = id
|
||||
self.graphqlId = graphqlId
|
||||
self.webURL = webURL
|
||||
self.scope = scope
|
||||
self.name = name
|
||||
self.location = location
|
||||
self.fileName = fileName
|
||||
self.instances = instances
|
||||
self.mostRecentInstanceAt = mostRecentInstanceAt
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case graphqlId = "graphql_id"
|
||||
case webURL = "web_url"
|
||||
case scope
|
||||
case name
|
||||
case location
|
||||
case fileName = "file_name"
|
||||
case instances
|
||||
case mostRecentInstanceAt = "most_recent_instance_at"
|
||||
}
|
||||
}
|
|
@ -99,7 +99,8 @@ extension BuildkiteClient {
|
|||
|
||||
private nonisolated func getTimestampAndSignature(_ header: String) throws -> (timestamp: String, signature: Data) {
|
||||
let parts: [String: String] = Dictionary(
|
||||
uniqueKeysWithValues: header
|
||||
uniqueKeysWithValues:
|
||||
header
|
||||
.split(separator: ",")
|
||||
.compactMap { kv in
|
||||
let kvp = kv.split(separator: "=", maxSplits: 2)
|
||||
|
@ -132,11 +133,13 @@ extension BuildkiteClient {
|
|||
|
||||
private nonisolated func checkMAC(message: Data, signature: Data, secretKey: Data) throws {
|
||||
let key = SymmetricKey(data: secretKey)
|
||||
guard HMAC<SHA256>.isValidAuthenticationCode(
|
||||
signature,
|
||||
authenticating: message,
|
||||
using: key
|
||||
)
|
||||
guard
|
||||
HMAC<SHA256>
|
||||
.isValidAuthenticationCode(
|
||||
signature,
|
||||
authenticating: message,
|
||||
using: key
|
||||
)
|
||||
else {
|
||||
throw WebhookValidationError.signatureRefused
|
||||
}
|
||||
|
|
|
@ -52,7 +52,8 @@ extension Date {
|
|||
init?(
|
||||
iso8601String: String
|
||||
) {
|
||||
guard let date = Formatters.iso8601WithFractionalSeconds.date(from: iso8601String)
|
||||
guard
|
||||
let date = Formatters.iso8601WithFractionalSeconds.date(from: iso8601String)
|
||||
?? Formatters.iso8601WithoutFractionalSeconds.date(from: iso8601String)
|
||||
else { return nil }
|
||||
self = date
|
||||
|
|
|
@ -31,7 +31,8 @@ public struct Page: Equatable, Hashable, Sendable {
|
|||
}
|
||||
|
||||
for link in header.split(separator: ",") {
|
||||
let segments = link
|
||||
let segments =
|
||||
link
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.split(separator: ";")
|
||||
guard
|
||||
|
|
|
@ -19,7 +19,7 @@ public enum ResourceError: Error, Equatable {
|
|||
}
|
||||
|
||||
/// Resource describes an endpoint on any of Buildkite's APIs. Do not implement this type.
|
||||
public protocol Resource {
|
||||
public protocol Resource: Sendable {
|
||||
/// The input type.
|
||||
associatedtype Body = Void
|
||||
/// The return type.
|
||||
|
|
|
@ -24,7 +24,7 @@ import FoundationNetworking
|
|||
/// if you need to read your token dynamically from the keychain, it may be more adventageous to do that as part of a
|
||||
/// ``TokenProvider`` implementation rather than tightly-coupling keychain raw string access directly to your
|
||||
/// client instantiation.
|
||||
public protocol TokenProvider {
|
||||
public protocol TokenProvider: Sendable {
|
||||
/// Returns a token string for the given ``APIVersion``. This will usually be a fixed constant such as ``APIVersion/REST/v2`` or ``APIVersion/GraphQL/v1``, so you can switch on the values of one of these.
|
||||
func token(for version: APIVersion) async -> String?
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ public enum TransportError: Error {
|
|||
}
|
||||
|
||||
/// Interface for the asynchronous communication layer the ``BuildkiteClient`` uses.
|
||||
public protocol Transport {
|
||||
public protocol Transport: Sendable {
|
||||
typealias Output = (data: Data, response: URLResponse)
|
||||
|
||||
/// Send the request and receive the ``Output`` asynchronously.
|
||||
|
@ -32,12 +32,16 @@ extension URLSession: Transport {
|
|||
// Task-based API for URLSession.
|
||||
#if os(Linux) || os(Windows)
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
send(request: request, completion: continuation.resume)
|
||||
send(request: request) {
|
||||
continuation.resume(with: $0)
|
||||
}
|
||||
}
|
||||
#else
|
||||
guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else {
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
send(request: request, completion: continuation.resume)
|
||||
send(request: request) {
|
||||
continuation.resume(with: $0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,7 +49,7 @@ extension URLSession: Transport {
|
|||
#endif
|
||||
}
|
||||
|
||||
private func send(request: URLRequest, completion: @escaping (Result<Output, Error>) -> Void) {
|
||||
private func send(request: URLRequest, completion: @Sendable @escaping (Result<Output, Error>) -> Void) {
|
||||
let task = dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
|
|
|
@ -0,0 +1,255 @@
|
|||
//
|
||||
// ClusterQueues.swift
|
||||
// Buildkite
|
||||
//
|
||||
// Created by Aaron Sky on 6/18/23.
|
||||
// Copyright © 2023 Aaron Sky. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
extension ClusterQueue {
|
||||
public enum Resources {}
|
||||
}
|
||||
|
||||
extension ClusterQueue.Resources {
|
||||
/// List cluster.
|
||||
///
|
||||
/// Returns a paginated list of an organization's clusters.
|
||||
public struct List: PaginatedResource, Equatable, Hashable, Sendable {
|
||||
public typealias Content = [ClusterQueue]
|
||||
/// organization slug
|
||||
public var organization: String
|
||||
/// cluster ID
|
||||
public var clusterId: UUID
|
||||
|
||||
public var path: String {
|
||||
"organizations/\(organization)/clusters\(clusterId)/queues"
|
||||
}
|
||||
|
||||
public init(
|
||||
organization: String,
|
||||
clusterId: UUID
|
||||
) {
|
||||
self.organization = organization
|
||||
self.clusterId = clusterId
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a cluster.
|
||||
public struct Get: Resource, Equatable, Hashable, Sendable {
|
||||
public typealias Content = ClusterQueue
|
||||
/// organization slug
|
||||
public var organization: String
|
||||
/// cluster ID
|
||||
public var clusterId: UUID
|
||||
/// queue ID
|
||||
public var queueId: UUID
|
||||
|
||||
public var path: String {
|
||||
"organizations/\(organization)/clusters/\(clusterId)/queues/\(queueId)"
|
||||
}
|
||||
|
||||
public init(
|
||||
organization: String,
|
||||
clusterId: UUID,
|
||||
queueId: UUID
|
||||
) {
|
||||
self.organization = organization
|
||||
self.clusterId = clusterId
|
||||
self.queueId = queueId
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a cluster queue.
|
||||
public struct Create: Resource, Equatable, Hashable, Sendable {
|
||||
public typealias Content = ClusterQueue
|
||||
/// organization slug
|
||||
public var organization: String
|
||||
/// cluster ID
|
||||
public var clusterId: UUID
|
||||
|
||||
public var body: Body
|
||||
|
||||
public struct Body: Codable, Equatable, Hashable, Sendable {
|
||||
/// Key for the queue.
|
||||
public var key: String
|
||||
/// Description for the queue.
|
||||
public var description: String?
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
description: String? = nil
|
||||
) {
|
||||
self.key = key
|
||||
self.description = description
|
||||
}
|
||||
}
|
||||
|
||||
public var path: String {
|
||||
"organizations/\(organization)/clusters/\(clusterId)/queues"
|
||||
}
|
||||
|
||||
public init(
|
||||
organization: String,
|
||||
clusterId: UUID,
|
||||
body: Body
|
||||
) {
|
||||
self.organization = organization
|
||||
self.clusterId = clusterId
|
||||
self.body = body
|
||||
}
|
||||
|
||||
public func transformRequest(_ request: inout URLRequest) {
|
||||
request.httpMethod = "POST"
|
||||
}
|
||||
}
|
||||
|
||||
/// Update a cluster queue.
|
||||
public struct Update: Resource, Equatable, Hashable, Sendable {
|
||||
public typealias Content = ClusterQueue
|
||||
/// organization slug
|
||||
public var organization: String
|
||||
/// cluster ID
|
||||
public var clusterId: UUID
|
||||
/// queue ID
|
||||
public var queueId: UUID
|
||||
|
||||
public var body: Body
|
||||
|
||||
public struct Body: Codable, Equatable, Hashable, Sendable {
|
||||
/// Description for the cluster.
|
||||
public var description: String
|
||||
|
||||
}
|
||||
|
||||
public var path: String {
|
||||
"organizations/\(organization)/clusters/\(clusterId)/queues/\(queueId)"
|
||||
}
|
||||
|
||||
public init(
|
||||
organization: String,
|
||||
clusterId: UUID,
|
||||
queueId: UUID,
|
||||
body: Body
|
||||
) {
|
||||
self.organization = organization
|
||||
self.clusterId = clusterId
|
||||
self.queueId = queueId
|
||||
self.body = body
|
||||
}
|
||||
|
||||
public func transformRequest(_ request: inout URLRequest) {
|
||||
request.httpMethod = "PUT"
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a cluster queue.
|
||||
public struct Delete: Resource, Equatable, Hashable, Sendable {
|
||||
/// organization slug
|
||||
public var organization: String
|
||||
/// cluster ID
|
||||
public var clusterId: UUID
|
||||
/// queue ID
|
||||
public var queueId: UUID
|
||||
|
||||
public var path: String {
|
||||
"organizations/\(organization)/clusters/\(clusterId)/queues/\(queueId)"
|
||||
}
|
||||
|
||||
public init(
|
||||
organization: String,
|
||||
clusterId: UUID,
|
||||
queueId: UUID
|
||||
) {
|
||||
self.organization = organization
|
||||
self.clusterId = clusterId
|
||||
self.queueId = queueId
|
||||
}
|
||||
|
||||
public func transformRequest(_ request: inout URLRequest) {
|
||||
request.httpMethod = "DELETE"
|
||||
}
|
||||
}
|
||||
|
||||
/// Resume a paused cluster queue.
|
||||
public struct ResumeDispatch: Resource, Equatable, Hashable, Sendable {
|
||||
/// organization slug
|
||||
public var organization: String
|
||||
/// cluster ID
|
||||
public var clusterId: UUID
|
||||
/// queue ID
|
||||
public var queueId: UUID
|
||||
|
||||
public var path: String {
|
||||
"organizations/\(organization)/clusters/\(clusterId)/queues/\(queueId)/resume_dispatch"
|
||||
}
|
||||
|
||||
public init(
|
||||
organization: String,
|
||||
clusterId: UUID,
|
||||
queueId: UUID
|
||||
) {
|
||||
self.organization = organization
|
||||
self.clusterId = clusterId
|
||||
self.queueId = queueId
|
||||
}
|
||||
|
||||
public func transformRequest(_ request: inout URLRequest) {
|
||||
request.httpMethod = "POST"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Resource where Self == ClusterQueue.Resources.List {
|
||||
/// List cluster tokens
|
||||
///
|
||||
/// Returns a paginated list of a cluster's tokens.
|
||||
public static func clusterQueues(in organization: String, clusterId: UUID) -> Self {
|
||||
Self(organization: organization, clusterId: clusterId)
|
||||
}
|
||||
}
|
||||
|
||||
extension Resource where Self == ClusterQueue.Resources.Get {
|
||||
/// Get a cluster token.
|
||||
public static func clusterQueue(_ id: UUID, in organization: String, clusterId: UUID) -> Self {
|
||||
Self(organization: organization, clusterId: clusterId, queueId: id)
|
||||
}
|
||||
}
|
||||
|
||||
extension Resource where Self == ClusterQueue.Resources.Create {
|
||||
/// Create a cluster token.
|
||||
public static func createClusterQueue(_ body: Self.Body, in organization: String, clusterId: UUID) -> Self {
|
||||
Self(organization: organization, clusterId: clusterId, body: body)
|
||||
}
|
||||
}
|
||||
|
||||
extension Resource where Self == ClusterQueue.Resources.Update {
|
||||
/// Update a cluster token.
|
||||
public static func updateClusterQueue(
|
||||
_ id: UUID,
|
||||
in organization: String,
|
||||
clusterId: UUID,
|
||||
with description: String
|
||||
) -> Self {
|
||||
Self(organization: organization, clusterId: clusterId, queueId: id, body: .init(description: description))
|
||||
}
|
||||
}
|
||||
|
||||
extension Resource where Self == ClusterQueue.Resources.Delete {
|
||||
/// Delete a cluster token.
|
||||
public static func deleteClusterQueue(_ id: UUID, in organization: String, clusterId: UUID) -> Self {
|
||||
Self(organization: organization, clusterId: clusterId, queueId: id)
|
||||
}
|
||||
}
|
||||
|
||||
extension Resource where Self == ClusterQueue.Resources.ResumeDispatch {
|
||||
/// Delete a cluster token.
|
||||
public static func resumeClusterQueueDispatch(_ id: UUID, in organization: String, clusterId: UUID) -> Self {
|
||||
Self(organization: organization, clusterId: clusterId, queueId: id)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
//
|
||||
// ClusterTokens.swift
|
||||
// Buildkite
|
||||
//
|
||||
// Created by Aaron Sky on 6/18/23.
|
||||
// Copyright © 2023 Aaron Sky. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
extension ClusterToken {
|
||||
public enum Resources {}
|
||||
}
|
||||
|
||||
extension ClusterToken.Resources {
|
||||
/// List cluster tokens
|
||||
///
|
||||
/// Returns a paginated list of a cluster's tokens.
|
||||
public struct List: PaginatedResource, Equatable, Hashable, Sendable {
|
||||
public typealias Content = [ClusterToken]
|
||||
/// organization slug
|
||||
public var organization: String
|
||||
/// cluster ID
|
||||
public var clusterId: UUID
|
||||
|
||||
public var path: String {
|
||||
"organizations/\(organization)/clusters/\(clusterId)/tokens"
|
||||
}
|
||||
|
||||
public init(
|
||||
organization: String,
|
||||
clusterId: UUID
|
||||
) {
|
||||
self.organization = organization
|
||||
self.clusterId = clusterId
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a cluster token.
|
||||
public struct Get: Resource, Equatable, Hashable, Sendable {
|
||||
public typealias Content = ClusterToken
|
||||
/// organization slug
|
||||
public var organization: String
|
||||
/// cluster ID
|
||||
public var clusterId: UUID
|
||||
/// cluster token ID
|
||||
public var tokenId: UUID
|
||||
|
||||
public var path: String {
|
||||
"organizations/\(organization)/clusters/\(clusterId)/tokens/\(tokenId)"
|
||||
}
|
||||
|
||||
public init(
|
||||
organization: String,
|
||||
clusterId: UUID,
|
||||
tokenId: UUID
|
||||
) {
|
||||
self.organization = organization
|
||||
self.clusterId = clusterId
|
||||
self.tokenId = tokenId
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a cluster token.
|
||||
public struct Create: Resource, Equatable, Hashable, Sendable {
|
||||
public typealias Content = ClusterToken
|
||||
/// organization slug
|
||||
public var organization: String
|
||||
/// cluster ID
|
||||
public var clusterId: UUID
|
||||
|
||||
public var body: Body
|
||||
|
||||
public struct Body: Codable, Equatable, Hashable, Sendable {
|
||||
/// Description for the token.
|
||||
public var description: String
|
||||
|
||||
public init(
|
||||
description: String
|
||||
) {
|
||||
self.description = description
|
||||
}
|
||||
}
|
||||
|
||||
public var path: String {
|
||||
"organizations/\(organization)/clusters/\(clusterId)/tokens"
|
||||
}
|
||||
|
||||
public init(
|
||||
organization: String,
|
||||
clusterId: UUID,
|
||||
body: Body
|
||||
) {
|
||||
self.organization = organization
|
||||
self.clusterId = clusterId
|
||||
self.body = body
|
||||
}
|
||||
|
||||
public func transformRequest(_ request: inout URLRequest) {
|
||||
request.httpMethod = "POST"
|
||||
}
|
||||
}
|
||||
|
||||
/// Update a cluster token.
|
||||
public struct Update: Resource, Equatable, Hashable, Sendable {
|
||||
public typealias Content = ClusterToken
|
||||
/// organization slug
|
||||
public var organization: String
|
||||
/// cluster ID
|
||||
public var clusterId: UUID
|
||||
/// cluster token ID
|
||||
public var tokenId: UUID
|
||||
|
||||
public var body: Body
|
||||
|
||||
public struct Body: Codable, Equatable, Hashable, Sendable {
|
||||
/// Description for the token.
|
||||
public var description: String
|
||||
|
||||
public init(
|
||||
description: String
|
||||
) {
|
||||
self.description = description
|
||||
}
|
||||
}
|
||||
|
||||
public var path: String {
|
||||
"organizations/\(organization)/clusters/\(clusterId)/tokens/\(tokenId)"
|
||||
}
|
||||
|
||||
public init(
|
||||
organization: String,
|
||||
clusterId: UUID,
|
||||
tokenId: UUID,
|
||||
body: Body
|
||||
) {
|
||||
self.organization = organization
|
||||
self.clusterId = clusterId
|
||||
self.tokenId = tokenId
|
||||
self.body = body
|
||||
}
|
||||
|
||||
public func transformRequest(_ request: inout URLRequest) {
|
||||
request.httpMethod = "PUT"
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a cluster token.
|
||||
public struct Delete: Resource, Equatable, Hashable, Sendable {
|
||||
/// organization slug
|
||||
public var organization: String
|
||||
/// cluster ID
|
||||
public var clusterId: UUID
|
||||
/// cluster token ID
|
||||
public var tokenId: UUID
|
||||
|
||||
public var path: String {
|
||||
"organizations/\(organization)/clusters/\(clusterId)/tokens/\(tokenId)"
|
||||
}
|
||||
|
||||
public init(
|
||||
organization: String,
|
||||
clusterId: UUID,
|
||||
tokenId: UUID
|
||||
) {
|
||||
self.organization = organization
|
||||
self.clusterId = clusterId
|
||||
self.tokenId = tokenId
|
||||
}
|
||||
|
||||
public func transformRequest(_ request: inout URLRequest) {
|
||||
request.httpMethod = "DELETE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Resource where Self == ClusterToken.Resources.List {
|
||||
/// List cluster tokens
|
||||
///
|
||||
/// Returns a paginated list of a cluster's tokens.
|
||||
public static func clusterTokens(in organization: String, clusterId: UUID) -> Self {
|
||||
Self(organization: organization, clusterId: clusterId)
|
||||
}
|
||||
}
|
||||
|
||||
extension Resource where Self == ClusterToken.Resources.Get {
|
||||
/// Get a cluster token.
|
||||
public static func clusterToken(_ id: UUID, in organization: String, clusterId: UUID) -> Self {
|
||||
Self(organization: organization, clusterId: clusterId, tokenId: id)
|
||||
}
|
||||
}
|
||||
|
||||
extension Resource where Self == ClusterToken.Resources.Create {
|
||||
/// Create a cluster token.
|
||||
public static func createClusterToken(with description: String, in organization: String, clusterId: UUID) -> Self {
|
||||
Self(organization: organization, clusterId: clusterId, body: Self.Body(description: description))
|
||||
}
|
||||
}
|
||||
|
||||
extension Resource where Self == ClusterToken.Resources.Update {
|
||||
/// Update a cluster token.
|
||||
public static func updateClusterToken(
|
||||
_ id: UUID,
|
||||
in organization: String,
|
||||
clusterId: UUID,
|
||||
with description: String
|
||||
) -> Self {
|
||||
Self(organization: organization, clusterId: clusterId, tokenId: id, body: Self.Body(description: description))
|
||||
}
|
||||
}
|
||||
|
||||
extension Resource where Self == ClusterToken.Resources.Delete {
|
||||
/// Delete a cluster token.
|
||||
public static func deleteClusterToken(_ id: UUID, in organization: String, clusterId: UUID) -> Self {
|
||||
Self(organization: organization, clusterId: clusterId, tokenId: id)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
//
|
||||
// Clusters.swift
|
||||
// Buildkite
|
||||
//
|
||||
// Created by Aaron Sky on 6/18/23.
|
||||
// Copyright © 2023 Aaron Sky. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
extension Cluster {
|
||||
public enum Resources {}
|
||||
}
|
||||
|
||||
extension Cluster.Resources {
|
||||
/// List cluster.
|
||||
///
|
||||
/// Returns a paginated list of an organization's clusters.
|
||||
public struct List: PaginatedResource, Equatable, Hashable, Sendable {
|
||||
public typealias Content = [Cluster]
|
||||
/// organization slug
|
||||
public var organization: String
|
||||
|
||||
public var path: String {
|
||||
"organizations/\(organization)/clusters"
|
||||
}
|
||||
|
||||
public init(
|
||||
organization: String
|
||||
) {
|
||||
self.organization = organization
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a cluster.
|
||||
public struct Get: Resource, Equatable, Hashable, Sendable {
|
||||
public typealias Content = Cluster
|
||||
/// organization slug
|
||||
public var organization: String
|
||||
/// cluster ID
|
||||
public var clusterId: UUID
|
||||
|
||||
public var path: String {
|
||||
"organizations/\(organization)/clusters/\(clusterId)"
|
||||
}
|
||||
|
||||
public init(
|
||||
organization: String,
|
||||
clusterId: UUID
|
||||
) {
|
||||
self.organization = organization
|
||||
self.clusterId = clusterId
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a cluster.
|
||||
public struct Create: Resource, Equatable, Hashable, Sendable {
|
||||
public typealias Content = Cluster
|
||||
/// organization slug
|
||||
public var organization: String
|
||||
|
||||
public var body: Body
|
||||
|
||||
public struct Body: Codable, Equatable, Hashable, Sendable {
|
||||
/// Name for the cluster.
|
||||
public var name: String
|
||||
/// Description for the cluster.
|
||||
public var description: String?
|
||||
/// Emoji for the cluster using the emoji syntax.
|
||||
public var emoji: String?
|
||||
/// Color hex code for the cluster.
|
||||
public var color: String?
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
description: String? = nil,
|
||||
emoji: String? = nil,
|
||||
color: String? = nil
|
||||
) {
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.emoji = emoji
|
||||
self.color = color
|
||||
}
|
||||
}
|
||||
|
||||
public var path: String {
|
||||
"organizations/\(organization)/clusters"
|
||||
}
|
||||
|
||||
public init(
|
||||
organization: String,
|
||||
body: Body
|
||||
) {
|
||||
self.organization = organization
|
||||
self.body = body
|
||||
}
|
||||
|
||||
public func transformRequest(_ request: inout URLRequest) {
|
||||
request.httpMethod = "POST"
|
||||
}
|
||||
}
|
||||
|
||||
/// Update a cluster.
|
||||
public struct Update: Resource, Equatable, Hashable, Sendable {
|
||||
public typealias Content = Cluster
|
||||
/// organization slug
|
||||
public var organization: String
|
||||
/// cluster ID
|
||||
public var clusterId: UUID
|
||||
|
||||
public var body: Body
|
||||
|
||||
public struct Body: Codable, Equatable, Hashable, Sendable {
|
||||
/// Name for the cluster.
|
||||
public var name: String
|
||||
/// Description for the cluster.
|
||||
public var description: String
|
||||
/// Emoji for the cluster using the emoji syntax.
|
||||
public var emoji: String
|
||||
/// Color hex code for the cluster.
|
||||
public var color: String
|
||||
/// ID of the queue to set as the cluster's default queue. Agents that connect to the cluster without specifying a queue will accept jobs from this queue.
|
||||
public var defaultQueueId: UUID
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
description: String,
|
||||
emoji: String,
|
||||
color: String,
|
||||
defaultQueueId: UUID
|
||||
) {
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.emoji = emoji
|
||||
self.color = color
|
||||
self.defaultQueueId = defaultQueueId
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case description
|
||||
case emoji
|
||||
case color
|
||||
case defaultQueueId = "default_queue_id"
|
||||
}
|
||||
}
|
||||
|
||||
public var path: String {
|
||||
"organizations/\(organization)/clusters/\(clusterId)"
|
||||
}
|
||||
|
||||
public init(
|
||||
organization: String,
|
||||
clusterId: UUID,
|
||||
body: Body
|
||||
) {
|
||||
self.organization = organization
|
||||
self.clusterId = clusterId
|
||||
self.body = body
|
||||
}
|
||||
|
||||
public func transformRequest(_ request: inout URLRequest) {
|
||||
request.httpMethod = "PUT"
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a cluster.
|
||||
public struct Delete: Resource, Equatable, Hashable, Sendable {
|
||||
/// organization slug
|
||||
public var organization: String
|
||||
/// cluster ID
|
||||
public var clusterId: UUID
|
||||
|
||||
public var path: String {
|
||||
"organizations/\(organization)/clusters/\(clusterId)"
|
||||
}
|
||||
|
||||
public init(
|
||||
organization: String,
|
||||
clusterId: UUID
|
||||
) {
|
||||
self.organization = organization
|
||||
self.clusterId = clusterId
|
||||
}
|
||||
|
||||
public func transformRequest(_ request: inout URLRequest) {
|
||||
request.httpMethod = "DELETE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Resource where Self == Cluster.Resources.List {
|
||||
/// List clusters
|
||||
///
|
||||
/// Returns a paginated list of an organization's clusters.
|
||||
public static func clusters(in organization: String) -> Self {
|
||||
Self(organization: organization)
|
||||
}
|
||||
}
|
||||
|
||||
extension Resource where Self == Cluster.Resources.Get {
|
||||
/// Get a cluster.
|
||||
public static func cluster(_ id: UUID, in organization: String) -> Self {
|
||||
Self(organization: organization, clusterId: id)
|
||||
}
|
||||
}
|
||||
|
||||
extension Resource where Self == Cluster.Resources.Create {
|
||||
/// Create a cluster.
|
||||
public static func createCluster(_ body: Self.Body, in organization: String) -> Self {
|
||||
Self(organization: organization, body: body)
|
||||
}
|
||||
}
|
||||
|
||||
extension Resource where Self == Cluster.Resources.Update {
|
||||
/// Update a cluster.
|
||||
public static func updateCluster(_ id: UUID, in organization: String, with body: Self.Body) -> Self {
|
||||
Self(organization: organization, clusterId: id, body: body)
|
||||
}
|
||||
}
|
||||
|
||||
extension Resource where Self == Cluster.Resources.Delete {
|
||||
/// Delete a cluster.
|
||||
public static func deleteCluster(_ id: UUID, in organization: String) -> Self {
|
||||
Self(organization: organization, clusterId: id)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// FlakyTests.swift
|
||||
// Buildkite
|
||||
//
|
||||
// Created by Aaron Sky on 6/18/23.
|
||||
// Copyright © 2023 Aaron Sky. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
extension FlakyTest {
|
||||
/// Resources for performing operations on flaky tests.
|
||||
public enum Resources {}
|
||||
}
|
||||
|
||||
extension FlakyTest.Resources {
|
||||
/// Provides information about tests detected as flaky in a test suite.
|
||||
///
|
||||
/// Returns a paginated list of the flaky tests detected in a test suite.
|
||||
public struct List: PaginatedResource, Equatable, Hashable, Sendable {
|
||||
public typealias Content = [FlakyTest]
|
||||
/// organization slug
|
||||
public var organization: String
|
||||
/// test suite slug
|
||||
public var suite: String
|
||||
|
||||
public var path: String {
|
||||
"analytics/organizations/\(organization)/suites/\(suite)/flaky-tests"
|
||||
}
|
||||
|
||||
public init(
|
||||
organization: String,
|
||||
suite: String
|
||||
) {
|
||||
self.organization = organization
|
||||
self.suite = suite
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Resource where Self == FlakyTest.Resources.List {
|
||||
/// List flaky tests
|
||||
///
|
||||
/// Returns a paginated list of a test suite's flaky tests.
|
||||
public static func flakyTests(in organization: String, suite: String) -> Self {
|
||||
Self(organization: organization, suite: suite)
|
||||
}
|
||||
}
|
|
@ -15,66 +15,80 @@ import XCTest
|
|||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
class TransportTests: XCTestCase {
|
||||
private func createSession(testCase: MockURLProtocol.Case = .success) -> URLSession {
|
||||
switch testCase {
|
||||
case .success:
|
||||
MockURLProtocol.requestHandler = MockData.mockingSuccessNoContent
|
||||
case .error:
|
||||
MockURLProtocol.requestHandler = MockData.mockingError
|
||||
}
|
||||
extension URLSession {
|
||||
fileprivate convenience init(
|
||||
_ testCase: MockURLProtocol.Case
|
||||
) {
|
||||
let config = URLSessionConfiguration.ephemeral
|
||||
config.protocolClasses = [MockURLProtocol.self]
|
||||
return URLSession(configuration: config)
|
||||
}
|
||||
|
||||
private class MockURLProtocol: URLProtocol {
|
||||
enum Case {
|
||||
case success
|
||||
case error
|
||||
}
|
||||
|
||||
typealias RequestHandler = (URLRequest) throws -> (Data, URLResponse)
|
||||
static var requestHandler: RequestHandler?
|
||||
|
||||
override class func canInit(with request: URLRequest) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
||||
return request
|
||||
}
|
||||
|
||||
override func startLoading() {
|
||||
guard let handler = MockURLProtocol.requestHandler else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
let (data, response) = try handler(request)
|
||||
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
||||
client?.urlProtocol(self, didLoad: data)
|
||||
client?.urlProtocolDidFinishLoading(self)
|
||||
} catch {
|
||||
client?.urlProtocol(self, didFailWithError: error)
|
||||
}
|
||||
}
|
||||
|
||||
override func stopLoading() {}
|
||||
self.init(configuration: config)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Async/Await-based Requests
|
||||
private final class MockURLProtocol: URLProtocol {
|
||||
typealias RequestHandler = @Sendable (URLRequest) throws -> (Data, URLResponse)
|
||||
|
||||
extension TransportTests {
|
||||
enum Case {
|
||||
case success
|
||||
case error
|
||||
|
||||
var handler: RequestHandler {
|
||||
switch self {
|
||||
case .success:
|
||||
return MockData.mockingSuccessNoContent(for:)
|
||||
case .error:
|
||||
return MockData.mockingError(for:)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override class func canInit(with request: URLRequest) -> Bool { true }
|
||||
|
||||
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
|
||||
|
||||
override func startLoading() {
|
||||
guard let client = client, let handler = Self.testCase(for: request)?.handler else { return }
|
||||
do {
|
||||
let (data, response) = try handler(request)
|
||||
client.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
||||
client.urlProtocol(self, didLoad: data)
|
||||
client.urlProtocolDidFinishLoading(self)
|
||||
} catch {
|
||||
client.urlProtocol(self, didFailWithError: error)
|
||||
}
|
||||
}
|
||||
|
||||
override func stopLoading() {}
|
||||
|
||||
static func testCase(for request: URLRequest) -> Case? {
|
||||
property(forKey: "testCase", in: request) as? Case
|
||||
}
|
||||
|
||||
static func setTestCase(_ testCase: Case, for request: inout URLRequest) {
|
||||
let mutableRequest = (request as NSURLRequest) as! NSMutableURLRequest
|
||||
setProperty(
|
||||
testCase,
|
||||
forKey: "testCase",
|
||||
in: mutableRequest
|
||||
)
|
||||
request = mutableRequest as URLRequest
|
||||
}
|
||||
}
|
||||
|
||||
class TransportTests: XCTestCase {
|
||||
func testURLSessionSendRequest() async throws {
|
||||
let request = URLRequest(url: URL())
|
||||
_ = try await createSession().send(request: request)
|
||||
var request = URLRequest(url: URL())
|
||||
let session = URLSession(.success)
|
||||
MockURLProtocol.setTestCase(.success, for: &request)
|
||||
_ = try await session.send(request: request)
|
||||
}
|
||||
|
||||
func testURLSessionSendRequestFailure() async {
|
||||
let request = URLRequest(url: URL())
|
||||
var request = URLRequest(url: URL())
|
||||
let session = URLSession(.error)
|
||||
MockURLProtocol.setTestCase(.error, for: &request)
|
||||
try await XCTAssertThrowsError(
|
||||
await createSession(testCase: .error).send(request: request)
|
||||
await session.send(request: request)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,10 +98,11 @@ class GraphQLTests: XCTestCase {
|
|||
func testGraphQLContentGet() throws {
|
||||
try XCTAssertNoThrow(GraphQL.Content.data("hello").get())
|
||||
|
||||
let errors = GraphQL<String>.Errors(
|
||||
errors: [],
|
||||
type: nil
|
||||
)
|
||||
let errors = GraphQL<String>
|
||||
.Errors(
|
||||
errors: [],
|
||||
type: nil
|
||||
)
|
||||
try XCTAssertThrowsError(
|
||||
GraphQL<String>.Content.errors(errors)
|
||||
.get(),
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
//
|
||||
// ClusterQueuesTests.swift
|
||||
// Buildkite
|
||||
//
|
||||
// Created by Aaron Sky on 6/18/23.
|
||||
// Copyright © 2023 Aaron Sky. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import Buildkite
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
extension ClusterQueue {
|
||||
init() {
|
||||
self.init(
|
||||
id: UUID(),
|
||||
graphqlId: "",
|
||||
key: "",
|
||||
description: "",
|
||||
url: .init(),
|
||||
webURL: URL(),
|
||||
clusterURL: .init(),
|
||||
dispatchPaused: false,
|
||||
dispatchPausedBy: nil,
|
||||
dispatchPausedAt: nil,
|
||||
dispatchPausedNote: nil,
|
||||
createdAt: Date(timeIntervalSince1970: 1000),
|
||||
createdBy: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ClusterQueuesTests: XCTestCase {
|
||||
func testClusterQueuesList() async throws {
|
||||
let expected = [ClusterQueue(), ClusterQueue()]
|
||||
let context = try MockContext(content: expected)
|
||||
|
||||
let response = try await context.client.send(.clusterQueues(in: "organization", clusterId: UUID()))
|
||||
|
||||
XCTAssertEqual(expected, response.content)
|
||||
}
|
||||
|
||||
func testClusterQueuesGet() async throws {
|
||||
let expected = ClusterQueue()
|
||||
let context = try MockContext(content: expected)
|
||||
|
||||
let response = try await context.client.send(.clusterQueue(UUID(), in: "organization", clusterId: UUID()))
|
||||
|
||||
XCTAssertEqual(expected, response.content)
|
||||
}
|
||||
|
||||
func testClusterQueuesCreate() async throws {
|
||||
let expected = ClusterQueue()
|
||||
let context = try MockContext(content: expected)
|
||||
|
||||
let response = try await context.client.send(
|
||||
.createClusterQueue(.init(key: "", description: ""), in: "organization", clusterId: UUID())
|
||||
)
|
||||
|
||||
XCTAssertEqual(expected, response.content)
|
||||
}
|
||||
|
||||
func testClusterQueuesUpdate() async throws {
|
||||
let expected = ClusterQueue()
|
||||
let context = try MockContext(content: expected)
|
||||
|
||||
let response = try await context.client.send(
|
||||
.updateClusterQueue(UUID(), in: "organization", clusterId: UUID(), with: "")
|
||||
)
|
||||
|
||||
XCTAssertEqual(expected, response.content)
|
||||
}
|
||||
|
||||
func testClusterQueuesDelete() async throws {
|
||||
let context = MockContext()
|
||||
|
||||
_ = try await context.client.send(.deleteClusterQueue(UUID(), in: "organization", clusterId: UUID()))
|
||||
}
|
||||
|
||||
func testClusterQueuesResumeDispatch() async throws {
|
||||
let context = MockContext()
|
||||
|
||||
_ = try await context.client.send(.resumeClusterQueueDispatch(UUID(), in: "organization", clusterId: UUID()))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
//
|
||||
// ClusterTokensTests.swift
|
||||
// Buildkite
|
||||
//
|
||||
// Created by Aaron Sky on 6/18/23.
|
||||
// Copyright © 2023 Aaron Sky. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import Buildkite
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
extension ClusterToken {
|
||||
init() {
|
||||
self.init(
|
||||
id: UUID(),
|
||||
graphqlId: "",
|
||||
description: "",
|
||||
url: .init(),
|
||||
clusterURL: .init(),
|
||||
createdAt: Date(timeIntervalSince1970: 1000),
|
||||
createdBy: nil,
|
||||
token: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ClusterTokensTests: XCTestCase {
|
||||
func testClusterTokensList() async throws {
|
||||
let expected = [ClusterToken(), ClusterToken()]
|
||||
let context = try MockContext(content: expected)
|
||||
|
||||
let response = try await context.client.send(.clusterTokens(in: "organization", clusterId: UUID()))
|
||||
|
||||
XCTAssertEqual(expected, response.content)
|
||||
}
|
||||
|
||||
func testClusterTokensGet() async throws {
|
||||
let expected = ClusterToken()
|
||||
let context = try MockContext(content: expected)
|
||||
|
||||
let response = try await context.client.send(.clusterToken(UUID(), in: "organization", clusterId: UUID()))
|
||||
|
||||
XCTAssertEqual(expected, response.content)
|
||||
}
|
||||
|
||||
func testClusterTokensCreate() async throws {
|
||||
let expected = ClusterToken()
|
||||
let context = try MockContext(content: expected)
|
||||
|
||||
let response = try await context.client.send(
|
||||
.createClusterToken(with: "", in: "organization", clusterId: UUID())
|
||||
)
|
||||
|
||||
XCTAssertEqual(expected, response.content)
|
||||
}
|
||||
|
||||
func testClusterTokensUpdate() async throws {
|
||||
let expected = ClusterToken()
|
||||
let context = try MockContext(content: expected)
|
||||
|
||||
let response = try await context.client.send(
|
||||
.updateClusterToken(UUID(), in: "organization", clusterId: UUID(), with: "")
|
||||
)
|
||||
|
||||
XCTAssertEqual(expected, response.content)
|
||||
}
|
||||
|
||||
func testClusterTokensDelete() async throws {
|
||||
let context = MockContext()
|
||||
|
||||
_ = try await context.client.send(.deleteClusterToken(UUID(), in: "organization", clusterId: UUID()))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
//
|
||||
// ClustersTests.swift
|
||||
// Buildkite
|
||||
//
|
||||
// Created by Aaron Sky on 6/18/23.
|
||||
// Copyright © 2023 Aaron Sky. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import Buildkite
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
extension Cluster {
|
||||
init() {
|
||||
self.init(
|
||||
id: UUID(),
|
||||
graphqlId: "",
|
||||
defaultQueueId: UUID(),
|
||||
name: "",
|
||||
description: "",
|
||||
emoji: "",
|
||||
color: "",
|
||||
url: .init(),
|
||||
webURL: URL(),
|
||||
queuesURL: .init(),
|
||||
defaultQueueURL: .init(),
|
||||
createdAt: Date(timeIntervalSince1970: 1000),
|
||||
createdBy: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ClustersTests: XCTestCase {
|
||||
func testClustersList() async throws {
|
||||
let expected = [Cluster(), Cluster()]
|
||||
let context = try MockContext(content: expected)
|
||||
|
||||
let response = try await context.client.send(.clusters(in: "buildkite"))
|
||||
|
||||
XCTAssertEqual(expected, response.content)
|
||||
}
|
||||
|
||||
func testClustersGet() async throws {
|
||||
let expected = Cluster()
|
||||
let context = try MockContext(content: expected)
|
||||
|
||||
let response = try await context.client.send(.cluster(UUID(), in: "organization"))
|
||||
|
||||
XCTAssertEqual(expected, response.content)
|
||||
}
|
||||
|
||||
func testClustersCreate() async throws {
|
||||
let expected = Cluster()
|
||||
let context = try MockContext(content: expected)
|
||||
|
||||
let response = try await context.client.send(
|
||||
.createCluster(.init(name: "", description: nil, emoji: nil, color: nil), in: "organization")
|
||||
)
|
||||
|
||||
XCTAssertEqual(expected, response.content)
|
||||
}
|
||||
|
||||
func testClustersUpdate() async throws {
|
||||
let expected = Cluster()
|
||||
let context = try MockContext(content: expected)
|
||||
|
||||
let response = try await context.client.send(
|
||||
.updateCluster(
|
||||
UUID(),
|
||||
in: "organization",
|
||||
with: .init(name: "", description: "", emoji: "", color: "", defaultQueueId: UUID())
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(expected, response.content)
|
||||
}
|
||||
|
||||
func testClustersDelete() async throws {
|
||||
let context = MockContext()
|
||||
|
||||
_ = try await context.client.send(.deleteCluster(UUID(), in: "organization"))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// PipelinesTests.swift
|
||||
// Buildkite
|
||||
//
|
||||
// Created by Aaron Sky on 5/4/20.
|
||||
// Copyright © 2020 Aaron Sky. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import Buildkite
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
extension FlakyTest {
|
||||
init() {
|
||||
self.init(
|
||||
id: UUID(),
|
||||
graphqlId: "",
|
||||
webURL: URL(),
|
||||
scope: "Test#failure",
|
||||
name: "failure",
|
||||
location: "./file:30",
|
||||
fileName: "./file",
|
||||
instances: 10,
|
||||
mostRecentInstanceAt: Date(timeIntervalSince1970: 1000)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class FlakyTestsTests: XCTestCase {
|
||||
func testFlakyTestsList() async throws {
|
||||
let expected = [FlakyTest(), FlakyTest()]
|
||||
let context = try MockContext(content: expected)
|
||||
|
||||
let response = try await context.client.send(.flakyTests(in: "buildkite", suite: "my-suite"))
|
||||
|
||||
XCTAssertEqual(expected, response.content)
|
||||
}
|
||||
}
|
|
@ -103,19 +103,23 @@ enum MockData {
|
|||
}
|
||||
|
||||
extension MockData {
|
||||
@Sendable
|
||||
static func mockingSuccess<Content: Codable>(with content: Content, url: URL) throws -> (Data, URLResponse) {
|
||||
let data = try encoder.encode(content)
|
||||
return (data, urlResponse(for: url, status: .ok))
|
||||
}
|
||||
|
||||
@Sendable
|
||||
static func mockingSuccessNoContent(url: URL) -> (Data, URLResponse) {
|
||||
return (Data(), urlResponse(for: url, status: .ok))
|
||||
}
|
||||
|
||||
@Sendable
|
||||
static func mockingIncompatibleResponse(for url: URL) -> (Data, URLResponse) {
|
||||
return (Data(), urlResponse(for: url, rawStatus: -128))
|
||||
}
|
||||
|
||||
@Sendable
|
||||
static func mockingUnsuccessfulResponse(for url: URL) -> (Data, URLResponse) {
|
||||
let json = """
|
||||
{"message":"not found","errors": ["go away"]}
|
||||
|
@ -125,18 +129,22 @@ extension MockData {
|
|||
return (json, urlResponse(for: url, status: .notFound))
|
||||
}
|
||||
|
||||
@Sendable
|
||||
static func mockingSuccessNoContent(for request: URLRequest) throws -> (Data, URLResponse) {
|
||||
return mockingSuccessNoContent(url: request.url!)
|
||||
}
|
||||
|
||||
@Sendable
|
||||
static func mockingError(for request: URLRequest) throws -> (Data, URLResponse) {
|
||||
throw URLError(.notConnectedToInternet)
|
||||
}
|
||||
|
||||
@Sendable
|
||||
static func mockingError(_ error: Error) throws -> (Data, URLResponse) {
|
||||
throw error
|
||||
}
|
||||
|
||||
@Sendable
|
||||
static func mockingUnrecognizedBuildkiteError(for url: URL) -> (Data, URLResponse) {
|
||||
let json = """
|
||||
{"message":-1000}
|
||||
|
@ -145,10 +153,12 @@ extension MockData {
|
|||
return (json, urlResponse(for: url, status: .notFound))
|
||||
}
|
||||
|
||||
@Sendable
|
||||
private static func urlResponse(for url: URL, status: StatusCode) -> URLResponse {
|
||||
urlResponse(for: url, rawStatus: status.rawValue)
|
||||
}
|
||||
|
||||
@Sendable
|
||||
private static func urlResponse(for url: URL, rawStatus status: Int) -> URLResponse {
|
||||
HTTPURLResponse(
|
||||
url: url,
|
||||
|
|
|
@ -13,7 +13,7 @@ import Foundation
|
|||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
final class MockTransport {
|
||||
actor MockTransport {
|
||||
enum Error: Swift.Error {
|
||||
case tooManyRequests
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue