Skip to content

T-8a: GitHubFeature state model + basic reducer #224

@kirich1409

Description

@kirich1409

Description

Implement TCA reducer GitHubFeature in GitHubIntegrationFeature/GitHubFeature.swiftstate model + basic reducer actions only. Polling coordinator → T-8b.

(1) Public state types

public enum CIStatusView: Equatable, Sendable {
    case unknown
    case fresh(GHCombinedStatus)
    case stale(GHCombinedStatus, age: Duration)
    case error(GitHubAPIError)
}

public struct Etags: Equatable, Sendable {
    public var combinedStatus: String?
    public var workflowRuns: String?
}

public struct BranchGitHubState: Equatable, Sendable {
    public var remote: GitRemote
    public var branch: String
    public var combinedStatus: CIStatusView
    public var workflowRuns: [GHWorkflowRun]
    public var etags: Etags
    public var lastFetchedAt: Date?
    public var quarantined: Bool   // true after .forbidden(.saml) — excluded from poll
}

public enum AuthState: Equatable, Sendable {
    case signedOut
    case signingIn
    case signedIn(GHAuthInfo)
    case error(GitHubAuthError)
}

(2) Reducer shape

@Reducer public struct GitHubFeature {
    @ObservableState public struct State: Equatable, Sendable {
        public var auth: AuthState = .signedOut
        public var perBranch: [URL: BranchGitHubState] = [:]
        public var selectedWorktreePath: URL? = nil
        public var windowActive: Bool = true
    }

    public enum Action: Sendable {
        case signInTapped
        case signInResult(Result<GHAuthInfo, GitHubAuthError>)
        case signOutTapped
        case remotesUpdated([URL: (remote: GitRemote, branch: String)])
        case selectedWorktreeChanged(URL?)
        case refreshNowTapped(URL?)
        case openInBrowser(URL)
    }

    public init()
}

(3) Reducer behavior

  • .signInTappedstate.auth = .signingIn; effect: authService.signIn(...).signInResult
  • .signInResult(.success(info)).signedIn(info); log GitHubLog.auth (no PII)
  • .signInResult(.failure(err)).error(err); log without sensitive fields
  • .signOutTappedclear all: auth = .signedOut, perBranch = [:], selectedWorktreePath = nil; effect: authService.signOut(login) + credentialStore.delete(for: login) + credentialStore.setCurrentAccount(nil)
  • .remotesUpdated(map) → add/update BranchGitHubState for each github worktree (.unknown status if new); eviction: remove entries whose paths are absent in map (ETag cleanup)
  • .selectedWorktreeChanged(path)state.selectedWorktreePath = path
  • .refreshNowTapped(path) → translated to immediate pollTick in T-8b
  • .openInBrowser(url) → effect @Dependency(\.openURL)(url)

(4) Dependencies

Register DependencyKey conformances in this task:

  • @Dependency(\.gitHubAuthService)
  • @Dependency(\.gitHubCredentialStore)
  • @Dependency(\.openURL)

Spec reference

See swarm-report/github-integration-decomposition.md#t-8a (split from original T-8 per BA critical-1).

Relationships

Acceptance criteria

TestStore tests:

  • .signInTapped.signingIn; mock auth success → .signedIn(info)
  • .signInResult(.failure(err)).error(err)
  • .signOutTappedperBranch = [:], selectedWorktreePath = nil, auth = .signedOut
  • .remotesUpdated for 2 github worktrees → perBranch has 2 entries with .unknown
  • .remotesUpdated where pathA is absent in new map → entry for pathA removed (eviction)
  • .remotesUpdated with non-github host → entry NOT added (filtered)
  • Swift 6 strict concurrency clean
  • Confirms GitHubAPIError: Equatable (depends on T-5 complete)

Complexity

L

Suggested agent

developer-workflow:swift-engineer

Module / Layer

GitHubIntegrationFeature / State + basic Effects

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions