Skip to content
Merged
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
1 change: 1 addition & 0 deletions cmd/codeaf/appfactory.go
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,7 @@ func (b *appBackend) FactoryCapabilities(context.Context) (appwire.FactoryCapabi
Local: true, Runner: true, Writes: false, Admin: true,
Links: true, CopyLink: b.copier().Available(), ReposManage: true, RunnerManage: true, Rules: true, PipelinePR: true, PipelinePRs: true, PipelinePRUpdate: true, PipelineSourceRef: true, SpendByRepo: true, SpendByModel: true, PipelineEdit: true, PipelineEditRuntime: true,
PipelineEditStructure: true, PipelineEditRules: true, RepoRead: true, RepoFacts: true, GroupPolicy: true, WaitsOn: true, MaxConnections: appwire.MaxFactoryConnections, MaxFlows: appwire.MaxFactoryFlows,
RepoIssues: true,
}, nil
}

Expand Down
6 changes: 5 additions & 1 deletion cmd/codeaf/appfactory_admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ type adminFactory struct {
// no way to save the list at all.
afterStore error
noRepoSwitch bool
// tokens is the factory's access-token file, for tests that mint a row as
// `codeaf factory token add` does.
tokens string
}

func startAdminFactory(t *testing.T) *adminFactory { return startAdminFactoryAt(t, true) }
Expand Down Expand Up @@ -75,6 +78,7 @@ func startAdminFactoryWith(t *testing.T, forwarded, noRepoSwitch bool) *adminFac
t.Fatal(err)
}
f.st = st
f.tokens = filepath.Join(dir, "tokens.json")
seam := factory.Seam{Load: func() (factory.Snapshot, error) { return factory.Snapshot{}, nil }}
if !f.noRepoSwitch {
seam.SetRepos = func(repos []string) error {
Expand All @@ -87,7 +91,7 @@ func startAdminFactoryWith(t *testing.T, forwarded, noRepoSwitch bool) *adminFac
h := serve.New(seam, serve.Config{
Version: "test", Name: "Acme factory", ServedBy: "factory", Address: adminFactoryAddress, AccessToken: testHostToken, Mode: api.FactoryTeam,
GitHubClientID: "client", GitHubHTTPClient: f.github.client(), Now: f.clock.now, Store: st, Record: rec, Root: st.Root(),
TokensPath: filepath.Join(dir, "tokens.json"),
TokensPath: f.tokens,
})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/about" {
Expand Down
96 changes: 96 additions & 0 deletions cmd/codeaf/appfactory_repo_issues.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package main

import (
"context"
"encoding/json"
"errors"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/Agent-Field/codeaf/internal/appwire"
"github.com/Agent-Field/codeaf/internal/factory/api"
)

var _ appwire.FactoryRepoIssuesReader = (*appBackend)(nil)

// FactoryRepoIssues reads one fixed route through the caller's connection.
func (b *appBackend) FactoryRepoIssues(ctx context.Context, p appwire.FactoryRepoIssuesParams) (appwire.FactoryRepoIssues, error) {
p, err := appwire.CheckRepoIssues(p)
if err != nil {
return appwire.FactoryRepoIssues{}, err
}
owner, name, _ := strings.Cut(p.Repo, "/")
query := url.Values{"page": {strconv.Itoa(*p.Page)}}
if p.Since != "" {
query.Set("since", p.Since)
}
path := appFactoryRepoRoute + url.PathEscape(owner) + "/" + url.PathEscape(name) + "/issues?" + query.Encode()
got, err := b.factoryHub().readRepoIssues(ctx, p.Factory, path)
if err != nil {
return appwire.FactoryRepoIssues{}, err
}
if got.Issues == nil {
got.Issues = []api.RepoIssue{}
}
return appwire.FactoryRepoIssues{Factory: p.Factory, RepoIssues: got}, nil
}

// This read keeps the response body until refusals have been decoded, because
// the ordinary read mapping has no rate-limit retry detail.
func (h *appFactoryHub) readRepoIssues(ctx context.Context, id, path string) (api.RepoIssues, error) {
var out api.RepoIssues
row, token, err := h.credentialed(ctx, id)
if err != nil {
return out, err
}
r, err := h.send(ctx, row.URL, token, http.MethodGet, path, nil, appFactoryBody, nil)
if err != nil {
return out, err
}
if err := h.staleCredential(id, token); err != nil {
return out, err
}
if r.status != http.StatusOK {
err := repoIssuesFailure(r)
var wire *appwire.Error
if errors.As(err, &wire) && wire.Code == appwire.CodeSignedOut {
h.refused(id, token)
}
return out, err
}
if json.Unmarshal(r.body, &out) != nil {
return out, appwire.Errorf(appwire.CodeEngine, "the factory sent an answer this app cannot read")
}
h.touch(id)
return out, nil
}

// repoIssuesFailure keeps signed_out, which forgets a restored sign-in, for the
// Factory's sign-in gate refusing the bearer. That gate always sends a bearer
// challenge. A 401 without one comes from the route's own GitHub step: the
// sign-in stands but has no GitHub access the route can use, such as a pasted
// access token with no GitHub sign-in. That is github_expired, including from a
// Factory that sent the step without a reason.
func repoIssuesFailure(r appFactoryReply) error {
if r.status == http.StatusUnauthorized && r.header.Get("WWW-Authenticate") == "" {
return appwire.Errorf(appwire.CodeGitHubExpired, "GitHub access for this sign-in is missing or expired; reconnect GitHub")
}
err := appFactoryFailure(r)
if routeMissing(err) {
var wire *appwire.Error
if errors.As(factoryTooOld(), &wire) {
wire.Detail = &appwire.FactoryRefusal{Reason: appwire.FactoryReasonTooOld}
return wire
}
}
if r.status == http.StatusTooManyRequests {
var retry api.RepoIssuesRetry
var wire *appwire.Error
if json.Unmarshal(r.body, &retry) == nil && !retry.RetryAt.IsZero() && errors.As(err, &wire) {
wire.Detail = &retry
}
}
return err
}
Loading
Loading