@decocms/blocks-cli 7.40.0 → 7.41.0

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.40.0",
3
+ "version": "7.41.0",
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.40.0",
34
+ "@decocms/blocks": "7.41.0",
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
- import type { MigrationContext } from "./types";
4
- import { log, logPhase } from "./types";
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 { generateLockfileCheckYml } from "./templates/lockfile-check-yml";
11
- import { generateTsconfig } from "./templates/tsconfig";
12
- import { generateViteConfig } from "./templates/vite-config";
13
- import { generateKnipConfig } from "./templates/knip-config";
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 { generateSetup } from "./templates/setup";
27
+ import { generateSdkFiles } from "./templates/sdk-gen";
28
+ import { generateSectionLoaders } from "./templates/section-loaders";
16
29
  import { generateServerEntry } from "./templates/server-entry";
17
- import { generateAppCss } from "./templates/app-css";
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 { generateHooks } from "./templates/hooks";
21
- import { generateCommerceInit } from "./templates/commerce-init";
22
- import { generateCommerceLoaders } from "./templates/commerce-loaders";
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(ctx, ".prettierrc", JSON.stringify({
82
- semi: true,
83
- singleQuote: false,
84
- trailingComma: "all" as const,
85
- printWidth: 100,
86
- tabWidth: 2,
87
- }, null, 2) + "\n");
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 JSON.stringify(
259
- {
260
- name: ctx.siteName,
261
- main: "src/worker-entry.ts",
262
- compatibility_date: "2025-05-01",
263
- compatibility_flags: ["nodejs_compat", "no_handle_cross_request_promise_resolution"],
264
- cache: { enabled: true },
265
- kv_namespaces: [{ binding: "SITES_KV", id: "dev-sites-kv" }],
266
- },
267
- null,
268
- 2,
269
- ) + "\n";
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 { /* skip */ }
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: 'No class= in JSX (should be className=)',
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(` Still has class= in JSX: ${bad.slice(0, 5).join(", ")}${bad.length > 5 ? ` (+${bad.length - 5} more)` : ""}`);
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
- path.join(publicDir, "sprites.svg"),
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(` These may be invisible due to stacking contexts. Replace with z-0 or positive z-index.`);
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(` Files use invoke.* but src/runtime.ts is missing: ${hasInvoke.join(", ")}`);
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(srcDir, /\bhx-(?:get|post|put|delete|patch|trigger|target|swap|on|indicator|sync|select)\b/);
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(` Still references frozen @decocms/start or @decocms/apps/* specifiers: ${unique.join(", ")}`);
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((f: string) => f.endsWith(".tsx") || f.endsWith(".ts"));
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(` src/islands/ still has ${tsxFiles.length} files — should be moved to components/`);
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") continue;
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") continue;
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 ? "\x1b[32m✓\x1b[0m" : check.severity === "error" ? "\x1b[31m✗\x1b[0m" : "\x1b[33m⚠\x1b[0m";
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(/Generate artifacts[\s\S]*?run: bun run generate && bun run generate:routes/);
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
+ }