@decocms/start 2.30.0 → 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,119 +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 || '' }}
53
+ #
54
+ # Note on forks: pull_request runs from forked repos cannot access repo
55
+ # secrets (incl. DECOCMS_DEPLOYER_APP_*), so they cannot trigger previews.
23
56
 
24
57
  on:
25
- workflow_call:
26
- secrets:
27
- CLOUDFLARE_API_TOKEN:
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
28
72
  required: true
29
- CLOUDFLARE_ACCOUNT_ID:
73
+ alias:
74
+ description: "Preview alias name (e.g. pr-123, feature-foo). Must match wrangler alias rules."
75
+ type: string
30
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: ""
31
82
 
32
83
  permissions:
33
84
  contents: read
34
- pull-requests: write
35
- statuses: write
36
85
 
37
86
  concurrency:
38
- 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 }}
39
88
  cancel-in-progress: true
40
89
 
41
90
  jobs:
42
91
  preview:
43
92
  runs-on: ubuntu-latest
44
93
  steps:
45
- - name: Resolve ref
46
- 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 }}
47
100
  run: |
48
- if [ "${{ github.event_name }}" = "repository_dispatch" ]; then
49
- echo "ref=${{ github.event.client_payload.ref }}" >> "$GITHUB_OUTPUT"
50
- else
51
- 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
52
105
  fi
53
106
 
54
- - uses: actions/checkout@v4
107
+ - name: Mint App token for storefront checkout
108
+ id: app-token
109
+ uses: actions/create-github-app-token@v1
55
110
  with:
56
- 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 }}
57
115
 
58
- - name: Resolve deco-start ref + site identity
59
- id: meta
60
- run: |
61
- WF_REF="${{ github.workflow_ref }}"
62
- REF="${WF_REF##*@}"
63
- REF="${REF#refs/tags/}"
64
- REF="${REF#refs/heads/}"
65
- echo "deco_start_ref=$REF" >> "$GITHUB_OUTPUT"
66
- echo "site_name=${GITHUB_REPOSITORY#*/}" >> "$GITHUB_OUTPUT"
67
-
68
- - name: Checkout deco-start registry at the same ref
116
+ - name: Checkout storefront at requested sha
69
117
  uses: actions/checkout@v4
70
118
  with:
71
- repository: decocms/deco-start
72
- ref: ${{ steps.meta.outputs.deco_start_ref }}
73
- 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
74
124
 
75
125
  - uses: actions/setup-node@v4
76
126
  with:
77
127
  node-version: 22
78
128
 
79
- - name: Compute preview alias
80
- id: alias
81
- run: |
82
- REF="${{ steps.resolve.outputs.ref }}"
83
- if echo "$REF" | grep -q '^env/'; then
84
- ALIAS=$(echo "$REF" | sed 's|^env/||')
85
- elif [ "${{ github.event_name }}" = "pull_request" ]; then
86
- ALIAS="pr-${{ github.event.pull_request.number }}"
87
- else
88
- ALIAS=$(echo "$REF" | sed 's|[^a-z0-9-]|-|g')
89
- fi
90
- 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 }}-
91
135
 
92
136
  - name: Install dependencies
93
- 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
94
143
 
95
144
  - name: Build
145
+ working-directory: site
96
146
  run: npm run build
97
147
 
98
- - name: Resolve site manifest
99
- id: site
100
- run: node .deco-start/scripts/deploy/resolve-site.mjs
101
- env:
102
- DECO_START_PATH: .deco-start
103
- SITE_NAME: ${{ steps.meta.outputs.site_name }}
104
-
105
- - name: Generate wrangler.jsonc
106
- run: node .deco-start/scripts/deploy/build-wrangler-config.mjs
148
+ - name: Generate wrangler.jsonc from template
149
+ working-directory: site
107
150
  env:
108
- DECO_START_PATH: .deco-start
109
- SITE_NAME: ${{ steps.meta.outputs.site_name }}
110
- 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"
111
155
 
112
156
  - name: Upload preview version
113
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 }}
114
163
  run: |
115
164
  set +e
116
- OUTPUT=$(npx wrangler versions upload --preview-alias ${{ steps.alias.outputs.alias }} 2>&1)
165
+ OUTPUT=$(npx wrangler versions upload --preview-alias "$ALIAS" 2>&1)
117
166
  EXIT_CODE=$?
118
167
  set -e
119
168
  echo "$OUTPUT"
@@ -125,19 +174,27 @@ jobs:
125
174
  ALIAS_URL=$(echo "$OUTPUT" | grep 'Version Preview Alias URL:' | sed 's/.*Version Preview Alias URL: //')
126
175
  echo "preview_url=${PREVIEW_URL}" >> "$GITHUB_OUTPUT"
127
176
  echo "alias_url=${ALIAS_URL}" >> "$GITHUB_OUTPUT"
128
- env:
129
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
130
- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
131
177
 
132
- - name: Comment preview URL on PR
133
- if: github.event_name == 'pull_request'
134
- 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
135
182
  with:
136
- header: preview-url
137
- message: |
138
- ### Preview deployed
139
-
140
- | | URL |
141
- |---|---|
142
- | **Version** | ${{ steps.deploy.outputs.preview_url }} |
143
- | **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,95 +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
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 }}
30
51
 
31
52
  on:
32
- workflow_call:
53
+ workflow_dispatch:
33
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
34
59
  mode:
35
60
  description: "dry-run = print diff only | apply = set secrets on worker"
36
- required: false
37
61
  type: string
62
+ required: false
38
63
  default: "dry-run"
39
- secrets:
40
- CLOUDFLARE_API_TOKEN:
41
- required: true
42
- CLOUDFLARE_ACCOUNT_ID:
43
- required: true
44
64
 
45
65
  permissions:
46
66
  contents: read
47
67
 
48
68
  concurrency:
49
- group: sync-secrets-${{ github.repository }}
69
+ group: sync-secrets-${{ inputs.site_name }}
50
70
  cancel-in-progress: false
51
71
 
52
72
  jobs:
53
73
  sync:
54
74
  runs-on: ubuntu-latest
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
55
79
  steps:
56
- - uses: actions/checkout@v4
57
-
58
- - name: Resolve deco-start ref + site identity
59
- id: meta
60
- run: |
61
- WF_REF="${{ github.workflow_ref }}"
62
- REF="${WF_REF##*@}"
63
- REF="${REF#refs/tags/}"
64
- REF="${REF#refs/heads/}"
65
- echo "deco_start_ref=$REF" >> "$GITHUB_OUTPUT"
66
- echo "site_name=${GITHUB_REPOSITORY#*/}" >> "$GITHUB_OUTPUT"
67
-
68
- - name: Checkout deco-start registry at the same ref
80
+ - name: Checkout deco-start (template + scripts)
69
81
  uses: actions/checkout@v4
70
- with:
71
- repository: decocms/deco-start
72
- ref: ${{ steps.meta.outputs.deco_start_ref }}
73
- 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
74
89
 
75
90
  - uses: actions/setup-node@v4
76
91
  with:
77
92
  node-version: 22
78
93
 
79
- - name: Resolve site manifest
80
- id: site
81
- run: node .deco-start/scripts/deploy/resolve-site.mjs
82
- env:
83
- DECO_START_PATH: .deco-start
84
- SITE_NAME: ${{ steps.meta.outputs.site_name }}
85
-
86
- - name: Generate wrangler.jsonc
87
- run: node .deco-start/scripts/deploy/build-wrangler-config.mjs
88
- env:
89
- DECO_START_PATH: .deco-start
90
- SITE_NAME: ${{ steps.meta.outputs.site_name }}
91
- OUTPUT_PATH: ./wrangler.jsonc
92
-
93
94
  - name: Install wrangler
94
95
  run: npm install --no-save wrangler@4
95
96
 
@@ -97,12 +98,13 @@ jobs:
97
98
  env:
98
99
  CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
99
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.
100
103
  ALL_SECRETS: ${{ toJSON(secrets) }}
101
104
  MODE: ${{ inputs.mode }}
102
105
  run: |
103
106
  set -euo pipefail
104
107
 
105
- # Build desired-state map from SECRET_* entries.
106
108
  desired_json=$(printf '%s' "$ALL_SECRETS" | jq '
107
109
  to_entries
108
110
  | map(select(.key | startswith("SECRET_")))
@@ -112,7 +114,6 @@ jobs:
112
114
 
113
115
  desired_names=$(printf '%s' "$desired_json" | jq -r 'keys[]' | sort)
114
116
 
115
- # Validate names before any side effect.
116
117
  while IFS= read -r name; do
117
118
  [ -z "$name" ] && continue
118
119
  if ! [[ "$name" =~ ^[A-Z][A-Z0-9_]{0,63}$ ]]; then
@@ -121,24 +122,22 @@ jobs:
121
122
  fi
122
123
  done <<< "$desired_names"
123
124
 
124
- # Snapshot what's currently on the worker.
125
125
  existing=$(npx wrangler secret list --format=json | jq -r '.[].name' | sort)
126
126
 
127
- # Diff
128
127
  to_set="$desired_names"
129
128
  orphans=$(comm -23 <(printf '%s\n' "$existing") <(printf '%s\n' "$desired_names") || true)
130
129
 
131
130
  {
132
- echo "## Diff"
131
+ echo "## Diff for ${{ inputs.site_name }}"
133
132
  echo ""
134
- echo "### Will set (from SECRET_* in GitHub):"
133
+ echo "### Will set (from SECRET_* in \`${{ inputs.site_name }}-secrets\` environment):"
135
134
  if [ -z "$to_set" ]; then
136
- echo " (none)"
135
+ echo " (none -- environment has no SECRET_* values)"
137
136
  else
138
137
  echo "$to_set" | sed 's/^/ + /'
139
138
  fi
140
139
  echo ""
141
- echo "### Orphans (on worker, not in GitHub as SECRET_*):"
140
+ echo "### Orphans (on worker, not in env as SECRET_*):"
142
141
  if [ -z "$orphans" ]; then
143
142
  echo " (none)"
144
143
  else
@@ -150,7 +149,7 @@ jobs:
150
149
  } | tee -a "$GITHUB_STEP_SUMMARY"
151
150
 
152
151
  if [ -n "$orphans" ]; then
153
- 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."
154
153
  fi
155
154
 
156
155
  if [ "$MODE" = "dry-run" ]; then
@@ -158,7 +157,6 @@ jobs:
158
157
  exit 0
159
158
  fi
160
159
 
161
- # Apply
162
160
  if [ -z "$to_set" ]; then
163
161
  echo "Nothing to apply."
164
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,6 +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*. 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). |
121
123
 
122
124
  The full text of the constitutional rule (loaded into every agent
123
125
  session for this repo) lives at
@@ -1676,15 +1678,19 @@ props. One broken section never takes the page down.
1676
1678
  `regen-blocks.yml` and its `wrangler.jsonc` lacked `account_id`,
1677
1679
  lebiscuit's preview workflow swallowed `wrangler` exit codes, and
1678
1680
  casaevideo's `loadtest:tail` referenced a worker name that didn't
1679
- match its `wrangler.jsonc`. **Resolved 2026-05-07 via D6:** all
1680
- 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
1681
1683
  [`deco-start/.github/workflows/`](./.github/workflows/) and
1682
- [`deco-start/deploy/`](./deploy/). New-site onboarding is now: open a
1683
- PR adding `deploy/sites/<repo>.jsonc` to deco-start, then drop the
1684
- ~5-line caller workflows into the new site's repo. No `wrangler.jsonc`
1685
- is committed to the site repo `deco-wrangler gen`
1686
- (a `bin` shipped from `@decocms/start`) materializes it from the
1687
- 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.
1688
1694
 
1689
1695
  #### Counter-evidence the user-rule asks for
1690
1696