Skip to content

Commit 4b88a27

Browse files
Copilotpelikhan
andauthored
Add compatibility codemods for audited errors
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent f8423bf commit 4b88a27

6 files changed

Lines changed: 444 additions & 0 deletions
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package cli
2+
3+
import (
4+
"github.com/github/gh-aw/pkg/logger"
5+
"github.com/github/gh-aw/pkg/workflow"
6+
)
7+
8+
var copilotWebFetchCodemodLog = logger.New("cli:codemod_copilot_web_fetch")
9+
10+
func getCopilotWebFetchRemovalCodemod() Codemod {
11+
return Codemod{
12+
ID: "copilot-web-fetch-removal",
13+
Name: "Remove unsupported Copilot web-fetch tool",
14+
Description: "Removes tools.web-fetch when Copilot CLI runs in offline BYOK mode. Copilot SDK mode retains its proxy-aware custom fetch tool.",
15+
IntroducedIn: "1.0.0",
16+
Apply: func(content string, frontmatter map[string]any) (string, bool, error) {
17+
if extractEngineIDFromFrontmatter(frontmatter) != "copilot" {
18+
return content, false, nil
19+
}
20+
21+
_, engineConfig, _ := (&workflow.Compiler{}).ExtractEngineConfig(frontmatter)
22+
if engineConfig != nil && engineConfig.CopilotSDK {
23+
return content, false, nil
24+
}
25+
26+
tools, ok := frontmatter["tools"].(map[string]any)
27+
if !ok {
28+
return content, false, nil
29+
}
30+
webFetch, exists := tools["web-fetch"]
31+
if !exists {
32+
return content, false, nil
33+
}
34+
if enabled, ok := webFetch.(bool); ok && !enabled {
35+
return content, false, nil
36+
}
37+
38+
newContent, applied, err := applyFrontmatterLineTransform(content, func(lines []string) ([]string, bool) {
39+
result, modified := removeFieldFromBlock(lines, "web-fetch", "tools")
40+
return result, modified
41+
})
42+
if applied {
43+
copilotWebFetchCodemodLog.Print("Removed unsupported tools.web-fetch for Copilot CLI")
44+
}
45+
return newContent, applied, err
46+
},
47+
}
48+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
//go:build !integration
2+
3+
package cli
4+
5+
import (
6+
"os"
7+
"path/filepath"
8+
"testing"
9+
10+
"github.com/github/gh-aw/pkg/workflow"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestCopilotWebFetchRemovalCodemod_Metadata(t *testing.T) {
16+
t.Parallel()
17+
18+
codemod := getCopilotWebFetchRemovalCodemod()
19+
assert.Equal(t, "copilot-web-fetch-removal", codemod.ID)
20+
assert.NotEmpty(t, codemod.Name)
21+
assert.NotEmpty(t, codemod.Description)
22+
assert.Equal(t, "1.0.0", codemod.IntroducedIn)
23+
require.NotNil(t, codemod.Apply)
24+
}
25+
26+
func TestCopilotWebFetchRemovalCodemod_Apply(t *testing.T) {
27+
t.Parallel()
28+
29+
content := `---
30+
engine:
31+
id: copilot
32+
tools:
33+
web-fetch: true # native fetch is unavailable
34+
bash: ["git"]
35+
---
36+
37+
# Keep this body intact.
38+
`
39+
frontmatter := map[string]any{
40+
"engine": map[string]any{"id": "copilot"},
41+
"tools": map[string]any{"web-fetch": true, "bash": []any{"git"}},
42+
}
43+
44+
codemod := getCopilotWebFetchRemovalCodemod()
45+
updated, applied, err := codemod.Apply(content, frontmatter)
46+
require.NoError(t, err)
47+
require.True(t, applied)
48+
assert.Equal(t, `---
49+
engine:
50+
id: copilot
51+
tools:
52+
bash: ["git"]
53+
---
54+
55+
# Keep this body intact.
56+
`, updated)
57+
58+
updatedAgain, appliedAgain, err := codemod.Apply(updated, frontmatter)
59+
require.NoError(t, err)
60+
assert.False(t, appliedAgain)
61+
assert.Equal(t, updated, updatedAgain)
62+
}
63+
64+
func TestCopilotWebFetchRemovalCodemod_CompatibleInputsAreUnchanged(t *testing.T) {
65+
t.Parallel()
66+
67+
codemod := getCopilotWebFetchRemovalCodemod()
68+
tests := []struct {
69+
name string
70+
frontmatter map[string]any
71+
content string
72+
}{
73+
{
74+
name: "Copilot SDK custom fetch",
75+
frontmatter: map[string]any{
76+
"engine": map[string]any{"id": "copilot", "copilot-sdk": true},
77+
"tools": map[string]any{"web-fetch": true},
78+
},
79+
content: "---\nengine:\n id: copilot\n copilot-sdk: true\ntools:\n web-fetch:\n---\n",
80+
},
81+
{
82+
name: "other engine",
83+
frontmatter: map[string]any{"engine": "codex", "tools": map[string]any{"web-fetch": true}},
84+
content: "---\nengine: codex\ntools:\n web-fetch:\n---\n",
85+
},
86+
{
87+
name: "explicitly disabled",
88+
frontmatter: map[string]any{"engine": "copilot", "tools": map[string]any{"web-fetch": false}},
89+
content: "---\nengine: copilot\ntools:\n web-fetch: false\n---\n",
90+
},
91+
{
92+
name: "no web-fetch setting",
93+
frontmatter: map[string]any{"engine": "copilot", "tools": map[string]any{"bash": true}},
94+
content: "---\nengine: copilot\ntools:\n bash: true\n---\n",
95+
},
96+
}
97+
98+
for _, tt := range tests {
99+
t.Run(tt.name, func(t *testing.T) {
100+
t.Parallel()
101+
updated, applied, err := codemod.Apply(tt.content, tt.frontmatter)
102+
require.NoError(t, err)
103+
assert.False(t, applied)
104+
assert.Equal(t, tt.content, updated)
105+
})
106+
}
107+
}
108+
109+
func TestCopilotWebFetchRemovalCodemod_FixesStrictCompilation(t *testing.T) {
110+
t.Parallel()
111+
112+
content := `---
113+
on: workflow_dispatch
114+
strict: true
115+
engine:
116+
id: copilot
117+
tools:
118+
web-fetch:
119+
---
120+
Fetch content.
121+
`
122+
compiler := workflow.NewCompiler()
123+
_, err := compiler.ParseWorkflowString(content, "copilot-web-fetch.md")
124+
require.ErrorContains(t, err, "Copilot's native 'web-fetch' tool is unavailable")
125+
126+
path := filepath.Join(t.TempDir(), "copilot-web-fetch.md")
127+
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
128+
fixed, _, err := processWorkflowFileWithInfo(path, GetAllCodemods(), true, false)
129+
require.NoError(t, err)
130+
require.True(t, fixed)
131+
132+
updatedBytes, err := os.ReadFile(path)
133+
require.NoError(t, err)
134+
updated := string(updatedBytes)
135+
compiler = workflow.NewCompiler()
136+
data, err := compiler.ParseWorkflowString(updated, "copilot-web-fetch.md")
137+
require.NoError(t, err)
138+
require.NoError(t, compiler.CompileWorkflowData(data, filepath.Join(t.TempDir(), "copilot-web-fetch.md")))
139+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package cli
2+
3+
import (
4+
"strings"
5+
6+
"github.com/github/gh-aw/pkg/logger"
7+
)
8+
9+
var workflowDispatchAwContextCodemodLog = logger.New("cli:codemod_workflow_dispatch_aw_context")
10+
11+
func getWorkflowDispatchAwContextRemovalCodemod() Codemod {
12+
return Codemod{
13+
ID: "workflow-dispatch-aw-context-removal",
14+
Name: "Remove reserved workflow_dispatch aw_context input",
15+
Description: "Removes on.workflow_dispatch.inputs.aw_context, which is reserved and managed by the compiler.",
16+
IntroducedIn: "1.0.0",
17+
Apply: func(content string, frontmatter map[string]any) (string, bool, error) {
18+
on, ok := frontmatter["on"].(map[string]any)
19+
if !ok {
20+
return content, false, nil
21+
}
22+
dispatch, ok := on["workflow_dispatch"].(map[string]any)
23+
if !ok {
24+
return content, false, nil
25+
}
26+
inputs, ok := dispatch["inputs"].(map[string]any)
27+
if !ok {
28+
return content, false, nil
29+
}
30+
if _, exists := inputs["aw_context"]; !exists {
31+
return content, false, nil
32+
}
33+
34+
newContent, applied, err := applyFrontmatterLineTransform(content, func(lines []string) ([]string, bool) {
35+
onLine := findFrontmatterKeyLine(lines, "on", 0, len(lines), -1)
36+
if onLine < 0 || onLine >= len(lines) {
37+
return lines, false
38+
}
39+
onEnd := frontmatterBlockEnd(lines, onLine, len(getIndentation(lines[onLine])))
40+
onIndent := len(getIndentation(lines[onLine]))
41+
dispatchLine := findFrontmatterKeyLine(lines, "workflow_dispatch", onLine+1, onEnd, onIndent)
42+
if dispatchLine < 0 || dispatchLine >= len(lines) {
43+
return lines, false
44+
}
45+
dispatchEnd := frontmatterBlockEnd(lines, dispatchLine, len(getIndentation(lines[dispatchLine])))
46+
block, modified := removeFieldFromBlock(lines[dispatchLine:dispatchEnd], "aw_context", "inputs")
47+
if !modified {
48+
return lines, false
49+
}
50+
result := make([]string, 0, len(lines)-(dispatchEnd-dispatchLine)+len(block))
51+
result = append(result, lines[:dispatchLine]...)
52+
result = append(result, block...)
53+
result = append(result, lines[dispatchEnd:]...)
54+
return result, true
55+
})
56+
if applied {
57+
workflowDispatchAwContextCodemodLog.Print("Removed reserved on.workflow_dispatch.inputs.aw_context")
58+
}
59+
return newContent, applied, err
60+
},
61+
}
62+
}
63+
64+
func findFrontmatterKeyLine(lines []string, key string, start, end, parentIndent int) int {
65+
childIndent := -1
66+
for i, line := range lines {
67+
if i < start || i >= end {
68+
continue
69+
}
70+
trimmed := strings.TrimSpace(line)
71+
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
72+
continue
73+
}
74+
indent := len(getIndentation(line))
75+
if parentIndent >= 0 {
76+
if indent <= parentIndent {
77+
continue
78+
}
79+
if childIndent < 0 {
80+
childIndent = indent
81+
}
82+
if indent < childIndent {
83+
return -1
84+
}
85+
if indent > childIndent {
86+
continue
87+
}
88+
}
89+
if frontmatterLineKey(line) == key {
90+
return i
91+
}
92+
}
93+
return -1
94+
}
95+
96+
func frontmatterBlockEnd(lines []string, start, parentIndent int) int {
97+
for i, line := range lines {
98+
if i <= start {
99+
continue
100+
}
101+
trimmed := strings.TrimSpace(line)
102+
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
103+
continue
104+
}
105+
if len(getIndentation(line)) <= parentIndent {
106+
return i
107+
}
108+
}
109+
return len(lines)
110+
}
111+
112+
func frontmatterLineKey(line string) string {
113+
trimmed := strings.TrimSpace(line)
114+
key, _, found := strings.Cut(trimmed, ":")
115+
if !found {
116+
return ""
117+
}
118+
return strings.Trim(key, `"'`)
119+
}

0 commit comments

Comments
 (0)