* Added graphql_id property
* renamed URL fields * WIP webhooks support * WIP webhooks example
This commit is contained in:
parent
2bd91f7315
commit
790f6e7453
|
@ -6,3 +6,4 @@
|
|||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
Package.resolved
|
||||
*.env*
|
|
@ -17,7 +17,8 @@ let package = Package(
|
|||
.executable(name: "webhooks", targets: ["webhooks"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "Buildkite", path: "../")
|
||||
.package(name: "Buildkite", path: "../"),
|
||||
.package(url: "https://github.com/vapor/vapor", .upToNextMajor(from: "4.0.0"))
|
||||
],
|
||||
targets: [
|
||||
.executableTarget(
|
||||
|
@ -51,7 +52,8 @@ let package = Package(
|
|||
.executableTarget(
|
||||
name: "webhooks",
|
||||
dependencies: [
|
||||
.product(name: "Buildkite", package: "Buildkite")
|
||||
.product(name: "Buildkite", package: "Buildkite"),
|
||||
.product(name: "Vapor", package: "vapor")
|
||||
],
|
||||
path: "webhooks"
|
||||
),
|
||||
|
|
|
@ -27,6 +27,6 @@ import Foundation
|
|||
)
|
||||
.content
|
||||
|
||||
print(result.runUrl)
|
||||
print(result.runURL)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
//
|
||||
// BuildkiteClient+Middleware.swift
|
||||
// webhooks
|
||||
//
|
||||
// Created by Aaron Sky on 9/5/22.
|
||||
// Copyright © 2022 Aaron Sky. All rights reserved.
|
||||
//
|
||||
|
||||
import Buildkite
|
||||
import Foundation
|
||||
import Vapor
|
||||
|
||||
extension HTTPHeaders {
|
||||
fileprivate var buildkiteEvent: String? {
|
||||
self.first(name: WebhookEvent.HTTPHeaders.buildkiteEvent)
|
||||
}
|
||||
|
||||
fileprivate var buildkiteToken: String? {
|
||||
self.first(name: WebhookEvent.HTTPHeaders.buildkiteToken)
|
||||
}
|
||||
|
||||
fileprivate var buildkiteSignature: String? {
|
||||
self.first(name: WebhookEvent.HTTPHeaders.buildkiteSignature)
|
||||
}
|
||||
}
|
||||
|
||||
extension BuildkiteClient: ContentDecoder {
|
||||
public nonisolated func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders) throws -> D where D : Decodable {
|
||||
// FIXME: force cast
|
||||
try decodeWebhook(from: Data(buffer: body)) as! D
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 12.0, *)
|
||||
extension BuildkiteClient: AsyncMiddleware {
|
||||
public func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Vapor.Response {
|
||||
guard Environment.get("BUILDKITE_NO_VERIFY") == nil else {
|
||||
return try await next.respond(to: request)
|
||||
}
|
||||
|
||||
guard
|
||||
let secretKey = Environment
|
||||
.get("BUILDKITE_WEBHOOK_SECRET")?
|
||||
.data(using: .utf8)
|
||||
else {
|
||||
throw Abort(
|
||||
.preconditionFailed,
|
||||
reason: "Server not configured with webhook authentication token"
|
||||
)
|
||||
}
|
||||
|
||||
if
|
||||
let signature = request.headers.buildkiteSignature,
|
||||
let payload = request.body.data {
|
||||
try self.validateWebhookPayload(
|
||||
signatureHeader: signature,
|
||||
body: Data(buffer: payload),
|
||||
secretKey: secretKey
|
||||
)
|
||||
} else if let token = request.headers.buildkiteToken {
|
||||
try self.validateWebhookPayload(tokenHeader: token, secretKey: secretKey)
|
||||
} else {
|
||||
throw Abort(.unauthorized)
|
||||
}
|
||||
|
||||
return try await next.respond(to: request)
|
||||
}
|
||||
}
|
||||
|
||||
extension BuildkiteClient: Middleware {
|
||||
public nonisolated func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture<Vapor.Response> {
|
||||
guard Environment.get("BUILDKITE_NO_VERIFY") == nil else {
|
||||
return next.respond(to: request)
|
||||
}
|
||||
|
||||
guard
|
||||
let secretKey = Environment
|
||||
.get("BUILDKITE_WEBHOOK_SECRET")?
|
||||
.data(using: .utf8)
|
||||
else {
|
||||
return request.eventLoop.future(
|
||||
error: Abort(
|
||||
.preconditionFailed,
|
||||
reason: "Server not configured with webhook authentication token"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if
|
||||
let signature = request.headers.buildkiteSignature,
|
||||
let payload = request.body.data {
|
||||
return request.eventLoop.tryFuture {
|
||||
try self.validateWebhookPayload(
|
||||
signatureHeader: signature,
|
||||
body: Data(buffer: payload),
|
||||
secretKey: secretKey
|
||||
)
|
||||
}.flatMap {
|
||||
next.respond(to: request)
|
||||
}
|
||||
} else if let token = request.headers.buildkiteToken {
|
||||
return request.eventLoop.tryFuture {
|
||||
try self.validateWebhookPayload(
|
||||
tokenHeader: token,
|
||||
secretKey: secretKey
|
||||
)
|
||||
}.flatMap {
|
||||
next.respond(to: request)
|
||||
}
|
||||
} else {
|
||||
return request.eventLoop.future(
|
||||
error: Abort(.unauthorized)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,14 +7,28 @@
|
|||
//
|
||||
|
||||
import Buildkite
|
||||
import Foundation
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
import Vapor
|
||||
|
||||
@available(macOS 12.0, *)
|
||||
@main struct Example {
|
||||
static func main() {
|
||||
static func main() throws {
|
||||
let app = try Application(.detect())
|
||||
defer { app.shutdown() }
|
||||
|
||||
let buildkite = BuildkiteClient(/*transport: app.client*/)
|
||||
|
||||
app.group(buildkite) {
|
||||
$0.post("buildkite_webhook") { req in
|
||||
let event = try req.content.decode(WebhookEvent.self, using: buildkite)
|
||||
print(event)
|
||||
return HTTPStatus.ok
|
||||
}
|
||||
}
|
||||
|
||||
app.get("buildkite_webhook") { req in
|
||||
"Hello world"
|
||||
}
|
||||
|
||||
try app.run()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,10 +16,18 @@ let package = Package(
|
|||
targets: ["Buildkite"]
|
||||
)
|
||||
],
|
||||
dependencies: [
|
||||
.package(
|
||||
url: "https://github.com/apple/swift-crypto.git",
|
||||
.upToNextMajor(from: "2.0.0")
|
||||
)
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "Buildkite",
|
||||
dependencies: []
|
||||
dependencies: [
|
||||
.product(name: "Crypto", package: "swift-crypto"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "BuildkiteTests",
|
||||
|
|
|
@ -13,17 +13,15 @@ import FoundationNetworking
|
|||
#endif
|
||||
|
||||
public actor BuildkiteClient {
|
||||
private var encoder: JSONEncoder {
|
||||
nonisolated var encoder: JSONEncoder {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.dateEncodingStrategy = .custom(Formatters.encodeISO8601)
|
||||
encoder.keyEncodingStrategy = .convertToSnakeCase
|
||||
return encoder
|
||||
}
|
||||
|
||||
private var decoder: JSONDecoder {
|
||||
nonisolated var decoder: JSONDecoder {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .custom(Formatters.decodeISO8601)
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
return decoder
|
||||
}
|
||||
|
||||
|
|
|
@ -14,8 +14,9 @@ import FoundationNetworking
|
|||
|
||||
public struct Agent: Codable, Equatable, Hashable, Identifiable, Sendable {
|
||||
public var id: UUID
|
||||
public var graphqlId: String
|
||||
public var url: Followable<Agent.Resources.Get>
|
||||
public var webUrl: URL
|
||||
public var webURL: URL
|
||||
public var name: String
|
||||
public var connectionState: String
|
||||
public var hostname: String
|
||||
|
@ -28,4 +29,23 @@ public struct Agent: Codable, Equatable, Hashable, Identifiable, Sendable {
|
|||
public var lastJobFinishedAt: Date?
|
||||
public var priority: Int?
|
||||
public var metaData: [String]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case graphqlId = "graphql_id"
|
||||
case url
|
||||
case webURL = "web_url"
|
||||
case name
|
||||
case connectionState = "connection_state"
|
||||
case hostname
|
||||
case ipAddress = "ip_address"
|
||||
case userAgent = "user_agent"
|
||||
case version
|
||||
case creator
|
||||
case createdAt = "created_at"
|
||||
case job
|
||||
case lastJobFinishedAt = "last_job_finished_at"
|
||||
case priority
|
||||
case metaData = "meta_data"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,4 +26,13 @@ public struct Annotation: Codable, Equatable, Hashable, Identifiable, Sendable {
|
|||
public var bodyHtml: String
|
||||
public var createdAt: Date
|
||||
public var updatedAt: Date
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case context
|
||||
case style
|
||||
case bodyHtml = "body_html"
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ public struct Artifact: Codable, Equatable, Hashable, Identifiable, Sendable {
|
|||
public var id: UUID
|
||||
public var jobId: UUID
|
||||
public var url: Followable<Artifact.Resources.Get>
|
||||
public var downloadUrl: Followable<Artifact.Resources.Download>
|
||||
public var downloadURL: Followable<Artifact.Resources.Download>
|
||||
public var state: State
|
||||
public var path: String
|
||||
public var dirname: String
|
||||
|
@ -35,4 +35,18 @@ public struct Artifact: Codable, Equatable, Hashable, Identifiable, Sendable {
|
|||
public struct URLs: Codable, Equatable {
|
||||
public var url: URL
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case jobId = "job_id"
|
||||
case url
|
||||
case downloadURL = "download_url"
|
||||
case state
|
||||
case path
|
||||
case dirname
|
||||
case filename
|
||||
case mimeType = "mime_type"
|
||||
case fileSize = "file_size"
|
||||
case sha1sum
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,9 @@ import FoundationNetworking
|
|||
|
||||
public struct Build: Codable, Equatable, Hashable, Identifiable, Sendable {
|
||||
public var id: UUID
|
||||
public var graphqlId: String
|
||||
public var url: Followable<Build.Resources.Get>
|
||||
public var webUrl: URL
|
||||
public var webURL: URL
|
||||
public var number: Int
|
||||
public var state: State
|
||||
public var blocked: Bool
|
||||
|
@ -46,4 +47,28 @@ public struct Build: Codable, Equatable, Hashable, Identifiable, Sendable {
|
|||
case notRun = "not_run"
|
||||
case finished
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case graphqlId = "graphql_id"
|
||||
case url
|
||||
case webURL = "web_url"
|
||||
case number
|
||||
case state
|
||||
case blocked
|
||||
case message
|
||||
case commit
|
||||
case branch
|
||||
case env
|
||||
case source
|
||||
case creator
|
||||
case jobs
|
||||
case createdAt = "created_at"
|
||||
case scheduledAt = "scheduled_at"
|
||||
case startedAt = "started_at"
|
||||
case finishedAt = "finished_at"
|
||||
case metaData = "meta_data"
|
||||
case pullRequest = "pull_request"
|
||||
case pipeline
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,15 +68,16 @@ public enum Job: Codable, Equatable, Hashable, Sendable {
|
|||
|
||||
public var type = "script"
|
||||
public var id: UUID
|
||||
public var graphqlId: String
|
||||
public var name: String?
|
||||
public var state: String?
|
||||
public var command: String?
|
||||
public var stepKey: String?
|
||||
public var buildUrl: URL
|
||||
public var webUrl: URL
|
||||
public var logUrl: Followable<Job.Resources.LogOutput>
|
||||
public var rawLogUrl: Followable<Job.Resources.LogOutput.Alternative>
|
||||
public var artifactsUrl: URL
|
||||
public var buildURL: URL
|
||||
public var webURL: URL
|
||||
public var logURL: Followable<Job.Resources.LogOutput>
|
||||
public var rawLogURL: Followable<Job.Resources.LogOutput.Alternative>
|
||||
public var artifactsURL: URL
|
||||
public var softFailed: Bool
|
||||
public var exitStatus: Int?
|
||||
public var artifactPaths: String?
|
||||
|
@ -92,23 +93,74 @@ public enum Job: Codable, Equatable, Hashable, Sendable {
|
|||
public var retriesCount: Int?
|
||||
public var parallelGroupIndex: Int?
|
||||
public var parallelGroupTotal: Int?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case id
|
||||
case graphqlId = "graphql_id"
|
||||
case name
|
||||
case state
|
||||
case command
|
||||
case stepKey = "step_key"
|
||||
case buildURL = "build_url"
|
||||
case webURL = "web_url"
|
||||
case logURL = "log_url"
|
||||
case rawLogURL = "raw_log_url"
|
||||
case artifactsURL = "artifacts_url"
|
||||
case softFailed = "soft_failed"
|
||||
case exitStatus = "exit_status"
|
||||
case artifactPaths = "artifact_paths"
|
||||
case agentQueryRules = "agent_query_rules"
|
||||
case agent
|
||||
case createdAt = "created_at"
|
||||
case scheduledAt = "scheduled_at"
|
||||
case runnableAt = "runnable_at"
|
||||
case startedAt = "started_at"
|
||||
case finishedAt = "finished_at"
|
||||
case retried
|
||||
case retriedInJobId = "retried_in_job_id"
|
||||
case retriesCount = "retries_count"
|
||||
case parallelGroupIndex = "parallel_group_index"
|
||||
case parallelGroupTotal = "parallel_group_total"
|
||||
}
|
||||
}
|
||||
|
||||
public struct Wait: Codable, Equatable, Hashable, Identifiable, Sendable {
|
||||
public var type = "waiter"
|
||||
public var id: UUID
|
||||
public var graphqlId: String
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case id
|
||||
case graphqlId = "graphql_id"
|
||||
}
|
||||
}
|
||||
|
||||
public struct Block: Codable, Equatable, Hashable, Identifiable, Sendable {
|
||||
public var type = "manual"
|
||||
public var id: UUID
|
||||
public var graphqlId: String
|
||||
public var label: String
|
||||
public var state: String
|
||||
public var webUrl: URL?
|
||||
public var webURL: URL?
|
||||
public var unblockedBy: User?
|
||||
public var unblockedAt: Date?
|
||||
public var unblockable: Bool
|
||||
public var unblockUrl: URL
|
||||
public var unblockURL: URL
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case id
|
||||
case graphqlId = "graphql_id"
|
||||
case label
|
||||
case state
|
||||
case webURL = "web_url"
|
||||
case unblockedBy = "unblocked_by"
|
||||
case unblockedAt = "unblocked_at"
|
||||
case unblockable
|
||||
case unblockURL = "unblock_url"
|
||||
}
|
||||
}
|
||||
|
||||
public struct Trigger: Codable, Equatable, Hashable, Sendable {
|
||||
|
@ -116,19 +168,39 @@ public enum Job: Codable, Equatable, Hashable, Sendable {
|
|||
public var id: UUID
|
||||
public var number: Int
|
||||
public var url: URL
|
||||
public var webUrl: URL
|
||||
public var webURL: URL
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case number
|
||||
case url
|
||||
case webURL = "web_url"
|
||||
}
|
||||
}
|
||||
|
||||
public var type = "trigger"
|
||||
public var name: String?
|
||||
public var state: String?
|
||||
public var buildUrl: URL
|
||||
public var webUrl: URL
|
||||
public var buildURL: URL
|
||||
public var webURL: URL
|
||||
public var createdAt: Date
|
||||
public var scheduledAt: Date?
|
||||
public var finishedAt: Date?
|
||||
public var runnableAt: Date?
|
||||
public var triggeredBuild: TriggeredBuild?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case name
|
||||
case state
|
||||
case buildURL = "build_url"
|
||||
case webURL = "web_url"
|
||||
case createdAt = "created_at"
|
||||
case scheduledAt = "scheduled_at"
|
||||
case finishedAt = "finished_at"
|
||||
case runnableAt = "runnable_at"
|
||||
case triggeredBuild = "triggered_build"
|
||||
}
|
||||
}
|
||||
|
||||
public struct LogOutput: Codable, Equatable, Hashable, Sendable {
|
||||
|
@ -136,6 +208,13 @@ public enum Job: Codable, Equatable, Hashable, Sendable {
|
|||
public var content: String
|
||||
public var size: Int
|
||||
public var headerTimes: [Int]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case url
|
||||
case content
|
||||
case size
|
||||
case headerTimes = "header_times"
|
||||
}
|
||||
}
|
||||
|
||||
public struct EnvironmentVariables: Codable, Equatable, Hashable, Sendable {
|
||||
|
|
|
@ -23,7 +23,6 @@ public struct Meta: Codable, Equatable, Hashable, Sendable {
|
|||
public var webhookIPRanges: [String]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
// This corresponds to the key "webhook_ips" from the Buildkite payload.
|
||||
case webhookIPRanges = "webhookIps"
|
||||
case webhookIPRanges = "webhook_ips"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,12 +14,26 @@ import FoundationNetworking
|
|||
|
||||
public struct Organization: Codable, Equatable, Hashable, Identifiable, Sendable {
|
||||
public var id: UUID
|
||||
public var graphqlId: String
|
||||
public var url: Followable<Organization.Resources.Get>
|
||||
public var webUrl: URL
|
||||
public var webURL: URL
|
||||
public var name: String
|
||||
public var slug: String
|
||||
public var pipelinesUrl: Followable<Pipeline.Resources.List>
|
||||
public var agentsUrl: Followable<Agent.Resources.List>
|
||||
public var emojisUrl: Followable<Emoji.Resources.List>
|
||||
public var pipelinesURL: Followable<Pipeline.Resources.List>
|
||||
public var agentsURL: Followable<Agent.Resources.List>
|
||||
public var emojisURL: Followable<Emoji.Resources.List>
|
||||
public var createdAt: Date
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case graphqlId = "graphql_id"
|
||||
case url
|
||||
case webURL = "web_url"
|
||||
case name
|
||||
case slug
|
||||
case pipelinesURL = "pipelines_url"
|
||||
case agentsURL = "agents_url"
|
||||
case emojisURL = "emojis_url"
|
||||
case createdAt = "created_at"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,9 @@ import FoundationNetworking
|
|||
|
||||
public struct Pipeline: Codable, Equatable, Hashable, Identifiable, Sendable {
|
||||
public var id: UUID
|
||||
public var graphqlId: String
|
||||
public var url: Followable<Pipeline.Resources.Get>
|
||||
public var webUrl: URL
|
||||
public var webURL: URL
|
||||
public var name: String
|
||||
public var slug: String
|
||||
public var repository: String
|
||||
|
@ -26,8 +27,8 @@ public struct Pipeline: Codable, Equatable, Hashable, Identifiable, Sendable {
|
|||
public var skipQueuedBranchBuildsFilter: String?
|
||||
public var cancelRunningBranchBuilds: Bool
|
||||
public var cancelRunningBranchBuildsFilter: String?
|
||||
public var buildsUrl: Followable<Build.Resources.ListForPipeline>
|
||||
public var badgeUrl: URL
|
||||
public var buildsURL: Followable<Build.Resources.ListForPipeline>
|
||||
public var badgeURL: URL
|
||||
public var createdAt: Date
|
||||
public var scheduledBuildsCount: Int
|
||||
public var runningBuildsCount: Int
|
||||
|
@ -40,8 +41,42 @@ public struct Pipeline: Codable, Equatable, Hashable, Identifiable, Sendable {
|
|||
|
||||
public struct Provider: Codable, Equatable, Hashable, Identifiable, Sendable {
|
||||
public var id: String
|
||||
public var webhookUrl: URL?
|
||||
public var webhookURL: URL?
|
||||
public var settings: Settings
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case webhookURL = "webhook_url"
|
||||
case settings
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case graphqlId = "graphql_id"
|
||||
case url
|
||||
case webURL = "web_url"
|
||||
case name
|
||||
case slug
|
||||
case repository
|
||||
case branchConfiguration = "branch_configuration"
|
||||
case defaultBranch = "default_branch"
|
||||
case provider
|
||||
case skipQueuedBranchBuilds = "skip_queued_branch_builds"
|
||||
case skipQueuedBranchBuildsFilter = "skip_queued_branch_builds_filter"
|
||||
case cancelRunningBranchBuilds = "cancel_running_branch_builds"
|
||||
case cancelRunningBranchBuildsFilter = "cancel_running_branch_builds_filter"
|
||||
case buildsURL = "builds_url"
|
||||
case badgeURL = "badge_url"
|
||||
case createdAt = "created_at"
|
||||
case scheduledBuildsCount = "scheduled_builds_count"
|
||||
case runningBuildsCount = "running_builds_count"
|
||||
case scheduledJobsCount = "scheduled_jobs_count"
|
||||
case runningJobsCount = "running_jobs_count"
|
||||
case waitingJobsCount = "waiting_jobs_count"
|
||||
case visibility
|
||||
case steps
|
||||
case env
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,6 +111,24 @@ extension Pipeline.Provider {
|
|||
public var separatePullRequestStatuses: Bool?
|
||||
/// The status to use for blocked builds. Pending can be used with required status checks to prevent merging pull requests with blocked builds.
|
||||
public var publishBlockedAsPending: Bool?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case repository
|
||||
case buildPullRequests = "build_pull_requests"
|
||||
case pullRequestBranchFilterEnabled = "pull_request_branch_filter_enabled"
|
||||
case pullRequestBranchFilterConfiguration = "pull_request_branch_filter_configuration"
|
||||
case skipPullRequestBuildsForExistingCommits = "skip_pull_request_builds_for_existing_commits"
|
||||
case buildTags = "build_tags"
|
||||
case publishCommitStatus = "publish_commit_status"
|
||||
case publishCommitStatusPerStep = "publish_commit_status_per_step"
|
||||
case triggerMode = "trigger_mode"
|
||||
case filterEnabled = "filter_enabled"
|
||||
case filterCondition = "filter_condition"
|
||||
case buildPullRequestForks = "build_pull_request_forks"
|
||||
case prefixPullRequestForkBranchNames = "prefix_pull_request_fork_branch_names"
|
||||
case separatePullRequestStatuses = "separate_pull_request_statuses"
|
||||
case publishBlockedAsPending = "publish_blocked_as_pending"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,12 +193,33 @@ extension Pipeline {
|
|||
public var async: Bool?
|
||||
public var concurrency: Int?
|
||||
public var parallelism: Int?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case name
|
||||
case command
|
||||
case label
|
||||
case artifactPaths = "artifact_paths"
|
||||
case branchConfiguration = "branch_configuration"
|
||||
case env
|
||||
case timeoutInMinutes = "timeout_in_minutes"
|
||||
case agentQueryRules = "agent_query_rules"
|
||||
case async
|
||||
case concurrency
|
||||
case parallelism
|
||||
}
|
||||
}
|
||||
|
||||
public struct Wait: Codable, Equatable, Hashable, Sendable {
|
||||
public var type = "waiter"
|
||||
public var label: String?
|
||||
public var continueAfterFailure: Bool?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case label
|
||||
case continueAfterFailure = "continue_after_failure"
|
||||
}
|
||||
}
|
||||
|
||||
public struct Block: Codable, Equatable, Hashable, Sendable {
|
||||
|
@ -160,6 +234,15 @@ extension Pipeline {
|
|||
public var triggerCommit: String?
|
||||
public var triggerBranch: String?
|
||||
public var triggerAsync: Bool?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case triggerProjectSlug = "trigger_project_slug"
|
||||
case label
|
||||
case triggerCommit = "trigger_commit"
|
||||
case triggerBranch = "trigger_branch"
|
||||
case triggerAsync = "trigger_async"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import FoundationNetworking
|
|||
public struct Team: Codable, Equatable, Hashable, Identifiable, Sendable {
|
||||
/// ID of the team
|
||||
public var id: UUID
|
||||
public var graphqlId: String
|
||||
/// Name of the team
|
||||
public var name: String
|
||||
/// URL slug of the team
|
||||
|
@ -34,4 +35,16 @@ public struct Team: Codable, Equatable, Hashable, Identifiable, Sendable {
|
|||
case visible
|
||||
case secret
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case graphqlId = "graphql_id"
|
||||
case name
|
||||
case slug
|
||||
case description
|
||||
case privacy
|
||||
case `default`
|
||||
case createdAt = "created_at"
|
||||
case createdBy = "created_by"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,5 +96,26 @@ public struct Trace: Codable, Equatable, Hashable, Identifiable, Sendable {
|
|||
self.detail = detail
|
||||
self.children = children
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case section
|
||||
case startAt = "start_at"
|
||||
case endAt = "end_at"
|
||||
case duration
|
||||
case detail
|
||||
case children
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case scope
|
||||
case name
|
||||
case identifier
|
||||
case location
|
||||
case fileName = "file_name"
|
||||
case result
|
||||
case failureReason = "failure_reason"
|
||||
case history
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,18 @@ import FoundationNetworking
|
|||
|
||||
public struct User: Codable, Equatable, Hashable, Identifiable, Sendable {
|
||||
public var id: UUID
|
||||
public var graphqlId: String
|
||||
public var name: String
|
||||
public var email: String
|
||||
public var avatarUrl: URL
|
||||
public var avatarURL: URL
|
||||
public var createdAt: Date
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case graphqlId = "graphql_id"
|
||||
case name
|
||||
case email
|
||||
case avatarURL = "avatar_url"
|
||||
case createdAt = "created_at"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,12 +84,17 @@ public enum WebhookEvent: Codable, Equatable, Hashable, Sendable {
|
|||
}
|
||||
|
||||
public struct Ping: Codable, Equatable, Hashable, Sendable {
|
||||
public var event: Event
|
||||
/// The notification service that sent this webhook
|
||||
public var service: Service
|
||||
/// The ``Organization`` this notification belongs to
|
||||
public var organization: Organization
|
||||
/// The user who created the webhook
|
||||
public var sender: Sender
|
||||
|
||||
public enum Event: String, Codable, Equatable, Hashable, Sendable {
|
||||
case ping
|
||||
}
|
||||
}
|
||||
|
||||
public struct Build: Codable, Equatable, Hashable, Sendable {
|
||||
|
@ -103,11 +108,11 @@ public enum WebhookEvent: Codable, Equatable, Hashable, Sendable {
|
|||
|
||||
public enum Event: String, Codable, Equatable, Hashable, Sendable {
|
||||
/// A build has been scheduled
|
||||
case scheduled
|
||||
case scheduled = "build.scheduled"
|
||||
/// A build has started running
|
||||
case running
|
||||
case running = "build.running"
|
||||
/// A build has finished
|
||||
case finished
|
||||
case finished = "build.finished"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,13 +129,13 @@ public enum WebhookEvent: Codable, Equatable, Hashable, Sendable {
|
|||
|
||||
public enum Event: String, Codable, Equatable, Hashable, Sendable {
|
||||
/// A command step job has been scheduled to run on an agent
|
||||
case scheduled
|
||||
case scheduled = "job.scheduled"
|
||||
/// A command step job has started running on an agent
|
||||
case started
|
||||
case started = "job.started"
|
||||
/// A job has finished
|
||||
case finished
|
||||
case finished = "job.finished"
|
||||
/// A block step job has been unblocked using the web or API
|
||||
case activated
|
||||
case activated = "job.activated"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,15 +148,15 @@ public enum WebhookEvent: Codable, Equatable, Hashable, Sendable {
|
|||
|
||||
public enum Event: String, Codable, Equatable, Hashable, Sendable {
|
||||
/// An agent has connected to the API
|
||||
case connected
|
||||
case connected = "agent.connected"
|
||||
/// An agent has been marked as lost. This happens when Buildkite stops receiving pings from the agent
|
||||
case lost
|
||||
case lost = "agent.lost"
|
||||
/// An agent has disconnected. This happens when the agent shuts down and disconnects from the API
|
||||
case disconnected
|
||||
case disconnected = "agent.disconnected"
|
||||
/// An agent is stopping. This happens when an agent is instructed to stop from the API. It first transitions to stopping and finishes any current jobs
|
||||
case stopping
|
||||
case stopping = "agent.stopping"
|
||||
/// An agent has stopped. This happens when an agent is instructed to stop from the API. It can be graceful or forceful
|
||||
case stopped
|
||||
case stopped = "agent.stopped"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
//
|
||||
// BuildkiteClient+Webhooks.swift
|
||||
// Buildkite
|
||||
//
|
||||
// Created by Aaron Sky on 9/10/22.
|
||||
// Copyright © 2022 Aaron Sky. All rights reserved.
|
||||
//
|
||||
|
||||
import Crypto
|
||||
import Foundation
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
extension WebhookEvent {
|
||||
/// Header names present in every Buildkite webhook request.
|
||||
///
|
||||
/// - SeeAlso: https://buildkite.com/docs/apis/webhooks#http-headers
|
||||
public enum HTTPHeaders {
|
||||
/// The type of event
|
||||
public static let buildkiteEvent = "X-Buildkite-Event"
|
||||
/// The webhook's token.
|
||||
public static let buildkiteToken = "X-Buildkite-Token"
|
||||
/// The signature created from your webhook payload, webhook token, and the SHA-256 hash function.
|
||||
public static let buildkiteSignature = "X-Buildkite-Signature"
|
||||
}
|
||||
}
|
||||
|
||||
extension BuildkiteClient {
|
||||
public enum WebhookValidationError: Error {
|
||||
// Token
|
||||
case tokenRefused
|
||||
// Signature
|
||||
case signatureFormatInvalid
|
||||
case signatureCorrupted
|
||||
case payloadCorrupted
|
||||
case signatureRefused
|
||||
// Replay Protection
|
||||
case timestampRefused
|
||||
}
|
||||
|
||||
public nonisolated func decodeWebhook(from body: Data) throws -> WebhookEvent {
|
||||
try decoder.decode(WebhookEvent.self, from: body)
|
||||
}
|
||||
|
||||
/// Validate a webhook payload using the token strategy.
|
||||
///
|
||||
/// - Warning: Buildkite passes the token in clear text. As a consequence, this strategy is less secure than the Buildkite Signature strategy, but easier for rapid testing and less computationally expensive.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - tokenHeader: Value for the header with the name at ``WebhookEvent/HTTPHeaders/buildkiteToken``
|
||||
/// - secretKey: Token value provided by Buildkite
|
||||
public nonisolated func validateWebhookPayload(
|
||||
tokenHeader: String,
|
||||
secretKey: Data
|
||||
) throws {
|
||||
guard secretKey == tokenHeader.data(using: .utf8) else {
|
||||
throw WebhookValidationError.tokenRefused
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate the webhook event using the signature strategy.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - signatureHeader: Value for the header with the name at ``WebhookEvent/HTTPHeaders/buildkiteSignature``
|
||||
/// - body: The request body from Buildkite
|
||||
/// - secretKey: Token value provided by Buildkite
|
||||
/// - replayLimit: Limit in seconds the period of time a webhook event will be accepted. This is used to defend against replay attacks, but is optional.
|
||||
public nonisolated func validateWebhookPayload(
|
||||
signatureHeader: String,
|
||||
body: Data,
|
||||
secretKey: Data,
|
||||
replayLimit: TimeInterval? = nil
|
||||
) throws {
|
||||
let (timestamp, signature) = try getTimestampAndSignature(signatureHeader)
|
||||
guard let macPayload = "\(timestamp).\(body)".data(using: .utf8) else {
|
||||
throw WebhookValidationError.payloadCorrupted
|
||||
}
|
||||
|
||||
try checkMAC(
|
||||
message: macPayload,
|
||||
signature: signature,
|
||||
secretKey: secretKey
|
||||
)
|
||||
|
||||
if let replayLimit = replayLimit {
|
||||
try checkReplayLimit(time: timestamp, replayLimit: replayLimit)
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated func getTimestampAndSignature(_ header: String) throws -> (timestamp: Date, signature: Data) {
|
||||
let parts: [String: String] = Dictionary(
|
||||
uniqueKeysWithValues: header
|
||||
.split(separator: ",")
|
||||
.map { kv in
|
||||
let kvp = kv.split(separator: "=", maxSplits: 2)
|
||||
.map { $0.trimmingCharacters(in: .whitespaces) }
|
||||
return (kvp[0], kvp[1])
|
||||
}
|
||||
)
|
||||
guard parts.count == 2 else {
|
||||
throw WebhookValidationError.signatureFormatInvalid
|
||||
}
|
||||
|
||||
guard
|
||||
let timestamp = parts["timestamp"]
|
||||
.flatMap(TimeInterval.init)
|
||||
.flatMap(Date.init(timeIntervalSince1970:)),
|
||||
// FIXME: Needs to be a hex-decoded
|
||||
let signature = parts["signature"]?
|
||||
.data(using: .utf8)
|
||||
else {
|
||||
throw WebhookValidationError.signatureCorrupted
|
||||
}
|
||||
|
||||
return (timestamp, signature)
|
||||
}
|
||||
|
||||
private nonisolated func checkMAC(message: Data, signature: Data, secretKey: Data) throws {
|
||||
let key = SymmetricKey(data: secretKey)
|
||||
let expectedMAC = HMAC<SHA256>.authenticationCode(
|
||||
for: message,
|
||||
using: key
|
||||
)
|
||||
guard
|
||||
HMAC.isValidAuthenticationCode(
|
||||
expectedMAC,
|
||||
authenticating: signature,
|
||||
using: key
|
||||
)
|
||||
else {
|
||||
throw WebhookValidationError.signatureRefused
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated func checkReplayLimit(
|
||||
time: Date,
|
||||
replayLimit: TimeInterval
|
||||
) throws {
|
||||
guard time.timeIntervalSinceNow <= replayLimit else {
|
||||
throw WebhookValidationError.timestampRefused
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,19 +25,10 @@ enum Formatters {
|
|||
return formatter
|
||||
}()
|
||||
|
||||
static func dateIfPossible(fromISO8601 string: String) -> Date? {
|
||||
if let date = iso8601WithFractionalSeconds.date(from: string) {
|
||||
return date
|
||||
} else if let date = iso8601WithoutFractionalSeconds.date(from: string) {
|
||||
return date
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@Sendable
|
||||
static func encodeISO8601(date: Date, encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
let dateString = iso8601WithoutFractionalSeconds.string(from: date)
|
||||
let dateString = iso8601WithFractionalSeconds.string(from: date)
|
||||
try container.encode(dateString)
|
||||
}
|
||||
|
||||
|
@ -45,14 +36,24 @@ enum Formatters {
|
|||
static func decodeISO8601(decoder: Decoder) throws -> Date {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let dateString = try container.decode(String.self)
|
||||
guard let date = dateIfPossible(fromISO8601: dateString) else {
|
||||
guard let date = Date(iso8601String: dateString) else {
|
||||
throw DecodingError.dataCorrupted(
|
||||
DecodingError.Context(
|
||||
codingPath: container.codingPath,
|
||||
debugDescription: "Expected date string to be ISO8601-formatted."
|
||||
debugDescription: "Expected date string \"\(dateString)\" to be ISO8601-formatted."
|
||||
)
|
||||
)
|
||||
}
|
||||
return date
|
||||
}
|
||||
}
|
||||
|
||||
extension Date {
|
||||
init?(iso8601String: String) {
|
||||
guard
|
||||
let date = Formatters.iso8601WithFractionalSeconds.date(from: iso8601String) ??
|
||||
Formatters.iso8601WithoutFractionalSeconds.date(from: iso8601String)
|
||||
else { return nil }
|
||||
self = date
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,25 +76,3 @@ extension Array where Element == URLQueryItem {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
//extension Date: LosslessStringConvertible {
|
||||
// public init?(
|
||||
// _ description: String
|
||||
// ) {
|
||||
// guard let date = Formatters.dateIfPossible(fromISO8601: description) else {
|
||||
// return nil
|
||||
// }
|
||||
// self = date
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension UUID: LosslessStringConvertible {
|
||||
// public init?(
|
||||
// _ description: String
|
||||
// ) {
|
||||
// guard let id = UUID(uuidString: description) else {
|
||||
// return nil
|
||||
// }
|
||||
// self = id
|
||||
// }
|
||||
//}
|
||||
|
|
|
@ -20,7 +20,7 @@ import FoundationNetworking
|
|||
/// ```swift
|
||||
/// let client = BuildkiteClient(token: "...")
|
||||
/// let organizationResponse = await client.send(.organization("buildkite")
|
||||
/// let agentsResponse = await client.send(organizationResponse.content.agentsUrl)
|
||||
/// let agentsResponse = await client.send(organizationResponse.content.agentsURL)
|
||||
/// print(agentsResponse.content) // Array<Agent>(...)
|
||||
/// ```
|
||||
public struct Followable<R: Resource>: Resource, Codable, Equatable, Hashable, Sendable {
|
||||
|
|
|
@ -69,7 +69,16 @@ extension TestAnalytics.Resources {
|
|||
public var queued: Int
|
||||
public var skipped: Int
|
||||
public var errors: [String]
|
||||
public var runUrl: URL
|
||||
public var runURL: URL
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case runId = "run_id"
|
||||
case queued
|
||||
case skipped
|
||||
case errors
|
||||
case runURL = "run_url"
|
||||
}
|
||||
}
|
||||
|
||||
public var version: APIVersion {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
// FormattersTests.swift
|
||||
// Buildkite
|
||||
//
|
||||
// Copyright © 2022 Aaron Sky. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import Buildkite
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
final class FormattersTests: XCTestCase {
|
||||
func testDecode() throws {
|
||||
// let expected = DateComponents(
|
||||
// year: 2020,
|
||||
// month: 3,
|
||||
// day: 14,
|
||||
// hour: 20,
|
||||
// minute: 4,
|
||||
// second: 43
|
||||
// ).date!
|
||||
// let actual = try XCTUnwrap(
|
||||
// Formatters.dateIfPossible(
|
||||
// fromISO8601: "2020-03-14T20:04:43.567Z"
|
||||
// )
|
||||
// )
|
||||
// XCTAssertEqual(actual, expected)
|
||||
}
|
||||
|
||||
func testEncode() {
|
||||
|
||||
}
|
||||
}
|
|
@ -20,15 +20,16 @@ extension Agent {
|
|||
let job = Job.script(
|
||||
Job.Command(
|
||||
id: UUID(),
|
||||
graphqlId: "",
|
||||
name: "📦",
|
||||
state: "passed",
|
||||
command: nil,
|
||||
stepKey: nil,
|
||||
buildUrl: URL(),
|
||||
webUrl: URL(),
|
||||
logUrl: Followable(),
|
||||
rawLogUrl: Followable(),
|
||||
artifactsUrl: URL(),
|
||||
buildURL: URL(),
|
||||
webURL: URL(),
|
||||
logURL: Followable(),
|
||||
rawLogURL: Followable(),
|
||||
artifactsURL: URL(),
|
||||
softFailed: false,
|
||||
exitStatus: 0,
|
||||
artifactPaths: nil,
|
||||
|
@ -49,8 +50,9 @@ extension Agent {
|
|||
|
||||
self.init(
|
||||
id: UUID(),
|
||||
graphqlId: "",
|
||||
url: Followable(),
|
||||
webUrl: URL(),
|
||||
webURL: URL(),
|
||||
name: "jeffrey",
|
||||
connectionState: "connected",
|
||||
hostname: "jeffrey",
|
||||
|
|
|
@ -21,7 +21,7 @@ extension Artifact {
|
|||
id: UUID(),
|
||||
jobId: UUID(),
|
||||
url: Followable(),
|
||||
downloadUrl: Followable(),
|
||||
downloadURL: Followable(),
|
||||
state: .new,
|
||||
path: "",
|
||||
dirname: "",
|
||||
|
|
|
@ -19,8 +19,9 @@ extension Build {
|
|||
init() {
|
||||
self.init(
|
||||
id: UUID(),
|
||||
graphqlId: "",
|
||||
url: Followable(),
|
||||
webUrl: URL(),
|
||||
webURL: URL(),
|
||||
number: 1,
|
||||
state: .passed,
|
||||
blocked: false,
|
||||
|
|
|
@ -17,7 +17,7 @@ import FoundationNetworking
|
|||
|
||||
class JobsTests: XCTestCase {
|
||||
func testJobsRetryWaiter() async throws {
|
||||
let expected: Job = .waiter(Job.Wait(id: UUID()))
|
||||
let expected: Job = .waiter(Job.Wait(id: UUID(), graphqlId: ""))
|
||||
let context = try MockContext(content: expected)
|
||||
|
||||
let response = try await context.client.send(
|
||||
|
@ -32,8 +32,8 @@ class JobsTests: XCTestCase {
|
|||
Job.Trigger(
|
||||
name: nil,
|
||||
state: nil,
|
||||
buildUrl: URL(),
|
||||
webUrl: URL(),
|
||||
buildURL: URL(),
|
||||
webURL: URL(),
|
||||
createdAt: Date(timeIntervalSince1970: 1000),
|
||||
scheduledAt: nil,
|
||||
finishedAt: nil,
|
||||
|
@ -42,7 +42,7 @@ class JobsTests: XCTestCase {
|
|||
id: UUID(),
|
||||
number: 0,
|
||||
url: URL(),
|
||||
webUrl: URL()
|
||||
webURL: URL()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -59,13 +59,14 @@ class JobsTests: XCTestCase {
|
|||
let expected: Job = .manual(
|
||||
Job.Block(
|
||||
id: UUID(),
|
||||
graphqlId: "",
|
||||
label: "",
|
||||
state: "",
|
||||
webUrl: nil,
|
||||
webURL: nil,
|
||||
unblockedBy: User(),
|
||||
unblockedAt: Date(timeIntervalSince1970: 1000),
|
||||
unblockable: true,
|
||||
unblockUrl: URL()
|
||||
unblockURL: URL()
|
||||
)
|
||||
)
|
||||
let context = try MockContext(content: expected)
|
||||
|
|
|
@ -19,13 +19,14 @@ extension Organization {
|
|||
init() {
|
||||
self.init(
|
||||
id: UUID(),
|
||||
graphqlId: "",
|
||||
url: Followable(),
|
||||
webUrl: URL(),
|
||||
webURL: URL(),
|
||||
name: "Buildkite",
|
||||
slug: "buildkite",
|
||||
pipelinesUrl: Followable(),
|
||||
agentsUrl: Followable(),
|
||||
emojisUrl: Followable(),
|
||||
pipelinesURL: Followable(),
|
||||
agentsURL: Followable(),
|
||||
emojisURL: Followable(),
|
||||
createdAt: Date(timeIntervalSince1970: 1000)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -21,8 +21,9 @@ extension Pipeline {
|
|||
) {
|
||||
self.init(
|
||||
id: UUID(),
|
||||
graphqlId: "",
|
||||
url: Followable(),
|
||||
webUrl: URL(),
|
||||
webURL: URL(),
|
||||
name: "My Pipeline",
|
||||
slug: "my-pipeline",
|
||||
repository: "git@github.com:buildkite/agent.git",
|
||||
|
@ -30,7 +31,7 @@ extension Pipeline {
|
|||
defaultBranch: "master",
|
||||
provider: Provider(
|
||||
id: "github",
|
||||
webhookUrl: URL(),
|
||||
webhookURL: URL(),
|
||||
settings: Provider.Settings(
|
||||
repository: nil,
|
||||
buildPullRequests: nil,
|
||||
|
@ -53,8 +54,8 @@ extension Pipeline {
|
|||
skipQueuedBranchBuildsFilter: nil,
|
||||
cancelRunningBranchBuilds: false,
|
||||
cancelRunningBranchBuildsFilter: nil,
|
||||
buildsUrl: Followable(),
|
||||
badgeUrl: URL(),
|
||||
buildsURL: Followable(),
|
||||
badgeURL: URL(),
|
||||
createdAt: Date(timeIntervalSince1970: 1000),
|
||||
scheduledBuildsCount: 0,
|
||||
runningBuildsCount: 0,
|
||||
|
|
|
@ -19,6 +19,7 @@ extension Team {
|
|||
fileprivate init() {
|
||||
self.init(
|
||||
id: UUID(),
|
||||
graphqlId: "",
|
||||
name: "",
|
||||
slug: "",
|
||||
description: "",
|
||||
|
|
|
@ -19,9 +19,10 @@ extension User {
|
|||
init() {
|
||||
self.init(
|
||||
id: UUID(),
|
||||
graphqlId: "",
|
||||
name: "Jeff",
|
||||
email: "jeff@buildkite.com",
|
||||
avatarUrl: URL(),
|
||||
avatarURL: URL(),
|
||||
createdAt: Date(timeIntervalSince1970: 1000)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ class TestAnalyticsUpload: XCTestCase {
|
|||
.init(id: .init(), history: .init(section: "http")),
|
||||
.init(id: .init(), history: .init(section: "http")),
|
||||
]
|
||||
let expected = Resource.Content(id: .init(), runId: .init(), queued: 0, skipped: 0, errors: [], runUrl: .init())
|
||||
let expected = Resource.Content(id: .init(), runId: .init(), queued: 0, skipped: 0, errors: [], runURL: .init())
|
||||
let context = try MockContext(content: expected)
|
||||
|
||||
let response = try await context.client.send(
|
||||
|
|
Loading…
Reference in New Issue