Conversation
…them Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
…rkflow fetching Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Done in commit
resources:
- triage-issue.md
- my-custom-action.yml
- ${{ vars.DYNAMIC_WORKFLOW }} # skipped — runtime expression
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot macros are NOT allowed in the resources field entries |
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Fixed in
|
|
@copilot add tests for shared agentic workflow |
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Added in
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot add integration tests in co.yml that install workflows using dispatch-workflow. |
…ching in ci.yml Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Added in
The new |
|
…404) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Fixed in Both Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
There was a problem hiding this comment.
Pull request overview
This PR extends the gh aw add command to automatically fetch and save dispatch-workflow dependencies and resources declared in workflow frontmatter when adding a remote workflow. The feature handles both direct frontmatter declarations and import-derived dispatch workflows that only become visible after compiler processing.
Changes:
- Adds a new
Resources []stringfrontmatter field (types, schema, compiler validation) for specifying additional files to fetch alongside a workflow - Adds
fetchAndSaveRemoteDispatchWorkflows,fetchAndSaveRemoteResources, andfetchAndSaveDispatchWorkflowsFromParsedFiletoremote_workflow.go, wired intoaddWorkflowWithTracking - Adds comprehensive unit and integration tests, plus a new CI job for integration coverage
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
pkg/workflow/frontmatter_types.go |
Adds Resources []string field to FrontmatterConfig |
pkg/workflow/compiler.go |
Adds validation that resources entries cannot contain ${{ expression syntax |
pkg/parser/schemas/main_workflow_schema.json |
Adds JSON schema definition for the resources field |
pkg/cli/remote_workflow.go |
Core implementation: extract/fetch/save helpers for dispatch-workflow deps and resources |
pkg/cli/add_command.go |
Wires the new fetch functions into the addWorkflowWithTracking flow |
pkg/cli/remote_workflow_test.go |
Unit tests for all new functions |
pkg/cli/add_integration_test.go |
Integration tests for dispatch-workflow dependency fetching |
.github/workflows/ci.yml |
New CI job running the integration tests |
Comments suppressed due to low confidence (1)
pkg/cli/remote_workflow.go:1228
- The same post-write stat bug exists in
fetchAndSaveDispatchWorkflowsFromParsedFile:os.Stat(ymlLocalPath)is called afteros.WriteFilesucceeds, so the file always exists at that point andTrackCreatedis never reached. AymlFileExistsflag should be captured before the write.
if tracker != nil {
if _, statErr := os.Stat(ymlLocalPath); statErr == nil {
tracker.TrackModified(ymlLocalPath)
} else {
tracker.TrackCreated(ymlLocalPath)
}
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
pkg/workflow/compiler.go
Outdated
| } | ||
|
|
||
| // Validate resources field — GitHub Actions expression syntax is not allowed. | ||
| log.Print("Validating resources field") |
There was a problem hiding this comment.
The log.Print("Validating resources field") call uses the stdlib log package directly. Looking at the surrounding code in compiler.go (e.g., lines 332, 353, 385, 404), the established pattern is log.Printf(...) with a format string, but the key difference here vs. log.Printf is that log.Print also exists in stdlib. More importantly, looking at the codebase conventions, other validations in this same function use log.Printf (e.g., log.Printf("Validating GitHub tools against enabled toolsets")). The call here (log.Print) is inconsistent with those (which use log.Printf). While functionally equivalent for a string-only argument, the inconsistency is worth noting.
| log.Print("Validating resources field") | |
| log.Printf("Validating resources field") |
| if tracker != nil { | ||
| if _, statErr := os.Stat(ymlLocalPath); statErr == nil { | ||
| tracker.TrackModified(ymlLocalPath) | ||
| } else { | ||
| tracker.TrackCreated(ymlLocalPath) | ||
| } | ||
| } |
There was a problem hiding this comment.
The tracker logic in the .yml fallback path checks whether the file exists after writing it (line 813). Since os.WriteFile has already succeeded at this point, os.Stat(ymlLocalPath) will always succeed, so TrackCreated (line 816) is never called — every newly-created .yml file is incorrectly tracked as modified instead of created. This will break rollback (which relies on CreatedFiles to know which files to delete) and also breaks staging correctness.
The fileExists boolean should be captured before the write, similar to how the .md path does it (see the fileExists flag set before os.WriteFile at lines ~760 and ~837-842). The same bug appears in the identical tracker block inside fetchAndSaveDispatchWorkflowsFromParsedFile at the corresponding location.
This issue also appears on line 1222 of the same file.
| func fetchAndSaveRemoteDispatchWorkflows(content string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker) error { | ||
| if spec.RepoSlug == "" { | ||
| return nil | ||
| } | ||
|
|
||
| parts := strings.SplitN(spec.RepoSlug, "/", 2) | ||
| if len(parts) != 2 { | ||
| return nil | ||
| } | ||
| owner, repo := parts[0], parts[1] | ||
| ref := spec.Version | ||
| if ref == "" { | ||
| defaultBranch, err := getRepoDefaultBranch(spec.RepoSlug) | ||
| if err != nil { | ||
| remoteWorkflowLog.Printf("Failed to resolve default branch for %s, falling back to 'main': %v", spec.RepoSlug, err) | ||
| ref = "main" | ||
| } else { | ||
| ref = defaultBranch | ||
| } | ||
| spec.Version = ref | ||
| } | ||
|
|
||
| workflowNames := extractDispatchWorkflowNames(content) | ||
| if len(workflowNames) == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| // workflowBaseDir is the directory of the source workflow in the remote repo | ||
| // (e.g. ".github/workflows"). Dispatch-workflow names are resolved relative to it. | ||
| workflowBaseDir := getParentDir(spec.WorkflowPath) | ||
|
|
||
| // Pre-compute the absolute target directory for path-traversal boundary checks. | ||
| absTargetDir, err := filepath.Abs(targetDir) | ||
| if err != nil { | ||
| remoteWorkflowLog.Printf("Failed to resolve absolute path for target directory %s: %v", targetDir, err) | ||
| return nil | ||
| } | ||
|
|
||
| for _, workflowName := range workflowNames { | ||
| // Build the remote file path for this dispatch workflow | ||
| var remoteFilePath string | ||
| if workflowBaseDir != "" { | ||
| remoteFilePath = path.Join(workflowBaseDir, workflowName+".md") | ||
| } else { | ||
| remoteFilePath = workflowName + ".md" | ||
| } | ||
| remoteFilePath = path.Clean(remoteFilePath) | ||
|
|
||
| // The local path is just the workflow filename in targetDir | ||
| localRelPath := filepath.Clean(workflowName + ".md") | ||
| targetPath := filepath.Join(targetDir, localRelPath) | ||
|
|
||
| // Belt-and-suspenders: verify the resolved path stays inside targetDir | ||
| absTargetPath, absErr := filepath.Abs(targetPath) | ||
| if absErr != nil { | ||
| remoteWorkflowLog.Printf("Failed to resolve absolute path for dispatch workflow %s: %v", workflowName, absErr) | ||
| continue | ||
| } | ||
| if rel, relErr := filepath.Rel(absTargetDir, absTargetPath); relErr != nil || strings.HasPrefix(rel, "..") { | ||
| if verbose { | ||
| fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Refusing to write dispatch workflow outside target directory: %q", workflowName))) | ||
| } | ||
| continue | ||
| } | ||
|
|
||
| // Check whether the target file already exists. | ||
| fileExists := false | ||
| if _, statErr := os.Stat(targetPath); statErr == nil { | ||
| fileExists = true | ||
| if !force { | ||
| // Allow if the existing file comes from the same source repository. | ||
| existingSourceRepo := readSourceRepoFromFile(targetPath) | ||
| if existingSourceRepo == spec.RepoSlug { | ||
| if verbose { | ||
| fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Dispatch workflow from same source already exists, skipping: "+targetPath)) | ||
| } | ||
| continue | ||
| } | ||
| // Different or missing source — this is a conflict. | ||
| return fmt.Errorf( | ||
| "dispatch workflow %q already exists at %s (existing source: %q, installing from: %q); remove the file or use --force to overwrite", | ||
| workflowName, targetPath, sourceRepoLabel(existingSourceRepo), spec.RepoSlug, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| // Download from the source repository — try .md first, then .yml as fallback | ||
| // (the dispatch-workflow validator accepts either .md or .yml files locally). | ||
| workflowContent, err := parser.DownloadFileFromGitHub(owner, repo, remoteFilePath, ref) | ||
| if err != nil { | ||
| // .md not found — try .yml fallback (e.g. plain GitHub Actions workflow) | ||
| ymlRemotePath := path.Clean(strings.TrimSuffix(remoteFilePath, ".md") + ".yml") | ||
| ymlLocalPath := filepath.Join(targetDir, filepath.Clean(workflowName+".yml")) | ||
|
|
||
| ymlContent, ymlErr := parser.DownloadFileFromGitHub(owner, repo, ymlRemotePath, ref) | ||
| if ymlErr != nil { | ||
| // Neither .md nor .yml found — best-effort, continue | ||
| if verbose { | ||
| fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to fetch dispatch workflow %s: %v", remoteFilePath, err))) | ||
| } | ||
| continue | ||
| } | ||
| // .yml fallback succeeded — write it (no source field for yml) | ||
| if mkErr := os.MkdirAll(filepath.Dir(ymlLocalPath), 0755); mkErr != nil { | ||
| if verbose { | ||
| fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to create directory for dispatch workflow %s: %v", ymlRemotePath, mkErr))) | ||
| } | ||
| continue | ||
| } | ||
| if writeErr := os.WriteFile(ymlLocalPath, ymlContent, 0600); writeErr != nil { | ||
| if verbose { | ||
| fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to write dispatch workflow %s: %v", ymlRemotePath, writeErr))) | ||
| } | ||
| continue | ||
| } | ||
| if verbose { | ||
| fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Fetched dispatch workflow (.yml): "+ymlLocalPath)) | ||
| } | ||
| if tracker != nil { | ||
| if _, statErr := os.Stat(ymlLocalPath); statErr == nil { | ||
| tracker.TrackModified(ymlLocalPath) | ||
| } else { | ||
| tracker.TrackCreated(ymlLocalPath) | ||
| } | ||
| } | ||
| continue | ||
| } | ||
|
|
||
| // Embed the source field so future adds can detect same-source conflicts. | ||
| depSourceString := spec.RepoSlug + "/" + remoteFilePath + "@" + ref | ||
| if updated, srcErr := addSourceToWorkflow(string(workflowContent), depSourceString); srcErr == nil { | ||
| workflowContent = []byte(updated) | ||
| } | ||
|
|
||
| // Create parent directory if needed | ||
| if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { | ||
| if verbose { | ||
| fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to create directory for dispatch workflow %s: %v", remoteFilePath, err))) | ||
| } | ||
| continue | ||
| } | ||
|
|
||
| // Write the file | ||
| if err := os.WriteFile(targetPath, workflowContent, 0600); err != nil { | ||
| if verbose { | ||
| fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to write dispatch workflow %s: %v", remoteFilePath, err))) | ||
| } | ||
| continue | ||
| } | ||
|
|
||
| if verbose { | ||
| fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Fetched dispatch workflow: "+targetPath)) | ||
| } | ||
|
|
||
| // Track the file | ||
| if tracker != nil { | ||
| if fileExists { | ||
| tracker.TrackModified(targetPath) | ||
| } else { | ||
| tracker.TrackCreated(targetPath) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // extractResources extracts file paths from the top-level "resources" frontmatter field. | ||
| // Returns an error if any entry contains GitHub Actions expression syntax (e.g. "${{"), | ||
| // since macros are not permitted in resource paths. | ||
| func extractResources(content string) ([]string, error) { | ||
| result, err := parser.ExtractFrontmatterFromContent(content) | ||
| if err != nil { | ||
| remoteWorkflowLog.Printf("Failed to extract frontmatter for resources: %v", err) | ||
| return nil, nil | ||
| } | ||
| if result.Frontmatter == nil { | ||
| return nil, nil | ||
| } | ||
|
|
||
| resourcesField, exists := result.Frontmatter["resources"] | ||
| if !exists { | ||
| return nil, nil | ||
| } | ||
|
|
||
| var paths []string | ||
| switch v := resourcesField.(type) { | ||
| case []any: | ||
| for _, item := range v { | ||
| if s, ok := item.(string); ok { | ||
| paths = append(paths, s) | ||
| } | ||
| } | ||
| case []string: | ||
| paths = v | ||
| } | ||
|
|
||
| // Reject entries that contain GitHub Actions expression syntax — macros are not allowed. | ||
| for _, p := range paths { | ||
| if strings.Contains(p, "${{") { | ||
| return nil, fmt.Errorf("resources entry %q contains GitHub Actions expression syntax (${{) which is not allowed; use static paths only", p) | ||
| } | ||
| } | ||
|
|
||
| return paths, nil | ||
| } | ||
|
|
||
| // fetchAndSaveRemoteResources fetches files listed in the top-level "resources" frontmatter | ||
| // field from the same remote repository and saves them locally. Resources are resolved as | ||
| // relative paths from the same directory as the source workflow in the remote repo. | ||
| // | ||
| // GitHub Actions expression syntax (e.g. "${{") is not allowed in resource paths and will | ||
| // cause an error. Download failures for individual files are non-fatal (best-effort). | ||
| // | ||
| // For Markdown resource files: if the target already exists from a different source repository | ||
| // (different 'source:' frontmatter field, or no source field), an error is returned. Files | ||
| // from the same source are silently skipped. | ||
| // For non-Markdown resource files: if the target already exists and force is false, an error | ||
| // is returned regardless of origin (non-markdown files have no source tracking). | ||
| func fetchAndSaveRemoteResources(content string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker) error { | ||
| if spec.RepoSlug == "" { | ||
| return nil | ||
| } | ||
|
|
||
| parts := strings.SplitN(spec.RepoSlug, "/", 2) | ||
| if len(parts) != 2 { | ||
| return nil | ||
| } | ||
| owner, repo := parts[0], parts[1] | ||
| ref := spec.Version | ||
| if ref == "" { | ||
| defaultBranch, err := getRepoDefaultBranch(spec.RepoSlug) | ||
| if err != nil { | ||
| remoteWorkflowLog.Printf("Failed to resolve default branch for %s, falling back to 'main': %v", spec.RepoSlug, err) | ||
| ref = "main" | ||
| } else { | ||
| ref = defaultBranch | ||
| } | ||
| spec.Version = ref | ||
| } | ||
|
|
||
| resourcePaths, err := extractResources(content) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if len(resourcePaths) == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| // Resources are resolved relative to the source workflow's directory in the remote repo. | ||
| workflowBaseDir := getParentDir(spec.WorkflowPath) | ||
|
|
||
| // Pre-compute the absolute target directory for path-traversal boundary checks. | ||
| absTargetDir, err := filepath.Abs(targetDir) | ||
| if err != nil { | ||
| remoteWorkflowLog.Printf("Failed to resolve absolute path for target directory %s: %v", targetDir, err) | ||
| return nil | ||
| } | ||
|
|
||
| for _, resourcePath := range resourcePaths { | ||
| // Early rejection of path traversal patterns. This is a fast first-pass check; | ||
| // the filepath.Rel boundary check below is the authoritative security control. | ||
| if strings.Contains(resourcePath, "..") { | ||
| if verbose { | ||
| fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Skipping resource with unsafe path: %q", resourcePath))) | ||
| } | ||
| continue | ||
| } | ||
|
|
||
| // Resolve the remote file path | ||
| var remoteFilePath string | ||
| if rest, ok := strings.CutPrefix(resourcePath, "/"); ok { | ||
| remoteFilePath = rest | ||
| } else if workflowBaseDir != "" { | ||
| remoteFilePath = path.Join(workflowBaseDir, resourcePath) | ||
| } else { | ||
| remoteFilePath = resourcePath | ||
| } | ||
| remoteFilePath = path.Clean(remoteFilePath) | ||
|
|
||
| // Derive the local relative path by stripping the workflow base dir prefix | ||
| localRelPath := remoteFilePath | ||
| if workflowBaseDir != "" && strings.HasPrefix(remoteFilePath, workflowBaseDir+"/") { | ||
| localRelPath = remoteFilePath[len(workflowBaseDir)+1:] | ||
| } | ||
| localRelPath = filepath.Clean(filepath.FromSlash(localRelPath)) | ||
| localRelPath = strings.TrimLeft(localRelPath, string(filepath.Separator)) | ||
| if localRelPath == "" || localRelPath == "." { | ||
| continue | ||
| } | ||
| targetPath := filepath.Join(targetDir, localRelPath) | ||
|
|
||
| // Belt-and-suspenders: verify the resolved path stays inside targetDir | ||
| absTargetPath, absErr := filepath.Abs(targetPath) | ||
| if absErr != nil { | ||
| remoteWorkflowLog.Printf("Failed to resolve absolute path for resource %s: %v", resourcePath, absErr) | ||
| continue | ||
| } | ||
| if rel, relErr := filepath.Rel(absTargetDir, absTargetPath); relErr != nil || strings.HasPrefix(rel, "..") { | ||
| if verbose { | ||
| fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Refusing to write resource outside target directory: %q", resourcePath))) | ||
| } | ||
| continue | ||
| } | ||
|
|
||
| // Check whether the target file already exists. | ||
| fileExists := false | ||
| if _, statErr := os.Stat(targetPath); statErr == nil { | ||
| fileExists = true | ||
| if !force { | ||
| isMarkdown := strings.HasSuffix(strings.ToLower(targetPath), ".md") | ||
| if isMarkdown { | ||
| // For markdown files, allow same-source overwrites. | ||
| existingSourceRepo := readSourceRepoFromFile(targetPath) | ||
| if existingSourceRepo == spec.RepoSlug { | ||
| if verbose { | ||
| fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Resource file from same source already exists, skipping: "+targetPath)) | ||
| } | ||
| continue | ||
| } | ||
| return fmt.Errorf( | ||
| "resource %q already exists at %s (existing source: %q, installing from: %q); remove the file or use --force to overwrite", | ||
| resourcePath, targetPath, sourceRepoLabel(existingSourceRepo), spec.RepoSlug, | ||
| ) | ||
| } | ||
| // Non-markdown files have no source tracking — always conflict. | ||
| return fmt.Errorf( | ||
| "resource %q already exists at %s; remove the file or use --force to overwrite", | ||
| resourcePath, targetPath, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| // Download from source repository | ||
| fileContent, err := parser.DownloadFileFromGitHub(owner, repo, remoteFilePath, ref) | ||
| if err != nil { | ||
| if verbose { | ||
| fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to fetch resource %s: %v", remoteFilePath, err))) | ||
| } | ||
| continue | ||
| } | ||
|
|
||
| // For markdown resources, embed the source field for future conflict detection. | ||
| if strings.HasSuffix(strings.ToLower(remoteFilePath), ".md") { | ||
| depSourceString := spec.RepoSlug + "/" + remoteFilePath + "@" + ref | ||
| if updated, srcErr := addSourceToWorkflow(string(fileContent), depSourceString); srcErr == nil { | ||
| fileContent = []byte(updated) | ||
| } | ||
| } | ||
|
|
||
| // Create parent directory if needed | ||
| if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { | ||
| if verbose { | ||
| fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to create directory for resource %s: %v", remoteFilePath, err))) | ||
| } | ||
| continue | ||
| } | ||
|
|
||
| // Write the file | ||
| if err := os.WriteFile(targetPath, fileContent, 0600); err != nil { | ||
| if verbose { | ||
| fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to write resource %s: %v", remoteFilePath, err))) | ||
| } | ||
| continue | ||
| } | ||
|
|
||
| if verbose { | ||
| fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Fetched resource: "+targetPath)) | ||
| } | ||
|
|
||
| // Track the file | ||
| if tracker != nil { | ||
| if fileExists { | ||
| tracker.TrackModified(targetPath) | ||
| } else { | ||
| tracker.TrackCreated(targetPath) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } |
There was a problem hiding this comment.
The fetchAndSaveRemoteDispatchWorkflows and fetchAndSaveRemoteResources functions contain nearly identical logic (ref resolution, abs-path safety checks, conflict detection, download, source embedding, directory creation, write, tracker update). This substantial duplication will increase maintenance burden — any future fix or enhancement must be applied in multiple places. Consider extracting the common "download-and-save-single-file" logic into a shared helper.
|
@copilot review comments and fix https://github.com/github/gh-aw/actions/runs/22803347780/job/66148490766 |
…flow; fix tracker bug and log.Print style Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Fixed in
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
…ad of erroring as conflict Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Fixed in Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
…ource conflict error Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Fixed in Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
Document the new `resources:` frontmatter field added in PR #19965, which declares additional workflow/action files fetched when running `gh aw add`. Also note that dispatch-workflow dependencies are automatically fetched. - Add `resources:` section to frontmatter.md (after `private:`) - Update packaging-imports.md to describe automatic dependency fetching - Add note to dispatch-workflow section in safe-outputs.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
resourcesfrontmatter field to JSON schemaResources []stringfield toFrontmatterConfigextractResources— errors on${{macro entriesfetchAndSaveRemoteResources— fetches resources from remote repofetchAndSaveDispatchWorkflowsFromParsedFile— parses merged safe-outputsaddWorkflowWithTracking${{inresourcesgh aw addwithdispatch-workflowdependencies.ymlwhen dispatch-workflow.mdis 404.mddependencies before main workflow (compileDispatchWorkflowDependencies)ymlFileExistsbeforeos.WriteFilein both.ymlfallback pathslog.Print→log.Printfincompiler.gofor style consistencyTestFetchDispatchWorkflows_NoSourceConflictcorrectly errors on no-source filesTestFetchDispatchWorkflows_NoSourceConflictexpects error for no-source files; updated the 4 old "SkipExistingWithoutForce"/"TrackerNoOpOnExisting" tests to include a matchingsource:field in their pre-existing file content so they test the "same source → skip" pathOriginal prompt
🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.