Skip to content
Closed
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
35 changes: 35 additions & 0 deletions .github/workflows/pr-sous-chef.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 56 additions & 0 deletions pkg/workflow/compiler_custom_actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,62 @@ func TestCheckoutActionsFolderDevModeAlwaysEmitsCheckout(t *testing.T) {
}
}

func TestCheckoutActionsFolderWithRetryDevMode(t *testing.T) {
compiler := NewCompiler(WithVersion("dev"))
compiler.SetActionMode(ActionModeDev)

lines := compiler.generateCheckoutActionsFolderWithRetry(nil)
combined := strings.Join(lines, "")

if !strings.Contains(combined, "id: checkout-actions-folder-attempt-1") {
t.Error("Expected first checkout attempt id in retry checkout step")
}
if !strings.Contains(combined, "id: checkout-actions-folder-attempt-2") {
t.Error("Expected second checkout attempt id in retry checkout step")
}
if !strings.Contains(combined, "id: checkout-actions-folder-attempt-3") {
t.Error("Expected third checkout attempt id in retry checkout step")
}
if !strings.Contains(combined, "Backoff before retrying checkout actions folder (5s)") {
t.Error("Expected 5s backoff step in retry checkout flow")
}
if !strings.Contains(combined, "Backoff before retrying checkout actions folder (15s)") {
t.Error("Expected 15s backoff step in retry checkout flow")
}
if !strings.Contains(combined, "Fail with clear checkout error after retries") {
t.Error("Expected explicit final checkout failure step")
}
if !strings.Contains(combined, "continue-on-error: true") {
t.Error("Expected checkout attempts to use continue-on-error: true")
}
if !strings.Contains(combined, "Failed to checkout github/gh-aw actions folder after 3 attempts") {
t.Error("Expected clear checkout failure error message after retries")
}
}

func TestConclusionSetupUsesRetryingCheckoutOnlyForPrSousChef(t *testing.T) {
compiler := NewCompiler(WithVersion("dev"))
compiler.SetActionMode(ActionModeDev)

sousChefSteps := strings.Join(compiler.buildConclusionSetupSteps(&WorkflowData{
Name: "PR Sous Chef",
WorkflowID: "pr-sous-chef",
SafeOutputs: &SafeOutputsConfig{},
}), "")
if !strings.Contains(sousChefSteps, "checkout-actions-folder-attempt-1") {
t.Error("Expected PR Sous Chef conclusion setup to include checkout retry steps")
}

otherSteps := strings.Join(compiler.buildConclusionSetupSteps(&WorkflowData{
Name: "Other Workflow",
WorkflowID: "other-workflow",
SafeOutputs: &SafeOutputsConfig{},
}), "")
if strings.Contains(otherSteps, "checkout-actions-folder-attempt-1") {
t.Error("Did not expect non-PR-Sous-Chef conclusion setup to include checkout retry steps")
}
}

// TestResolveSetupActionReferenceActionMode tests that action mode resolves to the external gh-aw-actions repo
func TestResolveSetupActionReferenceActionMode(t *testing.T) {
ref := ResolveSetupActionReference(context.Background(), ActionModeAction, "v1.2.3", "", nil)
Expand Down
49 changes: 49 additions & 0 deletions pkg/workflow/compiler_yaml_step_generation.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,55 @@ func (c *Compiler) generateCheckoutActionsFolder(data *WorkflowData) []string {
return nil
}

// generateCheckoutActionsFolderWithRetry generates checkout steps with retry/backoff.
// It is intended for failure-sensitive paths (for example conclusion jobs) where a
// transient checkout error should not cascade into misleading downstream failures.
func (c *Compiler) generateCheckoutActionsFolderWithRetry(data *WorkflowData) []string {
base := c.generateCheckoutActionsFolder(data)
if len(base) == 0 {
return nil
}
if len(base) < 2 {
return base
}

baseBody := base[1:] // uses + with + options
buildAttempt := func(name, id, ifExpr string) []string {
lines := []string{
fmt.Sprintf(" - name: %s\n", name),
fmt.Sprintf(" id: %s\n", id),
}
if ifExpr != "" {
lines = append(lines, fmt.Sprintf(" if: %s\n", ifExpr))
}
lines = append(lines, " continue-on-error: true\n")
lines = append(lines, baseBody...)
return lines
}

lines := buildAttempt("Checkout actions folder", "checkout-actions-folder-attempt-1", "")
lines = append(lines,
" - name: Backoff before retrying checkout actions folder (5s)\n",
" if: steps.checkout-actions-folder-attempt-1.outcome == 'failure'\n",
" run: sleep 5\n",
)
lines = append(lines, buildAttempt("Checkout actions folder (retry 1)", "checkout-actions-folder-attempt-2", "steps.checkout-actions-folder-attempt-1.outcome == 'failure'")...)
lines = append(lines,
" - name: Backoff before retrying checkout actions folder (15s)\n",
" if: steps.checkout-actions-folder-attempt-2.outcome == 'failure'\n",
" run: sleep 15\n",
)
lines = append(lines, buildAttempt("Checkout actions folder (retry 2)", "checkout-actions-folder-attempt-3", "steps.checkout-actions-folder-attempt-2.outcome == 'failure'")...)
lines = append(lines,
" - name: Fail with clear checkout error after retries\n",
" if: steps.checkout-actions-folder-attempt-3.outcome == 'failure'\n",
" run: |\n",
" echo \"::error::Failed to checkout github/gh-aw actions folder after 3 attempts (5s/15s backoff). This is usually a transient GitHub DNS or network failure.\"\n",
" exit 1\n",
)
return lines
}

// generateRestoreActionsSetupStep generates a single "Restore actions folder" step that
// re-checks out only the actions/setup subfolder from github/gh-aw. This is used in dev mode
// after a job step has checked out a different repository (or a different git branch) and
Expand Down
10 changes: 8 additions & 2 deletions pkg/workflow/notify_comment_conclusion_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,14 @@ func (c *Compiler) buildConclusionSetupSteps(data *WorkflowData) []string {
// Add setup step to copy scripts
setupActionRef := c.resolveActionReference("./actions/setup", data)
if setupActionRef != "" || c.actionMode.IsScript() {
// For dev mode (local action path), checkout the actions folder first
steps = append(steps, c.generateCheckoutActionsFolder(data)...)
// For dev mode (local action path), checkout the actions folder first.
// PR Sous Chef conclusion has regressed on transient checkout failures:
// use retries here so they do not cascade into MODULE_NOT_FOUND errors.
if data != nil && data.WorkflowID == "pr-sous-chef" {
steps = append(steps, c.generateCheckoutActionsFolderWithRetry(data)...)
} else {
steps = append(steps, c.generateCheckoutActionsFolder(data)...)
}

// Notify comment job doesn't need project support
// Conclusion/notify job depends on activation, reuse its trace ID
Expand Down