@decocms/start 2.30.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.
@@ -287,6 +287,16 @@ execs the real `wrangler` with that config in cwd.
287
287
  untamperable by user code) and looked up in `deploy/sites/<repo>.jsonc`.
288
288
  - A customer cannot misroute their deploy onto another site's worker
289
289
  because they can't write to `decocms/deco-start` (CODEOWNERS-protected).
290
+ - `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` live ONLY in
291
+ `decocms/deco-start`'s `production` GitHub Environment. The reusable
292
+ workflow declares `environment: production`, which makes
293
+ `${{ secrets.CLOUDFLARE_* }}` resolve from the called repo's environment
294
+ instead of the caller's repo secrets. Storefront repos never hold the
295
+ Cloudflare credential.
296
+ - Per-site runtime secrets in storefront repos are prefixed `SECRET_*` and
297
+ flow into the worker via the central `sync-secrets.yml`, which inherits
298
+ them via `secrets: inherit`, validates names, and `wrangler secret put`s
299
+ them under their unprefixed name.
290
300
 
291
301
  ### Common mistakes (do not do these)
292
302
 
@@ -296,9 +306,14 @@ execs the real `wrangler` with that config in cwd.
296
306
  Add it to `deco-start/.github/workflows/deploy.yml` instead so every site
297
307
  picks it up at once.
298
308
  - **Hard-coding `account_id` in a site's wrangler config.** It comes from
299
- `CLOUDFLARE_ACCOUNT_ID` (org-level GitHub secret in CI; `wrangler login`
300
- locally). Removing it from JSON is the one-way protection against
301
- accidentally deploying to the wrong account.
309
+ `CLOUDFLARE_ACCOUNT_ID` (in `decocms/deco-start`'s `production`
310
+ Environment in CI; `wrangler login` locally). Removing it from JSON is
311
+ the one-way protection against accidentally deploying to the wrong
312
+ account.
313
+ - **Adding `CLOUDFLARE_*` to a storefront repo's secrets.** They belong
314
+ in `decocms/deco-start` only, in the `production` environment. The
315
+ central workflow's `environment:` binding overrides anything passed
316
+ from the caller, so an accidental copy on a site is dead code.
302
317
  - **Setting `worker_name` to anything other than the repo name** without
303
318
  a strong reason. The 1:1 binding makes audit (and incident response)
304
319
  trivial. Exceptions today: `casaevideo-storefront` -> `casaevideo-tanstack`
@@ -88,12 +88,26 @@ boundary: the central workflow ignores caller `inputs:` for identity and
88
88
  derives the site name from `${{ github.repository }}`. `deploy/**` is
89
89
  CODEOWNERS-protected.
90
90
 
91
+ **Cloudflare credentials live in `decocms/deco-start` only**, in the
92
+ `production` GitHub Environment. The reusable workflows declare
93
+ `environment: production`, which makes `${{ secrets.CLOUDFLARE_* }}`
94
+ resolve from the called repo (deco-start) instead of the caller. A
95
+ storefront repo only ever holds its own `SECRET_*` runtime secrets
96
+ (pushed to its worker by the central `sync-secrets.yml` via
97
+ `secrets: inherit`). Two-tier secret model is the second half of the
98
+ trust boundary — even a fully compromised storefront repo has no path
99
+ to the Cloudflare token.
100
+
91
101
  **Agent behavior**: when adding or migrating a site, do **not** generate a
92
102
  per-site `wrangler.jsonc` or per-site `deploy.yml` / `preview.yml` /
93
103
  `sync-secrets.yml` / `regen-blocks.yml`. The site repo gets the four
94
104
  ~5-line caller stubs only. Per-site config goes in a PR to
95
105
  `decocms/deco-start` adding `deploy/sites/<repo>.jsonc`. Do not commit
96
- `wrangler.jsonc` or any wrangler config to a site repo.
106
+ `wrangler.jsonc` or any wrangler config to a site repo. Do **not** add
107
+ `CLOUDFLARE_*` to a storefront repo's secrets — the central workflow's
108
+ `environment:` binding overrides anything passed from the caller, so an
109
+ accidental copy on a site is dead code at best and a credential leak
110
+ risk at worst.
97
111
 
98
112
  ## Priority order
99
113
 
@@ -20,14 +20,16 @@ name: deploy (central)
20
20
  # deploy:
21
21
  # uses: decocms/deco-start/.github/workflows/deploy.yml@v2
22
22
  # secrets: inherit
23
+ #
24
+ # `secrets: inherit` is intentionally tolerated but UNUSED by the deploy job:
25
+ # `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` resolve from the
26
+ # `production` environment in this repo (via `environment:` below), NOT from
27
+ # the caller. That keeps the Cloudflare credential off every storefront repo
28
+ # entirely. The same env binding is repeated in `preview.yml` and
29
+ # `sync-secrets.yml`.
23
30
 
24
31
  on:
25
- workflow_call:
26
- secrets:
27
- CLOUDFLARE_API_TOKEN:
28
- required: true
29
- CLOUDFLARE_ACCOUNT_ID:
30
- required: true
32
+ workflow_call: {}
31
33
 
32
34
  permissions:
33
35
  contents: write
@@ -39,6 +41,11 @@ concurrency:
39
41
  jobs:
40
42
  deploy:
41
43
  runs-on: ubuntu-latest
44
+ # Resolves `secrets.CLOUDFLARE_*` against the `production` environment in
45
+ # decocms/deco-start (NOT against the caller's repo secrets). Setup:
46
+ # decocms/deco-start > Settings > Environments > production >
47
+ # CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID.
48
+ environment: production
42
49
  steps:
43
50
  - name: Checkout caller repo
44
51
  uses: actions/checkout@v4
@@ -20,14 +20,12 @@ name: preview (central)
20
20
  # preview:
21
21
  # uses: decocms/deco-start/.github/workflows/preview.yml@v2
22
22
  # secrets: inherit
23
+ #
24
+ # Same secret model as deploy.yml: `CLOUDFLARE_*` resolve from this repo's
25
+ # `production` environment, not from the caller. See deploy.yml for details.
23
26
 
24
27
  on:
25
- workflow_call:
26
- secrets:
27
- CLOUDFLARE_API_TOKEN:
28
- required: true
29
- CLOUDFLARE_ACCOUNT_ID:
30
- required: true
28
+ workflow_call: {}
31
29
 
32
30
  permissions:
33
31
  contents: read
@@ -41,6 +39,7 @@ concurrency:
41
39
  jobs:
42
40
  preview:
43
41
  runs-on: ubuntu-latest
42
+ environment: production
44
43
  steps:
45
44
  - name: Resolve ref
46
45
  id: resolve
@@ -27,6 +27,17 @@ name: sync-secrets (central)
27
27
  # with:
28
28
  # mode: ${{ inputs.mode }}
29
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).
30
41
 
31
42
  on:
32
43
  workflow_call:
@@ -36,11 +47,6 @@ on:
36
47
  required: false
37
48
  type: string
38
49
  default: "dry-run"
39
- secrets:
40
- CLOUDFLARE_API_TOKEN:
41
- required: true
42
- CLOUDFLARE_ACCOUNT_ID:
43
- required: true
44
50
 
45
51
  permissions:
46
52
  contents: read
@@ -52,6 +58,7 @@ concurrency:
52
58
  jobs:
53
59
  sync:
54
60
  runs-on: ubuntu-latest
61
+ environment: production
55
62
  steps:
56
63
  - uses: actions/checkout@v4
57
64
 
@@ -118,6 +118,7 @@ this plan.
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
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. |
121
122
 
122
123
  The full text of the constitutional rule (loaded into every agent
123
124
  session for this repo) lives at
package/deploy/README.md CHANGED
@@ -28,6 +28,29 @@ no other way to identify a site.
28
28
  `deploy/**` is CODEOWNERS-protected. Only the platform team can change site
29
29
  manifests or the template.
30
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
+
31
54
  ## How wrangler.jsonc is generated
32
55
 
33
56
  At deploy time, the central workflow runs
@@ -55,8 +78,11 @@ which:
55
78
  `@v2` major tag (the major-tag advance step lives inline in
56
79
  [`.github/workflows/release.yml`](../.github/workflows/release.yml)).
57
80
  3. In the new repo, add the four caller workflows from
58
- [`.github/workflows/`](../.github/workflows/) and set the org-level
59
- `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` GitHub secrets.
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.
60
86
  4. Push to `main` and verify the deploy lands on the right worker.
61
87
 
62
88
  ## Per-site override schema
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "2.30.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",