11#!/usr/bin/env node
2- import { execFileSync , spawnSync } from "node:child_process" ;
2+ import { execFileSync } from "node:child_process" ;
33import {
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
206130function laneLine ( label , lane ) {
@@ -220,27 +144,15 @@ function laneLine(label, lane) {
220144}
221145
222146export 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-
388198function 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