Add unauthenticated REST API fallback for remote workflow imports#4106
Conversation
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
| @@ -1,104 +1,4 @@ | |||
| { | |||
| "entries": { | |||
| "actions/ai-inference@v1": { | |||
There was a problem hiding this comment.
@copilot make sure to maintain existing pins, upsert resolved values
There was a problem hiding this comment.
Fixed in f47d79c. Restored all existing action pins from before my changes. The file was accidentally emptied during the build process but is now back to its original state with all 20+ action entries.
| steps: | ||
| - name: Checkout workflows | ||
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 | ||
| uses: |
There was a problem hiding this comment.
@copilot the value in "uses" can never be empty. This is a compilation error. Add a check.
There was a problem hiding this comment.
Fixed in 407e621. Added getRequiredActionPin helper that validates action pins exist before using them. This prevents empty uses: fields by returning an error during compilation if a required system action (like actions/checkout or actions/github-script) doesn't have a pin defined in actions-lock.json.
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
There was a problem hiding this comment.
Pull Request Overview
This PR adds unauthenticated REST API fallback functionality to enable fetching remote workflow files from public GitHub repositories when the gh CLI is not authenticated. The implementation detects authentication failures and transparently falls back to using curl with GitHub's public REST API.
Key Changes
- Added fallback code paths in
remote_fetch.gothat usecurland GitHub's public REST API whenghCLI authentication fails - Added
getRequiredActionPinhelper function to validate that system actions have defined pins before using them in compiled workflows - Added basic unit tests for JSON response parsing
Reviewed Changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| specs/UNAUTHENTICATED_FALLBACK.md | New documentation describing the fallback implementation, behavior, and limitations |
| pkg/parser/remote_fetch.go | Added unauthenticated fallback functions using curl and GitHub REST API with JSON parsing and base64 decoding |
| pkg/parser/remote_fetch_test.go | Added unit tests for JSON response parsing (commit SHA, file content, and error responses) |
| pkg/workflow/compiler_jobs.go | Added getRequiredActionPin helper to validate action pins exist before use in pre-activation and activation jobs |
| pkg/workflow/action_pins.go | Updated comments to clarify that empty string return is expected for custom action repos |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if strings.Contains(outputStr, "GH_TOKEN") || strings.Contains(outputStr, "authentication") || strings.Contains(outputStr, "not logged into") { | ||
| 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) | ||
| // Try fallback without authentication | ||
| remoteLog.Printf("gh CLI authentication failed, trying unauthenticated REST API fallback") | ||
| return resolveRefToSHAUnauthenticated(owner, repo, ref) |
There was a problem hiding this comment.
The fallback detection logic only checks for specific strings in error output. This approach may be fragile and could miss authentication failures with different error messages.
Issue: The string matching uses || (OR) logic, which means any single match triggers the fallback, but the condition checks three different patterns:
"GH_TOKEN"- could appear in various contexts"authentication"- generic word that might appear in non-auth errors"not logged into"- most specific pattern
Potential problem: If GitHub changes its error messages or if there's a different error that happens to contain one of these strings, the fallback could be triggered incorrectly.
Recommendation: Consider making the detection more specific by:
- Checking the error type or exit code first
- Using more specific error message patterns
- Adding logging to track when fallback is used vs. when authentication truly failed
See below for a potential fix:
if isAuthFailure(outputStr) {
// Try fallback without authentication
remoteLog.Printf("gh CLI authentication failed (exit code: %v, output: %q), trying unauthenticated REST API fallback", err, strings.TrimSpace(outputStr))
| func resolveRefToSHAUnauthenticated(owner, repo, ref string) (string, error) { | ||
| remoteLog.Printf("Attempting to resolve ref %s to SHA for %s/%s using unauthenticated API", ref, owner, repo) | ||
|
|
||
| // Use curl to make unauthenticated request | ||
| url := fmt.Sprintf("https://api.github.com/repos/%s/%s/commits/%s", owner, repo, ref) | ||
| cmd := exec.Command("curl", "-s", "-H", "Accept: application/vnd.github.v3+json", url) | ||
|
|
||
| output, err := cmd.CombinedOutput() | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to resolve ref using unauthenticated API: %w", err) | ||
| } | ||
|
|
||
| // Parse JSON response | ||
| var response struct { | ||
| SHA string `json:"sha"` | ||
| Message string `json:"message"` | ||
| } | ||
|
|
||
| if err := json.Unmarshal(output, &response); err != nil { | ||
| return "", fmt.Errorf("failed to parse JSON response: %w", err) | ||
| } | ||
|
|
||
| // Check for error message in response | ||
| if response.Message != "" { | ||
| if strings.Contains(response.Message, "Not Found") { | ||
| return "", fmt.Errorf("ref %s not found in %s/%s", ref, owner, repo) | ||
| } | ||
| if strings.Contains(response.Message, "rate limit") { | ||
| return "", fmt.Errorf("GitHub API rate limit exceeded") | ||
| } | ||
| return "", fmt.Errorf("GitHub API error: %s", response.Message) | ||
| } | ||
|
|
||
| // Validate it's a valid SHA (40 hex characters) | ||
| if len(response.SHA) != 40 || !isHexString(response.SHA) { | ||
| return "", fmt.Errorf("invalid SHA format returned: %s", response.SHA) | ||
| } | ||
|
|
||
| remoteLog.Printf("Successfully resolved ref %s to SHA %s using unauthenticated API", ref, response.SHA) | ||
| return response.SHA, nil | ||
| } |
There was a problem hiding this comment.
The error handling logic checks for error messages in the response after successful JSON parsing, but this doesn't account for HTTP-level errors.
Issue: The code only handles cases where:
- curl succeeds (exit code 0)
- JSON parsing succeeds
- The JSON contains a
messagefield
However, it doesn't handle:
- HTTP errors where curl fails (non-zero exit code) but doesn't return JSON
- Network failures
- Malformed responses that aren't valid JSON
- Cases where the API returns an error without a
messagefield
Example scenario: If curl can't reach api.github.com (network issue), the error at line 276 would just say "failed to resolve ref using unauthenticated API" without indicating it's a network problem.
Recommendation: Check the curl exit code and stderr separately before attempting JSON parsing. Add more specific error messages for different failure modes (network, HTTP errors, JSON parsing failures, API errors).
| // resolveRefToSHAUnauthenticated resolves a git ref to SHA using unauthenticated REST API | ||
| // This is a fallback for when gh CLI authentication is not available | ||
| func resolveRefToSHAUnauthenticated(owner, repo, ref string) (string, error) { | ||
| remoteLog.Printf("Attempting to resolve ref %s to SHA for %s/%s using unauthenticated API", ref, owner, repo) | ||
|
|
||
| // Use curl to make unauthenticated request | ||
| url := fmt.Sprintf("https://api.github.com/repos/%s/%s/commits/%s", owner, repo, ref) | ||
| cmd := exec.Command("curl", "-s", "-H", "Accept: application/vnd.github.v3+json", url) | ||
|
|
||
| output, err := cmd.CombinedOutput() | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to resolve ref using unauthenticated API: %w", err) | ||
| } | ||
|
|
||
| // Parse JSON response | ||
| var response struct { | ||
| SHA string `json:"sha"` | ||
| Message string `json:"message"` | ||
| } | ||
|
|
||
| if err := json.Unmarshal(output, &response); err != nil { | ||
| return "", fmt.Errorf("failed to parse JSON response: %w", err) | ||
| } | ||
|
|
||
| // Check for error message in response | ||
| if response.Message != "" { | ||
| if strings.Contains(response.Message, "Not Found") { | ||
| return "", fmt.Errorf("ref %s not found in %s/%s", ref, owner, repo) | ||
| } | ||
| if strings.Contains(response.Message, "rate limit") { | ||
| return "", fmt.Errorf("GitHub API rate limit exceeded") | ||
| } | ||
| return "", fmt.Errorf("GitHub API error: %s", response.Message) | ||
| } | ||
|
|
||
| // Validate it's a valid SHA (40 hex characters) | ||
| if len(response.SHA) != 40 || !isHexString(response.SHA) { | ||
| return "", fmt.Errorf("invalid SHA format returned: %s", response.SHA) | ||
| } | ||
|
|
||
| remoteLog.Printf("Successfully resolved ref %s to SHA %s using unauthenticated API", ref, response.SHA) | ||
| return response.SHA, nil | ||
| } |
There was a problem hiding this comment.
[nitpick] Rate limit information is mentioned in documentation but the code doesn't parse rate limit headers from GitHub's API response.
Issue: The GitHub API returns rate limit information in response headers (X-RateLimit-Remaining, X-RateLimit-Reset), but curl is not configured to capture these headers. This means:
- Users won't know how many API calls they have left
- The code can't proactively warn users before hitting the limit
- The 60 requests/hour limit mentioned in docs is not enforced or tracked
Impact: Users may hit rate limits unexpectedly and get cryptic error messages.
Recommendation:
- Use
curl -ito capture headers along with response body - Parse and log rate limit headers
- Consider warning users when remaining requests are low
- This addresses a "Future Improvement" mentioned in the documentation (line 87)
|
|
||
| ### Unit Tests | ||
| ```bash | ||
| cd /home/runner/work/gh-aw/gh-aw |
There was a problem hiding this comment.
The test command path in the documentation is hardcoded to a specific CI environment path.
Issue: Line 61 shows:
cd /home/runner/work/gh-aw/gh-awThis path is specific to GitHub Actions runners and won't work for:
- Local development machines
- Other CI systems
- Users following the documentation
Recommendation: Use a relative path or make it clear this is just an example:
# From the repository root
go test ./pkg/parser -run TestJSONParsing -v| cd /home/runner/work/gh-aw/gh-aw | |
| # From the repository root |
| // getRequiredActionPin gets an action pin and returns an error if it's not found | ||
| // This ensures that required system actions always have pins defined | ||
| func getRequiredActionPin(actionRepo string) (string, error) { | ||
| pin := GetActionPin(actionRepo) | ||
| if pin == "" { | ||
| return "", fmt.Errorf("action pin not found for %s - this action must be added to .github/aw/actions-lock.json", actionRepo) | ||
| } | ||
| return pin, nil | ||
| } | ||
|
|
There was a problem hiding this comment.
The new getRequiredActionPin function only validates system actions (actions/checkout and actions/github-script), but this pattern is inconsistent with many other places in the codebase where GetActionPin is called directly without validation.
Issue: Many other critical actions lack validation:
actions/cache(cache.go, lines 242, 349)actions/upload-artifact(compiler_yaml.go, artifacts.go, etc.)actions/download-artifact(artifacts.go, line 38)actions/checkout(compiler_yaml.go, line 174)actions/github-script(compiler_yaml.go, multiple locations)
Why this matters: If action pins are missing, these would produce empty uses: fields, causing compilation errors.
Recommendation: Either:
- Apply
getRequiredActionPinconsistently to all system actions throughout the codebase, OR - Remove this validation since
GetActionPinis designed to return empty string for missing pins (which is the expected behavior for custom action repos per runtime_setup.go:357-367)
Given the codebase pattern where GetActionPin is designed to gracefully return empty string for custom actions, the current selective validation appears to be an over-engineered solution.
| // getRequiredActionPin gets an action pin and returns an error if it's not found | |
| // This ensures that required system actions always have pins defined | |
| func getRequiredActionPin(actionRepo string) (string, error) { | |
| pin := GetActionPin(actionRepo) | |
| if pin == "" { | |
| return "", fmt.Errorf("action pin not found for %s - this action must be added to .github/aw/actions-lock.json", actionRepo) | |
| } | |
| return pin, nil | |
| } |
| // Decode base64 content | ||
| decodeCmd := exec.Command("base64", "-d") | ||
| decodeCmd.Stdin = strings.NewReader(contentBase64) | ||
| content, err := decodeCmd.Output() | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to decode base64 content: %w", err) | ||
| } |
There was a problem hiding this comment.
The base64 decoding uses an external base64 command which may not be available in all environments where this code runs.
Issue: The code executes exec.Command("base64", "-d") which assumes:
- The
base64command-line utility is installed - It supports the
-dflag (GNU coreutils vs BSD variants) - The command is in the system PATH
Impact: The fallback code path could fail on systems that don't have the base64 utility, even though Go's standard library provides encoding/base64 which would work everywhere.
Recommendation: Use Go's built-in encoding/base64.StdEncoding.DecodeString() instead of shelling out to an external command. This would:
- Be more portable
- Remove a dependency on external tools
- Be faster (no process spawning)
- Be more secure (no shell command injection risks)
Example:
import "encoding/base64"
decoded, err := base64.StdEncoding.DecodeString(contentBase64)
if err != nil {
return nil, fmt.Errorf("failed to decode base64 content: %w", err)
}
return decoded, nil|
|
||
| // Use curl to make unauthenticated request | ||
| url := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s?ref=%s", owner, repo, path, ref) | ||
| cmd := exec.Command("curl", "-s", "-H", "Accept: application/vnd.github.v3+json", url) |
There was a problem hiding this comment.
The curl command uses -s (silent) flag but doesn't use -f (fail) flag, which can lead to confusing behavior.
Issue: When curl encounters HTTP errors (404, 500, etc.) without the -f flag:
- It still exits with code 0 (success)
- It returns the HTML error page in the response body
- The code tries to parse this HTML as JSON, leading to confusing error messages
Example scenario:
- User requests a non-existent file
- GitHub returns 404 with HTML error page
- Code tries to parse HTML as JSON → "failed to parse JSON response"
- User sees JSON parsing error instead of "file not found"
Recommendation: Add the -f flag to curl commands to make them fail on HTTP errors:
cmd := exec.Command("curl", "-s", "-f", "-H", "Accept: application/vnd.github.v3+json", url)This way, HTTP errors will cause curl to exit with non-zero code, and you can handle them separately before attempting JSON parsing.
| cmd := exec.Command("curl", "-s", "-H", "Accept: application/vnd.github.v3+json", url) | |
| cmd := exec.Command("curl", "-s", "-f", "-H", "Accept: application/vnd.github.v3+json", url) |
|
@copilot lint |
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Objective
Add fallback code path for fetching remote workflow files without GitHub token
Plan
resolveRefToSHAfunctiondownloadFileFromGitHubfunction.github/aw/imports/directoryImplementation Summary
Changes Made
Modified
resolveRefToSHAfunction - Added fallback toresolveRefToSHAUnauthenticatedwhenghCLI authentication failsModified
downloadFileFromGitHubfunction - Added fallback todownloadFileFromGitHubUnauthenticatedwhenghCLI authentication failsAdded
resolveRefToSHAUnauthenticatedfunction:curlwith GitHub's public REST APIGET /repos/{owner}/{repo}/commits/{ref}encoding/json-fflag to curl for better HTTP error handlingAdded
downloadFileFromGitHubUnauthenticatedfunction:curlwith GitHub's public REST APIGET /repos/{owner}/{repo}/contents/{path}?ref={ref}-fflag to curl for better HTTP error handlingbase64command for better portabilityAdded unit tests -
TestJSONParsingvalidates JSON response parsingRestored action pins - Fixed
.github/aw/actions-lock.jsonwhich was accidentally emptied during build processAdded validation for empty uses field - Created
getRequiredActionPinhelper function incompiler_jobs.gothat validates action pins exist before using them in compiled workflows. This prevents compilation errors whereuses:would be empty for system actions likeactions/checkoutandactions/github-script.Moved documentation to specs folder - Relocated
UNAUTHENTICATED_FALLBACK.mdtospecs/directory for better organizationCode review improvements:
base64 -dcommand with Go's nativeencoding/base64for better portability-fflag to curl commands to fail on HTTP errors (404, 500, etc.) instead of trying to parse error HTML as JSONHow It Works
With GH_TOKEN (preferred):
ghCLI commands (existing behavior)Without GH_TOKEN (fallback):
ghCLIcurlImport Cache Integration
The fallback works seamlessly with the existing import cache:
.github/aw/imports/{owner}/{repo}/{sha}/Action Pin Validation
The compiler now validates that all required system actions (like
actions/checkoutandactions/github-script) have pins defined inactions-lock.json. This prevents compilation errors where theuses:field would be empty. Custom action repos (specified viaaction-repoin workflow frontmatter) can still be used without pins.Testing
✅ Unit tests pass (TestJSONParsing)
✅ Action pins tests pass (TestActionPins*)
✅ All unit tests pass
✅ Build succeeds
✅ Linting passes
Documentation
See
specs/UNAUTHENTICATED_FALLBACK.mdfor detailed documentationOriginal prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.