Skip to content

Commit dc8141b

Browse files
Copilotpelikhan
andcommitted
Add fallback for unauthenticated REST API access
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent 0dce1b3 commit dc8141b

File tree

3 files changed

+185
-102
lines changed

3 files changed

+185
-102
lines changed

pkg/parser/remote_fetch.go

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package parser
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"os"
67
"os/exec"
@@ -197,7 +198,9 @@ func resolveRefToSHA(owner, repo, ref string) (string, error) {
197198
if err != nil {
198199
outputStr := string(output)
199200
if strings.Contains(outputStr, "GH_TOKEN") || strings.Contains(outputStr, "authentication") || strings.Contains(outputStr, "not logged into") {
200-
return "", fmt.Errorf("failed to resolve ref to SHA: GitHub authentication required. Please run 'gh auth login' or set GH_TOKEN/GITHUB_TOKEN environment variable: %w", err)
201+
// Try fallback without authentication
202+
remoteLog.Printf("gh CLI authentication failed, trying unauthenticated REST API fallback")
203+
return resolveRefToSHAUnauthenticated(owner, repo, ref)
201204
}
202205
return "", fmt.Errorf("failed to resolve ref %s to SHA for %s/%s: %s: %w", ref, owner, repo, strings.TrimSpace(outputStr), err)
203206
}
@@ -236,7 +239,9 @@ func downloadFileFromGitHub(owner, repo, path, ref string) ([]byte, error) {
236239
// Check if this is an authentication error
237240
stderrStr := stderr.String()
238241
if strings.Contains(stderrStr, "GH_TOKEN") || strings.Contains(stderrStr, "authentication") || strings.Contains(stderrStr, "not logged into") {
239-
return nil, fmt.Errorf("failed to fetch file content: GitHub authentication required. Please run 'gh auth login' or set GH_TOKEN/GITHUB_TOKEN environment variable: %w", err)
242+
// Try fallback without authentication
243+
remoteLog.Printf("gh CLI authentication failed, trying unauthenticated REST API fallback")
244+
return downloadFileFromGitHubUnauthenticated(owner, repo, path, ref)
240245
}
241246
return nil, fmt.Errorf("failed to fetch file content from %s/%s/%s@%s: %s: %w", owner, repo, path, ref, strings.TrimSpace(stderrStr), err)
242247
}
@@ -256,3 +261,109 @@ func downloadFileFromGitHub(owner, repo, path, ref string) ([]byte, error) {
256261

257262
return content, nil
258263
}
264+
265+
// resolveRefToSHAUnauthenticated resolves a git ref to SHA using unauthenticated REST API
266+
// This is a fallback for when gh CLI authentication is not available
267+
func resolveRefToSHAUnauthenticated(owner, repo, ref string) (string, error) {
268+
remoteLog.Printf("Attempting to resolve ref %s to SHA for %s/%s using unauthenticated API", ref, owner, repo)
269+
270+
// Use curl to make unauthenticated request
271+
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/commits/%s", owner, repo, ref)
272+
cmd := exec.Command("curl", "-s", "-H", "Accept: application/vnd.github.v3+json", url)
273+
274+
output, err := cmd.CombinedOutput()
275+
if err != nil {
276+
return "", fmt.Errorf("failed to resolve ref using unauthenticated API: %w", err)
277+
}
278+
279+
// Parse JSON response
280+
var response struct {
281+
SHA string `json:"sha"`
282+
Message string `json:"message"`
283+
}
284+
285+
if err := json.Unmarshal(output, &response); err != nil {
286+
return "", fmt.Errorf("failed to parse JSON response: %w", err)
287+
}
288+
289+
// Check for error message in response
290+
if response.Message != "" {
291+
if strings.Contains(response.Message, "Not Found") {
292+
return "", fmt.Errorf("ref %s not found in %s/%s", ref, owner, repo)
293+
}
294+
if strings.Contains(response.Message, "rate limit") {
295+
return "", fmt.Errorf("GitHub API rate limit exceeded")
296+
}
297+
return "", fmt.Errorf("GitHub API error: %s", response.Message)
298+
}
299+
300+
// Validate it's a valid SHA (40 hex characters)
301+
if len(response.SHA) != 40 || !isHexString(response.SHA) {
302+
return "", fmt.Errorf("invalid SHA format returned: %s", response.SHA)
303+
}
304+
305+
remoteLog.Printf("Successfully resolved ref %s to SHA %s using unauthenticated API", ref, response.SHA)
306+
return response.SHA, nil
307+
}
308+
309+
// downloadFileFromGitHubUnauthenticated downloads a file using unauthenticated REST API
310+
// This is a fallback for when gh CLI authentication is not available
311+
func downloadFileFromGitHubUnauthenticated(owner, repo, path, ref string) ([]byte, error) {
312+
remoteLog.Printf("Attempting to download %s/%s/%s@%s using unauthenticated API", owner, repo, path, ref)
313+
314+
// Use curl to make unauthenticated request
315+
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s?ref=%s", owner, repo, path, ref)
316+
cmd := exec.Command("curl", "-s", "-H", "Accept: application/vnd.github.v3+json", url)
317+
318+
output, err := cmd.CombinedOutput()
319+
if err != nil {
320+
return nil, fmt.Errorf("failed to fetch file using unauthenticated API: %w", err)
321+
}
322+
323+
// Parse JSON response
324+
var response struct {
325+
Content string `json:"content"`
326+
Encoding string `json:"encoding"`
327+
Message string `json:"message"`
328+
}
329+
330+
if err := json.Unmarshal(output, &response); err != nil {
331+
return nil, fmt.Errorf("failed to parse JSON response: %w", err)
332+
}
333+
334+
// Check for error message in response
335+
if response.Message != "" {
336+
if strings.Contains(response.Message, "Not Found") {
337+
return nil, fmt.Errorf("file %s not found in %s/%s@%s", path, owner, repo, ref)
338+
}
339+
if strings.Contains(response.Message, "rate limit") {
340+
return nil, fmt.Errorf("GitHub API rate limit exceeded")
341+
}
342+
return nil, fmt.Errorf("GitHub API error: %s", response.Message)
343+
}
344+
345+
// Verify encoding
346+
if response.Encoding != "base64" {
347+
return nil, fmt.Errorf("unexpected encoding: %s (expected base64)", response.Encoding)
348+
}
349+
350+
// Remove newlines and whitespace from base64 content
351+
contentBase64 := strings.ReplaceAll(response.Content, "\n", "")
352+
contentBase64 = strings.ReplaceAll(contentBase64, " ", "")
353+
contentBase64 = strings.TrimSpace(contentBase64)
354+
355+
if contentBase64 == "" {
356+
return nil, fmt.Errorf("empty content returned from GitHub API")
357+
}
358+
359+
// Decode base64 content
360+
decodeCmd := exec.Command("base64", "-d")
361+
decodeCmd.Stdin = strings.NewReader(contentBase64)
362+
content, err := decodeCmd.Output()
363+
if err != nil {
364+
return nil, fmt.Errorf("failed to decode base64 content: %w", err)
365+
}
366+
367+
remoteLog.Printf("Successfully downloaded %s/%s/%s@%s using unauthenticated API (%d bytes)", owner, repo, path, ref, len(content))
368+
return content, nil
369+
}

pkg/parser/remote_fetch_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package parser
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
)
7+
8+
func TestJSONParsing(t *testing.T) {
9+
// Test SHA resolution JSON parsing
10+
t.Run("parse SHA from commit response", func(t *testing.T) {
11+
response := `{"sha":"1e366aa4518cf83d25defd84e454b9a41e87cf7c","node_id":"C_kwDOKr1234","commit":{"message":"test"}}`
12+
13+
var parsed struct {
14+
SHA string `json:"sha"`
15+
Message string `json:"message"`
16+
}
17+
18+
if err := json.Unmarshal([]byte(response), &parsed); err != nil {
19+
t.Fatalf("Failed to parse JSON: %v", err)
20+
}
21+
22+
if parsed.SHA != "1e366aa4518cf83d25defd84e454b9a41e87cf7c" {
23+
t.Errorf("Expected SHA 1e366aa4518cf83d25defd84e454b9a41e87cf7c, got %s", parsed.SHA)
24+
}
25+
})
26+
27+
// Test file content JSON parsing
28+
t.Run("parse content from file response", func(t *testing.T) {
29+
response := `{"content":"IyBUZXN0IGNvbnRlbnQ=\n","encoding":"base64","name":"test.md"}`
30+
31+
var parsed struct {
32+
Content string `json:"content"`
33+
Encoding string `json:"encoding"`
34+
Message string `json:"message"`
35+
}
36+
37+
if err := json.Unmarshal([]byte(response), &parsed); err != nil {
38+
t.Fatalf("Failed to parse JSON: %v", err)
39+
}
40+
41+
if parsed.Encoding != "base64" {
42+
t.Errorf("Expected encoding base64, got %s", parsed.Encoding)
43+
}
44+
45+
if parsed.Content == "" {
46+
t.Error("Expected non-empty content")
47+
}
48+
})
49+
50+
// Test error response parsing
51+
t.Run("parse error response", func(t *testing.T) {
52+
response := `{"message":"Not Found","documentation_url":"https://docs.github.com/rest"}`
53+
54+
var parsed struct {
55+
SHA string `json:"sha"`
56+
Message string `json:"message"`
57+
}
58+
59+
if err := json.Unmarshal([]byte(response), &parsed); err != nil {
60+
t.Fatalf("Failed to parse JSON: %v", err)
61+
}
62+
63+
if parsed.Message != "Not Found" {
64+
t.Errorf("Expected message 'Not Found', got %s", parsed.Message)
65+
}
66+
67+
if parsed.SHA != "" {
68+
t.Errorf("Expected empty SHA for error response, got %s", parsed.SHA)
69+
}
70+
})
71+
}
72+

pkg/workflow/data/action_pins.json

Lines changed: 0 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,4 @@
11
{
22
"entries": {
3-
"actions/ai-inference@v1": {
4-
"repo": "actions/ai-inference",
5-
"version": "v1",
6-
"sha": "b81b2afb8390ee6839b494a404766bef6493c7d9"
7-
},
8-
"actions/cache@v4": {
9-
"repo": "actions/cache",
10-
"version": "v4",
11-
"sha": "0057852bfaa89a56745cba8c7296529d2fc39830"
12-
},
13-
"actions/checkout@v5": {
14-
"repo": "actions/checkout",
15-
"version": "v5",
16-
"sha": "08c6903cd8c0fde910a37f88322edcfb5dd907a8"
17-
},
18-
"actions/download-artifact@v6": {
19-
"repo": "actions/download-artifact",
20-
"version": "v6",
21-
"sha": "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53"
22-
},
23-
"actions/github-script@v8": {
24-
"repo": "actions/github-script",
25-
"version": "v8",
26-
"sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd"
27-
},
28-
"actions/setup-dotnet@v4": {
29-
"repo": "actions/setup-dotnet",
30-
"version": "v4",
31-
"sha": "67a3573c9a986a3f9c594539f4ab511d57bb3ce9"
32-
},
33-
"actions/setup-go@v5": {
34-
"repo": "actions/setup-go",
35-
"version": "v5",
36-
"sha": "d35c59abb061a4a6fb18e82ac0862c26744d6ab5"
37-
},
38-
"actions/setup-java@v4": {
39-
"repo": "actions/setup-java",
40-
"version": "v4",
41-
"sha": "c5195efecf7bdfc987ee8bae7a71cb8b11521c00"
42-
},
43-
"actions/setup-node@v6": {
44-
"repo": "actions/setup-node",
45-
"version": "v6",
46-
"sha": "2028fbc5c25fe9cf00d9f06a71cc4710d4507903"
47-
},
48-
"actions/setup-python@v5": {
49-
"repo": "actions/setup-python",
50-
"version": "v5",
51-
"sha": "a26af69be951a213d495a4c3e4e4022e16d87065"
52-
},
53-
"actions/upload-artifact@v4": {
54-
"repo": "actions/upload-artifact",
55-
"version": "v4",
56-
"sha": "ea165f8d65b6e75b540449e92b4886f43607fa02"
57-
},
58-
"actions/upload-artifact@v5": {
59-
"repo": "actions/upload-artifact",
60-
"version": "v5",
61-
"sha": "330a01c490aca151604b8cf639adc76d48f6c5d4"
62-
},
63-
"astral-sh/setup-uv@v5": {
64-
"repo": "astral-sh/setup-uv",
65-
"version": "v5",
66-
"sha": "e58605a9b6da7c637471fab8847a5e5a6b8df081"
67-
},
68-
"denoland/setup-deno@v2": {
69-
"repo": "denoland/setup-deno",
70-
"version": "v2",
71-
"sha": "e95548e56dfa95d4e1a28d6f422fafe75c4c26fb"
72-
},
73-
"erlef/setup-beam@v1": {
74-
"repo": "erlef/setup-beam",
75-
"version": "v1",
76-
"sha": "3559ac3b631a9560f28817e8e7fdde1638664336"
77-
},
78-
"github/codeql-action/upload-sarif@v3": {
79-
"repo": "github/codeql-action/upload-sarif",
80-
"version": "v3",
81-
"sha": "fb2a9d4376843ba94460a73c39ca9a98b33a12ac"
82-
},
83-
"haskell-actions/setup@v2": {
84-
"repo": "haskell-actions/setup",
85-
"version": "v2",
86-
"sha": "d5d0f498b388e1a0eab1cd150202f664c5738e35"
87-
},
88-
"oven-sh/setup-bun@v2": {
89-
"repo": "oven-sh/setup-bun",
90-
"version": "v2",
91-
"sha": "735343b667d3e6f658f44d0eca948eb6282f2b76"
92-
},
93-
"ruby/setup-ruby@v1": {
94-
"repo": "ruby/setup-ruby",
95-
"version": "v1",
96-
"sha": "e5517072e87f198d9533967ae13d97c11b604005"
97-
},
98-
"super-linter/super-linter@v8.2.1": {
99-
"repo": "super-linter/super-linter",
100-
"version": "v8.2.1",
101-
"sha": "2bdd90ed3262e023ac84bf8fe35dc480721fc1f2"
102-
}
1033
}
1044
}

0 commit comments

Comments
 (0)