Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e0be67f
feat(ui): header and row rendering
Skeyelab Oct 7, 2026
1f72614
fix(ui): hint and header rules
Skeyelab Oct 7, 2026
36b45fd
fix(ui): clean untrusted text
Skeyelab Oct 7, 2026
1a5f1f4
feat(ui): review decisions
Skeyelab Oct 7, 2026
0fa682a
feat(ui): ticket list screen
Skeyelab Oct 7, 2026
dc02605
chore(deps): match jira-green versions
Skeyelab Oct 7, 2026
563989c
fix(ui): o opens the row's flag target
Skeyelab Oct 7, 2026
42fd14e
feat(ui): detail screen
Skeyelab Oct 7, 2026
fd8ae3f
feat(ui): scroll the detail screen
Skeyelab Oct 7, 2026
8b611e4
fix(ui): mark a rolled-back env's commit as the live one
Skeyelab Oct 7, 2026
2f710eb
feat(ui): refresh on detail
Skeyelab Oct 7, 2026
25b0cbe
fix(ui): list review fixes
Skeyelab Oct 7, 2026
3e8892c
feat: wire config, adapters, resolver and UI
Skeyelab Oct 7, 2026
1825012
fix(ui): detail scroll follows the selection only while it was visible
Skeyelab Oct 7, 2026
5c9df09
fix(ui): open the parsed URL, and skip non-web check URLs
Skeyelab Oct 7, 2026
9ac484e
refactor(ui): compute the detail's selected line directly
Skeyelab Oct 7, 2026
132de7a
feat(ui): show the pipeline on detail env lines when account and stag…
Skeyelab Oct 7, 2026
72ab13b
fix(ui): refuse URLs with bytes a browser launcher could mishandle
Skeyelab Oct 7, 2026
59b9428
feat(ui): say "nothing to open" when o finds no page
Skeyelab Oct 7, 2026
787928a
fix(resolver): log a poll warning when it appears, not on every poll
Skeyelab Oct 7, 2026
8f08ac3
docs(ui): showSelection comment matches short screens
Skeyelab Oct 7, 2026
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
23 changes: 23 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ require (
github.com/aws/aws-sdk-go-v2/service/codepipeline v1.40.1
github.com/aws/aws-sdk-go-v2/service/ecs v1.79.1
github.com/aws/smithy-go v1.25.1
github.com/charmbracelet/bubbles v0.21.1
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/x/ansi v0.11.5
github.com/muesli/termenv v0.16.0
)

require (
Expand All @@ -24,4 +29,22 @@ require (
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.23.0 // indirect
)
50 changes: 50 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,53 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/Xv
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.21.1 h1:nj0decPiixaZeL9diI4uzzQTkkz1kYY8+jgzCZXSmW0=
github.com/charmbracelet/bubbles v0.21.1/go.mod h1:HHvIYRCpbkCJw2yo0vNX1O5loCwSr9/mWS8GYSg50Sk=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.11.5 h1:NBWeBpj/lJPE3Q5l+Lusa4+mH6v7487OP8K0r1IhRg4=
github.com/charmbracelet/x/ansi v0.11.5/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
8 changes: 7 additions & 1 deletion internal/resolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ type Resolver struct {
recheck map[config.PipelineKey]bool // cached sources a refresh wants asked again
pipelines map[config.PipelineKey]pipelineData
health map[string]healthData // by Env.ID()
warned map[string]bool // the last poll's warnings, so only new ones are logged
}

// New returns a Resolver over ad. now is the clock (nil is time.Now); log
Expand Down Expand Up @@ -330,9 +331,14 @@ func (r *Resolver) Poll(ctx context.Context) Snapshot {
r.record("aws "+a, ar.err, now)
}
snap := r.build(ctx, now)
current := make(map[string]bool, len(snap.Warnings))
for _, w := range snap.Warnings {
r.log.Warn("poll warning", "msg", w)
current[w] = true
if !r.warned[w] { // a warning that persists is logged once, not every poll
r.log.Warn("poll warning", "msg", w)
}
}
r.warned = current
return snap
}

Expand Down
39 changes: 36 additions & 3 deletions internal/resolver/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ func TestEnvReposFromSourcesOnlyWhenConfigHasNone(t *testing.T) {
}
}

func TestWarningsDedupedAndLoggedOncePerPoll(t *testing.T) {
func TestWarningsAreDedupedWithinAPoll(t *testing.T) {
var buf safeBuffer
log := slog.New(slog.NewTextHandler(&buf, nil))
h := newHarness(t, log, testEnv, apiEnv)
Expand All @@ -539,9 +539,10 @@ func TestWarningsDedupedAndLoggedOncePerPoll(t *testing.T) {
t.Errorf("%q logged %d times in one poll, want 1\n%s", w, n, buf.String())
}
}
// Still there next poll: not logged again (TestPollWarningIsLoggedOnceWhileItPersists).
h.r.Poll(context.Background())
if n := strings.Count(buf.String(), "github: w2"); n != 2 {
t.Errorf("w2 logged %d times over two polls, want 2", n)
if n := strings.Count(buf.String(), "github: w2"); n != 1 {
t.Errorf("w2 logged %d times over two polls, want 1", n)
}
}

Expand Down Expand Up @@ -634,3 +635,35 @@ func TestSSOFlagOnlyForAWS(t *testing.T) {
t.Errorf("jira status = %+v, want SSO false (SSO is an AWS state)", j)
}
}

func TestPollWarningIsLoggedOnceWhileItPersists(t *testing.T) {
var buf safeBuffer
h := newHarness(t, slog.New(slog.NewTextHandler(&buf, nil)))
h.withShippedChain()
h.tracker.set(func(f *fakeTracker) { f.byKey = map[string]model.Ticket{"ABC-9": ticket("ABC-9")} })
h.host.set(func(f *fakeHost) { f.prs[repo] = append(f.prs[repo], openPR(9, "ABC-9: x")) })
fail := func(err error) { h.tracker.set(func(f *fakeTracker) { f.byKeyErr = err }) }
count := func() int { return strings.Count(buf.String(), "poll warning") }
h.r.Poll(context.Background())

fail(errors.New("jira: HTTP 500"))
h.r.Poll(context.Background())
first := count()
if first == 0 {
t.Fatalf("the warning is logged when it first appears: %q", buf.String())
}
h.r.Poll(context.Background())
h.r.Poll(context.Background())
if got := count(); got != first {
t.Errorf("a persistent warning is logged once: %d lines, want %d", got, first)
}

// Once it clears and returns it is news again.
fail(nil)
h.r.Poll(context.Background())
fail(errors.New("jira: HTTP 500"))
h.r.Poll(context.Background())
if got := count(); got != 2*first {
t.Errorf("a warning that returns is logged again: %d lines, want %d", got, 2*first)
}
}
Loading
Loading