@decocms/start 2.30.1 → 3.0.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/references/worker-cloudflare.md +90 -63
- package/.cursor/rules/migration-tooling-policy.mdc +41 -28
- package/.github/workflows/deploy.yml +89 -70
- package/.github/workflows/preview.yml +137 -79
- package/.github/workflows/sync-secrets.yml +61 -70
- package/CODEOWNERS +6 -8
- package/MIGRATION_TOOLING_PLAN.md +14 -9
- package/deploy/README.md +100 -90
- package/deploy/wrangler-template.jsonc +22 -4
- package/package.json +1 -1
- package/scripts/deploy/build-wrangler-config.mjs +10 -12
- package/scripts/deploy/site-registry.mjs +52 -99
- package/scripts/deploy/wrangler-wrapper.mjs +22 -30
- package/scripts/migrate/phase-scaffold.ts +6 -6
- package/scripts/migrate/phase-verify.ts +2 -2
- package/scripts/migrate/templates/github-workflows.ts +86 -25
- package/deploy/sites/als-tanstack.jsonc +0 -7
- package/deploy/sites/americanas-tanstack.jsonc +0 -4
- package/deploy/sites/baggagio-tanstack.jsonc +0 -4
- package/deploy/sites/casaevideo-storefront.jsonc +0 -11
- package/deploy/sites/lebiscuit-tanstack.jsonc +0 -19
- package/deploy/sites/miess-01-tanstack.jsonc +0 -8
- package/scripts/deploy/resolve-site.mjs +0 -58
package/deploy/README.md
CHANGED
|
@@ -1,111 +1,121 @@
|
|
|
1
|
-
# `deploy/` — central
|
|
1
|
+
# `deploy/` — central wrangler template
|
|
2
2
|
|
|
3
|
-
This directory
|
|
4
|
-
|
|
5
|
-
workflows under [`.github/workflows/`](../.github/workflows/)
|
|
6
|
-
`preview.yml`, `sync-secrets.yml`) and by the local
|
|
3
|
+
This directory holds **`wrangler-template.jsonc`** — the canonical wrangler config
|
|
4
|
+
that every storefront on the platform inherits. It is consumed by the reusable
|
|
5
|
+
GitHub workflows under [`.github/workflows/`](../.github/workflows/)
|
|
6
|
+
(`deploy.yml`, `preview.yml`, `sync-secrets.yml`) and by the local
|
|
7
|
+
`deco-wrangler` CLI.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
| `sites/<repo-name>.jsonc` | Per-site overrides. Only the keys that genuinely vary per-site live here (`worker_name` always; `routes`, `kv_namespaces`, `analytics_engine_datasets`, `version_metadata` when used). |
|
|
14
|
-
|
|
15
|
-
The repository name (the part of `${{ github.repository }}` after the `/`) is
|
|
16
|
-
the lookup key. `als-tanstack` deploys via `sites/als-tanstack.jsonc`. There is
|
|
17
|
-
no other way to identify a site.
|
|
9
|
+
There is **no per-site registry**. Worker name is the storefront repo basename
|
|
10
|
+
by convention (`deco-sites/baggagio-tanstack` → worker `baggagio-tanstack`).
|
|
11
|
+
Anything that must vary deterministically per worker (like the Analytics Engine
|
|
12
|
+
dataset name) is encoded as a substitution token in the template — see
|
|
13
|
+
[Substitution tokens](#substitution-tokens) below.
|
|
18
14
|
|
|
19
15
|
## Trust model
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
The deploy is gated by the `decocms-deployer` **GitHub App** being installed on
|
|
18
|
+
the target storefront repo:
|
|
19
|
+
|
|
20
|
+
1. The storefront's caller workflow mints a short-lived App-installation token.
|
|
21
|
+
2. It calls `gh workflow run deploy.yml --repo decocms/deco-start -f site_owner=… -f site_name=…`.
|
|
22
|
+
3. The central deploy workflow runs **in this repo's context** and itself mints
|
|
23
|
+
another short-lived App-installation token to check out the storefront. If
|
|
24
|
+
the App isn't installed on `<site_owner>/<site_name>`, the mint fails and
|
|
25
|
+
the deploy never starts.
|
|
26
|
+
4. The central workflow then runs build + `wrangler deploy` using
|
|
27
|
+
`CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` from this repo's plain
|
|
28
|
+
repo secrets.
|
|
29
|
+
|
|
30
|
+
Properties this gives:
|
|
31
|
+
|
|
32
|
+
- **CF credentials never leave decocms/deco-start.** The storefront repo holds
|
|
33
|
+
zero Cloudflare credentials — it only has the GitHub App credentials, which
|
|
34
|
+
can be used solely to trigger workflows on this repo.
|
|
35
|
+
- **Worker naming is convention-based and not customer-controlled.** A
|
|
36
|
+
customer with push access to their own storefront cannot rename the worker
|
|
37
|
+
their deploy lands on (the central workflow always uses
|
|
38
|
+
`inputs.site_name` as the worker name; modifying the caller stub to pass a
|
|
39
|
+
different `site_name` would also require the App to be installed on that
|
|
40
|
+
other repo).
|
|
41
|
+
- **Force-rollback is impossible for production.** The central deploy
|
|
42
|
+
workflow ignores any caller-supplied sha and always resolves the
|
|
43
|
+
storefront's current default-branch HEAD itself. The worst a compromised
|
|
44
|
+
storefront can do across tenants is trigger a no-op redeploy of another
|
|
45
|
+
storefront's current main.
|
|
46
|
+
|
|
47
|
+
`deploy/` and `scripts/deploy/` and the central workflow files are
|
|
48
|
+
CODEOWNERS-protected — only the platform team approves changes.
|
|
30
49
|
|
|
31
50
|
### Where Cloudflare credentials live
|
|
32
51
|
|
|
33
|
-
|
|
34
|
-
`production` GitHub Environment**, never in any storefront repo. The reusable
|
|
35
|
-
workflows declare `environment: production` on the deploy / preview /
|
|
36
|
-
sync-secrets jobs, which is what GitHub uses to resolve `secrets.CLOUDFLARE_*`
|
|
37
|
-
against the called repo (deco-start) instead of the caller (the storefront).
|
|
38
|
-
|
|
39
|
-
This is the second half of the trust property: even if a storefront repo were
|
|
40
|
-
fully compromised, the attacker has no path to the Cloudflare token. The
|
|
41
|
-
storefront repo only holds its own `SECRET_*` runtime secrets, which are
|
|
42
|
-
inherited by `sync-secrets.yml` and pushed to its own worker as runtime
|
|
43
|
-
secrets — never reaching another site's worker because the central workflow
|
|
44
|
-
resolves `worker_name` from this registry, not from caller input.
|
|
45
|
-
|
|
46
|
-
| Secret class | Lives in | Reaches worker via |
|
|
52
|
+
| Secret class | Lives in | How it reaches the worker |
|
|
47
53
|
|---|---|---|
|
|
48
|
-
| `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` | this repo's
|
|
49
|
-
| `
|
|
54
|
+
| `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` | this repo's **repo secrets** | central workflow runs in this repo's context, env-var resolves natively |
|
|
55
|
+
| `DECOCMS_DEPLOYER_APP_ID` / `DECOCMS_DEPLOYER_APP_PRIVATE_KEY` | this repo's repo secrets AND deco-sites org-level secrets | mints short-lived installation tokens for both directions of the dispatch flow |
|
|
56
|
+
| `SECRET_*` runtime secrets (per site) | this repo's `<site_name>-secrets` GitHub Environment | `sync-secrets.yml` binds to that environment, reads `SECRET_*` from `${{ secrets }}`, runs `wrangler secret put` |
|
|
50
57
|
|
|
51
|
-
To rotate
|
|
52
|
-
|
|
58
|
+
To rotate Cloudflare credentials, edit them in this repo only. To rotate a
|
|
59
|
+
runtime secret for one storefront, edit the corresponding environment in this
|
|
60
|
+
repo only. No storefront PR needed for either.
|
|
53
61
|
|
|
54
|
-
## How wrangler.jsonc is generated
|
|
62
|
+
## How `wrangler.jsonc` is generated
|
|
55
63
|
|
|
56
64
|
At deploy time, the central workflow runs
|
|
57
65
|
[`scripts/deploy/build-wrangler-config.mjs`](../scripts/deploy/build-wrangler-config.mjs),
|
|
58
66
|
which:
|
|
59
67
|
|
|
60
|
-
1. Loads `deploy/wrangler-template.jsonc
|
|
61
|
-
2.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
1. Loads `deploy/wrangler-template.jsonc`.
|
|
69
|
+
2. Substitutes `$WORKER_*` tokens (see below) using the worker name passed by
|
|
70
|
+
the central workflow (= storefront repo basename).
|
|
71
|
+
3. Writes the result to `./wrangler.jsonc` in the storefront checkout, with
|
|
72
|
+
`name` injected as the first key.
|
|
65
73
|
|
|
66
74
|
`account_id` is never written to JSON — wrangler reads it from
|
|
67
|
-
`CLOUDFLARE_ACCOUNT_ID` (env var in CI; `wrangler login` locally).
|
|
75
|
+
`CLOUDFLARE_ACCOUNT_ID` (env var in CI; `wrangler login` locally). This way a
|
|
76
|
+
typo cannot misroute a deploy to a different Cloudflare account.
|
|
77
|
+
|
|
78
|
+
### Substitution tokens
|
|
79
|
+
|
|
80
|
+
Any string in the template containing one of these literals is replaced at
|
|
81
|
+
build time:
|
|
82
|
+
|
|
83
|
+
| Token | Replacement | Example use |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `$WORKER_NAME` | worker name verbatim | rare; mostly available for parity |
|
|
86
|
+
| `$WORKER_UNDERSCORE` | worker name with `-` → `_` | `analytics_engine_datasets[].dataset` (must be a valid Postgres-style identifier) |
|
|
87
|
+
|
|
88
|
+
To add a new derived field, add the token wherever it makes sense in
|
|
89
|
+
`wrangler-template.jsonc`. Anything not in the substitution table appears
|
|
90
|
+
verbatim in the generated config.
|
|
68
91
|
|
|
69
92
|
## Adding a new site
|
|
70
93
|
|
|
71
|
-
1.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
"analytics_engine_datasets": [ // optional
|
|
100
|
-
{ "binding": "DECO_METRICS", "dataset": "deco_metrics_<site>" }
|
|
101
|
-
],
|
|
102
|
-
"version_metadata": { // optional
|
|
103
|
-
"binding": "CF_VERSION_METADATA"
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
All other wrangler keys (compatibility flags, `main`, observability, etc.) come
|
|
109
|
-
from the template — do not duplicate them per-site. If a per-site override is
|
|
110
|
-
genuinely needed for one of those keys, add it to the schema and document the
|
|
111
|
-
reason here.
|
|
94
|
+
1. Install the `decocms-deployer` GitHub App on the new storefront repo
|
|
95
|
+
(Settings → Integrations → GitHub Apps in the deco-sites org).
|
|
96
|
+
2. Add the four caller workflow stubs to the new repo (copy from any existing
|
|
97
|
+
storefront's `.github/workflows/{deploy,preview,sync-secrets,regen-blocks}.yml`).
|
|
98
|
+
3. Add `wrangler.jsonc` to the new repo's `.gitignore` and add the
|
|
99
|
+
`gen:wrangler` / `predev` / `prebuild` / `types` scripts to `package.json`
|
|
100
|
+
so local dev still works (use any existing storefront as a template).
|
|
101
|
+
4. If the site needs runtime secrets, create a new environment in this repo
|
|
102
|
+
named `<repo-basename>-secrets` and add the `SECRET_*` values there. Set
|
|
103
|
+
environment protection rules to grant the site team self-service access to
|
|
104
|
+
their own environment.
|
|
105
|
+
5. Push to `main` and verify the deploy lands on a worker named after the repo.
|
|
106
|
+
|
|
107
|
+
## Migrating an existing site whose worker name doesn't match its repo
|
|
108
|
+
|
|
109
|
+
Two cases to be aware of:
|
|
110
|
+
|
|
111
|
+
- **Worker rename.** The worker created by the first deploy will use the repo
|
|
112
|
+
basename. If an old worker exists with a different name (e.g.
|
|
113
|
+
`miess-01-tanstack` repo whose old worker was `miess-tanstack`), you'll need
|
|
114
|
+
a manual cutover: deploy the new worker, re-attach custom domain routes via
|
|
115
|
+
the Cloudflare dashboard, copy any wrangler secrets, then delete the old
|
|
116
|
+
worker. There is intentionally no per-site override for this — these cases
|
|
117
|
+
are rare and best resolved at the CF layer.
|
|
118
|
+
- **AE dataset rename.** The dataset name is derived from worker name, so a
|
|
119
|
+
worker rename also changes the AE dataset. Old data remains queryable under
|
|
120
|
+
the old dataset name; new data goes to the new name. Update Grafana panels
|
|
121
|
+
and saved queries accordingly.
|
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
// Canonical wrangler.jsonc template for every storefront on the platform.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// There is no per-site registry. Worker name == storefront repo basename by
|
|
4
|
+
// convention; the central deploy workflows pass `WORKER_NAME` to the build
|
|
5
|
+
// script, which substitutes `$WORKER_*` tokens in this template and writes
|
|
6
|
+
// the result to `wrangler.jsonc` in the caller checkout.
|
|
7
|
+
//
|
|
8
|
+
// Substitution tokens (see scripts/deploy/site-registry.mjs):
|
|
9
|
+
// $WORKER_NAME -> worker name verbatim (e.g. "als-tanstack")
|
|
10
|
+
// $WORKER_UNDERSCORE -> worker name, `-` -> `_` (e.g. "als_tanstack")
|
|
11
|
+
//
|
|
12
|
+
// To upgrade compatibility flags, observability, KV bindings, or any field
|
|
13
|
+
// that should change for every site at once, change this file and tag a new
|
|
14
|
+
// deco-start release.
|
|
7
15
|
//
|
|
8
16
|
// Notes on what is INTENTIONALLY missing here:
|
|
9
17
|
// - "name" -- always derived from the per-site `worker_name`.
|
|
10
18
|
// - "account_id" -- never lives in the JSON. Wrangler reads it from the
|
|
11
19
|
// CLOUDFLARE_ACCOUNT_ID env var in CI and from local config otherwise.
|
|
12
20
|
// This way, a typo in JSON cannot misroute a deploy.
|
|
21
|
+
// - "routes" -- production custom domains are managed in the Cloudflare
|
|
22
|
+
// dashboard, not in JSON. Avoids drift between JSON and CF reality and
|
|
23
|
+
// means a site going live doesn't need a deco-start PR.
|
|
13
24
|
//
|
|
14
25
|
// To upgrade compatibility flags, observability, or worker-entry path across
|
|
15
26
|
// every site at once, change this file and tag a new deco-start release.
|
|
@@ -19,6 +30,13 @@
|
|
|
19
30
|
"main": "./src/worker-entry.ts",
|
|
20
31
|
"workers_dev": true,
|
|
21
32
|
"preview_urls": true,
|
|
33
|
+
"kv_namespaces": [
|
|
34
|
+
{ "binding": "SITES_KV", "id": "ad0b74fc4d9341c9af9149c4ab85132f" }
|
|
35
|
+
],
|
|
36
|
+
"version_metadata": { "binding": "CF_VERSION_METADATA" },
|
|
37
|
+
"analytics_engine_datasets": [
|
|
38
|
+
{ "binding": "DECO_METRICS", "dataset": "deco_metrics_$WORKER_UNDERSCORE" }
|
|
39
|
+
],
|
|
22
40
|
"observability": {
|
|
23
41
|
"logs": {
|
|
24
42
|
"enabled": true,
|
package/package.json
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// build-wrangler-config.mjs
|
|
3
3
|
//
|
|
4
|
-
// Materializes a `wrangler.jsonc` from the canonical template
|
|
5
|
-
//
|
|
4
|
+
// Materializes a `wrangler.jsonc` from the canonical template, with
|
|
5
|
+
// $WORKER_* tokens substituted. The output file is what `wrangler deploy` /
|
|
6
6
|
// `wrangler versions upload` / `wrangler secret put` will read.
|
|
7
7
|
//
|
|
8
8
|
// Required env:
|
|
9
9
|
// DECO_START_PATH - path to a checked-out deco-start (e.g. ".deco-start")
|
|
10
|
-
//
|
|
10
|
+
// WORKER_NAME - the Cloudflare worker name (= storefront repo basename
|
|
11
|
+
// by convention; passed by the central CI workflows)
|
|
11
12
|
// OUTPUT_PATH - where to write the merged wrangler.jsonc
|
|
12
13
|
// (e.g. "./wrangler.jsonc" in the caller checkout)
|
|
13
14
|
|
|
14
15
|
import { writeFileSync } from "node:fs";
|
|
15
16
|
import { resolve } from "node:path";
|
|
16
|
-
import {
|
|
17
|
+
import { applyWorkerName, loadTemplate } from "./site-registry.mjs";
|
|
17
18
|
|
|
18
19
|
function fail(message) {
|
|
19
20
|
console.error(`::error::${message}`);
|
|
@@ -21,29 +22,26 @@ function fail(message) {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
const decoStartPath = process.env.DECO_START_PATH;
|
|
24
|
-
const
|
|
25
|
+
const workerName = process.env.WORKER_NAME;
|
|
25
26
|
const outputPath = process.env.OUTPUT_PATH;
|
|
26
27
|
|
|
27
28
|
if (!decoStartPath) fail("DECO_START_PATH env var is required");
|
|
28
|
-
if (!
|
|
29
|
+
if (!workerName) fail("WORKER_NAME env var is required");
|
|
29
30
|
if (!outputPath) fail("OUTPUT_PATH env var is required");
|
|
30
31
|
|
|
31
32
|
let merged;
|
|
32
33
|
try {
|
|
33
|
-
|
|
34
|
-
const manifest = loadSiteManifest(decoStartPath, siteName);
|
|
35
|
-
merged = mergeWithTemplate(template, manifest);
|
|
34
|
+
merged = applyWorkerName(loadTemplate(decoStartPath), workerName);
|
|
36
35
|
} catch (err) {
|
|
37
36
|
fail(err instanceof Error ? err.message : String(err));
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
const header = `// AUTOGENERATED by @decocms/start at deploy time.
|
|
41
40
|
// Do not edit -- changes will be overwritten on the next deploy.
|
|
42
|
-
// Source: decocms/deco-start deploy/
|
|
43
|
-
// + decocms/deco-start deploy/wrangler-template.jsonc
|
|
41
|
+
// Source: decocms/deco-start deploy/wrangler-template.jsonc + worker name "${workerName}"
|
|
44
42
|
`;
|
|
45
43
|
|
|
46
44
|
const body = JSON.stringify(merged, null, 2);
|
|
47
45
|
writeFileSync(resolve(outputPath), `${header}${body}\n`);
|
|
48
46
|
|
|
49
|
-
console.log(`Wrote ${outputPath}
|
|
47
|
+
console.log(`Wrote ${outputPath} (worker "${workerName}")`);
|
|
@@ -1,36 +1,23 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Template loader + token substitution for the canonical wrangler config.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// There is no per-site "registry" anymore: every site's wrangler config is
|
|
4
|
+
// produced from `deploy/wrangler-template.jsonc` plus the worker name (which
|
|
5
|
+
// equals the storefront repo basename by convention). To accommodate fields
|
|
6
|
+
// that must vary deterministically per worker, the template can use these
|
|
7
|
+
// substitution tokens, which are replaced at config-build time:
|
|
7
8
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// $WORKER_NAME -> worker name verbatim (e.g. "als-tanstack")
|
|
10
|
+
// $WORKER_UNDERSCORE -> worker name, `-` -> `_` (e.g. "als_tanstack")
|
|
11
|
+
//
|
|
12
|
+
// Trust model: callers cannot pass a fabricated worker name to the central
|
|
13
|
+
// CI workflows -- the deploy is gated by the `decocms-deployer` GitHub App
|
|
14
|
+
// being installed on the target storefront repo. If the App isn't installed
|
|
15
|
+
// there, the App-token mint fails and the deploy never starts.
|
|
12
16
|
|
|
13
17
|
import { existsSync } from "node:fs";
|
|
14
18
|
import { join } from "node:path";
|
|
15
19
|
import { readJsoncFile } from "./jsonc.mjs";
|
|
16
20
|
|
|
17
|
-
/** @typedef {{
|
|
18
|
-
* worker_name: string;
|
|
19
|
-
* routes?: Array<{ pattern: string; zone_name?: string; custom_domain?: boolean }>;
|
|
20
|
-
* kv_namespaces?: Array<{ binding: string; id: string; preview_id?: string }>;
|
|
21
|
-
* analytics_engine_datasets?: Array<{ binding: string; dataset: string }>;
|
|
22
|
-
* version_metadata?: { binding: string };
|
|
23
|
-
* }} SiteManifest
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
const ALLOWED_SITE_KEYS = new Set([
|
|
27
|
-
"worker_name",
|
|
28
|
-
"routes",
|
|
29
|
-
"kv_namespaces",
|
|
30
|
-
"analytics_engine_datasets",
|
|
31
|
-
"version_metadata",
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
21
|
/**
|
|
35
22
|
* @param {string} decoStartPath
|
|
36
23
|
* @returns {string}
|
|
@@ -39,56 +26,6 @@ export function templatePath(decoStartPath) {
|
|
|
39
26
|
return join(decoStartPath, "deploy", "wrangler-template.jsonc");
|
|
40
27
|
}
|
|
41
28
|
|
|
42
|
-
/**
|
|
43
|
-
* @param {string} decoStartPath
|
|
44
|
-
* @param {string} siteName
|
|
45
|
-
* @returns {string}
|
|
46
|
-
*/
|
|
47
|
-
export function siteManifestPath(decoStartPath, siteName) {
|
|
48
|
-
return join(decoStartPath, "deploy", "sites", `${siteName}.jsonc`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* @param {string} decoStartPath
|
|
53
|
-
* @param {string} siteName
|
|
54
|
-
* @returns {SiteManifest}
|
|
55
|
-
*/
|
|
56
|
-
export function loadSiteManifest(decoStartPath, siteName) {
|
|
57
|
-
if (!siteName || !/^[a-z0-9][a-z0-9-]*$/.test(siteName)) {
|
|
58
|
-
throw new Error(
|
|
59
|
-
`Refusing to load manifest for invalid site name: ${JSON.stringify(siteName)}. Site names must be lowercase, hyphen-separated.`,
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
const path = siteManifestPath(decoStartPath, siteName);
|
|
63
|
-
if (!existsSync(path)) {
|
|
64
|
-
throw new Error(
|
|
65
|
-
`No registry entry for site "${siteName}" at ${path}.\n` +
|
|
66
|
-
`Add deploy/sites/${siteName}.jsonc to decocms/deco-start before deploying.`,
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
const raw = readJsoncFile(path);
|
|
70
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
71
|
-
throw new Error(`Site manifest at ${path} must be a JSON object.`);
|
|
72
|
-
}
|
|
73
|
-
const manifest = /** @type {Record<string, unknown>} */ (raw);
|
|
74
|
-
if (typeof manifest.worker_name !== "string" || manifest.worker_name.length === 0) {
|
|
75
|
-
throw new Error(`Site manifest at ${path} is missing the required "worker_name" string.`);
|
|
76
|
-
}
|
|
77
|
-
if (!/^[a-z0-9][a-z0-9-]*$/.test(/** @type {string} */ (manifest.worker_name))) {
|
|
78
|
-
throw new Error(
|
|
79
|
-
`Site manifest at ${path} has an invalid "worker_name": ${JSON.stringify(manifest.worker_name)}. Use lowercase, hyphen-separated.`,
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
for (const key of Object.keys(manifest)) {
|
|
83
|
-
if (!ALLOWED_SITE_KEYS.has(key)) {
|
|
84
|
-
throw new Error(
|
|
85
|
-
`Site manifest at ${path} contains unsupported key "${key}". Allowed: ${[...ALLOWED_SITE_KEYS].join(", ")}.`,
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return /** @type {SiteManifest} */ (manifest);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
29
|
/**
|
|
93
30
|
* @param {string} decoStartPath
|
|
94
31
|
* @returns {Record<string, unknown>}
|
|
@@ -106,37 +43,53 @@ export function loadTemplate(decoStartPath) {
|
|
|
106
43
|
}
|
|
107
44
|
|
|
108
45
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* itself expects for `routes`, `kv_namespaces`, etc.
|
|
46
|
+
* Recursively replace `$WORKER_*` tokens in any string value of an
|
|
47
|
+
* object/array tree. Returns a new tree.
|
|
112
48
|
*
|
|
113
|
-
* @
|
|
114
|
-
* @param {
|
|
115
|
-
* @
|
|
116
|
-
* @returns {T}
|
|
49
|
+
* @param {unknown} value
|
|
50
|
+
* @param {Record<string, string>} replacements
|
|
51
|
+
* @returns {unknown}
|
|
117
52
|
*/
|
|
118
|
-
function
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
out[k] = deepMerge(out[k], v);
|
|
53
|
+
function substituteTokens(value, replacements) {
|
|
54
|
+
if (typeof value === "string") {
|
|
55
|
+
let out = value;
|
|
56
|
+
for (const [token, repl] of Object.entries(replacements)) {
|
|
57
|
+
out = out.split(token).join(repl);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
126
60
|
}
|
|
127
|
-
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
return value.map((v) => substituteTokens(v, replacements));
|
|
63
|
+
}
|
|
64
|
+
if (value && typeof value === "object") {
|
|
65
|
+
const out = /** @type {Record<string, unknown>} */ ({});
|
|
66
|
+
for (const [k, v] of Object.entries(value)) {
|
|
67
|
+
out[k] = substituteTokens(v, replacements);
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
128
72
|
}
|
|
129
73
|
|
|
130
74
|
/**
|
|
131
|
-
* Produce the wrangler config object by
|
|
132
|
-
* the
|
|
75
|
+
* Produce the wrangler config object by substituting `$WORKER_*` tokens in
|
|
76
|
+
* the template and prepending `name`.
|
|
133
77
|
*
|
|
134
78
|
* @param {Record<string, unknown>} template
|
|
135
|
-
* @param {
|
|
79
|
+
* @param {string} workerName
|
|
136
80
|
* @returns {Record<string, unknown>}
|
|
137
81
|
*/
|
|
138
|
-
export function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
82
|
+
export function applyWorkerName(template, workerName) {
|
|
83
|
+
if (typeof workerName !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(workerName)) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Invalid worker name: ${JSON.stringify(workerName)}. Use lowercase, hyphen-separated.`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const substituted = /** @type {Record<string, unknown>} */ (
|
|
89
|
+
substituteTokens(template, {
|
|
90
|
+
$WORKER_UNDERSCORE: workerName.replace(/-/g, "_"),
|
|
91
|
+
$WORKER_NAME: workerName,
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
return { name: workerName, ...substituted };
|
|
142
95
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Local-dev wrapper around `wrangler` for storefront repos that no longer
|
|
5
5
|
// commit a `wrangler.jsonc`. Generates the canonical config from
|
|
6
|
-
// `@decocms/start`'s
|
|
6
|
+
// `@decocms/start`'s template into `./wrangler.jsonc` (gitignored), then
|
|
7
7
|
// optionally execs the real `wrangler` with that config in cwd.
|
|
8
8
|
//
|
|
9
9
|
// Usage from a customer repo (after `npm i -D @decocms/start@latest`):
|
|
@@ -16,26 +16,25 @@
|
|
|
16
16
|
// npx deco-wrangler --help # passes through to wrangler
|
|
17
17
|
//
|
|
18
18
|
// All non-`gen` invocations also (re)generate ./wrangler.jsonc first so the
|
|
19
|
-
// file is always in sync with the
|
|
19
|
+
// file is always in sync with the template before wrangler runs. The
|
|
20
20
|
// `@cloudflare/vite-plugin` and the `wrangler` CLI both auto-discover
|
|
21
21
|
// ./wrangler.jsonc in cwd, so no extra config plumbing is required.
|
|
22
22
|
//
|
|
23
|
-
//
|
|
24
|
-
// order:
|
|
23
|
+
// Worker name resolves in this order:
|
|
25
24
|
//
|
|
26
|
-
// 1.
|
|
27
|
-
// 2. git remote `origin` URL parsed for the GitHub repo
|
|
25
|
+
// 1. DECO_WORKER_NAME env var (explicit override)
|
|
26
|
+
// 2. git remote `origin` URL parsed for the GitHub repo basename
|
|
28
27
|
// 3. package.json `name` field
|
|
29
28
|
//
|
|
30
|
-
//
|
|
31
|
-
// the
|
|
32
|
-
//
|
|
29
|
+
// By convention worker name == storefront repo basename. The CI deploys use
|
|
30
|
+
// the same convention. Local dev never deploys -- this wrapper is purely for
|
|
31
|
+
// generating a config that matches what the central workflow will produce.
|
|
33
32
|
|
|
34
33
|
import { execSync, spawnSync } from "node:child_process";
|
|
35
34
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
36
35
|
import { dirname, resolve } from "node:path";
|
|
37
36
|
import { fileURLToPath } from "node:url";
|
|
38
|
-
import {
|
|
37
|
+
import { applyWorkerName, loadTemplate } from "./site-registry.mjs";
|
|
39
38
|
|
|
40
39
|
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
41
40
|
// scripts/deploy/wrangler-wrapper.mjs -> deco-start root
|
|
@@ -45,7 +44,7 @@ function eprintln(msg) {
|
|
|
45
44
|
process.stderr.write(`[deco-wrangler] ${msg}\n`);
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
function
|
|
47
|
+
function tryGitRemoteWorkerName() {
|
|
49
48
|
try {
|
|
50
49
|
const url = execSync("git remote get-url origin", {
|
|
51
50
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -69,54 +68,47 @@ function tryPackageJsonName() {
|
|
|
69
68
|
}
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
function
|
|
73
|
-
const envName = process.env.
|
|
74
|
-
if (envName) return { name: envName, source: "
|
|
75
|
-
const gitName =
|
|
71
|
+
function resolveWorkerName() {
|
|
72
|
+
const envName = process.env.DECO_WORKER_NAME?.trim();
|
|
73
|
+
if (envName) return { name: envName, source: "DECO_WORKER_NAME env var" };
|
|
74
|
+
const gitName = tryGitRemoteWorkerName();
|
|
76
75
|
if (gitName) return { name: gitName, source: "git remote origin" };
|
|
77
76
|
const pkgName = tryPackageJsonName();
|
|
78
77
|
if (pkgName) return { name: pkgName, source: "package.json name" };
|
|
79
78
|
return undefined;
|
|
80
79
|
}
|
|
81
80
|
|
|
82
|
-
const resolved =
|
|
81
|
+
const resolved = resolveWorkerName();
|
|
83
82
|
if (!resolved) {
|
|
84
83
|
eprintln(
|
|
85
|
-
"Could not determine
|
|
84
|
+
"Could not determine worker name. Set DECO_WORKER_NAME or run from a repo with a github.com 'origin' remote.",
|
|
86
85
|
);
|
|
87
86
|
process.exit(1);
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
let
|
|
89
|
+
let merged;
|
|
91
90
|
try {
|
|
92
|
-
|
|
91
|
+
merged = applyWorkerName(loadTemplate(DECO_START_PATH), resolved.name);
|
|
93
92
|
} catch (err) {
|
|
94
93
|
eprintln(err instanceof Error ? err.message : String(err));
|
|
95
|
-
eprintln(`
|
|
96
|
-
eprintln(
|
|
97
|
-
`If this is a personal sandbox, set DECO_SITE_NAME=<registered-site> or open a PR to deco-start adding deploy/sites/${resolved.name}.jsonc.`,
|
|
98
|
-
);
|
|
94
|
+
eprintln(`Worker name "${resolved.name}" was inferred from ${resolved.source}.`);
|
|
99
95
|
process.exit(1);
|
|
100
96
|
}
|
|
101
97
|
|
|
102
|
-
const merged = mergeWithTemplate(loadTemplate(DECO_START_PATH), manifest);
|
|
103
|
-
|
|
104
98
|
const outputPath = resolve(process.cwd(), "wrangler.jsonc");
|
|
105
99
|
const header = `// AUTOGENERATED by deco-wrangler -- DO NOT COMMIT.
|
|
106
100
|
// Add wrangler.jsonc to .gitignore. Regenerated on every \`deco-wrangler\` run.
|
|
107
|
-
// Source: @decocms/start deploy/
|
|
101
|
+
// Source: @decocms/start deploy/wrangler-template.jsonc + worker name "${resolved.name}"
|
|
108
102
|
`;
|
|
109
103
|
writeFileSync(outputPath, `${header}${JSON.stringify(merged, null, 2)}\n`);
|
|
110
104
|
|
|
111
|
-
eprintln(
|
|
112
|
-
`Resolved site "${resolved.name}" -> worker "${manifest.worker_name}" (via ${resolved.source})`,
|
|
113
|
-
);
|
|
105
|
+
eprintln(`Resolved worker name "${resolved.name}" (via ${resolved.source})`);
|
|
114
106
|
eprintln(`Generated ${outputPath}`);
|
|
115
107
|
|
|
116
108
|
const argv = process.argv.slice(2);
|
|
117
109
|
// `gen` mode: just write the config and exit. Used by package.json
|
|
118
110
|
// predev/prebuild/prepare hooks to keep wrangler.jsonc in sync with the
|
|
119
|
-
//
|
|
111
|
+
// template without invoking wrangler itself.
|
|
120
112
|
if (argv[0] === "gen" || argv[0] === "generate") {
|
|
121
113
|
process.exit(0);
|
|
122
114
|
}
|