@decocms/blocks-cli 8.0.0 → 8.1.0-next.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 +5 -3
- package/scripts/analyze-traces.mjs +1 -0
- package/scripts/cdn-rules.test.ts +105 -0
- package/scripts/cdn-rules.ts +192 -0
- package/scripts/deco-migrate-cli.ts +6 -7
- package/scripts/fast-deploy-kv.test.ts +172 -0
- package/scripts/generate-blocks.test.ts +11 -0
- package/scripts/generate-blocks.ts +0 -10
- package/scripts/generate-invoke.test.ts +2 -2
- package/scripts/generate-invoke.ts +9 -21
- package/scripts/generate-loaders.test.ts +2 -2
- package/scripts/generate-loaders.ts +0 -10
- package/scripts/generate-schema-loader-aliases.test.ts +186 -0
- package/scripts/generate-schema-workspaces.test.ts +180 -0
- package/scripts/generate-schema.test.ts +68 -29
- package/scripts/generate-schema.ts +101 -89
- package/scripts/generate-sections.test.ts +104 -35
- package/scripts/generate-sections.ts +57 -11
- package/scripts/generate-workspaces.test.ts +179 -0
- package/scripts/generate.test.ts +1 -2
- package/scripts/generate.ts +43 -45
- package/scripts/lib/blocks-dedupe.test.ts +1 -1
- package/scripts/lib/cf-kv-rest.ts +49 -1
- package/scripts/lib/installedPackages.ts +42 -0
- package/scripts/lib/invokeSource.ts +18 -0
- package/scripts/lib/kv-snapshot.ts +44 -0
- package/scripts/lib/read-decofile.ts +13 -2
- package/scripts/lib/upgrade-6-to-7.ts +1 -1
- package/scripts/lib/wrangler-config.test.ts +49 -0
- package/scripts/lib/wrangler-config.ts +31 -0
- package/scripts/migrate/analyzers/htmx-analyze.test.ts +6 -6
- package/scripts/migrate/analyzers/tailwind-config.ts +95 -1
- package/scripts/migrate/config.ts +2 -2
- package/scripts/migrate/delete-sets.ts +75 -0
- package/scripts/migrate/fast-deploy-scaffold.test.ts +130 -0
- package/scripts/migrate/icon-casing.test.ts +63 -0
- package/scripts/migrate/phase-analyze.test.ts +66 -4
- package/scripts/migrate/phase-analyze.ts +79 -60
- package/scripts/migrate/phase-cleanup.test.ts +3 -1
- package/scripts/migrate/phase-cleanup.ts +1 -1
- package/scripts/migrate/phase-compile.test.ts +3 -1
- package/scripts/migrate/phase-report.test.ts +2 -0
- package/scripts/migrate/phase-report.ts +3 -4
- package/scripts/migrate/phase-scaffold.ts +150 -45
- package/scripts/migrate/phase-transform.ts +107 -2
- package/scripts/migrate/phase-verify-legacy-specifiers.test.ts +13 -0
- package/scripts/migrate/phase-verify.test.ts +3 -1
- package/scripts/migrate/phase-verify.ts +87 -33
- package/scripts/migrate/post-cleanup/rules.ts +29 -11
- package/scripts/migrate/post-cleanup/shim-classify.test.ts +1 -1
- package/scripts/migrate/post-cleanup/shim-classify.ts +2 -2
- package/scripts/migrate/source-layout.test.ts +43 -0
- package/scripts/migrate/source-layout.ts +32 -5
- package/scripts/migrate/templates/app-css.test.ts +24 -0
- package/scripts/migrate/templates/app-css.ts +27 -0
- package/scripts/migrate/templates/ci-workflows.test.ts +179 -0
- package/scripts/migrate/templates/ci-yml.ts +182 -0
- package/scripts/migrate/templates/cursor-rules.test.ts +3 -3
- package/scripts/migrate/templates/hooks.test.ts +3 -1
- package/scripts/migrate/templates/lib-utils.ts +3 -3
- package/scripts/migrate/templates/main-push-guard-yml.ts +45 -0
- package/scripts/migrate/templates/package-json.ts +10 -0
- package/scripts/migrate/templates/parity-yml.ts +136 -0
- package/scripts/migrate/templates/perf-yml.ts +449 -0
- package/scripts/migrate/templates/playwright-yml.ts +127 -0
- package/scripts/migrate/templates/react-doctor-yml.ts +46 -0
- package/scripts/migrate/templates/routes.test.ts +31 -0
- package/scripts/migrate/templates/routes.ts +19 -9
- package/scripts/migrate/templates/section-loaders.ts +3 -3
- package/scripts/migrate/templates/server-entry-vtex-invoke.test.ts +19 -0
- package/scripts/migrate/templates/server-entry.ts +19 -14
- package/scripts/migrate/templates/setup.ts +11 -3
- package/scripts/migrate/templates/sync-blocks-bot-yml.ts +201 -0
- package/scripts/migrate/templates/ui-components.ts +3 -0
- package/scripts/migrate/transforms/ctx-compat.test.ts +37 -0
- package/scripts/migrate/transforms/ctx-compat.ts +93 -1
- package/scripts/migrate/transforms/htmx-on-events.test.ts +1 -1
- package/scripts/migrate/transforms/htmx-on-events.ts +2 -2
- package/scripts/migrate/transforms/jsx-class-type.test.ts +28 -0
- package/scripts/migrate/transforms/jsx.ts +10 -0
- package/scripts/migrate/transforms/picture.test.ts +56 -0
- package/scripts/migrate/transforms/picture.ts +95 -0
- package/scripts/migrate/transforms/tailwind-renames.test.ts +29 -0
- package/scripts/migrate/transforms/tailwind-renames.ts +34 -0
- package/scripts/migrate/transforms/timer-types.test.ts +30 -0
- package/scripts/migrate/transforms/timer-types.ts +48 -0
- package/scripts/migrate/transforms/use-script-handlers.test.ts +37 -0
- package/scripts/migrate/transforms/use-script-handlers.ts +36 -0
- package/scripts/migrate/types.ts +13 -1
- package/scripts/migrate-blocks-to-kv.ts +16 -2
- package/scripts/migrate-to-cf-observability.test.ts +5 -5
- package/scripts/migrate-to-cf-observability.ts +1 -1
- package/scripts/migrate.ts +244 -6
- package/scripts/reconcile.test.ts +162 -0
- package/scripts/reconcile.ts +303 -0
- package/scripts/sync-blocks-bot.test.ts +276 -0
- package/scripts/sync-blocks-bot.ts +567 -0
- package/scripts/sync-blocks-to-kv.ts +20 -2
- package/scripts/upgrade-6-to-7.ts +0 -0
- package/scripts/lib/legacyArtifact.ts +0 -20
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffolds the advisory per-PR performance workflow and its companion tools:
|
|
3
|
+
* .github/workflows/perf.yml — GitHub Actions workflow
|
|
4
|
+
* tools/perf/changed-paths.sh — maps edited sections → CMS page paths
|
|
5
|
+
* tools/perf/compare.mjs — compares two LHCI result dirs, emits markdown
|
|
6
|
+
* lighthouserc.json — LHCI collect settings (3 runs, mobile UA, 4 categories)
|
|
7
|
+
*
|
|
8
|
+
* Design rationale:
|
|
9
|
+
* - CF Workers Builds generates a per-PR preview URL (workers.dev). Both the
|
|
10
|
+
* PR and main previews are workers.dev — no edge cache — so the delta is
|
|
11
|
+
* purely code, not cache state.
|
|
12
|
+
* - Gate signal = CLS + TBT + a11y/best-practices/SEO scores. CLS/TBT are
|
|
13
|
+
* layout stability / JS work; the three category scores are static-analysis
|
|
14
|
+
* audits. All cache-insensitive, so a regression is real code, not cold SSR.
|
|
15
|
+
* LCP and performance score are informational only (noisy on cold SSR).
|
|
16
|
+
* - Advisory: `continue-on-error: true`. Remove it once the site stabilises.
|
|
17
|
+
* - LHCI 0.15 does not write manifest.json; compare.mjs reads lhr-*.json
|
|
18
|
+
* directly and selects the median run by performance score.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param workerName Cloudflare Worker name (= wrangler `name` field, e.g. "my-site-tanstack").
|
|
23
|
+
* Used in the CF check-run name and the main preview base URL.
|
|
24
|
+
*/
|
|
25
|
+
export function generatePerfFiles(workerName: string): Record<string, string> {
|
|
26
|
+
return {
|
|
27
|
+
".github/workflows/perf.yml": generatePerfYml(workerName),
|
|
28
|
+
"tools/perf/changed-paths.sh": CHANGED_PATHS_SH,
|
|
29
|
+
"tools/perf/compare.mjs": COMPARE_MJS,
|
|
30
|
+
"lighthouserc.json": LIGHTHOUSERC_JSON,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function generatePerfYml(workerName: string): string {
|
|
35
|
+
return `name: Perf
|
|
36
|
+
|
|
37
|
+
# Teste de performance por PR (advisory — comenta, não trava; vire gate removendo
|
|
38
|
+
# o \`continue-on-error\` do job quando estabilizar, igual react-doctor/typecheck).
|
|
39
|
+
#
|
|
40
|
+
# Trigger: pull_request (único trigger que roda do branch do PR, não do main).
|
|
41
|
+
# Aguarda o check do CF Workers Builds completar via polling, então extrai a
|
|
42
|
+
# preview URL do output — sem token CF nem build local.
|
|
43
|
+
#
|
|
44
|
+
# Baseline: URL fixa da preview do main (https://${workerName}.deco-cx.workers.dev).
|
|
45
|
+
# Gate: CLS + TBT + A11y/BP/SEO (estrutura/JS + auditorias estáticas, insensíveis a cache). LCP/score: informativos.
|
|
46
|
+
|
|
47
|
+
on:
|
|
48
|
+
pull_request:
|
|
49
|
+
|
|
50
|
+
permissions:
|
|
51
|
+
contents: read
|
|
52
|
+
pull-requests: write
|
|
53
|
+
checks: read
|
|
54
|
+
|
|
55
|
+
concurrency:
|
|
56
|
+
group: perf-\${{ github.workflow }}-\${{ github.ref }}
|
|
57
|
+
cancel-in-progress: true
|
|
58
|
+
|
|
59
|
+
env:
|
|
60
|
+
MOBILE_UA: "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"
|
|
61
|
+
BASE_URL: "https://${workerName}.deco-cx.workers.dev"
|
|
62
|
+
PERF_FALLBACK_PATHS: |
|
|
63
|
+
/
|
|
64
|
+
|
|
65
|
+
jobs:
|
|
66
|
+
perf:
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
continue-on-error: true
|
|
69
|
+
steps:
|
|
70
|
+
# Aguarda o CF Workers Builds completar e extrai a preview URL do output.
|
|
71
|
+
# Timeout: 10 min (builds normalmente terminam em ~3 min).
|
|
72
|
+
- id: cf
|
|
73
|
+
name: Aguardar CF build e extrair preview URL
|
|
74
|
+
env:
|
|
75
|
+
GH_TOKEN: \${{ github.token }}
|
|
76
|
+
SHA: \${{ github.event.pull_request.head.sha }}
|
|
77
|
+
run: |
|
|
78
|
+
echo "Aguardando CF Workers Builds para \$SHA..."
|
|
79
|
+
for i in \$(seq 1 60); do
|
|
80
|
+
summary=\$(gh api \\
|
|
81
|
+
"repos/\$GITHUB_REPOSITORY/commits/\$SHA/check-runs" \\
|
|
82
|
+
--jq '.check_runs[] | select(.name == "Workers Builds: ${workerName}") | .output.summary' \\
|
|
83
|
+
2>/dev/null || true)
|
|
84
|
+
|
|
85
|
+
if [ -n "\$summary" ]; then
|
|
86
|
+
conclusion=\$(gh api \\
|
|
87
|
+
"repos/\$GITHUB_REPOSITORY/commits/\$SHA/check-runs" \\
|
|
88
|
+
--jq '.check_runs[] | select(.name == "Workers Builds: ${workerName}") | .conclusion' \\
|
|
89
|
+
2>/dev/null || true)
|
|
90
|
+
|
|
91
|
+
if [ "\$conclusion" = "success" ]; then
|
|
92
|
+
url=\$(printf '%s' "\$summary" | grep -oP '(?<=Preview URL: )https://\\S+' | head -1)
|
|
93
|
+
if [ -n "\$url" ]; then
|
|
94
|
+
echo "url=\$url" >> "\$GITHUB_OUTPUT"
|
|
95
|
+
echo "run=true" >> "\$GITHUB_OUTPUT"
|
|
96
|
+
echo "CF build OK — preview: \$url"
|
|
97
|
+
exit 0
|
|
98
|
+
fi
|
|
99
|
+
elif [ -n "\$conclusion" ] && [ "\$conclusion" != "null" ]; then
|
|
100
|
+
echo "CF build concluiu com: \$conclusion — pulando perf"
|
|
101
|
+
echo "run=false" >> "\$GITHUB_OUTPUT"
|
|
102
|
+
exit 0
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
echo "Tentativa \$i/60 — aguardando 10s..."
|
|
107
|
+
sleep 10
|
|
108
|
+
done
|
|
109
|
+
|
|
110
|
+
echo "Timeout aguardando CF build — pulando perf"
|
|
111
|
+
echo "run=false" >> "\$GITHUB_OUTPUT"
|
|
112
|
+
|
|
113
|
+
- uses: actions/checkout@v4
|
|
114
|
+
if: steps.cf.outputs.run == 'true'
|
|
115
|
+
with:
|
|
116
|
+
fetch-depth: 0
|
|
117
|
+
|
|
118
|
+
- uses: actions/setup-node@v4
|
|
119
|
+
if: steps.cf.outputs.run == 'true'
|
|
120
|
+
with:
|
|
121
|
+
node-version: "22.15.0"
|
|
122
|
+
|
|
123
|
+
- id: paths
|
|
124
|
+
name: Mapear seções editadas → paths
|
|
125
|
+
if: steps.cf.outputs.run == 'true'
|
|
126
|
+
env:
|
|
127
|
+
GH_TOKEN: \${{ github.token }}
|
|
128
|
+
run: |
|
|
129
|
+
# GitHub PR Files API = source of truth (evita problemas de squash merge)
|
|
130
|
+
changed=\$(gh api \\
|
|
131
|
+
"repos/\$GITHUB_REPOSITORY/pulls/\${{ github.event.pull_request.number }}/files" \\
|
|
132
|
+
--paginate --jq '.[].filename' \\
|
|
133
|
+
| grep -E '^src/(sections|previews)/.*\\.tsx\$' || true)
|
|
134
|
+
echo "--- seções alteradas neste PR ---"
|
|
135
|
+
printf '%s\\n' "\$changed"
|
|
136
|
+
chmod +x tools/perf/changed-paths.sh
|
|
137
|
+
CHANGED_FILES="\$changed" tools/perf/changed-paths.sh > perf-paths.txt || true
|
|
138
|
+
echo "--- paths a medir ---"; cat perf-paths.txt
|
|
139
|
+
if [ -s perf-paths.txt ]; then echo "run=true" >> "\$GITHUB_OUTPUT"
|
|
140
|
+
else echo "run=false" >> "\$GITHUB_OUTPUT"; fi
|
|
141
|
+
|
|
142
|
+
- name: Warm previews
|
|
143
|
+
if: steps.cf.outputs.run == 'true' && steps.paths.outputs.run == 'true'
|
|
144
|
+
env:
|
|
145
|
+
PR_URL: \${{ steps.cf.outputs.url }}
|
|
146
|
+
run: |
|
|
147
|
+
# --max-time 30: garante que curl não trava num SSR lento
|
|
148
|
+
while IFS= read -r p; do
|
|
149
|
+
[ -n "\$p" ] || continue
|
|
150
|
+
for origin in "\$PR_URL" "\$BASE_URL"; do
|
|
151
|
+
for _ in 1 2; do
|
|
152
|
+
curl -s -o /dev/null --max-time 30 -A "\$MOBILE_UA" \\
|
|
153
|
+
-w "warm %{http_code} \${origin}\${p} ttfb=%{time_starttransfer}s\\n" \\
|
|
154
|
+
"\${origin}\${p}" || true
|
|
155
|
+
done
|
|
156
|
+
done
|
|
157
|
+
done < perf-paths.txt
|
|
158
|
+
|
|
159
|
+
- name: Lighthouse (PR + base)
|
|
160
|
+
if: steps.cf.outputs.run == 'true' && steps.paths.outputs.run == 'true'
|
|
161
|
+
env:
|
|
162
|
+
PR_URL: \${{ steps.cf.outputs.url }}
|
|
163
|
+
run: |
|
|
164
|
+
collect() {
|
|
165
|
+
local origin="\$1" out="\$2" args=()
|
|
166
|
+
while IFS= read -r p; do [ -n "\$p" ] && args+=(--url="\${origin}\${p}"); done < perf-paths.txt
|
|
167
|
+
rm -rf .lighthouseci "\$out"
|
|
168
|
+
npx @lhci/cli collect --config=lighthouserc.json "\${args[@]}"
|
|
169
|
+
mv .lighthouseci "\$out"
|
|
170
|
+
echo "--- \$out contents ---"; ls -la "\$out" || true
|
|
171
|
+
}
|
|
172
|
+
collect "\$PR_URL" .lhci-pr
|
|
173
|
+
collect "\$BASE_URL" .lhci-base
|
|
174
|
+
|
|
175
|
+
- name: Montar comentário
|
|
176
|
+
if: steps.cf.outputs.run == 'true' && steps.paths.outputs.run == 'true'
|
|
177
|
+
run: |
|
|
178
|
+
node tools/perf/compare.mjs .lhci-base .lhci-pr | tee perf-comment.md >> "\$GITHUB_STEP_SUMMARY"
|
|
179
|
+
|
|
180
|
+
- name: Comentar no PR
|
|
181
|
+
if: steps.cf.outputs.run == 'true' && steps.paths.outputs.run == 'true'
|
|
182
|
+
uses: marocchino/sticky-pull-request-comment@v2
|
|
183
|
+
with:
|
|
184
|
+
header: perf
|
|
185
|
+
path: perf-comment.md
|
|
186
|
+
`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const CHANGED_PATHS_SH = `#!/usr/bin/env bash
|
|
190
|
+
set -euo pipefail
|
|
191
|
+
|
|
192
|
+
# Map the sections changed in this PR to the CMS page path(s) that render them,
|
|
193
|
+
# so the perf workflow only measures pages the PR can actually affect.
|
|
194
|
+
#
|
|
195
|
+
# How: each edited src/sections/**/*.tsx (or its previews/ twin) becomes a
|
|
196
|
+
# resolveType ("site/sections/<subpath>"); we grep the checked-in decofile
|
|
197
|
+
# (.deco/blocks/*.json) for that string and emit the \`path\` of every PAGE block
|
|
198
|
+
# that references it. Reusable/instance blocks (path == null) and param/wildcard
|
|
199
|
+
# paths (/*, /:slug/p, /conta/*) are skipped — they can't be measured as-is.
|
|
200
|
+
#
|
|
201
|
+
# Outcomes:
|
|
202
|
+
# - No section/preview files changed → print PERF_FALLBACK_PATHS (home sanity).
|
|
203
|
+
# - Sections changed, page(s) mapped → print the mapped path(s).
|
|
204
|
+
# - Sections changed, none mapped → print PERF_FALLBACK_PATHS + log why.
|
|
205
|
+
#
|
|
206
|
+
# Output: one URL path per line on stdout. Diagnostics go to stderr.
|
|
207
|
+
#
|
|
208
|
+
# Env:
|
|
209
|
+
# CHANGED_FILES newline-separated list of changed section/preview files
|
|
210
|
+
# (from GitHub PR Files API — preferred, avoids squash-merge issues)
|
|
211
|
+
# BASE_REF git ref/sha to diff against (fallback if CHANGED_FILES unset)
|
|
212
|
+
# PERF_FALLBACK_PATHS newline-separated fallback paths (default: "/")
|
|
213
|
+
# PERF_MAX_PATHS max paths to emit (default: 3). Sections used on many
|
|
214
|
+
# pages (Header/Footer) would otherwise produce 100+ paths;
|
|
215
|
+
# any page is representative for a shared section.
|
|
216
|
+
|
|
217
|
+
ROOT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
218
|
+
cd "\$ROOT_DIR"
|
|
219
|
+
|
|
220
|
+
BASE_REF="\${BASE_REF:-origin/main}"
|
|
221
|
+
BLOCKS_DIR=".deco/blocks"
|
|
222
|
+
|
|
223
|
+
log() { printf '%s\\n' "\$*" >&2; }
|
|
224
|
+
|
|
225
|
+
emit_fallback() {
|
|
226
|
+
log "perf: sections changed but no page mapped — using PERF_FALLBACK_PATHS"
|
|
227
|
+
printf '%s\\n' "\${PERF_FALLBACK_PATHS:-/}" | sed 's/[[:space:]]*\$//' | sed '/^\$/d'
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# Prefer CHANGED_FILES (set by the workflow from the GitHub PR Files API) to
|
|
231
|
+
# avoid squash-merge ancestry issues where git log A..B shows all historical commits.
|
|
232
|
+
# Fall back to git log only when running locally without CHANGED_FILES.
|
|
233
|
+
changed=()
|
|
234
|
+
if [ -n "\${CHANGED_FILES:-}" ]; then
|
|
235
|
+
while IFS= read -r line; do
|
|
236
|
+
[ -n "\$line" ] && changed+=("\$line")
|
|
237
|
+
done < <(printf '%s\\n' "\$CHANGED_FILES" | grep -E '\\.tsx\$' | sort -u || true)
|
|
238
|
+
else
|
|
239
|
+
while IFS= read -r line; do
|
|
240
|
+
[ -n "\$line" ] && changed+=("\$line")
|
|
241
|
+
done < <(git log --name-only --format="" "\$BASE_REF"..HEAD -- src/sections src/previews 2>/dev/null | grep -E '\\.tsx\$' | sort -u || true)
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
# No sections changed → measure home as a baseline sanity check.
|
|
245
|
+
if [ "\${#changed[@]}" -eq 0 ]; then
|
|
246
|
+
log "perf: nenhuma seção alterada — usando PERF_FALLBACK_PATHS como sanity"
|
|
247
|
+
emit_fallback
|
|
248
|
+
exit 0
|
|
249
|
+
fi
|
|
250
|
+
|
|
251
|
+
# Each changed file → the resolveType the decofile references it by.
|
|
252
|
+
resolvetypes=()
|
|
253
|
+
for f in "\${changed[@]}"; do
|
|
254
|
+
case "\$f" in
|
|
255
|
+
src/sections/*) resolvetypes+=("site/\${f#src/}") ;;
|
|
256
|
+
src/previews/*) resolvetypes+=("site/sections/\${f#src/previews/}") ;;
|
|
257
|
+
esac
|
|
258
|
+
done
|
|
259
|
+
[ "\${#resolvetypes[@]}" -eq 0 ] && exit 0
|
|
260
|
+
|
|
261
|
+
# Grep the decofile for each resolveType; collect page paths (non-null).
|
|
262
|
+
paths=()
|
|
263
|
+
for rt in "\${resolvetypes[@]}"; do
|
|
264
|
+
while IFS= read -r block; do
|
|
265
|
+
[ -n "\$block" ] || continue
|
|
266
|
+
while IFS= read -r p; do
|
|
267
|
+
[ -n "\$p" ] && paths+=("\$p")
|
|
268
|
+
done < <(jq -r 'select(.path != null and (.path | type == "string")) | .path' "\$block" 2>/dev/null || true)
|
|
269
|
+
done < <(grep -Fl "\$rt" "\$BLOCKS_DIR"/*.json 2>/dev/null || true)
|
|
270
|
+
done
|
|
271
|
+
|
|
272
|
+
# Dedupe + drop param/wildcard paths (need a concrete slug; can't measure as-is).
|
|
273
|
+
clean=()
|
|
274
|
+
if [ "\${#paths[@]}" -gt 0 ]; then
|
|
275
|
+
while IFS= read -r line; do
|
|
276
|
+
[ -n "\$line" ] && clean+=("\$line")
|
|
277
|
+
done < <(printf '%s\\n' "\${paths[@]}" | grep -vE '[:*]' | sort -u)
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
if [ "\${#clean[@]}" -eq 0 ]; then
|
|
281
|
+
emit_fallback
|
|
282
|
+
exit 0
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
MAX="\${PERF_MAX_PATHS:-3}"
|
|
286
|
+
total="\${#clean[@]}"
|
|
287
|
+
if [ "\$total" -gt "\$MAX" ]; then
|
|
288
|
+
log "perf: \${total} pages mapped — capped at \${MAX} (ponytail: PERF_MAX_PATHS). Dropped: \${clean[*]:\$MAX}"
|
|
289
|
+
clean=("\${clean[@]:0:\$MAX}")
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
log "perf: measuring \${#clean[@]} page(s) mapped from \${#changed[@]} changed file(s)"
|
|
293
|
+
printf '%s\\n' "\${clean[@]}"
|
|
294
|
+
`;
|
|
295
|
+
|
|
296
|
+
const COMPARE_MJS = `#!/usr/bin/env node
|
|
297
|
+
// Compare two Lighthouse CI result dirs (base = main preview, pr = PR preview)
|
|
298
|
+
// and emit a markdown table. Both previews run on *.workers.dev with no edge
|
|
299
|
+
// cache, same runner/region, same mobile UA — so the delta is the PR's code
|
|
300
|
+
// effect, not cache warm/cold.
|
|
301
|
+
//
|
|
302
|
+
// Gate signal = CLS + TBT + a11y/best-practices/SEO scores. CLS/TBT are page
|
|
303
|
+
// structure / JS work; the three category scores are static-analysis audits.
|
|
304
|
+
// All are cache-insensitive, so a regression is real code, not cold SSR.
|
|
305
|
+
// LCP / performance score are shown but informative only (noisy on cold workers.dev SSR).
|
|
306
|
+
//
|
|
307
|
+
// LHCI 0.15 does not write manifest.json; we read lhr-*.json directly.
|
|
308
|
+
//
|
|
309
|
+
// Usage: node compare.mjs <baseDir> <prDir> → writes markdown to stdout
|
|
310
|
+
// node compare.mjs --selftest → runs assertions, exits 0/1
|
|
311
|
+
|
|
312
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
313
|
+
import { join } from "node:path";
|
|
314
|
+
|
|
315
|
+
const TBT_ABS = 50; // ms — deltas below this are noise
|
|
316
|
+
const TBT_REL = 0.15; // 15% relative change
|
|
317
|
+
const CLS_ABS = 0.02; // absolute CLS delta that matters
|
|
318
|
+
const CLS_FLOOR = 0.1; // only flag CLS regression when PR is above "good"
|
|
319
|
+
const SCORE_DROP = 3; // pts — a11y/bp/seo score drop that gates (tolerates 1–2pt audit jitter)
|
|
320
|
+
const SCORES = ["a11y", "bp", "seo"]; // gated category scores (0–100)
|
|
321
|
+
|
|
322
|
+
// base/pr = { tbt, cls, a11y, bp, seo }. Called from selftest with only
|
|
323
|
+
// { tbt, cls }: the SCORES deltas become NaN and NaN comparisons are false,
|
|
324
|
+
// so score gating is simply inert there.
|
|
325
|
+
function verdict(base, pr) {
|
|
326
|
+
const tbtUp = pr.tbt - base.tbt;
|
|
327
|
+
const tbtBad = tbtUp > TBT_ABS && tbtUp > base.tbt * TBT_REL;
|
|
328
|
+
const tbtGood = -tbtUp > TBT_ABS && -tbtUp > base.tbt * TBT_REL;
|
|
329
|
+
const clsUp = pr.cls - base.cls;
|
|
330
|
+
const clsBad = clsUp > CLS_ABS && pr.cls > CLS_FLOOR;
|
|
331
|
+
const clsGood = -clsUp > CLS_ABS && base.cls > CLS_FLOOR;
|
|
332
|
+
const scoreBad = SCORES.some((k) => pr[k] - base[k] <= -SCORE_DROP);
|
|
333
|
+
const scoreGood = !scoreBad && SCORES.some((k) => pr[k] - base[k] >= SCORE_DROP);
|
|
334
|
+
if (tbtBad || clsBad || scoreBad) return { icon: "🔴", gate: true };
|
|
335
|
+
if (tbtGood || clsGood || scoreGood) return { icon: "🟢", gate: false };
|
|
336
|
+
return { icon: "⚪", gate: false };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function loadDir(dir) {
|
|
340
|
+
const files = readdirSync(dir).filter((f) => f.startsWith("lhr-") && f.endsWith(".json"));
|
|
341
|
+
const byUrl = {};
|
|
342
|
+
for (const f of files) {
|
|
343
|
+
const lhr = JSON.parse(readFileSync(join(dir, f), "utf8"));
|
|
344
|
+
const url = lhr.finalUrl || lhr.requestedUrl;
|
|
345
|
+
if (!byUrl[url]) byUrl[url] = [];
|
|
346
|
+
byUrl[url].push(lhr);
|
|
347
|
+
}
|
|
348
|
+
const byPath = {};
|
|
349
|
+
for (const [url, runs] of Object.entries(byUrl)) {
|
|
350
|
+
runs.sort((a, b) => a.categories.performance.score - b.categories.performance.score);
|
|
351
|
+
const lhr = runs[Math.floor(runs.length / 2)];
|
|
352
|
+
const a = lhr.audits;
|
|
353
|
+
byPath[new URL(url).pathname] = {
|
|
354
|
+
score: Math.round((lhr.categories.performance?.score ?? 0) * 100),
|
|
355
|
+
a11y: Math.round((lhr.categories.accessibility?.score ?? 0) * 100),
|
|
356
|
+
bp: Math.round((lhr.categories["best-practices"]?.score ?? 0) * 100),
|
|
357
|
+
seo: Math.round((lhr.categories.seo?.score ?? 0) * 100),
|
|
358
|
+
lcp: a["largest-contentful-paint"].numericValue,
|
|
359
|
+
cls: a["cumulative-layout-shift"].numericValue,
|
|
360
|
+
tbt: a["total-blocking-time"].numericValue,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return byPath;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const ms = (v) => \`\${Math.round(v)}ms\`;
|
|
367
|
+
const cls = (v) => v.toFixed(3);
|
|
368
|
+
const pts = (v) => \`\${Math.round(v)}\`;
|
|
369
|
+
function delta(base, pr, fmt) {
|
|
370
|
+
const d = pr - base;
|
|
371
|
+
const arrow = d < 0 ? "↓" : d > 0 ? "↑" : "";
|
|
372
|
+
return \`\${d > 0 ? "+" : ""}\${fmt(d)} \${arrow}\`.trim();
|
|
373
|
+
}
|
|
374
|
+
// base→PR (Δ) for a 0–100 category score.
|
|
375
|
+
const scoreCell = (b, p) => \`\${b}→\${p} (\${delta(b, p, pts)})\`;
|
|
376
|
+
|
|
377
|
+
function render(base, pr) {
|
|
378
|
+
const paths = [...new Set([...Object.keys(base), ...Object.keys(pr)])].sort();
|
|
379
|
+
let anyGate = false;
|
|
380
|
+
const lines = [
|
|
381
|
+
"### ⚡ Perf — preview do PR × preview do main",
|
|
382
|
+
"",
|
|
383
|
+
"Ambas as previews rodam em \`*.workers.dev\` (sem edge cache), mesmo runner e UA mobile — o delta é efeito do código, não de cache quente/frio.",
|
|
384
|
+
"",
|
|
385
|
+
"**Gate:** CLS + TBT + A11y/BP/SEO (estrutura/JS + auditorias estáticas). LCP e score de performance são informativos (ruído esperado em SSR frio).",
|
|
386
|
+
"",
|
|
387
|
+
"| Página | | CLS (base→PR) | TBT (base→PR) | A11y | BP | SEO | LCP | Score |",
|
|
388
|
+
"|---|:--:|---|---|---|---|---|---|---|",
|
|
389
|
+
];
|
|
390
|
+
for (const p of paths) {
|
|
391
|
+
const b = base[p];
|
|
392
|
+
const r = pr[p];
|
|
393
|
+
if (!b || !r) {
|
|
394
|
+
lines.push(\`| \\\`\${p}\\\` | ⚠️ | \${b ? "faltou PR" : "faltou base"} | | | | | | |\`);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const v = verdict(b, r);
|
|
398
|
+
anyGate = anyGate || v.gate;
|
|
399
|
+
lines.push(
|
|
400
|
+
\`| \\\`\${p}\\\` | \${v.icon} | \${cls(b.cls)}→\${cls(r.cls)} (\${delta(b.cls, r.cls, cls)}) | \${ms(b.tbt)}→\${ms(r.tbt)} (\${delta(b.tbt, r.tbt, ms)}) | \${scoreCell(b.a11y, r.a11y)} | \${scoreCell(b.bp, r.bp)} | \${scoreCell(b.seo, r.seo)} | \${ms(b.lcp)}→\${ms(r.lcp)} | \${b.score}→\${r.score} |\`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
lines.push("");
|
|
404
|
+
lines.push(
|
|
405
|
+
anyGate
|
|
406
|
+
? "🔴 **Regressão detectada em CLS/TBT/A11y/BP/SEO** (advisory — não trava o merge)."
|
|
407
|
+
: "🟢 Sem regressão nas métricas gateadas (CLS/TBT/A11y/BP/SEO).",
|
|
408
|
+
);
|
|
409
|
+
return { md: lines.join("\\n"), anyGate };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function selftest() {
|
|
413
|
+
const ok = (c, m) => { if (!c) throw new Error("FAIL: " + m); };
|
|
414
|
+
ok(verdict({ tbt: 100, cls: 0.01 }, { tbt: 200, cls: 0.01 }).gate, "tbt +100ms should gate");
|
|
415
|
+
ok(!verdict({ tbt: 100, cls: 0.01 }, { tbt: 130, cls: 0.01 }).gate, "tbt +30ms is noise");
|
|
416
|
+
ok(verdict({ tbt: 100, cls: 0.11 }, { tbt: 100, cls: 0.2 }).gate, "cls 0.11→0.20 should gate");
|
|
417
|
+
ok(!verdict({ tbt: 100, cls: 0.02 }, { tbt: 100, cls: 0.08 }).gate, "cls under 0.1 floor no gate");
|
|
418
|
+
ok(verdict({ tbt: 300, cls: 0.01 }, { tbt: 100, cls: 0.01 }).icon === "🟢", "tbt drop is green");
|
|
419
|
+
ok(verdict({ tbt: 100, cls: 0.01 }, { tbt: 105, cls: 0.01 }).icon === "⚪", "tiny change is neutral");
|
|
420
|
+
// Category-score gating. S() supplies a neutral CLS/TBT baseline plus perfect scores.
|
|
421
|
+
const S = (o) => ({ tbt: 100, cls: 0.01, a11y: 100, bp: 100, seo: 100, ...o });
|
|
422
|
+
ok(verdict(S({}), S({ seo: 96 })).gate, "seo 100→96 (-4) should gate");
|
|
423
|
+
ok(verdict(S({ a11y: 90 }), S({ a11y: 87 })).gate, "a11y -3 should gate");
|
|
424
|
+
ok(!verdict(S({}), S({ bp: 98 })).gate, "bp -2 is jitter, no gate");
|
|
425
|
+
ok(verdict(S({ a11y: 90 }), S({ a11y: 95 })).icon === "🟢", "a11y +5 is green");
|
|
426
|
+
ok(verdict(S({}), S({ a11y: 99, seo: 97 })).icon === "🔴", "seo -3 gates even with a11y -1");
|
|
427
|
+
console.log("compare.mjs selftest: OK");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (process.argv[2] === "--selftest") { selftest(); process.exit(0); }
|
|
431
|
+
|
|
432
|
+
const [baseDir, prDir] = process.argv.slice(2);
|
|
433
|
+
if (!baseDir || !prDir) { console.error("usage: compare.mjs <baseDir> <prDir> | --selftest"); process.exit(2); }
|
|
434
|
+
process.stdout.write(render(loadDir(baseDir), loadDir(prDir)).md + "\\n");
|
|
435
|
+
`;
|
|
436
|
+
|
|
437
|
+
const LIGHTHOUSERC_JSON = `{
|
|
438
|
+
"//": "Lighthouse CI config for the per-PR perf workflow (.github/workflows/perf.yml). Mobile UA matches worker-entry.ts buildSegment (MOBILE_RE) so warmup and measurement hit the same device cache key. accessibility/best-practices/seo são análise estática determinística (não sofrem ruído de SSR frio como LCP/score), então compare.mjs os gateia por regressão de score.",
|
|
439
|
+
"ci": {
|
|
440
|
+
"collect": {
|
|
441
|
+
"numberOfRuns": 3,
|
|
442
|
+
"settings": {
|
|
443
|
+
"onlyCategories": ["performance", "accessibility", "best-practices", "seo"],
|
|
444
|
+
"emulatedUserAgent": "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
`;
|