@decocms/start 2.29.0 → 2.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/.agents/skills/deco-to-tanstack-migration/SKILL.md +1 -1
  2. package/.agents/skills/deco-to-tanstack-migration/references/vtex-commerce.md +5 -1
  3. package/.agents/skills/deco-to-tanstack-migration/references/worker-cloudflare.md +122 -10
  4. package/.agents/skills/deco-to-tanstack-migration/templates/package-json.md +5 -1
  5. package/.cursor/rules/migration-tooling-policy.mdc +36 -2
  6. package/.github/workflows/deploy.yml +122 -0
  7. package/.github/workflows/preview.yml +142 -0
  8. package/.github/workflows/regen-blocks.yml +56 -0
  9. package/.github/workflows/release.yml +26 -0
  10. package/.github/workflows/sync-secrets.yml +180 -0
  11. package/CODEOWNERS +16 -0
  12. package/MIGRATION_TOOLING_PLAN.md +17 -4
  13. package/deploy/README.md +111 -0
  14. package/deploy/sites/als-tanstack.jsonc +7 -0
  15. package/deploy/sites/americanas-tanstack.jsonc +4 -0
  16. package/deploy/sites/baggagio-tanstack.jsonc +4 -0
  17. package/deploy/sites/casaevideo-storefront.jsonc +11 -0
  18. package/deploy/sites/lebiscuit-tanstack.jsonc +19 -0
  19. package/deploy/sites/miess-01-tanstack.jsonc +8 -0
  20. package/deploy/wrangler-template.jsonc +28 -0
  21. package/package.json +3 -2
  22. package/scripts/deploy/build-wrangler-config.mjs +49 -0
  23. package/scripts/deploy/jsonc.mjs +76 -0
  24. package/scripts/deploy/resolve-site.mjs +58 -0
  25. package/scripts/deploy/site-registry.mjs +142 -0
  26. package/scripts/deploy/wrangler-wrapper.mjs +126 -0
  27. package/scripts/migrate/phase-scaffold.ts +13 -3
  28. package/scripts/migrate/phase-verify.ts +6 -1
  29. package/scripts/migrate/templates/github-workflows.ts +98 -0
  30. package/scripts/migrate/templates/package-json.ts +9 -2
  31. package/scripts/migrate/templates/wrangler.ts +0 -30
@@ -0,0 +1,180 @@
1
+ name: sync-secrets (central)
2
+
3
+ # Reusable workflow. Reconciles the caller's `SECRET_*` GitHub repo secrets
4
+ # with the Cloudflare Worker's runtime secrets (with the prefix stripped).
5
+ #
6
+ # Examples:
7
+ # SECRET_STRIPE_KEY in GitHub -> STRIPE_KEY on the worker
8
+ #
9
+ # Defaults to dry-run; the caller must pass `mode: apply` to actually write.
10
+ # Orphans on the worker (present on worker but not in GitHub as SECRET_*) are
11
+ # warned about, never deleted.
12
+ #
13
+ # Caller usage (in customer repo, `.github/workflows/sync-secrets.yml`):
14
+ #
15
+ # on:
16
+ # workflow_dispatch:
17
+ # inputs:
18
+ # mode:
19
+ # description: "dry-run | apply"
20
+ # required: true
21
+ # default: "dry-run"
22
+ # type: choice
23
+ # options: [dry-run, apply]
24
+ # jobs:
25
+ # sync:
26
+ # uses: decocms/deco-start/.github/workflows/sync-secrets.yml@v2
27
+ # with:
28
+ # mode: ${{ inputs.mode }}
29
+ # secrets: inherit
30
+ #
31
+ # Two distinct secret classes flow through this job:
32
+ # - `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` -> resolved from this
33
+ # repo's `production` environment (via `environment:` below). NEVER from
34
+ # the caller storefront. Same model as deploy.yml / preview.yml.
35
+ # - `SECRET_*` -> inherited from the caller storefront via `secrets: inherit`
36
+ # in the caller stub. These ARE site-owned and get pushed to the worker
37
+ # as runtime secrets (with the `SECRET_` prefix stripped).
38
+ # `secrets: inherit` is therefore REQUIRED on the caller side for this
39
+ # workflow specifically (unlike deploy.yml / preview.yml where it's just
40
+ # tolerated).
41
+
42
+ on:
43
+ workflow_call:
44
+ inputs:
45
+ mode:
46
+ description: "dry-run = print diff only | apply = set secrets on worker"
47
+ required: false
48
+ type: string
49
+ default: "dry-run"
50
+
51
+ permissions:
52
+ contents: read
53
+
54
+ concurrency:
55
+ group: sync-secrets-${{ github.repository }}
56
+ cancel-in-progress: false
57
+
58
+ jobs:
59
+ sync:
60
+ runs-on: ubuntu-latest
61
+ environment: production
62
+ steps:
63
+ - uses: actions/checkout@v4
64
+
65
+ - name: Resolve deco-start ref + site identity
66
+ id: meta
67
+ run: |
68
+ WF_REF="${{ github.workflow_ref }}"
69
+ REF="${WF_REF##*@}"
70
+ REF="${REF#refs/tags/}"
71
+ REF="${REF#refs/heads/}"
72
+ echo "deco_start_ref=$REF" >> "$GITHUB_OUTPUT"
73
+ echo "site_name=${GITHUB_REPOSITORY#*/}" >> "$GITHUB_OUTPUT"
74
+
75
+ - name: Checkout deco-start registry at the same ref
76
+ uses: actions/checkout@v4
77
+ with:
78
+ repository: decocms/deco-start
79
+ ref: ${{ steps.meta.outputs.deco_start_ref }}
80
+ path: .deco-start
81
+
82
+ - uses: actions/setup-node@v4
83
+ with:
84
+ node-version: 22
85
+
86
+ - name: Resolve site manifest
87
+ id: site
88
+ run: node .deco-start/scripts/deploy/resolve-site.mjs
89
+ env:
90
+ DECO_START_PATH: .deco-start
91
+ SITE_NAME: ${{ steps.meta.outputs.site_name }}
92
+
93
+ - name: Generate wrangler.jsonc
94
+ run: node .deco-start/scripts/deploy/build-wrangler-config.mjs
95
+ env:
96
+ DECO_START_PATH: .deco-start
97
+ SITE_NAME: ${{ steps.meta.outputs.site_name }}
98
+ OUTPUT_PATH: ./wrangler.jsonc
99
+
100
+ - name: Install wrangler
101
+ run: npm install --no-save wrangler@4
102
+
103
+ - name: Plan and (optionally) apply
104
+ env:
105
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
106
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
107
+ ALL_SECRETS: ${{ toJSON(secrets) }}
108
+ MODE: ${{ inputs.mode }}
109
+ run: |
110
+ set -euo pipefail
111
+
112
+ # Build desired-state map from SECRET_* entries.
113
+ desired_json=$(printf '%s' "$ALL_SECRETS" | jq '
114
+ to_entries
115
+ | map(select(.key | startswith("SECRET_")))
116
+ | map({ key: (.key | sub("^SECRET_"; "")), value: .value })
117
+ | from_entries
118
+ ')
119
+
120
+ desired_names=$(printf '%s' "$desired_json" | jq -r 'keys[]' | sort)
121
+
122
+ # Validate names before any side effect.
123
+ while IFS= read -r name; do
124
+ [ -z "$name" ] && continue
125
+ if ! [[ "$name" =~ ^[A-Z][A-Z0-9_]{0,63}$ ]]; then
126
+ echo "::error::Invalid worker secret name derived from SECRET_${name}: must match ^[A-Z][A-Z0-9_]{0,63}$"
127
+ exit 1
128
+ fi
129
+ done <<< "$desired_names"
130
+
131
+ # Snapshot what's currently on the worker.
132
+ existing=$(npx wrangler secret list --format=json | jq -r '.[].name' | sort)
133
+
134
+ # Diff
135
+ to_set="$desired_names"
136
+ orphans=$(comm -23 <(printf '%s\n' "$existing") <(printf '%s\n' "$desired_names") || true)
137
+
138
+ {
139
+ echo "## Diff"
140
+ echo ""
141
+ echo "### Will set (from SECRET_* in GitHub):"
142
+ if [ -z "$to_set" ]; then
143
+ echo " (none)"
144
+ else
145
+ echo "$to_set" | sed 's/^/ + /'
146
+ fi
147
+ echo ""
148
+ echo "### Orphans (on worker, not in GitHub as SECRET_*):"
149
+ if [ -z "$orphans" ]; then
150
+ echo " (none)"
151
+ else
152
+ echo "$orphans" | sed 's/^/ ! /'
153
+ echo ""
154
+ echo " These will NOT be deleted automatically. To remove manually:"
155
+ echo "$orphans" | sed 's|^| npx wrangler secret delete "|; s|$|" --force|'
156
+ fi
157
+ } | tee -a "$GITHUB_STEP_SUMMARY"
158
+
159
+ if [ -n "$orphans" ]; then
160
+ echo "::warning::${orphans//$'\n'/, } exist on the worker but not in GitHub as SECRET_*. Not deleting."
161
+ fi
162
+
163
+ if [ "$MODE" = "dry-run" ]; then
164
+ echo "::notice::dry-run mode -- no changes applied"
165
+ exit 0
166
+ fi
167
+
168
+ # Apply
169
+ if [ -z "$to_set" ]; then
170
+ echo "Nothing to apply."
171
+ exit 0
172
+ fi
173
+
174
+ printf '%s' "$desired_json" | jq -r 'to_entries[] | "\(.key)\t\(.value)"' \
175
+ | while IFS=$'\t' read -r name value; do
176
+ echo "Setting $name..."
177
+ printf '%s' "$value" | npx wrangler secret put "$name"
178
+ done
179
+
180
+ echo "::notice::Applied $(echo "$to_set" | wc -l | tr -d ' ') secrets."
package/CODEOWNERS ADDED
@@ -0,0 +1,16 @@
1
+ # CODEOWNERS for decocms/deco-start
2
+ #
3
+ # `deploy/` is the trust boundary for the central deploy pipeline. The files
4
+ # under `deploy/sites/` immutably bind a customer repo to a Cloudflare worker;
5
+ # `deploy/wrangler-template.jsonc` is the canonical wrangler config every site
6
+ # inherits. A bad PR here can misroute a deploy or change every site's runtime
7
+ # configuration in one shot. Only the platform team approves changes.
8
+ #
9
+ # The central reusable workflows are in the same trust boundary: they decide
10
+ # how every site is built and deployed.
11
+ deploy/ @vibe-dex
12
+ .github/workflows/deploy.yml @vibe-dex
13
+ .github/workflows/preview.yml @vibe-dex
14
+ .github/workflows/sync-secrets.yml @vibe-dex
15
+ .github/workflows/regen-blocks.yml @vibe-dex
16
+ scripts/deploy/ @vibe-dex
@@ -117,6 +117,8 @@ this plan.
117
117
  | 2026-05-01 | **D3 — Stub generation: throw at runtime (Option C)** | Migration-time stubs throw with a clear pointer to the canonical replacement instead of silently identity-casting. Forces audit `--fix` to cover swap cases (no permanent detect-only state) and skills to keep up with stub generation. |
118
118
  | 2026-05-01 | **D4 — Site-local apps: local by default, promote at 3** | Site-specific apps live in `src/apps/local/` until ≥3 sites use them, then promote to `@decocms/apps`. |
119
119
  | 2026-05-01 | **D5 — Failed migrations: rm -rf and re-run** | No `--restart` mode. Half-migrated sites are throwaways. Failure modes get documented in skills, not encoded as escape hatches. |
120
+ | 2026-05-07 | **D6 — Deploy / preview / secrets pipelines: centralize in `deco-start`** | At 6 sites the "1-minute copy" of `deploy.yml` / `preview.yml` / `wrangler.jsonc` had already produced unintended drift (lebiscuit missing 2 workflows, miess missing `account_id`, casaevideo's `loadtest:tail` worker name out of sync with its wrangler config). All sites now consume reusable workflows from `decocms/deco-start@v2` and a per-site registry under [`deploy/sites/<repo>.jsonc`](./deploy/) deep-merged on top of [`deploy/wrangler-template.jsonc`](./deploy/wrangler-template.jsonc) at deploy time. Customer repos hold only ~5-line caller workflows; `wrangler.jsonc` is generated and gitignored. The repo→worker binding is the trust boundary that prevents one site's commits from misrouting onto another site's worker (the central workflow ignores caller `inputs:` for identity and derives the site name from `${{ github.repository }}`). See [`deploy/README.md`](./deploy/README.md) for the contract. |
121
+ | 2026-05-07 | **D6.1 — Cloudflare credentials never leave `deco-start`** | Same-day refinement of D6 after the first central deploy on `baggagio-tanstack` failed with `Secret CLOUDFLARE_API_TOKEN is required, but not provided while calling`. The original D6 design used `secrets: inherit` from the storefront stub and required `CLOUDFLARE_*` to live in the `deco-sites` org, which broke the principle that *the only secrets a storefront repo holds are the secrets that go into wrangler secrets, not the ones used to deploy*. Refinement: the central `deploy.yml` / `preview.yml` / `sync-secrets.yml` jobs declare `environment: production`, which makes `${{ secrets.CLOUDFLARE_* }}` resolve from the called repo (`decocms/deco-start`'s `production` GitHub Environment) instead of the caller's repo secrets. Storefronts now hold only their own `SECRET_*` runtime secrets; `sync-secrets` inherits those via `secrets: inherit` and pushes them as worker secrets. The trust boundary becomes two-tier: even a fully compromised storefront repo has no path to the Cloudflare credential, and even a compromised storefront cannot misroute *its own* deploy onto another site's worker because `worker_name` comes from the CODEOWNERS-protected registry, not caller input. See [`deploy/README.md`](./deploy/README.md) "Where Cloudflare credentials live" section. |
120
122
 
121
123
  The full text of the constitutional rule (loaded into every agent
122
124
  session for this repo) lives at
@@ -1669,10 +1671,21 @@ props. One broken section never takes the page down.
1669
1671
  release picks up the framework export — TODO is checked into
1670
1672
  `src/lib/http-utils.ts`.
1671
1673
  - **CI/CD porting from casaevideo to a new TanStack site is a 1-minute
1672
- copy.** `deploy.yml`, `preview.yml`, `regen-blocks.yml`, plus
1673
- `wrangler.jsonc` worker `name` rename, plus `account_id` paste from
1674
- another site. No template needed yet — three sites is too few. Will
1675
- template if a fourth migration needs it.
1674
+ copy** (when this note was written, at 3 sites). By 6 sites
1675
+ the copy-paste had drifted: lebiscuit was missing
1676
+ `regen-blocks.yml` and `sync-secrets.yml`, miess was missing
1677
+ `regen-blocks.yml` and its `wrangler.jsonc` lacked `account_id`,
1678
+ lebiscuit's preview workflow swallowed `wrangler` exit codes, and
1679
+ casaevideo's `loadtest:tail` referenced a worker name that didn't
1680
+ match its `wrangler.jsonc`. **Resolved 2026-05-07 via D6:** all
1681
+ workflows + wrangler config are centralized in
1682
+ [`deco-start/.github/workflows/`](./.github/workflows/) and
1683
+ [`deco-start/deploy/`](./deploy/). New-site onboarding is now: open a
1684
+ PR adding `deploy/sites/<repo>.jsonc` to deco-start, then drop the
1685
+ ~5-line caller workflows into the new site's repo. No `wrangler.jsonc`
1686
+ is committed to the site repo — `deco-wrangler gen`
1687
+ (a `bin` shipped from `@decocms/start`) materializes it from the
1688
+ central registry on demand for local dev and CI alike.
1676
1689
 
1677
1690
  #### Counter-evidence the user-rule asks for
1678
1691
 
@@ -0,0 +1,111 @@
1
+ # `deploy/` — central deploy registry
2
+
3
+ This directory is the single source of truth for **what gets deployed where**
4
+ across every storefront on the platform. It is consumed by the reusable GitHub
5
+ workflows under [`.github/workflows/`](../.github/workflows/) (`deploy.yml`,
6
+ `preview.yml`, `sync-secrets.yml`) and by the local `deco-wrangler` CLI.
7
+
8
+ ## Files
9
+
10
+ | File | Purpose |
11
+ |------|---------|
12
+ | `wrangler-template.jsonc` | Canonical wrangler config that every site inherits. Compatibility flags, worker-entry path, observability — everything that is the same for every site. |
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.
18
+
19
+ ## Trust model
20
+
21
+ - Customer caller workflows pass **no inputs** to the central reusable workflow.
22
+ - The central workflow derives the site name from `${{ github.repository }}`
23
+ (set by GitHub, untamperable by user code) and looks up
24
+ `sites/<repo-name>.jsonc` from this registry.
25
+ - A customer cannot misroute a deploy onto another customer's worker because
26
+ they can't write to `decocms/deco-start`.
27
+
28
+ `deploy/**` is CODEOWNERS-protected. Only the platform team can change site
29
+ manifests or the template.
30
+
31
+ ### Where Cloudflare credentials live
32
+
33
+ `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` live in **this repo's
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 |
47
+ |---|---|---|
48
+ | `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` | this repo's `production` environment | central workflow's `environment:` binding (no caller passthrough) |
49
+ | `SECRET_*` runtime secrets (per site) | each storefront repo | `sync-secrets.yml` inherits via `secrets: inherit` and pushes via `wrangler secret put` |
50
+
51
+ To rotate the Cloudflare credentials, edit the environment in this repo only.
52
+ No storefront PR needed.
53
+
54
+ ## How wrangler.jsonc is generated
55
+
56
+ At deploy time, the central workflow runs
57
+ [`scripts/deploy/build-wrangler-config.mjs`](../scripts/deploy/build-wrangler-config.mjs),
58
+ which:
59
+
60
+ 1. Loads `deploy/wrangler-template.jsonc` (canonical defaults).
61
+ 2. Loads `deploy/sites/<site>.jsonc` (per-site overrides).
62
+ 3. Deep-merges: site overrides win. `worker_name` becomes wrangler's `name`.
63
+ Arrays are replaced, not concatenated.
64
+ 4. Writes the result to `./wrangler.jsonc` in the caller checkout.
65
+
66
+ `account_id` is never written to JSON — wrangler reads it from
67
+ `CLOUDFLARE_ACCOUNT_ID` (env var in CI; `wrangler login` locally).
68
+
69
+ ## Adding a new site
70
+
71
+ 1. Open a PR to this repo adding `deploy/sites/<new-repo>.jsonc`:
72
+ ```jsonc
73
+ {
74
+ "worker_name": "<new-repo>" // can differ from repo name if needed
75
+ }
76
+ ```
77
+ 2. After merge, the next `v2.x.y` semantic-release publish auto-moves the
78
+ `@v2` major tag (the major-tag advance step lives inline in
79
+ [`.github/workflows/release.yml`](../.github/workflows/release.yml)).
80
+ 3. In the new repo, add the four caller workflows from
81
+ [`.github/workflows/`](../.github/workflows/). **Do not** add
82
+ `CLOUDFLARE_*` to the storefront — those live in this repo's `production`
83
+ environment and reach the runner via the central workflow's `environment:`
84
+ binding. The storefront only needs `SECRET_*` entries for its own worker
85
+ runtime secrets.
86
+ 4. Push to `main` and verify the deploy lands on the right worker.
87
+
88
+ ## Per-site override schema
89
+
90
+ ```jsonc
91
+ {
92
+ "worker_name": "string (required, immutable)",
93
+ "routes": [ // optional
94
+ { "pattern": "www.example.com/*", "zone_name": "decocdn.com" }
95
+ ],
96
+ "kv_namespaces": [ // optional
97
+ { "binding": "SITES_KV", "id": "<cf-kv-id>" }
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.
@@ -0,0 +1,7 @@
1
+ // Per-site overrides for `als-tanstack` (immutable repo->worker binding).
2
+ // Renaming `worker_name` is a breaking change: it points the next deploy at a
3
+ // different Cloudflare worker. Only the platform team can change this file
4
+ // (CODEOWNERS-protected).
5
+ {
6
+ "worker_name": "als-tanstack"
7
+ }
@@ -0,0 +1,4 @@
1
+ // Per-site overrides for `americanas-tanstack`.
2
+ {
3
+ "worker_name": "americanas-tanstack"
4
+ }
@@ -0,0 +1,4 @@
1
+ // Per-site overrides for `baggagio-tanstack`.
2
+ {
3
+ "worker_name": "baggagio-tanstack"
4
+ }
@@ -0,0 +1,11 @@
1
+ // Per-site overrides for `casaevideo-storefront`.
2
+ //
3
+ // NOTE: `kv_namespaces[].id` is shared with `lebiscuit-tanstack`. Verify this
4
+ // is intentional before promoting this registry to v1. R4 in the central-deploy
5
+ // plan adds a CI check that flags shared KV ids as a copy-paste smell.
6
+ {
7
+ "worker_name": "casaevideo-tanstack",
8
+ "kv_namespaces": [
9
+ { "binding": "SITES_KV", "id": "ad0b74fc4d9341c9af9149c4ab85132f" }
10
+ ]
11
+ }
@@ -0,0 +1,19 @@
1
+ // Per-site overrides for `lebiscuit-tanstack`.
2
+ //
3
+ // NOTE: `kv_namespaces[].id` is shared with `casaevideo-storefront`. See the
4
+ // note in casaevideo's site file. R4 will add CI validation for this.
5
+ //
6
+ // `version_metadata` and `analytics_engine_datasets` are consumed by
7
+ // @decocms/start's `instrumentWorker`:
8
+ // - version_metadata populates `service.version` on the OTel resource.
9
+ // - analytics_engine_datasets is the dual-emit metrics target.
10
+ {
11
+ "worker_name": "lebiscuit-tanstack",
12
+ "kv_namespaces": [
13
+ { "binding": "SITES_KV", "id": "ad0b74fc4d9341c9af9149c4ab85132f" }
14
+ ],
15
+ "version_metadata": { "binding": "CF_VERSION_METADATA" },
16
+ "analytics_engine_datasets": [
17
+ { "binding": "DECO_METRICS", "dataset": "deco_metrics_lebiscuit" }
18
+ ]
19
+ }
@@ -0,0 +1,8 @@
1
+ // Per-site overrides for `miess-01-tanstack`.
2
+ //
3
+ // Worker name keeps the historical `miess-tanstack` (no "-01") so the deploy
4
+ // continues to land on the existing Cloudflare worker. The repo name has the
5
+ // `-01-` suffix; the worker name does not.
6
+ {
7
+ "worker_name": "miess-tanstack"
8
+ }
@@ -0,0 +1,28 @@
1
+ // Canonical wrangler.jsonc template for every storefront on the platform.
2
+ //
3
+ // Per-site overrides live in `deploy/sites/<repo-name>.jsonc`. The deploy
4
+ // workflow deep-merges site overrides on top of this template at runtime and
5
+ // writes the result to `wrangler.jsonc` in the caller checkout. Site overrides
6
+ // always win. Arrays are replaced (not concatenated).
7
+ //
8
+ // Notes on what is INTENTIONALLY missing here:
9
+ // - "name" -- always derived from the per-site `worker_name`.
10
+ // - "account_id" -- never lives in the JSON. Wrangler reads it from the
11
+ // CLOUDFLARE_ACCOUNT_ID env var in CI and from local config otherwise.
12
+ // This way, a typo in JSON cannot misroute a deploy.
13
+ //
14
+ // To upgrade compatibility flags, observability, or worker-entry path across
15
+ // every site at once, change this file and tag a new deco-start release.
16
+ {
17
+ "compatibility_date": "2026-02-14",
18
+ "compatibility_flags": ["nodejs_compat", "no_handle_cross_request_promise_resolution"],
19
+ "main": "./src/worker-entry.ts",
20
+ "workers_dev": true,
21
+ "preview_urls": true,
22
+ "observability": {
23
+ "logs": {
24
+ "enabled": true,
25
+ "invocation_logs": true
26
+ }
27
+ }
28
+ }
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "2.29.0",
3
+ "version": "2.30.1",
4
4
  "type": "module",
5
5
  "description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
6
6
  "main": "./src/index.ts",
7
7
  "bin": {
8
8
  "deco-migrate": "./scripts/migrate.ts",
9
9
  "deco-post-cleanup": "./scripts/migrate-post-cleanup.ts",
10
- "deco-htmx-analyze": "./scripts/htmx-analyze.ts"
10
+ "deco-htmx-analyze": "./scripts/htmx-analyze.ts",
11
+ "deco-wrangler": "./scripts/deploy/wrangler-wrapper.mjs"
11
12
  },
12
13
  "exports": {
13
14
  ".": "./src/index.ts",
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ // build-wrangler-config.mjs
3
+ //
4
+ // Materializes a `wrangler.jsonc` from the canonical template + the site's
5
+ // per-site overrides. The output file is what `wrangler deploy` /
6
+ // `wrangler versions upload` / `wrangler secret put` will read.
7
+ //
8
+ // Required env:
9
+ // DECO_START_PATH - path to a checked-out deco-start (e.g. ".deco-start")
10
+ // SITE_NAME - the registry key (== caller repo name)
11
+ // OUTPUT_PATH - where to write the merged wrangler.jsonc
12
+ // (e.g. "./wrangler.jsonc" in the caller checkout)
13
+
14
+ import { writeFileSync } from "node:fs";
15
+ import { resolve } from "node:path";
16
+ import { loadSiteManifest, loadTemplate, mergeWithTemplate } from "./site-registry.mjs";
17
+
18
+ function fail(message) {
19
+ console.error(`::error::${message}`);
20
+ process.exit(1);
21
+ }
22
+
23
+ const decoStartPath = process.env.DECO_START_PATH;
24
+ const siteName = process.env.SITE_NAME;
25
+ const outputPath = process.env.OUTPUT_PATH;
26
+
27
+ if (!decoStartPath) fail("DECO_START_PATH env var is required");
28
+ if (!siteName) fail("SITE_NAME env var is required");
29
+ if (!outputPath) fail("OUTPUT_PATH env var is required");
30
+
31
+ let merged;
32
+ try {
33
+ const template = loadTemplate(decoStartPath);
34
+ const manifest = loadSiteManifest(decoStartPath, siteName);
35
+ merged = mergeWithTemplate(template, manifest);
36
+ } catch (err) {
37
+ fail(err instanceof Error ? err.message : String(err));
38
+ }
39
+
40
+ const header = `// AUTOGENERATED by @decocms/start at deploy time.
41
+ // Do not edit -- changes will be overwritten on the next deploy.
42
+ // Source: decocms/deco-start deploy/sites/${siteName}.jsonc
43
+ // + decocms/deco-start deploy/wrangler-template.jsonc
44
+ `;
45
+
46
+ const body = JSON.stringify(merged, null, 2);
47
+ writeFileSync(resolve(outputPath), `${header}${body}\n`);
48
+
49
+ console.log(`Wrote ${outputPath} for site "${siteName}" (worker "${merged.name}")`);
@@ -0,0 +1,76 @@
1
+ // Minimal JSONC -> JSON parser used by the deploy scripts.
2
+ //
3
+ // Strips // line comments and /* block comments */ outside of string literals
4
+ // and tolerates trailing commas in objects/arrays. Dependency-free so the
5
+ // deploy scripts can run with vanilla `node` in CI.
6
+
7
+ import { readFileSync } from "node:fs";
8
+
9
+ /**
10
+ * @param {string} input
11
+ * @returns {string}
12
+ */
13
+ function stripComments(input) {
14
+ let out = "";
15
+ let i = 0;
16
+ let inStr = false;
17
+ let strChar = "";
18
+ while (i < input.length) {
19
+ const c = input[i];
20
+ const n = input[i + 1];
21
+ if (inStr) {
22
+ out += c;
23
+ if (c === "\\" && i + 1 < input.length) {
24
+ out += input[i + 1];
25
+ i += 2;
26
+ continue;
27
+ }
28
+ if (c === strChar) inStr = false;
29
+ i++;
30
+ continue;
31
+ }
32
+ if (c === '"') {
33
+ inStr = true;
34
+ strChar = c;
35
+ out += c;
36
+ i++;
37
+ continue;
38
+ }
39
+ if (c === "/" && n === "/") {
40
+ while (i < input.length && input[i] !== "\n") i++;
41
+ continue;
42
+ }
43
+ if (c === "/" && n === "*") {
44
+ i += 2;
45
+ while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++;
46
+ i += 2;
47
+ continue;
48
+ }
49
+ out += c;
50
+ i++;
51
+ }
52
+ return out;
53
+ }
54
+
55
+ /**
56
+ * @param {string} text
57
+ * @returns {unknown}
58
+ */
59
+ export function parseJsonc(text) {
60
+ const stripped = stripComments(text).replace(/,\s*([}\]])/g, "$1");
61
+ return JSON.parse(stripped);
62
+ }
63
+
64
+ /**
65
+ * @param {string} path
66
+ * @returns {unknown}
67
+ */
68
+ export function readJsoncFile(path) {
69
+ const raw = readFileSync(path, "utf8");
70
+ try {
71
+ return parseJsonc(raw);
72
+ } catch (err) {
73
+ const message = err instanceof Error ? err.message : String(err);
74
+ throw new Error(`Failed to parse JSONC at ${path}: ${message}`);
75
+ }
76
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ // resolve-site.mjs
3
+ //
4
+ // Validates that the calling repository has a registered site manifest in
5
+ // deco-start, and emits the resolved fields to GITHUB_OUTPUT for downstream
6
+ // steps (wrangler tail, status comments, etc.).
7
+ //
8
+ // Required env:
9
+ // DECO_START_PATH - path to a checked-out deco-start (e.g. ".deco-start")
10
+ // SITE_NAME - typically `${GITHUB_REPOSITORY#*/}` set by the workflow
11
+ //
12
+ // Optional env:
13
+ // GITHUB_OUTPUT - if set, emit `key=value` lines here (CI mode).
14
+ // If unset, prints a human-readable summary to stdout.
15
+
16
+ import { appendFileSync } from "node:fs";
17
+ import { loadSiteManifest } from "./site-registry.mjs";
18
+
19
+ function fail(message) {
20
+ console.error(`::error::${message}`);
21
+ process.exit(1);
22
+ }
23
+
24
+ const decoStartPath = process.env.DECO_START_PATH;
25
+ const siteName = process.env.SITE_NAME;
26
+ const ghOutput = process.env.GITHUB_OUTPUT;
27
+
28
+ if (!decoStartPath) fail("DECO_START_PATH env var is required");
29
+ if (!siteName) fail("SITE_NAME env var is required");
30
+
31
+ let manifest;
32
+ try {
33
+ manifest = loadSiteManifest(decoStartPath, siteName);
34
+ } catch (err) {
35
+ fail(err instanceof Error ? err.message : String(err));
36
+ }
37
+
38
+ const summary = {
39
+ site_name: siteName,
40
+ worker_name: manifest.worker_name,
41
+ has_routes: String(Array.isArray(manifest.routes) && manifest.routes.length > 0),
42
+ has_kv: String(Array.isArray(manifest.kv_namespaces) && manifest.kv_namespaces.length > 0),
43
+ has_analytics: String(
44
+ Array.isArray(manifest.analytics_engine_datasets) &&
45
+ manifest.analytics_engine_datasets.length > 0,
46
+ ),
47
+ has_version_metadata: String(Boolean(manifest.version_metadata)),
48
+ };
49
+
50
+ if (ghOutput) {
51
+ const lines = Object.entries(summary).map(([k, v]) => `${k}=${v}`);
52
+ appendFileSync(ghOutput, `${lines.join("\n")}\n`);
53
+ }
54
+
55
+ console.log(`Resolved site "${siteName}" -> worker "${manifest.worker_name}"`);
56
+ for (const [k, v] of Object.entries(summary)) {
57
+ console.log(` ${k}: ${v}`);
58
+ }