@decocms/start 3.0.0 → 4.1.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/.agents/skills/deco-to-tanstack-migration/SKILL.md +1 -1
- package/.agents/skills/deco-to-tanstack-migration/references/worker-cloudflare.md +32 -135
- package/.agents/skills/deco-to-tanstack-migration/templates/package-json.md +2 -5
- package/.cursor/rules/migration-tooling-policy.mdc +32 -48
- package/CODEOWNERS +4 -12
- package/MIGRATION_TOOLING_PLAN.md +9 -13
- package/package.json +2 -3
- package/scripts/migrate/phase-scaffold.ts +9 -12
- package/scripts/migrate/phase-verify.ts +3 -5
- package/scripts/migrate/templates/package-json.ts +6 -9
- package/src/sdk/logger.test.ts +53 -0
- package/src/sdk/logger.ts +58 -3
- package/src/vite/plugin.js +32 -18
- package/src/vite/plugin.test.js +54 -0
- package/.github/workflows/deploy.yml +0 -141
- package/.github/workflows/preview.yml +0 -200
- package/.github/workflows/sync-secrets.yml +0 -171
- package/deploy/README.md +0 -121
- package/deploy/wrangler-template.jsonc +0 -46
- package/scripts/deploy/build-wrangler-config.mjs +0 -47
- package/scripts/deploy/jsonc.mjs +0 -76
- package/scripts/deploy/site-registry.mjs +0 -95
- package/scripts/deploy/wrangler-wrapper.mjs +0 -118
- package/scripts/migrate/templates/github-workflows.ts +0 -159
package/src/sdk/logger.ts
CHANGED
|
@@ -127,11 +127,25 @@ function shouldLog(level: LogLevel): boolean {
|
|
|
127
127
|
// ---------------------------------------------------------------------------
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
|
-
* Mirrors `@deco/deco/o11y
|
|
131
|
-
* - first arg is
|
|
130
|
+
* Strict structured logger. Mirrors `@deco/deco/o11y`:
|
|
131
|
+
* - first arg is a human-readable message string
|
|
132
132
|
* - optional second arg is a flat attributes object
|
|
133
133
|
*
|
|
134
|
-
* Adapters decide the destination (stdout JSON, OTLP, both, …).
|
|
134
|
+
* Adapters decide the destination (stdout JSON, OTLP, both, …). The
|
|
135
|
+
* contract is intentionally narrow so structured output stays predictable
|
|
136
|
+
* across all sinks.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* logger.info("checkout started", { orderFormId, items });
|
|
141
|
+
* logger.warn("retrying vtex call", { attempt, host });
|
|
142
|
+
*
|
|
143
|
+
* // For Errors, serialize explicitly into the attrs payload:
|
|
144
|
+
* try { ... } catch (err) {
|
|
145
|
+
* const e = serializeError(err);
|
|
146
|
+
* logger.error(e.message, { error: e, stage: "checkout" });
|
|
147
|
+
* }
|
|
148
|
+
* ```
|
|
135
149
|
*/
|
|
136
150
|
export interface Logger {
|
|
137
151
|
debug(msg: string, attrs?: Record<string, unknown>): void;
|
|
@@ -140,6 +154,47 @@ export interface Logger {
|
|
|
140
154
|
error(msg: string, attrs?: Record<string, unknown>): void;
|
|
141
155
|
}
|
|
142
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Normalised, JSON-safe error shape suitable for inclusion in logger
|
|
159
|
+
* attributes. `serializeError` always returns this shape regardless of
|
|
160
|
+
* what was thrown.
|
|
161
|
+
*/
|
|
162
|
+
export interface SerializedError {
|
|
163
|
+
name: string;
|
|
164
|
+
message: string;
|
|
165
|
+
stack?: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Convert any thrown value into a flat, structured object that survives
|
|
170
|
+
* `JSON.stringify` and round-trips cleanly to OTel / Cloudflare Logs.
|
|
171
|
+
* Strict logger sites should call this from their catch blocks rather
|
|
172
|
+
* than passing the Error directly.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```ts
|
|
176
|
+
* try { ... } catch (err) {
|
|
177
|
+
* const e = serializeError(err);
|
|
178
|
+
* logger.error(e.message, { error: e });
|
|
179
|
+
* }
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
export function serializeError(err: unknown): SerializedError {
|
|
183
|
+
if (err instanceof Error) {
|
|
184
|
+
return { name: err.name, message: err.message, stack: err.stack };
|
|
185
|
+
}
|
|
186
|
+
if (err && typeof err === "object") {
|
|
187
|
+
let body: string;
|
|
188
|
+
try {
|
|
189
|
+
body = JSON.stringify(err);
|
|
190
|
+
} catch {
|
|
191
|
+
body = String(err);
|
|
192
|
+
}
|
|
193
|
+
return { name: "NonError", message: body };
|
|
194
|
+
}
|
|
195
|
+
return { name: "NonError", message: String(err) };
|
|
196
|
+
}
|
|
197
|
+
|
|
143
198
|
function emit(level: LogLevel, msg: string, attrs?: Record<string, unknown>): void {
|
|
144
199
|
if (!shouldLog(level)) return;
|
|
145
200
|
try {
|
package/src/vite/plugin.js
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* export default defineConfig({ plugins: [decoVitePlugin(), ...] });
|
|
32
32
|
* ```
|
|
33
33
|
*/
|
|
34
|
-
import {
|
|
34
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
35
35
|
|
|
36
36
|
// Bare-specifier stubs resolved by ID before Vite touches them.
|
|
37
37
|
/** @type {Record<string, string>} */
|
|
@@ -44,6 +44,23 @@ const CLIENT_STUBS = {
|
|
|
44
44
|
"tanstack-start-injected-head-scripts:v": "\0stub:tanstack-head-scripts",
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
+
// SSR-only stubs. Same mechanism as CLIENT_STUBS but applied to the worker
|
|
48
|
+
// SSR build instead of the browser build.
|
|
49
|
+
/** @type {Record<string, string>} */
|
|
50
|
+
const SSR_STUBS = {
|
|
51
|
+
// `@opentelemetry/resources` (transitively pulled in by sdk-logs /
|
|
52
|
+
// sdk-metrics / exporter-* OTel packages — five copies in node_modules due
|
|
53
|
+
// to OTel monorepo peer-dep version pinning) statically imports bare `fs`
|
|
54
|
+
// inside its node-platform machine-id detectors. We never call those
|
|
55
|
+
// detectors — `instrumentWorker` builds the OTel Resource from explicit
|
|
56
|
+
// attributes only — but Vite's CF Workers SSR resolver still walks the
|
|
57
|
+
// re-export barrel and chokes on the bare `fs` specifier (workerd's
|
|
58
|
+
// `nodejs_compat` only exposes the prefixed `node:fs`, not the legacy
|
|
59
|
+
// bare form). Stub it; the static import resolves and the unreachable
|
|
60
|
+
// detector code is never executed.
|
|
61
|
+
fs: "\0stub:bare-fs",
|
|
62
|
+
};
|
|
63
|
+
|
|
47
64
|
// Minimal stub source for each virtual module.
|
|
48
65
|
/** @type {Record<string, string>} */
|
|
49
66
|
const STUB_SOURCE = {
|
|
@@ -72,13 +89,18 @@ const STUB_SOURCE = {
|
|
|
72
89
|
"export default { AsyncLocalStorage: _ALS, AsyncResource, executionAsyncId, createHook };",
|
|
73
90
|
].join("\n"),
|
|
74
91
|
|
|
75
|
-
"\0stub:tanstack-head-scripts":
|
|
76
|
-
"export const injectedHeadScripts = undefined;",
|
|
92
|
+
"\0stub:tanstack-head-scripts": "export const injectedHeadScripts = undefined;",
|
|
77
93
|
|
|
78
94
|
// The admin schema bundle is server-only — the client receives pre-resolved
|
|
79
95
|
// blocks via the SSR payload. Stubbing it on the client cuts a large module
|
|
80
96
|
// (typically 0.5-5 MB) out of the browser bundle.
|
|
81
97
|
"\0stub:meta-gen": "export default {};",
|
|
98
|
+
|
|
99
|
+
// Bare `fs` shim — see SSR_STUBS comment above for the rationale. Surfaces
|
|
100
|
+
// just enough of `import { promises as fs } from 'fs'` to satisfy static
|
|
101
|
+
// module resolution; method calls would throw, but the OTel detector code
|
|
102
|
+
// path is unreachable from `instrumentWorker`.
|
|
103
|
+
"\0stub:bare-fs": "export const promises = {}; export default { promises };",
|
|
82
104
|
};
|
|
83
105
|
|
|
84
106
|
/** @returns {import("vite").PluginOption} */
|
|
@@ -89,6 +111,9 @@ export function decoVitePlugin() {
|
|
|
89
111
|
enforce: "pre",
|
|
90
112
|
|
|
91
113
|
resolveId(id, importer, options) {
|
|
114
|
+
// SSR-only stubs — must be checked first since the client guard below
|
|
115
|
+
// returns undefined for everything that hasn't matched yet on SSR.
|
|
116
|
+
if (options?.ssr && SSR_STUBS[id]) return SSR_STUBS[id];
|
|
92
117
|
// Server builds keep the real modules.
|
|
93
118
|
if (options?.ssr) return undefined;
|
|
94
119
|
// Bare-specifier exact-match stubs (react-dom/server, node:stream, etc.).
|
|
@@ -98,10 +123,7 @@ export function decoVitePlugin() {
|
|
|
98
123
|
// plugin works whether `setup.ts` imports the .json directly (current)
|
|
99
124
|
// or a future variant routes through a generated .ts wrapper.
|
|
100
125
|
// Requires `importer` so we don't accidentally stub the entry module.
|
|
101
|
-
if (
|
|
102
|
-
importer &&
|
|
103
|
-
(id.endsWith("meta.gen.json") || id.endsWith("meta.gen.ts"))
|
|
104
|
-
) {
|
|
126
|
+
if (importer && (id.endsWith("meta.gen.json") || id.endsWith("meta.gen.ts"))) {
|
|
105
127
|
return "\0stub:meta-gen";
|
|
106
128
|
}
|
|
107
129
|
return undefined;
|
|
@@ -154,7 +176,6 @@ export function decoVitePlugin() {
|
|
|
154
176
|
const siteName = process.env.DECO_SITE_NAME;
|
|
155
177
|
const envName = process.env.DECO_ENV_NAME;
|
|
156
178
|
if (siteName && envName) {
|
|
157
|
-
|
|
158
179
|
// Daemon files are .ts and live inside node_modules. Node's
|
|
159
180
|
// experimental strip-types refuses to transpile node_modules, so
|
|
160
181
|
// a plain dynamic `import()` blows up under `vite dev`. Use tsx's
|
|
@@ -214,18 +235,12 @@ export function decoVitePlugin() {
|
|
|
214
235
|
rollupOptions: {
|
|
215
236
|
output: {
|
|
216
237
|
manualChunks(id) {
|
|
217
|
-
if (
|
|
218
|
-
id.includes("node_modules/react-dom") ||
|
|
219
|
-
id.includes("node_modules/react/")
|
|
220
|
-
) {
|
|
238
|
+
if (id.includes("node_modules/react-dom") || id.includes("node_modules/react/")) {
|
|
221
239
|
return "vendor-react";
|
|
222
240
|
}
|
|
223
241
|
|
|
224
242
|
// TanStack Router — client-side router (always needed)
|
|
225
|
-
if (
|
|
226
|
-
id.includes("@tanstack/react-router") ||
|
|
227
|
-
id.includes("@tanstack/router-core")
|
|
228
|
-
) {
|
|
243
|
+
if (id.includes("@tanstack/react-router") || id.includes("@tanstack/router-core")) {
|
|
229
244
|
return "vendor-router";
|
|
230
245
|
}
|
|
231
246
|
|
|
@@ -275,8 +290,7 @@ export function decoVitePlugin() {
|
|
|
275
290
|
configEnvironment(name, env) {
|
|
276
291
|
if (name === "ssr" || name === "client") {
|
|
277
292
|
env.optimizeDeps = env.optimizeDeps || {};
|
|
278
|
-
env.optimizeDeps.esbuildOptions =
|
|
279
|
-
env.optimizeDeps.esbuildOptions || {};
|
|
293
|
+
env.optimizeDeps.esbuildOptions = env.optimizeDeps.esbuildOptions || {};
|
|
280
294
|
env.optimizeDeps.esbuildOptions.jsx = "automatic";
|
|
281
295
|
env.optimizeDeps.esbuildOptions.jsxImportSource = "react";
|
|
282
296
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { decoVitePlugin } from "./plugin.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Vite plugin's `resolveId` / `load` hooks are pure functions over their
|
|
6
|
+
* inputs, so we can exercise them without spinning up a Vite environment.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function getPlugin() {
|
|
10
|
+
const result = decoVitePlugin();
|
|
11
|
+
// decoVitePlugin returns a single plugin object today, but the type is
|
|
12
|
+
// `PluginOption` which permits arrays — handle both.
|
|
13
|
+
return Array.isArray(result) ? result[0] : result;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("decoVitePlugin SSR stubs", () => {
|
|
17
|
+
it("rewrites bare `fs` to a virtual module on SSR", () => {
|
|
18
|
+
const p = getPlugin();
|
|
19
|
+
const id = p.resolveId.call({}, "fs", undefined, { ssr: true });
|
|
20
|
+
expect(id).toBe("\0stub:bare-fs");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("does NOT rewrite bare `fs` on client (browser builds don't import fs)", () => {
|
|
24
|
+
const p = getPlugin();
|
|
25
|
+
const id = p.resolveId.call({}, "fs", undefined, { ssr: false });
|
|
26
|
+
expect(id).toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("loads an empty surface for the bare-fs virtual module", () => {
|
|
30
|
+
const p = getPlugin();
|
|
31
|
+
const src = p.load.call({}, "\0stub:bare-fs", { ssr: true });
|
|
32
|
+
expect(src).toContain("export const promises = {}");
|
|
33
|
+
expect(src).toContain("export default");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("does not interfere with real SSR modules", () => {
|
|
37
|
+
const p = getPlugin();
|
|
38
|
+
expect(p.resolveId.call({}, "@decocms/start/cms", undefined, { ssr: true })).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("decoVitePlugin client stubs (regression guard)", () => {
|
|
43
|
+
it("still rewrites node:async_hooks on the client build", () => {
|
|
44
|
+
const p = getPlugin();
|
|
45
|
+
const id = p.resolveId.call({}, "node:async_hooks", undefined, { ssr: false });
|
|
46
|
+
expect(id).toBe("\0stub:node-async-hooks");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("does not rewrite client stubs on SSR", () => {
|
|
50
|
+
const p = getPlugin();
|
|
51
|
+
const id = p.resolveId.call({}, "react-dom/server", undefined, { ssr: true });
|
|
52
|
+
expect(id).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
name: deploy (central)
|
|
2
|
-
|
|
3
|
-
# Reusable workflow that drives `wrangler deploy` for any storefront repo.
|
|
4
|
-
# Worker name is the storefront repo basename by convention; there is no
|
|
5
|
-
# per-site registry. The deploy is gated by the `decocms-deployer` GitHub App
|
|
6
|
-
# being installed on the target storefront repo -- the App-token mint fails
|
|
7
|
-
# (and the deploy never starts) if the App isn't installed there.
|
|
8
|
-
#
|
|
9
|
-
# v3 architecture (D6.2): triggered via `workflow_dispatch` from the storefront,
|
|
10
|
-
# authenticated as the `decocms-deployer` GitHub App. The deploy runs IN THIS
|
|
11
|
-
# REPO'S CONTEXT, so `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` resolve
|
|
12
|
-
# from this repo's plain repo secrets and never leave decocms/deco-start.
|
|
13
|
-
#
|
|
14
|
-
# Caller usage (in the storefront repo, `.github/workflows/deploy.yml`):
|
|
15
|
-
#
|
|
16
|
-
# on:
|
|
17
|
-
# push:
|
|
18
|
-
# branches: [main]
|
|
19
|
-
# permissions:
|
|
20
|
-
# contents: read
|
|
21
|
-
# jobs:
|
|
22
|
-
# trigger:
|
|
23
|
-
# runs-on: ubuntu-latest
|
|
24
|
-
# steps:
|
|
25
|
-
# - uses: actions/create-github-app-token@v1
|
|
26
|
-
# id: app-token
|
|
27
|
-
# with:
|
|
28
|
-
# app-id: ${{ secrets.DECOCMS_DEPLOYER_APP_ID }}
|
|
29
|
-
# private-key: ${{ secrets.DECOCMS_DEPLOYER_APP_PRIVATE_KEY }}
|
|
30
|
-
# owner: decocms
|
|
31
|
-
# repositories: deco-start
|
|
32
|
-
# - env:
|
|
33
|
-
# GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
34
|
-
# run: |
|
|
35
|
-
# gh workflow run deploy.yml \
|
|
36
|
-
# --repo decocms/deco-start \
|
|
37
|
-
# --ref v3 \
|
|
38
|
-
# -f site_owner=${GITHUB_REPOSITORY%%/*} \
|
|
39
|
-
# -f site_name=${GITHUB_REPOSITORY##*/}
|
|
40
|
-
|
|
41
|
-
on:
|
|
42
|
-
workflow_dispatch:
|
|
43
|
-
inputs:
|
|
44
|
-
site_owner:
|
|
45
|
-
description: "GitHub org of the storefront (e.g. deco-sites). Defaults to deco-sites."
|
|
46
|
-
type: string
|
|
47
|
-
required: false
|
|
48
|
-
default: deco-sites
|
|
49
|
-
site_name:
|
|
50
|
-
description: "Storefront repo basename. Becomes the Cloudflare worker name."
|
|
51
|
-
type: string
|
|
52
|
-
required: true
|
|
53
|
-
|
|
54
|
-
permissions:
|
|
55
|
-
contents: read
|
|
56
|
-
|
|
57
|
-
concurrency:
|
|
58
|
-
group: deploy-${{ inputs.site_owner }}-${{ inputs.site_name }}
|
|
59
|
-
cancel-in-progress: false
|
|
60
|
-
|
|
61
|
-
jobs:
|
|
62
|
-
deploy:
|
|
63
|
-
runs-on: ubuntu-latest
|
|
64
|
-
steps:
|
|
65
|
-
- name: Checkout deco-start (template + scripts)
|
|
66
|
-
uses: actions/checkout@v4
|
|
67
|
-
|
|
68
|
-
- name: Mint App token for storefront checkout
|
|
69
|
-
id: app-token
|
|
70
|
-
uses: actions/create-github-app-token@v1
|
|
71
|
-
with:
|
|
72
|
-
app-id: ${{ secrets.DECOCMS_DEPLOYER_APP_ID }}
|
|
73
|
-
private-key: ${{ secrets.DECOCMS_DEPLOYER_APP_PRIVATE_KEY }}
|
|
74
|
-
owner: ${{ inputs.site_owner }}
|
|
75
|
-
repositories: ${{ inputs.site_name }}
|
|
76
|
-
|
|
77
|
-
# SECURITY: production deploys IGNORE any caller-supplied sha. The deploy
|
|
78
|
-
# always targets the storefront's CURRENT default-branch HEAD. This means
|
|
79
|
-
# an attacker with push to repo A who triggers a deploy of repo B can
|
|
80
|
-
# only force a no-op redeploy of B's current main -- they cannot select
|
|
81
|
-
# an arbitrary historical commit (no force-rollback attack).
|
|
82
|
-
- name: Resolve target sha (storefront default branch HEAD)
|
|
83
|
-
id: target
|
|
84
|
-
env:
|
|
85
|
-
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
86
|
-
SITE_REPO: ${{ inputs.site_owner }}/${{ inputs.site_name }}
|
|
87
|
-
run: |
|
|
88
|
-
set -euo pipefail
|
|
89
|
-
DEFAULT_BRANCH=$(gh api "repos/$SITE_REPO" --jq .default_branch)
|
|
90
|
-
SHA=$(gh api "repos/$SITE_REPO/branches/$DEFAULT_BRANCH" --jq .commit.sha)
|
|
91
|
-
echo "ref=$DEFAULT_BRANCH" >> "$GITHUB_OUTPUT"
|
|
92
|
-
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
|
|
93
|
-
echo "::notice::Deploying $SITE_REPO @ $DEFAULT_BRANCH ($SHA)"
|
|
94
|
-
|
|
95
|
-
- name: Checkout storefront at default-branch HEAD
|
|
96
|
-
uses: actions/checkout@v4
|
|
97
|
-
with:
|
|
98
|
-
repository: ${{ inputs.site_owner }}/${{ inputs.site_name }}
|
|
99
|
-
ref: ${{ steps.target.outputs.sha }}
|
|
100
|
-
token: ${{ steps.app-token.outputs.token }}
|
|
101
|
-
path: site
|
|
102
|
-
fetch-depth: 1
|
|
103
|
-
|
|
104
|
-
- uses: actions/setup-node@v4
|
|
105
|
-
with:
|
|
106
|
-
node-version: 22
|
|
107
|
-
|
|
108
|
-
- name: Restore npm cache
|
|
109
|
-
uses: actions/cache@v4
|
|
110
|
-
with:
|
|
111
|
-
path: ~/.npm
|
|
112
|
-
key: npm-${{ runner.os }}-${{ hashFiles('site/package-lock.json') }}
|
|
113
|
-
restore-keys: npm-${{ runner.os }}-
|
|
114
|
-
|
|
115
|
-
- name: Install dependencies
|
|
116
|
-
working-directory: site
|
|
117
|
-
run: |
|
|
118
|
-
if [ ! -f package-lock.json ]; then
|
|
119
|
-
npm install --package-lock-only
|
|
120
|
-
fi
|
|
121
|
-
npm ci
|
|
122
|
-
|
|
123
|
-
- name: Build
|
|
124
|
-
working-directory: site
|
|
125
|
-
run: npm run build
|
|
126
|
-
|
|
127
|
-
- name: Generate wrangler.jsonc from template
|
|
128
|
-
working-directory: site
|
|
129
|
-
env:
|
|
130
|
-
DECO_START_PATH: "${{ github.workspace }}"
|
|
131
|
-
WORKER_NAME: ${{ inputs.site_name }}
|
|
132
|
-
OUTPUT_PATH: "./wrangler.jsonc"
|
|
133
|
-
run: node "$DECO_START_PATH/scripts/deploy/build-wrangler-config.mjs"
|
|
134
|
-
|
|
135
|
-
- name: Deploy to Cloudflare Workers
|
|
136
|
-
working-directory: site
|
|
137
|
-
env:
|
|
138
|
-
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
139
|
-
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
140
|
-
BUILD_HASH: ${{ steps.target.outputs.sha }}
|
|
141
|
-
run: npx wrangler deploy --var "BUILD_HASH:${BUILD_HASH:0:7}"
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
name: preview (central)
|
|
2
|
-
|
|
3
|
-
# Reusable workflow that uploads a preview Worker version (alias) for any
|
|
4
|
-
# storefront repo. Worker name is the storefront repo basename by convention;
|
|
5
|
-
# there is no per-site registry. The preview is gated by the `decocms-deployer`
|
|
6
|
-
# GitHub App being installed on the target storefront repo.
|
|
7
|
-
#
|
|
8
|
-
# v3 architecture (D6.2): triggered via `workflow_dispatch` from the storefront,
|
|
9
|
-
# authenticated as the `decocms-deployer` GitHub App. CF secrets resolve from
|
|
10
|
-
# this repo's plain repo secrets and never leave decocms/deco-start.
|
|
11
|
-
#
|
|
12
|
-
# Caller usage (in the storefront repo, `.github/workflows/preview.yml`):
|
|
13
|
-
#
|
|
14
|
-
# on:
|
|
15
|
-
# pull_request:
|
|
16
|
-
# types: [opened, synchronize, reopened]
|
|
17
|
-
# push:
|
|
18
|
-
# branches: ['env/**']
|
|
19
|
-
# permissions:
|
|
20
|
-
# contents: read
|
|
21
|
-
# jobs:
|
|
22
|
-
# trigger:
|
|
23
|
-
# runs-on: ubuntu-latest
|
|
24
|
-
# steps:
|
|
25
|
-
# - id: meta
|
|
26
|
-
# run: |
|
|
27
|
-
# if [ "${{ github.event_name }}" = "pull_request" ]; then
|
|
28
|
-
# echo "alias=pr-${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT"
|
|
29
|
-
# echo "sha=${{ github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT"
|
|
30
|
-
# else
|
|
31
|
-
# REF="${GITHUB_REF#refs/heads/env/}"
|
|
32
|
-
# echo "alias=$(echo "$REF" | sed 's|[^a-z0-9-]|-|g')" >> "$GITHUB_OUTPUT"
|
|
33
|
-
# echo "sha=${{ github.sha }}" >> "$GITHUB_OUTPUT"
|
|
34
|
-
# fi
|
|
35
|
-
# - uses: actions/create-github-app-token@v1
|
|
36
|
-
# id: app-token
|
|
37
|
-
# with:
|
|
38
|
-
# app-id: ${{ secrets.DECOCMS_DEPLOYER_APP_ID }}
|
|
39
|
-
# private-key: ${{ secrets.DECOCMS_DEPLOYER_APP_PRIVATE_KEY }}
|
|
40
|
-
# owner: decocms
|
|
41
|
-
# repositories: deco-start
|
|
42
|
-
# - env:
|
|
43
|
-
# GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
44
|
-
# run: |
|
|
45
|
-
# gh workflow run preview.yml \
|
|
46
|
-
# --repo decocms/deco-start \
|
|
47
|
-
# --ref v3 \
|
|
48
|
-
# -f site_owner=${GITHUB_REPOSITORY%%/*} \
|
|
49
|
-
# -f site_name=${GITHUB_REPOSITORY##*/} \
|
|
50
|
-
# -f site_sha=${{ steps.meta.outputs.sha }} \
|
|
51
|
-
# -f alias=${{ steps.meta.outputs.alias }} \
|
|
52
|
-
# -f pr_number=${{ github.event.pull_request.number || '' }}
|
|
53
|
-
#
|
|
54
|
-
# Note on forks: pull_request runs from forked repos cannot access repo
|
|
55
|
-
# secrets (incl. DECOCMS_DEPLOYER_APP_*), so they cannot trigger previews.
|
|
56
|
-
|
|
57
|
-
on:
|
|
58
|
-
workflow_dispatch:
|
|
59
|
-
inputs:
|
|
60
|
-
site_owner:
|
|
61
|
-
description: "GitHub org of the storefront. Defaults to deco-sites."
|
|
62
|
-
type: string
|
|
63
|
-
required: false
|
|
64
|
-
default: deco-sites
|
|
65
|
-
site_name:
|
|
66
|
-
description: "Storefront repo basename. Becomes the Cloudflare worker name."
|
|
67
|
-
type: string
|
|
68
|
-
required: true
|
|
69
|
-
site_sha:
|
|
70
|
-
description: "Commit sha to build & preview. Trusted as-is (preview alias has no production blast radius)."
|
|
71
|
-
type: string
|
|
72
|
-
required: true
|
|
73
|
-
alias:
|
|
74
|
-
description: "Preview alias name (e.g. pr-123, feature-foo). Must match wrangler alias rules."
|
|
75
|
-
type: string
|
|
76
|
-
required: true
|
|
77
|
-
pr_number:
|
|
78
|
-
description: "Optional PR number on the storefront repo. If set, the preview URL is commented back on the PR."
|
|
79
|
-
type: string
|
|
80
|
-
required: false
|
|
81
|
-
default: ""
|
|
82
|
-
|
|
83
|
-
permissions:
|
|
84
|
-
contents: read
|
|
85
|
-
|
|
86
|
-
concurrency:
|
|
87
|
-
group: preview-${{ inputs.site_owner }}-${{ inputs.site_name }}-${{ inputs.alias }}
|
|
88
|
-
cancel-in-progress: true
|
|
89
|
-
|
|
90
|
-
jobs:
|
|
91
|
-
preview:
|
|
92
|
-
runs-on: ubuntu-latest
|
|
93
|
-
steps:
|
|
94
|
-
- name: Checkout deco-start (template + scripts)
|
|
95
|
-
uses: actions/checkout@v4
|
|
96
|
-
|
|
97
|
-
- name: Validate alias format
|
|
98
|
-
env:
|
|
99
|
-
ALIAS: ${{ inputs.alias }}
|
|
100
|
-
run: |
|
|
101
|
-
set -euo pipefail
|
|
102
|
-
if ! echo "$ALIAS" | grep -Eq '^[a-z0-9][a-z0-9-]{0,62}$'; then
|
|
103
|
-
echo "::error::Invalid alias '$ALIAS'. Must be lowercase alphanumeric/hyphens, max 63 chars, start with alphanumeric."
|
|
104
|
-
exit 1
|
|
105
|
-
fi
|
|
106
|
-
|
|
107
|
-
- name: Mint App token for storefront checkout
|
|
108
|
-
id: app-token
|
|
109
|
-
uses: actions/create-github-app-token@v1
|
|
110
|
-
with:
|
|
111
|
-
app-id: ${{ secrets.DECOCMS_DEPLOYER_APP_ID }}
|
|
112
|
-
private-key: ${{ secrets.DECOCMS_DEPLOYER_APP_PRIVATE_KEY }}
|
|
113
|
-
owner: ${{ inputs.site_owner }}
|
|
114
|
-
repositories: ${{ inputs.site_name }}
|
|
115
|
-
|
|
116
|
-
- name: Checkout storefront at requested sha
|
|
117
|
-
uses: actions/checkout@v4
|
|
118
|
-
with:
|
|
119
|
-
repository: ${{ inputs.site_owner }}/${{ inputs.site_name }}
|
|
120
|
-
ref: ${{ inputs.site_sha }}
|
|
121
|
-
token: ${{ steps.app-token.outputs.token }}
|
|
122
|
-
path: site
|
|
123
|
-
fetch-depth: 1
|
|
124
|
-
|
|
125
|
-
- uses: actions/setup-node@v4
|
|
126
|
-
with:
|
|
127
|
-
node-version: 22
|
|
128
|
-
|
|
129
|
-
- name: Restore npm cache
|
|
130
|
-
uses: actions/cache@v4
|
|
131
|
-
with:
|
|
132
|
-
path: ~/.npm
|
|
133
|
-
key: npm-${{ runner.os }}-${{ hashFiles('site/package-lock.json') }}
|
|
134
|
-
restore-keys: npm-${{ runner.os }}-
|
|
135
|
-
|
|
136
|
-
- name: Install dependencies
|
|
137
|
-
working-directory: site
|
|
138
|
-
run: |
|
|
139
|
-
if [ ! -f package-lock.json ]; then
|
|
140
|
-
npm install --package-lock-only
|
|
141
|
-
fi
|
|
142
|
-
npm ci
|
|
143
|
-
|
|
144
|
-
- name: Build
|
|
145
|
-
working-directory: site
|
|
146
|
-
run: npm run build
|
|
147
|
-
|
|
148
|
-
- name: Generate wrangler.jsonc from template
|
|
149
|
-
working-directory: site
|
|
150
|
-
env:
|
|
151
|
-
DECO_START_PATH: "${{ github.workspace }}"
|
|
152
|
-
WORKER_NAME: ${{ inputs.site_name }}
|
|
153
|
-
OUTPUT_PATH: "./wrangler.jsonc"
|
|
154
|
-
run: node "$DECO_START_PATH/scripts/deploy/build-wrangler-config.mjs"
|
|
155
|
-
|
|
156
|
-
- name: Upload preview version
|
|
157
|
-
id: deploy
|
|
158
|
-
working-directory: site
|
|
159
|
-
env:
|
|
160
|
-
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
161
|
-
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
162
|
-
ALIAS: ${{ inputs.alias }}
|
|
163
|
-
run: |
|
|
164
|
-
set +e
|
|
165
|
-
OUTPUT=$(npx wrangler versions upload --preview-alias "$ALIAS" 2>&1)
|
|
166
|
-
EXIT_CODE=$?
|
|
167
|
-
set -e
|
|
168
|
-
echo "$OUTPUT"
|
|
169
|
-
if [ $EXIT_CODE -ne 0 ]; then
|
|
170
|
-
echo "::error::wrangler versions upload failed with exit code $EXIT_CODE"
|
|
171
|
-
exit $EXIT_CODE
|
|
172
|
-
fi
|
|
173
|
-
PREVIEW_URL=$(echo "$OUTPUT" | grep 'Version Preview URL:' | sed 's/.*Version Preview URL: //')
|
|
174
|
-
ALIAS_URL=$(echo "$OUTPUT" | grep 'Version Preview Alias URL:' | sed 's/.*Version Preview Alias URL: //')
|
|
175
|
-
echo "preview_url=${PREVIEW_URL}" >> "$GITHUB_OUTPUT"
|
|
176
|
-
echo "alias_url=${ALIAS_URL}" >> "$GITHUB_OUTPUT"
|
|
177
|
-
|
|
178
|
-
- name: Mint App token for PR comment
|
|
179
|
-
id: comment-token
|
|
180
|
-
if: inputs.pr_number != ''
|
|
181
|
-
uses: actions/create-github-app-token@v1
|
|
182
|
-
with:
|
|
183
|
-
app-id: ${{ secrets.DECOCMS_DEPLOYER_APP_ID }}
|
|
184
|
-
private-key: ${{ secrets.DECOCMS_DEPLOYER_APP_PRIVATE_KEY }}
|
|
185
|
-
owner: ${{ inputs.site_owner }}
|
|
186
|
-
repositories: ${{ inputs.site_name }}
|
|
187
|
-
permission-pull-requests: write
|
|
188
|
-
|
|
189
|
-
- name: Comment preview URL on storefront PR
|
|
190
|
-
if: inputs.pr_number != ''
|
|
191
|
-
env:
|
|
192
|
-
GH_TOKEN: ${{ steps.comment-token.outputs.token }}
|
|
193
|
-
REPO: ${{ inputs.site_owner }}/${{ inputs.site_name }}
|
|
194
|
-
PR_NUMBER: ${{ inputs.pr_number }}
|
|
195
|
-
PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }}
|
|
196
|
-
ALIAS_URL: ${{ steps.deploy.outputs.alias_url }}
|
|
197
|
-
run: |
|
|
198
|
-
set -euo pipefail
|
|
199
|
-
BODY=$(printf '### Preview deployed\n\n| | URL |\n|---|---|\n| **Version** | %s |\n| **Alias** | %s |\n' "$PREVIEW_URL" "$ALIAS_URL")
|
|
200
|
-
gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$BODY"
|