Repository navigation
Add fix codemods for Copilot web-fetch and reserved inputs #66556
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 2 commits
f8423bf
4b88a27
92f6331
daa2f74
b52582f
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 |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package cli | ||
|
|
||
| import ( | ||
| "github.com/github/gh-aw/pkg/logger" | ||
| "github.com/github/gh-aw/pkg/workflow" | ||
| ) | ||
|
|
||
| var copilotWebFetchCodemodLog = logger.New("cli:codemod_copilot_web_fetch") | ||
|
|
||
| func getCopilotWebFetchRemovalCodemod() Codemod { | ||
| return Codemod{ | ||
| ID: "copilot-web-fetch-removal", | ||
| Name: "Remove unsupported Copilot web-fetch tool", | ||
| Description: "Removes tools.web-fetch when Copilot CLI runs in offline BYOK mode. Copilot SDK mode retains its proxy-aware custom fetch tool.", | ||
| IntroducedIn: "1.0.0", | ||
| Apply: func(content string, frontmatter map[string]any) (string, bool, error) { | ||
| if extractEngineIDFromFrontmatter(frontmatter) != "copilot" { | ||
| return content, false, nil | ||
| } | ||
|
|
||
| _, engineConfig, _ := (&workflow.Compiler{}).ExtractEngineConfig(frontmatter) | ||
| if engineConfig != nil && engineConfig.CopilotSDK { | ||
| return content, false, nil | ||
| } | ||
|
|
||
| tools, ok := frontmatter["tools"].(map[string]any) | ||
| if !ok { | ||
| return content, false, nil | ||
| } | ||
| webFetch, exists := tools["web-fetch"] | ||
| if !exists { | ||
| return content, false, nil | ||
| } | ||
| if enabled, ok := webFetch.(bool); ok && !enabled { | ||
| return content, false, nil | ||
| } | ||
|
|
||
| newContent, applied, err := applyFrontmatterLineTransform(content, func(lines []string) ([]string, bool) { | ||
| result, modified := removeFieldFromBlock(lines, "web-fetch", "tools") | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both removals now edit YAML mapping nodes, covering flow maps and quoted keys while retaining siblings. Regression cases were added in |
||
| return result, modified | ||
| }) | ||
| if applied { | ||
| copilotWebFetchCodemodLog.Print("Removed unsupported tools.web-fetch for Copilot CLI") | ||
| } | ||
| return newContent, applied, err | ||
| }, | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| //go:build !integration | ||
|
|
||
| package cli | ||
|
|
||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "github.com/github/gh-aw/pkg/workflow" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestCopilotWebFetchRemovalCodemod_Metadata(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| codemod := getCopilotWebFetchRemovalCodemod() | ||
| assert.Equal(t, "copilot-web-fetch-removal", codemod.ID) | ||
| assert.NotEmpty(t, codemod.Name) | ||
| assert.NotEmpty(t, codemod.Description) | ||
| assert.Equal(t, "1.0.0", codemod.IntroducedIn) | ||
| require.NotNil(t, codemod.Apply) | ||
| } | ||
|
|
||
| func TestCopilotWebFetchRemovalCodemod_Apply(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| content := `--- | ||
| engine: | ||
| id: copilot | ||
| tools: | ||
| web-fetch: true # native fetch is unavailable | ||
| bash: ["git"] | ||
| --- | ||
|
|
||
| # Keep this body intact. | ||
| ` | ||
| frontmatter := map[string]any{ | ||
| "engine": map[string]any{"id": "copilot"}, | ||
| "tools": map[string]any{"web-fetch": true, "bash": []any{"git"}}, | ||
| } | ||
|
|
||
| codemod := getCopilotWebFetchRemovalCodemod() | ||
| updated, applied, err := codemod.Apply(content, frontmatter) | ||
| require.NoError(t, err) | ||
| require.True(t, applied) | ||
| assert.Equal(t, `--- | ||
| engine: | ||
| id: copilot | ||
| tools: | ||
| bash: ["git"] | ||
| --- | ||
|
|
||
| # Keep this body intact. | ||
| `, updated) | ||
|
|
||
| updatedAgain, appliedAgain, err := codemod.Apply(updated, frontmatter) | ||
| require.NoError(t, err) | ||
| assert.False(t, appliedAgain) | ||
| assert.Equal(t, updated, updatedAgain) | ||
| } | ||
|
|
||
| func TestCopilotWebFetchRemovalCodemod_CompatibleInputsAreUnchanged(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| codemod := getCopilotWebFetchRemovalCodemod() | ||
| tests := []struct { | ||
| name string | ||
| frontmatter map[string]any | ||
| content string | ||
| }{ | ||
| { | ||
| name: "Copilot SDK custom fetch", | ||
| frontmatter: map[string]any{ | ||
| "engine": map[string]any{"id": "copilot", "copilot-sdk": true}, | ||
| "tools": map[string]any{"web-fetch": true}, | ||
| }, | ||
| content: "---\nengine:\n id: copilot\n copilot-sdk: true\ntools:\n web-fetch:\n---\n", | ||
| }, | ||
| { | ||
| name: "other engine", | ||
| frontmatter: map[string]any{"engine": "codex", "tools": map[string]any{"web-fetch": true}}, | ||
| content: "---\nengine: codex\ntools:\n web-fetch:\n---\n", | ||
| }, | ||
| { | ||
| name: "explicitly disabled", | ||
| frontmatter: map[string]any{"engine": "copilot", "tools": map[string]any{"web-fetch": false}}, | ||
| content: "---\nengine: copilot\ntools:\n web-fetch: false\n---\n", | ||
| }, | ||
| { | ||
| name: "no web-fetch setting", | ||
| frontmatter: map[string]any{"engine": "copilot", "tools": map[string]any{"bash": true}}, | ||
| content: "---\nengine: copilot\ntools:\n bash: true\n---\n", | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
| updated, applied, err := codemod.Apply(tt.content, tt.frontmatter) | ||
| require.NoError(t, err) | ||
| assert.False(t, applied) | ||
| assert.Equal(t, tt.content, updated) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestCopilotWebFetchRemovalCodemod_FixesStrictCompilation(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| content := `--- | ||
| on: workflow_dispatch | ||
| strict: true | ||
| engine: | ||
| id: copilot | ||
| tools: | ||
| web-fetch: | ||
| --- | ||
| Fetch content. | ||
| ` | ||
| compiler := workflow.NewCompiler() | ||
| _, err := compiler.ParseWorkflowString(content, "copilot-web-fetch.md") | ||
| require.ErrorContains(t, err, "Copilot's native 'web-fetch' tool is unavailable") | ||
|
|
||
| path := filepath.Join(t.TempDir(), "copilot-web-fetch.md") | ||
| require.NoError(t, os.WriteFile(path, []byte(content), 0o600)) | ||
| fixed, _, err := processWorkflowFileWithInfo(path, GetAllCodemods(), true, false) | ||
| require.NoError(t, err) | ||
| require.True(t, fixed) | ||
|
|
||
| updatedBytes, err := os.ReadFile(path) | ||
| require.NoError(t, err) | ||
| updated := string(updatedBytes) | ||
| compiler = workflow.NewCompiler() | ||
| data, err := compiler.ParseWorkflowString(updated, "copilot-web-fetch.md") | ||
| require.NoError(t, err) | ||
| require.NoError(t, compiler.CompileWorkflowData(data, filepath.Join(t.TempDir(), "copilot-web-fetch.md"))) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| package cli | ||
|
|
||
| import ( | ||
| "strings" | ||
|
|
||
| "github.com/github/gh-aw/pkg/logger" | ||
| ) | ||
|
|
||
| var workflowDispatchAwContextCodemodLog = logger.New("cli:codemod_workflow_dispatch_aw_context") | ||
|
|
||
| func getWorkflowDispatchAwContextRemovalCodemod() Codemod { | ||
| return Codemod{ | ||
| ID: "workflow-dispatch-aw-context-removal", | ||
| Name: "Remove reserved workflow_dispatch aw_context input", | ||
| Description: "Removes on.workflow_dispatch.inputs.aw_context, which is reserved and managed by the compiler.", | ||
| IntroducedIn: "1.0.0", | ||
| Apply: func(content string, frontmatter map[string]any) (string, bool, error) { | ||
| on, ok := frontmatter["on"].(map[string]any) | ||
| if !ok { | ||
| return content, false, nil | ||
| } | ||
| dispatch, ok := on["workflow_dispatch"].(map[string]any) | ||
| if !ok { | ||
| return content, false, nil | ||
| } | ||
| inputs, ok := dispatch["inputs"].(map[string]any) | ||
| if !ok { | ||
| return content, false, nil | ||
| } | ||
| if _, exists := inputs["aw_context"]; !exists { | ||
| return content, false, nil | ||
| } | ||
|
|
||
| newContent, applied, err := applyFrontmatterLineTransform(content, func(lines []string) ([]string, bool) { | ||
| onLine := findFrontmatterKeyLine(lines, "on", 0, len(lines), -1) | ||
| if onLine < 0 || onLine >= len(lines) { | ||
| return lines, false | ||
| } | ||
| onEnd := frontmatterBlockEnd(lines, onLine, len(getIndentation(lines[onLine]))) | ||
| onIndent := len(getIndentation(lines[onLine])) | ||
| dispatchLine := findFrontmatterKeyLine(lines, "workflow_dispatch", onLine+1, onEnd, onIndent) | ||
| if dispatchLine < 0 || dispatchLine >= len(lines) { | ||
| return lines, false | ||
| } | ||
| dispatchEnd := frontmatterBlockEnd(lines, dispatchLine, len(getIndentation(lines[dispatchLine]))) | ||
| block, modified := removeFieldFromBlock(lines[dispatchLine:dispatchEnd], "aw_context", "inputs") | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The removals now follow the actual YAML mapping path, so matching text inside block scalars remains untouched. Added literal-text regression tests in |
||
| if !modified { | ||
| return lines, false | ||
| } | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the reserved input is the only entry, the codemod now leaves |
||
| result := make([]string, 0, len(lines)-(dispatchEnd-dispatchLine)+len(block)) | ||
| result = append(result, lines[:dispatchLine]...) | ||
| result = append(result, block...) | ||
| result = append(result, lines[dispatchEnd:]...) | ||
| return result, true | ||
| }) | ||
| if applied { | ||
| workflowDispatchAwContextCodemodLog.Print("Removed reserved on.workflow_dispatch.inputs.aw_context") | ||
| } | ||
| return newContent, applied, err | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| func findFrontmatterKeyLine(lines []string, key string, start, end, parentIndent int) int { | ||
| childIndent := -1 | ||
| for i, line := range lines { | ||
| if i < start || i >= end { | ||
| continue | ||
| } | ||
| trimmed := strings.TrimSpace(line) | ||
| if trimmed == "" || strings.HasPrefix(trimmed, "#") { | ||
| continue | ||
| } | ||
| indent := len(getIndentation(line)) | ||
| if parentIndent >= 0 { | ||
| if indent <= parentIndent { | ||
| continue | ||
| } | ||
| if childIndent < 0 { | ||
| childIndent = indent | ||
| } | ||
| if indent < childIndent { | ||
| return -1 | ||
| } | ||
| if indent > childIndent { | ||
| continue | ||
| } | ||
| } | ||
| if frontmatterLineKey(line) == key { | ||
| return i | ||
| } | ||
| } | ||
| return -1 | ||
| } | ||
|
|
||
| func frontmatterBlockEnd(lines []string, start, parentIndent int) int { | ||
| for i, line := range lines { | ||
| if i <= start { | ||
| continue | ||
| } | ||
| trimmed := strings.TrimSpace(line) | ||
| if trimmed == "" || strings.HasPrefix(trimmed, "#") { | ||
| continue | ||
| } | ||
| if len(getIndentation(line)) <= parentIndent { | ||
| return i | ||
| } | ||
| } | ||
| return len(lines) | ||
| } | ||
|
|
||
| func frontmatterLineKey(line string) string { | ||
| trimmed := strings.TrimSpace(line) | ||
| key, _, found := strings.Cut(trimmed, ":") | ||
| if !found { | ||
| return "" | ||
| } | ||
| return strings.Trim(key, `"'`) | ||
| } | ||
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.
Resolved the effective engine and SDK settings through
ApplyWithContext; when resolution fails, the codemod now preservestools.web-fetch. Added import/include coverage indaa2f746.