@reshotdev/screenshot 0.0.1-beta.2 → 0.0.1-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +138 -47
- package/package.json +27 -16
- package/src/commands/auth.js +159 -30
- package/src/commands/capture-dom.js +50 -0
- package/src/commands/certify.js +62 -0
- package/src/commands/compose.js +220 -0
- package/src/commands/doctor-release.js +74 -0
- package/src/commands/doctor-target.js +108 -0
- package/src/commands/drifts.js +16 -69
- package/src/commands/import-tests.js +13 -13
- package/src/commands/init.js +16 -277
- package/src/commands/publish.js +484 -257
- package/src/commands/pull.js +302 -35
- package/src/commands/refresh.js +166 -0
- package/src/commands/run.js +292 -12
- package/src/commands/setup-wizard.js +348 -496
- package/src/commands/status.js +334 -126
- package/src/commands/sync.js +28 -236
- package/src/commands/ui.js +1 -1
- package/src/commands/variation.js +194 -0
- package/src/commands/verify-publish.js +46 -0
- package/src/index.js +383 -118
- package/src/lib/api-client.js +172 -60
- package/src/lib/auto-update/refresh.js +598 -0
- package/src/lib/auto-update/scene-runtime.compose.tsx +73 -0
- package/src/lib/auto-update/spec.js +89 -0
- package/src/lib/capture-engine.js +179 -9
- package/src/lib/capture-script-runner.js +639 -214
- package/src/lib/certification.js +887 -0
- package/src/lib/compose-context.js +156 -0
- package/src/lib/compose-pack.js +42 -0
- package/src/lib/compose-runtime.js +34 -0
- package/src/lib/compose-upload.js +142 -0
- package/src/lib/config.js +186 -81
- package/src/lib/dom-capture.js +64 -0
- package/src/lib/ensure-browser.js +147 -0
- package/src/lib/output-path-template.js +3 -3
- package/src/lib/record-cdp.js +288 -16
- package/src/lib/record-clip.js +83 -3
- package/src/lib/record-config.js +1 -5
- package/src/lib/release-doctor.js +321 -0
- package/src/lib/resolve-targets.js +60 -0
- package/src/lib/run-manifest.js +148 -0
- package/src/lib/standalone-mode.js +1 -1
- package/src/lib/storage-providers.js +5 -5
- package/src/lib/style-engine.js +5 -5
- package/src/lib/target-contract.js +292 -0
- package/src/lib/ui-api-helpers.js +118 -0
- package/src/lib/ui-api.js +31 -824
- package/src/lib/ui-asset-cleanup.js +62 -0
- package/src/lib/ui-output-versions.js +165 -0
- package/src/lib/ui-recorder-routes.js +341 -0
- package/src/lib/ui-scenario-metadata.js +161 -0
- package/vendor/compose/dist/auto-update.cjs +5544 -0
- package/vendor/compose/dist/auto-update.mjs +5518 -0
- package/vendor/compose/dist/capture.cjs +1450 -0
- package/vendor/compose/dist/capture.mjs +1416 -0
- package/vendor/compose/dist/eligibility.cjs +5331 -0
- package/vendor/compose/dist/eligibility.mjs +5313 -0
- package/vendor/compose/dist/index.cjs +2046 -0
- package/vendor/compose/dist/index.mjs +1997 -0
- package/vendor/compose/dist/jsx-dev-runtime.cjs +55 -0
- package/vendor/compose/dist/jsx-dev-runtime.mjs +27 -0
- package/vendor/compose/dist/jsx-runtime.cjs +58 -0
- package/vendor/compose/dist/jsx-runtime.mjs +31 -0
- package/vendor/compose/dist/render.cjs +558 -0
- package/vendor/compose/dist/render.mjs +515 -0
- package/vendor/compose/dist/verify-cli.cjs +3806 -0
- package/vendor/compose/dist/verify-cli.mjs +3812 -0
- package/vendor/compose/dist/verify.cjs +3880 -0
- package/vendor/compose/dist/verify.mjs +3858 -0
- package/web/manager/dist/assets/index-D0S2otug.js +507 -0
- package/web/manager/dist/index.html +1 -1
- package/src/commands/ci-run.js +0 -123
- package/src/commands/ci-setup.js +0 -288
- package/src/commands/ingest.js +0 -458
- package/src/commands/setup.js +0 -137
- package/src/commands/validate-docs.js +0 -529
- package/src/lib/playwright-runner.js +0 -252
- package/web/manager/dist/assets/index--ZgioErz.js +0 -507
package/src/commands/publish.js
CHANGED
|
@@ -15,6 +15,10 @@ const {
|
|
|
15
15
|
getStorageMode,
|
|
16
16
|
isPlatformAvailable,
|
|
17
17
|
} = require("../lib/storage-providers");
|
|
18
|
+
const {
|
|
19
|
+
getLatestSuccessfulRunManifest,
|
|
20
|
+
getLatestUsableRunManifest,
|
|
21
|
+
} = require("../lib/run-manifest");
|
|
18
22
|
const pkg = require("../../package.json");
|
|
19
23
|
|
|
20
24
|
// Check if transactional flow should be used (R2 configured on server)
|
|
@@ -52,20 +56,44 @@ function loadDiffManifests(outputBaseDir) {
|
|
|
52
56
|
const latestVersion = versions.sort().reverse()[0];
|
|
53
57
|
if (!latestVersion) continue;
|
|
54
58
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
const versionDir = path.join(scenarioDir, latestVersion);
|
|
60
|
+
|
|
61
|
+
// `run` writes diff-manifest.json into the ACTUAL output dir, which
|
|
62
|
+
// includes the variant subdir (e.g. <version>/default/diff-manifest.json),
|
|
63
|
+
// not the version dir itself. Read the version-level manifest if present
|
|
64
|
+
// AND every variant subdir's manifest, merging their `assets` so the diff
|
|
65
|
+
// the CLI computed actually reaches publish (otherwise the platform stores
|
|
66
|
+
// diffPercentage: null and Activity shows "0 changed" — audit run-13 H1).
|
|
67
|
+
const candidatePaths = [path.join(versionDir, "diff-manifest.json")];
|
|
68
|
+
try {
|
|
69
|
+
for (const entry of fs.readdirSync(versionDir)) {
|
|
70
|
+
const entryPath = path.join(versionDir, entry);
|
|
71
|
+
if (fs.statSync(entryPath).isDirectory()) {
|
|
72
|
+
candidatePaths.push(path.join(entryPath, "diff-manifest.json"));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch (e) {
|
|
76
|
+
// version dir unreadable — fall back to the version-level path only
|
|
77
|
+
}
|
|
60
78
|
|
|
61
|
-
|
|
79
|
+
const merged = { assets: {} };
|
|
80
|
+
let foundAny = false;
|
|
81
|
+
for (const manifestPath of candidatePaths) {
|
|
82
|
+
if (!fs.existsSync(manifestPath)) continue;
|
|
62
83
|
try {
|
|
63
84
|
const manifest = fs.readJSONSync(manifestPath);
|
|
64
|
-
|
|
85
|
+
foundAny = true;
|
|
86
|
+
Object.assign(merged, manifest, {
|
|
87
|
+
assets: { ...merged.assets, ...(manifest.assets || {}) },
|
|
88
|
+
});
|
|
65
89
|
} catch (e) {
|
|
66
90
|
// Skip malformed manifests
|
|
67
91
|
}
|
|
68
92
|
}
|
|
93
|
+
|
|
94
|
+
if (foundAny) {
|
|
95
|
+
manifests.set(`${scenarioKey}/${latestVersion}`, merged);
|
|
96
|
+
}
|
|
69
97
|
}
|
|
70
98
|
} catch (e) {
|
|
71
99
|
// Return empty if directory structure is unexpected
|
|
@@ -390,13 +418,13 @@ function resolveProjectContext({ settings, docSyncConfig, storageMode }) {
|
|
|
390
418
|
if (!apiKey) {
|
|
391
419
|
throw new Error(
|
|
392
420
|
"No API key found. Set RESHOT_API_KEY in your environment or run `reshot auth` locally to create .reshot/settings.json.\n" +
|
|
393
|
-
"Alternatively, configure BYOS (Bring Your Own Storage) in
|
|
421
|
+
"Alternatively, configure BYOS (Bring Your Own Storage) in reshot.config.json to publish without authentication.",
|
|
394
422
|
);
|
|
395
423
|
}
|
|
396
424
|
|
|
397
425
|
if (!projectId) {
|
|
398
426
|
throw new Error(
|
|
399
|
-
"No project ID found. Set RESHOT_PROJECT_ID in your environment or ensure
|
|
427
|
+
"No project ID found. Set RESHOT_PROJECT_ID in your environment or ensure reshot.config.json contains _metadata.projectId.",
|
|
400
428
|
);
|
|
401
429
|
}
|
|
402
430
|
|
|
@@ -487,6 +515,48 @@ function groupAssetsByScenario(assetFiles, outputBaseDir) {
|
|
|
487
515
|
return Array.from(groups.values());
|
|
488
516
|
}
|
|
489
517
|
|
|
518
|
+
function collectAssetFilesFromDirectories(directories) {
|
|
519
|
+
const deduped = new Set();
|
|
520
|
+
const files = [];
|
|
521
|
+
|
|
522
|
+
for (const directory of directories) {
|
|
523
|
+
if (!directory || !fs.existsSync(directory)) continue;
|
|
524
|
+
for (const filePath of findAssetFiles(directory)) {
|
|
525
|
+
if (deduped.has(filePath)) continue;
|
|
526
|
+
deduped.add(filePath);
|
|
527
|
+
files.push(filePath);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return files;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function resolveManifestScopedScreenshotFiles(outputBaseDir, latestRunManifest) {
|
|
535
|
+
if (!latestRunManifest) {
|
|
536
|
+
throw new Error(
|
|
537
|
+
"No run manifest is available. Run `reshot run` first or use `reshot publish --all-output` to publish from the full output tree.",
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const scenarioDirs = (latestRunManifest.scenarios || [])
|
|
542
|
+
.filter((scenario) => scenario.success !== false)
|
|
543
|
+
.map((scenario) => scenario.outputDir)
|
|
544
|
+
.filter(Boolean);
|
|
545
|
+
|
|
546
|
+
if (scenarioDirs.length === 0) {
|
|
547
|
+
throw new Error(
|
|
548
|
+
"The run manifest does not contain any successful scenario output directories.",
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const screenshotFiles = collectAssetFilesFromDirectories(scenarioDirs);
|
|
553
|
+
return {
|
|
554
|
+
screenshotFiles,
|
|
555
|
+
mode: "latest-run",
|
|
556
|
+
scenarioCount: scenarioDirs.length,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
490
560
|
function buildContextForVariation(scenario, variationSlug) {
|
|
491
561
|
const safeVariation = variationSlug || "default";
|
|
492
562
|
if (!scenario || !scenario.contexts) {
|
|
@@ -539,17 +609,25 @@ function buildScenarioDefinition(scenario) {
|
|
|
539
609
|
function buildPublishMetadata({
|
|
540
610
|
projectId,
|
|
541
611
|
publishSessionId,
|
|
612
|
+
tag,
|
|
542
613
|
scenarioKey,
|
|
543
614
|
scenarioConfig,
|
|
544
615
|
variationSlug,
|
|
545
616
|
contextData,
|
|
546
617
|
gitInfo,
|
|
618
|
+
autoApprove = false,
|
|
547
619
|
}) {
|
|
548
620
|
const scenarioDefinition = buildScenarioDefinition(scenarioConfig);
|
|
549
621
|
|
|
622
|
+
// Only attach git metadata when a real commit hash is available. Sending
|
|
623
|
+
// empty git values when the repo has no HEAD causes the platform to reject
|
|
624
|
+
// the batch with an opaque 400.
|
|
625
|
+
const hasGit = !!(gitInfo && gitInfo.commitHash);
|
|
626
|
+
|
|
550
627
|
return {
|
|
551
628
|
projectId,
|
|
552
629
|
publishSessionId, // Unique ID for this CLI publish run
|
|
630
|
+
tag: tag || undefined,
|
|
553
631
|
scenarioName: scenarioConfig?.name || scenarioKey,
|
|
554
632
|
scenario: scenarioDefinition,
|
|
555
633
|
context: {
|
|
@@ -557,10 +635,17 @@ function buildPublishMetadata({
|
|
|
557
635
|
data: contextData,
|
|
558
636
|
},
|
|
559
637
|
autoCreateVisuals: true,
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
commitMessage: gitInfo.commitMessage,
|
|
638
|
+
publish: {
|
|
639
|
+
autoApprove,
|
|
563
640
|
},
|
|
641
|
+
...(hasGit
|
|
642
|
+
? {
|
|
643
|
+
git: {
|
|
644
|
+
commitHash: gitInfo.commitHash,
|
|
645
|
+
commitMessage: gitInfo.commitMessage,
|
|
646
|
+
},
|
|
647
|
+
}
|
|
648
|
+
: {}),
|
|
564
649
|
cli: {
|
|
565
650
|
version: pkg.version,
|
|
566
651
|
captureTimestamp: new Date().toISOString(),
|
|
@@ -568,6 +653,24 @@ function buildPublishMetadata({
|
|
|
568
653
|
};
|
|
569
654
|
}
|
|
570
655
|
|
|
656
|
+
/**
|
|
657
|
+
* Read pixel dimensions from a captured image so the platform can store and
|
|
658
|
+
* expose them (used by `reshot pull` for CLS-safe `<img width height>`
|
|
659
|
+
* embedding). Returns nulls for non-images or on any error — never throws.
|
|
660
|
+
*/
|
|
661
|
+
async function getImageDimensions(filePath, contentType) {
|
|
662
|
+
if (!filePath || !(contentType || "").startsWith("image/")) {
|
|
663
|
+
return { width: null, height: null };
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
const sharp = require("sharp");
|
|
667
|
+
const meta = await sharp(filePath).metadata();
|
|
668
|
+
return { width: meta.width ?? null, height: meta.height ?? null };
|
|
669
|
+
} catch {
|
|
670
|
+
return { width: null, height: null };
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
571
674
|
/**
|
|
572
675
|
* Publish using transactional flow (direct R2 upload with presigned URLs)
|
|
573
676
|
*/
|
|
@@ -578,6 +681,7 @@ async function publishWithTransactionalFlow(
|
|
|
578
681
|
docSyncConfig,
|
|
579
682
|
gitInfo,
|
|
580
683
|
diffManifests = null,
|
|
684
|
+
{ autoApprove = false } = {},
|
|
581
685
|
) {
|
|
582
686
|
console.log(
|
|
583
687
|
chalk.cyan(" 🚀 Using transactional upload (direct to R2)...\n"),
|
|
@@ -586,6 +690,7 @@ async function publishWithTransactionalFlow(
|
|
|
586
690
|
let successCount = 0;
|
|
587
691
|
let failCount = 0;
|
|
588
692
|
let skippedCount = 0;
|
|
693
|
+
let reviewPendingCount = 0;
|
|
589
694
|
let viewUrl = null;
|
|
590
695
|
|
|
591
696
|
// Flatten all assets with metadata
|
|
@@ -627,6 +732,52 @@ async function publishWithTransactionalFlow(
|
|
|
627
732
|
}
|
|
628
733
|
}
|
|
629
734
|
|
|
735
|
+
// ── DOM scene sidecars (MHTML bundles next to each PNG) ─────────────
|
|
736
|
+
// The CLI emits an .mhtml alongside each PNG when scenario.domScene !==
|
|
737
|
+
// false. We upload them as sidecar assets linked to the parent PNG's
|
|
738
|
+
// visual version via _parentVisualKey, so the server can persist the
|
|
739
|
+
// sidecar's s3Path on the same VisualVersion row.
|
|
740
|
+
const domSceneSidecars = [];
|
|
741
|
+
for (const f of allFiles) {
|
|
742
|
+
// Only attach MHTML to PNG screenshots (not videos, not other sidecars).
|
|
743
|
+
if (path.extname(f.path).toLowerCase() !== ".png") continue;
|
|
744
|
+
if (f._isThumbnail || f._isDomScene) continue;
|
|
745
|
+
|
|
746
|
+
const candidate =
|
|
747
|
+
(f.asset && f.asset.domScenePath) ||
|
|
748
|
+
f.path.replace(/\.png$/i, ".mhtml");
|
|
749
|
+
if (!fs.existsSync(candidate)) continue;
|
|
750
|
+
|
|
751
|
+
const sidecarStat = fs.statSync(candidate);
|
|
752
|
+
domSceneSidecars.push({
|
|
753
|
+
group: f.group,
|
|
754
|
+
asset: {
|
|
755
|
+
captureKey: f.asset.captureKey,
|
|
756
|
+
path: candidate,
|
|
757
|
+
filename: path.basename(candidate),
|
|
758
|
+
},
|
|
759
|
+
scenarioConfig: f.scenarioConfig,
|
|
760
|
+
key: f.key,
|
|
761
|
+
visualKey: f.visualKey,
|
|
762
|
+
path: candidate,
|
|
763
|
+
size: sidecarStat.size,
|
|
764
|
+
contentType: "multipart/related",
|
|
765
|
+
hash: null,
|
|
766
|
+
diffData: null,
|
|
767
|
+
thumbnailPath: null,
|
|
768
|
+
_isDomScene: true,
|
|
769
|
+
_parentVisualKey: f.visualKey,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
if (domSceneSidecars.length > 0) {
|
|
773
|
+
console.log(
|
|
774
|
+
chalk.gray(
|
|
775
|
+
` Attaching ${domSceneSidecars.length} DOM scene sidecar(s)...`,
|
|
776
|
+
),
|
|
777
|
+
);
|
|
778
|
+
allFiles.push(...domSceneSidecars);
|
|
779
|
+
}
|
|
780
|
+
|
|
630
781
|
// Generate thumbnails for video files (first frame as PNG)
|
|
631
782
|
const videoExts = new Set([".mp4", ".webm", ".mov"]);
|
|
632
783
|
const videoFiles = allFiles.filter((f) =>
|
|
@@ -726,7 +877,10 @@ async function publishWithTransactionalFlow(
|
|
|
726
877
|
|
|
727
878
|
// Step 3: Upload files directly to R2 (parallel with concurrency limit)
|
|
728
879
|
console.log(chalk.gray(" Uploading files directly to R2..."));
|
|
729
|
-
const CONCURRENCY =
|
|
880
|
+
const CONCURRENCY = Math.min(
|
|
881
|
+
Math.max(parseInt(process.env.RESHOT_UPLOAD_CONCURRENCY || "10", 10), 1),
|
|
882
|
+
20,
|
|
883
|
+
);
|
|
730
884
|
const uploadQueue = [...allFiles];
|
|
731
885
|
const uploadResults = [];
|
|
732
886
|
|
|
@@ -749,7 +903,16 @@ async function publishWithTransactionalFlow(
|
|
|
749
903
|
contentType: file.contentType,
|
|
750
904
|
});
|
|
751
905
|
|
|
752
|
-
|
|
906
|
+
// Label the artifact type so PNG + MHTML-sidecar lines for the same
|
|
907
|
+
// visual don't read as a duplicate-upload bug (audit run-10 F3).
|
|
908
|
+
const artifactLabel = file._isThumbnail
|
|
909
|
+
? "thumbnail"
|
|
910
|
+
: file._isDomScene
|
|
911
|
+
? "dom scene .mhtml"
|
|
912
|
+
: (path.extname(file.path || "").replace(/^\./, "") || "asset");
|
|
913
|
+
console.log(
|
|
914
|
+
chalk.green(` ✔ Uploaded ${file.visualKey} (${artifactLabel})`),
|
|
915
|
+
);
|
|
753
916
|
return { success: true, file, s3Path: urlInfo.path };
|
|
754
917
|
} catch (err) {
|
|
755
918
|
console.log(
|
|
@@ -768,21 +931,33 @@ async function publishWithTransactionalFlow(
|
|
|
768
931
|
|
|
769
932
|
// Build a map of thumbnail s3Paths keyed by parent visualKey
|
|
770
933
|
const thumbnailS3Paths = new Map();
|
|
934
|
+
// Build a map of DOM-scene MHTML s3Paths keyed by parent visualKey.
|
|
935
|
+
// The server attaches these to the parent VisualVersion as a sidecar.
|
|
936
|
+
const domSceneS3Paths = new Map();
|
|
937
|
+
const domSceneSizes = new Map();
|
|
771
938
|
for (const result of uploadResults) {
|
|
772
|
-
if (result.success
|
|
939
|
+
if (!result.success) continue;
|
|
940
|
+
if (result.file._isThumbnail) {
|
|
773
941
|
thumbnailS3Paths.set(result.file._parentVisualKey, result.s3Path);
|
|
942
|
+
} else if (result.file._isDomScene) {
|
|
943
|
+
domSceneS3Paths.set(result.file._parentVisualKey, result.s3Path);
|
|
944
|
+
domSceneSizes.set(result.file._parentVisualKey, result.file.size);
|
|
774
945
|
}
|
|
775
946
|
}
|
|
776
947
|
|
|
777
|
-
// Group successful uploads by scenario/variant (skip thumbnails
|
|
948
|
+
// Group successful uploads by scenario/variant (skip thumbnails + DOM
|
|
949
|
+
// sidecars — they're metadata attached to their parent asset).
|
|
950
|
+
const failedUploadKeys = [];
|
|
778
951
|
const groupMap = new Map();
|
|
779
952
|
for (const result of uploadResults) {
|
|
780
953
|
if (!result.success) {
|
|
781
|
-
if (!result.file._isThumbnail)
|
|
954
|
+
if (!result.file._isThumbnail && !result.file._isDomScene) {
|
|
955
|
+
failCount++;
|
|
956
|
+
failedUploadKeys.push(result.file.visualKey);
|
|
957
|
+
}
|
|
782
958
|
continue;
|
|
783
959
|
}
|
|
784
|
-
|
|
785
|
-
if (result.file._isThumbnail) continue;
|
|
960
|
+
if (result.file._isThumbnail || result.file._isDomScene) continue;
|
|
786
961
|
|
|
787
962
|
const groupKey = `${result.file.group.scenarioKey}::${result.file.group.variationSlug}`;
|
|
788
963
|
if (!groupMap.has(groupKey)) {
|
|
@@ -792,22 +967,36 @@ async function publishWithTransactionalFlow(
|
|
|
792
967
|
assets: [],
|
|
793
968
|
});
|
|
794
969
|
}
|
|
970
|
+
const dimensions = await getImageDimensions(
|
|
971
|
+
result.file.path,
|
|
972
|
+
result.file.contentType,
|
|
973
|
+
);
|
|
795
974
|
groupMap.get(groupKey).assets.push({
|
|
796
975
|
key: result.file.key,
|
|
797
976
|
s3Path: result.s3Path,
|
|
798
977
|
hash: result.file.hash,
|
|
799
978
|
visualKey: result.file.visualKey,
|
|
800
979
|
size: result.file.size,
|
|
980
|
+
width: dimensions.width,
|
|
981
|
+
height: dimensions.height,
|
|
801
982
|
contentType: result.file.contentType,
|
|
802
983
|
// Include diff data from CLI analysis
|
|
803
984
|
diffPercentage: result.file.diffData?.diffPercentage ?? null,
|
|
804
985
|
diffStatus: result.file.diffData?.diffStatus ?? null,
|
|
805
986
|
// Attach thumbnail for video assets
|
|
806
987
|
thumbnailS3Path: thumbnailS3Paths.get(result.file.visualKey) ?? null,
|
|
988
|
+
// Attach DOM scene sidecar (MHTML) — the source of truth for
|
|
989
|
+
// marketing-team variations.
|
|
990
|
+
domSceneS3Path: domSceneS3Paths.get(result.file.visualKey) ?? null,
|
|
991
|
+
domSceneSize: domSceneSizes.get(result.file.visualKey) ?? null,
|
|
807
992
|
});
|
|
808
993
|
}
|
|
809
994
|
|
|
810
|
-
//
|
|
995
|
+
// Build all commits for batch request
|
|
996
|
+
// Vercel serverless functions have ~60s timeout; keep batches small enough to complete
|
|
997
|
+
const MAX_BATCH_SIZE = 25;
|
|
998
|
+
const commits = [];
|
|
999
|
+
|
|
811
1000
|
for (const { group, scenarioConfig, assets } of groupMap.values()) {
|
|
812
1001
|
const contextObj = buildContextForVariation(
|
|
813
1002
|
scenarioConfig,
|
|
@@ -816,50 +1005,98 @@ async function publishWithTransactionalFlow(
|
|
|
816
1005
|
const metadata = buildPublishMetadata({
|
|
817
1006
|
projectId,
|
|
818
1007
|
publishSessionId: gitInfo.publishSessionId,
|
|
1008
|
+
tag: gitInfo.tag,
|
|
819
1009
|
scenarioKey: group.scenarioKey,
|
|
820
1010
|
scenarioConfig,
|
|
821
1011
|
variationSlug: group.variationSlug,
|
|
822
1012
|
contextData: contextObj,
|
|
823
1013
|
gitInfo,
|
|
1014
|
+
autoApprove,
|
|
824
1015
|
});
|
|
825
1016
|
|
|
826
1017
|
if (metadata.cli) {
|
|
827
|
-
metadata.cli.features = ["steps", "transactional"];
|
|
1018
|
+
metadata.cli.features = ["steps", "transactional", "batch"];
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
commits.push({ metadata, assets });
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Send in batches
|
|
1025
|
+
console.log(
|
|
1026
|
+
chalk.gray(` Committing ${commits.length} scenario(s) to platform...`),
|
|
1027
|
+
);
|
|
1028
|
+
|
|
1029
|
+
const totalBatches = Math.ceil(commits.length / MAX_BATCH_SIZE);
|
|
1030
|
+
for (let i = 0; i < commits.length; i += MAX_BATCH_SIZE) {
|
|
1031
|
+
const chunk = commits.slice(i, i + MAX_BATCH_SIZE);
|
|
1032
|
+
const batchNum = Math.floor(i / MAX_BATCH_SIZE) + 1;
|
|
1033
|
+
if (totalBatches > 1) {
|
|
1034
|
+
console.log(chalk.gray(` Batch ${batchNum}/${totalBatches}...`));
|
|
828
1035
|
}
|
|
829
1036
|
|
|
830
1037
|
try {
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
1038
|
+
const rawBatchResult = await apiClient.publishBatch(apiKey, {
|
|
1039
|
+
commits: chunk,
|
|
1040
|
+
autoApprove: autoApprove || false,
|
|
834
1041
|
});
|
|
1042
|
+
// Unwrap API envelope: response may be { data: { results, ... } } or { results, ... }
|
|
1043
|
+
const batchResult = rawBatchResult.data || rawBatchResult;
|
|
835
1044
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1045
|
+
// Count only NEWLY-pending (new/changed) captures, so the final summary
|
|
1046
|
+
// doesn't imply a re-publish reset already-live captures (audit F1).
|
|
1047
|
+
reviewPendingCount += batchResult.reviewQueueItems || 0;
|
|
1048
|
+
|
|
1049
|
+
for (const r of batchResult.results || []) {
|
|
1050
|
+
if (r.status === "ok") {
|
|
1051
|
+
const count = r.assetsProcessed || 0;
|
|
1052
|
+
console.log(
|
|
1053
|
+
chalk.green(
|
|
1054
|
+
` ✔ Committed "${r.scenario}" (${r.context}): ${count} asset(s)`,
|
|
1055
|
+
),
|
|
1056
|
+
);
|
|
1057
|
+
successCount += count;
|
|
843
1058
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1059
|
+
if (r.skippedAssets?.length > 0) {
|
|
1060
|
+
for (const key of r.skippedAssets) {
|
|
1061
|
+
console.log(chalk.yellow(` ⚠ Skipped "${key}" (plan limit reached)`));
|
|
1062
|
+
}
|
|
1063
|
+
skippedCount += r.skippedAssets.length;
|
|
1064
|
+
}
|
|
1065
|
+
} else {
|
|
1066
|
+
console.log(
|
|
1067
|
+
chalk.red(
|
|
1068
|
+
` ✖ "${r.scenario}" (${r.context}): ${r.error || "Unknown error"}`,
|
|
1069
|
+
),
|
|
1070
|
+
);
|
|
1071
|
+
failCount++;
|
|
848
1072
|
}
|
|
849
|
-
skippedCount += result.skippedAssets.length;
|
|
850
1073
|
}
|
|
851
1074
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
viewUrl = result.viewUrl;
|
|
1075
|
+
if (!viewUrl && batchResult.viewUrl) {
|
|
1076
|
+
viewUrl = batchResult.viewUrl;
|
|
855
1077
|
}
|
|
856
1078
|
} catch (error) {
|
|
857
|
-
|
|
858
|
-
|
|
1079
|
+
// Surface the server's descriptive error body (e.g. a validation
|
|
1080
|
+
// message) instead of just axios's opaque "Request failed with status
|
|
1081
|
+
// code 400" — otherwise a 1-line fix turns into a support ticket.
|
|
1082
|
+
const serverMsg =
|
|
1083
|
+
error.response?.data?.error || error.response?.data?.message;
|
|
1084
|
+
const detail = serverMsg
|
|
1085
|
+
? `${error.message} — ${serverMsg}`
|
|
1086
|
+
: error.message;
|
|
1087
|
+
console.log(chalk.red(` ✖ Batch request failed: ${detail}`));
|
|
1088
|
+
failCount += chunk.length;
|
|
859
1089
|
}
|
|
860
1090
|
}
|
|
861
1091
|
|
|
862
|
-
return {
|
|
1092
|
+
return {
|
|
1093
|
+
successCount,
|
|
1094
|
+
failCount,
|
|
1095
|
+
skippedCount,
|
|
1096
|
+
reviewPendingCount,
|
|
1097
|
+
viewUrl,
|
|
1098
|
+
failedUploadKeys,
|
|
1099
|
+
};
|
|
863
1100
|
}
|
|
864
1101
|
|
|
865
1102
|
/**
|
|
@@ -871,6 +1108,7 @@ async function publishWithLegacyFlow(
|
|
|
871
1108
|
groupedAssets,
|
|
872
1109
|
docSyncConfig,
|
|
873
1110
|
gitInfo,
|
|
1111
|
+
{ autoApprove = false } = {},
|
|
874
1112
|
) {
|
|
875
1113
|
let successCount = 0;
|
|
876
1114
|
let failCount = 0;
|
|
@@ -887,11 +1125,13 @@ async function publishWithLegacyFlow(
|
|
|
887
1125
|
const metadata = buildPublishMetadata({
|
|
888
1126
|
projectId,
|
|
889
1127
|
publishSessionId: gitInfo.publishSessionId,
|
|
1128
|
+
tag: gitInfo.tag,
|
|
890
1129
|
scenarioKey: group.scenarioKey,
|
|
891
1130
|
scenarioConfig,
|
|
892
1131
|
variationSlug: group.variationSlug,
|
|
893
1132
|
contextData: contextObj,
|
|
894
1133
|
gitInfo,
|
|
1134
|
+
autoApprove,
|
|
895
1135
|
});
|
|
896
1136
|
|
|
897
1137
|
if (metadata.cli) {
|
|
@@ -1131,14 +1371,28 @@ function getGitInfo() {
|
|
|
1131
1371
|
try {
|
|
1132
1372
|
const commitHash = execSync("git rev-parse HEAD", {
|
|
1133
1373
|
encoding: "utf-8",
|
|
1374
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1134
1375
|
}).trim();
|
|
1135
1376
|
const commitMessage = execSync("git log -1 --pretty=%B", {
|
|
1136
1377
|
encoding: "utf-8",
|
|
1378
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1137
1379
|
}).trim();
|
|
1138
|
-
return { commitHash, commitMessage };
|
|
1380
|
+
return { commitHash, commitMessage, hasCommit: !!commitHash };
|
|
1139
1381
|
} catch (error) {
|
|
1140
|
-
|
|
1141
|
-
|
|
1382
|
+
// No git HEAD — either not a git repo or a brand-new repo with no commits.
|
|
1383
|
+
// Proceed WITHOUT git metadata rather than sending empty values that the
|
|
1384
|
+
// platform rejects with an opaque 400 "Batch request failed".
|
|
1385
|
+
console.log(
|
|
1386
|
+
chalk.yellow(
|
|
1387
|
+
" ⚠ No git commit found — publishing without commit metadata.",
|
|
1388
|
+
),
|
|
1389
|
+
);
|
|
1390
|
+
console.log(
|
|
1391
|
+
chalk.gray(
|
|
1392
|
+
" Tip: run `git commit` first to attach commit info to this publish.",
|
|
1393
|
+
),
|
|
1394
|
+
);
|
|
1395
|
+
return { commitHash: "", commitMessage: "", hasCommit: false };
|
|
1142
1396
|
}
|
|
1143
1397
|
}
|
|
1144
1398
|
|
|
@@ -1167,70 +1421,6 @@ function getRecentCommits(lastCommitHash) {
|
|
|
1167
1421
|
}
|
|
1168
1422
|
}
|
|
1169
1423
|
|
|
1170
|
-
/**
|
|
1171
|
-
* Recursively find all markdown files matching include/exclude patterns
|
|
1172
|
-
*/
|
|
1173
|
-
function findDocFiles(
|
|
1174
|
-
docsRoot,
|
|
1175
|
-
includePatterns = ["**/*.md", "**/*.mdx"],
|
|
1176
|
-
excludePatterns = [],
|
|
1177
|
-
) {
|
|
1178
|
-
const files = [];
|
|
1179
|
-
const rootPath = path.resolve(process.cwd(), docsRoot);
|
|
1180
|
-
|
|
1181
|
-
if (!fs.existsSync(rootPath)) {
|
|
1182
|
-
return files;
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
function walkDir(dir, relativePath = "") {
|
|
1186
|
-
const items = fs.readdirSync(dir);
|
|
1187
|
-
|
|
1188
|
-
for (const item of items) {
|
|
1189
|
-
const fullPath = path.join(dir, item);
|
|
1190
|
-
const relativeItemPath = path
|
|
1191
|
-
.join(relativePath, item)
|
|
1192
|
-
.replace(/\\/g, "/");
|
|
1193
|
-
const stat = fs.statSync(fullPath);
|
|
1194
|
-
|
|
1195
|
-
// Check exclude patterns
|
|
1196
|
-
const shouldExclude = excludePatterns.some((pattern) => {
|
|
1197
|
-
const regex = new RegExp(
|
|
1198
|
-
pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*"),
|
|
1199
|
-
);
|
|
1200
|
-
return regex.test(relativeItemPath) || regex.test(fullPath);
|
|
1201
|
-
});
|
|
1202
|
-
|
|
1203
|
-
if (shouldExclude) {
|
|
1204
|
-
continue;
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
if (stat.isDirectory()) {
|
|
1208
|
-
walkDir(fullPath, relativeItemPath);
|
|
1209
|
-
} else if (stat.isFile()) {
|
|
1210
|
-
const ext = path.extname(item).toLowerCase();
|
|
1211
|
-
const matchesInclude = includePatterns.some((pattern) => {
|
|
1212
|
-
const regex = new RegExp(
|
|
1213
|
-
pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*"),
|
|
1214
|
-
);
|
|
1215
|
-
return (
|
|
1216
|
-
regex.test(relativeItemPath) || ext === ".md" || ext === ".mdx"
|
|
1217
|
-
);
|
|
1218
|
-
});
|
|
1219
|
-
|
|
1220
|
-
if (matchesInclude && (ext === ".md" || ext === ".mdx")) {
|
|
1221
|
-
files.push({
|
|
1222
|
-
fullPath,
|
|
1223
|
-
relativePath: relativeItemPath,
|
|
1224
|
-
});
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
walkDir(rootPath);
|
|
1231
|
-
return files;
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
1424
|
/**
|
|
1235
1425
|
* Parse frontmatter from markdown content
|
|
1236
1426
|
*/
|
|
@@ -1268,7 +1458,18 @@ function parseFrontmatter(content) {
|
|
|
1268
1458
|
}
|
|
1269
1459
|
|
|
1270
1460
|
async function publishCommand(options = {}) {
|
|
1271
|
-
const {
|
|
1461
|
+
const {
|
|
1462
|
+
tag,
|
|
1463
|
+
message,
|
|
1464
|
+
dryRun,
|
|
1465
|
+
force,
|
|
1466
|
+
video,
|
|
1467
|
+
allOutput = false,
|
|
1468
|
+
outputJson,
|
|
1469
|
+
autoApprove,
|
|
1470
|
+
skipReleaseDoctor = false,
|
|
1471
|
+
noExit = false,
|
|
1472
|
+
} = options;
|
|
1272
1473
|
|
|
1273
1474
|
// Result tracking for --output-json and programmatic callers
|
|
1274
1475
|
const publishResult = {
|
|
@@ -1277,6 +1478,7 @@ async function publishCommand(options = {}) {
|
|
|
1277
1478
|
assetsSkipped: 0,
|
|
1278
1479
|
reviewQueueItems: 0,
|
|
1279
1480
|
viewUrl: null,
|
|
1481
|
+
releaseDoctor: null,
|
|
1280
1482
|
tag: tag || null,
|
|
1281
1483
|
dryRun: !!dryRun,
|
|
1282
1484
|
timestamp: new Date().toISOString(),
|
|
@@ -1294,6 +1496,10 @@ async function publishCommand(options = {}) {
|
|
|
1294
1496
|
console.log(chalk.yellow("🔍 DRY RUN MODE - No assets will be uploaded\n"));
|
|
1295
1497
|
}
|
|
1296
1498
|
|
|
1499
|
+
if (autoApprove) {
|
|
1500
|
+
console.log(chalk.cyan(" ✅ Auto-approve enabled: visuals will be approved immediately\n"));
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1297
1503
|
// Read config + settings (if available)
|
|
1298
1504
|
const settings = readSettingsSafe();
|
|
1299
1505
|
let docSyncConfig = null;
|
|
@@ -1305,6 +1511,57 @@ async function publishCommand(options = {}) {
|
|
|
1305
1511
|
);
|
|
1306
1512
|
}
|
|
1307
1513
|
|
|
1514
|
+
if (skipReleaseDoctor) {
|
|
1515
|
+
publishResult.releaseDoctor = {
|
|
1516
|
+
skipped: true,
|
|
1517
|
+
success: true,
|
|
1518
|
+
};
|
|
1519
|
+
} else if (docSyncConfig) {
|
|
1520
|
+
console.log(chalk.cyan("🧪 Running release doctor before publish...\n"));
|
|
1521
|
+
const { runReleaseDoctor } = require("../lib/release-doctor");
|
|
1522
|
+
const releaseDoctor = await runReleaseDoctor({});
|
|
1523
|
+
publishResult.releaseDoctor = {
|
|
1524
|
+
skipped: false,
|
|
1525
|
+
success: releaseDoctor.ok,
|
|
1526
|
+
reportPath: releaseDoctor.reportPath || null,
|
|
1527
|
+
};
|
|
1528
|
+
|
|
1529
|
+
if (!releaseDoctor.ok) {
|
|
1530
|
+
console.log(chalk.red(" ✖ Release doctor failed. Fix the issues below before publishing:\n"));
|
|
1531
|
+
const blockingIssues = releaseDoctor.summary?.blockingIssues || [];
|
|
1532
|
+
if (blockingIssues.length > 0) {
|
|
1533
|
+
for (const issue of blockingIssues) {
|
|
1534
|
+
const scope = issue.scope ? `${issue.scope}: ` : "";
|
|
1535
|
+
console.log(chalk.red(` ✖ ${scope}${issue.message}`));
|
|
1536
|
+
}
|
|
1537
|
+
} else {
|
|
1538
|
+
console.log(chalk.red(" ✖ Release gate checks failed (see report for details)."));
|
|
1539
|
+
}
|
|
1540
|
+
const advisories = releaseDoctor.summary?.advisories || [];
|
|
1541
|
+
for (const advisory of advisories) {
|
|
1542
|
+
const scope = advisory.scope ? `${advisory.scope}: ` : "";
|
|
1543
|
+
console.log(chalk.yellow(` ⚠ ${scope}${advisory.message}`));
|
|
1544
|
+
}
|
|
1545
|
+
if (releaseDoctor.reportPath) {
|
|
1546
|
+
console.log(chalk.gray(`\n Full report: ${releaseDoctor.reportPath}`));
|
|
1547
|
+
}
|
|
1548
|
+
if (!noExit) process.exit(1);
|
|
1549
|
+
return {
|
|
1550
|
+
...publishResult,
|
|
1551
|
+
success: false,
|
|
1552
|
+
error: "Release doctor failed",
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
console.log(chalk.green(" ✔ Release doctor passed\n"));
|
|
1557
|
+
} else {
|
|
1558
|
+
publishResult.releaseDoctor = {
|
|
1559
|
+
skipped: true,
|
|
1560
|
+
success: true,
|
|
1561
|
+
reason: "config-unavailable",
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1308
1565
|
// Determine storage mode and validate configuration
|
|
1309
1566
|
const storageConfig = docSyncConfig?.storage;
|
|
1310
1567
|
const storageMode = getStorageMode(docSyncConfig);
|
|
@@ -1324,7 +1581,8 @@ async function publishCommand(options = {}) {
|
|
|
1324
1581
|
console.log(chalk.red(` • ${error}`));
|
|
1325
1582
|
}
|
|
1326
1583
|
console.log(getStorageSetupHelp(storageConfig?.type || "reshot"));
|
|
1327
|
-
process.exit(1);
|
|
1584
|
+
if (!noExit) process.exit(1);
|
|
1585
|
+
return { ...publishResult, success: false, error: "Invalid storage configuration" };
|
|
1328
1586
|
}
|
|
1329
1587
|
|
|
1330
1588
|
// Resolve project context based on storage mode
|
|
@@ -1343,7 +1601,8 @@ async function publishCommand(options = {}) {
|
|
|
1343
1601
|
);
|
|
1344
1602
|
console.log(getStorageSetupHelp("s3"));
|
|
1345
1603
|
}
|
|
1346
|
-
process.exit(1);
|
|
1604
|
+
if (!noExit) process.exit(1);
|
|
1605
|
+
return { ...publishResult, success: false, error: error.message };
|
|
1347
1606
|
}
|
|
1348
1607
|
|
|
1349
1608
|
const { apiKey, projectId, storageMode: resolvedMode } = projectContext;
|
|
@@ -1368,8 +1627,6 @@ async function publishCommand(options = {}) {
|
|
|
1368
1627
|
// Get feature toggles
|
|
1369
1628
|
const features = docSyncConfig?._metadata?.features || {
|
|
1370
1629
|
visuals: true,
|
|
1371
|
-
docs: false,
|
|
1372
|
-
changelog: true,
|
|
1373
1630
|
};
|
|
1374
1631
|
|
|
1375
1632
|
// Get git information
|
|
@@ -1388,7 +1645,7 @@ async function publishCommand(options = {}) {
|
|
|
1388
1645
|
});
|
|
1389
1646
|
console.log(chalk.green(` ✔ Version tag "${tag}" created`));
|
|
1390
1647
|
console.log(
|
|
1391
|
-
chalk.gray(` Pinned URL: cdn.reshot.dev/assets/{
|
|
1648
|
+
chalk.gray(` Pinned URL: cdn.reshot.dev/v1/assets/{projectId}/{visualKey}?tag=${tag}\n`),
|
|
1392
1649
|
);
|
|
1393
1650
|
} catch (tagError) {
|
|
1394
1651
|
console.log(
|
|
@@ -1401,9 +1658,51 @@ async function publishCommand(options = {}) {
|
|
|
1401
1658
|
if (features.visuals === true) {
|
|
1402
1659
|
const projectRoot = process.cwd();
|
|
1403
1660
|
const outputBaseDir = path.join(projectRoot, ".reshot", "output");
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1661
|
+
let screenshotFiles = [];
|
|
1662
|
+
let screenshotScopeLabel = "all-output";
|
|
1663
|
+
if (fs.existsSync(outputBaseDir)) {
|
|
1664
|
+
if (allOutput) {
|
|
1665
|
+
screenshotFiles = findAssetFiles(outputBaseDir);
|
|
1666
|
+
} else {
|
|
1667
|
+
const usable = getLatestUsableRunManifest();
|
|
1668
|
+
if (!usable) {
|
|
1669
|
+
throw new Error(
|
|
1670
|
+
"No run manifest with successful scenarios found. Run `reshot run` first or use `reshot publish --all-output` to publish from the full output tree.",
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
const { manifest: latestRunManifest, isFallback, isPartialSuccess } = usable;
|
|
1674
|
+
|
|
1675
|
+
if (isFallback) {
|
|
1676
|
+
const age = latestRunManifest.generatedAt
|
|
1677
|
+
? `generated ${latestRunManifest.generatedAt}`
|
|
1678
|
+
: "unknown age";
|
|
1679
|
+
console.log(
|
|
1680
|
+
chalk.yellow(
|
|
1681
|
+
`\n WARNING: Latest run has no successful scenarios. Falling back to older manifest (${age}).` +
|
|
1682
|
+
`\n The published screenshots may be STALE. Run \`reshot run\` to capture fresh screenshots.\n`,
|
|
1683
|
+
),
|
|
1684
|
+
);
|
|
1685
|
+
} else if (isPartialSuccess) {
|
|
1686
|
+
const allScenarios = latestRunManifest.scenarios || [];
|
|
1687
|
+
const failed = allScenarios.filter((s) => s.success === false);
|
|
1688
|
+
const succeeded = allScenarios.filter((s) => s.success !== false);
|
|
1689
|
+
console.log(
|
|
1690
|
+
chalk.yellow(
|
|
1691
|
+
`\n WARNING: Latest run had partial failures (${succeeded.length} passed, ${failed.length} failed).` +
|
|
1692
|
+
`\n Publishing only the ${succeeded.length} successful scenario(s).` +
|
|
1693
|
+
`\n Failed: ${failed.map((s) => s.key).join(", ")}\n`,
|
|
1694
|
+
),
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
const manifestScoped = resolveManifestScopedScreenshotFiles(
|
|
1699
|
+
outputBaseDir,
|
|
1700
|
+
latestRunManifest,
|
|
1701
|
+
);
|
|
1702
|
+
screenshotFiles = manifestScoped.screenshotFiles;
|
|
1703
|
+
screenshotScopeLabel = `${manifestScoped.mode}:${manifestScoped.scenarioCount}`;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1407
1706
|
const screenshotGroups =
|
|
1408
1707
|
screenshotFiles.length > 0
|
|
1409
1708
|
? groupAssetsByScenario(screenshotFiles, outputBaseDir)
|
|
@@ -1434,10 +1733,16 @@ async function publishCommand(options = {}) {
|
|
|
1434
1733
|
` (${screenshotFiles.length} screenshots, ${exportVideoFiles.length} videos)\n`,
|
|
1435
1734
|
),
|
|
1436
1735
|
);
|
|
1736
|
+
console.log(
|
|
1737
|
+
chalk.gray(
|
|
1738
|
+
` Screenshot scope: ${allOutput ? "all historical output (--all-output)" : `latest successful run (${screenshotScopeLabel})`}\n`,
|
|
1739
|
+
),
|
|
1740
|
+
);
|
|
1437
1741
|
|
|
1438
1742
|
let successCount = 0;
|
|
1439
1743
|
let failCount = 0;
|
|
1440
1744
|
let skippedCount = 0;
|
|
1745
|
+
let reviewPendingCount = 0;
|
|
1441
1746
|
let viewUrl = null;
|
|
1442
1747
|
|
|
1443
1748
|
// Load diff manifests for attaching diff data to screenshot assets
|
|
@@ -1477,12 +1782,14 @@ async function publishCommand(options = {}) {
|
|
|
1477
1782
|
projectId,
|
|
1478
1783
|
groupedAssets,
|
|
1479
1784
|
docSyncConfig,
|
|
1480
|
-
{ commitHash, commitMessage, publishSessionId },
|
|
1785
|
+
{ commitHash, commitMessage, publishSessionId, tag },
|
|
1481
1786
|
diffManifests,
|
|
1787
|
+
{ autoApprove },
|
|
1482
1788
|
);
|
|
1483
1789
|
successCount = result.successCount;
|
|
1484
1790
|
failCount = result.failCount;
|
|
1485
1791
|
skippedCount = result.skippedCount || 0;
|
|
1792
|
+
reviewPendingCount = result.reviewPendingCount || 0;
|
|
1486
1793
|
viewUrl = result.viewUrl || null;
|
|
1487
1794
|
} catch (txError) {
|
|
1488
1795
|
// Fall back to legacy flow if transactional fails
|
|
@@ -1496,7 +1803,8 @@ async function publishCommand(options = {}) {
|
|
|
1496
1803
|
projectId,
|
|
1497
1804
|
groupedAssets,
|
|
1498
1805
|
docSyncConfig,
|
|
1499
|
-
{ commitHash, commitMessage, publishSessionId },
|
|
1806
|
+
{ commitHash, commitMessage, publishSessionId, tag },
|
|
1807
|
+
{ autoApprove },
|
|
1500
1808
|
);
|
|
1501
1809
|
successCount = result.successCount;
|
|
1502
1810
|
failCount = result.failCount;
|
|
@@ -1508,7 +1816,8 @@ async function publishCommand(options = {}) {
|
|
|
1508
1816
|
projectId,
|
|
1509
1817
|
groupedAssets,
|
|
1510
1818
|
docSyncConfig,
|
|
1511
|
-
{ commitHash, commitMessage, publishSessionId },
|
|
1819
|
+
{ commitHash, commitMessage, publishSessionId, tag },
|
|
1820
|
+
{ autoApprove },
|
|
1512
1821
|
);
|
|
1513
1822
|
successCount = result.successCount;
|
|
1514
1823
|
failCount = result.failCount;
|
|
@@ -1532,164 +1841,75 @@ async function publishCommand(options = {}) {
|
|
|
1532
1841
|
}
|
|
1533
1842
|
if (failCount > 0) {
|
|
1534
1843
|
console.log(chalk.red(` ✖ Failed: ${failCount}`));
|
|
1844
|
+
const failedKeys = viewUrl ? [] : (publishResult.failedUploadKeys || []);
|
|
1845
|
+
if (failedKeys.length > 0) {
|
|
1846
|
+
for (const key of failedKeys.slice(0, 5)) {
|
|
1847
|
+
console.log(chalk.red(` - ${key}`));
|
|
1848
|
+
}
|
|
1849
|
+
if (failedKeys.length > 5) {
|
|
1850
|
+
console.log(chalk.red(` ... and ${failedKeys.length - 5} more`));
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1535
1853
|
}
|
|
1536
1854
|
if (viewUrl) {
|
|
1537
1855
|
console.log(chalk.cyan(`\n 🔗 View in platform: ${viewUrl}`));
|
|
1538
1856
|
}
|
|
1539
1857
|
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
);
|
|
1547
|
-
|
|
1548
|
-
console.log(
|
|
1549
|
-
chalk.gray(
|
|
1550
|
-
"\n 💡 Tip: Run 'reshot run' before publish to see change percentages.",
|
|
1551
|
-
),
|
|
1552
|
-
);
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
} else {
|
|
1556
|
-
console.log(
|
|
1557
|
-
chalk.yellow(" ⚠ Visual asset publishing is disabled for this project."),
|
|
1558
|
-
);
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
// Stream B: Documentation
|
|
1562
|
-
if (features.docs === true) {
|
|
1563
|
-
console.log(chalk.cyan("\n📚 Publishing documentation...\n"));
|
|
1564
|
-
|
|
1565
|
-
if (!docSyncConfig || !docSyncConfig.docs) {
|
|
1566
|
-
console.log(
|
|
1567
|
-
chalk.yellow(
|
|
1568
|
-
" ⚠ No docs configuration found in docsync.config.json. Skipping docs stream.",
|
|
1569
|
-
),
|
|
1570
|
-
);
|
|
1571
|
-
} else {
|
|
1572
|
-
const docsConfig = docSyncConfig.docs;
|
|
1573
|
-
const docsRoot = docsConfig.root || "./docs";
|
|
1574
|
-
const includePatterns = docsConfig.include || ["**/*.md", "**/*.mdx"];
|
|
1575
|
-
const excludePatterns = docsConfig.exclude || [
|
|
1576
|
-
"**/node_modules/**",
|
|
1577
|
-
"**/.git/**",
|
|
1578
|
-
];
|
|
1579
|
-
|
|
1580
|
-
const docFiles = findDocFiles(docsRoot, includePatterns, excludePatterns);
|
|
1581
|
-
|
|
1582
|
-
if (docFiles.length === 0) {
|
|
1583
|
-
console.log(
|
|
1584
|
-
chalk.yellow(" ⚠ No documentation files found to publish."),
|
|
1585
|
-
);
|
|
1586
|
-
} else {
|
|
1587
|
-
console.log(
|
|
1588
|
-
chalk.cyan(` Found ${docFiles.length} documentation file(s)\n`),
|
|
1589
|
-
);
|
|
1590
|
-
|
|
1591
|
-
const docsPayload = [];
|
|
1592
|
-
for (const docFile of docFiles) {
|
|
1593
|
-
const content = fs.readFileSync(docFile.fullPath, "utf-8");
|
|
1594
|
-
const { frontmatter, content: docContent } =
|
|
1595
|
-
parseFrontmatter(content);
|
|
1596
|
-
|
|
1597
|
-
docsPayload.push({
|
|
1598
|
-
path: docFile.relativePath,
|
|
1599
|
-
content: docContent,
|
|
1600
|
-
frontmatter:
|
|
1601
|
-
Object.keys(frontmatter).length > 0 ? frontmatter : undefined,
|
|
1602
|
-
status: frontmatter.status || "draft",
|
|
1603
|
-
});
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
// Docs publishing requires platform (no BYOS support yet)
|
|
1607
|
-
if (resolvedMode === "byos") {
|
|
1608
|
-
console.log(
|
|
1609
|
-
chalk.yellow(
|
|
1610
|
-
" ⚠ Documentation publishing requires Reshot platform.",
|
|
1611
|
-
),
|
|
1612
|
-
);
|
|
1858
|
+
if (!autoApprove && successCount > 0) {
|
|
1859
|
+
// Only NEW/CHANGED captures await review; unchanged captures dedup to
|
|
1860
|
+
// their existing (often already-approved) version and stay live. Report
|
|
1861
|
+
// the accurate pending count so a re-publish doesn't look like it reset
|
|
1862
|
+
// already-approved work (audit run-10 F1).
|
|
1863
|
+
const pending = reviewPendingCount;
|
|
1864
|
+
const keptLive = Math.max(0, successCount - pending);
|
|
1865
|
+
if (pending > 0) {
|
|
1613
1866
|
console.log(
|
|
1614
|
-
chalk.
|
|
1615
|
-
|
|
1867
|
+
chalk.cyan(
|
|
1868
|
+
`\n ℹ ${pending} new/changed visual${pending === 1 ? "" : "s"} awaiting review (PENDING)`,
|
|
1616
1869
|
),
|
|
1617
1870
|
);
|
|
1618
|
-
|
|
1619
|
-
try {
|
|
1620
|
-
const result = await apiClient.publishDocs(apiKey, {
|
|
1621
|
-
projectId,
|
|
1622
|
-
docs: docsPayload,
|
|
1623
|
-
});
|
|
1871
|
+
if (keptLive > 0) {
|
|
1624
1872
|
console.log(
|
|
1625
|
-
chalk.
|
|
1626
|
-
`
|
|
1627
|
-
result.updated || 0
|
|
1628
|
-
} doc(s)`,
|
|
1873
|
+
chalk.gray(
|
|
1874
|
+
` ${keptLive} unchanged visual${keptLive === 1 ? "" : "s"} kept current state (live pointers preserved; no re-approval needed).`,
|
|
1629
1875
|
),
|
|
1630
1876
|
);
|
|
1631
|
-
} catch (error) {
|
|
1632
|
-
console.log(
|
|
1633
|
-
chalk.red(` ✖ Failed to publish docs: ${error.message}`),
|
|
1634
|
-
);
|
|
1635
1877
|
}
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
} else {
|
|
1640
|
-
console.log(
|
|
1641
|
-
chalk.yellow(
|
|
1642
|
-
" ⚠ Documentation publishing is disabled for this project.",
|
|
1643
|
-
),
|
|
1644
|
-
);
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
// Stream C: Changelog
|
|
1648
|
-
if (features.changelog === true) {
|
|
1649
|
-
// Changelog requires platform (no BYOS support)
|
|
1650
|
-
if (resolvedMode === "byos") {
|
|
1651
|
-
console.log(chalk.cyan("\n📝 Changelog drafts...\n"));
|
|
1652
|
-
console.log(
|
|
1653
|
-
chalk.yellow(
|
|
1654
|
-
" ⚠ Changelog requires Reshot platform for tracking and publishing.",
|
|
1655
|
-
),
|
|
1656
|
-
);
|
|
1657
|
-
console.log(
|
|
1658
|
-
chalk.gray(" Run 'reshot auth' to enable changelog generation."),
|
|
1659
|
-
);
|
|
1660
|
-
} else {
|
|
1661
|
-
console.log(chalk.cyan("\n📝 Posting changelog drafts...\n"));
|
|
1662
|
-
const lastPublishedHash = settings?.lastPublishedCommitHash;
|
|
1663
|
-
const recentCommits = getRecentCommits(lastPublishedHash);
|
|
1664
|
-
|
|
1665
|
-
if (recentCommits.length > 0) {
|
|
1666
|
-
try {
|
|
1667
|
-
await apiClient.postChangelogDrafts(projectId, recentCommits, apiKey);
|
|
1668
1878
|
console.log(
|
|
1669
|
-
chalk.
|
|
1670
|
-
|
|
1879
|
+
chalk.gray(
|
|
1880
|
+
" To skip review for first-time captures, re-run with `reshot publish --auto-approve`",
|
|
1671
1881
|
),
|
|
1672
1882
|
);
|
|
1673
|
-
|
|
1674
|
-
// Update last published commit hash
|
|
1675
|
-
if (settings) {
|
|
1676
|
-
settings.lastPublishedCommitHash = commitHash;
|
|
1677
|
-
config.writeSettings(settings);
|
|
1678
|
-
}
|
|
1679
|
-
} catch (error) {
|
|
1680
1883
|
console.log(
|
|
1681
|
-
chalk.
|
|
1682
|
-
|
|
1884
|
+
chalk.gray(" or approve them in the studio link above."),
|
|
1885
|
+
);
|
|
1886
|
+
} else {
|
|
1887
|
+
console.log(
|
|
1888
|
+
chalk.cyan(
|
|
1889
|
+
`\n ℹ No new captures to review — all ${successCount} published visual${successCount === 1 ? "" : "s"} were unchanged; existing live pointers are preserved (already-approved stay live, pending stay pending).`,
|
|
1683
1890
|
),
|
|
1684
1891
|
);
|
|
1685
1892
|
}
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// Helpful guidance about diff percentages
|
|
1896
|
+
if (diffManifests && diffManifests.size > 0) {
|
|
1897
|
+
console.log(
|
|
1898
|
+
chalk.gray(
|
|
1899
|
+
"\n 💡 Diff data included! View change percentages in the platform.",
|
|
1900
|
+
),
|
|
1901
|
+
);
|
|
1686
1902
|
} else {
|
|
1687
|
-
console.log(
|
|
1903
|
+
console.log(
|
|
1904
|
+
chalk.gray(
|
|
1905
|
+
"\n 💡 Tip: Run 'reshot run' before publish to see change percentages.",
|
|
1906
|
+
),
|
|
1907
|
+
);
|
|
1688
1908
|
}
|
|
1689
1909
|
}
|
|
1690
1910
|
} else {
|
|
1691
1911
|
console.log(
|
|
1692
|
-
chalk.yellow(" ⚠
|
|
1912
|
+
chalk.yellow(" ⚠ Visual asset publishing is disabled for this project."),
|
|
1693
1913
|
);
|
|
1694
1914
|
}
|
|
1695
1915
|
|
|
@@ -1700,7 +1920,7 @@ async function publishCommand(options = {}) {
|
|
|
1700
1920
|
console.log(chalk.gray(" • Unbreakable URLs that never change"));
|
|
1701
1921
|
console.log(chalk.gray(" • Version history and rollback"));
|
|
1702
1922
|
console.log(chalk.gray(" • Team collaboration and RBAC"));
|
|
1703
|
-
console.log(chalk.gray(" •
|
|
1923
|
+
console.log(chalk.gray(" • Drift detection and notifications"));
|
|
1704
1924
|
console.log(chalk.gray("\n Run 'reshot auth' to connect your project."));
|
|
1705
1925
|
}
|
|
1706
1926
|
|
|
@@ -1715,7 +1935,14 @@ async function publishCommand(options = {}) {
|
|
|
1715
1935
|
|
|
1716
1936
|
console.log();
|
|
1717
1937
|
|
|
1718
|
-
return
|
|
1938
|
+
return {
|
|
1939
|
+
...publishResult,
|
|
1940
|
+
success: publishResult.assetsFailed === 0 && publishResult.assetsProcessed > 0,
|
|
1941
|
+
};
|
|
1719
1942
|
}
|
|
1720
1943
|
|
|
1721
1944
|
module.exports = publishCommand;
|
|
1945
|
+
module.exports.groupAssetsByScenario = groupAssetsByScenario;
|
|
1946
|
+
module.exports.resolveManifestScopedScreenshotFiles =
|
|
1947
|
+
resolveManifestScopedScreenshotFiles;
|
|
1948
|
+
module.exports.collectAssetFilesFromDirectories = collectAssetFilesFromDirectories;
|