Skip to content

Commit c9b6b0b

Browse files
committed
fix(mantis): stop git-backed evidence publishing
1 parent 59be6d6 commit c9b6b0b

5 files changed

Lines changed: 39 additions & 231 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ apps/macos/.build/
4141
apps/macos-mlx-tts/.build/
4242
apps/shared/MoltbotKit/.build/
4343
apps/shared/OpenClawKit/.build/
44+
apps/shared/*/.build/
4445
apps/shared/OpenClawKit/Package.resolved
4546
**/ModuleCache/
4647
bin/
@@ -50,6 +51,7 @@ apps/macos/.build-local/
5051
apps/macos/.swiftpm/
5152
apps/shared/MoltbotKit/.swiftpm/
5253
apps/shared/OpenClawKit/.swiftpm/
54+
apps/shared/*/.swiftpm/
5355
Core/
5456
apps/ios/*.xcodeproj/
5557
apps/ios/*.xcworkspace/
@@ -108,6 +110,9 @@ USER.md
108110
# local tooling
109111
.serena/
110112

113+
# local QA evidence mirrors; CI publishes canonical Mantis files as Actions artifacts
114+
mantis/
115+
111116
# Local project-agent skill installs. Only repo-owned skills are visible by
112117
# default; promoting a new repo skill should require an intentional `git add -f`.
113118
.agents/skills/*

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
2424
- Doctor/Codex: stop warning that the message tool is unavailable for source-reply paths where OpenClaw grants `message` at runtime, keeping update and doctor output aligned with the OpenAI happy path. Thanks @pashpashpash.
2525
- Build: keep externalized Slack, OpenShell sandbox, and Anthropic Vertex runtime dependency declarations out of the root dist artifact build.
2626
- Auto-reply/Claude CLI: bridge CLI-runtime assistant text-delta agent events into the chat reasoning preview through `onReasoningStream`, mirroring the existing assistant-text (#76914) and tool-event (#80046) bridges and adding gating so non-CLI runtimes are unaffected. Thanks @anagnorisis2peripeteia and @pashpashpash.
27+
- Mantis: keep QA evidence in Actions artifacts only and stop publishing evidence files to Git-backed artifact branches.
2728
- CLI/migrate: handle delayed Codex plugin marketplace responses so warnings, next-steps, and conflict states render with ⚠️ glyphs and post-install migration retries the marketplace fetch instead of silently skipping plugin items. (#81625) Thanks @sjf.
2829
- Channels/Weixin: bump the bundled `@tencent-weixin/openclaw-weixin` external entry to `2.4.3` (from `2.4.1`) so onboarding and `openclaw channels add` install the current Tencent Weixin (personal WeChat) plugin release. (#81730) Thanks @scotthuang.
2930
- CLI: lazy-load model, plugin, and device runtime helpers and keep channel option help on generated startup metadata or generic fallback text so parent/help output renders without importing those runtime paths.

docs/concepts/mantis.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,8 @@ This schema is the handoff between scenario code and GitHub comments:
318318
```
319319

320320
Artifact `path` values are relative to the manifest directory. `targetPath`
321-
values are relative paths under the `qa-artifacts` branch publish directory.
321+
values are relative paths inside the uploaded Actions artifact bundle. Mantis
322+
must not publish evidence to Git branches; Git history is not artifact storage.
322323
The publisher rejects path traversal and skips entries marked
323324
`"required": false` when optional previews or videos are unavailable.
324325

@@ -333,11 +334,11 @@ Supported artifact kinds:
333334
- `report`: Markdown report.
334335

335336
The reusable publisher is `scripts/mantis/publish-pr-evidence.mjs`. Workflows
336-
call it with the manifest, target PR, `qa-artifacts` target root, comment marker,
337-
Actions artifact URL, run URL, and request source. It copies declared artifacts
338-
to the `qa-artifacts` branch, builds a summary-first PR comment with inline
339-
images/previews and linked videos, then updates the existing marker comment or
340-
creates one.
337+
call it with the manifest, target PR, artifact root, comment marker, Actions
338+
artifact URL, run URL, and request source. The workflow uploads the declared
339+
files through `actions/upload-artifact`; the publisher builds a summary-first PR
340+
comment that links to that artifact and lists the bundled file names. It never
341+
commits or pushes evidence files.
341342

342343
You can also trigger the status-reactions run directly from a PR comment:
343344

@@ -627,10 +628,11 @@ after the new secret has been stored.
627628

628629
Mantis workflows should upload the full evidence bundle as a short-lived Actions
629630
artifact. When the workflow is run for a bug report or fix PR, it should also
630-
publish the redacted PNG screenshots to the `qa-artifacts` branch and upsert a
631-
comment on that bug or fix PR with inline before/after screenshots. Do not post
632-
the primary proof only on a generic QA automation PR. Raw logs, observed
633-
messages, and other bulky evidence stay in the Actions artifact.
631+
upsert a comment on that bug or fix PR with a short summary and a link to the
632+
Actions artifact. Do not post the primary proof only on a generic QA automation
633+
PR. Do not use Git branches, tags, or commits as Mantis artifact storage. Raw
634+
logs, screenshots, recordings, observed messages, and other bulky evidence stay
635+
in the Actions artifact.
634636

635637
Production workflows should post those comments with the Mantis GitHub App, not
636638
with `github-actions[bot]`. Store the app id and private key as

scripts/mantis/publish-pr-evidence.mjs

Lines changed: 10 additions & 207 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
#!/usr/bin/env node
2-
import { execFileSync, spawnSync } from "node:child_process";
2+
import { execFileSync } from "node:child_process";
33
import {
4-
copyFileSync,
54
existsSync,
6-
mkdirSync,
75
mkdtempSync,
86
readFileSync,
97
rmSync,
@@ -121,86 +119,12 @@ export function loadEvidenceManifest(manifestPath) {
121119
};
122120
}
123121

124-
function encodePathForUrl(input) {
125-
return input
126-
.split("/")
127-
.filter(Boolean)
128-
.map((part) => encodeURIComponent(part))
129-
.join("/");
130-
}
131-
132-
function artifactUrl(rawBase, artifact) {
133-
return `${rawBase}/${encodePathForUrl(artifact.targetPath)}`;
134-
}
135-
136-
function byLane(artifacts, kind) {
137-
const lanes = new Map();
138-
for (const artifact of artifacts) {
139-
if (artifact.kind !== kind) {
140-
continue;
141-
}
142-
lanes.set(artifact.lane, artifact);
143-
}
144-
return lanes;
145-
}
146-
147-
function findPair(artifacts, kind, leftLane, rightLane) {
148-
const lanes = byLane(artifacts, kind);
149-
const left = lanes.get(leftLane);
150-
const right = lanes.get(rightLane);
151-
return left && right ? { left, right } : null;
152-
}
153-
154-
function renderPairTable({ pair, rawBase }) {
155-
const { left, right } = pair;
156-
if (!left || !right) {
157-
return "";
158-
}
159-
return [
160-
'<table width="100%">',
161-
" <thead>",
162-
" <tr>",
163-
` <th width="50%">${left.label}</th>`,
164-
` <th width="50%">${right.label}</th>`,
165-
" </tr>",
166-
" </thead>",
167-
" <tbody>",
168-
" <tr>",
169-
` <td width="50%" align="center"><img src="${artifactUrl(rawBase, left)}" width="100%" alt="${left.alt ?? left.label}"></td>`,
170-
` <td width="50%" align="center"><img src="${artifactUrl(rawBase, right)}" width="100%" alt="${right.alt ?? right.label}"></td>`,
171-
" </tr>",
172-
" </tbody>",
173-
"</table>",
174-
"",
175-
].join("\n");
176-
}
177-
178-
function renderSingleImageTables({ artifacts, rawBase, pairedKeys }) {
179-
const renderedPairs = new Set(pairedKeys);
180-
return artifacts
181-
.filter(
182-
(artifact) => artifact.inline && !renderedPairs.has(`${artifact.kind}:${artifact.lane}`),
183-
)
184-
.map((artifact) => {
185-
const width = Math.min(Number(artifact.width ?? 720) || 720, 900);
186-
return [
187-
`**${artifact.label}**`,
188-
"",
189-
`<img src="${artifactUrl(rawBase, artifact)}" width="${width}" alt="${artifact.alt ?? artifact.label}">`,
190-
"",
191-
].join("\n");
192-
})
193-
.join("\n");
194-
}
195-
196-
function renderLinkList({ artifacts, kind, rawBase, title }) {
197-
const links = artifacts
198-
.filter((artifact) => artifact.kind === kind)
199-
.map((artifact) => `- [${artifact.label}](${artifactUrl(rawBase, artifact)})`);
122+
function renderArtifactFileList(artifacts) {
123+
const links = artifacts.map((artifact) => `- ${artifact.label}: \`${artifact.targetPath}\``);
200124
if (links.length === 0) {
201125
return "";
202126
}
203-
return [`${title}:`, ...links, ""].join("\n");
127+
return ["Artifact files:", ...links, ""].join("\n");
204128
}
205129

206130
function laneLine(label, lane) {
@@ -220,27 +144,15 @@ function laneLine(label, lane) {
220144
}
221145

222146
export function renderEvidenceComment({
223-
artifactRoot,
224147
artifactUrl: actionsArtifactUrl,
225148
manifest,
226149
marker,
227-
rawBase,
228150
requestSource,
229151
runUrl,
230-
treeUrl,
231152
}) {
232153
const comparison = manifest.comparison ?? {};
233154
const baseline = comparison.baseline;
234155
const candidate = comparison.candidate;
235-
const pairs = [
236-
findPair(manifest.artifacts, "timeline", "baseline", "candidate"),
237-
findPair(manifest.artifacts, "desktopScreenshot", "baseline", "candidate"),
238-
findPair(manifest.artifacts, "motionPreview", "baseline", "candidate"),
239-
].filter(Boolean);
240-
const pairedKeys = pairs.flatMap((pair) => [
241-
`${pair.left.kind}:${pair.left.lane}`,
242-
`${pair.right.kind}:${pair.right.lane}`,
243-
]);
244156
const lines = [
245157
marker,
246158
`## ${manifest.title}`,
@@ -270,39 +182,8 @@ export function renderEvidenceComment({
270182
lines.push(`- Overall: \`${comparison.pass}\``);
271183
}
272184
lines.push("");
273-
274-
const pairedSections = pairs.map((pair) => renderPairTable({ pair, rawBase }));
275-
276-
lines.push(...pairedSections);
277-
const singleTables = renderSingleImageTables({
278-
artifacts: manifest.artifacts,
279-
pairedKeys,
280-
rawBase,
281-
});
282-
if (singleTables) {
283-
lines.push(singleTables);
284-
}
285-
const motionClips = renderLinkList({
286-
artifacts: manifest.artifacts,
287-
kind: "motionClip",
288-
rawBase,
289-
title: "Motion-trimmed clips",
290-
});
291-
if (motionClips) {
292-
lines.push(motionClips);
293-
}
294-
const fullVideos = renderLinkList({
295-
artifacts: manifest.artifacts,
296-
kind: "fullVideo",
297-
rawBase,
298-
title: "Full videos",
299-
});
300-
if (fullVideos) {
301-
lines.push(fullVideos);
302-
}
303-
lines.push(
304-
`Raw QA files: ${treeUrl ?? `https://github.com/${process.env.GITHUB_REPOSITORY}/tree/qa-artifacts/${artifactRoot}`}`,
305-
);
185+
lines.push(renderArtifactFileList(manifest.artifacts));
186+
lines.push(`Raw QA files: ${actionsArtifactUrl}`);
306187
return `${lines.join("\n").replace(/\n{3,}/gu, "\n\n")}\n`;
307188
}
308189

@@ -314,77 +195,6 @@ function run(command, args, options = {}) {
314195
});
315196
}
316197

317-
function runStatus(command, args, options = {}) {
318-
const result = spawnSync(command, args, {
319-
stdio: "ignore",
320-
...options,
321-
});
322-
if (result.error) {
323-
throw result.error;
324-
}
325-
return result.status ?? 1;
326-
}
327-
328-
function publishArtifactFiles({ artifactRoot, ghToken, manifest, repo }) {
329-
const worktree = mkdtempSync(path.join(tmpdir(), "mantis-qa-artifacts-"));
330-
const safeArtifactRoot = normalizeTargetPath(artifactRoot);
331-
try {
332-
run("git", ["init", "--quiet", worktree]);
333-
run("git", ["-C", worktree, "config", "user.name", "github-actions[bot]"]);
334-
run("git", [
335-
"-C",
336-
worktree,
337-
"config",
338-
"user.email",
339-
"41898282+github-actions[bot]@users.noreply.github.com",
340-
]);
341-
run("git", [
342-
"-C",
343-
worktree,
344-
"remote",
345-
"add",
346-
"origin",
347-
`https://x-access-token:${ghToken}@github.com/${repo}.git`,
348-
]);
349-
try {
350-
run("git", ["-C", worktree, "fetch", "--quiet", "origin", "qa-artifacts"]);
351-
run("git", ["-C", worktree, "checkout", "--quiet", "-B", "qa-artifacts", "FETCH_HEAD"]);
352-
} catch {
353-
run("git", ["-C", worktree, "checkout", "--quiet", "--orphan", "qa-artifacts"]);
354-
}
355-
356-
const destinationRoot = path.join(worktree, safeArtifactRoot);
357-
for (const artifact of manifest.artifacts) {
358-
const destination = assertInside(
359-
destinationRoot,
360-
path.resolve(destinationRoot, artifact.targetPath),
361-
`Artifact target ${artifact.targetPath}`,
362-
);
363-
mkdirSync(path.dirname(destination), { recursive: true });
364-
copyFileSync(artifact.source, destination);
365-
}
366-
367-
run("git", ["-C", worktree, "add", safeArtifactRoot]);
368-
const hasChanges = runStatus("git", ["-C", worktree, "diff", "--cached", "--quiet"]) !== 0;
369-
if (hasChanges) {
370-
run("git", [
371-
"-C",
372-
worktree,
373-
"commit",
374-
"--quiet",
375-
"-m",
376-
`qa: publish Mantis evidence for ${manifest.id}`,
377-
]);
378-
run("git", ["-C", worktree, "push", "--quiet", "origin", "HEAD:qa-artifacts"]);
379-
} else {
380-
console.log("No QA evidence artifact changes to publish.");
381-
}
382-
} finally {
383-
rmSync(worktree, { force: true, recursive: true });
384-
}
385-
return safeArtifactRoot;
386-
}
387-
388198
function upsertPrComment({ body, marker, prNumber, repo }) {
389199
run("gh", ["api", `repos/${repo}/pulls/${prNumber}`, "--jq", ".number"]);
390200
const commentId = run("gh", [
@@ -446,25 +256,18 @@ export function publishEvidence(rawArgs = process.argv.slice(2)) {
446256
if (!ghToken) {
447257
throw new Error("Missing GH_TOKEN or GITHUB_TOKEN.");
448258
}
259+
if (!args.artifact_url) {
260+
throw new Error("Missing --artifact-url. Mantis evidence must use Actions artifacts, not Git.");
261+
}
449262

450263
const manifest = loadEvidenceManifest(args.manifest);
451-
const artifactRoot = publishArtifactFiles({
452-
artifactRoot: args.artifact_root,
453-
ghToken,
454-
manifest,
455-
repo,
456-
});
457-
const rawBase = `https://raw.githubusercontent.com/${repo}/qa-artifacts/${encodePathForUrl(artifactRoot)}`;
458-
const treeUrl = `https://github.com/${repo}/tree/qa-artifacts/${encodePathForUrl(artifactRoot)}`;
264+
normalizeTargetPath(args.artifact_root);
459265
const body = renderEvidenceComment({
460-
artifactRoot,
461266
artifactUrl: args.artifact_url,
462267
manifest,
463268
marker: args.marker,
464-
rawBase,
465269
requestSource: args.request_source,
466270
runUrl: args.run_url,
467-
treeUrl,
468271
});
469272
upsertPrComment({
470273
body,

0 commit comments

Comments
 (0)