Skip to content
Merged
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
28 changes: 20 additions & 8 deletions actions/setup/js/update_pr_description_helpers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,27 @@
* @module update_pr_description_helpers
*/

const { getFooterMessage } = require("./messages_footer.cjs");
const { generateFooterWithMessages } = require("./messages_footer.cjs");
const { generateWorkflowIdMarker } = require("./generate_footer.cjs");
const { sanitizeContent } = require("./sanitize_content.cjs");

/**
* Build the AI footer with workflow attribution
* Uses the messages system to support custom templates from frontmatter
* Uses the common generateFooterWithMessages helper (includes install instructions,
* missing info sections, blocked domains, and XML metadata marker).
* @param {string} workflowName - Name of the workflow
* @param {string} runUrl - URL of the workflow run
* @returns {string} AI attribution footer
*/
function buildAIFooter(workflowName, runUrl) {
return "\n\n" + getFooterMessage({ workflowName, runUrl });
const workflowSource = process.env.GH_AW_WORKFLOW_SOURCE ?? "";
const workflowSourceURL = process.env.GH_AW_WORKFLOW_SOURCE_URL ?? "";
// Use typeof guard since context is a global injected by the Actions Script runtime
const ctx = typeof context !== "undefined" ? context : null;
const triggeringIssueNumber = ctx?.payload?.issue?.number;
const triggeringPRNumber = ctx?.payload?.pull_request?.number;
const triggeringDiscussionNumber = ctx?.payload?.discussion?.number;
return generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber).trimEnd();
}

/**
Expand Down Expand Up @@ -77,15 +86,18 @@ function findIsland(body, workflowId) {
*/
function updateBody(params) {
const { currentBody, newContent, operation, workflowName, runUrl, workflowId, includeFooter = true } = params;
// When footer is enabled use the full footer (includes install instructions, XML marker, etc.)
// When footer is disabled still add standalone workflow-id marker for searchability
const aiFooter = includeFooter ? buildAIFooter(workflowName, runUrl) : "";
const workflowIdMarker = !includeFooter && workflowId ? `\n\n${generateWorkflowIdMarker(workflowId)}` : "";
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

The workflow ID marker should be added unconditionally (when workflowId is present), not just when includeFooter is false. This creates an inconsistency with create_issue and create_pull_request, which always add the workflow-id marker for searchability.

In create_issue.cjs (line 446-448) and create_pull_request.cjs (line 489-491), the workflow-id marker is added unconditionally with the comment "Always add XML markers even when footer is disabled". The same pattern should be followed here.

Change line 92 from:

const workflowIdMarker = !includeFooter && workflowId ? `\n\n${generateWorkflowIdMarker(workflowId)}` : "";

to:

const workflowIdMarker = workflowId ? `\n\n${generateWorkflowIdMarker(workflowId)}` : "";

This ensures the workflow-id marker is always present for searchability, regardless of footer settings.

Suggested change
const workflowIdMarker = !includeFooter && workflowId ? `\n\n${generateWorkflowIdMarker(workflowId)}` : "";
const workflowIdMarker = workflowId ? `\n\n${generateWorkflowIdMarker(workflowId)}` : "";

Copilot uses AI. Check for mistakes.

// Sanitize new content to prevent injection attacks
const sanitizedNewContent = sanitizeContent(newContent);

if (operation === "replace") {
// Replace: use new content with optional AI footer
core.info("Operation: replace (full body replacement)");
return sanitizedNewContent + aiFooter;
return sanitizedNewContent + aiFooter + workflowIdMarker;
}

if (operation === "replace-island") {
Expand All @@ -97,7 +109,7 @@ function updateBody(params) {
core.info(`Operation: replace-island (updating existing island for workflow ${workflowId})`);
const startMarker = buildIslandStartMarker(workflowId);
const endMarker = buildIslandEndMarker(workflowId);
const islandContent = `${startMarker}\n${sanitizedNewContent}${aiFooter}\n${endMarker}`;
const islandContent = `${startMarker}\n${sanitizedNewContent}${aiFooter}${workflowIdMarker}\n${endMarker}`;

const before = currentBody.substring(0, island.startIndex);
const after = currentBody.substring(island.endIndex);
Expand All @@ -107,7 +119,7 @@ function updateBody(params) {
core.info(`Operation: replace-island (island not found for workflow ${workflowId}, falling back to append)`);
const startMarker = buildIslandStartMarker(workflowId);
const endMarker = buildIslandEndMarker(workflowId);
const islandContent = `${startMarker}\n${sanitizedNewContent}${aiFooter}\n${endMarker}`;
const islandContent = `${startMarker}\n${sanitizedNewContent}${aiFooter}${workflowIdMarker}\n${endMarker}`;
const appendSection = `\n\n---\n\n${islandContent}`;
return currentBody + appendSection;
}
Expand All @@ -116,13 +128,13 @@ function updateBody(params) {
if (operation === "prepend") {
// Prepend: add content, AI footer (if enabled), and horizontal line at the start
core.info("Operation: prepend (add to start with separator)");
const prependSection = `${sanitizedNewContent}${aiFooter}\n\n---\n\n`;
const prependSection = `${sanitizedNewContent}${aiFooter}${workflowIdMarker}\n\n---\n\n`;
return prependSection + currentBody;
}

// Default to append
core.info("Operation: append (add to end with separator)");
const appendSection = `\n\n---\n\n${sanitizedNewContent}${aiFooter}`;
const appendSection = `\n\n---\n\n${sanitizedNewContent}${aiFooter}${workflowIdMarker}`;
return currentBody + appendSection;
}

Expand Down
2 changes: 1 addition & 1 deletion actions/setup/js/update_pr_description_helpers.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ describe("update_pr_description_helpers.cjs", () => {
workflowId: "test-workflow",
includeFooter: false,
});
expect(result).toBe("Replacement");
expect(result).toBe("Replacement\n\n<!-- gh-aw-workflow-id: test-workflow -->");
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

After fixing the bug on line 92 (making the workflow ID marker unconditional), this test expectation will need to be updated. The workflow ID marker should be present in the output regardless of the includeFooter setting. Additionally, tests should be added to verify that the workflow ID marker is present when includeFooter is true (currently only line 461 tests for it when false).

Copilot uses AI. Check for mistakes.
expect(result).not.toContain("Generated by");
});

Expand Down
7 changes: 6 additions & 1 deletion actions/setup/js/update_pull_request.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ async function executePRUpdate(github, context, prNumber, updateData) {
// Handle body operation (append/prepend/replace/replace-island)
const operation = updateData._operation || "replace";
const rawBody = updateData._rawBody;
const includeFooter = updateData._includeFooter !== false; // Default to true

// Remove internal fields
const { _operation, _rawBody, ...apiData } = updateData;
const { _operation, _rawBody, _includeFooter, ...apiData } = updateData;

// If we have a body, process it with the appropriate operation
if (rawBody !== undefined) {
Expand All @@ -52,6 +53,7 @@ async function executePRUpdate(github, context, prNumber, updateData) {
workflowName,
runUrl,
workflowId,
includeFooter, // Pass footer flag to helper
});

core.info(`Will update body (length: ${apiData.body.length})`);
Expand Down Expand Up @@ -129,6 +131,9 @@ function buildPRUpdateData(item, config) {
};
}

// Pass footer config to executeUpdate (default to true)
updateData._includeFooter = config.footer !== false;

return { success: true, data: updateData };
}

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/footers.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ safe-outputs:
When `footer: false` is set:
- **Visible footer content is omitted** - No AI-generated attribution text appears in the item body
- **XML markers are preserved** - Hidden workflow-id and tracker-id markers remain for searchability
- **All safe output types affected** - Applies to create-issue, create-pull-request, create-discussion, update-issue, update-discussion, and update-release
- **All safe output types affected** - Applies to create-issue, create-pull-request, create-discussion, update-issue, update-pull-request, update-discussion, and update-release

## Per-Handler Footer Control

Expand Down
1 change: 1 addition & 0 deletions docs/src/content/docs/reference/safe-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ safe-outputs:
update-pull-request:
title: true # enable title updates (default: true)
body: true # enable body updates (default: true)
footer: false # omit AI-generated footer from body updates (default: true)
max: 1 # max updates (default: 1)
target: "*" # "triggering" (default), "*", or number
target-repo: "owner/repo" # cross-repository
Expand Down
5 changes: 5 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -5734,6 +5734,11 @@
"description": "Default operation for body updates: 'append' (add to end), 'prepend' (add to start), or 'replace' (overwrite completely). Defaults to 'replace' if not specified.",
"enum": ["append", "prepend", "replace"]
},
"footer": {
"type": "boolean",
"description": "Controls whether AI-generated footer is added when updating the pull request body. When false, the visible footer content is omitted but XML markers are still included. Defaults to true. Only applies when 'body' is enabled.",
"default": true
},
"max": {
"type": "integer",
"description": "Maximum number of pull requests to update (default: 1)",
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/compiler_safe_outputs_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ var handlerRegistry = map[string]handlerBuilder{
AddBoolPtrOrDefault("allow_title", c.Title, true).
AddBoolPtrOrDefault("allow_body", c.Body, true).
AddStringPtr("default_operation", c.Operation).
AddBoolPtr("footer", getEffectiveFooter(c.Footer, cfg.Footer)).
AddIfNotEmpty("target-repo", c.TargetRepoSlug).
AddStringSlice("allowed_repos", c.AllowedRepos).
Build()
Expand Down
4 changes: 4 additions & 0 deletions pkg/workflow/safe_outputs_footer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func TestGlobalFooterConfiguration(t *testing.T) {
"update-issue": map[string]any{"body": nil},
"update-discussion": map[string]any{"body": nil},
"update-release": nil,
"update-pull-request": map[string]any{"body": nil},
},
}
config := compiler.extractSafeOutputsConfig(frontmatter)
Expand Down Expand Up @@ -86,6 +87,9 @@ func TestGlobalFooterConfiguration(t *testing.T) {
if updateReleaseConfig, ok := handlerConfig["update_release"].(map[string]any); ok {
assert.Equal(t, false, updateReleaseConfig["footer"], "update_release should inherit global footer: false")
}
if updatePRConfig, ok := handlerConfig["update_pull_request"].(map[string]any); ok {
assert.Equal(t, false, updatePRConfig["footer"], "update_pull_request should inherit global footer: false")
}
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/workflow/update_pull_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type UpdatePullRequestsConfig struct {
Title *bool `yaml:"title,omitempty"` // Allow updating PR title - defaults to true, set to false to disable
Body *bool `yaml:"body,omitempty"` // Allow updating PR body - defaults to true, set to false to disable
Operation *string `yaml:"operation,omitempty"` // Default operation for body updates: "append", "prepend", or "replace" (defaults to "replace")
Footer *bool `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted.
}

// parseUpdatePullRequestsConfig handles update-pull-request configuration
Expand All @@ -24,6 +25,7 @@ func (c *Compiler) parseUpdatePullRequestsConfig(outputMap map[string]any) *Upda
return []UpdateEntityFieldSpec{
{Name: "title", Mode: FieldParsingBoolValue, Dest: &cfg.Title},
{Name: "body", Mode: FieldParsingBoolValue, Dest: &cfg.Body},
{Name: "footer", Mode: FieldParsingBoolValue, Dest: &cfg.Footer},
}
}, func(configMap map[string]any, cfg *UpdatePullRequestsConfig) {
// Parse operation field
Expand Down
Loading