@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.
@@ -1,118 +1,168 @@
1
1
  name: preview (central)
2
2
 
3
- # Reusable workflow that uploads a preview version (alias) for any storefront
4
- # repo registered under `deploy/sites/<repo-name>.jsonc`.
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.
5
7
  #
6
- # Caller usage (in customer repo, `.github/workflows/preview.yml`):
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`):
7
13
  #
8
14
  # on:
9
- # repository_dispatch:
10
- # types: [preview-deploy]
11
15
  # pull_request:
12
16
  # types: [opened, synchronize, reopened]
13
17
  # push:
14
18
  # branches: ['env/**']
15
19
  # permissions:
16
20
  # contents: read
17
- # pull-requests: write
18
- # statuses: write
19
21
  # jobs:
20
- # preview:
21
- # uses: decocms/deco-start/.github/workflows/preview.yml@v2
22
- # secrets: inherit
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 || '' }}
23
53
  #
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.
54
+ # Note on forks: pull_request runs from forked repos cannot access repo
55
+ # secrets (incl. DECOCMS_DEPLOYER_APP_*), so they cannot trigger previews.
26
56
 
27
57
  on:
28
- workflow_call: {}
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: ""
29
82
 
30
83
  permissions:
31
84
  contents: read
32
- pull-requests: write
33
- statuses: write
34
85
 
35
86
  concurrency:
36
- group: preview-${{ github.repository }}-${{ github.event.client_payload.ref || github.head_ref || github.ref_name }}
87
+ group: preview-${{ inputs.site_owner }}-${{ inputs.site_name }}-${{ inputs.alias }}
37
88
  cancel-in-progress: true
38
89
 
39
90
  jobs:
40
91
  preview:
41
92
  runs-on: ubuntu-latest
42
- environment: production
43
93
  steps:
44
- - name: Resolve ref
45
- id: resolve
94
+ - name: Checkout deco-start (template + scripts)
95
+ uses: actions/checkout@v4
96
+
97
+ - name: Validate alias format
98
+ env:
99
+ ALIAS: ${{ inputs.alias }}
46
100
  run: |
47
- if [ "${{ github.event_name }}" = "repository_dispatch" ]; then
48
- echo "ref=${{ github.event.client_payload.ref }}" >> "$GITHUB_OUTPUT"
49
- else
50
- echo "ref=${{ github.head_ref || github.ref_name }}" >> "$GITHUB_OUTPUT"
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
51
105
  fi
52
106
 
53
- - uses: actions/checkout@v4
107
+ - name: Mint App token for storefront checkout
108
+ id: app-token
109
+ uses: actions/create-github-app-token@v1
54
110
  with:
55
- ref: ${{ steps.resolve.outputs.ref }}
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 }}
56
115
 
57
- - name: Resolve deco-start ref + site identity
58
- id: meta
59
- run: |
60
- WF_REF="${{ github.workflow_ref }}"
61
- REF="${WF_REF##*@}"
62
- REF="${REF#refs/tags/}"
63
- REF="${REF#refs/heads/}"
64
- echo "deco_start_ref=$REF" >> "$GITHUB_OUTPUT"
65
- echo "site_name=${GITHUB_REPOSITORY#*/}" >> "$GITHUB_OUTPUT"
66
-
67
- - name: Checkout deco-start registry at the same ref
116
+ - name: Checkout storefront at requested sha
68
117
  uses: actions/checkout@v4
69
118
  with:
70
- repository: decocms/deco-start
71
- ref: ${{ steps.meta.outputs.deco_start_ref }}
72
- path: .deco-start
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
73
124
 
74
125
  - uses: actions/setup-node@v4
75
126
  with:
76
127
  node-version: 22
77
128
 
78
- - name: Compute preview alias
79
- id: alias
80
- run: |
81
- REF="${{ steps.resolve.outputs.ref }}"
82
- if echo "$REF" | grep -q '^env/'; then
83
- ALIAS=$(echo "$REF" | sed 's|^env/||')
84
- elif [ "${{ github.event_name }}" = "pull_request" ]; then
85
- ALIAS="pr-${{ github.event.pull_request.number }}"
86
- else
87
- ALIAS=$(echo "$REF" | sed 's|[^a-z0-9-]|-|g')
88
- fi
89
- echo "alias=$ALIAS" >> "$GITHUB_OUTPUT"
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 }}-
90
135
 
91
136
  - name: Install dependencies
92
- run: npm install
137
+ working-directory: site
138
+ run: |
139
+ if [ ! -f package-lock.json ]; then
140
+ npm install --package-lock-only
141
+ fi
142
+ npm ci
93
143
 
94
144
  - name: Build
145
+ working-directory: site
95
146
  run: npm run build
96
147
 
97
- - name: Resolve site manifest
98
- id: site
99
- run: node .deco-start/scripts/deploy/resolve-site.mjs
148
+ - name: Generate wrangler.jsonc from template
149
+ working-directory: site
100
150
  env:
101
- DECO_START_PATH: .deco-start
102
- SITE_NAME: ${{ steps.meta.outputs.site_name }}
103
-
104
- - name: Generate wrangler.jsonc
105
- run: node .deco-start/scripts/deploy/build-wrangler-config.mjs
106
- env:
107
- DECO_START_PATH: .deco-start
108
- SITE_NAME: ${{ steps.meta.outputs.site_name }}
109
- OUTPUT_PATH: ./wrangler.jsonc
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"
110
155
 
111
156
  - name: Upload preview version
112
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 }}
113
163
  run: |
114
164
  set +e
115
- OUTPUT=$(npx wrangler versions upload --preview-alias ${{ steps.alias.outputs.alias }} 2>&1)
165
+ OUTPUT=$(npx wrangler versions upload --preview-alias "$ALIAS" 2>&1)
116
166
  EXIT_CODE=$?
117
167
  set -e
118
168
  echo "$OUTPUT"
@@ -124,19 +174,27 @@ jobs:
124
174
  ALIAS_URL=$(echo "$OUTPUT" | grep 'Version Preview Alias URL:' | sed 's/.*Version Preview Alias URL: //')
125
175
  echo "preview_url=${PREVIEW_URL}" >> "$GITHUB_OUTPUT"
126
176
  echo "alias_url=${ALIAS_URL}" >> "$GITHUB_OUTPUT"
127
- env:
128
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
129
- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
130
177
 
131
- - name: Comment preview URL on PR
132
- if: github.event_name == 'pull_request'
133
- uses: marocchino/sticky-pull-request-comment@v2
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
134
182
  with:
135
- header: preview-url
136
- message: |
137
- ### Preview deployed
138
-
139
- | | URL |
140
- |---|---|
141
- | **Version** | ${{ steps.deploy.outputs.preview_url }} |
142
- | **Alias** | ${{ steps.deploy.outputs.alias_url }} |
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"
@@ -1,102 +1,96 @@
1
1
  name: sync-secrets (central)
2
2
 
3
- # Reusable workflow. Reconciles the caller's `SECRET_*` GitHub repo secrets
4
- # with the Cloudflare Worker's runtime secrets (with the prefix stripped).
3
+ # Reusable workflow. Reconciles the per-storefront `SECRET_*` values stored in
4
+ # this repo's `<site_name>-secrets` GitHub Environment with the Cloudflare
5
+ # Worker's runtime secrets (with the `SECRET_` prefix stripped).
5
6
  #
6
7
  # Examples:
7
- # SECRET_STRIPE_KEY in GitHub -> STRIPE_KEY on the worker
8
+ # SECRET_STRIPE_KEY in deco-start env -> STRIPE_KEY on the worker
8
9
  #
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
10
+ # v3 architecture (D6.2 / S1): SECRET_* values live in deco-start environments
11
+ # named `<site_name>-secrets` (e.g. `baggagio-tanstack-secrets`). Site teams
12
+ # get per-environment edit/approve permissions via GitHub Environment
13
+ # protection rules, enforced by GitHub itself. CF credentials never leave
14
+ # decocms/deco-start, AND site secrets never leave decocms/deco-start either.
15
+ # The storefront repo holds zero credentials.
16
+ #
17
+ # Defaults to dry-run; the caller must pass `mode=apply` to actually write.
18
+ # Orphans on the worker (present on worker but not in the env as SECRET_*) are
11
19
  # warned about, never deleted.
12
20
  #
13
- # Caller usage (in customer repo, `.github/workflows/sync-secrets.yml`):
21
+ # Caller usage (in the storefront repo, `.github/workflows/sync-secrets.yml`):
14
22
  #
15
23
  # on:
16
24
  # workflow_dispatch:
17
25
  # inputs:
18
26
  # mode:
19
- # description: "dry-run | apply"
20
- # required: true
21
- # default: "dry-run"
22
27
  # type: choice
23
28
  # options: [dry-run, apply]
29
+ # default: dry-run
30
+ # permissions:
31
+ # contents: read
24
32
  # 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).
33
+ # trigger:
34
+ # runs-on: ubuntu-latest
35
+ # steps:
36
+ # - uses: actions/create-github-app-token@v1
37
+ # id: app-token
38
+ # with:
39
+ # app-id: ${{ secrets.DECOCMS_DEPLOYER_APP_ID }}
40
+ # private-key: ${{ secrets.DECOCMS_DEPLOYER_APP_PRIVATE_KEY }}
41
+ # owner: decocms
42
+ # repositories: deco-start
43
+ # - env:
44
+ # GH_TOKEN: ${{ steps.app-token.outputs.token }}
45
+ # run: |
46
+ # gh workflow run sync-secrets.yml \
47
+ # --repo decocms/deco-start \
48
+ # --ref v3 \
49
+ # -f site_name=${GITHUB_REPOSITORY##*/} \
50
+ # -f mode=${{ inputs.mode }}
41
51
 
42
52
  on:
43
- workflow_call:
53
+ workflow_dispatch:
44
54
  inputs:
55
+ site_name:
56
+ description: "Storefront repo basename. Becomes the Cloudflare worker name AND the environment name (`<site_name>-secrets`)."
57
+ type: string
58
+ required: true
45
59
  mode:
46
60
  description: "dry-run = print diff only | apply = set secrets on worker"
47
- required: false
48
61
  type: string
62
+ required: false
49
63
  default: "dry-run"
50
64
 
51
65
  permissions:
52
66
  contents: read
53
67
 
54
68
  concurrency:
55
- group: sync-secrets-${{ github.repository }}
69
+ group: sync-secrets-${{ inputs.site_name }}
56
70
  cancel-in-progress: false
57
71
 
58
72
  jobs:
59
73
  sync:
60
74
  runs-on: ubuntu-latest
61
- environment: production
75
+ # Dynamic per-storefront environment. Site teams get edit/approve
76
+ # permissions on their own environment via GitHub Environment protection
77
+ # rules (set up in this repo's Settings -> Environments).
78
+ environment: ${{ inputs.site_name }}-secrets
62
79
  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
80
+ - name: Checkout deco-start (template + scripts)
76
81
  uses: actions/checkout@v4
77
- with:
78
- repository: decocms/deco-start
79
- ref: ${{ steps.meta.outputs.deco_start_ref }}
80
- path: .deco-start
82
+
83
+ - name: Generate wrangler.jsonc (so wrangler knows which worker to target)
84
+ env:
85
+ DECO_START_PATH: "."
86
+ WORKER_NAME: ${{ inputs.site_name }}
87
+ OUTPUT_PATH: "./wrangler.jsonc"
88
+ run: node scripts/deploy/build-wrangler-config.mjs
81
89
 
82
90
  - uses: actions/setup-node@v4
83
91
  with:
84
92
  node-version: 22
85
93
 
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
94
  - name: Install wrangler
101
95
  run: npm install --no-save wrangler@4
102
96
 
@@ -104,12 +98,13 @@ jobs:
104
98
  env:
105
99
  CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
106
100
  CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
101
+ # ALL_SECRETS includes both repo secrets (CLOUDFLARE_*) AND
102
+ # environment secrets (SECRET_*) because we're bound to the env.
107
103
  ALL_SECRETS: ${{ toJSON(secrets) }}
108
104
  MODE: ${{ inputs.mode }}
109
105
  run: |
110
106
  set -euo pipefail
111
107
 
112
- # Build desired-state map from SECRET_* entries.
113
108
  desired_json=$(printf '%s' "$ALL_SECRETS" | jq '
114
109
  to_entries
115
110
  | map(select(.key | startswith("SECRET_")))
@@ -119,7 +114,6 @@ jobs:
119
114
 
120
115
  desired_names=$(printf '%s' "$desired_json" | jq -r 'keys[]' | sort)
121
116
 
122
- # Validate names before any side effect.
123
117
  while IFS= read -r name; do
124
118
  [ -z "$name" ] && continue
125
119
  if ! [[ "$name" =~ ^[A-Z][A-Z0-9_]{0,63}$ ]]; then
@@ -128,24 +122,22 @@ jobs:
128
122
  fi
129
123
  done <<< "$desired_names"
130
124
 
131
- # Snapshot what's currently on the worker.
132
125
  existing=$(npx wrangler secret list --format=json | jq -r '.[].name' | sort)
133
126
 
134
- # Diff
135
127
  to_set="$desired_names"
136
128
  orphans=$(comm -23 <(printf '%s\n' "$existing") <(printf '%s\n' "$desired_names") || true)
137
129
 
138
130
  {
139
- echo "## Diff"
131
+ echo "## Diff for ${{ inputs.site_name }}"
140
132
  echo ""
141
- echo "### Will set (from SECRET_* in GitHub):"
133
+ echo "### Will set (from SECRET_* in \`${{ inputs.site_name }}-secrets\` environment):"
142
134
  if [ -z "$to_set" ]; then
143
- echo " (none)"
135
+ echo " (none -- environment has no SECRET_* values)"
144
136
  else
145
137
  echo "$to_set" | sed 's/^/ + /'
146
138
  fi
147
139
  echo ""
148
- echo "### Orphans (on worker, not in GitHub as SECRET_*):"
140
+ echo "### Orphans (on worker, not in env as SECRET_*):"
149
141
  if [ -z "$orphans" ]; then
150
142
  echo " (none)"
151
143
  else
@@ -157,7 +149,7 @@ jobs:
157
149
  } | tee -a "$GITHUB_STEP_SUMMARY"
158
150
 
159
151
  if [ -n "$orphans" ]; then
160
- echo "::warning::${orphans//$'\n'/, } exist on the worker but not in GitHub as SECRET_*. Not deleting."
152
+ echo "::warning::${orphans//$'\n'/, } exist on the worker but not in env as SECRET_*. Not deleting."
161
153
  fi
162
154
 
163
155
  if [ "$MODE" = "dry-run" ]; then
@@ -165,7 +157,6 @@ jobs:
165
157
  exit 0
166
158
  fi
167
159
 
168
- # Apply
169
160
  if [ -z "$to_set" ]; then
170
161
  echo "Nothing to apply."
171
162
  exit 0
package/CODEOWNERS CHANGED
@@ -1,14 +1,12 @@
1
1
  # CODEOWNERS for decocms/deco-start
2
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.
3
+ # `deploy/wrangler-template.jsonc` is the canonical wrangler config every
4
+ # storefront inherits. A bad PR here can change every site's runtime config in
5
+ # one shot. Only the platform team approves changes.
8
6
  #
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
7
+ # The central reusable workflows under `.github/workflows/` are in the same
8
+ # trust boundary: they decide how every site is built and deployed.
9
+ deploy/ @vibe-dex
12
10
  .github/workflows/deploy.yml @vibe-dex
13
11
  .github/workflows/preview.yml @vibe-dex
14
12
  .github/workflows/sync-secrets.yml @vibe-dex
@@ -118,7 +118,8 @@ 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
+ | 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*. First-pass refinement: the central `deploy.yml` / `preview.yml` / `sync-secrets.yml` jobs declared `environment: production` to try to make `${{ secrets.CLOUDFLARE_* }}` resolve from `decocms/deco-start`'s `production` Environment. **Found broken empirically on 2026-05-07** the deployment registers in the *caller* repo, not the called workflow's repo, so the environment lookup uses the caller's `production` env (auto-created with no secrets). Superseded by D6.2 the same evening. |
122
+ | 2026-05-07 | **D6.2 — App-mediated dispatch + no per-site registry (supersedes D6 + D6.1)** | After D6.1's `environment:` mechanism was empirically shown not to work cross-repo, the architecture pivoted: a `decocms-deployer` GitHub App is installed on `decocms/deco-start` (`actions:write`) and on each storefront repo (`contents:read`, optionally `pull-requests:write`). The storefront caller stub mints a short-lived App-installation token and calls `gh workflow run deploy.yml --repo decocms/deco-start --ref v3 -f site_owner=… -f site_name=…`. The central workflow runs in `decocms/deco-start`'s context, so `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` are ordinary repo secrets. For runtime `SECRET_*` values, each storefront has a `<site_name>-secrets` GitHub Environment in `decocms/deco-start` (S1 design); `sync-secrets.yml` binds to that environment and pushes to `wrangler secret put`. The per-site registry under `deploy/sites/<repo>.jsonc` was dropped entirely (Pure C): worker name = repo basename by convention; the App being installed on the storefront repo is the deploy authorization gate; rare per-worker derived fields (like AE dataset name) use `$WORKER_*` substitution tokens in the template. Force-rollback is impossible for production deploys because the central workflow ignores caller-supplied `site_sha` and resolves the storefront's current default-branch HEAD itself. See [`deploy/README.md`](./deploy/README.md) for the full trust model. **Operational migrations required by Pure C:** `miess-01-tanstack` repo's worker shifts from `miess-tanstack` to `miess-01-tanstack` (CF-side cutover); `lebiscuit-tanstack` AE dataset shifts from `deco_metrics_lebiscuit` to `deco_metrics_lebiscuit_tanstack` (orphans old data). |
122
123
 
123
124
  The full text of the constitutional rule (loaded into every agent
124
125
  session for this repo) lives at
@@ -1677,15 +1678,19 @@ props. One broken section never takes the page down.
1677
1678
  `regen-blocks.yml` and its `wrangler.jsonc` lacked `account_id`,
1678
1679
  lebiscuit's preview workflow swallowed `wrangler` exit codes, and
1679
1680
  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
1681
+ match its `wrangler.jsonc`. **Resolved 2026-05-07 via D6 → D6.1 →
1682
+ D6.2:** all workflows + wrangler config are centralized in
1682
1683
  [`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.
1684
+ [`deco-start/deploy/`](./deploy/), and storefront caller stubs use
1685
+ a `decocms-deployer` GitHub App to trigger them via `workflow_dispatch`
1686
+ with no Cloudflare credentials in the storefront repo. New-site
1687
+ onboarding is now: install the `decocms-deployer` App on the new
1688
+ storefront repo, drop the ~15-line caller workflows in, push to main.
1689
+ No `deploy/sites/<repo>.jsonc` PR is needed worker name is the
1690
+ repo basename by convention. No `wrangler.jsonc` is committed to the
1691
+ site repo — `deco-wrangler gen` (a `bin` shipped from `@decocms/start`)
1692
+ materializes it from the central template on demand for local dev
1693
+ and CI alike.
1689
1694
 
1690
1695
  #### Counter-evidence the user-rule asks for
1691
1696