diff --git a/actions/setup/js/comment_memory.cjs b/actions/setup/js/comment_memory.cjs
index 81889b6a90a..ffd71472f33 100644
--- a/actions/setup/js/comment_memory.cjs
+++ b/actions/setup/js/comment_memory.cjs
@@ -14,7 +14,7 @@ const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs");
const { getTrackerID } = require("./get_tracker_id.cjs");
const { generateHistoryUrl } = require("./generate_history_link.cjs");
const { enforceCommentLimits } = require("./comment_limit_helpers.cjs");
-const { COMMENT_MEMORY_TAG, COMMENT_MEMORY_MAX_SCAN_PAGES } = require("./comment_memory_helpers.cjs");
+const { COMMENT_MEMORY_TAG, COMMENT_MEMORY_MAX_SCAN_PAGES, COMMENT_MEMORY_CODE_FENCE } = require("./comment_memory_helpers.cjs");
// Require provenance marker to avoid accidentally updating user-authored comments
// that happen to contain a matching comment-memory tag.
const MANAGED_COMMENT_PROVENANCE_MARKER = "";
const COMMENT_MEMORY_PROMPT_END_MARKER = "";
+const COMMENT_MEMORY_CODE_FENCE = "``````";
+const ESCAPED_COMMENT_MEMORY_CODE_FENCE = COMMENT_MEMORY_CODE_FENCE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+
+function stripCommentMemoryCodeFence(content) {
+ const trimmed = typeof content === "string" ? content.trim() : "";
+ if (trimmed.length === 0) {
+ return "";
+ }
+ if (!trimmed.startsWith(COMMENT_MEMORY_CODE_FENCE)) {
+ return trimmed;
+ }
+ const match = trimmed.match(new RegExp(`^${ESCAPED_COMMENT_MEMORY_CODE_FENCE}[^\\n]*\\n([\\s\\S]*)\\n${ESCAPED_COMMENT_MEMORY_CODE_FENCE}$`));
+ if (!match) {
+ return trimmed;
+ }
+ return match[1].trim();
+}
function isSafeMemoryId(memoryId) {
if (typeof memoryId !== "string" || memoryId.length === 0 || memoryId.length > MAX_MEMORY_ID_LENGTH) {
@@ -57,7 +74,7 @@ function extractCommentMemoryEntries(commentBody, warn = () => {}) {
if (isSafeMemoryId(memoryId)) {
entries.push({
memoryId,
- content: (commentBody.slice(contentStart, closeStart) || "").trim(),
+ content: stripCommentMemoryCodeFence(commentBody.slice(contentStart, closeStart)),
});
} else {
warn(`skipping unsafe memory_id '${memoryId}'`);
@@ -99,7 +116,9 @@ module.exports = {
COMMENT_MEMORY_MAX_SCAN_EMPTY_PAGES,
COMMENT_MEMORY_PROMPT_START_MARKER,
COMMENT_MEMORY_PROMPT_END_MARKER,
+ COMMENT_MEMORY_CODE_FENCE,
isSafeMemoryId,
+ stripCommentMemoryCodeFence,
extractCommentMemoryEntries,
listCommentMemoryFiles,
resolveCommentMemoryConfig,
diff --git a/actions/setup/js/comment_memory_helpers.test.cjs b/actions/setup/js/comment_memory_helpers.test.cjs
index 0c7ef0d79a2..405929f9af3 100644
--- a/actions/setup/js/comment_memory_helpers.test.cjs
+++ b/actions/setup/js/comment_memory_helpers.test.cjs
@@ -1,12 +1,47 @@
import { describe, it, expect, vi } from "vitest";
-import { extractCommentMemoryEntries, isSafeMemoryId } from "./comment_memory_helpers.cjs";
+import { extractCommentMemoryEntries, isSafeMemoryId, stripCommentMemoryCodeFence } from "./comment_memory_helpers.cjs";
describe("comment_memory_helpers", () => {
it("extracts managed memory entries", () => {
+ const entries = extractCommentMemoryEntries('\n``````\nhello\n``````\n');
+ expect(entries).toEqual([{ memoryId: "default", content: "hello" }]);
+ });
+
+ it("supports legacy memory entries without code fence markers", () => {
const entries = extractCommentMemoryEntries('\nhello\n');
expect(entries).toEqual([{ memoryId: "default", content: "hello" }]);
});
+ it("keeps fenced text unchanged when trailing content exists after closing fence", () => {
+ const content = "``````\nhello\n``````\ntrailing";
+ expect(stripCommentMemoryCodeFence(content)).toBe(content);
+ });
+
+ it("keeps fenced text unchanged when closing fence is missing", () => {
+ const content = "``````\nhello";
+ expect(stripCommentMemoryCodeFence(content)).toBe(content);
+ });
+
+ it("keeps malformed fenced text unchanged", () => {
+ const content = "``````hello\n``````";
+ expect(stripCommentMemoryCodeFence(content)).toBe(content);
+ });
+
+ it("strips valid fenced text with extra newlines before content", () => {
+ const content = "``````\n\nhello\n``````";
+ expect(stripCommentMemoryCodeFence(content)).toBe("hello");
+ });
+
+ it("strips valid fenced text when content contains six-backtick lines", () => {
+ const content = "``````\nline 1\n``````\nline 2\n``````";
+ expect(stripCommentMemoryCodeFence(content)).toBe("line 1\n``````\nline 2");
+ });
+
+ it("keeps fenced text unchanged when closing fence has no leading newline", () => {
+ const content = "``````\nhello``````";
+ expect(stripCommentMemoryCodeFence(content)).toBe(content);
+ });
+
it("rejects unsafe memory IDs", () => {
const warning = vi.fn();
const entries = extractCommentMemoryEntries('\nhello\n', warning);
diff --git a/actions/setup/js/setup_comment_memory_files.test.cjs b/actions/setup/js/setup_comment_memory_files.test.cjs
index 1580880cf30..be2bf764834 100644
--- a/actions/setup/js/setup_comment_memory_files.test.cjs
+++ b/actions/setup/js/setup_comment_memory_files.test.cjs
@@ -35,7 +35,7 @@ describe("setup_comment_memory_files", () => {
it("extracts memory entries from managed comment body", async () => {
const module = await import("./setup_comment_memory_files.cjs");
- const entries = module.extractCommentMemoryEntries('\nhello\n');
+ const entries = module.extractCommentMemoryEntries('\n``````\nhello\n``````\n');
expect(entries).toEqual([{ memoryId: "default", content: "hello" }]);
});
@@ -47,7 +47,7 @@ describe("setup_comment_memory_files", () => {
listComments: vi.fn().mockResolvedValue({
data: [
{
- body: '\nSaved memory\n\nfooter',
+ body: '\n``````\nSaved memory\n``````\n\nfooter',
},
],
}),
@@ -76,7 +76,7 @@ describe("setup_comment_memory_files", () => {
});
}
if (page === 6) {
- return Promise.resolve({ data: [{ body: '\nLate memory\n' }] });
+ return Promise.resolve({ data: [{ body: '\n``````\nLate memory\n``````\n' }] });
}
return Promise.resolve({ data: [] });
});
@@ -156,7 +156,7 @@ describe("setup_comment_memory_files", () => {
})
);
const listComments = vi.fn().mockResolvedValue({
- data: [{ body: '\nCross repo memory\n' }],
+ data: [{ body: '\n``````\nCross repo memory\n``````\n' }],
});
global.github = {
rest: {
@@ -192,7 +192,7 @@ describe("setup_comment_memory_files", () => {
})
);
const listComments = vi.fn().mockResolvedValue({
- data: [{ body: '\nSame repo memory\n' }],
+ data: [{ body: '\n``````\nSame repo memory\n``````\n' }],
});
global.github = {
rest: {
diff --git a/pkg/cli/spec_test.go b/pkg/cli/spec_test.go
index c6b739b93fc..f46eabc88ec 100644
--- a/pkg/cli/spec_test.go
+++ b/pkg/cli/spec_test.go
@@ -1117,11 +1117,11 @@ func TestSpec_PublicAPI_ValidateWorkflowIntent(t *testing.T) {
// Spec: "Sets a field in frontmatter YAML"
func TestSpec_PublicAPI_UpdateFieldInFrontmatter(t *testing.T) {
tests := []struct {
- name string
- content string
- fieldName string
- fieldValue string
- wantErr bool
+ name string
+ content string
+ fieldName string
+ fieldValue string
+ wantErr bool
checkContains string
}{
{