Skip to content

Commit 9971e2d

Browse files
authored
Merge pull request #56 from tstromberg/gooselink
add next action to goose value in URL
2 parents b7785b0 + f25fdac commit 9971e2d

File tree

7 files changed

+74
-20
lines changed

7 files changed

+74
-20
lines changed

cmd/goose/github.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,10 +462,12 @@ func (app *App) fetchTurnDataSync(ctx context.Context, issues []*github.Issue, u
462462
needsReview := false
463463
isBlocked := false
464464
actionReason := ""
465+
actionKind := ""
465466
if action, exists := result.turnData.Analysis.NextAction[user]; exists {
466467
needsReview = true
467468
isBlocked = action.Critical // Only critical actions are blocking
468469
actionReason = action.Reason
470+
actionKind = string(action.Kind)
469471
// Only log fresh API calls
470472
if !result.wasFromCache {
471473
slog.Debug("[TURN] NextAction", "url", result.url, "reason", action.Reason, "kind", action.Kind, "critical", action.Critical)
@@ -479,6 +481,7 @@ func (app *App) fetchTurnDataSync(ctx context.Context, issues []*github.Issue, u
479481
(*outgoing)[i].NeedsReview = needsReview
480482
(*outgoing)[i].IsBlocked = isBlocked
481483
(*outgoing)[i].ActionReason = actionReason
484+
(*outgoing)[i].ActionKind = actionKind
482485
break
483486
}
484487
}
@@ -487,6 +490,7 @@ func (app *App) fetchTurnDataSync(ctx context.Context, issues []*github.Issue, u
487490
if (*incoming)[i].URL == result.url {
488491
(*incoming)[i].NeedsReview = needsReview
489492
(*incoming)[i].ActionReason = actionReason
493+
(*incoming)[i].ActionKind = actionKind
490494
break
491495
}
492496
}

cmd/goose/main.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type PR struct {
5757
URL string
5858
Repository string
5959
ActionReason string
60+
ActionKind string // The kind of action expected (review, merge, fix_tests, etc.)
6061
Number int
6162
IsBlocked bool
6263
NeedsReview bool
@@ -675,12 +676,12 @@ func (app *App) tryAutoOpenPR(ctx context.Context, pr PR, autoBrowserEnabled boo
675676
slog.Warn("Auto-open strict validation failed", "url", sanitizeForLog(pr.URL), "error", err)
676677
return
677678
}
678-
if err := openURL(ctx, pr.URL); err != nil {
679+
if err := openURL(ctx, pr.URL, pr.ActionKind); err != nil {
679680
slog.Error("[BROWSER] Failed to auto-open PR", "url", pr.URL, "error", err)
680681
} else {
681682
app.browserRateLimiter.RecordOpen(pr.URL)
682683
slog.Info("[BROWSER] Successfully opened PR in browser",
683-
"repo", pr.Repository, "number", pr.Number)
684+
"repo", pr.Repository, "number", pr.Number, "action", pr.ActionKind)
684685
}
685686
}
686687
}

cmd/goose/security.go

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -124,17 +124,17 @@ func validateURL(rawURL string) error {
124124
// validateGitHubPRURL performs strict validation for GitHub PR URLs used in auto-opening.
125125
// This ensures the URL follows the exact pattern: https://github.com/{owner}/{repo}/pull/{number}
126126
// with no additional path segments, fragments, or suspicious characters.
127-
// The URL may optionally have ?goose=1 parameter which we add for tracking.
127+
// The URL may optionally have ?goose=<action> parameter which we add for tracking.
128128
func validateGitHubPRURL(rawURL string) error {
129129
// First do basic URL validation
130130
if err := validateURL(rawURL); err != nil {
131131
return err
132132
}
133133

134-
// Strip the ?goose=1 parameter if present for pattern validation
134+
// Strip the ?goose parameter if present for pattern validation
135135
urlToValidate := rawURL
136-
if strings.HasSuffix(rawURL, "?goose=1") {
137-
urlToValidate = strings.TrimSuffix(rawURL, "?goose=1")
136+
if idx := strings.Index(rawURL, "?goose="); idx != -1 {
137+
urlToValidate = rawURL[:idx]
138138
}
139139

140140
// Check against strict GitHub PR URL pattern
@@ -149,19 +149,34 @@ func validateGitHubPRURL(rawURL string) error {
149149
}
150150

151151
// Reject URLs with URL encoding (could hide malicious content)
152-
// Exception: %3D which is = in URL encoding, only as part of ?goose=1
153-
if strings.Contains(rawURL, "%") && !strings.HasSuffix(rawURL, "?goose%3D1") {
154-
return errors.New("URL contains encoded characters")
152+
// Exception: %3D which is = in URL encoding, only as part of ?goose parameter
153+
if strings.Contains(rawURL, "%") {
154+
// Allow URL encoding only in the goose parameter value
155+
if idx := strings.Index(rawURL, "?goose="); idx != -1 {
156+
// Check if encoding is only in the goose parameter
157+
if strings.Contains(rawURL[:idx], "%") {
158+
return errors.New("URL contains encoded characters outside goose parameter")
159+
}
160+
} else {
161+
return errors.New("URL contains encoded characters")
162+
}
155163
}
156164

157165
// Reject URLs with fragments
158166
if strings.Contains(rawURL, "#") {
159167
return errors.New("URL contains fragments")
160168
}
161169

162-
// Allow only ?goose=1 query parameter, nothing else
163-
if strings.Contains(rawURL, "?") && !strings.HasSuffix(rawURL, "?goose=1") && !strings.HasSuffix(rawURL, "?goose%3D1") {
164-
return errors.New("URL contains unexpected query parameters")
170+
// Allow only ?goose=<value> query parameter, nothing else
171+
if strings.Contains(rawURL, "?") {
172+
// Check if it's the goose parameter
173+
if idx := strings.Index(rawURL, "?goose="); idx == -1 {
174+
return errors.New("URL contains unexpected query parameters")
175+
}
176+
// Ensure no additional parameters after goose
177+
if strings.Contains(rawURL[strings.Index(rawURL, "?goose=")+7:], "&") {
178+
return errors.New("URL contains additional query parameters")
179+
}
165180
}
166181

167182
// Reject URLs with double slashes (except after https:)

cmd/goose/security_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,26 @@ func TestValidateGitHubPRURL(t *testing.T) {
2121
url: "https://github.com/owner/repo/pull/123?goose=1",
2222
wantErr: false,
2323
},
24+
{
25+
name: "valid PR URL with goose=review param",
26+
url: "https://github.com/owner/repo/pull/123?goose=review",
27+
wantErr: false,
28+
},
29+
{
30+
name: "valid PR URL with goose=merge param",
31+
url: "https://github.com/owner/repo/pull/123?goose=merge",
32+
wantErr: false,
33+
},
34+
{
35+
name: "valid PR URL with goose=fix_tests param",
36+
url: "https://github.com/owner/repo/pull/123?goose=fix_tests",
37+
wantErr: false,
38+
},
39+
{
40+
name: "valid PR URL with goose=resolve_comments param",
41+
url: "https://github.com/owner/repo/pull/123?goose=resolve_comments",
42+
wantErr: false,
43+
},
2444
{
2545
name: "valid PR URL with hyphen in owner",
2646
url: "https://github.com/owner-name/repo/pull/1",
@@ -65,6 +85,11 @@ func TestValidateGitHubPRURL(t *testing.T) {
6585
},
6686

6787
// Invalid URLs - wrong format
88+
{
89+
name: "URL with multiple query parameters",
90+
url: "https://github.com/owner/repo/pull/123?goose=review&other=param",
91+
wantErr: true,
92+
},
6893
{
6994
name: "not a PR URL",
7095
url: "https://github.com/owner/repo",

cmd/goose/ui.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import (
2020
var _ *systray.MenuItem = nil
2121

2222
// openURL safely opens a URL in the default browser after validation.
23-
func openURL(ctx context.Context, rawURL string) error {
23+
// action parameter is optional and specifies the expected action type for tracking.
24+
func openURL(ctx context.Context, rawURL string, action string) error {
2425
// Parse and validate the URL
2526
u, err := url.Parse(rawURL)
2627
if err != nil {
@@ -45,10 +46,15 @@ func openURL(ctx context.Context, rawURL string) error {
4546
return errors.New("URLs with user info are not allowed")
4647
}
4748

48-
// Add goose=1 parameter to track source for GitHub and dash URLs
49+
// Add goose parameter to track source and action for GitHub and dash URLs
4950
if u.Host == "github.com" || u.Host == "www.github.com" || u.Host == "dash.ready-to-review.dev" {
5051
q := u.Query()
51-
q.Set("goose", "1")
52+
// Use action if provided, otherwise default to "1" for backward compatibility
53+
if action != "" {
54+
q.Set("goose", action)
55+
} else {
56+
q.Set("goose", "1")
57+
}
5258
u.RawQuery = q.Encode()
5359
rawURL = u.String()
5460
}
@@ -284,10 +290,11 @@ func (app *App) addPRSection(ctx context.Context, prs []PR, sectionTitle string,
284290
// Create PR menu item
285291
item := app.systrayInterface.AddMenuItem(title, tooltip)
286292

287-
// Capture URL to avoid loop variable capture bug
293+
// Capture URL and action to avoid loop variable capture bug
288294
prURL := sortedPRs[prIndex].URL
295+
prAction := sortedPRs[prIndex].ActionKind
289296
item.Click(func() {
290-
if err := openURL(ctx, prURL); err != nil {
297+
if err := openURL(ctx, prURL, prAction); err != nil {
291298
slog.Error("failed to open url", "error", err)
292299
}
293300
})
@@ -426,7 +433,7 @@ func (app *App) rebuildMenu(ctx context.Context) {
426433
// Add error details
427434
errorMsg := app.systrayInterface.AddMenuItem(authError, "Click to see setup instructions")
428435
errorMsg.Click(func() {
429-
if err := openURL(ctx, "https://cli.github.com/manual/gh_auth_login"); err != nil {
436+
if err := openURL(ctx, "https://cli.github.com/manual/gh_auth_login", ""); err != nil {
430437
slog.Error("failed to open setup instructions", "error", err)
431438
}
432439
})
@@ -536,7 +543,7 @@ func (app *App) rebuildMenu(ctx context.Context) {
536543
// Add Web Dashboard link
537544
dashboardItem := app.systrayInterface.AddMenuItem("Web Dashboard", "")
538545
dashboardItem.Click(func() {
539-
if err := openURL(ctx, "https://dash.ready-to-review.dev/"); err != nil {
546+
if err := openURL(ctx, "https://dash.ready-to-review.dev/", ""); err != nil {
540547
slog.Error("failed to open dashboard", "error", err)
541548
}
542549
})

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ require (
1313

1414
require (
1515
git.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect
16-
github.com/codeGROOVE-dev/prx v0.0.0-20250908203157-0711b3ec5471 // indirect
16+
github.com/codeGROOVE-dev/prx v0.0.0-20250923100916-d2b60be50274 // indirect
1717
github.com/esiqveland/notify v0.13.3 // indirect
1818
github.com/go-ole/go-ole v1.3.0 // indirect
1919
github.com/godbus/dbus/v5 v5.1.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ git.sr.ht/~jackmordaunt/go-toast v1.1.2 h1:/yrfI55LRt1M7H1vkaw+NaH1+L1CDxrqDltwm
22
git.sr.ht/~jackmordaunt/go-toast v1.1.2/go.mod h1:jA4OqHKTQ4AFBdwrSnwnskUIIS3HYzlJSgdzCKqfavo=
33
github.com/codeGROOVE-dev/prx v0.0.0-20250908203157-0711b3ec5471 h1:CbUa70O+iNC9rPk5aoZGs/RZbpmPyfNydv5ncKLdOvk=
44
github.com/codeGROOVE-dev/prx v0.0.0-20250908203157-0711b3ec5471/go.mod h1:7qLbi18baOyS8yO/6/64SBIqtyzSzLFdsDST15NPH3w=
5+
github.com/codeGROOVE-dev/prx v0.0.0-20250923100916-d2b60be50274 h1:9eLzQdOaQEn30279ai3YjNdJOM/efbcYanWC9juAJ+M=
6+
github.com/codeGROOVE-dev/prx v0.0.0-20250923100916-d2b60be50274/go.mod h1:7qLbi18baOyS8yO/6/64SBIqtyzSzLFdsDST15NPH3w=
57
github.com/codeGROOVE-dev/retry v1.2.0 h1:xYpYPX2PQZmdHwuiQAGGzsBm392xIMl4nfMEFApQnu8=
68
github.com/codeGROOVE-dev/retry v1.2.0/go.mod h1:8OgefgV1XP7lzX2PdKlCXILsYKuz6b4ZpHa/20iLi8E=
79
github.com/codeGROOVE-dev/turnclient v0.0.0-20250922145707-664c2dcdf5b8 h1:3088TLJGgxzjM/bR1gafKQ609NMkBNlZe1Fd5SnRrrY=

0 commit comments

Comments
 (0)