Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4c7492e
Initial plan
Copilot Jul 9, 2026
c1e11ea
Add disclosure-header message for AI authorship disclosure
Copilot Jul 9, 2026
eea258e
Clarify disclosure-header ordering in JSON schema description
Copilot Jul 9, 2026
36ec286
Merge branch 'main' into copilot/feature-ai-authorship-footer
github-actions[bot] Jul 9, 2026
259ec46
Fix unresolved disclosure-header review feedback
Copilot Jul 9, 2026
943eeba
Merge branch 'main' into copilot/feature-ai-authorship-footer
github-actions[bot] Jul 9, 2026
3bd6f77
Merge branch 'main' into copilot/feature-ai-authorship-footer
github-actions[bot] Jul 9, 2026
4f03911
Merge remote-tracking branch 'origin/main' into copilot/feature-ai-au…
Copilot Jul 9, 2026
0d150f5
Refactor disclosure header default into prompt markdown
Copilot Jul 9, 2026
892581a
Clarify runtime prompt selection naming for disclosure header
Copilot Jul 9, 2026
84ac639
Extract disclosure template path resolution helper
Copilot Jul 9, 2026
3760309
Merge branch 'main' into copilot/feature-ai-authorship-footer
github-actions[bot] Jul 9, 2026
42988c2
Shorten default disclosure header text
Copilot Jul 10, 2026
d019149
Remove disclosure-header default text fallback
Copilot Jul 10, 2026
9ce8984
Clarify disclosure template path warnings
Copilot Jul 10, 2026
834f6ab
Remove source-tree fallback for disclosure header template
Copilot Jul 10, 2026
7334916
Isolate messages_header prompt env setup in test hooks
Copilot Jul 10, 2026
e581471
Make messages_header test prompt path setup deterministic
Copilot Jul 10, 2026
4f3d09c
Remove disclosure-header fallback guards
Copilot Jul 10, 2026
36ff9e0
refactor: use renderTemplateFromFile for disclosure header template
Copilot Jul 10, 2026
7850dbc
Merge remote-tracking branch 'origin/main' into copilot/feature-ai-au…
Copilot Jul 10, 2026
bdb704a
Merge branch 'main' into copilot/feature-ai-authorship-footer
github-actions[bot] Jul 10, 2026
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
9 changes: 5 additions & 4 deletions actions/setup/js/add_comment.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_help
const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs");
const { getMissingInfoSections } = require("./missing_messages_helper.cjs");
const { getMessages } = require("./messages_core.cjs");
const { getBodyHeader } = require("./messages_header.cjs");
const { getBodyHeader, getDisclosureHeader } = require("./messages_header.cjs");
const { sanitizeContent } = require("./sanitize_content.cjs");
const { MAX_COMMENT_LENGTH, MAX_MENTIONS, MAX_LINKS, enforceCommentLimits } = require("./comment_limit_helpers.cjs");
const { createDiscussionComment, resolveTopLevelDiscussionCommentId } = require("./github_api_helpers.cjs");
Expand Down Expand Up @@ -669,11 +669,12 @@ async function main(config = {}) {
runUrl,
}).detectionCaution;

// Inject body header if configured (placed after caution, before user content)
// Inject body header and disclosure header if configured (placed after caution, before user content)
const bodyHeader = getBodyHeader({ workflowName, runUrl });
const disclosureHeader = getDisclosureHeader({ workflowName, runUrl });

// Build prefix: caution (if any) β†’ body header (if any) β†’ user content
const prefixParts = [detectionCaution, bodyHeader].filter(Boolean);
// Build prefix: caution (if any) β†’ disclosure header (if any) β†’ body header (if any) β†’ user content
const prefixParts = [detectionCaution, disclosureHeader, bodyHeader].filter(Boolean);
if (prefixParts.length > 0) processedBody = prefixParts.join("\n\n") + "\n\n" + processedBody;

// Add tracker ID and footer
Expand Down
8 changes: 7 additions & 1 deletion actions/setup/js/create_discussion.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const { getErrorMessage } = require("./error_helpers.cjs");
const { ERR_VALIDATION } = require("./error_codes.cjs");
const { createExpirationLine, generateFooterWithExpiration, addExpirationToFooter } = require("./ephemerals.cjs");
const { assembleMarkdownBodyParts } = require("./markdown_body_helpers.cjs");
const { getBodyHeader } = require("./messages_header.cjs");
const { getBodyHeader, getDisclosureHeader } = require("./messages_header.cjs");
const { generateWorkflowIdMarker, generateWorkflowCallIdMarker, generateCloseKeyMarker, normalizeCloseOlderKey } = require("./generate_footer.cjs");
const { sanitizeContent } = require("./sanitize_content.cjs");
const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
Expand Down Expand Up @@ -545,6 +545,12 @@ async function main(config = {}) {
bodyLines.unshift(...bodyHeader.split("\n"), "");
}

// Inject disclosure header (this runs after body-header, but appears before it because unshift prepends)
const disclosureHeader = getDisclosureHeader({ workflowName, runUrl });
if (disclosureHeader) {
bodyLines.unshift(...disclosureHeader.split("\n"), "");
}

const triggeringIssueNumber = context.payload?.issue?.number && !context.payload?.issue?.pull_request ? context.payload.issue.number : undefined;
const triggeringPRNumber = context.payload?.pull_request?.number || (context.payload?.issue?.pull_request ? context.payload.issue.number : undefined);
const triggeringDiscussionNumber = context.payload?.discussion?.number;
Expand Down
8 changes: 7 additions & 1 deletion actions/setup/js/create_issue.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
const { sanitizeTitle, applyTitlePrefix } = require("./sanitize_title.cjs");
const { sanitizeContent } = require("./sanitize_content.cjs");
const { generateFooterWithMessages, getDetectionCautionAlert } = require("./messages_footer.cjs");
const { getBodyHeader } = require("./messages_header.cjs");
const { getBodyHeader, getDisclosureHeader } = require("./messages_header.cjs");
const { generateWorkflowIdMarker, generateWorkflowCallIdMarker, generateCloseKeyMarker, normalizeCloseOlderKey } = require("./generate_footer.cjs");
const { generateHistoryUrl } = require("./generate_history_link.cjs");
const { getTrackerID } = require("./get_tracker_id.cjs");
Expand Down Expand Up @@ -883,6 +883,12 @@ async function main(config = {}) {
bodyLines.unshift(...bodyHeader.split("\n"), "");
}

// Inject disclosure header (this runs after body-header, but appears before it because unshift prepends)
const disclosureHeader = getDisclosureHeader({ workflowName, runUrl });
if (disclosureHeader) {
bodyLines.unshift(...disclosureHeader.split("\n"), "");
}

// Inject CAUTION at top of body if threat detection warning was raised
// (unshifted after header so it appears first in the final output)
const detectionCaution = getDetectionCautionAlert(workflowName, runUrl);
Expand Down
8 changes: 7 additions & 1 deletion actions/setup/js/create_pull_request.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const { addExpirationToFooter } = require("./ephemerals.cjs");
const { generateWorkflowIdMarker, generateWorkflowCallIdMarker, generateCloseKeyMarker, normalizeCloseOlderKey } = require("./generate_footer.cjs");
const { parseBoolTemplatable } = require("./templatable.cjs");
const { assembleMarkdownBodyParts } = require("./markdown_body_helpers.cjs");
const { getBodyHeader } = require("./messages_header.cjs");
const { getBodyHeader, getDisclosureHeader } = require("./messages_header.cjs");
const { generateHistoryUrl } = require("./generate_history_link.cjs");
const { normalizeBranchName } = require("./normalize_branch_name.cjs");
const { pushExtraEmptyCommit } = require("./extra_empty_commit.cjs");
Expand Down Expand Up @@ -1368,6 +1368,12 @@ async function main(config = {}) {
bodyLines.unshift(...bodyHeader.split("\n"), "");
}

// Inject disclosure header (this runs after body-header, but appears before it because unshift prepends)
const disclosureHeader = getDisclosureHeader({ workflowName, runUrl });
if (disclosureHeader) {
bodyLines.unshift(...disclosureHeader.split("\n"), "");
}

// Keep the protected-files notice directly under detection caution:
// this block runs first, then detectionCaution below unshifts to index 0.
if (manifestProtectionRequestReview && manifestProtectionRequestReview.length > 0) {
Expand Down
3 changes: 2 additions & 1 deletion actions/setup/js/messages.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const { getRunStartedMessage, getRunSuccessMessage, getRunFailureMessage } = req
const { getCloseOlderDiscussionMessage } = require("./messages_close_discussion.cjs");

// Re-export body header messages
const { getBodyHeader } = require("./messages_header.cjs");
const { getBodyHeader, getDisclosureHeader } = require("./messages_header.cjs");

module.exports = {
getMessages,
Expand All @@ -56,4 +56,5 @@ module.exports = {
getRunFailureMessage,
getCloseOlderDiscussionMessage,
getBodyHeader,
getDisclosureHeader,
};
1 change: 1 addition & 0 deletions actions/setup/js/messages_core.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const fs = require("fs");
* @property {string} [agentFailureComment] - Custom footer template for comments on agent failure tracking issues
* @property {string} [closeOlderDiscussion] - Custom message for closing older discussions as outdated
* @property {string} [bodyHeader] - Custom header text prepended to every message body (issues, comments, PRs, discussions). Placeholders: {workflow_name}, {run_url}
* @property {string|boolean} [disclosureHeader] - AI authorship disclosure header. Set true/"true" for built-in default or provide a custom template
* @property {boolean} [appendOnlyComments] - If true, create new comments instead of updating the activation comment
* @property {string|boolean} [activationComments] - If false or "false", disable all activation/fallback comments entirely. Supports templatable boolean values (default: true)
*/
Expand Down
40 changes: 38 additions & 2 deletions actions/setup/js/messages_header.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,24 @@
/**
* Body Header Message Module
*
* This module provides the body-header generation for safe-output workflows.
* This module provides the body-header and disclosure-header generation for safe-output workflows.
* The body header is prepended to every message body generated by safe outputs
* (issues, comments, pull requests, discussions).
*/

const { getMessages, renderTemplate, toSnakeCase } = require("./messages_core.cjs");
const { getMessages, getPromptPath, renderTemplate, renderTemplateFromFile, toSnakeCase } = require("./messages_core.cjs");

/**
* @typedef {Object} BodyHeaderContext
* @property {string} workflowName - Name of the workflow
* @property {string} runUrl - URL of the workflow run
*/

const DEFAULT_DISCLOSURE_HEADER_TEMPLATE = "safe_outputs_disclosure_header.md";
const DISCLOSURE_HEADER_DEFAULT_SENTINEL = "true";

const DEFAULT_DISCLOSURE_HEADER = renderTemplateFromFile(getPromptPath(DEFAULT_DISCLOSURE_HEADER_TEMPLATE), {}).trimEnd();

/**
* Get the body header text, using the custom template if configured.
* Returns an empty string when no body-header is configured.
Expand All @@ -33,6 +38,37 @@ function getBodyHeader(ctx) {
return renderTemplate(messages.bodyHeader, templateContext);
}

/**
* Get the AI authorship disclosure header text.
* When disclosure-header is "true", returns the built-in default AI disclosure text.
* When disclosure-header is a custom string, renders it as a template.
* Returns an empty string when disclosure-header is not configured.
* @param {BodyHeaderContext} ctx - Context for header generation
* @returns {string} The rendered disclosure header text, or empty string if not configured
*/
function getDisclosureHeader(ctx) {
const messages = getMessages();
const disclosureHeader = messages?.disclosureHeader;
if (!disclosureHeader) {
return "";
}

let template = "";
if (disclosureHeader === true || disclosureHeader === DISCLOSURE_HEADER_DEFAULT_SENTINEL) {
template = DEFAULT_DISCLOSURE_HEADER;
} else if (typeof disclosureHeader === "string") {
template = disclosureHeader;
} else {
return "";
}

const templateContext = toSnakeCase(ctx);
return renderTemplate(template, templateContext);
}
Comment thread
pelikhan marked this conversation as resolved.

module.exports = {
getBodyHeader,
getDisclosureHeader,
DEFAULT_DISCLOSURE_HEADER,
DISCLOSURE_HEADER_DEFAULT_SENTINEL,
};
89 changes: 87 additions & 2 deletions actions/setup/js/messages_header.test.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// @ts-check
import { describe, it, expect, beforeEach, vi } from "vitest";
import { describe, it, expect, beforeAll, beforeEach, afterAll, vi } from "vitest";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";

// messages_core.cjs calls core.warning on parse failures - provide a stub
const mockCore = {
Expand All @@ -11,12 +14,32 @@ const mockCore = {
};
global.core = mockCore;

const { getBodyHeader } = require("./messages_header.cjs");
const require = createRequire(import.meta.url);
let originalPromptsDir;
let getBodyHeader;
let getDisclosureHeader;
let DEFAULT_DISCLOSURE_HEADER;
let DISCLOSURE_HEADER_DEFAULT_SENTINEL;

const WORKFLOW = "My Workflow";
const RUN_URL = "https://github.com/owner/repo/actions/runs/99";

describe("messages_header", () => {
beforeAll(() => {
originalPromptsDir = process.env.GH_AW_PROMPTS_DIR;
process.env.GH_AW_PROMPTS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "../md");
vi.resetModules();
({ getBodyHeader, getDisclosureHeader, DEFAULT_DISCLOSURE_HEADER, DISCLOSURE_HEADER_DEFAULT_SENTINEL } = require("./messages_header.cjs"));
});

afterAll(() => {
if (originalPromptsDir === undefined) {
delete process.env.GH_AW_PROMPTS_DIR;
} else {
process.env.GH_AW_PROMPTS_DIR = originalPromptsDir;
}
});

beforeEach(() => {
vi.clearAllMocks();
delete process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
Expand Down Expand Up @@ -73,4 +96,66 @@ describe("messages_header", () => {
expect(result).toBe(`> Header ${WORKFLOW} {unknown_placeholder}`);
});
});

describe("getDisclosureHeader", () => {
it("returns empty string when disclosure-header is not configured", () => {
const result = getDisclosureHeader({ workflowName: WORKFLOW, runUrl: RUN_URL });
expect(result).toBe("");
});
Comment thread
pelikhan marked this conversation as resolved.

it("returns rendered default text when disclosure-header is sentinel", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({
disclosureHeader: DISCLOSURE_HEADER_DEFAULT_SENTINEL,
});

const result = getDisclosureHeader({ workflowName: WORKFLOW, runUrl: RUN_URL });
expect(result).toContain(WORKFLOW);
expect(result).not.toContain(RUN_URL);
expect(result).not.toContain("πŸ€–");
});

it("returns rendered default text when disclosure-header is boolean true", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({
disclosureHeader: true,
});

const result = getDisclosureHeader({ workflowName: WORKFLOW, runUrl: RUN_URL });
expect(result).toContain(WORKFLOW);
expect(result).not.toContain(RUN_URL);
expect(result).not.toContain("πŸ€–");
});

it("returns rendered custom template when disclosure-header is a string", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({
disclosureHeader: "> πŸ€– Custom disclosure for [{workflow_name}]({run_url}).",
});

const result = getDisclosureHeader({ workflowName: WORKFLOW, runUrl: RUN_URL });
expect(result).toBe(`> πŸ€– Custom disclosure for [${WORKFLOW}](${RUN_URL}).`);
});

it("returns empty string when messages env is set but disclosure-header is absent", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({
footer: "> Custom footer",
});

const result = getDisclosureHeader({ workflowName: WORKFLOW, runUrl: RUN_URL });
expect(result).toBe("");
});

it("returns empty string when disclosure-header is boolean false", () => {
process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({
disclosureHeader: false,
});

const result = getDisclosureHeader({ workflowName: WORKFLOW, runUrl: RUN_URL });
expect(result).toBe("");
});

it("DEFAULT_DISCLOSURE_HEADER contains expected placeholders", () => {
expect(DEFAULT_DISCLOSURE_HEADER).toContain("{workflow_name}");
expect(DEFAULT_DISCLOSURE_HEADER).not.toContain("{run_url}");
expect(DEFAULT_DISCLOSURE_HEADER).not.toContain("πŸ€–");
});
});
});
1 change: 1 addition & 0 deletions actions/setup/md/safe_outputs_disclosure_header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
> **Automated content** generated by the {workflow_name} workflow.
14 changes: 14 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -10498,6 +10498,20 @@
"description": "Custom header text prepended to every message body generated by safe outputs (issues, comments, pull requests, discussions). Applied after any threat-detection caution alert and before the agent-generated content. Available placeholders: {workflow_name}, {run_url}.",
"examples": ["> ⚠️ This content was generated by [{workflow_name}]({run_url}).", "> πŸ€– AI-generated output β€” please review before acting."]
},
"disclosure-header": {
"oneOf": [
{
"type": "boolean",
"description": "Set to true to prepend the built-in AI authorship disclosure header to every message body (issues, comments, pull requests, discussions). Set to false to disable disclosure-header. The default text states the content was automatically generated and was not written or reviewed by the account owner personally."
},
Comment thread
pelikhan marked this conversation as resolved.
{
"type": "string",
"description": "Custom AI authorship disclosure header prepended to every message body. Available placeholders: {workflow_name}, {run_url}.",
"examples": ["> πŸ€– This content was generated by [{workflow_name}]({run_url}) and was not written by the account owner."]
}
],
"description": "AI authorship disclosure header prepended to every message body. Set to true for built-in default text, or provide a custom template string. Insertion order from top to bottom: threat-detection caution alert (if any) β†’ disclosure-header β†’ body-header β†’ agent-generated content. Available placeholders: {workflow_name}, {run_url}."
},
"append-only-comments": {
"type": "boolean",
"description": "When enabled, workflow completion notifier creates a new comment instead of editing the activation comment. Creates an append-only timeline of workflow runs. Default: false",
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/safe_outputs_config_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ type SafeOutputMessagesConfig struct {
AgentFailureIssue string `yaml:"agent-failure-issue,omitempty" json:"agentFailureIssue,omitempty"` // Custom footer template for agent failure tracking issues
AgentFailureComment string `yaml:"agent-failure-comment,omitempty" json:"agentFailureComment,omitempty"` // Custom footer template for comments on agent failure tracking issues
BodyHeader string `yaml:"body-header,omitempty" json:"bodyHeader,omitempty"` // Custom header text prepended to every message body (issues, comments, PRs, discussions). Placeholders: {workflow_name}, {run_url}
DisclosureHeader string `yaml:"disclosure-header,omitempty" json:"disclosureHeader,omitempty"` // AI authorship disclosure header prepended to every message body. Set to "true" for built-in default text, or provide a custom template string. Placeholders: {workflow_name}, {run_url}
}

// MentionsConfig holds configuration for @mention filtering in safe outputs
Expand Down
14 changes: 14 additions & 0 deletions pkg/workflow/safe_outputs_messages_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (

var safeOutputMessagesLog = logger.New("workflow:safe_outputs_config_messages")

const disclosureHeaderDefaultSentinel = "true"

// ========================================
// Safe Output Messages Configuration
// ========================================
Expand Down Expand Up @@ -42,6 +44,18 @@ func parseMessagesConfig(messagesMap map[string]any) *SafeOutputMessagesConfig {
config.AgentFailureComment = extractStringFromMap(messagesMap, "agent-failure-comment", nil)
config.BodyHeader = extractStringFromMap(messagesMap, "body-header", nil)

// Handle disclosure-header: can be bool (true for default built-in text) or custom string
if dh, exists := messagesMap["disclosure-header"]; exists {
switch v := dh.(type) {
case bool:
if v {
config.DisclosureHeader = disclosureHeaderDefaultSentinel
}
case string:
config.DisclosureHeader = v
}
}
Comment thread
pelikhan marked this conversation as resolved.

return config
}

Expand Down
Loading
Loading