@decocms/blocks-cli 7.40.0 → 7.41.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/blocks-cli",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.41.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco codegen (generate-blocks, generate-schema, generate-invoke) and Fresh-to-TanStack migration tooling",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"lint:unused": "knip"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@decocms/blocks": "7.
|
|
34
|
+
"@decocms/blocks": "7.41.1",
|
|
35
35
|
"ts-morph": "^27.0.0",
|
|
36
36
|
"tsx": "^4.22.5"
|
|
37
37
|
},
|
|
@@ -1,38 +1,39 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
// `lib-utils` is imported lazily — see end of phase-cleanup. Eager
|
|
4
|
+
// generation of all 11 shims left every site with dead code that had
|
|
5
|
+
// to be cleaned up by hand.
|
|
6
|
+
import { extractTheme } from "./analyzers/theme-extractor";
|
|
7
|
+
import { generateAppCss } from "./templates/app-css";
|
|
8
|
+
import { generateCacheConfig } from "./templates/cache-config";
|
|
9
|
+
import { generateCiFiles } from "./templates/ci-yml";
|
|
10
|
+
import { generateCommerceInit } from "./templates/commerce-init";
|
|
11
|
+
import { generateCommerceLoaders } from "./templates/commerce-loaders";
|
|
12
|
+
import { generateMigrationPolicyPointerRule } from "./templates/cursor-rules";
|
|
13
|
+
import { generateHooks } from "./templates/hooks";
|
|
14
|
+
import { generateKnipConfig } from "./templates/knip-config";
|
|
15
|
+
import { generateLockfileCheckYml } from "./templates/lockfile-check-yml";
|
|
16
|
+
import { generateMainPushGuardYml } from "./templates/main-push-guard-yml";
|
|
5
17
|
import {
|
|
6
18
|
CANONICAL_BUN_VERSION,
|
|
7
19
|
CANONICAL_NODE_VERSION,
|
|
8
20
|
generatePackageJson,
|
|
9
21
|
} from "./templates/package-json";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
22
|
+
import { generateParityYml } from "./templates/parity-yml";
|
|
23
|
+
import { generatePerfFiles } from "./templates/perf-yml";
|
|
24
|
+
import { generatePlaywrightFiles } from "./templates/playwright-yml";
|
|
25
|
+
import { generateReactDoctorYml } from "./templates/react-doctor-yml";
|
|
14
26
|
import { generateRoutes } from "./templates/routes";
|
|
15
|
-
import {
|
|
27
|
+
import { generateSdkFiles } from "./templates/sdk-gen";
|
|
28
|
+
import { generateSectionLoaders } from "./templates/section-loaders";
|
|
16
29
|
import { generateServerEntry } from "./templates/server-entry";
|
|
17
|
-
import {
|
|
30
|
+
import { generateSetup } from "./templates/setup";
|
|
31
|
+
import { generateTsconfig } from "./templates/tsconfig";
|
|
18
32
|
import { generateTypeFiles } from "./templates/types-gen";
|
|
19
33
|
import { generateUiComponents } from "./templates/ui-components";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { generateSectionLoaders } from "./templates/section-loaders";
|
|
24
|
-
import { generateCacheConfig } from "./templates/cache-config";
|
|
25
|
-
import { generateSdkFiles } from "./templates/sdk-gen";
|
|
26
|
-
import { generateMigrationPolicyPointerRule } from "./templates/cursor-rules";
|
|
27
|
-
import { generatePerfFiles } from "./templates/perf-yml";
|
|
28
|
-
import { generateCiFiles } from "./templates/ci-yml";
|
|
29
|
-
import { generateMainPushGuardYml } from "./templates/main-push-guard-yml";
|
|
30
|
-
import { generatePlaywrightFiles } from "./templates/playwright-yml";
|
|
31
|
-
import { generateReactDoctorYml } from "./templates/react-doctor-yml";
|
|
32
|
-
// `lib-utils` is imported lazily — see end of phase-cleanup. Eager
|
|
33
|
-
// generation of all 11 shims left every site with dead code that had
|
|
34
|
-
// to be cleaned up by hand.
|
|
35
|
-
import { extractTheme } from "./analyzers/theme-extractor";
|
|
34
|
+
import { generateViteConfig } from "./templates/vite-config";
|
|
35
|
+
import type { MigrationContext } from "./types";
|
|
36
|
+
import { log, logPhase } from "./types";
|
|
36
37
|
|
|
37
38
|
function writeFile(ctx: MigrationContext, relPath: string, content: string) {
|
|
38
39
|
const fullPath = path.join(ctx.sourceDir, relPath);
|
|
@@ -78,13 +79,21 @@ export function scaffold(ctx: MigrationContext): void {
|
|
|
78
79
|
// does NOT scaffold deploy/preview/sync-secrets workflows in the site
|
|
79
80
|
// repo; the operator wires the repo<->worker connection in the CF
|
|
80
81
|
// dashboard once after the first push.
|
|
81
|
-
writeFile(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
writeFile(
|
|
83
|
+
ctx,
|
|
84
|
+
".prettierrc",
|
|
85
|
+
JSON.stringify(
|
|
86
|
+
{
|
|
87
|
+
semi: true,
|
|
88
|
+
singleQuote: false,
|
|
89
|
+
trailingComma: "all" as const,
|
|
90
|
+
printWidth: 100,
|
|
91
|
+
tabWidth: 2,
|
|
92
|
+
},
|
|
93
|
+
null,
|
|
94
|
+
2,
|
|
95
|
+
) + "\n",
|
|
96
|
+
);
|
|
88
97
|
|
|
89
98
|
// PR-time lockfile guardrail. Lives in the site repo (per-site, not
|
|
90
99
|
// a centralised reusable workflow) because per D6.3 we are not
|
|
@@ -117,6 +126,10 @@ export function scaffold(ctx: MigrationContext): void {
|
|
|
117
126
|
// Advisory React lint (react-doctor): comments on PRs, never fails.
|
|
118
127
|
writeFile(ctx, ".github/workflows/react-doctor.yml", generateReactDoctorYml());
|
|
119
128
|
|
|
129
|
+
// Advisory parity validation: compares each PR preview against the original
|
|
130
|
+
// live storefront (@decocms/parity). Inert until PARITY_PROD_URL is set.
|
|
131
|
+
writeFile(ctx, ".github/workflows/parity.yml", generateParityYml(ctx.siteName));
|
|
132
|
+
|
|
120
133
|
// Server entry files (server.ts, worker-entry.ts, router.tsx, runtime.ts, context.ts)
|
|
121
134
|
writeMultiFile(ctx, generateServerEntry(ctx));
|
|
122
135
|
|
|
@@ -255,18 +268,20 @@ export { type Font as SiteThemeFont };
|
|
|
255
268
|
}
|
|
256
269
|
|
|
257
270
|
function generateWranglerConfig(ctx: MigrationContext): string {
|
|
258
|
-
return
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
271
|
+
return (
|
|
272
|
+
JSON.stringify(
|
|
273
|
+
{
|
|
274
|
+
name: ctx.siteName,
|
|
275
|
+
main: "src/worker-entry.ts",
|
|
276
|
+
compatibility_date: "2025-05-01",
|
|
277
|
+
compatibility_flags: ["nodejs_compat", "no_handle_cross_request_promise_resolution"],
|
|
278
|
+
cache: { enabled: true },
|
|
279
|
+
kv_namespaces: [{ binding: "SITES_KV", id: "dev-sites-kv" }],
|
|
280
|
+
},
|
|
281
|
+
null,
|
|
282
|
+
2,
|
|
283
|
+
) + "\n"
|
|
284
|
+
);
|
|
270
285
|
}
|
|
271
286
|
|
|
272
287
|
function generateGitignore(): string {
|
|
@@ -580,10 +595,7 @@ ${entries.join("\n")}
|
|
|
580
595
|
}
|
|
581
596
|
|
|
582
597
|
function hasLocationMatcher(ctx: MigrationContext): boolean {
|
|
583
|
-
const dirs = [
|
|
584
|
-
path.join(ctx.sourceDir, "matchers"),
|
|
585
|
-
path.join(ctx.sourceDir, "src", "matchers"),
|
|
586
|
-
];
|
|
598
|
+
const dirs = [path.join(ctx.sourceDir, "matchers"), path.join(ctx.sourceDir, "src", "matchers")];
|
|
587
599
|
for (const dir of dirs) {
|
|
588
600
|
if (fs.existsSync(dir)) {
|
|
589
601
|
const files = fs.readdirSync(dir);
|
|
@@ -598,7 +610,9 @@ function hasLocationMatcher(ctx: MigrationContext): boolean {
|
|
|
598
610
|
try {
|
|
599
611
|
const content = fs.readFileSync(path.join(blocksDir, file), "utf-8");
|
|
600
612
|
if (content.includes("website/matchers/location")) return true;
|
|
601
|
-
} catch {
|
|
613
|
+
} catch {
|
|
614
|
+
/* skip */
|
|
615
|
+
}
|
|
602
616
|
}
|
|
603
617
|
}
|
|
604
618
|
return false;
|
|
@@ -25,6 +25,7 @@ const REQUIRED_FILES = [
|
|
|
25
25
|
".github/workflows/main-push-guard.yml",
|
|
26
26
|
".github/workflows/playwright.yml",
|
|
27
27
|
".github/workflows/react-doctor.yml",
|
|
28
|
+
".github/workflows/parity.yml",
|
|
28
29
|
"tools/gates/no-suppressions.sh",
|
|
29
30
|
"playwright.config.ts",
|
|
30
31
|
"knip.config.ts",
|
|
@@ -74,9 +75,7 @@ export const checks: Check[] = [
|
|
|
74
75
|
name: "All scaffolded files exist",
|
|
75
76
|
severity: "error",
|
|
76
77
|
fn: (ctx) => {
|
|
77
|
-
const missing = REQUIRED_FILES.filter(
|
|
78
|
-
(f) => !fs.existsSync(path.join(ctx.sourceDir, f)),
|
|
79
|
-
);
|
|
78
|
+
const missing = REQUIRED_FILES.filter((f) => !fs.existsSync(path.join(ctx.sourceDir, f)));
|
|
80
79
|
if (missing.length > 0) {
|
|
81
80
|
console.log(` Missing: ${missing.join(", ")}`);
|
|
82
81
|
return false;
|
|
@@ -88,9 +87,7 @@ export const checks: Check[] = [
|
|
|
88
87
|
name: "Old artifacts removed",
|
|
89
88
|
severity: "error",
|
|
90
89
|
fn: (ctx) => {
|
|
91
|
-
const remaining = MUST_NOT_EXIST.filter(
|
|
92
|
-
(f) => fs.existsSync(path.join(ctx.sourceDir, f)),
|
|
93
|
-
);
|
|
90
|
+
const remaining = MUST_NOT_EXIST.filter((f) => fs.existsSync(path.join(ctx.sourceDir, f)));
|
|
94
91
|
if (remaining.length > 0) {
|
|
95
92
|
console.log(` Still exists: ${remaining.join(", ")}`);
|
|
96
93
|
return false;
|
|
@@ -141,14 +138,16 @@ export const checks: Check[] = [
|
|
|
141
138
|
},
|
|
142
139
|
},
|
|
143
140
|
{
|
|
144
|
-
name:
|
|
141
|
+
name: "No class= in JSX (should be className=)",
|
|
145
142
|
severity: "warning",
|
|
146
143
|
fn: (ctx) => {
|
|
147
144
|
const srcDir = path.join(ctx.sourceDir, "src");
|
|
148
145
|
if (!fs.existsSync(srcDir)) return true;
|
|
149
146
|
const bad = findFilesWithPattern(srcDir, /<[a-zA-Z][^>]*\sclass\s*=/);
|
|
150
147
|
if (bad.length > 0) {
|
|
151
|
-
console.log(
|
|
148
|
+
console.log(
|
|
149
|
+
` Still has class= in JSX: ${bad.slice(0, 5).join(", ")}${bad.length > 5 ? ` (+${bad.length - 5} more)` : ""}`,
|
|
150
|
+
);
|
|
152
151
|
return false;
|
|
153
152
|
}
|
|
154
153
|
return true;
|
|
@@ -163,12 +162,8 @@ export const checks: Check[] = [
|
|
|
163
162
|
console.log(" public/ directory missing");
|
|
164
163
|
return false;
|
|
165
164
|
}
|
|
166
|
-
const hasSprites = fs.existsSync(
|
|
167
|
-
|
|
168
|
-
);
|
|
169
|
-
const hasFavicon = fs.existsSync(
|
|
170
|
-
path.join(publicDir, "favicon.ico"),
|
|
171
|
-
);
|
|
165
|
+
const hasSprites = fs.existsSync(path.join(publicDir, "sprites.svg"));
|
|
166
|
+
const hasFavicon = fs.existsSync(path.join(publicDir, "favicon.ico"));
|
|
172
167
|
if (!hasSprites) console.log(" Missing: public/sprites.svg");
|
|
173
168
|
if (!hasFavicon) console.log(" Missing: public/favicon.ico");
|
|
174
169
|
return hasSprites && hasFavicon;
|
|
@@ -277,7 +272,9 @@ export const checks: Check[] = [
|
|
|
277
272
|
const bad = findFilesWithPattern(srcDir, /(?<!<(?:img|Image)[^>]*)-z-\d+/);
|
|
278
273
|
if (bad.length > 0) {
|
|
279
274
|
console.log(` Negative z-index on non-image elements: ${bad.join(", ")}`);
|
|
280
|
-
console.log(
|
|
275
|
+
console.log(
|
|
276
|
+
` These may be invisible due to stacking contexts. Replace with z-0 or positive z-index.`,
|
|
277
|
+
);
|
|
281
278
|
return false;
|
|
282
279
|
}
|
|
283
280
|
return true;
|
|
@@ -324,7 +321,9 @@ export const checks: Check[] = [
|
|
|
324
321
|
// Check that runtime.ts exists and has the invoke proxy
|
|
325
322
|
const runtimePath = path.join(srcDir, "runtime.ts");
|
|
326
323
|
if (!fs.existsSync(runtimePath)) {
|
|
327
|
-
console.log(
|
|
324
|
+
console.log(
|
|
325
|
+
` Files use invoke.* but src/runtime.ts is missing: ${hasInvoke.join(", ")}`,
|
|
326
|
+
);
|
|
328
327
|
return false;
|
|
329
328
|
}
|
|
330
329
|
}
|
|
@@ -379,7 +378,10 @@ export const checks: Check[] = [
|
|
|
379
378
|
fn: (ctx) => {
|
|
380
379
|
const srcDir = path.join(ctx.sourceDir, "src");
|
|
381
380
|
if (!fs.existsSync(srcDir)) return true;
|
|
382
|
-
const bad = findFilesWithPattern(
|
|
381
|
+
const bad = findFilesWithPattern(
|
|
382
|
+
srcDir,
|
|
383
|
+
/\bhx-(?:get|post|put|delete|patch|trigger|target|swap|on|indicator|sync|select)\b/,
|
|
384
|
+
);
|
|
383
385
|
if (bad.length > 0) {
|
|
384
386
|
console.log(` HTMX attributes found (needs manual React migration): ${bad.join(", ")}`);
|
|
385
387
|
return false;
|
|
@@ -433,7 +435,9 @@ export const checks: Check[] = [
|
|
|
433
435
|
];
|
|
434
436
|
const unique = [...new Set(bad)];
|
|
435
437
|
if (unique.length > 0) {
|
|
436
|
-
console.log(
|
|
438
|
+
console.log(
|
|
439
|
+
` Still references frozen @decocms/start or @decocms/apps/* specifiers: ${unique.join(", ")}`,
|
|
440
|
+
);
|
|
437
441
|
return false;
|
|
438
442
|
}
|
|
439
443
|
return true;
|
|
@@ -450,9 +454,7 @@ export const checks: Check[] = [
|
|
|
450
454
|
"src/setup/commerce-init.ts",
|
|
451
455
|
"src/setup/section-loaders.ts",
|
|
452
456
|
];
|
|
453
|
-
const missing = setupFiles.filter(
|
|
454
|
-
(f) => !fs.existsSync(path.join(ctx.sourceDir, f)),
|
|
455
|
-
);
|
|
457
|
+
const missing = setupFiles.filter((f) => !fs.existsSync(path.join(ctx.sourceDir, f)));
|
|
456
458
|
if (missing.length > 0) {
|
|
457
459
|
console.log(` Missing setup infrastructure: ${missing.join(", ")}`);
|
|
458
460
|
return false;
|
|
@@ -468,9 +470,13 @@ export const checks: Check[] = [
|
|
|
468
470
|
if (fs.existsSync(islandsDir)) {
|
|
469
471
|
try {
|
|
470
472
|
const files = fs.readdirSync(islandsDir, { recursive: true });
|
|
471
|
-
const tsxFiles = (files as string[]).filter(
|
|
473
|
+
const tsxFiles = (files as string[]).filter(
|
|
474
|
+
(f: string) => f.endsWith(".tsx") || f.endsWith(".ts"),
|
|
475
|
+
);
|
|
472
476
|
if (tsxFiles.length > 0) {
|
|
473
|
-
console.log(
|
|
477
|
+
console.log(
|
|
478
|
+
` src/islands/ still has ${tsxFiles.length} files — should be moved to components/`,
|
|
479
|
+
);
|
|
474
480
|
return false;
|
|
475
481
|
}
|
|
476
482
|
} catch {}
|
|
@@ -487,9 +493,7 @@ export const checks: Check[] = [
|
|
|
487
493
|
"src/hooks/useUser.ts",
|
|
488
494
|
"src/hooks/useWishlist.ts",
|
|
489
495
|
];
|
|
490
|
-
const missing = hookFiles.filter(
|
|
491
|
-
(f) => !fs.existsSync(path.join(ctx.sourceDir, f)),
|
|
492
|
-
);
|
|
496
|
+
const missing = hookFiles.filter((f) => !fs.existsSync(path.join(ctx.sourceDir, f)));
|
|
493
497
|
if (missing.length > 0) {
|
|
494
498
|
console.log(` Missing hooks: ${missing.join(", ")}`);
|
|
495
499
|
return false;
|
|
@@ -507,9 +511,7 @@ export const checks: Check[] = [
|
|
|
507
511
|
"src/types/deco.ts",
|
|
508
512
|
"src/types/commerce-app.ts",
|
|
509
513
|
];
|
|
510
|
-
const missing = typeFiles.filter(
|
|
511
|
-
(f) => !fs.existsSync(path.join(ctx.sourceDir, f)),
|
|
512
|
-
);
|
|
514
|
+
const missing = typeFiles.filter((f) => !fs.existsSync(path.join(ctx.sourceDir, f)));
|
|
513
515
|
if (missing.length > 0) {
|
|
514
516
|
console.log(` Missing type files: ${missing.join(", ")}`);
|
|
515
517
|
return false;
|
|
@@ -530,7 +532,8 @@ function findFilesWithPattern(
|
|
|
530
532
|
for (const entry of entries) {
|
|
531
533
|
const fullPath = path.join(dir, entry.name);
|
|
532
534
|
if (entry.isDirectory()) {
|
|
533
|
-
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "server")
|
|
535
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "server")
|
|
536
|
+
continue;
|
|
534
537
|
findFilesWithPattern(fullPath, pattern, results, root);
|
|
535
538
|
} else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
|
|
536
539
|
const content = fs.readFileSync(fullPath, "utf-8");
|
|
@@ -563,7 +566,8 @@ function findMatchesWithPattern(
|
|
|
563
566
|
for (const entry of entries) {
|
|
564
567
|
const fullPath = path.join(dir, entry.name);
|
|
565
568
|
if (entry.isDirectory()) {
|
|
566
|
-
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "server")
|
|
569
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "server")
|
|
570
|
+
continue;
|
|
567
571
|
findMatchesWithPattern(fullPath, pattern, results, root);
|
|
568
572
|
} else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
|
|
569
573
|
const content = fs.readFileSync(fullPath, "utf-8");
|
|
@@ -591,7 +595,11 @@ export function verify(ctx: MigrationContext): boolean {
|
|
|
591
595
|
|
|
592
596
|
for (const check of checks) {
|
|
593
597
|
const pass = check.fn(ctx);
|
|
594
|
-
const icon = pass
|
|
598
|
+
const icon = pass
|
|
599
|
+
? "\x1b[32m✓\x1b[0m"
|
|
600
|
+
: check.severity === "error"
|
|
601
|
+
? "\x1b[31m✗\x1b[0m"
|
|
602
|
+
: "\x1b[33m⚠\x1b[0m";
|
|
595
603
|
console.log(` ${icon} ${check.name}`);
|
|
596
604
|
if (!pass) {
|
|
597
605
|
if (check.severity === "error") errors++;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { generateCiFiles } from "./ci-yml";
|
|
3
3
|
import { generateMainPushGuardYml } from "./main-push-guard-yml";
|
|
4
|
+
import { generateParityYml } from "./parity-yml";
|
|
4
5
|
import { generatePlaywrightFiles } from "./playwright-yml";
|
|
5
6
|
import { generateReactDoctorYml } from "./react-doctor-yml";
|
|
6
7
|
|
|
@@ -25,7 +26,9 @@ describe("generateCiFiles", () => {
|
|
|
25
26
|
|
|
26
27
|
it("blocks on generate + build, keeps cleanliness gates advisory", () => {
|
|
27
28
|
// generate + build have NO continue-on-error
|
|
28
|
-
expect(ci).toMatch(
|
|
29
|
+
expect(ci).toMatch(
|
|
30
|
+
/Generate artifacts[\s\S]*?run: bun run generate && bun run generate:routes/,
|
|
31
|
+
);
|
|
29
32
|
expect(ci).toMatch(/Build \(vite\)\n\s+run: bun run build\n/);
|
|
30
33
|
// the four cleanliness gates are advisory
|
|
31
34
|
for (const advisory of ["Typecheck", "Format check", "Knip", "no new suppression"]) {
|
|
@@ -106,3 +109,17 @@ describe("generateReactDoctorYml", () => {
|
|
|
106
109
|
expect(yml).not.toMatch(/^\s*blocking: error/m);
|
|
107
110
|
});
|
|
108
111
|
});
|
|
112
|
+
|
|
113
|
+
describe("generateParityYml", () => {
|
|
114
|
+
const yml = generateParityYml("loja-tanstack");
|
|
115
|
+
it("runs parity journey advisory, gated on PARITY_PROD_URL", () => {
|
|
116
|
+
expect(yml).toContain("name: Parity");
|
|
117
|
+
expect(yml).toContain("continue-on-error: true"); // advisory, never blocks
|
|
118
|
+
expect(yml).toContain("vars.PARITY_PROD_URL"); // prod = original storefront
|
|
119
|
+
expect(yml).toContain("bunx @decocms/parity journey");
|
|
120
|
+
expect(yml).toContain("--junit parity-results.xml");
|
|
121
|
+
expect(yml).toContain("--github");
|
|
122
|
+
expect(yml).toContain("Workers Builds: loja-tanstack"); // worker name interpolated
|
|
123
|
+
expect(yml).toContain("secrets.ANTHROPIC_API_KEY"); // optional LLM key
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffolds `.github/workflows/parity.yml` — validates the migrated storefront
|
|
3
|
+
* against the ORIGINAL live site on every PR, using `@decocms/parity`.
|
|
4
|
+
*
|
|
5
|
+
* Why day-one: a fresh migration is exactly when parity earns its keep — it
|
|
6
|
+
* catches the sections/SEO/journey steps that silently didn't survive the
|
|
7
|
+
* VTEX IO → blocks port, comparing the PR preview against the source of truth
|
|
8
|
+
* (the still-live original storefront).
|
|
9
|
+
*
|
|
10
|
+
* Shape (mirrors perf.yml):
|
|
11
|
+
* - Advisory (`continue-on-error: true`) so day-one CI is green; comments,
|
|
12
|
+
* never blocks. Flip to a gate by removing continue-on-error once stable.
|
|
13
|
+
* - cand = the CF Workers Builds preview URL, discovered by polling the
|
|
14
|
+
* check-run output (same block as perf.yml — no CF token, no local build).
|
|
15
|
+
* - prod = repo variable `PARITY_PROD_URL` (the original storefront). The one
|
|
16
|
+
* calibration knob the operator sets once; the job SKIPS cleanly if unset,
|
|
17
|
+
* so the workflow is inert until wired.
|
|
18
|
+
* - Runs `parity journey` (the tightest loop: purchase journey only, JUnit +
|
|
19
|
+
* GitHub annotations, no LLM aggregation). ANTHROPIC_API_KEY is an OPTIONAL
|
|
20
|
+
* secret — parity runs without it (issues severity-sorted, not LLM-ranked).
|
|
21
|
+
*
|
|
22
|
+
* Docs: https://docs.decocms.com/v2/en/parity/ci
|
|
23
|
+
*
|
|
24
|
+
* @param workerName Cloudflare Worker name (= wrangler `name`, e.g. "loja-tanstack").
|
|
25
|
+
* Used to match the "Workers Builds: <name>" check run.
|
|
26
|
+
*/
|
|
27
|
+
export function generateParityYml(workerName: string): string {
|
|
28
|
+
return `name: Parity
|
|
29
|
+
|
|
30
|
+
# Valida a loja migrada contra a loja ORIGINAL (source of truth) a cada PR, com
|
|
31
|
+
# @decocms/parity. Advisory (comenta, não trava) — vire gate removendo o
|
|
32
|
+
# \`continue-on-error\` do job quando estabilizar, igual perf/react-doctor.
|
|
33
|
+
#
|
|
34
|
+
# Requer a variável de repo \`PARITY_PROD_URL\` = URL da loja original (ex.:
|
|
35
|
+
# https://www.minhaloja.com.br). Sem ela, o job pula limpo. \`ANTHROPIC_API_KEY\`
|
|
36
|
+
# é secret OPCIONAL (ranqueia issues com IA; parity roda sem ele).
|
|
37
|
+
#
|
|
38
|
+
# cand = preview URL do CF Workers Builds (mesmo polling do perf.yml).
|
|
39
|
+
|
|
40
|
+
on:
|
|
41
|
+
pull_request:
|
|
42
|
+
|
|
43
|
+
permissions:
|
|
44
|
+
contents: read
|
|
45
|
+
pull-requests: write
|
|
46
|
+
checks: read
|
|
47
|
+
|
|
48
|
+
concurrency:
|
|
49
|
+
group: parity-\${{ github.workflow }}-\${{ github.ref }}
|
|
50
|
+
cancel-in-progress: true
|
|
51
|
+
|
|
52
|
+
jobs:
|
|
53
|
+
parity:
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
continue-on-error: true
|
|
56
|
+
steps:
|
|
57
|
+
# Pula tudo se a URL da loja original não foi configurada.
|
|
58
|
+
- id: guard
|
|
59
|
+
name: Checar PARITY_PROD_URL
|
|
60
|
+
env:
|
|
61
|
+
PROD_URL: \${{ vars.PARITY_PROD_URL }}
|
|
62
|
+
run: |
|
|
63
|
+
if [ -z "$PROD_URL" ]; then
|
|
64
|
+
echo "PARITY_PROD_URL não configurada — pulando parity." >> "$GITHUB_STEP_SUMMARY"
|
|
65
|
+
echo "Configure em Settings → Variables → Actions para ativar." >> "$GITHUB_STEP_SUMMARY"
|
|
66
|
+
echo "run=false" >> "$GITHUB_OUTPUT"
|
|
67
|
+
else
|
|
68
|
+
echo "run=true" >> "$GITHUB_OUTPUT"
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Aguarda o CF Workers Builds completar e extrai a preview URL do output.
|
|
72
|
+
# Timeout: 10 min (builds normalmente terminam em ~3 min).
|
|
73
|
+
- id: cf
|
|
74
|
+
name: Aguardar CF build e extrair preview URL
|
|
75
|
+
if: steps.guard.outputs.run == 'true'
|
|
76
|
+
env:
|
|
77
|
+
GH_TOKEN: \${{ github.token }}
|
|
78
|
+
SHA: \${{ github.event.pull_request.head.sha }}
|
|
79
|
+
run: |
|
|
80
|
+
echo "Aguardando CF Workers Builds para $SHA..."
|
|
81
|
+
for i in $(seq 1 60); do
|
|
82
|
+
summary=$(gh api \\
|
|
83
|
+
"repos/$GITHUB_REPOSITORY/commits/$SHA/check-runs" \\
|
|
84
|
+
--jq '.check_runs[] | select(.name == "Workers Builds: ${workerName}") | .output.summary' \\
|
|
85
|
+
2>/dev/null || true)
|
|
86
|
+
|
|
87
|
+
if [ -n "$summary" ]; then
|
|
88
|
+
conclusion=$(gh api \\
|
|
89
|
+
"repos/$GITHUB_REPOSITORY/commits/$SHA/check-runs" \\
|
|
90
|
+
--jq '.check_runs[] | select(.name == "Workers Builds: ${workerName}") | .conclusion' \\
|
|
91
|
+
2>/dev/null || true)
|
|
92
|
+
|
|
93
|
+
if [ "$conclusion" = "success" ]; then
|
|
94
|
+
url=$(printf '%s' "$summary" | grep -oP '(?<=Preview URL: )https://\\S+' | head -1)
|
|
95
|
+
if [ -n "$url" ]; then
|
|
96
|
+
echo "url=$url" >> "$GITHUB_OUTPUT"
|
|
97
|
+
echo "run=true" >> "$GITHUB_OUTPUT"
|
|
98
|
+
echo "CF build OK — preview: $url"
|
|
99
|
+
exit 0
|
|
100
|
+
fi
|
|
101
|
+
elif [ -n "$conclusion" ] && [ "$conclusion" != "null" ]; then
|
|
102
|
+
echo "CF build concluiu com: $conclusion — pulando parity"
|
|
103
|
+
echo "run=false" >> "$GITHUB_OUTPUT"
|
|
104
|
+
exit 0
|
|
105
|
+
fi
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
echo "Tentativa $i/60 — aguardando 10s..."
|
|
109
|
+
sleep 10
|
|
110
|
+
done
|
|
111
|
+
|
|
112
|
+
echo "Timeout aguardando CF build — pulando parity"
|
|
113
|
+
echo "run=false" >> "$GITHUB_OUTPUT"
|
|
114
|
+
|
|
115
|
+
- uses: oven-sh/setup-bun@v2
|
|
116
|
+
if: steps.cf.outputs.run == 'true'
|
|
117
|
+
with:
|
|
118
|
+
bun-version: "1.3.5"
|
|
119
|
+
|
|
120
|
+
# parity journey dirige um browser real — precisa do binário do chromium.
|
|
121
|
+
- name: Instalar chromium
|
|
122
|
+
if: steps.cf.outputs.run == 'true'
|
|
123
|
+
run: bunx playwright install --with-deps chromium
|
|
124
|
+
|
|
125
|
+
- name: Rodar parity journey (prod × preview)
|
|
126
|
+
if: steps.cf.outputs.run == 'true'
|
|
127
|
+
env:
|
|
128
|
+
ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
|
|
129
|
+
run: |
|
|
130
|
+
bunx @decocms/parity journey \\
|
|
131
|
+
--prod "\${{ vars.PARITY_PROD_URL }}" \\
|
|
132
|
+
--cand "\${{ steps.cf.outputs.url }}" \\
|
|
133
|
+
--junit parity-results.xml \\
|
|
134
|
+
--github
|
|
135
|
+
`;
|
|
136
|
+
}
|