diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 8a2d5167b7..fdb211a12f 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -15,6 +15,7 @@ const { replaceTemporaryIdReferences, isTemporaryId } = require("./temporary_id. const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); const { addExpirationToFooter } = require("./ephemerals.cjs"); const { generateWorkflowIdMarker } = require("./generate_footer.cjs"); +const { parseBoolTemplatable } = require("./templatable.cjs"); const { generateFooterWithMessages } = require("./messages_footer.cjs"); const { normalizeBranchName } = require("./normalize_branch_name.cjs"); @@ -92,7 +93,7 @@ async function main(config = {}) { // Extract configuration const titlePrefix = config.title_prefix || ""; const envLabels = config.labels ? (Array.isArray(config.labels) ? config.labels : config.labels.split(",")).map(label => String(label).trim()).filter(label => label) : []; - const draftDefault = config.draft !== undefined ? config.draft : true; + const draftDefault = parseBoolTemplatable(config.draft, true); const ifNoChanges = config.if_no_changes || "warn"; const allowEmpty = config.allow_empty || false; const autoMerge = config.auto_merge || false; diff --git a/actions/setup/js/templatable.cjs b/actions/setup/js/templatable.cjs new file mode 100644 index 0000000000..4526d288c5 --- /dev/null +++ b/actions/setup/js/templatable.cjs @@ -0,0 +1,38 @@ +// @ts-check + +/** + * Helpers for "templatable" safe-output config fields. + * + * A templatable field is one that: + * - Does NOT affect the generated .lock.yml (no compile-time structural + * impact). + * - Can be supplied as a literal boolean/string value OR as a GitHub + * Actions expression ("${{ inputs.foo }}") that is resolved at runtime + * when the env-var containing the handler-config JSON is expanded. + * + * The Go counterpart lives in pkg/workflow/templatables.go. + */ + +/** + * Parses a templatable boolean config value. + * + * Handles all representations that can arrive in a handler config: + * - `undefined` / `null` → `defaultValue` + * - boolean `true` → `true` + * - boolean `false` → `false` + * - string `"true"` → `true` + * - string `"false"` → `false` + * - any other string (e.g. a resolved GitHub Actions expression value + * that was not "false") → `true` + * + * @param {any} value - The config field value to parse. + * @param {boolean} [defaultValue=true] - Value returned when `value` is + * `undefined` or `null`. + * @returns {boolean} + */ +function parseBoolTemplatable(value, defaultValue = true) { + if (value === undefined || value === null) return defaultValue; + return String(value) !== "false"; +} + +module.exports = { parseBoolTemplatable }; diff --git a/actions/setup/js/templatable.test.cjs b/actions/setup/js/templatable.test.cjs new file mode 100644 index 0000000000..e5ee5e3e75 --- /dev/null +++ b/actions/setup/js/templatable.test.cjs @@ -0,0 +1,47 @@ +import { describe, it, expect } from "vitest"; + +const { parseBoolTemplatable } = require("./templatable.cjs"); + +describe("templatable.cjs", () => { + describe("parseBoolTemplatable", () => { + it("returns defaultValue (true) for undefined", () => { + expect(parseBoolTemplatable(undefined)).toBe(true); + }); + + it("returns defaultValue (true) for null", () => { + expect(parseBoolTemplatable(null)).toBe(true); + }); + + it("respects a custom defaultValue", () => { + expect(parseBoolTemplatable(undefined, false)).toBe(false); + expect(parseBoolTemplatable(null, false)).toBe(false); + }); + + it("handles boolean true", () => { + expect(parseBoolTemplatable(true)).toBe(true); + }); + + it("handles boolean false", () => { + expect(parseBoolTemplatable(false)).toBe(false); + }); + + it('handles string "true"', () => { + expect(parseBoolTemplatable("true")).toBe(true); + }); + + it('handles string "false"', () => { + expect(parseBoolTemplatable("false")).toBe(false); + }); + + it("treats a resolved expression value other than false as truthy", () => { + // GitHub Actions expressions that resolve to something other than "false" + // (e.g. "yes", "1", an empty object representation) should be truthy. + expect(parseBoolTemplatable("yes")).toBe(true); + expect(parseBoolTemplatable("1")).toBe(true); + }); + + it("treats the string false-equivalent as falsy", () => { + expect(parseBoolTemplatable("false", true)).toBe(false); + }); + }); +}); diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 8e996d63f6..e9c1b001ba 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -4935,8 +4935,8 @@ "description": "Optional reviewer(s) to assign to the pull request. Accepts either a single string or an array of usernames. Use 'copilot' to request a code review from GitHub Copilot." }, "draft": { - "type": "boolean", - "description": "Whether to create pull request as draft (defaults to true)", + "allOf": [{ "$ref": "#/$defs/templatable_boolean" }], + "description": "Whether to create pull request as draft (defaults to true). Accepts a boolean or a GitHub Actions expression.", "default": true }, "if-no-changes": { @@ -6877,6 +6877,19 @@ } ], "$defs": { + "templatable_boolean": { + "description": "A boolean value that may also be specified as a GitHub Actions expression string that resolves to a boolean at runtime (e.g. '${{ inputs.my-flag }}').", + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^\\$\\{\\{.*\\}\\}$", + "description": "GitHub Actions expression that resolves to a boolean at runtime" + } + ] + }, "engine_config": { "examples": [ "claude", diff --git a/pkg/workflow/compile_outputs_pr_test.go b/pkg/workflow/compile_outputs_pr_test.go index 14b9e62fa2..e9604d1fd5 100644 --- a/pkg/workflow/compile_outputs_pr_test.go +++ b/pkg/workflow/compile_outputs_pr_test.go @@ -339,6 +339,77 @@ This workflow tests the create_pull_request job generation with draft: true. // t.Logf("Generated workflow content:\n%s", lockContentStr) } +func TestOutputPullRequestDraftExpression(t *testing.T) { + // Create temporary directory for test files + tmpDir := testutil.TempDir(t, "output-pr-draft-expr-test") + + // Test case with create-pull-request configuration with draft as an expression + testContent := `--- +on: + workflow_dispatch: + inputs: + draft-prs: + type: boolean + default: true +permissions: + contents: read + pull-requests: write + issues: read +tools: + github: + allowed: [list_issues] +engine: claude +features: + dangerous-permissions-write: true +strict: false +safe-outputs: + create-pull-request: + title-prefix: "[agent] " + labels: [automation] + draft: ${{ inputs.draft-prs }} +--- + +# Test Output Pull Request with Draft Expression + +This workflow tests the create_pull_request job generation with draft as an expression. +` + + testFile := filepath.Join(tmpDir, "test-output-pr-draft-expr.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + compiler := NewCompiler() + + // Compile the workflow + if err := compiler.CompileWorkflow(testFile); err != nil { + t.Fatalf("Unexpected error compiling workflow with draft expression: %v", err) + } + + // Read the generated lock file + lockFile := stringutil.MarkdownToLockFile(testFile) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read generated lock file: %v", err) + } + + lockContentStr := string(lockContent) + + // Verify GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG is present and contains the draft expression + if !strings.Contains(lockContentStr, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") { + t.Error("Expected handler manager config environment variable") + } + + if !strings.Contains(lockContentStr, "draft") { + t.Error("Expected 'draft' field in handler manager config") + } + + // The expression should be preserved in the handler config + if !strings.Contains(lockContentStr, "inputs.draft-prs") { + t.Error("Expected expression '${{ inputs.draft-prs }}' to be preserved in the handler config") + } +} + func TestCreatePullRequestIfNoChangesConfig(t *testing.T) { // Create temporary directory for test files tmpDir := testutil.TempDir(t, "create-pr-if-no-changes-test") @@ -658,7 +729,7 @@ This workflow tests the create-pull-request with fallback-as-issue disabled. t.Fatal("Expected fallback-as-issue to be set") } - if *workflowData.SafeOutputs.CreatePullRequests.FallbackAsIssue != false { + if *workflowData.SafeOutputs.CreatePullRequests.FallbackAsIssue { t.Error("Expected fallback-as-issue to be false") } diff --git a/pkg/workflow/compiler_safe_outputs_config.go b/pkg/workflow/compiler_safe_outputs_config.go index b7c5066672..e26e0ab731 100644 --- a/pkg/workflow/compiler_safe_outputs_config.go +++ b/pkg/workflow/compiler_safe_outputs_config.go @@ -436,7 +436,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("title_prefix", c.TitlePrefix). AddStringSlice("labels", c.Labels). AddStringSlice("reviewers", c.Reviewers). - AddBoolPtr("draft", c.Draft). + AddTemplatableBool("draft", c.Draft). AddIfNotEmpty("if_no_changes", c.IfNoChanges). AddIfTrue("allow_empty", c.AllowEmpty). AddIfTrue("auto_merge", c.AutoMerge). diff --git a/pkg/workflow/compiler_safe_outputs_config_test.go b/pkg/workflow/compiler_safe_outputs_config_test.go index b08af76611..4b758a0979 100644 --- a/pkg/workflow/compiler_safe_outputs_config_test.go +++ b/pkg/workflow/compiler_safe_outputs_config_test.go @@ -132,7 +132,7 @@ func TestAddHandlerManagerConfigEnvVar(t *testing.T) { }, TitlePrefix: "[PR] ", Labels: []string{"automated"}, - Draft: testBoolPtr(true), + Draft: testStringPtr("true"), IfNoChanges: "skip", AllowEmpty: true, Expires: 7, @@ -153,7 +153,7 @@ func TestAddHandlerManagerConfigEnvVar(t *testing.T) { }, Reviewers: []string{"user1", "user2"}, Labels: []string{"automated"}, - Draft: testBoolPtr(false), + Draft: testStringPtr("false"), }, }, checkContains: []string{ @@ -418,6 +418,7 @@ func TestHandlerConfigBooleanFields(t *testing.T) { safeOutputs *SafeOutputsConfig checkField string checkKey string + expected any // expected value in JSON (bool or string) }{ { name: "hide older comments", @@ -428,6 +429,7 @@ func TestHandlerConfigBooleanFields(t *testing.T) { }, checkField: "add_comment", checkKey: "hide_older_comments", + expected: true, }, { name: "close older discussions", @@ -438,6 +440,7 @@ func TestHandlerConfigBooleanFields(t *testing.T) { }, checkField: "create_discussion", checkKey: "close_older_discussions", + expected: true, }, { name: "allow empty PR", @@ -448,16 +451,18 @@ func TestHandlerConfigBooleanFields(t *testing.T) { }, checkField: "create_pull_request", checkKey: "allow_empty", + expected: true, }, { name: "draft PR", safeOutputs: &SafeOutputsConfig{ CreatePullRequests: &CreatePullRequestsConfig{ - Draft: testBoolPtr(true), + Draft: testStringPtr("true"), }, }, checkField: "create_pull_request", checkKey: "draft", + expected: true, // AddTemplatableBool converts "true" string to JSON boolean }, } @@ -491,7 +496,7 @@ func TestHandlerConfigBooleanFields(t *testing.T) { val, ok := fieldConfig[tt.checkKey] require.True(t, ok, "Expected key: "+tt.checkKey) - assert.Equal(t, true, val) + assert.Equal(t, tt.expected, val) } } } @@ -696,6 +701,11 @@ func testBoolPtr(b bool) *bool { return &b } +// testStringPtr is a helper function for string pointers in config tests +func testStringPtr(s string) *string { + return &s +} + // TestAutoEnabledHandlers tests that missing_tool and missing_data // are automatically enabled even when not explicitly configured. // Note: noop is NOT included here because it is always processed by a dedicated diff --git a/pkg/workflow/create_discussion.go b/pkg/workflow/create_discussion.go index f7c68b6c93..f7a3e2b28c 100644 --- a/pkg/workflow/create_discussion.go +++ b/pkg/workflow/create_discussion.go @@ -22,7 +22,7 @@ type CreateDiscussionsConfig struct { CloseOlderDiscussions bool `yaml:"close-older-discussions,omitempty"` // When true, close older discussions with same title prefix or labels as outdated RequiredCategory string `yaml:"required-category,omitempty"` // Required category for matching when close-older-discussions is enabled Expires int `yaml:"expires,omitempty"` // Hours until the discussion expires and should be automatically closed - FallbackToIssue *bool `yaml:"fallback-to-issue,omitempty"` // When true (default), fallback to create-issue if discussion creation fails due to permissions + FallbackToIssue *bool `yaml:"fallback-to-issue,omitempty"` // When true (default), fallback to create-issue if discussion creation fails due to permissions. Footer *bool `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted but XML markers are kept. } @@ -65,8 +65,8 @@ func (c *Compiler) parseDiscussionsConfig(outputMap map[string]any) *CreateDiscu // Set default fallback-to-issue to true if not specified if config.FallbackToIssue == nil { - trueValue := true - config.FallbackToIssue = &trueValue + trueVal := true + config.FallbackToIssue = &trueVal discussionLog.Print("Using default fallback-to-issue: true") } @@ -140,7 +140,10 @@ func (c *Compiler) buildCreateOutputDiscussionJob(data *WorkflowData, mainJobNam } // Add fallback-to-issue flag - if data.SafeOutputs.CreateDiscussions.FallbackToIssue != nil && *data.SafeOutputs.CreateDiscussions.FallbackToIssue { + ftiVal := data.SafeOutputs.CreateDiscussions.FallbackToIssue + if ftiVal != nil { + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_DISCUSSION_FALLBACK_TO_ISSUE: \"%t\"\n", *ftiVal)) + } else { customEnvVars = append(customEnvVars, " GH_AW_DISCUSSION_FALLBACK_TO_ISSUE: \"true\"\n") } diff --git a/pkg/workflow/create_pull_request.go b/pkg/workflow/create_pull_request.go index 963c8365b1..687c2960eb 100644 --- a/pkg/workflow/create_pull_request.go +++ b/pkg/workflow/create_pull_request.go @@ -2,6 +2,7 @@ package workflow import ( "fmt" + "strings" "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" @@ -9,7 +10,7 @@ import ( var createPRLog = logger.New("workflow:create_pull_request") -// getFallbackAsIssue returns the effective fallback-as-issue setting (defaults to true) +// getFallbackAsIssue returns the effective fallback-as-issue setting (defaults to true). func getFallbackAsIssue(config *CreatePullRequestsConfig) bool { if config == nil || config.FallbackAsIssue == nil { return true // Default @@ -24,7 +25,7 @@ type CreatePullRequestsConfig struct { Labels []string `yaml:"labels,omitempty"` AllowedLabels []string `yaml:"allowed-labels,omitempty"` // Optional list of allowed labels. If omitted, any labels are allowed (including creating new ones). Reviewers []string `yaml:"reviewers,omitempty"` // List of users/bots to assign as reviewers to the pull request - Draft *bool `yaml:"draft,omitempty"` // Pointer to distinguish between unset (nil) and explicitly false + Draft *string `yaml:"draft,omitempty"` // Pointer to distinguish between unset (nil), literal bool, and expression values IfNoChanges string `yaml:"if-no-changes,omitempty"` // Behavior when no changes to push: "warn" (default), "error", or "ignore" AllowEmpty bool `yaml:"allow-empty,omitempty"` // Allow creating PR without patch file or with empty patch (useful for preparing feature branches) TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in format "owner/repo" for cross-repository pull requests @@ -43,7 +44,7 @@ func (c *Compiler) buildCreateOutputPullRequestJob(data *WorkflowData, mainJobNa } if createPRLog.Enabled() { - draftValue := true // Default + draftValue := "true" // Default if data.SafeOutputs.CreatePullRequests.Draft != nil { draftValue = *data.SafeOutputs.CreatePullRequests.Draft } @@ -104,11 +105,17 @@ func (c *Compiler) buildCreateOutputPullRequestJob(data *WorkflowData, mainJobNa customEnvVars = append(customEnvVars, buildLabelsEnvVar("GH_AW_PR_LABELS", data.SafeOutputs.CreatePullRequests.Labels)...) customEnvVars = append(customEnvVars, buildLabelsEnvVar("GH_AW_PR_ALLOWED_LABELS", data.SafeOutputs.CreatePullRequests.AllowedLabels)...) // Pass draft setting - default to true for backwards compatibility - draftValue := true // Default value if data.SafeOutputs.CreatePullRequests.Draft != nil { - draftValue = *data.SafeOutputs.CreatePullRequests.Draft + draftVal := *data.SafeOutputs.CreatePullRequests.Draft + if strings.HasPrefix(draftVal, "${{") { + // Expression value - embed unquoted so GitHub Actions evaluates it + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_PR_DRAFT: %s\n", draftVal)) + } else { + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_PR_DRAFT: %q\n", draftVal)) + } + } else { + customEnvVars = append(customEnvVars, " GH_AW_PR_DRAFT: \"true\"\n") } - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_PR_DRAFT: %q\n", fmt.Sprintf("%t", draftValue))) // Pass the if-no-changes configuration ifNoChanges := data.SafeOutputs.CreatePullRequests.IfNoChanges @@ -124,8 +131,11 @@ func (c *Compiler) buildCreateOutputPullRequestJob(data *WorkflowData, mainJobNa customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_PR_AUTO_MERGE: %q\n", fmt.Sprintf("%t", data.SafeOutputs.CreatePullRequests.AutoMerge))) // Pass the fallback-as-issue configuration - default to true for backwards compatibility - fallbackAsIssue := getFallbackAsIssue(data.SafeOutputs.CreatePullRequests) - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_PR_FALLBACK_AS_ISSUE: %q\n", fmt.Sprintf("%t", fallbackAsIssue))) + if data.SafeOutputs.CreatePullRequests.FallbackAsIssue != nil { + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_PR_FALLBACK_AS_ISSUE: \"%t\"\n", *data.SafeOutputs.CreatePullRequests.FallbackAsIssue)) + } else { + customEnvVars = append(customEnvVars, " GH_AW_PR_FALLBACK_AS_ISSUE: \"true\"\n") + } // Pass the maximum patch size configuration maxPatchSize := 1024 // Default value @@ -186,6 +196,7 @@ func (c *Compiler) buildCreateOutputPullRequestJob(data *WorkflowData, mainJobNa } // Choose permissions based on fallback-as-issue setting + fallbackAsIssue := getFallbackAsIssue(data.SafeOutputs.CreatePullRequests) var permissions *Permissions if fallbackAsIssue { // Default: include issues: write for fallback behavior @@ -253,6 +264,13 @@ func (c *Compiler) parsePullRequestsConfig(outputMap map[string]any) *CreatePull } } + // Pre-process templatable bool fields: convert literal booleans to strings so that + // GitHub Actions expression strings (e.g. "${{ inputs.draft-prs }}") are also accepted. + if err := preprocessBoolFieldAsString(configData, "draft", createPRLog); err != nil { + createPRLog.Printf("Invalid draft value: %v", err) + return nil + } + // Unmarshal into typed config struct var config CreatePullRequestsConfig if err := unmarshalConfig(outputMap, "create-pull-request", &config, createPRLog); err != nil { diff --git a/pkg/workflow/safe_outputs_max_test.go b/pkg/workflow/safe_outputs_max_test.go index a1eac20add..88e3bf0053 100644 --- a/pkg/workflow/safe_outputs_max_test.go +++ b/pkg/workflow/safe_outputs_max_test.go @@ -176,8 +176,8 @@ func TestSafeOutputsMaxConfiguration(t *testing.T) { if len(config.CreatePullRequests.Labels) != 1 || config.CreatePullRequests.Labels[0] != "fix" { t.Errorf("Expected CreatePullRequests.Labels to be ['fix'], got %v", config.CreatePullRequests.Labels) } - if config.CreatePullRequests.Draft == nil || *config.CreatePullRequests.Draft != true { - t.Errorf("Expected CreatePullRequests.Draft to be true, got %v", config.CreatePullRequests.Draft) + if config.CreatePullRequests.Draft == nil || *config.CreatePullRequests.Draft != "true" { + t.Errorf("Expected CreatePullRequests.Draft to be 'true', got %v", config.CreatePullRequests.Draft) } // Check update-issue diff --git a/pkg/workflow/safe_outputs_permissions_test.go b/pkg/workflow/safe_outputs_permissions_test.go index 5d81f4cb80..9ea26f0dc8 100644 --- a/pkg/workflow/safe_outputs_permissions_test.go +++ b/pkg/workflow/safe_outputs_permissions_test.go @@ -191,7 +191,7 @@ func TestComputePermissionsForSafeOutputs(t *testing.T) { safeOutputs: &SafeOutputsConfig{ CreatePullRequests: &CreatePullRequestsConfig{ BaseSafeOutputConfig: BaseSafeOutputConfig{Max: 1}, - FallbackAsIssue: ptrBool(false), + FallbackAsIssue: boolPtr(false), }, }, expected: map[PermissionScope]PermissionLevel{ diff --git a/pkg/workflow/templatables.go b/pkg/workflow/templatables.go new file mode 100644 index 0000000000..104830049b --- /dev/null +++ b/pkg/workflow/templatables.go @@ -0,0 +1,87 @@ +// Package workflow – templatable field helpers +// +// A "templatable" field is a safe-output config field that: +// - Does NOT affect the generated .lock.yml file (i.e. it carries no +// compile-time information that changes the workflow YAML structure). +// - CAN be supplied as a literal value (bool/string/int …) OR as a +// GitHub Actions expression ("${{ inputs.foo }}") that is evaluated at +// runtime when the env var containing the JSON config is expanded. +// +// # Go side +// +// preprocessBoolFieldAsString must be called before YAML unmarshaling so +// that a struct field typed as *string can store both literal booleans +// ("true"/"false") and GitHub Actions expression strings. Free-form +// string literals that are not expressions are rejected with an error. +// +// # JS side +// +// parseBoolTemplatable (in templatable.cjs) is the counterpart used by +// safe-output handlers when reading the JSON config at runtime. + +package workflow + +import ( + "fmt" + "strings" + + "github.com/github/gh-aw/pkg/logger" +) + +// preprocessBoolFieldAsString converts the value of a boolean config field +// to a string before YAML unmarshaling. This lets struct fields typed as +// *string accept both literal boolean values (true/false) and GitHub Actions +// expression strings (e.g. "${{ inputs.draft-prs }}"). +// +// If the value is a bool it is converted to "true" or "false". +// If the value is a string it must be a GitHub Actions expression (starts +// with "${{" and ends with "}}"); any other free-form string is rejected +// and an error is returned. +func preprocessBoolFieldAsString(configData map[string]any, fieldName string, log *logger.Logger) error { + if configData == nil { + return nil + } + if val, exists := configData[fieldName]; exists { + switch v := val.(type) { + case bool: + if v { + configData[fieldName] = "true" + } else { + configData[fieldName] = "false" + } + if log != nil { + log.Printf("Converted %s bool to string before unmarshaling", fieldName) + } + case string: + if !strings.HasPrefix(v, "${{") || !strings.HasSuffix(v, "}}") { + return fmt.Errorf("field %q must be a boolean or a GitHub Actions expression (e.g. '${{ inputs.flag }}'), got string %q", fieldName, v) + } + // expression string is already in the correct form + } + } + return nil +} + +// AddTemplatableBool adds a templatable boolean field to the handler config. +// +// The stored JSON value depends on the content of *value: +// - "true" → JSON boolean true (backward-compatible with existing handlers) +// - "false" → JSON boolean false +// - any other string (GitHub Actions expression) → stored as a JSON string so +// that GitHub Actions can evaluate it at runtime when the env var that +// contains the JSON config is expanded +// - nil → field is omitted +func (b *handlerConfigBuilder) AddTemplatableBool(key string, value *string) *handlerConfigBuilder { + if value == nil { + return b + } + switch *value { + case "true": + b.config[key] = true + case "false": + b.config[key] = false + default: + b.config[key] = *value // expression string – evaluated at runtime + } + return b +} diff --git a/pkg/workflow/tool_description_enhancer.go b/pkg/workflow/tool_description_enhancer.go index 98adcd99aa..32fffb360f 100644 --- a/pkg/workflow/tool_description_enhancer.go +++ b/pkg/workflow/tool_description_enhancer.go @@ -143,7 +143,7 @@ func enhanceToolDescription(toolName, baseDescription string, safeOutputs *SafeO if len(config.AllowedLabels) > 0 { constraints = append(constraints, fmt.Sprintf("Only these labels are allowed: %v.", config.AllowedLabels)) } - if config.Draft != nil && *config.Draft { + if config.Draft != nil && *config.Draft == "true" { constraints = append(constraints, "PRs will be created as drafts.") } if len(config.Reviewers) > 0 {