Description
Implement public struct GitHubClient: Sendable in GitHubIntegrationClient/GitHubClient.swift:
public init(token: String, session: URLSession = .shared)
Three MVP functions
1. Combined Status (trust-critical aggregate):
func getCombinedStatus(owner: String, repo: String, ref: String, etag: String?)
async throws(GitHubAPIError) -> (GHCombinedStatus?, newETag: String?)
GET /repos/{o}/{r}/commits/{ref}/status
2. Workflow runs for branch:
func listWorkflowRuns(owner: String, repo: String, branch: String, etag: String?)
async throws(GitHubAPIError) -> (GHPagedWorkflowRuns?, newETag: String?)
GET /repos/{o}/{r}/actions/runs?branch={branch}&per_page=10
3. Validate token:
func validateToken() async throws(GitHubAPIError) -> GHAuthInfo
GET /user + GET /rate_limit + parse X-OAuth-Scopes.
DTOs (Sendable + Codable + Equatable)
GHCombinedStatus { state: String, statuses: [GHStatusItem] } (state: success|pending|failure)
GHStatusItem { context, state, description, target_url }
GHPagedWorkflowRuns { total_count, workflow_runs: [GHWorkflowRun] }
GHWorkflowRun { id, name, status, conclusion, head_branch, html_url, workflow_id, created_at, pull_requests: [GHPullRequestStub] }
GHPullRequestStub { id, number, head_ref, html_url? }
GHAuthInfo { login, scopes: [String], rateLimit: GHRateLimit }
GHRateLimit { limit, remaining, resetAt: Date }
Typed errors
public enum GitHubAPIError: Error, Equatable, Sendable {
case unauthorized
case forbidden(reason: ForbidReason)
case notFound
case rateLimited(resetAt: Date)
case networkError(URLError)
case decodingFailed(description: String)
case serverError(statusCode: Int)
}
public enum ForbidReason: Equatable, Sendable { case rateLimit, saml, scope, other }
Helper
func perform<T: Decodable>(path:, etag:, decodeAs: T.Type)
async throws(GitHubAPIError) -> (T?, String?)
Headers (every request): Authorization: Bearer <token> (NEVER logged), Accept: application/vnd.github+json, X-GitHub-Api-Version: 2026-03-10, User-Agent: Relay/<bundleVersion>, If-None-Match: <etag> when present.
HTTP mapping: 200 → decode+ETag; 304 → (nil, requestETag); 401 → unauthorized; 403 inspect headers (ratelimit-remaining=0 → rateLimit; x-github-sso or SAML in body → saml; else scope/other); 404 → notFound; 429 → rateLimited with x-ratelimit-reset; 5xx → serverError; malformed JSON → decodingFailed.
Spec reference
See swarm-report/github-integration-decomposition.md#t-5 + research (A1 REST+ETag).
Relationships
Acceptance criteria
Unit tests via URLProtocol mock:
Complexity
L
Suggested agent
developer-workflow:swift-engineer
Module / Layer
GitHubIntegrationClient / Infrastructure (HTTP)
Description
Implement
public struct GitHubClient: SendableinGitHubIntegrationClient/GitHubClient.swift:Three MVP functions
1. Combined Status (trust-critical aggregate):
GET /repos/{o}/{r}/commits/{ref}/status2. Workflow runs for branch:
GET /repos/{o}/{r}/actions/runs?branch={branch}&per_page=103. Validate token:
GET /user+GET /rate_limit+ parseX-OAuth-Scopes.DTOs (Sendable + Codable + Equatable)
GHCombinedStatus { state: String, statuses: [GHStatusItem] }(state:success|pending|failure)GHStatusItem { context, state, description, target_url }GHPagedWorkflowRuns { total_count, workflow_runs: [GHWorkflowRun] }GHWorkflowRun { id, name, status, conclusion, head_branch, html_url, workflow_id, created_at, pull_requests: [GHPullRequestStub] }GHPullRequestStub { id, number, head_ref, html_url? }GHAuthInfo { login, scopes: [String], rateLimit: GHRateLimit }GHRateLimit { limit, remaining, resetAt: Date }Typed errors
Helper
Headers (every request):
Authorization: Bearer <token>(NEVER logged),Accept: application/vnd.github+json,X-GitHub-Api-Version: 2026-03-10,User-Agent: Relay/<bundleVersion>,If-None-Match: <etag>when present.HTTP mapping: 200 → decode+ETag; 304 →
(nil, requestETag); 401 → unauthorized; 403 inspect headers (ratelimit-remaining=0 → rateLimit; x-github-sso or SAML in body → saml; else scope/other); 404 → notFound; 429 → rateLimited withx-ratelimit-reset; 5xx → serverError; malformed JSON → decodingFailed.Spec reference
See
swarm-report/github-integration-decomposition.md#t-5+ research (A1 REST+ETag).Relationships
apicategory)Acceptance criteria
Unit tests via
URLProtocolmock:(decoded, newETag)(nil, previousETag).unauthorizedx-ratelimit-remaining: 0→.forbidden(.rateLimit)x-github-sso→.forbidden(.saml).forbidden(.scope)or.forbidden(.other)x-ratelimit-reset: <epoch>→.rateLimited(resetAt)with correct Date.serverError(status)URLError.notConnectedToInternet→.networkError(...).decodingFailedAuthorizationheader NOT present in any logger outputGitHubAPIError: Equatableworks (used by T-8a TestStore)Complexity
L
Suggested agent
developer-workflow:swift-engineerModule / Layer
GitHubIntegrationClient/ Infrastructure (HTTP)