Skip to content

Commit 0012ca2

Browse files
pelikhanCopilotCopilotgh-aw-botgithub-actions[bot]
authored
Refactor issue and comment Markdown into report trees (#72415)
* Refactor issue and comment Markdown into report trees Compose typed bodies, diagnostics, recovery instructions, and attribution before publication rendering. Preserve metadata, protected footers, managed memory, and literal Jira ADF contracts. Add an issue-markdown skill and regression coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Trim report publication bodies centrally Normalize trailing whitespace before sizing publication output and remove redundant caller trims and newline allowances. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test: isolate notification footer run ID fixtures Exercise exact attribution markers with and without GITHUB_RUN_ID so the activation-link fence regression test is deterministic in CI. Restore the inherited run ID after each case. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Remove obsolete string-based PR fallback helpers Use direct typed-builder tests to retain patch preview, push error, and fallback composition coverage, and remove stale helper references. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test: distinguish serialized and published report whitespace Keep redaction publication expectations aligned with automatic trailing-whitespace removal while asserting low-level serialization still ends with a newline. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Fix Markdown publication regressions and raw-input metadata boundaries Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> * Add changeset * Use a tip label inside the firewall warning Replace a misplaced alert marker inside the warning details with a bold label, retain the guidance, and refresh the consolidated preview. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Preserve summary subscripts and render firewall tips as alerts Keep supported summary subscript nodes instead of stripping their wrapper. Render the firewall tip as a sibling alert, verify all five GitHub alert kinds round-trip through the AST, and refresh the consolidated preview. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Fix footer mentions, empty managed memory, and type directives Preserve the resolved mention allowlist when composing project status footers, retain protected empty memory fences and update identity, and restore the github-script type directive before requires. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent d5a51e0 commit 0012ca2

130 files changed

Lines changed: 4432 additions & 2328 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.changeset/patch-canonical-report-rendering.md‎

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
---
2+
name: issue-markdown
3+
description: Build and refactor issue, comment, and review bodies in actions/setup/js using typed report trees and a single publication render.
4+
---
5+
6+
# Issue and comment Markdown
7+
8+
Use this skill when generating or changing Markdown published to issues, comments,
9+
or reviews, especially in `actions/setup/js/`. Build a `Report` tree first; render
10+
only the complete document. Do not assemble Markdown using interpolation,
11+
concatenation, `body +=`, or arrays of Markdown strings joined with newlines.
12+
Parsing an assembled string afterward is not a tree-first refactor.
13+
14+
## Find publication paths
15+
16+
Search production `.cjs` files for `issues.create`, `issues.update`,
17+
`createComment`, `updateComment`, `createReview`, `createReplyForReviewComment`,
18+
and GraphQL body variables. Trace each body back through shared helpers, fallback
19+
comments, retries, staged previews, and append/prepend/replace-island updates.
20+
Exclude test fixtures, HTTP payload assembly, and step-summary-only output.
21+
Jira ADF text is not Markdown: preserve its literal entities, Unicode, and
22+
paragraphs instead of routing it through the Markdown parser or renderer.
23+
24+
## Use the existing tree
25+
26+
`actions/setup/js/report_ast.cjs` defines `Report`, `ReportBlock`, and `ReportInline`.
27+
Use JSDoc annotations so `npm run typecheck` verifies node kinds:
28+
29+
```javascript
30+
/** @type {import("./report_ast.cjs").Report} */
31+
const report = {
32+
type: "report",
33+
title: "Workflow result",
34+
children: [
35+
{
36+
type: "paragraph",
37+
agentTainted: false,
38+
children: [
39+
{ type: "text", value: "Created related issue: ", agentTainted: false },
40+
{ type: "issue_reference", number: String(issue.number), agentTainted: false },
41+
],
42+
},
43+
{ type: "code_block", language: "text", value: diagnostic, agentTainted: true },
44+
],
45+
};
46+
```
47+
48+
Use `heading`, `paragraph`, `list`, `table`, `details`, `quote`, `alert`,
49+
`separator`, `code_block`, and typed inline nodes instead of Markdown punctuation.
50+
GitHub `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, and `[!CAUTION]`
51+
map to `alert` nodes with the matching `kind`; the renderer emits their markers.
52+
Keep alerts separate rather than nesting them inside another alert, which GitHub
53+
does not support. Preserve supported `<sub>` formatting in details summaries.
54+
Use `xml_marker`, `json_marker`, `expiration`, and `closing_reference` for machine
55+
metadata and closing links. Preserve `xml_marker.compact` for identifiers whose
56+
exact spelling is used by cleanup. Use `review_marker` for the durable,
57+
URI-encoded review caller reference; keep the authoritative marker last.
58+
Dynamic labels, paths, errors, and identifiers belong
59+
in text/code nodes; link destinations belong in `link.url`.
60+
61+
Import existing agent/user Markdown with `parseReportMarkdown`, specifying
62+
`agentTainted: true`, the correct `allowedMentions`, and the existing heading and
63+
metadata policy. Parse configured templates before scalar substitution using
64+
`context`; use typed inline/block slots for inserted nodes. Never interpolate
65+
untrusted values into a template before parsing it. A literal code fence in
66+
imported content must not swallow attribution or adjacent generated blocks.
67+
68+
For managed comment memory, set `code_block.language` to
69+
`gh-aw-comment-memory:<id>` and `fenceLength: 6`; rendering lengthens the fence
70+
past embedded backticks. Do not construct or slice fences by hand.
71+
72+
## Compose shared attribution
73+
74+
Use the typed variants instead of rendering a helper and reparsing its output:
75+
76+
| Purpose | Tree-first API |
77+
| --- | --- |
78+
| Generated attribution and guard notices | `buildGeneratedFooterBlocks` / `assembleReportBodyParts` in `markdown_body_helpers.cjs` |
79+
| Deferred footer templates and diagnostic sections | `buildFooterBlocks(getFooterMessageParts(context))` in `messages_footer.cjs` |
80+
| Configured policy footer | `getBodyFooterBlocks` or `appendConfiguredReportFooter` |
81+
| Detection warning | `getDetectionCautionBlocks` |
82+
| Failure-comment attribution | `getFooterAgentFailureCommentBlocks` |
83+
| Authoritative execution metadata | `appendReportExecutionProvenance` |
84+
85+
Keep custom templates and their scalar context separate until composition.
86+
Headers and footers may jointly open/close `<details>`; use
87+
`buildIssueBodyReport` to parse these wrappers together around body slots.
88+
Allocate unique slot names against both source text and previously allocated
89+
slots, and reject wrappers that swallow slots inside code fences.
90+
Internal framework notes carried between handlers use non-JSON-forgeable Symbols
91+
such as `COMMENT_PREAMBLE` and `ISSUE_BODY_SUFFIX`, never agent-settable flags.
92+
93+
## Preserve publication contracts
94+
95+
Framework-authored nodes use `agentTainted: false`; agent-owned descendants retain
96+
their taint even inside framework wrappers. Keep required footer diagnostics,
97+
metadata, and policy text protected during size fitting. Do not mark an entire
98+
agent body trusted to keep it from being shortened.
99+
100+
For attributed safe-output issues and comments, use `buildIssueBodyReport` in
101+
`issue_body.cjs`; it accepts Markdown or typed body blocks and retains configured
102+
wrappers, attribution, expiration, and execution provenance. Comment callers set
103+
the appropriate `itemType` and `workflowIdMarker: false`. Pass the original
104+
workflow `runUrl` when the target repository differs from the execution repository.
105+
Fallback issues must import the original agent body and compose recovery nodes,
106+
not reparse an already assembled or rendered PR/discussion description.
107+
108+
For other issue producers, pass a typed report to
109+
`renderIssueParameters`/`createReportIssue` in `issue_report.cjs`, preserving the
110+
existing authentication and retry boundary. `buildIssueReport` accepts typed
111+
blocks as well as imported Markdown. For comments, render the complete tree with
112+
`renderSizedReport` from `report_size.cjs` and pass its body to the existing API.
113+
Do not render fragments and concatenate or reparse them. Keep one logical render
114+
boundary, including metadata and trusted footers in its size budget.
115+
`renderSizedReport` trims trailing whitespace automatically and measures the
116+
publication body against the exact `maxLength`. Do not trim its result or reserve
117+
an extra character for a terminal newline. Low-level `renderReport` retains its
118+
terminal newline for Markdown serialization. Never truncate rendered Markdown
119+
or trusted policy text to fit.
120+
121+
## Validate the migration
122+
123+
Preserve footer on/off behavior, marker identity and order, mention/link limits,
124+
expiration, cross-repository attribution, structured state recovery, managed
125+
memory extraction, review fallbacks, and append/prepend/island semantics.
126+
Assert tree shape as well as rendered output. Cover hostile backticks and HTML
127+
delimiters, Unicode, escaped table cells, and the exact publication size limit.
128+
Update formatting expectations only for intentional canonical rendering, not to
129+
hide lost content or metadata.
130+
131+
Run the matching Vitest suites from `actions/setup/js`, `npm run typecheck`,
132+
and the repository JavaScript formatting/lint gates. Follow the developer skill
133+
for final repository validation and publication.

‎AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ When the relevant skill is not obvious, first discover candidates locally and th
7070
- Query helpers for issues/PRs/workflows/discussions/labels → matching `.github/skills/github-*-query/SKILL.md`
7171
- Security-focused reviews of agentic workflow changes (compile/scans/audit/optimization) → `.github/skills/review-agentic-workflows/SKILL.md`
7272
- Doc-writing conventions → `.github/skills/documentation/SKILL.md`
73+
- Issue/comment Markdown construction and refactoring → `.github/skills/issue-markdown/SKILL.md`
7374
- Visual changes to the docs site or landing page (layout, widths, spacing, colour, header nav) → `.github/skills/docs-design/SKILL.md`
7475
- Reviewing or writing `git`/`gh`/remote operations against checkouts (per-checkout credentials, sparse/shallow monorepos, safe-outputs MCP runs without credentials) → `.github/skills/checkout-credential-review/SKILL.md`
7576
- Authoring, validating, or debugging canvas extensions (loopback servers, actions, iframe rendering, state model, theme tokens) → `.github/skills/create-canvas/SKILL.md`

‎actions/setup/js/add_comment.cjs‎

Lines changed: 13 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,27 @@
55
* @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction
66
*/
77

8-
const { assembleMarkdownBodyParts } = require("./markdown_body_helpers.cjs");
9-
const { appendExecutionProvenance } = require("./execution_provenance.cjs");
10-
const { getBodyFooterMessage } = require("./messages_footer.cjs");
8+
const { buildIssueBodyReport, COMMENT_PREAMBLE } = require("./issue_body.cjs");
9+
const { parseReportMarkdown } = require("./report_markdown.cjs");
10+
const { stripExecutionProvenance } = require("./execution_provenance.cjs");
1111
const { generateWorkflowCallIdMarker, matchesWorkflowId } = require("./generate_footer.cjs");
1212
const { getRepositoryUrl } = require("./get_repository_url.cjs");
1313
const { replaceTemporaryIdReferences, resolveSafeOutputIssueTarget } = require("./temporary_id.cjs");
14-
const { getTrackerID } = require("./get_tracker_id.cjs");
1514
const { getErrorMessage } = require("./error_helpers.cjs");
1615
const { parseBoolTemplatable, parseIntTemplatable } = require("./templatable.cjs");
1716
const { resolveTarget, isStagedMode } = require("./safe_output_helpers.cjs");
1817
const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs");
1918
const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs");
20-
const { getMissingInfoSections } = require("./missing_messages_helper.cjs");
2119
const { getMessages } = require("./messages_core.cjs");
22-
const { getBodyHeader, getDisclosureHeader } = require("./messages_header.cjs");
2320
const { sanitizeContent } = require("./sanitize_content.cjs");
2421
const { MAX_COMMENT_LENGTH, MAX_COMMENT_INPUT_LENGTH, MAX_MENTIONS, MAX_LINKS, enforceCommentLimits } = require("./comment_limit_helpers.cjs");
25-
const { fitMarkdownBody } = require("./report_size.cjs");
22+
const { renderSizedReport } = require("./report_size.cjs");
2623
const { createDiscussionComment, resolveTopLevelDiscussionCommentId } = require("./github_api_helpers.cjs");
2724
const { logStagedPreviewInfo } = require("./staged_preview.cjs");
2825
const { ERR_CONFIG, ERR_NOT_FOUND } = require("./error_codes.cjs");
2926
const { isPayloadUserBot } = require("./resolve_mentions.cjs");
3027
const { getMentionsGithubClient, resolveMentionsForItem } = require("./resolve_mentions_from_payload.cjs");
3128
const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs");
32-
const { generateHistoryUrl } = require("./generate_history_link.cjs");
3329
const { resolveInvocationContext } = require("./invocation_context_helpers.cjs");
3430

3531
/** @type {string} Safe output type handled by this module */
@@ -829,92 +825,21 @@ async function main(config = {}) {
829825
}
830826

831827
const linkBody = processedBody;
832-
const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow";
833828
const runUrl = buildWorkflowRunUrl(context, effectiveEventContext.workflowRepo ?? context.repo);
834-
const workflowSource = process.env.GH_AW_WORKFLOW_SOURCE ?? "";
835-
const workflowSourceURL = process.env.GH_AW_WORKFLOW_SOURCE_URL ?? "";
836-
837-
// Compute caution first so prefix assembly preserves the original execution order.
838-
const detectionCaution = assembleMarkdownBodyParts({
839-
includeFooter: false,
840-
workflowName,
841-
runUrl,
842-
}).detectionCaution;
843-
844-
// Inject body header and disclosure header if configured (placed after caution, before user content)
845-
const bodyHeader = getBodyHeader({ workflowName, runUrl });
846-
const disclosureHeader = getDisclosureHeader({ workflowName, runUrl });
847-
848-
// Build prefix: caution (if any) → disclosure header (if any) → body header (if any) → user content
849-
const prefixParts = [detectionCaution, disclosureHeader, bodyHeader].filter(Boolean);
850-
const bodyPrefix = prefixParts.length > 0 ? prefixParts.join("\n\n") + "\n\n" : "";
851-
let bodySuffix = "";
852-
853-
// Add tracker ID and footer
854-
const trackerIDComment = getTrackerID("markdown");
855-
if (trackerIDComment) {
856-
bodySuffix += "\n\n" + trackerIDComment;
857-
}
858-
859-
// Get triggering context for footer
860-
const triggeringIssueNumber = context.payload.issue?.number;
861-
const triggeringPRNumber = context.payload.pull_request?.number;
862-
const triggeringDiscussionNumber = context.payload.discussion?.number;
863-
864-
// Generate history URL with type= based on execution context
865-
const historyUrl =
866-
generateHistoryUrl({
867-
owner: repoParts.owner,
868-
repo: repoParts.repo,
869-
itemType: isDiscussion ? "discussion_comment" : "comment",
870-
workflowCallId: callerWorkflowId,
871-
workflowId,
872-
serverUrl: context.serverUrl,
873-
}) || undefined;
874-
875-
const markdownParts = assembleMarkdownBodyParts({
876-
includeFooter,
877-
workflowName,
829+
const bodyNodes = parseReportMarkdown(stripExecutionProvenance(linkBody, { stripGroupingMarkers: true }), { minimumHeadingLevel: 2, allowMetadata: false, agentTainted: true, allowedMentions: allowedMentionAliases });
830+
const report = buildIssueBodyReport("Comment", [...(message[COMMENT_PREAMBLE] || []), ...bodyNodes], {
831+
repo: repoParts,
832+
footer: includeFooter,
833+
bodyFooter: config.body_footer,
834+
allowedMentions: allowedMentionAliases,
835+
itemType: isDiscussion ? "discussion_comment" : "comment",
878836
runUrl,
879-
workflowSource,
880-
workflowSourceURL,
881-
triggeringIssueNumber,
882-
triggeringPRNumber,
883-
triggeringDiscussionNumber,
884-
historyUrl,
885-
markerWhenFooterDisabled: "xml",
837+
workflowIdMarker: false,
886838
});
887-
888-
if (includeFooter) {
889-
// When footer is enabled, add full footer with attribution and XML markers.
890-
bodySuffix += "\n\n" + markdownParts.footer;
891-
} else {
892-
// When footer is disabled, only add XML marker for searchability (no visible attribution text)
893-
bodySuffix += "\n\n" + markdownParts.noFooterMarker;
894-
}
895-
const bodyFooter = getBodyFooterMessage(config.body_footer, { workflowName, runUrl });
896-
if (bodyFooter) {
897-
bodySuffix += "\n\n" + bodyFooter.trimEnd();
898-
}
899-
900-
// Add workflow-call-id marker when available to allow close-older-comments to
901-
// distinguish callers that share the same reusable workflow (and GH_AW_WORKFLOW_ID)
902-
if (callerWorkflowId) {
903-
bodySuffix += "\n" + generateWorkflowCallIdMarker(callerWorkflowId);
904-
}
905-
906-
const framedBody = bodyPrefix + linkBody + bodySuffix;
907-
processedBody = appendExecutionProvenance(framedBody, workflowName, runUrl);
908839
// Enforce max limits again after adding footer and metadata
909840
// Length and mention limits include metadata; only agent content counts toward the link limit.
910841
try {
911-
if (processedBody.length > MAX_COMMENT_LENGTH) {
912-
// Provenance replaces reserved markers; reserve its actual net overhead, not duplicate markers.
913-
const fittingLimit = MAX_COMMENT_LENGTH + framedBody.length - processedBody.length;
914-
if (fittingLimit < 1) throw new Error(`E006: Protected execution metadata exceeds the maximum body length of ${MAX_COMMENT_LENGTH} characters`);
915-
processedBody = fitMarkdownBody(linkBody, { prefix: bodyPrefix, suffix: bodySuffix, maxLength: fittingLimit, allowedMentions: allowedMentionAliases });
916-
processedBody = appendExecutionProvenance(processedBody, workflowName, runUrl);
917-
}
842+
processedBody = renderSizedReport(report, MAX_COMMENT_LENGTH).body;
918843
enforceCommentLimits(processedBody, { maxLinks, linkBody });
919844
} catch (error) {
920845
const errorMessage = getErrorMessage(error);

0 commit comments

Comments
 (0)