diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml
index d6ea4e3142f..fb168fe885b 100644
--- a/.github/workflows/dev.lock.yml
+++ b/.github/workflows/dev.lock.yml
@@ -7316,6 +7316,7 @@ jobs:
GH_AW_WORKFLOW_NAME: "Dev"
GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.result }}
+ GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e π Poetry generated by [{workflow_name}]({run_url})\",\"footerInstall\":\"\\u003e Want to add poems to your discussions? Install with `gh aw add {workflow_source}`\"}"
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
@@ -7858,103 +7859,6 @@ jobs:
shell: bash
run: |
mkdir -p /tmp/gh-aw/scripts
- cat > /tmp/gh-aw/scripts/generate_footer.cjs << 'EOF_88f9d2d4'
- // @ts-check
- ///
-
- /**
- * Generates an XML comment marker with agentic workflow metadata for traceability.
- * This marker enables searching and tracing back items generated by an agentic workflow.
- *
- * Note: This function is duplicated in messages_footer.cjs. While normally we would
- * consolidate to a shared module, importing messages_footer.cjs here would cause the
- * bundler to inline messages_core.cjs which contains 'GH_AW_SAFE_OUTPUT_MESSAGES:' in
- * a warning message, breaking tests that check for env var declarations.
- *
- * @param {string} workflowName - Name of the workflow
- * @param {string} runUrl - URL of the workflow run
- * @returns {string} XML comment marker with workflow metadata
- */
- function generateXMLMarker(workflowName, runUrl) {
- // Read engine metadata from environment variables
- const engineId = process.env.GH_AW_ENGINE_ID || "";
- const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
- const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
- const trackerId = process.env.GH_AW_TRACKER_ID || "";
-
- // Build the key-value pairs for the marker
- const parts = [];
-
- // Always include agentic-workflow name
- parts.push(`agentic-workflow: ${workflowName}`);
-
- // Add tracker-id if available (for searchability and tracing)
- if (trackerId) {
- parts.push(`tracker-id: ${trackerId}`);
- }
-
- // Add engine ID if available
- if (engineId) {
- parts.push(`engine: ${engineId}`);
- }
-
- // Add version if available
- if (engineVersion) {
- parts.push(`version: ${engineVersion}`);
- }
-
- // Add model if available
- if (engineModel) {
- parts.push(`model: ${engineModel}`);
- }
-
- // Always include run URL
- parts.push(`run: ${runUrl}`);
-
- // Return the XML comment marker
- return ``;
- }
-
- /**
- * Generate footer with AI attribution and workflow installation instructions
- * @param {string} workflowName - Name of the workflow
- * @param {string} runUrl - URL of the workflow run
- * @param {string} workflowSource - Source of the workflow (owner/repo/path@ref)
- * @param {string} workflowSourceURL - GitHub URL for the workflow source
- * @param {number|undefined} triggeringIssueNumber - Issue number that triggered this workflow
- * @param {number|undefined} triggeringPRNumber - Pull request number that triggered this workflow
- * @param {number|undefined} triggeringDiscussionNumber - Discussion number that triggered this workflow
- * @returns {string} Footer text
- */
- function generateFooter(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber) {
- let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
-
- // Add reference to triggering issue/PR/discussion if available
- if (triggeringIssueNumber) {
- footer += ` for #${triggeringIssueNumber}`;
- } else if (triggeringPRNumber) {
- footer += ` for #${triggeringPRNumber}`;
- } else if (triggeringDiscussionNumber) {
- footer += ` for discussion #${triggeringDiscussionNumber}`;
- }
-
- if (workflowSource && workflowSourceURL) {
- footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
- }
-
- // Add XML comment marker for traceability
- footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
-
- footer += "\n";
- return footer;
- }
-
- module.exports = {
- generateFooter,
- generateXMLMarker,
- };
-
- EOF_88f9d2d4
cat > /tmp/gh-aw/scripts/load_agent_output.cjs << 'EOF_b93f537f'
// @ts-check
///
@@ -8048,6 +7952,274 @@ jobs:
module.exports = { loadAgentOutput, truncateForLogging, MAX_LOG_CONTENT_LENGTH };
EOF_b93f537f
+ cat > /tmp/gh-aw/scripts/messages_core.cjs << 'EOF_6cdb27e0'
+ // @ts-check
+ ///
+
+ /**
+ * Core Message Utilities Module
+ *
+ * This module provides shared utilities for message template processing.
+ * It includes configuration parsing and template rendering functions.
+ *
+ * Supported placeholders:
+ * - {workflow_name} - Name of the workflow
+ * - {run_url} - URL to the workflow run
+ * - {workflow_source} - Source specification (owner/repo/path@ref)
+ * - {workflow_source_url} - GitHub URL for the workflow source
+ * - {triggering_number} - Issue/PR/Discussion number that triggered this workflow
+ * - {operation} - Operation name (for staged mode titles/descriptions)
+ * - {event_type} - Event type description (for run-started messages)
+ * - {status} - Workflow status text (for run-failure messages)
+ *
+ * Both camelCase and snake_case placeholder formats are supported.
+ */
+
+ /**
+ * @typedef {Object} SafeOutputMessages
+ * @property {string} [footer] - Custom footer message template
+ * @property {string} [footerInstall] - Custom installation instructions template
+ * @property {string} [stagedTitle] - Custom staged mode title template
+ * @property {string} [stagedDescription] - Custom staged mode description template
+ * @property {string} [runStarted] - Custom workflow activation message template
+ * @property {string} [runSuccess] - Custom workflow success message template
+ * @property {string} [runFailure] - Custom workflow failure message template
+ * @property {string} [detectionFailure] - Custom detection job failure message template
+ * @property {string} [closeOlderDiscussion] - Custom message for closing older discussions as outdated
+ */
+
+ /**
+ * Get the safe-output messages configuration from environment variable.
+ * @returns {SafeOutputMessages|null} Parsed messages config or null if not set
+ */
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+
+ try {
+ // Parse JSON with camelCase keys from Go struct (using json struct tags)
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+
+ /**
+ * Replace placeholders in a template string with values from context.
+ * Supports {key} syntax for placeholder replacement.
+ * @param {string} template - Template string with {key} placeholders
+ * @param {Record} context - Key-value pairs for replacement
+ * @returns {string} Template with placeholders replaced
+ */
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+
+ /**
+ * Convert context object keys to snake_case for template rendering
+ * @param {Record} obj - Object with camelCase keys
+ * @returns {Record} Object with snake_case keys
+ */
+ function toSnakeCase(obj) {
+ /** @type {Record} */
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ // Convert camelCase to snake_case
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ // Also keep original key for backwards compatibility
+ result[key] = value;
+ }
+ return result;
+ }
+
+ module.exports = {
+ getMessages,
+ renderTemplate,
+ toSnakeCase,
+ };
+
+ EOF_6cdb27e0
+ cat > /tmp/gh-aw/scripts/messages_footer.cjs << 'EOF_c14886c6'
+ // @ts-check
+ ///
+
+ /**
+ * Footer Message Module
+ *
+ * This module provides footer and installation instructions generation
+ * for safe-output workflows.
+ */
+
+ const { getMessages, renderTemplate, toSnakeCase } = require('/tmp/gh-aw/scripts/messages_core.cjs');
+
+ /**
+ * @typedef {Object} FooterContext
+ * @property {string} workflowName - Name of the workflow
+ * @property {string} runUrl - URL of the workflow run
+ * @property {string} [workflowSource] - Source of the workflow (owner/repo/path@ref)
+ * @property {string} [workflowSourceUrl] - GitHub URL for the workflow source
+ * @property {number|string} [triggeringNumber] - Issue, PR, or discussion number that triggered this workflow
+ */
+
+ /**
+ * Get the footer message, using custom template if configured.
+ * @param {FooterContext} ctx - Context for footer generation
+ * @returns {string} Footer message
+ */
+ function getFooterMessage(ctx) {
+ const messages = getMessages();
+
+ // Create context with both camelCase and snake_case keys
+ const templateContext = toSnakeCase(ctx);
+
+ // Default footer template - pirate themed! π΄ββ οΈ
+ const defaultFooter = "> Ahoy! This treasure was crafted by [π΄ββ οΈ {workflow_name}]({run_url})";
+
+ // Use custom footer if configured
+ let footer = messages?.footer ? renderTemplate(messages.footer, templateContext) : renderTemplate(defaultFooter, templateContext);
+
+ // Add triggering reference if available
+ if (ctx.triggeringNumber) {
+ footer += ` fer issue #{triggering_number} πΊοΈ`.replace("{triggering_number}", String(ctx.triggeringNumber));
+ }
+
+ return footer;
+ }
+
+ /**
+ * Get the footer installation instructions, using custom template if configured.
+ * @param {FooterContext} ctx - Context for footer generation
+ * @returns {string} Footer installation message or empty string if no source
+ */
+ function getFooterInstallMessage(ctx) {
+ if (!ctx.workflowSource || !ctx.workflowSourceUrl) {
+ return "";
+ }
+
+ const messages = getMessages();
+
+ // Create context with both camelCase and snake_case keys
+ const templateContext = toSnakeCase(ctx);
+
+ // Default installation template - pirate themed! π΄ββ οΈ
+ const defaultInstall = "> Arr! To plunder this workflow fer yer own ship, run `gh aw add {workflow_source}`. Chart yer course at [π¦ {workflow_source_url}]({workflow_source_url})!";
+
+ // Use custom installation message if configured
+ return messages?.footerInstall ? renderTemplate(messages.footerInstall, templateContext) : renderTemplate(defaultInstall, templateContext);
+ }
+
+ /**
+ * Generates an XML comment marker with agentic workflow metadata for traceability.
+ * This marker enables searching and tracing back items generated by an agentic workflow.
+ *
+ * The marker format is:
+ *
+ *
+ * @param {string} workflowName - Name of the workflow
+ * @param {string} runUrl - URL of the workflow run
+ * @returns {string} XML comment marker with workflow metadata
+ */
+ function generateXMLMarker(workflowName, runUrl) {
+ // Read engine metadata from environment variables
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+
+ // Build the key-value pairs for the marker
+ const parts = [];
+
+ // Always include agentic-workflow name
+ parts.push(`agentic-workflow: ${workflowName}`);
+
+ // Add tracker-id if available (for searchability and tracing)
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+
+ // Add engine ID if available
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+
+ // Add version if available
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+
+ // Add model if available
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+
+ // Always include run URL
+ parts.push(`run: ${runUrl}`);
+
+ // Return the XML comment marker
+ return ``;
+ }
+
+ /**
+ * Generate the complete footer with AI attribution and optional installation instructions.
+ * This is a drop-in replacement for the original generateFooter function.
+ * @param {string} workflowName - Name of the workflow
+ * @param {string} runUrl - URL of the workflow run
+ * @param {string} workflowSource - Source of the workflow (owner/repo/path@ref)
+ * @param {string} workflowSourceURL - GitHub URL for the workflow source
+ * @param {number|undefined} triggeringIssueNumber - Issue number that triggered this workflow
+ * @param {number|undefined} triggeringPRNumber - Pull request number that triggered this workflow
+ * @param {number|undefined} triggeringDiscussionNumber - Discussion number that triggered this workflow
+ * @returns {string} Complete footer text
+ */
+ function generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber) {
+ // Determine triggering number (issue takes precedence, then PR, then discussion)
+ let triggeringNumber;
+ if (triggeringIssueNumber) {
+ triggeringNumber = triggeringIssueNumber;
+ } else if (triggeringPRNumber) {
+ triggeringNumber = triggeringPRNumber;
+ } else if (triggeringDiscussionNumber) {
+ triggeringNumber = `discussion #${triggeringDiscussionNumber}`;
+ }
+
+ const ctx = {
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceUrl: workflowSourceURL,
+ triggeringNumber,
+ };
+
+ let footer = "\n\n" + getFooterMessage(ctx);
+
+ // Add installation instructions if source is available
+ const installMessage = getFooterInstallMessage(ctx);
+ if (installMessage) {
+ footer += "\n>\n" + installMessage;
+ }
+
+ // Add XML comment marker for traceability
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+
+ footer += "\n";
+ return footer;
+ }
+
+ module.exports = {
+ getFooterMessage,
+ getFooterInstallMessage,
+ generateFooterWithMessages,
+ generateXMLMarker,
+ };
+
+ EOF_c14886c6
cat > /tmp/gh-aw/scripts/remove_duplicate_title.cjs << 'EOF_bb4a8126'
// @ts-check
/**
@@ -8594,6 +8766,7 @@ jobs:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_WORKFLOW_NAME: "Dev"
GH_AW_ENGINE_ID: "copilot"
+ GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e π Poetry generated by [{workflow_name}]({run_url})\",\"footerInstall\":\"\\u003e Want to add poems to your discussions? Install with `gh aw add {workflow_source}`\"}"
GH_AW_UPDATE_TARGET: "*"
GH_AW_UPDATE_BODY: "true"
with:
@@ -8606,7 +8779,7 @@ jobs:
globalThis.io = io;
const { runUpdateWorkflow, createRenderStagedItem, createGetSummaryLine } = require('/tmp/gh-aw/scripts/update_runner.cjs');
const { isDiscussionContext, getDiscussionNumber } = require('/tmp/gh-aw/scripts/update_context_helpers.cjs');
- const { generateFooter } = require('/tmp/gh-aw/scripts/generate_footer.cjs');
+ const { generateFooterWithMessages } = require('/tmp/gh-aw/scripts/messages_footer.cjs');
const renderStagedItem = createRenderStagedItem({
entityName: "Discussion",
numberField: "discussion_number",
@@ -8650,7 +8823,7 @@ jobs:
const triggeringIssueNumber = context.payload.issue?.number;
const triggeringPRNumber = context.payload.pull_request?.number;
const triggeringDiscussionNumber = context.payload.discussion?.number;
- const footer = generateFooter(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber);
+ const footer = generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber);
fieldsToUpdate.body = fieldsToUpdate.body + footer;
}
const mutationFields = [];
diff --git a/.github/workflows/dev.md b/.github/workflows/dev.md
index 450ee15475b..3301b901084 100644
--- a/.github/workflows/dev.md
+++ b/.github/workflows/dev.md
@@ -20,6 +20,9 @@ safe-outputs:
update-discussion:
target: "*"
body:
+ messages:
+ footer: "> π Poetry generated by [{workflow_name}]({run_url})"
+ footer-install: "> Want to add poems to your discussions? Install with `gh aw add {workflow_source}`"
---
Find the latest discussion in this repository and update its body by appending a short, creative poem about GitHub Agentic Workflows.
diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md
index b5aade72c47..ac95c1ba104 100644
--- a/docs/src/content/docs/reference/frontmatter-full.md
+++ b/docs/src/content/docs/reference/frontmatter-full.md
@@ -1925,6 +1925,42 @@ safe-outputs:
# (optional)
# This field supports multiple formats (oneOf):
+ # Option 1: Configuration for updating GitHub discussions from agentic workflow
+ # output
+ update-discussion:
+ # Target for updates: 'triggering' (default), '*' (any discussion), or explicit
+ # discussion number
+ # (optional)
+ target: "example-value"
+
+ # Allow updating discussion title - presence of key indicates field can be updated
+ # (optional)
+ title: null
+
+ # Allow updating discussion body - presence of key indicates field can be updated
+ # (optional)
+ body: null
+
+ # Maximum number of discussions to update (default: 1)
+ # (optional)
+ max: 1
+
+ # Target repository in format 'owner/repo' for cross-repository discussion
+ # updates. Takes precedence over trial target repo settings.
+ # (optional)
+ target-repo: "example-value"
+
+ # GitHub token to use for this specific output type. Overrides global github-token
+ # if specified.
+ # (optional)
+ github-token: "${{ secrets.GITHUB_TOKEN }}"
+
+ # Option 2: Enable discussion updating with default configuration
+ update-discussion: null
+
+ # (optional)
+ # This field supports multiple formats (oneOf):
+
# Option 1: Configuration for closing GitHub issues with comment from agentic
# workflow output
close-issue:
@@ -2863,10 +2899,7 @@ strict: true
# (JavaScript), 'run' (shell), or 'py' (Python) must be specified per tool.
# (optional)
safe-inputs:
- # Transport mode for the safe-inputs MCP server. Only 'http' mode is supported,
- # which starts the server as a separate step.
- # (optional)
- mode: "http"
+
# Runtime environment version overrides. Allows customizing runtime versions
# (e.g., Node.js, Python) or defining new runtimes. Runtimes from imported shared
diff --git a/pkg/cli/fix_command_test.go b/pkg/cli/fix_command_test.go
index 7e75b112b0b..2a34cd4be6f 100644
--- a/pkg/cli/fix_command_test.go
+++ b/pkg/cli/fix_command_test.go
@@ -542,12 +542,12 @@ This is a test workflow with slash command.
}
func TestFixCommand_SafeInputsModeRemoval(t *testing.T) {
-// Create a temporary directory for test files
-tmpDir := t.TempDir()
-workflowFile := filepath.Join(tmpDir, "test-workflow.md")
+ // Create a temporary directory for test files
+ tmpDir := t.TempDir()
+ workflowFile := filepath.Join(tmpDir, "test-workflow.md")
-// Create a workflow with deprecated safe-inputs.mode field
-content := `---
+ // Create a workflow with deprecated safe-inputs.mode field
+ content := `---
on: workflow_dispatch
engine: copilot
safe-inputs:
@@ -563,51 +563,51 @@ safe-inputs:
This is a test workflow with safe-inputs mode field.
`
-if err := os.WriteFile(workflowFile, []byte(content), 0644); err != nil {
-t.Fatalf("Failed to create test file: %v", err)
-}
+ if err := os.WriteFile(workflowFile, []byte(content), 0644); err != nil {
+ t.Fatalf("Failed to create test file: %v", err)
+ }
-// Get the safe-inputs mode removal codemod
-modeCodemod := getCodemodByID("safe-inputs-mode-removal")
-if modeCodemod == nil {
-t.Fatal("safe-inputs-mode-removal codemod not found")
-}
+ // Get the safe-inputs mode removal codemod
+ modeCodemod := getCodemodByID("safe-inputs-mode-removal")
+ if modeCodemod == nil {
+ t.Fatal("safe-inputs-mode-removal codemod not found")
+ }
-// Process the file
-fixed, err := processWorkflowFile(workflowFile, []Codemod{*modeCodemod}, true, false)
-if err != nil {
-t.Fatalf("Failed to process workflow file: %v", err)
-}
+ // Process the file
+ fixed, err := processWorkflowFile(workflowFile, []Codemod{*modeCodemod}, true, false)
+ if err != nil {
+ t.Fatalf("Failed to process workflow file: %v", err)
+ }
-if !fixed {
-t.Error("Expected file to be fixed, but no changes were made")
-}
+ if !fixed {
+ t.Error("Expected file to be fixed, but no changes were made")
+ }
-// Read the updated content
-updatedContent, err := os.ReadFile(workflowFile)
-if err != nil {
-t.Fatalf("Failed to read updated file: %v", err)
-}
+ // Read the updated content
+ updatedContent, err := os.ReadFile(workflowFile)
+ if err != nil {
+ t.Fatalf("Failed to read updated file: %v", err)
+ }
-updatedStr := string(updatedContent)
+ updatedStr := string(updatedContent)
-t.Logf("Updated content:\n%s", updatedStr)
+ t.Logf("Updated content:\n%s", updatedStr)
-// Verify the change - mode field should be removed
-if strings.Contains(updatedStr, "mode:") {
-t.Errorf("Expected mode field to be removed, but it still exists:\n%s", updatedStr)
-}
+ // Verify the change - mode field should be removed
+ if strings.Contains(updatedStr, "mode:") {
+ t.Errorf("Expected mode field to be removed, but it still exists:\n%s", updatedStr)
+ }
-// Verify safe-inputs block and test-tool are preserved
-if !strings.Contains(updatedStr, "safe-inputs:") {
-t.Error("Expected safe-inputs block to be preserved")
-}
+ // Verify safe-inputs block and test-tool are preserved
+ if !strings.Contains(updatedStr, "safe-inputs:") {
+ t.Error("Expected safe-inputs block to be preserved")
+ }
-if !strings.Contains(updatedStr, "test-tool:") {
-t.Error("Expected test-tool to be preserved")
-}
+ if !strings.Contains(updatedStr, "test-tool:") {
+ t.Error("Expected test-tool to be preserved")
+ }
-if !strings.Contains(updatedStr, "description: Test tool") {
-t.Error("Expected test-tool description to be preserved")
-}
+ if !strings.Contains(updatedStr, "description: Test tool") {
+ t.Error("Expected test-tool description to be preserved")
+ }
}
diff --git a/pkg/workflow/js/update_discussion.cjs b/pkg/workflow/js/update_discussion.cjs
index 665b0fefff0..c8432e9f94f 100644
--- a/pkg/workflow/js/update_discussion.cjs
+++ b/pkg/workflow/js/update_discussion.cjs
@@ -3,7 +3,7 @@
const { runUpdateWorkflow, createRenderStagedItem, createGetSummaryLine } = require("./update_runner.cjs");
const { isDiscussionContext, getDiscussionNumber } = require("./update_context_helpers.cjs");
-const { generateFooter } = require("./generate_footer.cjs");
+const { generateFooterWithMessages } = require("./messages_footer.cjs");
// Use shared helper for staged preview rendering
const renderStagedItem = createRenderStagedItem({
@@ -72,7 +72,7 @@ async function executeDiscussionUpdate(github, context, discussionNumber, update
const triggeringDiscussionNumber = context.payload.discussion?.number;
// Append footer to the body
- const footer = generateFooter(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber);
+ const footer = generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber);
fieldsToUpdate.body = fieldsToUpdate.body + footer;
}
diff --git a/pkg/workflow/js/update_discussion.test.cjs b/pkg/workflow/js/update_discussion.test.cjs
index 29948b1aaea..518469fac05 100644
--- a/pkg/workflow/js/update_discussion.test.cjs
+++ b/pkg/workflow/js/update_discussion.test.cjs
@@ -291,4 +291,53 @@ describe("update_discussion.cjs", () => {
expect(mockCore.info).toHaveBeenCalledWith("No valid updates to apply for this item");
expect(mockGithub.graphql).not.toHaveBeenCalled();
});
+
+ it("should use custom footer message when configured", async () => {
+ setAgentOutput({
+ items: [{ type: "update_discussion", body: "New discussion body" }],
+ });
+ process.env.GH_AW_UPDATE_BODY = "true";
+ process.env.GH_AW_WORKFLOW_NAME = "Custom Workflow";
+ process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({
+ footer: "> Custom footer by [{workflow_name}]({run_url})",
+ });
+ global.context.eventName = "discussion";
+ global.context.runId = 789;
+
+ const mockDiscussion = {
+ id: "D_kwDOABCD123",
+ number: 123,
+ title: "Test Discussion",
+ body: "Old body",
+ url: "https://github.com/testowner/testrepo/discussions/123",
+ };
+
+ // Mock the query
+ mockGithub.graphql.mockResolvedValueOnce({
+ repository: {
+ discussion: mockDiscussion,
+ },
+ });
+
+ // Mock the update and capture the body parameter
+ let capturedBody;
+ mockGithub.graphql.mockImplementationOnce((query, variables) => {
+ capturedBody = variables.body;
+ return Promise.resolve({
+ updateDiscussion: {
+ discussion: {
+ ...mockDiscussion,
+ body: variables.body,
+ },
+ },
+ });
+ });
+
+ await eval(`(async () => { ${updateDiscussionScript} })()`);
+
+ expect(mockGithub.graphql).toHaveBeenCalledTimes(2);
+ // Verify the custom footer was used
+ expect(capturedBody).toContain("Custom footer by");
+ expect(capturedBody).toContain("Custom Workflow");
+ });
});