-
Notifications
You must be signed in to change notification settings - Fork 343
fix: preserve local relative imports during gh aw update #23809
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -58,8 +58,10 @@ func resolveImportPath(importPath string, workflowPath string) string { | |||||||||||||||
| // processImportsWithWorkflowSpec processes imports field in frontmatter and replaces local file references | ||||||||||||||||
| // with workflowspec format (owner/repo/path@sha) for all imports found. | ||||||||||||||||
| // Handles both array form and object form (with 'aw' subfield) of the imports field. | ||||||||||||||||
| func processImportsWithWorkflowSpec(content string, workflow *WorkflowSpec, commitSHA string, verbose bool) (string, error) { | ||||||||||||||||
| importsLog.Printf("Processing imports with workflowspec: repo=%s, sha=%s", workflow.RepoSlug, commitSHA) | ||||||||||||||||
| // If localWorkflowDir is non-empty, any import path whose file exists under that directory is | ||||||||||||||||
| // left as a local relative path rather than being rewritten to a cross-repo reference. | ||||||||||||||||
| func processImportsWithWorkflowSpec(content string, workflow *WorkflowSpec, commitSHA string, localWorkflowDir string, verbose bool) (string, error) { | ||||||||||||||||
| importsLog.Printf("Processing imports with workflowspec: repo=%s, sha=%s, localWorkflowDir=%s", workflow.RepoSlug, commitSHA, localWorkflowDir) | ||||||||||||||||
| if verbose { | ||||||||||||||||
| fmt.Fprintln(os.Stderr, console.FormatVerboseMessage("Processing imports field to replace with workflowspec")) | ||||||||||||||||
| } | ||||||||||||||||
|
|
@@ -79,6 +81,10 @@ func processImportsWithWorkflowSpec(content string, workflow *WorkflowSpec, comm | |||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // processImportPaths converts a list of raw import paths to workflowspec format. | ||||||||||||||||
| // Paths that already use the workflowspec format (contain "@") are left unchanged. | ||||||||||||||||
| // When localWorkflowDir is set, relative paths whose files exist locally are also | ||||||||||||||||
| // preserved as-is so that consumers who have copied shared files into their own repo | ||||||||||||||||
| // are not forced onto cross-repo references after every `gh aw update`. | ||||||||||||||||
| processImportPaths := func(imports []string) []string { | ||||||||||||||||
| processed := make([]string, 0, len(imports)) | ||||||||||||||||
| for _, importPath := range imports { | ||||||||||||||||
|
|
@@ -87,6 +93,16 @@ func processImportsWithWorkflowSpec(content string, workflow *WorkflowSpec, comm | |||||||||||||||
| processed = append(processed, importPath) | ||||||||||||||||
| continue | ||||||||||||||||
| } | ||||||||||||||||
| // Preserve relative paths whose files exist in the local workflow directory. | ||||||||||||||||
| // Absolute paths (starting with "/") are not checked — they are always resolved | ||||||||||||||||
| // relative to the repo root and cannot be reliably tested here. | ||||||||||||||||
| if localWorkflowDir != "" && !strings.HasPrefix(importPath, "/") { | ||||||||||||||||
| if isLocalFileForUpdate(localWorkflowDir, importPath) { | ||||||||||||||||
| importsLog.Printf("Import path exists locally, preserving relative path: %s", importPath) | ||||||||||||||||
| processed = append(processed, importPath) | ||||||||||||||||
| continue | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| resolvedPath := resolveImportPath(importPath, workflow.WorkflowPath) | ||||||||||||||||
| importsLog.Printf("Resolved import path: %s -> %s (workflow: %s)", importPath, resolvedPath, workflow.WorkflowPath) | ||||||||||||||||
| workflowSpec := buildWorkflowSpecRef(workflow.RepoSlug, resolvedPath, commitSHA, workflow.Version) | ||||||||||||||||
|
|
@@ -317,10 +333,12 @@ func processIncludesWithWorkflowSpec(content string, workflow *WorkflowSpec, com | |||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // processIncludesInContent processes @include directives in workflow content for update command | ||||||||||||||||
| // and also processes imports field in frontmatter | ||||||||||||||||
| func processIncludesInContent(content string, workflow *WorkflowSpec, commitSHA string, verbose bool) (string, error) { | ||||||||||||||||
| // and also processes imports field in frontmatter. | ||||||||||||||||
| // If localWorkflowDir is non-empty, any relative import/include path whose file exists under | ||||||||||||||||
| // that directory is left as-is rather than being rewritten to a cross-repo reference. | ||||||||||||||||
| func processIncludesInContent(content string, workflow *WorkflowSpec, commitSHA string, localWorkflowDir string, verbose bool) (string, error) { | ||||||||||||||||
| // First process imports field in frontmatter | ||||||||||||||||
| processedImportsContent, err := processImportsWithWorkflowSpec(content, workflow, commitSHA, verbose) | ||||||||||||||||
| processedImportsContent, err := processImportsWithWorkflowSpec(content, workflow, commitSHA, localWorkflowDir, verbose) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| if verbose { | ||||||||||||||||
| fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to process imports: %v", err))) | ||||||||||||||||
|
|
@@ -367,6 +385,15 @@ func processIncludesInContent(content string, workflow *WorkflowSpec, commitSHA | |||||||||||||||
| continue | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Preserve relative @include paths whose files exist in the local workflow directory. | ||||||||||||||||
| if localWorkflowDir != "" && !strings.HasPrefix(filePath, "/") { | ||||||||||||||||
| if isLocalFileForUpdate(localWorkflowDir, filePath) { | ||||||||||||||||
| importsLog.Printf("Include path exists locally, preserving: %s", filePath) | ||||||||||||||||
| result.WriteString(line + "\n") | ||||||||||||||||
| continue | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Resolve the file path relative to the workflow file's directory | ||||||||||||||||
| resolvedPath := resolveImportPath(filePath, workflow.WorkflowPath) | ||||||||||||||||
|
|
||||||||||||||||
|
|
@@ -393,7 +420,28 @@ func processIncludesInContent(content string, workflow *WorkflowSpec, commitSHA | |||||||||||||||
| return result.String(), scanner.Err() | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // isWorkflowSpecFormat checks if a path already looks like a workflowspec | ||||||||||||||||
| // isLocalFileForUpdate returns true when importPath resolves to an existing file | ||||||||||||||||
| // within localWorkflowDir. The resolved absolute path must stay inside localWorkflowDir | ||||||||||||||||
| // to guard against path traversal (e.g. "../../etc/passwd" in import paths). | ||||||||||||||||
| // importPath must be a relative path — callers must not pass absolute paths here. | ||||||||||||||||
| func isLocalFileForUpdate(localWorkflowDir, importPath string) bool { | ||||||||||||||||
| if localWorkflowDir == "" || importPath == "" { | ||||||||||||||||
| return false | ||||||||||||||||
| } | ||||||||||||||||
| localPath := filepath.Join(localWorkflowDir, importPath) | ||||||||||||||||
| absDir, err1 := filepath.Abs(localWorkflowDir) | ||||||||||||||||
| absPath, err2 := filepath.Abs(localPath) | ||||||||||||||||
| if err1 != nil || err2 != nil { | ||||||||||||||||
| return false | ||||||||||||||||
| } | ||||||||||||||||
| // Reject traversal attempts: the resolved path must be a child of localWorkflowDir | ||||||||||||||||
| if !strings.HasPrefix(absPath, absDir+string(filepath.Separator)) { | ||||||||||||||||
| return false | ||||||||||||||||
| } | ||||||||||||||||
| _, statErr := os.Stat(localPath) | ||||||||||||||||
| return statErr == nil | ||||||||||||||||
|
Comment on lines
+441
to
+442
|
||||||||||||||||
| _, statErr := os.Stat(localPath) | |
| return statErr == nil | |
| fi, statErr := os.Stat(localPath) | |
| if statErr != nil { | |
| return false | |
| } | |
| return fi.Mode().IsRegular() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment says “Python is used for robust YAML-aware parsing”, but the script is using a regex over the raw file contents (not a YAML parser). This is misleading for future maintenance; either adjust the comment or switch to an actual YAML parser (e.g.,
python3 -c 'import yaml'oryq) if true YAML parsing is intended.