From 4f7e7583458c7eaf5c6c407c66ef473b7d541b55 Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Wed, 21 Oct 2026 12:01:23 -0700 Subject: [PATCH 01/10] ci: post the cli-interface label and comment from a workflow_run job Signed-off-by: Nikolay Petrov --- .github/workflows/cli-interface-comment.yml | 117 +++++++++ .../test_cli_interface_comment_workflow.py | 236 ++++++++++++++++++ 2 files changed, 353 insertions(+) create mode 100644 .github/workflows/cli-interface-comment.yml create mode 100644 dokimi/tests/test_cli_interface_comment_workflow.py diff --git a/.github/workflows/cli-interface-comment.yml b/.github/workflows/cli-interface-comment.yml new file mode 100644 index 00000000..11a83c66 --- /dev/null +++ b/.github/workflows/cli-interface-comment.yml @@ -0,0 +1,117 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +name: cli-interface-comment + +# Applies the `cli-interface-change` label and the sticky comment for the result that +# the cli-interface workflow uploads. That workflow runs on pull_request, so on a fork +# PR its token is read-only; this one runs in the base repository's context after it +# finishes, with a write token. +# +# Security: this job never checks out or runs PR code. The artifact is data produced by +# PR-controlled code, so it is untrusted: its size is capped, it is parsed as JSON only, +# the PR number must be an integer, the PR's current head must be the commit the run was +# for, and the text is only ever passed to the REST API as a body (no shell, no eval). +on: + workflow_run: + workflows: [cli-interface] + types: [completed] + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_sha }} + cancel-in-progress: false + +jobs: + post: + name: Label and comment + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' + runs-on: ${{ github.repository_owner == 'intel' && 'intel-ubuntu-latest' || 'ubuntu-latest' }} + permissions: + actions: read # list and download the run's artifact + pull-requests: write # create/apply the label + issues: write # github.rest.issues.* APIs for labels and comments + steps: + - name: Validate the result and post it + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const fs = require('fs'); + const os = require('os'); + const path = require('path'); + const { execFileSync } = require('child_process'); + const { owner, repo } = context.repo; + const run = context.payload.workflow_run; + const label = 'cli-interface-change'; + const marker = ''; + const MAX_ZIP_BYTES = 256 * 1024; + const MAX_JSON_BYTES = 200 * 1024; + const MAX_DIFF_CHARS = 60000; + + const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, + { owner, repo, run_id: run.id }); + const art = artifacts.find((a) => a.name === 'cli-interface-result' && !a.expired && + a.workflow_run && a.workflow_run.id === run.id); + if (!art) { core.warning('No cli-interface-result artifact on the run.'); return; } + if (!(art.size_in_bytes <= MAX_ZIP_BYTES)) { + core.warning(`Artifact is ${art.size_in_bytes} bytes, over the ${MAX_ZIP_BYTES} cap.`); + return; + } + const dl = await github.rest.actions.downloadArtifact({ owner, repo, + artifact_id: art.id, archive_format: 'zip' }); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-interface-')); + const zip = path.join(dir, 'a.zip'); + fs.writeFileSync(zip, Buffer.from(dl.data)); + if (fs.statSync(zip).size > MAX_ZIP_BYTES) { core.warning('Downloaded artifact too large.'); return; } + let raw; + try { + // argv only, no shell; exactly the one expected member is read, size-capped. + raw = execFileSync('unzip', ['-p', zip, 'result.json'], { maxBuffer: MAX_JSON_BYTES }); + } catch (e) { core.warning(`Could not read result.json: ${e.message}`); return; } + let res; + try { res = JSON.parse(raw.toString('utf8')); } catch (e) { + core.warning('result.json is not valid JSON.'); return; + } + const bad = (why) => core.warning(`Ignoring the artifact: ${why}`); + if (res === null || typeof res !== 'object' || Array.isArray(res)) return bad('not an object'); + const issue_number = res.pr_number; + if (!Number.isSafeInteger(issue_number) || issue_number <= 0) return bad('pr_number is not a positive integer'); + if (res.head_sha !== run.head_sha) return bad('head_sha differs from the run\'s head'); + if (!['0', '1', 'error'].includes(res.changed)) return bad('changed is not one of 0, 1, error'); + if (typeof res.diff !== 'string' || res.diff.length > MAX_DIFF_CHARS) return bad('diff is missing or too long'); + + // The number is only a claim of the PR's code: check it is a PR of this + // repository whose current head is the commit this run was for. + const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: issue_number }); + if (pr.base.repo.full_name !== `${owner}/${repo}`) return bad('PR is not in this repository'); + if (pr.head.sha !== run.head_sha) { core.info('A newer commit was pushed; skipping the stale result.'); return; } + + // Text derived from the PR: no mentions, and no forged marker. + const diff = res.diff.split(marker).join('').replace(/@/g, '@​'); + const comments = await github.paginate(github.rest.issues.listComments, + { owner, repo, issue_number }); + const existing = comments.find((c) => c.user?.login === 'github-actions[bot]' && + c.body && c.body.includes(marker)); + const put = async (body) => existing + ? github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }) + : github.rest.issues.createComment({ owner, repo, issue_number, body }); + + if (res.changed === 'error') { + await put(`${marker}\n${diff}`); // surface unknown: leave the label as it is + } else if (res.changed === '1') { + try { + await github.rest.issues.createLabel({ owner, repo, name: label, color: 'B60205', + description: "Changes dokimi's user-facing CLI surface" }); + } catch (e) { /* already exists */ } + await github.rest.issues.addLabels({ owner, repo, issue_number, labels: [label] }); + await put(`${marker}\n${diff}\n_Updated on every push; the \`${label}\` ` + + 'label is on while the diff is non-empty._'); + } else { + try { + await github.rest.issues.removeLabel({ owner, repo, issue_number, name: label }); + } catch (e) { /* was not applied */ } + if (existing) { + await put(`${marker}\nThe CLI surface now matches the base branch: no user-facing CLI change.`); + } + } diff --git a/dokimi/tests/test_cli_interface_comment_workflow.py b/dokimi/tests/test_cli_interface_comment_workflow.py new file mode 100644 index 00000000..6e1a2cc3 --- /dev/null +++ b/dokimi/tests/test_cli_interface_comment_workflow.py @@ -0,0 +1,236 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""cli-interface-comment.yml: a write-token workflow_run job that must stay safe. + +Static checks pin the trust boundary; behavioural checks run the embedded script under +node with a fake GitHub client, feeding it good and hostile artifacts. +""" + +from __future__ import annotations + +import io +import json +import re +import shutil +import subprocess +import zipfile +from pathlib import Path + +import pytest +import yaml + +WORKFLOW = Path(__file__).resolve().parents[2] / ".github/workflows/cli-interface-comment.yml" +SHA = "a" * 40 +MARKER = "" + + +def _wf() -> dict: + return yaml.safe_load(WORKFLOW.read_text(encoding="utf-8")) + + +def _trigger(wf: dict) -> dict: + return wf.get("on") or wf[True] # PyYAML reads the bare key `on` as True + + +def test_runs_only_after_cli_interface_and_never_checks_out_code(): + wf = _wf() + assert set(_trigger(wf)) == {"workflow_run"} + assert _trigger(wf)["workflow_run"]["workflows"] == ["cli-interface"] + text = WORKFLOW.read_text(encoding="utf-8") + assert "actions/checkout" not in text + assert "pull_request_target" not in text + assert "github.event.workflow_run.event == 'pull_request'" in wf["jobs"]["post"]["if"] + + +def test_permissions_are_minimal(): + wf = _wf() + assert wf["permissions"] == {} + assert wf["jobs"]["post"]["permissions"] == { + "actions": "read", + "pull-requests": "write", + "issues": "write", + } + + +def test_actions_are_sha_pinned_and_no_run_steps_interpolate_expressions(): + for step in _wf()["jobs"]["post"]["steps"]: + assert re.fullmatch(r"[\w./-]+@[0-9a-f]{40}", step["uses"]), step + assert "run" not in step + + +# ---- behaviour, under node ------------------------------------------------------------ + +_HARNESS = r""" +const fs = require('fs'); +const [scriptFile, caseFile] = process.argv.slice(2); +const c = JSON.parse(fs.readFileSync(caseFile, 'utf8')); +const calls = []; +const rec = (name, ret) => async (a) => { calls.push([name, a]); return ret; }; +const rest = { + actions: { listWorkflowRunArtifacts: 'list', downloadArtifact: async (a) => { + calls.push(['download', a]); return { data: fs.readFileSync(c.zip) }; } }, + pulls: { get: async (a) => { calls.push(['pulls.get', a]); return { data: c.pr }; } }, + issues: { listComments: 'comments', createLabel: rec('createLabel'), addLabels: rec('addLabels'), + removeLabel: rec('removeLabel'), createComment: rec('createComment'), + updateComment: rec('updateComment') }, +}; +const github = { rest, paginate: async (fn, a) => fn === 'list' ? c.artifacts : c.comments }; +const context = { repo: { owner: 'intel', repo: 'dokimi' }, + payload: { workflow_run: { id: 7, head_sha: c.run_sha } } }; +const core = { warning: (m) => calls.push(['warning', m]), info: () => {} }; +const script = JSON.parse(fs.readFileSync(scriptFile, 'utf8')); +const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; +new AsyncFunction('github', 'context', 'core', 'require', script)(github, context, core, require) + .then(() => console.log(JSON.stringify(calls)), + (e) => { console.log(JSON.stringify([['throw', String(e)]])); }); +""" + + +def _run( + tmp_path: Path, + result, + *, + size: int | None = None, + pr_sha: str = SHA, + run_sha: str = SHA, + comments=(), + pr_repo: str = "intel/dokimi", + artifact_run: int = 7, +): + node = shutil.which("node") + if not (node and shutil.which("unzip")): + pytest.skip("needs node and unzip") + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w") as z: + z.writestr("result.json", result if isinstance(result, str) else json.dumps(result)) + zpath = tmp_path / "a.zip" + zpath.write_bytes(buf.getvalue()) + script = next(s["with"]["script"] for s in _wf()["jobs"]["post"]["steps"]) + (tmp_path / "script.json").write_text(json.dumps(script), encoding="utf-8") + (tmp_path / "harness.js").write_text(_HARNESS, encoding="utf-8") + case = { + "zip": str(zpath), + "run_sha": run_sha, + "comments": list(comments), + "pr": {"head": {"sha": pr_sha}, "base": {"repo": {"full_name": pr_repo}}}, + "artifacts": [ + { + "id": 3, + "name": "cli-interface-result", + "expired": False, + "size_in_bytes": size if size is not None else len(buf.getvalue()), + "workflow_run": {"id": artifact_run}, + } + ], + } + (tmp_path / "case.json").write_text(json.dumps(case), encoding="utf-8") + out = subprocess.run( + [ + node, + str(tmp_path / "harness.js"), + str(tmp_path / "script.json"), + str(tmp_path / "case.json"), + ], + check=True, + capture_output=True, + text=True, + ) + return json.loads(out.stdout) + + +def _names(calls): + return [c[0] for c in calls] + + +def _good(**kw): + return { + "pr_number": 12, + "head_sha": SHA, + "changed": "1", + "diff": "## CLI diff\n+ `--x`\n", + **kw, + } + + +def test_changed_result_labels_and_creates_the_sticky_comment(tmp_path): + calls = _run(tmp_path, _good()) + kinds = _names(calls) + assert {"addLabels", "createComment"} <= set(kinds) + body = next(a for n, a in calls if n == "createComment") + assert body["issue_number"] == 12 and body["body"].startswith(MARKER) + assert next(a for n, a in calls if n == "addLabels")["labels"] == ["cli-interface-change"] + + +def test_existing_sticky_comment_is_updated_not_duplicated(tmp_path): + calls = _run( + tmp_path, + _good(), + comments=[{"id": 99, "user": {"login": "github-actions[bot]"}, "body": MARKER + "\nold"}], + ) + assert "createComment" not in _names(calls) + assert next(a for n, a in calls if n == "updateComment")["comment_id"] == 99 + + +def test_unchanged_result_removes_the_label(tmp_path): + calls = _run(tmp_path, _good(changed="0")) + assert "removeLabel" in _names(calls) and "addLabels" not in _names(calls) + assert "createComment" not in _names(calls) + + +def test_error_result_comments_and_leaves_the_label(tmp_path): + calls = _run(tmp_path, _good(changed="error")) + assert "createComment" in _names(calls) + assert not {"addLabels", "removeLabel"} & set(_names(calls)) + + +def test_mentions_and_forged_markers_in_the_diff_are_neutralised(tmp_path): + calls = _run(tmp_path, _good(diff=f"{MARKER} hi @octocat")) + body = next(a for n, a in calls if n == "createComment")["body"] + assert body.count(MARKER) == 1 and "@octocat" not in body + + +@pytest.mark.parametrize( + "bad", + [ + _good(pr_number="12"), + _good(pr_number=1.5), + _good(pr_number=-3), + _good(pr_number=True), + _good(pr_number=None), + _good(head_sha="b" * 40), + _good(changed="yes"), + _good(changed=1), + _good(diff=None), + _good(diff="x" * 60001), + [1, 2], + "not json", + "null", + ], +) +def test_malformed_artifacts_post_nothing(tmp_path, bad): + calls = _run(tmp_path, bad) + assert not {"createComment", "updateComment", "addLabels", "removeLabel", "pulls.get"} & set( + _names(calls) + ) + assert "warning" in _names(calls) + + +def test_stale_result_for_an_older_head_is_skipped(tmp_path): + calls = _run(tmp_path, _good(), pr_sha="c" * 40) + assert not {"createComment", "addLabels"} & set(_names(calls)) + + +def test_pr_number_of_another_repository_is_refused(tmp_path): + calls = _run(tmp_path, _good(), pr_repo="other/repo") + assert not {"createComment", "addLabels"} & set(_names(calls)) + + +def test_oversized_artifact_is_not_downloaded(tmp_path): + calls = _run(tmp_path, _good(), size=10**9) + assert "download" not in _names(calls) + + +def test_artifact_from_another_run_is_ignored(tmp_path): + calls = _run(tmp_path, _good(), artifact_run=8) + assert "download" not in _names(calls) From f780f1827ae4f13960a9b7bc59f9d6390efad5b3 Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Wed, 21 Oct 2026 12:29:07 -0700 Subject: [PATCH 02/10] ci(cli-interface): suppress zizmor dangerous-triggers on the audited workflow_run poster Signed-off-by: Nikolay Petrov --- .github/workflows/cli-interface-comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cli-interface-comment.yml b/.github/workflows/cli-interface-comment.yml index 11a83c66..936751a8 100644 --- a/.github/workflows/cli-interface-comment.yml +++ b/.github/workflows/cli-interface-comment.yml @@ -13,7 +13,7 @@ name: cli-interface-comment # the PR number must be an integer, the PR's current head must be the commit the run was # for, and the text is only ever passed to the REST API as a body (no shell, no eval). on: - workflow_run: + workflow_run: # zizmor: ignore[dangerous-triggers] -- intended: no checkout, untrusted artifact is validated (see above) workflows: [cli-interface] types: [completed] From a176a67e2c1c7268d074899d444075717dc53845 Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Wed, 21 Oct 2026 14:43:12 -0700 Subject: [PATCH 03/10] fix(ci): fence and cap the cli-interface comment; serialise posts per PR branch Signed-off-by: Nikolay Petrov --- .github/workflows/cli-interface-comment.yml | 19 +++++++++-- .../test_cli_interface_comment_workflow.py | 32 ++++++++++++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cli-interface-comment.yml b/.github/workflows/cli-interface-comment.yml index 936751a8..8fc49684 100644 --- a/.github/workflows/cli-interface-comment.yml +++ b/.github/workflows/cli-interface-comment.yml @@ -20,7 +20,9 @@ on: permissions: {} concurrency: - group: ${{ github.workflow }}-${{ github.event.workflow_run.head_sha }} + # One group per PR branch (pull_requests[] is empty for forks), so pushes to one PR + # post one at a time and cannot both find no sticky comment and each create one. + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }} cancel-in-progress: false jobs: @@ -87,8 +89,19 @@ jobs: if (pr.base.repo.full_name !== `${owner}/${repo}`) return bad('PR is not in this repository'); if (pr.head.sha !== run.head_sha) { core.info('A newer commit was pushed; skipping the stale result.'); return; } - // Text derived from the PR: no mentions, and no forged marker. - const diff = res.diff.split(marker).join('').replace(/@/g, '@​'); + // Text derived from the PR: no forged marker, no mentions, and it is shown + // verbatim in a code fence (longer than any backtick run inside) so links, + // images, HTML and #N references do not render. The cap is applied after + // the mention rewrite, which adds characters, so the body stays under + // GitHub's 65,536 limit. + let text = res.diff.split(marker).join('').replace(/@/g, '@\u200b'); + if (text.length > MAX_DIFF_CHARS) { + text = text.slice(0, MAX_DIFF_CHARS).replace(/[\ud800-\udbff]$/, '') + + '\n... (truncated)'; + } + const ticks = Math.max(2, ...(text.match(/`+/g) || []).map((t) => t.length)) + 1; + const fence = '`'.repeat(ticks); + const diff = `${fence}diff\n${text.replace(/\n$/, '')}\n${fence}`; const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }); const existing = comments.find((c) => c.user?.login === 'github-actions[bot]' && diff --git a/dokimi/tests/test_cli_interface_comment_workflow.py b/dokimi/tests/test_cli_interface_comment_workflow.py index 6e1a2cc3..0b22a78f 100644 --- a/dokimi/tests/test_cli_interface_comment_workflow.py +++ b/dokimi/tests/test_cli_interface_comment_workflow.py @@ -72,7 +72,7 @@ def test_actions_are_sha_pinned_and_no_run_steps_interpolate_expressions(): calls.push(['download', a]); return { data: fs.readFileSync(c.zip) }; } }, pulls: { get: async (a) => { calls.push(['pulls.get', a]); return { data: c.pr }; } }, issues: { listComments: 'comments', createLabel: rec('createLabel'), addLabels: rec('addLabels'), - removeLabel: rec('removeLabel'), createComment: rec('createComment'), + removeLabel: c.fail_labels ? async () => { throw new Error('404'); } : rec('removeLabel'), createComment: rec('createComment'), updateComment: rec('updateComment') }, }; const github = { rest, paginate: async (fn, a) => fn === 'list' ? c.artifacts : c.comments }; @@ -97,6 +97,7 @@ def _run( comments=(), pr_repo: str = "intel/dokimi", artifact_run: int = 7, + fail_labels: bool = False, ): node = shutil.which("node") if not (node and shutil.which("unzip")): @@ -113,6 +114,7 @@ def _run( "zip": str(zpath), "run_sha": run_sha, "comments": list(comments), + "fail_labels": fail_labels, "pr": {"head": {"sha": pr_sha}, "base": {"repo": {"full_name": pr_repo}}}, "artifacts": [ { @@ -178,6 +180,14 @@ def test_unchanged_result_removes_the_label(tmp_path): assert "createComment" not in _names(calls) +def test_unchanged_result_rewrites_an_existing_comment_even_if_removing_the_label_fails(tmp_path): + comments = [{"id": 99, "user": {"login": "github-actions[bot]"}, "body": MARKER + "\nold"}] + calls = _run(tmp_path, _good(changed="0"), comments=comments, fail_labels=True) + assert "throw" not in _names(calls) + body = next(a for n, a in calls if n == "updateComment")["body"] + assert body.startswith(MARKER) and "matches the base branch" in body + + def test_error_result_comments_and_leaves_the_label(tmp_path): calls = _run(tmp_path, _good(changed="error")) assert "createComment" in _names(calls) @@ -234,3 +244,23 @@ def test_oversized_artifact_is_not_downloaded(tmp_path): def test_artifact_from_another_run_is_ignored(tmp_path): calls = _run(tmp_path, _good(), artifact_run=8) assert "download" not in _names(calls) + + +def test_many_mentions_cannot_push_the_body_past_githubs_limit(tmp_path): + calls = _run(tmp_path, _good(diff="@" * 60000)) + body = next(a for n, a in calls if n == "createComment")["body"] + assert len(body) < 65536 and "truncated" in body + + +def test_diff_is_fenced_so_links_and_references_do_not_render(tmp_path): + diff = "[x](http://evil) ![i](http://t/p.png) intel/dokimi#1 ``` break out" + body = next(a for n, a in _run(tmp_path, _good(diff=diff)) if n == "createComment")["body"] + lines = body.split("\n") + assert lines[1].startswith("````diff") and lines[-2].startswith("````") + assert not lines[-2].startswith("`````") + + +def test_concurrency_is_per_pr_branch_not_per_commit(): + group = _wf()["concurrency"]["group"] + assert "head_branch" in group and "head_repository.full_name" in group + assert "head_sha" not in group From a0bd9ba70c046c9f4cdf997f11518a77a8a7c823 Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Wed, 21 Oct 2026 20:42:32 -0700 Subject: [PATCH 04/10] fix(ci): break every HTML comment opener in the cli-interface diff, not one marker pass Signed-off-by: Nikolay Petrov --- .github/workflows/cli-interface-comment.yml | 5 +++-- dokimi/tests/test_cli_interface_comment_workflow.py | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cli-interface-comment.yml b/.github/workflows/cli-interface-comment.yml index 8fc49684..9492ff96 100644 --- a/.github/workflows/cli-interface-comment.yml +++ b/.github/workflows/cli-interface-comment.yml @@ -89,12 +89,13 @@ jobs: if (pr.base.repo.full_name !== `${owner}/${repo}`) return bad('PR is not in this repository'); if (pr.head.sha !== run.head_sha) { core.info('A newer commit was pushed; skipping the stale result.'); return; } - // Text derived from the PR: no forged marker, no mentions, and it is shown + // Text derived from the PR: no forged marker (every HTML-comment opener is + // broken in one pass, so a nested marker cannot reassemble), no mentions, and it is shown // verbatim in a code fence (longer than any backtick run inside) so links, // images, HTML and #N references do not render. The cap is applied after // the mention rewrite, which adds characters, so the body stays under // GitHub's 65,536 limit. - let text = res.diff.split(marker).join('').replace(/@/g, '@\u200b'); + let text = res.diff.replace(/" + calls = _run(tmp_path, _good(diff=nested)) + body = next(a for n, a in calls if n == "createComment")["body"] + assert body.count(MARKER) == 1 + + @pytest.mark.parametrize( "bad", [ From 7bcb4c9277226cdf16c86425a5cae632d3e56a31 Mon Sep 17 00:00:00 2001 From: Alexander Rybkin Date: Wed, 21 Oct 2026 20:41:13 -0700 Subject: [PATCH 05/10] fix(ci): bound the cli-interface comment fence The fence was one longer than the longest backtick run, so a diff of backticks under the 60,000-character cap produced a body of up to 180,000 characters, which GitHub rejects after the label is applied. Split runs of 8+ backticks before the cap so the fence is at most 8. Signed-off-by: Alexander Rybkin --- .github/workflows/cli-interface-comment.yml | 8 +++++--- dokimi/tests/test_cli_interface_comment_workflow.py | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cli-interface-comment.yml b/.github/workflows/cli-interface-comment.yml index 9492ff96..ae22c1a6 100644 --- a/.github/workflows/cli-interface-comment.yml +++ b/.github/workflows/cli-interface-comment.yml @@ -93,9 +93,11 @@ jobs: // broken in one pass, so a nested marker cannot reassemble), no mentions, and it is shown // verbatim in a code fence (longer than any backtick run inside) so links, // images, HTML and #N references do not render. The cap is applied after - // the mention rewrite, which adds characters, so the body stays under - // GitHub's 65,536 limit. - let text = res.diff.replace(/