Skip to content

Commit dfebdbe

Browse files
authored
Merge pull request #38 from neongreen/copilot/update-authentication-methods
Add transparent authentication support for private repositories in prrun
2 parents 2bf7445 + c02ab8f commit dfebdbe

File tree

3 files changed

+174
-2
lines changed

3 files changed

+174
-2
lines changed

prrun/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,36 @@ curl -fsSL https://raw.githubusercontent.com/neongreen/mono/main/install.sh | ba
6464
4. **Cache**: Downloads to `~/.cache/prrun/<release-tag>/<binary-name>`
6565
5. **Execute**: Runs the cached binary with your arguments
6666

67+
## Authentication
68+
69+
`prrun` transparently supports authentication for accessing private repositories. It tries multiple authentication methods in order:
70+
71+
1. **GITHUB_TOKEN** environment variable
72+
2. **MISE_GITHUB_TOKEN** environment variable
73+
3. **gh CLI** tool (if authenticated via `gh auth login`)
74+
75+
If any of these are available, `prrun` will use them to authenticate GitHub API requests. This allows you to:
76+
77+
- Access releases from private repositories
78+
- Avoid GitHub API rate limits
79+
- Download private release assets
80+
81+
### Examples
82+
83+
```bash
84+
# Using GITHUB_TOKEN
85+
export GITHUB_TOKEN="ghp_your_token_here"
86+
prrun https://github.com/private-org/private-repo/pull/123 tool
87+
88+
# Using MISE_GITHUB_TOKEN
89+
export MISE_GITHUB_TOKEN="ghp_your_token_here"
90+
prrun https://github.com/private-org/private-repo/pull/123 tool
91+
92+
# Using gh CLI (if already authenticated)
93+
gh auth login
94+
prrun https://github.com/private-org/private-repo/pull/123 tool
95+
```
96+
6797
## Caching
6898

6999
Binaries are cached at `~/.cache/prrun/` organized by release tag:

prrun/main.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,56 @@ func getCacheDir() (string, error) {
6666
return cacheDir, nil
6767
}
6868

69+
// getGitHubToken attempts to get a GitHub token from multiple sources
70+
// It tries in order: GITHUB_TOKEN, MISE_GITHUB_TOKEN, gh CLI tool
71+
func getGitHubToken() string {
72+
// Try GITHUB_TOKEN environment variable first
73+
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
74+
return token
75+
}
76+
77+
// Try MISE_GITHUB_TOKEN environment variable
78+
if token := os.Getenv("MISE_GITHUB_TOKEN"); token != "" {
79+
return token
80+
}
81+
82+
// Try gh CLI tool
83+
cmd := exec.Command("gh", "auth", "token")
84+
output, err := cmd.Output()
85+
if err == nil && len(output) > 0 {
86+
return strings.TrimSpace(string(output))
87+
}
88+
89+
// No token found
90+
return ""
91+
}
92+
93+
// createAuthenticatedRequest creates an HTTP request with authentication if available
94+
func createAuthenticatedRequest(method, url string) (*http.Request, error) {
95+
req, err := http.NewRequest(method, url, nil)
96+
if err != nil {
97+
return nil, err
98+
}
99+
100+
// Add authentication token if available
101+
if token := getGitHubToken(); token != "" {
102+
req.Header.Set("Authorization", "Bearer "+token)
103+
}
104+
105+
return req, nil
106+
}
107+
69108
// findPRRelease finds the latest release for a specific PR
70109
func findPRRelease(owner, repo string, prNum int, project string) (*GitHubRelease, error) {
71110
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", owner, repo)
72111

73-
resp, err := http.Get(apiURL)
112+
req, err := createAuthenticatedRequest("GET", apiURL)
113+
if err != nil {
114+
return nil, fmt.Errorf("failed to create request: %w", err)
115+
}
116+
117+
client := &http.Client{}
118+
resp, err := client.Do(req)
74119
if err != nil {
75120
return nil, fmt.Errorf("failed to fetch releases: %w", err)
76121
}
@@ -157,7 +202,13 @@ func getPlatformBinaryName(release *GitHubRelease, projectName string) (string,
157202
func downloadBinary(downloadURL, destPath string) error {
158203
fmt.Printf("Downloading binary from %s...\n", downloadURL)
159204

160-
resp, err := http.Get(downloadURL)
205+
req, err := createAuthenticatedRequest("GET", downloadURL)
206+
if err != nil {
207+
return fmt.Errorf("failed to create request: %w", err)
208+
}
209+
210+
client := &http.Client{}
211+
resp, err := client.Do(req)
161212
if err != nil {
162213
return fmt.Errorf("failed to download binary: %w", err)
163214
}

prrun/main_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,94 @@ func TestGetPlatformBinaryName(t *testing.T) {
120120
t.Logf("Platform binary: %s", binaryName)
121121
t.Logf("Download URL: %s", downloadURL)
122122
}
123+
124+
func TestGetGitHubToken(t *testing.T) {
125+
// Save original environment variables
126+
origGithubToken := os.Getenv("GITHUB_TOKEN")
127+
origMiseToken := os.Getenv("MISE_GITHUB_TOKEN")
128+
129+
// Clean up after test
130+
defer func() {
131+
if origGithubToken != "" {
132+
os.Setenv("GITHUB_TOKEN", origGithubToken)
133+
} else {
134+
os.Unsetenv("GITHUB_TOKEN")
135+
}
136+
if origMiseToken != "" {
137+
os.Setenv("MISE_GITHUB_TOKEN", origMiseToken)
138+
} else {
139+
os.Unsetenv("MISE_GITHUB_TOKEN")
140+
}
141+
}()
142+
143+
t.Run("GITHUB_TOKEN takes precedence", func(t *testing.T) {
144+
os.Setenv("GITHUB_TOKEN", "github_token")
145+
os.Setenv("MISE_GITHUB_TOKEN", "mise_token")
146+
147+
token := getGitHubToken()
148+
if token != "github_token" {
149+
t.Errorf("getGitHubToken() = %v, want %v", token, "github_token")
150+
}
151+
})
152+
153+
t.Run("MISE_GITHUB_TOKEN used when GITHUB_TOKEN not set", func(t *testing.T) {
154+
os.Unsetenv("GITHUB_TOKEN")
155+
os.Setenv("MISE_GITHUB_TOKEN", "mise_token")
156+
157+
token := getGitHubToken()
158+
if token != "mise_token" {
159+
t.Errorf("getGitHubToken() = %v, want %v", token, "mise_token")
160+
}
161+
})
162+
163+
t.Run("returns empty string when no tokens available", func(t *testing.T) {
164+
os.Unsetenv("GITHUB_TOKEN")
165+
os.Unsetenv("MISE_GITHUB_TOKEN")
166+
167+
token := getGitHubToken()
168+
// Token might be empty or come from gh CLI
169+
// We just verify the function doesn't crash
170+
t.Logf("getGitHubToken() = %q", token)
171+
})
172+
}
173+
174+
func TestCreateAuthenticatedRequest(t *testing.T) {
175+
// Save original environment variable
176+
origGithubToken := os.Getenv("GITHUB_TOKEN")
177+
defer func() {
178+
if origGithubToken != "" {
179+
os.Setenv("GITHUB_TOKEN", origGithubToken)
180+
} else {
181+
os.Unsetenv("GITHUB_TOKEN")
182+
}
183+
}()
184+
185+
t.Run("adds authorization header when token available", func(t *testing.T) {
186+
os.Setenv("GITHUB_TOKEN", "test_token_123")
187+
188+
req, err := createAuthenticatedRequest("GET", "https://api.github.com/repos/test/test")
189+
if err != nil {
190+
t.Fatalf("createAuthenticatedRequest() error = %v", err)
191+
}
192+
193+
authHeader := req.Header.Get("Authorization")
194+
expectedHeader := "Bearer test_token_123"
195+
if authHeader != expectedHeader {
196+
t.Errorf("Authorization header = %v, want %v", authHeader, expectedHeader)
197+
}
198+
})
199+
200+
t.Run("creates request without authorization when token not available", func(t *testing.T) {
201+
os.Unsetenv("GITHUB_TOKEN")
202+
os.Unsetenv("MISE_GITHUB_TOKEN")
203+
204+
req, err := createAuthenticatedRequest("GET", "https://api.github.com/repos/test/test")
205+
if err != nil {
206+
t.Fatalf("createAuthenticatedRequest() error = %v", err)
207+
}
208+
209+
authHeader := req.Header.Get("Authorization")
210+
// If gh CLI is available, there might be a token; otherwise it should be empty
211+
t.Logf("Authorization header = %q", authHeader)
212+
})
213+
}

0 commit comments

Comments
 (0)