Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 29 additions & 29 deletions backend/internal/adapters/scm/github/observer_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,36 +57,36 @@ func (p *Provider) RepoPRListGuard(ctx context.Context, repo ports.SCMRepo, etag
return ports.SCMGuardResult{ETag: firstNonEmptyHeader(resp.ETag, etag), NotModified: resp.NotModified}, nil
}

// DetectPRByBranch finds an open PR whose head branch matches the session branch.
func (p *Provider) DetectPRByBranch(ctx context.Context, repo ports.SCMRepo, branch string) (ports.SCMPRObservation, error) {
branch = strings.TrimSpace(branch)
if branch == "" {
return ports.SCMPRObservation{}, fmt.Errorf("%w: empty branch", ErrNotFound)
}
pulls, err := p.detectPRByHead(ctx, repo, repo.Owner+":"+branch)
if err != nil {
return ports.SCMPRObservation{}, err
}
if len(pulls) == 0 {
return ports.SCMPRObservation{}, fmt.Errorf("%w: no open PR for branch %s", ErrNotFound, branch)
}
return restListPullToSCM(pulls[0]), nil
}

func (p *Provider) detectPRByHead(ctx context.Context, repo ports.SCMRepo, head string) ([]restListPull, error) {
q := url.Values{}
q.Set("state", "open")
q.Set("head", head)
q.Set("per_page", "10")
resp, err := p.client.doREST(ctx, http.MethodGet, repoPath(repo.Owner, repo.Name, "pulls"), q, nil)
if err != nil {
return nil, err
}
var pulls []restListPull
if err := json.Unmarshal(resp.Body, &pulls); err != nil {
return nil, fmt.Errorf("github scm: decode branch PR list: %w", err)
// ListOpenPRsByRepo lists every open pull request in the repository so the
// observer can attribute each to a session by head-branch prefix. It paginates
// the REST pulls endpoint; AO repos are not expected to carry thousands of
// concurrent open PRs, and the observer only calls this when the repo PR-list
// ETag guard reports a change.
func (p *Provider) ListOpenPRsByRepo(ctx context.Context, repo ports.SCMRepo) ([]ports.SCMPRObservation, error) {
const perPage = 100
out := []ports.SCMPRObservation{}
for page := 1; ; page++ {
q := url.Values{}
q.Set("state", "open")
q.Set("sort", "updated")
q.Set("direction", "desc")
q.Set("per_page", strconv.Itoa(perPage))
q.Set("page", strconv.Itoa(page))
resp, err := p.client.doREST(ctx, http.MethodGet, repoPath(repo.Owner, repo.Name, "pulls"), q, nil)
if err != nil {
return nil, err
}
var pulls []restListPull
if err := json.Unmarshal(resp.Body, &pulls); err != nil {
return nil, fmt.Errorf("github scm: decode open PR list: %w", err)
}
for _, pull := range pulls {
out = append(out, restListPullToSCM(pull))
}
if len(pulls) < perPage {
return out, nil
}
}
return pulls, nil
}

// CommitChecksGuard checks GitHub's per-commit check-runs ETag guard.
Expand Down
2 changes: 2 additions & 0 deletions backend/internal/domain/pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type PRFacts struct {
Review ReviewDecision
Mergeability Mergeability
ReviewComments bool // has unresolved review comments (any author) to address
SourceBranch string
TargetBranch string
UpdatedAt time.Time
}

Expand Down
4 changes: 4 additions & 0 deletions backend/internal/domain/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,8 @@ type Session struct {
SessionRecord
Status SessionStatus `json:"status"`
TerminalHandleID string `json:"terminalHandleId,omitempty"`
// PRs are the session's attributed pull requests (one session can own many).
// They feed status derivation and are surfaced on the API read model. Not
// serialized here: the HTTP boundary maps them to the curated wire shape.
PRs []PRFacts `json:"-"`
}
87 changes: 46 additions & 41 deletions backend/internal/httpd/apispec/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,49 @@ components:
- sessionId
- reason
type: object
ControllersSessionView:
properties:
activity:
$ref: '#/components/schemas/DomainActivity'
createdAt:
format: date-time
type: string
displayName:
type: string
harness:
type: string
id:
type: string
isTerminated:
type: boolean
issueId:
type: string
kind:
type: string
projectId:
type: string
prs:
items:
$ref: '#/components/schemas/SessionPRFacts'
type: array
status:
type: string
terminalHandleId:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- projectId
- kind
- activity
- isTerminated
- createdAt
- updatedAt
- status
- prs
type: object
DegradedProject:
properties:
id:
Expand Down Expand Up @@ -1220,7 +1263,7 @@ components:
properties:
sessions:
items:
$ref: '#/components/schemas/Session'
$ref: '#/components/schemas/ControllersSessionView'
type: array
required:
- sessions
Expand Down Expand Up @@ -1397,7 +1440,7 @@ components:
ok:
type: boolean
session:
$ref: '#/components/schemas/Session'
$ref: '#/components/schemas/ControllersSessionView'
sessionId:
type: string
required:
Expand Down Expand Up @@ -1496,44 +1539,6 @@ components:
- sessionId
- message
type: object
Session:
properties:
activity:
$ref: '#/components/schemas/DomainActivity'
createdAt:
format: date-time
type: string
displayName:
type: string
harness:
type: string
id:
type: string
isTerminated:
type: boolean
issueId:
type: string
kind:
type: string
projectId:
type: string
status:
type: string
terminalHandleId:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- projectId
- kind
- activity
- isTerminated
- createdAt
- updatedAt
- status
type: object
SessionPRFacts:
properties:
ci:
Expand Down Expand Up @@ -1571,7 +1576,7 @@ components:
SessionResponse:
properties:
session:
$ref: '#/components/schemas/Session'
$ref: '#/components/schemas/ControllersSessionView'
required:
- session
type: object
Expand Down
15 changes: 12 additions & 3 deletions backend/internal/httpd/controllers/dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,18 @@ type CleanupSessionsQuery struct {
Project string `query:"project,omitempty" description:"Project id filter. When omitted, clean terminated sessions across all projects."`
}

// SessionView is the session wire shape: the domain read model plus the
// session's attributed pull requests in the curated SessionPRFacts shape. One
// session can own many PRs (e.g. a stack), so prs is a list. The embedded
// domain.Session.PRs field is json:"-"; this curated prs is what serializes.
type SessionView struct {
domain.Session
PRs []SessionPRFacts `json:"prs"`
}

// ListSessionsResponse is the body of GET /api/v1/sessions.
type ListSessionsResponse struct {
Sessions []domain.Session `json:"sessions"`
Sessions []SessionView `json:"sessions"`
}

// SpawnSessionRequest is the body of POST /api/v1/sessions.
Expand All @@ -128,7 +137,7 @@ type SpawnSessionRequest struct {

// SessionResponse is the { session } body shared by session create/get.
type SessionResponse struct {
Session domain.Session `json:"session"`
Session SessionView `json:"session"`
}

// RenameSessionRequest is the body of PATCH /api/v1/sessions/{sessionId}.
Expand All @@ -147,7 +156,7 @@ type RenameSessionResponse struct {
type RestoreSessionResponse struct {
OK bool `json:"ok"`
SessionID domain.SessionID `json:"sessionId"`
Session domain.Session `json:"session"`
Session SessionView `json:"session"`
}

// KillSessionResponse is the body of POST /api/v1/sessions/{sessionId}/kill.
Expand Down
24 changes: 18 additions & 6 deletions backend/internal/httpd/controllers/sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (c *SessionsController) list(w http.ResponseWriter, r *http.Request) {
envelope.WriteError(w, r, err)
return
}
envelope.WriteJSON(w, http.StatusOK, ListSessionsResponse{Sessions: sessions})
envelope.WriteJSON(w, http.StatusOK, ListSessionsResponse{Sessions: sessionViews(sessions)})
}

func (c *SessionsController) spawn(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -117,7 +117,7 @@ func (c *SessionsController) spawn(w http.ResponseWriter, r *http.Request) {
envelope.WriteError(w, r, err)
return
}
envelope.WriteJSON(w, http.StatusCreated, SessionResponse{Session: sess})
envelope.WriteJSON(w, http.StatusCreated, SessionResponse{Session: sessionView(sess)})
}

func (c *SessionsController) get(w http.ResponseWriter, r *http.Request) {
Expand All @@ -130,7 +130,7 @@ func (c *SessionsController) get(w http.ResponseWriter, r *http.Request) {
envelope.WriteError(w, r, err)
return
}
envelope.WriteJSON(w, http.StatusOK, SessionResponse{Session: sess})
envelope.WriteJSON(w, http.StatusOK, SessionResponse{Session: sessionView(sess)})
}

func (c *SessionsController) listPRs(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -204,7 +204,7 @@ func (c *SessionsController) restore(w http.ResponseWriter, r *http.Request) {
envelope.WriteError(w, r, err)
return
}
envelope.WriteJSON(w, http.StatusOK, RestoreSessionResponse{OK: true, SessionID: sessionID(r), Session: sess})
envelope.WriteJSON(w, http.StatusOK, RestoreSessionResponse{OK: true, SessionID: sessionID(r), Session: sessionView(sess)})
}

func (c *SessionsController) kill(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -344,7 +344,7 @@ func (c *SessionsController) listOrchestrators(w http.ResponseWriter, r *http.Re
envelope.WriteError(w, r, err)
return
}
envelope.WriteJSON(w, http.StatusOK, ListSessionsResponse{Sessions: sessions})
envelope.WriteJSON(w, http.StatusOK, ListSessionsResponse{Sessions: sessionViews(sessions)})
}

func (c *SessionsController) getOrchestrator(w http.ResponseWriter, r *http.Request) {
Expand All @@ -361,7 +361,7 @@ func (c *SessionsController) getOrchestrator(w http.ResponseWriter, r *http.Requ
envelope.WriteAPIError(w, r, http.StatusNotFound, "not_found", "SESSION_NOT_FOUND", "Unknown session", nil)
return
}
envelope.WriteJSON(w, http.StatusOK, SessionResponse{Session: sess})
envelope.WriteJSON(w, http.StatusOK, SessionResponse{Session: sessionView(sess)})
}

func sessionID(r *http.Request) domain.SessionID {
Expand Down Expand Up @@ -432,6 +432,18 @@ func writeSessionPRError(w http.ResponseWriter, r *http.Request, err error) {
}
}

func sessionView(s domain.Session) SessionView {
return SessionView{Session: s, PRs: sessionPRFacts(s.PRs)}
}

func sessionViews(sessions []domain.Session) []SessionView {
out := make([]SessionView, 0, len(sessions))
for _, s := range sessions {
out = append(out, sessionView(s))
}
return out
}

func sessionPRFacts(prs []domain.PRFacts) []SessionPRFacts {
out := make([]SessionPRFacts, 0, len(prs))
for _, pr := range prs {
Expand Down
Loading
Loading