Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions pkg/cli/codemod_install_script_url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package cli

import (
"strings"

"github.com/github/gh-aw/pkg/logger"
)

var installScriptURLCodemodLog = logger.New("cli:codemod_install_script_url")

// getInstallScriptURLCodemod creates a codemod for migrating githubnext/gh-aw to github/gh-aw in install script URLs
func getInstallScriptURLCodemod() Codemod {
return Codemod{
ID: "install-script-url-migration",
Name: "Migrate install script URL from githubnext/gh-aw to github/gh-aw",
Description: "Updates install script URLs in job steps from the older githubnext/gh-aw location to the new github/gh-aw location",
IntroducedIn: "0.9.0",
Apply: func(content string, frontmatter map[string]any) (string, bool, error) {
// Parse frontmatter to get raw lines
frontmatterLines, markdown, err := parseFrontmatterLines(content)
if err != nil {
return content, false, err
}

// Define patterns to search and replace
// Order matters: Check URL patterns first (with slash), then general patterns
oldPatterns := []string{
"https://raw.githubusercontent.com/githubnext/gh-aw/",
"githubnext/gh-aw",
}

newReplacements := []string{
"https://raw.githubusercontent.com/github/gh-aw/",
"github/gh-aw",
}

modified := false
result := make([]string, len(frontmatterLines))

for i, line := range frontmatterLines {
modifiedLine := line

// Try to replace each old pattern with the new one in all lines
for j, oldPattern := range oldPatterns {
if strings.Contains(modifiedLine, oldPattern) {
modifiedLine = strings.ReplaceAll(modifiedLine, oldPattern, newReplacements[j])
modified = true
installScriptURLCodemodLog.Printf("Replaced '%s' with '%s' on line %d", oldPattern, newReplacements[j], i+1)
}
}

result[i] = modifiedLine
}

if !modified {
return content, false, nil
}

// Reconstruct the content
newContent := reconstructContent(result, markdown)
installScriptURLCodemodLog.Print("Applied install script URL migration")
return newContent, true, nil
},
}
}
286 changes: 286 additions & 0 deletions pkg/cli/codemod_install_script_url_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
//go:build !integration

package cli

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetInstallScriptURLCodemod(t *testing.T) {
codemod := getInstallScriptURLCodemod()

// Verify codemod metadata
assert.Equal(t, "install-script-url-migration", codemod.ID, "Codemod ID should match")
assert.Equal(t, "Migrate install script URL from githubnext/gh-aw to github/gh-aw", codemod.Name, "Codemod name should match")
assert.NotEmpty(t, codemod.Description, "Codemod should have a description")
assert.Equal(t, "0.9.0", codemod.IntroducedIn, "Codemod version should match")
require.NotNil(t, codemod.Apply, "Codemod should have an Apply function")
}

func TestInstallScriptURLCodemod_RawGitHubUserContent(t *testing.T) {
codemod := getInstallScriptURLCodemod()

content := `---
on: workflow_dispatch
jobs:
setup:
runs-on: ubuntu-latest
steps:
- name: Install gh-aw
run: curl -fsSL https://raw.githubusercontent.com/githubnext/gh-aw/main/install-gh-aw.sh | bash
---

# Test Workflow`

frontmatter := map[string]any{
"on": "workflow_dispatch",
"jobs": map[string]any{
"setup": map[string]any{
"runs-on": "ubuntu-latest",
"steps": []any{
map[string]any{
"name": "Install gh-aw",
"run": "curl -fsSL https://raw.githubusercontent.com/githubnext/gh-aw/main/install-gh-aw.sh | bash",
},
},
},
},
}

result, applied, err := codemod.Apply(content, frontmatter)

require.NoError(t, err, "Apply should not return an error")
assert.True(t, applied, "Codemod should report changes")
assert.Contains(t, result, "https://raw.githubusercontent.com/github/gh-aw/main/install-gh-aw.sh", "Result should contain updated URL")
assert.NotContains(t, result, "githubnext/gh-aw", "Result should not contain old URL")
}

func TestInstallScriptURLCodemod_RefsHeadsMain(t *testing.T) {
codemod := getInstallScriptURLCodemod()

content := `---
on: workflow_dispatch
jobs:
setup:
steps:
- name: Install gh-aw extension
run: curl -fsSL https://raw.githubusercontent.com/githubnext/gh-aw/refs/heads/main/install-gh-aw.sh | bash
---

# Test Workflow`

frontmatter := map[string]any{
"on": "workflow_dispatch",
}

result, applied, err := codemod.Apply(content, frontmatter)

require.NoError(t, err, "Apply should not return an error")
assert.True(t, applied, "Codemod should report changes")
assert.Contains(t, result, "https://raw.githubusercontent.com/github/gh-aw/refs/heads/main/install-gh-aw.sh", "Result should contain updated URL")
assert.NotContains(t, result, "githubnext/gh-aw", "Result should not contain old URL")
}

func TestInstallScriptURLCodemod_ShortForm(t *testing.T) {
codemod := getInstallScriptURLCodemod()

content := `---
on: workflow_dispatch
jobs:
setup:
steps:
- name: Install extension
run: gh extension install githubnext/gh-aw
---

# Test Workflow`

frontmatter := map[string]any{
"on": "workflow_dispatch",
}

result, applied, err := codemod.Apply(content, frontmatter)

require.NoError(t, err, "Apply should not return an error")
assert.True(t, applied, "Codemod should report changes")
assert.Contains(t, result, "github/gh-aw", "Result should contain updated repo")
assert.NotContains(t, result, "githubnext/gh-aw", "Result should not contain old repo")
}

func TestInstallScriptURLCodemod_AlreadyMigrated(t *testing.T) {
codemod := getInstallScriptURLCodemod()

content := `---
on: workflow_dispatch
jobs:
setup:
steps:
- name: Install gh-aw
run: curl -fsSL https://raw.githubusercontent.com/github/gh-aw/main/install-gh-aw.sh | bash
---

# Test Workflow`

frontmatter := map[string]any{
"on": "workflow_dispatch",
}

result, applied, err := codemod.Apply(content, frontmatter)

require.NoError(t, err, "Apply should not return an error")
assert.False(t, applied, "Codemod should not report changes when already migrated")
assert.Equal(t, content, result, "Content should remain unchanged")
}

func TestInstallScriptURLCodemod_NoInstallScript(t *testing.T) {
codemod := getInstallScriptURLCodemod()

content := `---
on: workflow_dispatch
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Run tests
run: npm test
---

# Test Workflow`

frontmatter := map[string]any{
"on": "workflow_dispatch",
}

result, applied, err := codemod.Apply(content, frontmatter)

require.NoError(t, err, "Apply should not return an error")
assert.False(t, applied, "Codemod should not report changes when no install script found")
assert.Equal(t, content, result, "Content should remain unchanged")
}

func TestInstallScriptURLCodemod_MultipleOccurrences(t *testing.T) {
codemod := getInstallScriptURLCodemod()

content := `---
on: workflow_dispatch
jobs:
setup:
steps:
- name: Install gh-aw
run: curl -fsSL https://raw.githubusercontent.com/githubnext/gh-aw/main/install-gh-aw.sh | bash
- name: Install extension
run: gh extension install githubnext/gh-aw
---

# Test Workflow`

frontmatter := map[string]any{
"on": "workflow_dispatch",
}

result, applied, err := codemod.Apply(content, frontmatter)

require.NoError(t, err, "Apply should not return an error")
assert.True(t, applied, "Codemod should report changes")
assert.Contains(t, result, "https://raw.githubusercontent.com/github/gh-aw/main/install-gh-aw.sh", "Result should contain updated URL")
assert.Contains(t, result, "gh extension install github/gh-aw", "Result should contain updated repo")
assert.NotContains(t, result, "githubnext/gh-aw", "Result should not contain old references")
}

func TestInstallScriptURLCodemod_PreservesMarkdown(t *testing.T) {
codemod := getInstallScriptURLCodemod()

content := `---
on: workflow_dispatch
jobs:
setup:
steps:
- name: Install gh-aw
run: curl -fsSL https://raw.githubusercontent.com/githubnext/gh-aw/main/install-gh-aw.sh | bash
---

# Test Workflow

This workflow installs gh-aw from githubnext.

## Steps
- Download install script
- Run install script

` + "```bash" + `
curl -fsSL https://example.com/script.sh | bash
` + "```"

frontmatter := map[string]any{
"on": "workflow_dispatch",
}

result, applied, err := codemod.Apply(content, frontmatter)

require.NoError(t, err, "Apply should not return an error")
assert.True(t, applied, "Codemod should report changes")
assert.Contains(t, result, "# Test Workflow", "Result should preserve markdown")
assert.Contains(t, result, "## Steps", "Result should preserve markdown sections")
assert.Contains(t, result, "```bash", "Result should preserve code blocks")
assert.Contains(t, result, "This workflow installs gh-aw from githubnext", "Result should preserve markdown text")
}

func TestInstallScriptURLCodemod_PreservesIndentation(t *testing.T) {
codemod := getInstallScriptURLCodemod()

content := `---
on: workflow_dispatch
jobs:
setup:
runs-on: ubuntu-latest
steps:
- name: Install gh-aw
run: curl -fsSL https://raw.githubusercontent.com/githubnext/gh-aw/main/install-gh-aw.sh | bash
---

# Test`

frontmatter := map[string]any{
"on": "workflow_dispatch",
}

result, applied, err := codemod.Apply(content, frontmatter)

require.NoError(t, err, "Apply should not return an error")
assert.True(t, applied, "Codemod should report changes")
// Check that indentation is preserved
assert.Contains(t, result, " - name: Install gh-aw", "Result should preserve indentation")
assert.Contains(t, result, " run: curl", "Result should preserve indentation")
}

func TestInstallScriptURLCodemod_DifferentBranches(t *testing.T) {
codemod := getInstallScriptURLCodemod()

content := `---
on: workflow_dispatch
jobs:
setup:
steps:
- name: Install from develop
run: curl -fsSL https://raw.githubusercontent.com/githubnext/gh-aw/develop/install-gh-aw.sh | bash
- name: Install from specific tag
run: curl -fsSL https://raw.githubusercontent.com/githubnext/gh-aw/v1.0.0/install-gh-aw.sh | bash
---

# Test`

frontmatter := map[string]any{
"on": "workflow_dispatch",
}

result, applied, err := codemod.Apply(content, frontmatter)

require.NoError(t, err, "Apply should not return an error")
assert.True(t, applied, "Codemod should report changes")
assert.Contains(t, result, "https://raw.githubusercontent.com/github/gh-aw/develop/install-gh-aw.sh", "Result should update develop branch")
assert.Contains(t, result, "https://raw.githubusercontent.com/github/gh-aw/v1.0.0/install-gh-aw.sh", "Result should update tag reference")
assert.NotContains(t, result, "githubnext/gh-aw", "Result should not contain old references")
}
1 change: 1 addition & 0 deletions pkg/cli/fix_codemods.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@ func GetAllCodemods() []Codemod {
getMCPNetworkMigrationCodemod(),
getDiscussionFlagRemovalCodemod(),
getMCPModeToTypeCodemod(),
getInstallScriptURLCodemod(),
}
}
3 changes: 2 additions & 1 deletion pkg/cli/fix_codemods_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func TestGetAllCodemods_ReturnsAllCodemods(t *testing.T) {
codemods := GetAllCodemods()

// Verify we have the expected number of codemods
expectedCount := 15
expectedCount := 16
assert.Len(t, codemods, expectedCount, "Should return all %d codemods", expectedCount)

// Verify all codemods have required fields
Expand Down Expand Up @@ -119,6 +119,7 @@ func TestGetAllCodemods_InExpectedOrder(t *testing.T) {
"mcp-network-to-top-level-migration",
"add-comment-discussion-removal",
"mcp-mode-to-type-migration",
"install-script-url-migration",
}

require.Len(t, codemods, len(expectedOrder), "Should have expected number of codemods")
Expand Down
Loading
Loading