diff --git a/pkg/cli/codemod_pull_request_target_checkout_false.go b/pkg/cli/codemod_pull_request_target_checkout_false.go index 6975a76b142..331da6551aa 100644 --- a/pkg/cli/codemod_pull_request_target_checkout_false.go +++ b/pkg/cli/codemod_pull_request_target_checkout_false.go @@ -27,6 +27,11 @@ func getPullRequestTargetCheckoutFalseCodemod() Codemod { return content, false, nil } + if isCheckoutMapping(frontmatter) { + pullRequestTargetCheckoutFalseCodemodLog.Print("Skipping pull_request_target checkout codemod: checkout is already a mapping with sub-keys") + return content, false, nil + } + if hasExplicitCheckoutCommands(content) { pullRequestTargetCheckoutFalseCodemodLog.Print("Skipping pull_request_target checkout codemod: explicit checkout command detected") return content, false, nil @@ -81,6 +86,19 @@ func isPullRequestTargetCheckoutDisabled(frontmatter map[string]any) bool { return ok && !checkoutDisabled } +// isCheckoutMapping returns true when the frontmatter "checkout" key is a YAML +// mapping (e.g. sparse-checkout:, fetch-depth:) rather than a scalar or absent. +// Overwriting a mapping with a scalar would orphan the child keys and produce +// invalid YAML, so callers should skip the codemod in this case. +func isCheckoutMapping(frontmatter map[string]any) bool { + checkoutAny, hasCheckout := frontmatter["checkout"] + if !hasCheckout { + return false + } + _, isMap := checkoutAny.(map[string]any) + return isMap +} + func hasExplicitCheckoutCommands(content string) bool { lowerContent := strings.ToLower(content) @@ -113,6 +131,22 @@ func ensureCheckoutFalseForPullRequestTarget(lines []string) ([]string, bool) { } if strings.HasPrefix(trimmed, "checkout:") { + // If the checkout line has no inline value, the next indented line + // would make this a YAML mapping block. Replacing it with a scalar + // would orphan those child keys and produce invalid YAML. + inlineValue := strings.TrimSpace(strings.TrimPrefix(trimmed, "checkout:")) + if inlineValue == "" { + for j := i + 1; j < len(lines); j++ { + next := lines[j] + if strings.TrimSpace(next) == "" { + continue + } + if len(next) > 0 && (next[0] == ' ' || next[0] == '\t') { + return lines, false + } + break + } + } checkoutLine, modified := normalizeCheckoutFalseLine(line) if !modified { return lines, false diff --git a/pkg/cli/codemod_pull_request_target_checkout_false_test.go b/pkg/cli/codemod_pull_request_target_checkout_false_test.go index 804cf68cc01..c904d1e5726 100644 --- a/pkg/cli/codemod_pull_request_target_checkout_false_test.go +++ b/pkg/cli/codemod_pull_request_target_checkout_false_test.go @@ -167,4 +167,30 @@ strict: false assert.False(t, applied, "codemod should not apply when strict is false") assert.Equal(t, content, result, "content should remain unchanged") }) + + t.Run("does not modify when checkout is a mapping with sub-keys", func(t *testing.T) { + content := `--- +description: "Review Azure SDK management-plane PRs" +on: + pull_request_target: +checkout: + sparse-checkout: | + .github +inlined-imports: true +--- +` + frontmatter := map[string]any{ + "on": map[string]any{ + "pull_request_target": map[string]any{}, + }, + "checkout": map[string]any{ + "sparse-checkout": ".github\n", + }, + } + + result, applied, err := codemod.Apply(content, frontmatter) + require.NoError(t, err, "codemod should not return an error") + assert.False(t, applied, "codemod should not apply when checkout is a mapping") + assert.Equal(t, content, result, "content should remain unchanged and not corrupt the mapping") + }) }