Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 20, 2025

When workflows define checkout in custom steps, runtime setup steps (e.g., Setup Node.js, Setup Python) were inserted before the checkout instead of after it.

Changes

Step generation logic (pkg/workflow/compiler_yaml.go)

  • Detect when custom steps contain checkout action
  • Insert runtime setup steps after first checkout in custom steps
  • Add addCustomStepsWithRuntimeInsertion() to parse custom steps and inject runtime steps at correct position
  • Add addCustomStepsAsIs() for simple passthrough case

Test coverage (pkg/workflow/checkout_runtime_order_test.go)

  • TestCheckoutRuntimeOrderInCustomSteps: Verifies runtime steps inserted after custom checkout
  • TestCheckoutFirstWhenNoCustomSteps: Verifies automatic checkout remains first when no custom steps

Example

Before:

- name: Setup Node.js        # ❌ Runtime before checkout
- name: Checkout code
- name: Run Node script

After:

- name: Checkout code        # ✅ Checkout first
- name: Setup Node.js        # ✅ Runtime after checkout
- name: Run Node script

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:

  • https://api.github.com/repos/githubnext/gh-aw/actions/runs/19531377115/jobs
    • Triggering command: curl -s -H Authorization: token REDACTED (http block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

A checkout step must always be placed as the first step. The runtime step should be placed afterwards.

See this run that failed https://github.com/githubnext/gh-aw/actions/runs/19531377115/job/55914978823


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

When workflows have custom steps containing a checkout action, the runtime setup steps are now correctly inserted AFTER the checkout step instead of before it. This ensures checkout is always the first step as required.

- Modified generateMainJobSteps to detect checkout in custom steps
- Added helper functions to insert runtime steps after checkout
- Added tests to verify correct step ordering
- All existing tests pass

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix order of checkout and runtime steps Fix runtime setup steps inserted before checkout in custom steps Nov 20, 2025
Copilot finished work on behalf of pelikhan November 20, 2025 15:19
Copilot AI requested a review from pelikhan November 20, 2025 15:19
@pelikhan pelikhan marked this pull request as ready for review November 20, 2025 15:32
Copilot AI review requested due to automatic review settings November 20, 2025 15:32
Copilot finished reviewing on behalf of pelikhan November 20, 2025 15:35
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes a bug where runtime setup steps (e.g., Setup Node.js, Setup Python) were incorrectly inserted before checkout steps when workflows define checkout in custom steps, violating the requirement that checkout must always be the first step.

Key Changes

  • Added decision logic to detect when custom steps contain checkout and insert runtime setup steps after the first checkout step instead of before it
  • Refactored custom step insertion into two functions: addCustomStepsAsIs for simple cases and addCustomStepsWithRuntimeInsertion for cases requiring runtime step injection
  • Added comprehensive test coverage with two test cases verifying correct step ordering in both custom-checkout and automatic-checkout scenarios

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
pkg/workflow/compiler_yaml.go Implements the core fix with decision logic for runtime step placement and new functions to handle custom step insertion with or without runtime injection
pkg/workflow/checkout_runtime_order_test.go Adds test coverage for checkout-before-runtime ordering in custom steps and automatic checkout scenarios
.github/workflows/unbloat-docs.lock.yml Demonstrates the fix working correctly by showing checkout now appears before runtime setup steps

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +210 to +238
// Extract the agent job section
agentJobStart := strings.Index(lockStr, " agent:")
if agentJobStart == -1 {
t.Fatal("Could not find agent job in compiled workflow")
}

// Find the next job (starts with " " followed by a non-space character, e.g., " activation:")
// We need to skip the agent job content which has more indentation
remainingContent := lockStr[agentJobStart+10:]
nextJobStart := -1
lines := strings.Split(remainingContent, "\n")
for i, line := range lines {
// A new job starts with exactly 2 spaces followed by a letter/number (not more spaces)
if len(line) > 2 && line[0] == ' ' && line[1] == ' ' && line[2] != ' ' && line[2] != '\t' {
// Calculate the position in the original string
nextJobStart = 0
for j := 0; j < i; j++ {
nextJobStart += len(lines[j]) + 1 // +1 for newline
}
break
}
}

var agentJobSection string
if nextJobStart == -1 {
agentJobSection = lockStr[agentJobStart:]
} else {
agentJobSection = lockStr[agentJobStart : agentJobStart+10+nextJobStart]
}
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Significant code duplication: The agent job section extraction logic (lines 210-238) is identical to lines 66-94 in the first test function. Consider extracting this into a helper function:

func extractAgentJobSection(t *testing.T, lockContent string) string {
    agentJobStart := strings.Index(lockContent, "  agent:")
    if agentJobStart == -1 {
        t.Fatal("Could not find agent job in compiled workflow")
    }
    
    remainingContent := lockContent[agentJobStart+10:]
    nextJobStart := -1
    lines := strings.Split(remainingContent, "\n")
    for i, line := range lines {
        if len(line) > 2 && line[0] == ' ' && line[1] == ' ' && line[2] != ' ' && line[2] != '\t' {
            nextJobStart = 0
            for j := 0; j < i; j++ {
                nextJobStart += len(lines[j]) + 1
            }
            break
        }
    }
    
    if nextJobStart == -1 {
        return lockContent[agentJobStart:]
    }
    return lockContent[agentJobStart : agentJobStart+10+nextJobStart]
}

This would improve maintainability and reduce the chance of bugs from inconsistent updates.

Copilot uses AI. Check for mistakes.
Comment on lines +240 to +253
// Find all step names in order
stepNames := []string{}
stepLines := strings.Split(agentJobSection, "\n")
for _, line := range stepLines {
// Check if line contains "- name:" (with any amount of leading whitespace)
if strings.Contains(line, "- name:") {
// Extract the name part after "- name:"
parts := strings.SplitN(line, "- name:", 2)
if len(parts) == 2 {
name := strings.TrimSpace(parts[1])
stepNames = append(stepNames, name)
}
}
}
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code duplication: The step name extraction logic (lines 240-253) is identical to lines 100-113 in the first test function. Consider extracting this into a helper function:

func extractStepNames(agentJobSection string) []string {
    stepNames := []string{}
    stepLines := strings.Split(agentJobSection, "\n")
    for _, line := range stepLines {
        if strings.Contains(line, "- name:") {
            parts := strings.SplitN(line, "- name:", 2)
            if len(parts) == 2 {
                name := strings.TrimSpace(parts[1])
                stepNames = append(stepNames, name)
            }
        }
    }
    return stepNames
}

This would reduce code duplication and improve maintainability.

Copilot uses AI. Check for mistakes.
Comment on lines +177 to +200
// Create temporary directory for test
tempDir, err := os.MkdirTemp("", "checkout-first-test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)

// Create workflows directory
workflowsDir := filepath.Join(tempDir, constants.GetWorkflowDir())
if err := os.MkdirAll(workflowsDir, 0755); err != nil {
t.Fatalf("Failed to create workflows directory: %v", err)
}

// Write test workflow file
workflowPath := filepath.Join(workflowsDir, "test-workflow.md")
if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil {
t.Fatalf("Failed to write workflow file: %v", err)
}

// Compile workflow
compiler := NewCompiler(false, "", "test-version")
if err := compiler.CompileWorkflow(workflowPath); err != nil {
t.Fatalf("Failed to compile workflow: %v", err)
}
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code duplication: The test setup logic (lines 177-200) is nearly identical to lines 33-56 in the first test function, with only the temp directory name and workflow content differing. Consider extracting this into a helper function:

func setupWorkflowTest(t *testing.T, testName, workflowContent string) (string, string) {
    tempDir, err := os.MkdirTemp("", testName)
    if err != nil {
        t.Fatalf("Failed to create temp dir: %v", err)
    }
    
    workflowsDir := filepath.Join(tempDir, constants.GetWorkflowDir())
    if err := os.MkdirAll(workflowsDir, 0755); err != nil {
        t.Fatalf("Failed to create workflows directory: %v", err)
    }
    
    workflowPath := filepath.Join(workflowsDir, "test-workflow.md")
    if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil {
        t.Fatalf("Failed to write workflow file: %v", err)
    }
    
    compiler := NewCompiler(false, "", "test-version")
    if err := compiler.CompileWorkflow(workflowPath); err != nil {
        t.Fatalf("Failed to compile workflow: %v", err)
    }
    
    lockPath := filepath.Join(workflowsDir, "test-workflow.lock.yml")
    return tempDir, lockPath
}

This would significantly reduce duplication and make the tests more concise.

Copilot uses AI. Check for mistakes.
Comment on lines +1083 to +1096
// Look ahead to find "uses:" line with "checkout"
for j := i + 1; j < len(lines); j++ {
nextLine := lines[j]
nextTrimmed := strings.TrimSpace(nextLine)

// Stop if we hit the next step
if strings.HasPrefix(nextTrimmed, "- name:") || strings.HasPrefix(nextTrimmed, "- uses:") {
break
}

// Check if this is a uses line with checkout
if strings.Contains(nextTrimmed, "uses:") && strings.Contains(nextTrimmed, "checkout") {
isCheckoutStep = true
break
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checkout detection logic may fail when the step uses the compact format - uses: actions/checkout@v5. The look-ahead loop (lines 1084-1098) searches for a uses: line with "checkout" starting from i + 1, but when the step starts with - uses: (line i), the uses: is on the current line that was already processed and written to output at line 1073.

This means:

  1. If the step is - name: Checkout\n uses: actions/checkout@v5 - works correctly (uses: is on the next line)
  2. If the step is - uses: actions/checkout@v5 - fails to detect checkout (uses: is on current line, already written)

Suggested fix: Check the current line for checkout before the look-ahead:

// Check if this line itself contains uses: and checkout (compact format)
if strings.Contains(trimmed, "uses:") && strings.Contains(trimmed, "checkout") {
    isCheckoutStep = true
} else {
    // Look ahead to find "uses:" line with "checkout"
    for j := i + 1; j < len(lines); j++ {
        // ... existing look-ahead logic
    }
}
Suggested change
// Look ahead to find "uses:" line with "checkout"
for j := i + 1; j < len(lines); j++ {
nextLine := lines[j]
nextTrimmed := strings.TrimSpace(nextLine)
// Stop if we hit the next step
if strings.HasPrefix(nextTrimmed, "- name:") || strings.HasPrefix(nextTrimmed, "- uses:") {
break
}
// Check if this is a uses line with checkout
if strings.Contains(nextTrimmed, "uses:") && strings.Contains(nextTrimmed, "checkout") {
isCheckoutStep = true
break
// Check if this line itself contains uses: and checkout (compact format)
if strings.Contains(trimmed, "uses:") && strings.Contains(trimmed, "checkout") {
isCheckoutStep = true
} else {
// Look ahead to find "uses:" line with "checkout"
for j := i + 1; j < len(lines); j++ {
nextLine := lines[j]
nextTrimmed := strings.TrimSpace(nextLine)
// Stop if we hit the next step
if strings.HasPrefix(nextTrimmed, "- name:") || strings.HasPrefix(nextTrimmed, "- uses:") {
break
}
// Check if this is a uses line with checkout
if strings.Contains(nextTrimmed, "uses:") && strings.Contains(nextTrimmed, "checkout") {
isCheckoutStep = true
break
}

Copilot uses AI. Check for mistakes.
@pelikhan pelikhan merged commit ee4a08f into main Nov 20, 2025
122 of 123 checks passed
@pelikhan pelikhan deleted the copilot/fix-checkout-step-order branch November 20, 2025 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants