feat: add gh board-row upsert
This commit is contained in:
@@ -109,6 +109,40 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
].join("\n");
|
||||
let boardIssueBody = boardIssueBodyInitial;
|
||||
let boardIssueUpdatedAt = "2026-05-20T01:00:00Z";
|
||||
const upsertBoardIssueBodyInitial = [
|
||||
"# Upsert Board",
|
||||
"",
|
||||
"## 看板(OPEN)",
|
||||
"",
|
||||
"| Issue | GitHub 状态 | Category | Branch | Summary | 验收状态 | 相关 Code Queue 任务 | 当前关注点 | 进度 |",
|
||||
"| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
|
||||
"| #70 | OPEN | cli | master | existing summary | pass | cq-70 | existing focus | doing |",
|
||||
"| #72 | OPEN | cli | master | duplicate open | pass | cq-72 | duplicate open | doing |",
|
||||
"| #79 | OPEN | defect | master | bad row | pass | cq-79 | missing progress |",
|
||||
"",
|
||||
"## 看板(CLOSED)",
|
||||
"",
|
||||
"| Issue | GitHub 状态 | Category | Branch | Summary | 验收状态 | 相关 Code Queue 任务 | 当前关注点 | 进度 |",
|
||||
"| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
|
||||
"| #71 | CLOSED | ops | master | closed summary | pass | — | archived focus | done |",
|
||||
"| #72 | CLOSED | ops | master | duplicate closed | pass | — | duplicate closed | done |",
|
||||
"",
|
||||
"## 更新 2026-05-21 10:00 北京时间",
|
||||
"",
|
||||
"- 表后的更新段落必须保留。",
|
||||
"",
|
||||
].join("\n");
|
||||
let upsertBoardIssueBody = upsertBoardIssueBodyInitial;
|
||||
let upsertBoardIssueUpdatedAt = "2026-05-20T01:30:00Z";
|
||||
const upsertBoardIssue = {
|
||||
...issue,
|
||||
id: 2062,
|
||||
number: 62,
|
||||
title: "Upsert board fixture",
|
||||
body: upsertBoardIssueBody,
|
||||
html_url: "https://github.com/pikasTech/unidesk/issues/62",
|
||||
updated_at: upsertBoardIssueUpdatedAt,
|
||||
};
|
||||
const legacyBoardIssue = {
|
||||
...issue,
|
||||
id: 2600,
|
||||
@@ -413,6 +447,10 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
sendJson(res, 200, legacyBoardIssue);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues/62") {
|
||||
sendJson(res, 200, { ...upsertBoardIssue, body: upsertBoardIssueBody, updated_at: upsertBoardIssueUpdatedAt });
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues/20/comments?per_page=100") {
|
||||
sendJson(res, 200, comments);
|
||||
return;
|
||||
@@ -466,6 +504,13 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
sendJson(res, 200, { ...issue, body: boardIssueBody, updated_at: boardIssueUpdatedAt });
|
||||
return;
|
||||
}
|
||||
if (req.method === "PATCH" && req.url === "/repos/pikasTech/unidesk/issues/62") {
|
||||
const parsed = JSON.parse(body) as JsonRecord;
|
||||
upsertBoardIssueBody = String(parsed.body ?? upsertBoardIssueBody);
|
||||
upsertBoardIssueUpdatedAt = "2026-05-20T01:35:00Z";
|
||||
sendJson(res, 200, { ...upsertBoardIssue, body: upsertBoardIssueBody, updated_at: upsertBoardIssueUpdatedAt });
|
||||
return;
|
||||
}
|
||||
if (req.method === "POST" && req.url === "/repos/pikasTech/unidesk/issues/20/comments") {
|
||||
const parsed = JSON.parse(body) as JsonRecord;
|
||||
sendJson(res, 201, { id: 9001, body: String(parsed.body ?? ""), html_url: "https://github.com/pikasTech/unidesk/issues/20#issuecomment-9001", user: { login: "tester" }, created_at: "2026-05-20T06:00:00Z", updated_at: "2026-05-20T06:00:00Z" });
|
||||
@@ -528,11 +573,13 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
assertCondition(usage.some((line) => line.includes("gh issue board-row list")), "gh help should list board-row list", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh issue board-row update")), "gh help should list board-row update", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh issue board-row add")), "gh help should list board-row add", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh issue board-row upsert")), "gh help should list board-row upsert", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh issue board-row move")), "gh help should list board-row move", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh issue board-row delete")), "gh help should list board-row delete", { usage });
|
||||
assertCondition(notes.some((line) => line.includes("canonical read path")), "gh help should state issue read is canonical", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("compatibility alias")), "gh help should state issue view is alias", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("board-row update changes one table cell")), "gh help should describe board-row update safety", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("board-row upsert updates an existing row")), "gh help should describe board-row upsert safety", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("board-row add/move/delete are row-scoped")), "gh help should describe board-row row mutation safety", { notes });
|
||||
|
||||
const mock = await startMockGitHub();
|
||||
@@ -658,6 +705,176 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
assertCondition(Array.isArray(boardRowGetRow.cells) && boardRowGetRow.cells[1] === "OPEN", "board-row get should expose the GitHub status column", boardRowGetRow);
|
||||
assertCondition(boardRowGetFields.branch === "master" && boardRowGetFields.status === "pass" && boardRowGetFields.validation === "pass" && boardRowGetFields.tasks === "cq-35" && String(boardRowGetFields.focus ?? "").includes("当前关注点"), "board-row get should expose canonical field aliases", boardRowGetFields);
|
||||
|
||||
const boardRowUpsertUpdateRequestCountBefore = mock.requests.length;
|
||||
const boardRowUpsertUpdate = await runCli([
|
||||
"gh", "issue", "board-row", "upsert", "70",
|
||||
"--repo", "pikasTech/unidesk",
|
||||
"--board-issue", "62",
|
||||
"--section", "open",
|
||||
"--summary", "中文 `code` [#20](https://github.com/pikasTech/unidesk/issues/20) A | B\nsecond line",
|
||||
"--focus", "焦点 A | B\n下一行",
|
||||
"--validation", "manual pass",
|
||||
"--progress", "reviewing",
|
||||
"--dry-run",
|
||||
], env);
|
||||
assertCondition(boardRowUpsertUpdate.status === 0, "board-row upsert existing row should dry-run update", boardRowUpsertUpdate.json ?? { stdout: boardRowUpsertUpdate.stdout, stderr: boardRowUpsertUpdate.stderr });
|
||||
const boardRowUpsertUpdateData = dataOf(boardRowUpsertUpdate.json ?? {});
|
||||
assertCondition(boardRowUpsertUpdateData.command === "issue board-row upsert" && boardRowUpsertUpdateData.operation === "update" && boardRowUpsertUpdateData.dryRun === true && boardRowUpsertUpdateData.planned === true, "upsert existing row should report update operation", boardRowUpsertUpdateData);
|
||||
const boardRowUpsertUpdatePlan = boardRowUpsertUpdateData.upsert as JsonRecord;
|
||||
assertCondition(boardRowUpsertUpdatePlan.operation === "update" && boardRowUpsertUpdatePlan.section === "open", "upsert update plan should stay in existing section", boardRowUpsertUpdatePlan);
|
||||
assertCondition(boardRowUpsertUpdatePlan.oldRow === "| #70 | OPEN | cli | master | existing summary | pass | cq-70 | existing focus | doing |", "upsert update should expose old row", boardRowUpsertUpdatePlan);
|
||||
assertCondition(boardRowUpsertUpdatePlan.newRow === "| #70 | OPEN | cli | master | 中文 `code` [#20](https://github.com/pikasTech/unidesk/issues/20) A \\| B second line | manual pass | cq-70 | 焦点 A \\| B 下一行 | reviewing |", "upsert update should escape pipes, preserve backticks/link text, and fold real newlines", boardRowUpsertUpdatePlan);
|
||||
const boardRowUpsertUpdateSafety = boardRowUpsertUpdateData.bodyOnlySafety as JsonRecord;
|
||||
const boardRowUpsertUpdateNewBody = boardRowUpsertUpdateSafety.newBody as JsonRecord;
|
||||
assertCondition(boardRowUpsertUpdateNewBody.containsLiteralBackslashN === false && boardRowUpsertUpdateNewBody.containsBackticks === true && boardRowUpsertUpdateNewBody.containsMarkdownTable === true, "upsert update should preserve markdown safety signals", boardRowUpsertUpdateNewBody);
|
||||
const boardRowUpsertUpdateWriteCount = mock.requests.slice(boardRowUpsertUpdateRequestCountBefore).filter((request) => request.method === "PATCH").length;
|
||||
assertCondition(boardRowUpsertUpdateWriteCount === 0, "board-row upsert update dry-run must not PATCH GitHub", { requests: mock.requests.slice(boardRowUpsertUpdateRequestCountBefore) });
|
||||
|
||||
const boardRowUpsertAddRequestCountBefore = mock.requests.length;
|
||||
const boardRowUpsertAdd = await runCli([
|
||||
"gh", "issue", "board-row", "upsert", "73",
|
||||
"--repo", "pikasTech/unidesk",
|
||||
"--board-issue", "62",
|
||||
"--section", "open",
|
||||
"--category", "cli",
|
||||
"--branch", "master",
|
||||
"--tasks", "cq-73",
|
||||
"--summary", "新增中文 `code` [#20](https://github.com/pikasTech/unidesk/issues/20) A | B\n真实换行",
|
||||
"--focus", "关注 | 重点\nnext",
|
||||
"--validation", "pending",
|
||||
"--progress", "queued",
|
||||
"--dry-run",
|
||||
], env);
|
||||
assertCondition(boardRowUpsertAdd.status === 0, "board-row upsert missing row should dry-run add", boardRowUpsertAdd.json ?? { stdout: boardRowUpsertAdd.stdout, stderr: boardRowUpsertAdd.stderr });
|
||||
const boardRowUpsertAddData = dataOf(boardRowUpsertAdd.json ?? {});
|
||||
assertCondition(boardRowUpsertAddData.command === "issue board-row upsert" && boardRowUpsertAddData.operation === "add" && boardRowUpsertAddData.dryRun === true && boardRowUpsertAddData.planned === true, "upsert missing row should report add operation", boardRowUpsertAddData);
|
||||
const boardRowUpsertAddPlan = boardRowUpsertAddData.upsert as JsonRecord;
|
||||
assertCondition(boardRowUpsertAddPlan.operation === "add" && boardRowUpsertAddPlan.section === "open" && Number(boardRowUpsertAddPlan.insertAfterLine ?? 0) > 0, "upsert add should expose insertion plan", boardRowUpsertAddPlan);
|
||||
assertCondition(boardRowUpsertAddPlan.newRow === "| #73 | OPEN | cli | master | 新增中文 `code` [#20](https://github.com/pikasTech/unidesk/issues/20) A \\| B 真实换行 | pending | cq-73 | 关注 \\| 重点 next | queued |", "upsert add should generate a full escaped row", boardRowUpsertAddPlan);
|
||||
const boardRowUpsertAddNewBody = ((boardRowUpsertAddData.bodyOnlySafety as JsonRecord).newBody as JsonRecord);
|
||||
assertCondition(boardRowUpsertAddNewBody.containsLiteralBackslashN === false && boardRowUpsertAddNewBody.containsBackticks === true, "upsert add should not introduce literal backslash-n", boardRowUpsertAddNewBody);
|
||||
const boardRowUpsertAddWriteCount = mock.requests.slice(boardRowUpsertAddRequestCountBefore).filter((request) => request.method === "PATCH").length;
|
||||
assertCondition(boardRowUpsertAddWriteCount === 0, "board-row upsert add dry-run must not PATCH GitHub", { requests: mock.requests.slice(boardRowUpsertAddRequestCountBefore) });
|
||||
|
||||
const boardRowUpsertNoGuardRequestCountBefore = mock.requests.length;
|
||||
const boardRowUpsertNoGuard = await runCli([
|
||||
"gh", "issue", "board-row", "upsert", "74",
|
||||
"--repo", "pikasTech/unidesk",
|
||||
"--board-issue", "62",
|
||||
"--section", "open",
|
||||
"--category", "cli",
|
||||
"--branch", "master",
|
||||
"--tasks", "cq-74",
|
||||
"--summary", "formal write omitted guard",
|
||||
"--focus", "focus",
|
||||
"--validation", "pending",
|
||||
"--progress", "queued",
|
||||
], env);
|
||||
assertCondition(boardRowUpsertNoGuard.status === 0, "board-row upsert without guard should stay on the dry-run path", boardRowUpsertNoGuard.json ?? { stdout: boardRowUpsertNoGuard.stdout, stderr: boardRowUpsertNoGuard.stderr });
|
||||
const boardRowUpsertNoGuardData = dataOf(boardRowUpsertNoGuard.json ?? {});
|
||||
assertCondition(boardRowUpsertNoGuardData.dryRun === true && boardRowUpsertNoGuardData.planned === true && boardRowUpsertNoGuardData.operation === "add", "upsert without guard should not PATCH GitHub", boardRowUpsertNoGuardData);
|
||||
const boardRowUpsertNoGuardWriteCount = mock.requests.slice(boardRowUpsertNoGuardRequestCountBefore).filter((request) => request.method === "PATCH").length;
|
||||
assertCondition(boardRowUpsertNoGuardWriteCount === 0, "board-row upsert without guard must not PATCH GitHub", { requests: mock.requests.slice(boardRowUpsertNoGuardRequestCountBefore) });
|
||||
|
||||
const boardRowUpsertListBeforeWrite = await runCli(["gh", "issue", "board-row", "list", "--repo", "pikasTech/unidesk", "--board-issue", "62", "--state", "all"], env);
|
||||
assertCondition(boardRowUpsertListBeforeWrite.status === 0, "upsert board list before guarded write should succeed", boardRowUpsertListBeforeWrite.json ?? { stdout: boardRowUpsertListBeforeWrite.stdout, stderr: boardRowUpsertListBeforeWrite.stderr });
|
||||
const boardRowUpsertBoardSha = String((dataOf(boardRowUpsertListBeforeWrite.json ?? {}).boardIssue as JsonRecord).bodySha ?? "");
|
||||
const boardRowUpsertWriteRequestCountBefore = mock.requests.length;
|
||||
const boardRowUpsertWrite = await runCli([
|
||||
"gh", "issue", "board-row", "upsert", "73",
|
||||
"--repo", "pikasTech/unidesk",
|
||||
"--board-issue", "62",
|
||||
"--section", "open",
|
||||
"--category", "cli",
|
||||
"--branch", "master",
|
||||
"--tasks", "cq-73",
|
||||
"--summary", "guarded add keeps table trailer",
|
||||
"--focus", "focus",
|
||||
"--validation", "pending",
|
||||
"--progress", "queued",
|
||||
"--expect-body-sha", boardRowUpsertBoardSha,
|
||||
], env);
|
||||
assertCondition(boardRowUpsertWrite.status === 0, "board-row upsert with expect body sha should PATCH", boardRowUpsertWrite.json ?? { stdout: boardRowUpsertWrite.stdout, stderr: boardRowUpsertWrite.stderr });
|
||||
const boardRowUpsertWriteData = dataOf(boardRowUpsertWrite.json ?? {});
|
||||
assertCondition(boardRowUpsertWriteData.dryRun === false && boardRowUpsertWriteData.rest === true && boardRowUpsertWriteData.operation === "add", "upsert guarded write should report real add", boardRowUpsertWriteData);
|
||||
const boardRowUpsertWriteRequests = mock.requests.slice(boardRowUpsertWriteRequestCountBefore).filter((request) => request.method === "PATCH" && request.url === "/repos/pikasTech/unidesk/issues/62");
|
||||
assertCondition(boardRowUpsertWriteRequests.length === 1, "board-row upsert should send exactly one PATCH", { requests: mock.requests.slice(boardRowUpsertWriteRequestCountBefore) });
|
||||
const boardRowUpsertWritePayload = JSON.parse(boardRowUpsertWriteRequests[0]?.body ?? "{}") as JsonRecord;
|
||||
const boardRowUpsertWrittenBody = String(boardRowUpsertWritePayload.body ?? "");
|
||||
assertCondition(boardRowUpsertWrittenBody.includes("| #73 | OPEN | cli | master | guarded add keeps table trailer | pending | cq-73 | focus | queued |"), "upsert write payload should include generated row", boardRowUpsertWritePayload);
|
||||
assertCondition(boardRowUpsertWrittenBody.includes("## 看板(CLOSED)") && boardRowUpsertWrittenBody.includes("## 更新 2026-05-21 10:00 北京时间") && boardRowUpsertWrittenBody.includes("- 表后的更新段落必须保留。"), "upsert write should preserve CLOSED table and post-table update section", boardRowUpsertWritePayload);
|
||||
|
||||
const boardRowUpsertStale = await runCli([
|
||||
"gh", "issue", "board-row", "upsert", "76",
|
||||
"--repo", "pikasTech/unidesk",
|
||||
"--board-issue", "62",
|
||||
"--section", "open",
|
||||
"--category", "cli",
|
||||
"--branch", "master",
|
||||
"--tasks", "cq-76",
|
||||
"--summary", "stale guard must fail",
|
||||
"--focus", "focus",
|
||||
"--validation", "pending",
|
||||
"--progress", "queued",
|
||||
"--expect-body-sha", boardRowUpsertBoardSha,
|
||||
], env);
|
||||
assertCondition(boardRowUpsertStale.status !== 0, "stale board-row upsert should fail structurally", boardRowUpsertStale.json ?? { stdout: boardRowUpsertStale.stdout, stderr: boardRowUpsertStale.stderr });
|
||||
const boardRowUpsertStaleData = failedDataOf(boardRowUpsertStale.json ?? {});
|
||||
assertCondition(boardRowUpsertStaleData.degradedReason === "validation-failed" && boardRowUpsertStaleData.command === "issue board-row upsert", "stale board-row upsert should be validation-failed", boardRowUpsertStaleData);
|
||||
|
||||
const boardRowUpsertDuplicate = await runCli([
|
||||
"gh", "issue", "board-row", "upsert", "72",
|
||||
"--repo", "pikasTech/unidesk",
|
||||
"--board-issue", "62",
|
||||
"--section", "open",
|
||||
"--focus", "duplicate should fail",
|
||||
"--dry-run",
|
||||
], env);
|
||||
assertCondition(boardRowUpsertDuplicate.status !== 0, "duplicate board-row upsert should fail structurally", boardRowUpsertDuplicate.json ?? { stdout: boardRowUpsertDuplicate.stdout, stderr: boardRowUpsertDuplicate.stderr });
|
||||
const boardRowUpsertDuplicateData = failedDataOf(boardRowUpsertDuplicate.json ?? {});
|
||||
assertCondition(String((boardRowUpsertDuplicateData.details as JsonRecord).message ?? "").includes("ambiguous"), "duplicate upsert should report ambiguous row", boardRowUpsertDuplicateData);
|
||||
|
||||
const boardRowUpsertSectionConflict = await runCli([
|
||||
"gh", "issue", "board-row", "upsert", "70",
|
||||
"--repo", "pikasTech/unidesk",
|
||||
"--board-issue", "62",
|
||||
"--section", "closed",
|
||||
"--focus", "migration belongs to move",
|
||||
"--dry-run",
|
||||
], env);
|
||||
assertCondition(boardRowUpsertSectionConflict.status !== 0, "upsert section conflict should fail structurally", boardRowUpsertSectionConflict.json ?? { stdout: boardRowUpsertSectionConflict.stdout, stderr: boardRowUpsertSectionConflict.stderr });
|
||||
const boardRowUpsertSectionConflictData = failedDataOf(boardRowUpsertSectionConflict.json ?? {});
|
||||
assertCondition(String((boardRowUpsertSectionConflictData.details as JsonRecord).message ?? "").includes("use gh issue board-row move"), "upsert section conflict should point to move", boardRowUpsertSectionConflictData);
|
||||
|
||||
const boardRowUpsertMissingField = await runCli([
|
||||
"gh", "issue", "board-row", "upsert", "75",
|
||||
"--repo", "pikasTech/unidesk",
|
||||
"--board-issue", "62",
|
||||
"--section", "open",
|
||||
"--category", "cli",
|
||||
"--branch", "master",
|
||||
"--tasks", "cq-75",
|
||||
"--summary", "missing progress field",
|
||||
"--focus", "focus",
|
||||
"--validation", "pending",
|
||||
"--dry-run",
|
||||
], env);
|
||||
assertCondition(boardRowUpsertMissingField.status !== 0, "upsert add missing required generated cell should fail", boardRowUpsertMissingField.json ?? { stdout: boardRowUpsertMissingField.stdout, stderr: boardRowUpsertMissingField.stderr });
|
||||
const boardRowUpsertMissingFieldData = failedDataOf(boardRowUpsertMissingField.json ?? {});
|
||||
const boardRowUpsertMissingFieldDetails = boardRowUpsertMissingFieldData.details as JsonRecord;
|
||||
assertCondition(String(boardRowUpsertMissingFieldDetails.message ?? "").includes("requires values") && Array.isArray(boardRowUpsertMissingFieldData.missingFields) && (boardRowUpsertMissingFieldData.missingFields as unknown[]).includes("progress"), "upsert missing field should be structured", boardRowUpsertMissingFieldData);
|
||||
|
||||
const boardRowUpsertColumnMismatch = await runCli([
|
||||
"gh", "issue", "board-row", "upsert", "79",
|
||||
"--repo", "pikasTech/unidesk",
|
||||
"--board-issue", "62",
|
||||
"--focus", "bad existing row",
|
||||
"--dry-run",
|
||||
], env);
|
||||
assertCondition(boardRowUpsertColumnMismatch.status !== 0, "upsert existing row with column mismatch should fail", boardRowUpsertColumnMismatch.json ?? { stdout: boardRowUpsertColumnMismatch.stdout, stderr: boardRowUpsertColumnMismatch.stderr });
|
||||
const boardRowUpsertColumnMismatchData = failedDataOf(boardRowUpsertColumnMismatch.json ?? {});
|
||||
assertCondition(String((boardRowUpsertColumnMismatchData.details as JsonRecord).message ?? "").includes("column count"), "upsert column mismatch should be structured", boardRowUpsertColumnMismatchData);
|
||||
|
||||
const boardRowDryRunRequestCountBefore = mock.requests.length;
|
||||
const boardRowDryRun = await runCli(["gh", "issue", "board-row", "update", "35", "--repo", "pikasTech/unidesk", "--board-issue", "20", "--field", "focus", "--value", "复核 A | B\nsecond line"], env);
|
||||
assertCondition(boardRowDryRun.status === 0, "board-row update should default to dry-run without concurrency expectation", boardRowDryRun.json ?? { stdout: boardRowDryRun.stdout, stderr: boardRowDryRun.stderr });
|
||||
@@ -1078,6 +1295,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
"issue cleanup-plan remains dry-run with body/comment cleanup suggestions",
|
||||
"issue board-audit reports missing open rows, closed/open section mismatches, missing closed rows, meta/brief-index ignores, and row validation warnings without writes",
|
||||
"issue board-row list/get expose parsed #20 rows without writes",
|
||||
"issue board-row upsert updates existing rows, adds missing rows, reports operation, preserves table trailers, rejects ambiguous rows, blocks stale body SHA writes, and stays dry-run without concurrency guards",
|
||||
"issue board-row add/delete without guard stay on dry-run and do not PATCH",
|
||||
"issue board-row update defaults to dry-run, reports old/new row, body SHA, guard result, and does not introduce literal backslash-n",
|
||||
"issue board-row update rejects literal backslash-n cell values",
|
||||
|
||||
Reference in New Issue
Block a user